[ https://issues.apache.org/jira/browse/TINKERPOP-3166?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18004312#comment-18004312 ]
ASF GitHub Bot commented on TINKERPOP-3166: ------------------------------------------- Cole-Greer commented on code in PR #3153: URL: https://github.com/apache/tinkerpop/pull/3153#discussion_r2196334364 ########## gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsNumberStep.java: ########## @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.process.traversal.step.map; + +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.tinkerpop.gremlin.process.traversal.N; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; +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> { + + private N numberToken; + + public AsNumberStep(final Traversal.Admin traversal) { + super(traversal); + this.numberToken = null; + } + + public AsNumberStep(final Traversal.Admin traversal, final N numberToken) { + super(traversal); + this.numberToken = numberToken; + } + + @Override + protected Number map(Traverser.Admin<S> traverser) { + final Object object = traverser.get(); + if (object instanceof String) { + String numberText = (String) object; + Number number = parseNumber(numberText); + return numberToken == null ? autoNumber(number) : castNumber(number, numberToken); + } else if (object instanceof Number) { + Number number = (Number) object; + return numberToken == null ? autoNumber(number) : castNumber(number, numberToken); + } + throw new IllegalArgumentException(String.format("Can't parse type %s as number.", object == null ? "null" : object.getClass().getSimpleName())); + } + + @Override + public Set<TraverserRequirement> getRequirements() { + return Collections.singleton(TraverserRequirement.OBJECT); + } + + @Override + public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) { + super.setTraversal(parentTraversal); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (numberToken != null ? numberToken.hashCode() : 0); + return result; + } + + @Override + public AsNumberStep<S> clone() { + final AsNumberStep<S> clone = (AsNumberStep<S>) super.clone(); + clone.numberToken = this.numberToken; + return clone; + } + + @Override + public String toString() { + return StringFactory.stepString(this); + } + + 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)); + } + } + + 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 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)) { Review Comment: Relying on range alone to determine if it should be a float or a double is a bit dubious as it ignores precision. Excluding numbers such as `0.5` which are exactly represent-able as floats, the double-to-float conversion is always lossy even within the float range. Perhaps it's best to lean into this lossy conversion considering it only happens when users explicitly request floats, and don't bother with exceptions at all for floats. If the range is exceeded we could simply produce +/- infinity. This is already done by Java when casting: ``` (float) Double.MAX_VALUE ==>Infinity ``` > Add number conversion step asNumber() > ------------------------------------- > > Key: TINKERPOP-3166 > URL: https://issues.apache.org/jira/browse/TINKERPOP-3166 > Project: TinkerPop > Issue Type: Improvement > Components: language > Affects Versions: 3.8.0 > Reporter: Yang Xia > Priority: Major > > Given the addition of the {{asString()}} and {{asDate()}} steps in the 3.7 > line, it should also be helpful to add an {{asNumber()}} step that does > numerical casting/conversions. > The current idea is for the {{asNumber()}} step to convert the incoming > traverser to the nearest parsable type (e.g. int or double) if no argument is > provided, or to the desired numerical type, based on a number token > ({{{}N{}}}) provided. Like the {{asDate()}} step, it will not be scoped (for > now, scopes can be added in the future). > Some conjured examples: > {code:java} > gremlin> g.inject(5).asNumber() > ==> 5 // parses to int > gremlin> g.inject(5.123f).asNumber() > ==> 5.123 > gremlin> g.inject(5.43).asNumber(N.int) > ==> 5 {code} > More details can be found in the [proposal > doc|https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-asnumber-step-6.asciidoc]. > -- This message was sent by Atlassian Jira (v8.20.10#820010)