This is an automated email from the ASF dual-hosted git repository. xiazcy pushed a commit to branch type-enum-poc in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 01c2d78ec1939750e5c2a7fc711217a2f1a823f6 Author: xiazcy <xia...@gmail.com> AuthorDate: Mon Jul 7 17:55:22 2025 -0400 Minor tweak for string parsing and updated relevant docs --- CHANGELOG.asciidoc | 1 + docs/src/reference/the-traversal.asciidoc | 31 ++++++++++++++ docs/src/upgrade/release-3.8.x.asciidoc | 47 ++++++++++++++++++++++ .../process/traversal/step/map/AsNumberStep.java | 6 +-- .../traversal/step/map/AsNumberStepTest.java | 39 ++++++++++++++++++ .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 2 +- gremlin-go/driver/cucumber/gremlin.go | 2 +- .../gremlin-javascript/test/cucumber/gremlin.js | 2 +- gremlin-python/src/main/python/radish/gremlin.py | 2 +- .../gremlin/test/features/map/AsNumber.feature | 8 ++-- 10 files changed, 127 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 36d0d90344..040f96ca9b 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -75,6 +75,7 @@ This release also includes changes from <<release-3-7-XXX, 3.7.XXX>>. * Moved all lambda oriented Gremlin tests to `LambdaStepTest` in the Java test suite. * Removed the `@RemoteOnly` testing tag in Gherkin as lambda tests have all been moved to the Java test suite. * Updated gremlin-javascript to use GraphBinary as default instead of GraphSONv3 +* Added the `asNumber()` steps to perform number conversion. * Renamed many types in the grammar for consistent use of terms "Literal", "Argument", and "Varargs" == TinkerPop 3.7.0 (Gremfir Master of the Pan Flute) diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index eaf36d2b96..e39425881c 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -829,6 +829,37 @@ g.inject(datetime("2023-08-24T00:00:00Z")).asDate() <3> link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#asDate()++[`asDate()`] +[[asNumber-step]] +=== AsNumber Step + +The `asNumber()`-step (*map*) converts the incoming traverser to the nearest parsable type if no argument is provided, or to the desired numerical type, based on the number token (N) provided. + +Numerical input will pass through unless a type is specified by the number token, with the exception that any float number will be converted into double. `ArithmeticException` will be thrown for any overflow during narrowing of types. + +String input will be parsed. By default, the smalled unit of number to be parsed into is `int` if no number token is provided. `NumberFormatException` will be thrown for any unparsable strings. + +All other input types will result in `IllegalArgumentException`. + +[gremlin-groovy,modern] +---- +g.inject(1).asNumber() <1> +g.inject(1.76).asNumber() <2> +g.inject(1.76).asNumber(N.nint) <3> +g.inject("1b").asNumber() <4> +g.inject(33550336).asNumber(N.nbyte) <5> +---- + +<1> An int will be passed through. +<2> A double will be passed through. +<3> A double is converted into an in. +<4> String containing any character other than numerical ones will result in `NumberFormatException`. +<5> Narrowing of int to byte that overflows will throw `ArithmeticException`. + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#asNumber()++[`asNumber()`] +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#asNumber(org.apache.tinkerpop.gremlin.process.traversal.N)++[`asNumber(N)`] + [[barrier-step]] === Barrier Step diff --git a/docs/src/upgrade/release-3.8.x.asciidoc b/docs/src/upgrade/release-3.8.x.asciidoc index c1de647afc..50dea6654b 100644 --- a/docs/src/upgrade/release-3.8.x.asciidoc +++ b/docs/src/upgrade/release-3.8.x.asciidoc @@ -30,6 +30,53 @@ complete list of all the modifications that are part of this release. === Upgrading for Users +==== Number Conversion Step + +We have introduced a number conversion step, `asNumber()`, which converts the incoming traverser to the nearest parsable type if no argument is provided, or to the desired numerical type, based on the number token (`N`) provided. + +Numerical input will pass through unless a type is specified by the number token, with the exception that any float number will be converted into double. `ArithmeticException` will be thrown for any overflow during narrowing of types: + +[source,text] +---- +gremlin> g.inject(5).asNumber() +==> 5 // parses to int +gremlin> g.inject(5.0).asNumber() +==> 5 // parses to double +gremlin> g.inject(5.123f).asNumber() +==> 5.123 // will cast float to double +// Narrowing of types may result in ArithmeticException due to overflow +gremlin> g.inject(12).asNumber(N.byte) +==> 12 +gremlin> g.inject(128).asNumber(N.byte) +==> ArithmeticException +gremlin> g.inject(300).asNumber(N.byte) +==> ArithmeticException +---- + +String input will be parsed. By default, the smalled unit of number to be parsed into is `int` if no number token is provided. `NumberFormatException` will be thrown for any unparsable strings: + +[source,text] +---- +gremlin> g.inject("5").asNumber() +==> 5 +gremlin> g.inject("5.7").asNumber(N.int) +==> 5 +gremlin> g.inject("1,000").asNumber(N.nint) +==> NumberFormatException +gremlin> g.inject("128").asNumber(N.nbyte) +==> ArithmeticException +---- + +All other input types will result in `IllegalArgumentException`: +[source,text] +---- +gremlin> g.inject([1, 2, 3, 4]).asNumber() +==> IllegalArgumentException +---- + +See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asNumber-step[asNumber()-step] +See: link:https://issues.apache.org/jira/browse/TINKERPOP-3166[TINKERPOP-3166] + ==== Auto promotion of number types Previously, operations like sum or sack that involved mathematical calculations did not automatically promote the result diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStep.java index 1fe1a52051..1fb093be11 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStep.java @@ -106,11 +106,7 @@ public final class AsNumberStep<S> extends ScalarMapStep<S, Number> { return result; } BigInteger result = new BigInteger(value.trim()); - if (result.bitLength() <= 7) { - return result.byteValue(); - } else if (result.bitLength() <= 15) { - return result.shortValue(); - } else if (result.bitLength() <= 31) { + if (result.bitLength() <= 31) { // default to int if not specified, smaller sizes need to be intentionally set return result.intValue(); } else if (result.bitLength() <= 63) { return result.longValue(); diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStepTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStepTest.java index a812aac827..396d4bd8e4 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStepTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStepTest.java @@ -18,6 +18,7 @@ */ package org.apache.tinkerpop.gremlin.process.traversal.step.map; +import org.apache.tinkerpop.gremlin.process.traversal.N; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.process.traversal.step.StepTest; @@ -26,6 +27,7 @@ import org.junit.Test; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -40,6 +42,43 @@ public class AsNumberStepTest extends StepTest { @Test public void testReturnTypes() { assertEquals(1, __.__(1).asNumber().next()); + assertEquals((byte) 1, __.__(1).asNumber(N.nbyte).next()); + assertEquals(1, __.__(1.8).asNumber(N.nint).next()); + assertEquals(1, __.__(1L).asNumber(N.nint).next()); + assertEquals(1L, __.__(1L).asNumber().next()); + assertEquals(1, __.__("1").asNumber(N.nint).next()); + assertEquals(1, __.__("1").asNumber().next()); + assertEquals((byte) 1, __.__("1").asNumber(N.nbyte).next()); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenInvalidStringInput() { + __.__("This String is not a number").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowParseExceptionWithInvalidNumberStringInput() { + __.__("128abc").asNumber().next(); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenArrayInput() { + __.__(Arrays.asList(1, 2)).asNumber().next(); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenUUIDInput() { + __.__(UUID.randomUUID()).asNumber().next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenParsedNumberOverflows() { + __.__("128").asNumber(N.nbyte).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenCastNumberOverflows() { + __.__(128).asNumber(N.nbyte).next(); } } diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 25ba75311e..e624af80f5 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -636,7 +636,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin {"g_injectX32768X_asNumberXN_nshortX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(32768).AsNumber(N.Nshort)}}, {"g_injectX300X_asNumberXN_nbyteX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(300).AsNumber(N.Nbyte)}}, {"g_injectX5X_asNumberXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("5").AsNumber()}}, - {"g_injectX5X_asNumberXN_nintX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("5").AsNumber(N.Nint)}}, + {"g_injectX5X_asNumberXN_byteX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("5").AsNumber(N.Nbyte)}}, {"g_injectX1_000X_asNumberXN_nintX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("1,000").AsNumber(N.Nint)}}, {"g_injectXtestX_asNumberXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("test").AsNumber()}}, {"g_injectX_1__2__3__4_X_asNumberXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(new List<object> { 1, 2, 3, 4 }).AsNumber()}}, diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go index 8612cdbf5e..be4614c490 100644 --- a/gremlin-go/driver/cucumber/gremlin.go +++ b/gremlin-go/driver/cucumber/gremlin.go @@ -606,7 +606,7 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_injectX32768X_asNumberXN_nshortX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(32768).AsNumber(gremlingo.N.Nshort)}}, "g_injectX300X_asNumberXN_nbyteX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(300).AsNumber(gremlingo.N.Nbyte)}}, "g_injectX5X_asNumberXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("5").AsNumber()}}, - "g_injectX5X_asNumberXN_nintX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("5").AsNumber(gremlingo.N.Nint)}}, + "g_injectX5X_asNumberXN_byteX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("5").AsNumber(gremlingo.N.Nbyte)}}, "g_injectX1_000X_asNumberXN_nintX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("1,000").AsNumber(gremlingo.N.Nint)}}, "g_injectXtestX_asNumberXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("test").AsNumber()}}, "g_injectX_1__2__3__4_X_asNumberXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject([]interface{}{1, 2, 3, 4}).AsNumber()}}, diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js index 5f75840cd2..eaf50c7474 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js @@ -637,7 +637,7 @@ const gremlins = { g_injectX32768X_asNumberXN_nshortX: [function({g}) { return g.inject(32768).asNumber(N.nshort) }], g_injectX300X_asNumberXN_nbyteX: [function({g}) { return g.inject(300).asNumber(N.nbyte) }], g_injectX5X_asNumberXX: [function({g}) { return g.inject("5").asNumber() }], - g_injectX5X_asNumberXN_nintX: [function({g}) { return g.inject("5").asNumber(N.nint) }], + g_injectX5X_asNumberXN_byteX: [function({g}) { return g.inject("5").asNumber(N.nbyte) }], g_injectX1_000X_asNumberXN_nintX: [function({g}) { return g.inject("1,000").asNumber(N.nint) }], g_injectXtestX_asNumberXX: [function({g}) { return g.inject("test").asNumber() }], g_injectX_1__2__3__4_X_asNumberXX: [function({g}) { return g.inject([1, 2, 3, 4]).asNumber() }], diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py index 8c2134f592..89731ece25 100644 --- a/gremlin-python/src/main/python/radish/gremlin.py +++ b/gremlin-python/src/main/python/radish/gremlin.py @@ -609,7 +609,7 @@ world.gremlins = { 'g_injectX32768X_asNumberXN_nshortX': [(lambda g:g.inject(32768).as_number(N.nshort))], 'g_injectX300X_asNumberXN_nbyteX': [(lambda g:g.inject(300).as_number(N.nbyte))], 'g_injectX5X_asNumberXX': [(lambda g:g.inject('5').as_number())], - 'g_injectX5X_asNumberXN_nintX': [(lambda g:g.inject('5').as_number(N.nint))], + 'g_injectX5X_asNumberXN_byteX': [(lambda g:g.inject('5').as_number(N.nbyte))], 'g_injectX1_000X_asNumberXN_nintX': [(lambda g:g.inject('1,000').as_number(N.nint))], 'g_injectXtestX_asNumberXX': [(lambda g:g.inject('test').as_number())], 'g_injectX_1__2__3__4_X_asNumberXX': [(lambda g:g.inject([1, 2, 3, 4]).as_number())], diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AsNumber.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AsNumber.feature index 868eb80274..2a5972a067 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AsNumber.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AsNumber.feature @@ -180,19 +180,19 @@ Feature: Step - asNumber() When iterated to list Then the result should be unordered | result | - | d[5].b | + | d[5].i | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectX5X_asNumberXN_nintX + Scenario: g_injectX5X_asNumberXN_byteX Given the empty graph And the traversal of """ - g.inject("5").asNumber(N.nint) + g.inject("5").asNumber(N.nbyte) """ When iterated to list Then the result should be unordered | result | - | d[5].i | + | d[5].b | @GraphComputerVerificationInjectionNotSupported Scenario: g_injectX1_000X_asNumberXN_nintX