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 7ef100a5630103fe101c51e2b0c3da4e7cf95b63 Author: xiazcy <[email protected]> AuthorDate: Thu Jul 17 10:35:17 2025 -0700 added comprehensive unit tests & updated asNumber() implementation --- docs/src/reference/the-traversal.asciidoc | 6 +- .../tinkerpop/gremlin/process/traversal/N.java | 29 +- .../traversal/dsl/graph/GraphTraversal.java | 8 +- .../process/traversal/step/map/AsNumberStep.java | 105 +------- .../gremlin/structure/io/gryo/GryoVersion.java | 17 +- .../tinkerpop/gremlin/util/NumberHelper.java | 75 ++++++ .../traversal/step/map/AsNumberStepTest.java | 291 ++++++++++++++++++++- .../tinkerpop/gremlin/util/NumberHelperTest.java | 75 ++++++ .../gremlin/test/features/map/AsNumber.feature | 2 +- 9 files changed, 476 insertions(+), 132 deletions(-) diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index e39425881c..39dd9f9c39 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -836,13 +836,13 @@ The `asNumber()`-step (*map*) converts the incoming traverser to the nearest par 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. +String inputs are parsed into numeric values. By default, the value will be parsed as an integer if it represents a whole number, or as a double if it contains a decimal point. A `NumberFormatException` will be thrown if the string cannot be parsed into a valid number format. All other input types will result in `IllegalArgumentException`. [gremlin-groovy,modern] ---- -g.inject(1).asNumber() <1> +g.inject(1234).asNumber() <1> g.inject(1.76).asNumber() <2> g.inject(1.76).asNumber(N.nint) <3> g.inject("1b").asNumber() <4> @@ -851,7 +851,7 @@ 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. +<3> A double is converted into an int. <4> String containing any character other than numerical ones will result in `NumberFormatException`. <5> Narrowing of int to byte that overflows will throw `ArithmeticException`. diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/N.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/N.java index 67eda53a7d..4a8d56b48b 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/N.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/N.java @@ -20,26 +20,33 @@ package org.apache.tinkerpop.gremlin.process.traversal; import org.apache.tinkerpop.gremlin.process.traversal.step.map.AsNumberStep; +import java.math.BigDecimal; +import java.math.BigInteger; + /** * Tokens that are used to denote different units of number. * Used with {@link AsNumberStep} step. */ public enum N { - nbyte("Byte"), - nshort("Short"), - nint("Integer"), - nlong("Long"), - nfloat("Float"), - ndouble("Double"), - nbigInt("BigInt"), - nbigDecimal("BigDecimal"),; + nbyte(Byte.class), + nshort(Short.class), + nint(Integer.class), + nlong(Long.class), + nfloat(Float.class), + ndouble(Double.class), + nbigInt(BigInteger.class), + nbigDecimal(BigDecimal.class),; + + private final Class<?> type; - private final String typeName; + N(Class<?> type) {this.type = type;} - N(String name) {typeName = name;} + public Class<?> getType() { + return this.type; + } @Override public String toString() { - return typeName; + return this.type.getSimpleName(); } } \ No newline at end of file diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java index 40cda64a36..aa3c2cfbc5 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java @@ -1918,11 +1918,11 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { } /** - * Parse value of the incoming traverser as an ISO-8601 {@link Number}. + * Parse value of the incoming traverser as a {@link Number}. * * @return the traversal with an appended {@link AsNumberStep}. * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#AsNumberStep-step" target="_blank">Reference Documentation - AsNumberStep Step</a> - * @since 3.7.1 + * @since 3.8.0 */ public default GraphTraversal<S, Number> asNumber() { this.asAdmin().getBytecode().addStep(Symbols.asNumber); @@ -1930,11 +1930,11 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { } /** - * Parse value of the incoming traverser as an ISO-8601 {@link Number}. + * Parse value of the incoming traverser as a {@link Number}. * * @return the traversal with an appended {@link AsNumberStep}. * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#AsNumberStep-step" target="_blank">Reference Documentation - AsNumberStep Step</a> - * @since 3.7.1 + * @since 3.8.0 */ public default GraphTraversal<S, Number> asNumber(final N numberToken) { this.asAdmin().getBytecode().addStep(Symbols.asNumber, numberToken); 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 1fb093be11..59e88ef604 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 @@ -26,19 +26,13 @@ import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequire import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.util.NumberHelper; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.Set; /** * Reference implementation for number parsing step. */ -public final class AsNumberStep<S> extends ScalarMapStep<S, Number> { +public class AsNumberStep<S> extends ScalarMapStep<S, Number> { private N numberToken; @@ -58,10 +52,10 @@ public final class AsNumberStep<S> extends ScalarMapStep<S, Number> { if (object instanceof String) { String numberText = (String) object; Number number = parseNumber(numberText); - return numberToken == null ? autoNumber(number) : castNumber(number, numberToken); + return numberToken == null ? number : castNumber(number, numberToken); } else if (object instanceof Number) { Number number = (Number) object; - return numberToken == null ? autoNumber(number) : castNumber(number, numberToken); + return numberToken == null ? number : castNumber(number, numberToken); } throw new IllegalArgumentException(String.format("Can't parse type %s as number.", object == null ? "null" : object.getClass().getSimpleName())); } @@ -96,97 +90,14 @@ public final class AsNumberStep<S> extends ScalarMapStep<S, Number> { } public static Number parseNumber(final String value) { - try { - boolean isFloatingPoint = value.contains(".") || value.contains("e") || value.contains("E"); - if (isFloatingPoint) { - BigDecimal result = new BigDecimal(value.trim()); - if (BigDecimal.valueOf(result.doubleValue()).compareTo(result) == 0) { - return result.doubleValue(); - } - return result; - } - BigInteger result = new BigInteger(value.trim()); - 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(); - } - return result; - } catch (NumberFormatException nfe) { - throw new NumberFormatException(String.format("Can't parse string '%s' as number.", value)); + if (NumberUtils.isCreatable(value.trim())) { + return NumberUtils.createNumber(value.trim()); } + throw new NumberFormatException(String.format("Can't parse string '%s' as number.", value)); } - private static Number autoNumber(Number number) { - final Class<? extends Number> clazz = number.getClass(); - if (clazz.equals(Float.class)) { - return castNumber(number, N.ndouble); - } - return number; - } - - private static Number castNumber(Number number, N numberToken) { - int sourceBits = getNumberBitsBasedOnValue(number); - int targetBits = getNumberTokenBits(numberToken); - if (sourceBits > targetBits) { - throw new ArithmeticException(String.format("Can't convert number of type %s to %s due to overflow.", - number.getClass().getSimpleName(), numberToken.toString())); - } - if (numberToken == N.nbyte) { - return number.byteValue(); - } else if (numberToken == N.nshort) { - return number.shortValue(); - } else if (numberToken == N.nint) { - return number.intValue(); - } else if (numberToken == N.nlong) { - return number.longValue(); - } else if (numberToken == N.nfloat) { - return number.floatValue(); - } else if (numberToken == N.ndouble) { - return number.doubleValue(); - } else if (numberToken == N.nbigInt) { - return BigInteger.valueOf(number.longValue()); - } else if (numberToken == N.nbigDecimal) { - return BigDecimal.valueOf(number.doubleValue()); - } - return number; + private static Number castNumber(final Number number, final N numberToken) { + return NumberHelper.castTo(number, numberToken); } - private static int getNumberBitsBasedOnValue(Number number) { - final Class<? extends Number> clazz = number.getClass(); - if (clazz.equals(BigInteger.class)) { - return 128; - } else if (clazz.equals(BigDecimal.class)) { - return 128; - } - boolean floatingPoint = (clazz.equals(Float.class) || clazz.equals(Double.class)); - if (!floatingPoint && (number.longValue() >= Byte.MIN_VALUE) && (number.longValue() <= Byte.MAX_VALUE)) { - return 8; - } else if (!floatingPoint && (number.longValue() >= Short.MIN_VALUE) && (number.longValue() <= Short.MAX_VALUE)) { - return 16; - } else if (!floatingPoint && (number.longValue() >= Integer.MIN_VALUE) && (number.longValue() <= Integer.MAX_VALUE)) { - return 32; - } else if (floatingPoint && (number.doubleValue() >= Float.MIN_VALUE) && (number.doubleValue() <= Float.MAX_VALUE)) { - return 32; - } else { - return 64; - } - } - - private static int getNumberTokenBits(N numberToken) { - if (numberToken == N.nbyte) { - return 8; - } else if (numberToken == N.nshort) { - return 16; - } else if (numberToken == N.nint) { - return 32; - } else if (numberToken == N.nlong) { - return 64; - } else if (numberToken == N.nfloat) { - return 32; - } else if (numberToken == N.ndouble) { - return 64; - } - return 128; - } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoVersion.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoVersion.java index 65b0f7164c..d3e3a79d29 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoVersion.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoVersion.java @@ -25,20 +25,7 @@ import org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.optimiza import org.apache.tinkerpop.gremlin.process.computer.util.ComputerGraph; import org.apache.tinkerpop.gremlin.process.computer.util.MapMemory; import org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraverser; -import org.apache.tinkerpop.gremlin.process.traversal.Bytecode; -import org.apache.tinkerpop.gremlin.process.traversal.Contains; -import org.apache.tinkerpop.gremlin.process.traversal.DT; -import org.apache.tinkerpop.gremlin.process.traversal.Merge; -import org.apache.tinkerpop.gremlin.process.traversal.Operator; -import org.apache.tinkerpop.gremlin.process.traversal.Order; -import org.apache.tinkerpop.gremlin.process.traversal.P; -import org.apache.tinkerpop.gremlin.process.traversal.Path; -import org.apache.tinkerpop.gremlin.process.traversal.Pick; -import org.apache.tinkerpop.gremlin.process.traversal.Pop; -import org.apache.tinkerpop.gremlin.process.traversal.SackFunctions; -import org.apache.tinkerpop.gremlin.process.traversal.Scope; -import org.apache.tinkerpop.gremlin.process.traversal.Text; -import org.apache.tinkerpop.gremlin.process.traversal.TextP; +import org.apache.tinkerpop.gremlin.process.traversal.*; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.FoldStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GroupCountStep; @@ -551,7 +538,7 @@ public enum GryoVersion { add(GryoTypeReg.of(Pick.class, 137)); add(GryoTypeReg.of(DT.class, 198)); add(GryoTypeReg.of(Merge.class, 196)); - add(GryoTypeReg.of(DT.class, 199)); + add(GryoTypeReg.of(N.class, 200)); add(GryoTypeReg.of(HashSetSupplier.class, 136, new UtilSerializers.HashSetSupplierSerializer())); add(GryoTypeReg.of(MultiComparator.class, 165)); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/NumberHelper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/NumberHelper.java index 78df426e9f..137156fbcc 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/NumberHelper.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/NumberHelper.java @@ -18,6 +18,8 @@ */ package org.apache.tinkerpop.gremlin.util; +import org.apache.tinkerpop.gremlin.process.traversal.N; + import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; @@ -721,6 +723,79 @@ public final class NumberHelper { return a; } + /** + * Casts the given number to the specified numeric type if it can fit into it. + * Otherwise, throw. + * + * @param a the number to be cast + * @param numberToken the number token denoting the desired type to cast + * @return the number cast to the specified type + * @throws IllegalArgumentException if the specified numeric type is unsupported + * @throws ArithmeticException if the number overflows + */ + public static Number castTo(final Number a, final N numberToken) { + Class<?> clazz = numberToken.getType(); + if (a.getClass().equals(clazz)) { + return a; + } else if (clazz.equals(Integer.class)) { + Long val = getLong(a, numberToken); + if (val >= Integer.MIN_VALUE && val <= Integer.MAX_VALUE) { + return a.intValue(); + } + } else if (clazz.equals(Long.class)) { + return getLong(a, numberToken); + } else if (clazz.equals(Float.class)) { + // BigDecimal to double will overflow into Infinity, we want to throw instead of passing through + if (!a.getClass().equals(BigDecimal.class) && + (Double.isInfinite(a.doubleValue()) || Double.isNaN(a.doubleValue()))) { + return a.floatValue(); + } + if (a.doubleValue() >= -Float.MAX_VALUE && a.doubleValue() <= Float.MAX_VALUE) { + return a.floatValue(); + } + } else if (clazz.equals(Double.class)) { + // BigDecimal to double will overflow into Infinity, we want to throw instead of passing through + if (!a.getClass().equals(BigDecimal.class) && + (Double.isInfinite(a.doubleValue()) || Double.isNaN(a.doubleValue()))) { + return a.doubleValue(); + } + if (!Double.isInfinite(a.doubleValue())) { + // float losses precision, use string intermediate + return a.getClass().equals(Float.class) ? Double.parseDouble(a.toString()) : a.doubleValue(); + } + } else if (clazz.equals(Byte.class)) { + Long val = getLong(a, numberToken); + if (val >= Byte.MIN_VALUE && val <= Byte.MAX_VALUE) { + return a.byteValue(); + } + } else if (clazz.equals(Short.class)) { + Long val = getLong(a, numberToken); + if (val >= Short.MIN_VALUE && val <= Short.MAX_VALUE) { + return a.shortValue(); + } + } else if (clazz.equals(BigInteger.class)) { + return NumberHelper.bigIntegerValue(a); + } else if (clazz.equals(BigDecimal.class)) { + // float losses precision, use string intermediate + return a.getClass().equals(Float.class) ? new BigDecimal(a.toString()) : NumberHelper.bigDecimalValue(a); + } else { + throw new IllegalArgumentException("Unsupported number type token: " + numberToken); + } + + throw new ArithmeticException(String.format("Can't convert number of type %s to %s due to overflow.", + a.getClass().getSimpleName(), numberToken)); + } + + private static Long getLong(Number num, N numberToken) { + try { + return num.getClass().equals(BigInteger.class) ? ((BigInteger) num).longValueExact() : num.longValue(); + } catch (ArithmeticException ae) { + throw new ArithmeticException(String.format("Can't convert number of type %s to %s due to overflow.", + num.getClass().getSimpleName(), numberToken)); + } + + } + private static NumberHelper getHelper(final Class<? extends Number> clazz) { if (clazz.equals(Byte.class)) { return BYTE_NUMBER_HELPER; 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 396d4bd8e4..eb1f9547d8 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 @@ -24,12 +24,13 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.process.traversal.step.StepTest; import org.junit.Test; +import java.math.BigDecimal; +import java.math.BigInteger; 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; public class AsNumberStepTest extends StepTest { @@ -46,9 +47,18 @@ public class AsNumberStepTest extends StepTest { assertEquals(1, __.__(1.8).asNumber(N.nint).next()); assertEquals(1, __.__(1L).asNumber(N.nint).next()); assertEquals(1L, __.__(1L).asNumber().next()); + assertEquals(3.14f, __.__(3.14).asNumber(N.nfloat).next()); + assertEquals(3.14, __.__(3.14f).asNumber(N.ndouble).next()); assertEquals(1, __.__("1").asNumber(N.nint).next()); assertEquals(1, __.__("1").asNumber().next()); assertEquals((byte) 1, __.__("1").asNumber(N.nbyte).next()); + assertEquals((short) 1, __.__("1").asNumber(N.nshort).next()); + assertEquals(1L, __.__("1").asNumber(N.nlong).next()); + assertEquals(3.14, __.__("3.14").asNumber(N.ndouble).next()); //float to double + // NumberUtils allows additional string processing + assertEquals(123.0f, __.__("123.").asNumber().next()); + assertEquals(291, __.__("0x123").asNumber().next()); + assertEquals(83, __.__("0123").asNumber().next()); } @Test(expected = NumberFormatException.class) @@ -81,4 +91,283 @@ public class AsNumberStepTest extends StepTest { __.__(128).asNumber(N.nbyte).next(); } + @Test + public void testStringToByte() { + assertEquals((byte) 0, __.__("0").asNumber(N.nbyte).next()); + assertEquals((byte) 127, __.__("127").asNumber(N.nbyte).next()); + assertEquals((byte) -128, __.__("-128").asNumber(N.nbyte).next()); + assertEquals((byte) 42, __.__("42").asNumber(N.nbyte).next()); + assertEquals((byte) -42, __.__("-42").asNumber(N.nbyte).next()); + assertEquals((byte) 1, __.__("1").asNumber(N.nbyte).next()); + } + + @Test + public void testStringToShort() { + assertEquals((short) 0, __.__("0").asNumber(N.nshort).next()); + assertEquals((short) 32767, __.__("32767").asNumber(N.nshort).next()); + assertEquals((short) -32768, __.__("-32768").asNumber(N.nshort).next()); + assertEquals((short) 1000, __.__("1000").asNumber(N.nshort).next()); + assertEquals((short) -1000, __.__("-1000").asNumber(N.nshort).next()); + assertEquals((short) 255, __.__("255").asNumber(N.nshort).next()); + } + + @Test + public void testStringToInt() { + assertEquals(0, __.__("0").asNumber(N.nint).next()); + assertEquals(2147483647, __.__("2147483647").asNumber(N.nint).next()); + assertEquals(-2147483648, __.__("-2147483648").asNumber(N.nint).next()); + assertEquals(123456, __.__("123456").asNumber(N.nint).next()); + assertEquals(-123456, __.__("-123456").asNumber(N.nint).next()); + assertEquals(65536, __.__("65536").asNumber(N.nint).next()); + } + + @Test + public void testStringToLong() { + assertEquals(0L, __.__("0").asNumber(N.nlong).next()); + assertEquals(9223372036854775807L, __.__("9223372036854775807").asNumber(N.nlong).next()); + assertEquals(-9223372036854775808L, __.__("-9223372036854775808").asNumber(N.nlong).next()); + assertEquals(123456789L, __.__("123456789").asNumber(N.nlong).next()); + assertEquals(-123456789L, __.__("-123456789").asNumber(N.nlong).next()); + assertEquals(4294967296L, __.__("4294967296").asNumber(N.nlong).next()); + } + + @Test + public void testStringToFloat() { + assertEquals(0.0f, __.__("0.0").asNumber(N.nfloat).next()); + assertEquals(3.14f, __.__("3.14").asNumber(N.nfloat).next()); + assertEquals(-3.14f, __.__("-3.14").asNumber(N.nfloat).next()); + assertEquals(1.23e10f, __.__("1.23e10").asNumber(N.nfloat).next()); + assertEquals(-1.23e-10f, __.__("-1.23e-10").asNumber(N.nfloat).next()); + assertEquals(Float.MAX_VALUE, __.__("3.4028235E38").asNumber(N.nfloat).next()); + assertEquals(Float.MIN_VALUE, __.__("1.4E-45").asNumber(N.nfloat).next()); + } + + @Test + public void testStringToDouble() { + assertEquals(0.0, __.__("0.0").asNumber(N.ndouble).next()); + assertEquals(3.141592653589793, __.__("3.141592653589793").asNumber(N.ndouble).next()); + assertEquals(-3.141592653589793, __.__("-3.141592653589793").asNumber(N.ndouble).next()); + assertEquals(1.23e100, __.__("1.23e100").asNumber(N.ndouble).next()); + assertEquals(-1.23e-100, __.__("-1.23e-100").asNumber(N.ndouble).next()); + assertEquals(Double.MAX_VALUE, __.__("1.7976931348623157E308").asNumber(N.ndouble).next()); + assertEquals(Double.MIN_VALUE, __.__("4.9E-324").asNumber(N.ndouble).next()); + } + + @Test + public void testStringToBigInteger() { + assertEquals(new BigInteger("0"), __.__("0").asNumber(N.nbigInt).next()); + assertEquals(new BigInteger("123456789012345678901234567890"), + __.__("123456789012345678901234567890").asNumber(N.nbigInt).next()); + assertEquals(new BigInteger("-123456789012345678901234567890"), + __.__("-123456789012345678901234567890").asNumber(N.nbigInt).next()); + assertEquals(new BigInteger("999999999999999999999999999999999999999999999999999"), + __.__("999999999999999999999999999999999999999999999999999").asNumber(N.nbigInt).next()); + assertEquals(new BigInteger("1"), __.__("1").asNumber(N.nbigInt).next()); + } + + @Test + public void testStringToBigDecimal() { + assertEquals(new BigDecimal("0.0"), __.__("0.0").asNumber(N.nbigDecimal).next()); + assertEquals(new BigDecimal("123456789012345678901234567890.123456789"), + __.__("123456789012345678901234567890.123456789").asNumber(N.nbigDecimal).next()); + assertEquals(new BigDecimal("-123456789012345678901234567890.123456789"), + __.__("-123456789012345678901234567890.123456789").asNumber(N.nbigDecimal).next()); + // Note directly constructing BigDecimal returns 1E-39, but parsing then converting from double results in 1.0E-39 + assertEquals(BigDecimal.valueOf(Double.parseDouble("0.000000000000000000000000000000000000001")), + __.__("0.000000000000000000000000000000000000001").asNumber(N.nbigDecimal).next()); + assertEquals(new BigDecimal("1.0"), __.__("1.0").asNumber(N.nbigDecimal).next()); + } + +// ===== EDGE CASE TESTS ===== + + @Test + public void testCastInfinityAndNaN() { + assertEquals(Double.POSITIVE_INFINITY, __.__(Float.POSITIVE_INFINITY).asNumber(N.ndouble).next()); + assertEquals(Double.NEGATIVE_INFINITY, __.__(Float.NEGATIVE_INFINITY).asNumber(N.ndouble).next()); + assertEquals(Double.NaN, __.__(Float.NaN).asNumber(N.ndouble).next()); + assertEquals(Float.POSITIVE_INFINITY, __.__(Double.POSITIVE_INFINITY).asNumber(N.nfloat).next()); + assertEquals(Float.NEGATIVE_INFINITY, __.__(Double.NEGATIVE_INFINITY).asNumber(N.nfloat).next()); + assertEquals(Float.NaN, __.__(Double.NaN).asNumber(N.nfloat).next()); + assertEquals(Long.MAX_VALUE, __.__(Double.POSITIVE_INFINITY).asNumber(N.nlong).next()); + assertEquals(0, __.__(Double.NaN).asNumber(N.nint).next()); + } + + @Test + public void testStringWithWhitespace() { + assertEquals(42, __.__(" 42 ").asNumber(N.nint).next()); + assertEquals(42, __.__("\t42\n").asNumber(N.nint).next()); + assertEquals(3.14, __.__(" 3.14 ").asNumber(N.ndouble).next()); + assertEquals((byte) 127, __.__(" 127 ").asNumber(N.nbyte).next()); + } + + @Test + public void testStringWithPlusSign() { + assertEquals(42, __.__("+42").asNumber(N.nint).next()); + assertEquals(3.14, __.__("+3.14").asNumber(N.ndouble).next()); + assertEquals((byte) 42, __.__("+42").asNumber(N.nbyte).next()); + assertEquals(new BigInteger("42"), __.__("+42").asNumber(N.nbigInt).next()); + } + + @Test + public void testZeroValues() { + assertEquals((byte) 0, __.__("0").asNumber(N.nbyte).next()); + assertEquals((short) 0, __.__("0").asNumber(N.nshort).next()); + assertEquals(0, __.__("0").asNumber(N.nint).next()); + assertEquals(0L, __.__("0").asNumber(N.nlong).next()); + assertEquals(0.0f, __.__("0.0").asNumber(N.nfloat).next()); + assertEquals(0.0, __.__("0.0").asNumber(N.ndouble).next()); + assertEquals(new BigInteger("0"), __.__("0").asNumber(N.nbigInt).next()); + assertEquals(new BigDecimal("0.0"), __.__("0.0").asNumber(N.nbigDecimal).next()); + } + + @Test + public void testScientificNotation() { + assertEquals(1.23e10f, __.__("1.23e10").asNumber(N.nfloat).next()); + assertEquals(1.23e10, __.__("1.23e10").asNumber(N.ndouble).next()); + assertEquals(1.23e-10f, __.__("1.23e-10").asNumber(N.nfloat).next()); + assertEquals(1.23e-10, __.__("1.23e-10").asNumber(N.ndouble).next()); + assertEquals(new BigDecimal("1.23E10"), __.__("1.23e10").asNumber(N.nbigDecimal).next()); + } + +// ===== OVERFLOW EXCEPTION TESTS ===== + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringByteOverflowsPositive() { + __.__("128").asNumber(N.nbyte).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringByteOverflowsNegative() { + __.__("-129").asNumber(N.nbyte).next(); + + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringShortOverflowsPositive() { + __.__("32768").asNumber(N.nshort).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringShortOverflowsNegative() { + __.__("-32769").asNumber(N.nshort).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringIntOverflowsPositive() { + __.__("2147483648").asNumber(N.nint).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringIntOverflowsNegative() { + __.__("-2147483649").asNumber(N.nint).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringLongOverflowsPositive() { + __.__("9223372036854775809").asNumber(N.nlong).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenStringLongOverflowsNegative() { + __.__("-9223372036854775809").asNumber(N.nlong).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenDoubleStringToFloatOverflows() { + __.__("3.5E38").asNumber(N.nfloat).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenBigDecimalStringToDoubleOverflows() { + __.__("1.8E308").asNumber(N.ndouble).next(); + } + + @Test(expected = ArithmeticException.class) + public void shouldThrowOverflowExceptionWhenBigDecimalStringToFloatOverflows() { + __.__("1.8E308").asNumber(N.nfloat).next(); + } + +// ===== INVALID FORMAT EXCEPTION TESTS ===== + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenEmptyString() { + __.__("").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenWhitespaceOnlyString() { + __.__(" ").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenAlphabeticString() { + __.__("abc").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenMixedAlphanumericString() { + __.__("123abc456").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenSpecialCharactersString() { + __.__("12@34").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenMultipleDecimalPoints() { + __.__("12.34.56").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenMultipleSigns() { + __.__("++123").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenSignInMiddle() { + __.__("12+34").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenInvalidScientificNotation() { + __.__("12e").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenInvalidScientificNotationWithSign() { + __.__("12e+").asNumber().next(); + } + + @Test(expected = NumberFormatException.class) + public void shouldThrowExceptionWhenInvalidDecimalFormat() { + __.__(".").asNumber().next(); + } + +// ===== PRECISION AND BOUNDARY TESTS ===== + + @Test + public void testBoundaryValues() { + assertEquals(Byte.MAX_VALUE, __.__("127").asNumber(N.nbyte).next()); + assertEquals(Byte.MIN_VALUE, __.__("-128").asNumber(N.nbyte).next()); + assertEquals(Short.MAX_VALUE, __.__("32767").asNumber(N.nshort).next()); + assertEquals(Short.MIN_VALUE, __.__("-32768").asNumber(N.nshort).next()); + assertEquals(Integer.MAX_VALUE, __.__("2147483647").asNumber(N.nint).next()); + assertEquals(Integer.MIN_VALUE, __.__("-2147483648").asNumber(N.nint).next()); + assertEquals(Long.MAX_VALUE, __.__("9223372036854775807").asNumber(N.nlong).next()); + assertEquals(Long.MIN_VALUE, __.__("-9223372036854775808").asNumber(N.nlong).next()); + } + + @Test + public void testHighPrecisionDecimals() { + assertEquals(new BigDecimal("3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067"), + __.__("3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067").asNumber(N.nbigDecimal).next()); + } + + @Test + public void testVeryLargeNumbers() { + String largeNumber = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"; + assertEquals(new BigInteger(largeNumber), __.__(largeNumber).asNumber(N.nbigInt).next()); + } + } diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/NumberHelperTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/NumberHelperTest.java index 314be66fcb..ef88db95ce 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/NumberHelperTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/NumberHelperTest.java @@ -18,6 +18,7 @@ */ package org.apache.tinkerpop.gremlin.util; +import org.apache.tinkerpop.gremlin.process.traversal.N; import org.javatuples.Quartet; import org.javatuples.Triplet; import org.junit.Test; @@ -709,4 +710,78 @@ public class NumberHelperTest { final Double value = 42.0; assertEquals(Float.valueOf(42.0f), NumberHelper.coerceTo(value, Float.class)); } + + + @Test + public void shouldCastToReturnSameInstanceForSameClass() { + final Integer value = 42; + assertEquals(value, NumberHelper.castTo(value, N.nint)); + } + + @Test + public void shouldCastToConvertToByte() { + final Integer value = 42; + assertEquals(Byte.valueOf((byte) 42), NumberHelper.castTo(value, N.nbyte)); + } + + @Test + public void shouldCastToConvertToShort() { + final Integer value = 42; + assertEquals(Short.valueOf((short) 42), NumberHelper.castTo(value, N.nbyte)); + } + + @Test + public void shouldCastToConvertToLong() { + final Integer value = 42; + assertEquals(Long.valueOf(42L), NumberHelper.castTo(value, N.nlong)); + } + + @Test + public void shouldCastToConvertToFloat() { + final Integer value = 42; + assertEquals(Float.valueOf(42.0f), NumberHelper.castTo(value, N.nfloat)); + } + + @Test + public void shouldCastToConvertToDouble() { + final Integer value = 42; + assertEquals(Double.valueOf(42.0), NumberHelper.castTo(value, N.ndouble)); + } + + @Test + public void shouldCastToConvertToBigInteger() { + final Integer value = 42; + assertEquals(BigInteger.valueOf(42), NumberHelper.castTo(value, N.nbigInt)); + } + + @Test + public void shouldCastToConvertToBigDecimal() { + final Integer value = 42; + assertEquals(BigDecimal.valueOf(42), NumberHelper.castTo(value, N.nbigDecimal)); + } + + @Test(expected = ArithmeticException.class) + public void shouldOverflowIfCannotFitInByte() { + final Integer value = 128; + NumberHelper.castTo(value, N.nbyte); + } + + @Test(expected = ArithmeticException.class) + public void shouldOverflowIfCannotFitInShort() { + final Integer value = 32768; + NumberHelper.castTo(value, N.nshort); + } + + @Test(expected = ArithmeticException.class) + public void shouldOverflowIfCannotFitInInteger() { + final Long value = 2147483648L; + NumberHelper.castTo(value, N.nint); + } + + @Test(expected = ArithmeticException.class) + public void shouldOverflowIfCannotFitInFloat() { + final Double value = Double.MAX_VALUE; + NumberHelper.castTo(value, N.nfloat); + } + } 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 2a5972a067..70c2154841 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 @@ -100,7 +100,7 @@ Feature: Step - asNumber() When iterated to list Then the result should be unordered | result | - | d[5.75].d | + | d[5.75].f | @GraphComputerVerificationInjectionNotSupported Scenario: g_injectX5_43X_asNumberXN_nintX
