-
Notifications
You must be signed in to change notification settings - Fork 178
issue #4514 tonumber function as part of roadmap #4287 #4605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 26 commits
0ed23de
5a5b778
9d71a95
be2c2e2
fc763a4
a04eb14
590a8e6
6938e2c
314fccd
0ee17b9
6e24aa3
454cfc8
b221e8c
a0a89c4
ea9ba0f
370b9bc
f070684
a2accf5
3adda5d
17b1d86
eca7d13
e780c15
e3884b0
042d3e8
38ce420
58d8e1b
34f8635
8279bf3
b5346a6
7cf867b
8d9db4e
46f010b
b7afa17
0f0125a
804db79
7588397
04a5564
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.expression.function.udf; | ||
|
|
||
| import java.util.List; | ||
| import org.apache.calcite.adapter.enumerable.NotNullImplementor; | ||
| import org.apache.calcite.adapter.enumerable.NullPolicy; | ||
| import org.apache.calcite.adapter.enumerable.RexToLixTranslator; | ||
| import org.apache.calcite.linq4j.function.Strict; | ||
| import org.apache.calcite.linq4j.tree.Expression; | ||
| import org.apache.calcite.linq4j.tree.Expressions; | ||
| import org.apache.calcite.rex.RexCall; | ||
| import org.apache.calcite.runtime.SqlFunctions; | ||
| import org.apache.calcite.sql.type.ReturnTypes; | ||
| import org.apache.calcite.sql.type.SqlReturnTypeInference; | ||
| import org.opensearch.sql.calcite.utils.PPLOperandTypes; | ||
| import org.opensearch.sql.expression.function.ImplementorUDF; | ||
| import org.opensearch.sql.expression.function.UDFOperandMetadata; | ||
|
|
||
| /** | ||
| * A custom implementation of number/boolean to string . | ||
| * | ||
| * <p>This operator is necessary because tostring has following requirements "binary" Converts a | ||
| * number to a binary value. "hex" Converts the number to a hexadecimal value. "commas" Formats the | ||
| * number with commas. If the number includes a decimal, the function rounds the number to nearest | ||
| * two decimal places. "duration" Converts the value in seconds to the readable time format | ||
| * HH:MM:SS. if not format parameter provided, then consider value as boolean | ||
| */ | ||
| public class ToNumberFunction extends ImplementorUDF { | ||
| public ToNumberFunction() { | ||
| super( | ||
| new org.opensearch.sql.expression.function.udf.ToNumberFunction.ToNumberImplementor(), | ||
| NullPolicy.ANY); | ||
| } | ||
|
|
||
| public static final String DURATION_FORMAT = "duration"; | ||
| public static final String DURATION_MILLIS_FORMAT = "duration_millis"; | ||
| public static final String HEX_FORMAT = "hex"; | ||
| public static final String COMMAS_FORMAT = "commas"; | ||
| public static final String BINARY_FORMAT = "binary"; | ||
| public static final SqlFunctions.DateFormatFunction dateTimeFormatter = | ||
| new SqlFunctions.DateFormatFunction(); | ||
| public static final String format24hour = "%H:%M:%S"; // 24-hour format | ||
|
|
||
| @Override | ||
| public SqlReturnTypeInference getReturnTypeInference() { | ||
| return ReturnTypes.DOUBLE_FORCE_NULLABLE; | ||
| } | ||
|
|
||
| @Override | ||
| public UDFOperandMetadata getOperandMetadata() { | ||
| return PPLOperandTypes.STRING_OR_STRING_INTEGER; | ||
| } | ||
|
|
||
| public static class ToNumberImplementor implements NotNullImplementor { | ||
|
|
||
| @Override | ||
| public Expression implement( | ||
| RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) { | ||
| Expression fieldValue = translatedOperands.get(0); | ||
| int base = 10; | ||
| if (translatedOperands.size() > 1) { | ||
| Expression baseExpr = translatedOperands.get(1); | ||
| return Expressions.call(ToNumberFunction.class, "toNumber", fieldValue, baseExpr); | ||
| } else { | ||
| return Expressions.call(ToNumberFunction.class, "toNumber", fieldValue); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Strict | ||
| public static Number toNumber(String numStr) { | ||
| return toNumber(numStr, 10); | ||
| } | ||
|
|
||
| @Strict | ||
| public static Number toNumber(String numStr, int base) { | ||
asifabashar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (base < 2 || base > 36) { | ||
| throw new IllegalArgumentException("Base must be between 2 and 36"); | ||
| } | ||
|
|
||
| if (numStr.contains(".")) { | ||
|
|
||
| boolean isNegative = numStr.startsWith("-"); | ||
| if (isNegative) { | ||
| numStr = numStr.substring(1); | ||
| } | ||
|
|
||
| // Split integer and fractional parts | ||
| String[] parts = numStr.split("\\."); | ||
| String intPart = parts[0]; | ||
| String fracPart = parts.length > 1 ? parts[1] : ""; | ||
|
|
||
| // Convert integer part | ||
| double intValue = 0; | ||
| for (char c : intPart.toCharArray()) { | ||
| int digit = Character.digit(c, base); | ||
| if (digit < 0) throw new IllegalArgumentException("Invalid digit: " + c); | ||
| intValue = intValue * base + digit; | ||
| } | ||
|
|
||
| // Convert fractional part | ||
| double fracValue = 0; | ||
| double divisor = base; | ||
| for (char c : fracPart.toCharArray()) { | ||
| int digit = Character.digit(c, base); | ||
| if (digit < 0) throw new IllegalArgumentException("Invalid digit: " + c); | ||
| fracValue += (double) digit / divisor; | ||
| divisor *= base; | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this by design? |
||
| double result = intValue + fracValue; | ||
| return isNegative ? -result : result; | ||
| } else { | ||
| return Integer.parseInt(numStr, base); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.expression.function.udf; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import org.apache.calcite.sql.type.ReturnTypes; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.opensearch.sql.calcite.utils.PPLOperandTypes; | ||
|
|
||
| public class ToNumberFunctionTest { | ||
|
|
||
| private final ToNumberFunction function = new ToNumberFunction(); | ||
|
|
||
| @Test | ||
| void testGetReturnTypeInference() { | ||
| assertEquals(ReturnTypes.DOUBLE_FORCE_NULLABLE, function.getReturnTypeInference()); | ||
| } | ||
|
|
||
| @Test | ||
| void testGetOperandMetadata() { | ||
| assertEquals(PPLOperandTypes.STRING_OR_STRING_INTEGER, function.getOperandMetadata()); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithDefaultBase() { | ||
| assertEquals(123, ToNumberFunction.toNumber("123")); | ||
| assertEquals(0, ToNumberFunction.toNumber("0")); | ||
| assertEquals(-456, ToNumberFunction.toNumber("-456")); | ||
| assertEquals(123.45, ToNumberFunction.toNumber("123.45")); | ||
| assertEquals(-123.45, ToNumberFunction.toNumber("-123.45")); | ||
| assertEquals(0.5, ToNumberFunction.toNumber("0.5")); | ||
| assertEquals(-0.5, ToNumberFunction.toNumber("-0.5")); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithBase10() { | ||
| assertEquals(123, ToNumberFunction.toNumber("123", 10)); | ||
| assertEquals(0, ToNumberFunction.toNumber("0", 10)); | ||
| assertEquals(-456, ToNumberFunction.toNumber("-456", 10)); | ||
| assertEquals(123.45, ToNumberFunction.toNumber("123.45", 10)); | ||
| assertEquals(-123.45, ToNumberFunction.toNumber("-123.45", 10)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithBase2() { | ||
| assertEquals(5, ToNumberFunction.toNumber("101", 2)); | ||
| assertEquals(0, ToNumberFunction.toNumber("0", 2)); | ||
| assertEquals(1, ToNumberFunction.toNumber("1", 2)); | ||
| assertEquals(7, ToNumberFunction.toNumber("111", 2)); | ||
| assertEquals(10, ToNumberFunction.toNumber("1010", 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithBase8() { | ||
| assertEquals(64, ToNumberFunction.toNumber("100", 8)); | ||
| assertEquals(8, ToNumberFunction.toNumber("10", 8)); | ||
| assertEquals(83, ToNumberFunction.toNumber("123", 8)); | ||
| assertEquals(511, ToNumberFunction.toNumber("777", 8)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithBase16() { | ||
| assertEquals(255, ToNumberFunction.toNumber("FF", 16)); | ||
| assertEquals(16, ToNumberFunction.toNumber("10", 16)); | ||
| assertEquals(171, ToNumberFunction.toNumber("AB", 16)); | ||
| assertEquals(291, ToNumberFunction.toNumber("123", 16)); | ||
| assertEquals(4095, ToNumberFunction.toNumber("FFF", 16)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithBase36() { | ||
| assertEquals(35, ToNumberFunction.toNumber("Z", 36)); | ||
| assertEquals(1295, ToNumberFunction.toNumber("ZZ", 36)); | ||
| assertEquals(46655, ToNumberFunction.toNumber("ZZZ", 36)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithDecimalBase2() { | ||
| assertEquals(2.5, ToNumberFunction.toNumber("10.1", 2)); | ||
| assertEquals(1.5, ToNumberFunction.toNumber("1.1", 2)); | ||
| assertEquals(3.75, ToNumberFunction.toNumber("11.11", 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithDecimalBase16() { | ||
| assertEquals(255.5, ToNumberFunction.toNumber("FF.8", 16)); | ||
| assertEquals(16.25, ToNumberFunction.toNumber("10.4", 16)); | ||
| assertEquals(171.6875, ToNumberFunction.toNumber("AB.B", 16)); | ||
|
||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithNegativeDecimal() { | ||
| assertEquals(-2.5, ToNumberFunction.toNumber("-10.1", 2)); | ||
| assertEquals(-255.5, ToNumberFunction.toNumber("-FF.8", 16)); | ||
| assertEquals(-123.45, ToNumberFunction.toNumber("-123.45", 10)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithEmptyFractionalPart() { | ||
| assertEquals(123.0, ToNumberFunction.toNumber("123.", 10)); | ||
| assertEquals(255.0, ToNumberFunction.toNumber("FF.", 16)); | ||
| assertEquals(5.0, ToNumberFunction.toNumber("101.", 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberWithZeroIntegerPart() { | ||
| assertEquals(0.5, ToNumberFunction.toNumber("0.5", 10)); | ||
| assertEquals(0.5, ToNumberFunction.toNumber("0.1", 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberInvalidBase() { | ||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("123", 1); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("123", 37); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("123", 0); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("123", -1); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberInvalidDigits() { | ||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("12A", 10); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("102", 2); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("189", 8); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("GHI", 16); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberInvalidFractionalDigits() { | ||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("10.2", 2); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("FF.G", 16); | ||
| }); | ||
|
|
||
| assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> { | ||
| ToNumberFunction.toNumber("123.ABC", 10); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberEdgeCases() { | ||
| assertEquals(0, ToNumberFunction.toNumber("0", 2)); | ||
| assertEquals(0, ToNumberFunction.toNumber("0", 36)); | ||
| assertEquals(0.0, ToNumberFunction.toNumber("0.0", 10)); | ||
| assertEquals(0.0, ToNumberFunction.toNumber("0.000", 10)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberLargeNumbers() { | ||
| assertEquals( | ||
| Integer.MAX_VALUE, ToNumberFunction.toNumber(String.valueOf(Integer.MAX_VALUE), 10)); | ||
| assertEquals( | ||
| Integer.MIN_VALUE, ToNumberFunction.toNumber(String.valueOf(Integer.MIN_VALUE), 10)); | ||
| } | ||
|
|
||
| @Test | ||
| void testToNumberCaseInsensitivity() { | ||
| assertEquals(255, ToNumberFunction.toNumber("ff", 16)); | ||
| assertEquals(255, ToNumberFunction.toNumber("FF", 16)); | ||
| assertEquals(255, ToNumberFunction.toNumber("fF", 16)); | ||
| assertEquals(171, ToNumberFunction.toNumber("ab", 16)); | ||
| assertEquals(171, ToNumberFunction.toNumber("AB", 16)); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useless?