This is an automated email from the ASF dual-hosted git repository.

tanner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new aa5397bed4 [CALCITE-5821] Add FORMAT_NUMBER function (enabled in Hive 
and Spark library)
aa5397bed4 is described below

commit aa5397bed42313865d3a43908427ad5f12b27ef3
Author: Runkang He <[email protected]>
AuthorDate: Sun Jul 9 22:27:00 2023 +0800

    [CALCITE-5821] Add FORMAT_NUMBER function (enabled in Hive and Spark 
library)
---
 .../calcite/adapter/enumerable/RexImpTable.java    |  2 +
 .../apache/calcite/runtime/CalciteResource.java    |  3 +
 .../org/apache/calcite/runtime/SqlFunctions.java   | 56 ++++++++++++
 .../calcite/sql/fun/SqlLibraryOperators.java       | 10 +++
 .../org/apache/calcite/sql/type/OperandTypes.java  |  3 +
 .../org/apache/calcite/util/BuiltInMethod.java     |  1 +
 .../calcite/runtime/CalciteResource.properties     |  1 +
 site/_docs/reference.md                            |  2 +
 .../org/apache/calcite/test/SqlOperatorTest.java   | 99 ++++++++++++++++++++++
 9 files changed, 177 insertions(+)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index b4a5cb8ed9..097da9bbc1 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -175,6 +175,7 @@ import static 
org.apache.calcite.sql.fun.SqlLibraryOperators.EXTRACT_XML;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.FLOOR_BIG_QUERY;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_DATE;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_DATETIME;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_NUMBER;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_TIME;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_TIMESTAMP;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.FROM_BASE32;
@@ -521,6 +522,7 @@ public class RexImpTable {
       defineMethod(SHA256, BuiltInMethod.SHA256.method, NullPolicy.STRICT);
       defineMethod(SHA512, BuiltInMethod.SHA512.method, NullPolicy.STRICT);
       defineMethod(SUBSTRING, BuiltInMethod.SUBSTRING.method, 
NullPolicy.STRICT);
+      defineMethod(FORMAT_NUMBER, BuiltInMethod.FORMAT_NUMBER.method, 
NullPolicy.STRICT);
       defineMethod(LEFT, BuiltInMethod.LEFT.method, NullPolicy.ANY);
       defineMethod(RIGHT, BuiltInMethod.RIGHT.method, NullPolicy.ANY);
       defineMethod(LPAD, BuiltInMethod.LPAD.method, NullPolicy.STRICT);
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java 
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index 9456e3aab9..230f2ea0ca 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -903,6 +903,9 @@ public interface CalciteResource {
   @BaseMessage("Substring error: negative substring length not allowed")
   ExInst<CalciteException> illegalNegativeSubstringLength();
 
+  @BaseMessage("Illegal arguments for 'FORMAT_NUMBER' function: negative 
decimal value not allowed")
+  ExInst<CalciteException> illegalNegativeDecimalValue();
+
   @BaseMessage("Illegal arguments: The length of the keys array {0,number,#} 
is not equal to the length of the values array {1,number,#} in MAP_FROM_ARRAYS 
function")
   ExInst<CalciteException> illegalArgumentsInMapFromArraysFunc(int arg0, int 
arg1);
 
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index c64b70ad18..b6a7e1be0b 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -886,6 +886,62 @@ public class SqlFunctions {
     return c.substring(s0, e0);
   }
 
+  /** SQL FORMAT_NUMBER(value, decimalOrFormat) function. */
+  public static String formatNumber(long value, int decimalVal) {
+    DecimalFormat numberFormat = getNumberFormat(decimalVal);
+    return numberFormat.format(value);
+  }
+
+  public static String formatNumber(double value, int decimalVal) {
+    DecimalFormat numberFormat = getNumberFormat(decimalVal);
+    return numberFormat.format(value);
+  }
+
+  public static String formatNumber(BigDecimal value, int decimalVal) {
+    DecimalFormat numberFormat = getNumberFormat(decimalVal);
+    return numberFormat.format(value);
+  }
+
+  public static String formatNumber(long value, String format) {
+    DecimalFormat numberFormat = getNumberFormat(format);
+    return numberFormat.format(value);
+  }
+
+  public static String formatNumber(double value, String format) {
+    DecimalFormat numberFormat = getNumberFormat(format);
+    return numberFormat.format(value);
+  }
+
+  public static String formatNumber(BigDecimal value, String format) {
+    DecimalFormat numberFormat = getNumberFormat(format);
+    return numberFormat.format(value);
+  }
+
+  public static String getFormatPattern(int decimalVal) {
+    StringBuilder pattern = new StringBuilder();
+    pattern.append("#,###,###,###,###,###,##0");
+
+    if (decimalVal > 0) {
+      pattern.append(".");
+      for (int i = 0; i < decimalVal; i++) {
+        pattern.append("0");
+      }
+    }
+    return pattern.toString();
+  }
+
+  private static DecimalFormat getNumberFormat(String pattern) {
+    return NumberUtil.decimalFormat(pattern);
+  }
+
+  private static DecimalFormat getNumberFormat(int decimalVal) {
+    if (decimalVal < 0) {
+      throw RESOURCE.illegalNegativeDecimalValue().ex();
+    }
+    String pattern = getFormatPattern(decimalVal);
+    return getNumberFormat(pattern);
+  }
+
   /** SQL UPPER(string) function. */
   public static String upper(String s) {
     return s.toUpperCase(Locale.ROOT);
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index 21503e4b73..badd4aae9c 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -1400,6 +1400,16 @@ public abstract class SqlLibraryOperators {
           OperandTypes.BINARY,
           SqlFunctionCategory.STRING);
 
+  /** The "FORMAT_NUMBER(value, decimalOrFormat)" function. */
+  @LibraryOperator(libraries = {HIVE, SPARK})
+  public static final SqlFunction FORMAT_NUMBER =
+      SqlBasicFunction.create("FORMAT_NUMBER",
+          ReturnTypes.VARCHAR_NULLABLE,
+          OperandTypes.or(
+              OperandTypes.NUMERIC_NUMERIC,
+              OperandTypes.NUMERIC_CHARACTER),
+          SqlFunctionCategory.STRING);
+
   /** The "TO_CHAR(timestamp, format)" function;
    * converts {@code timestamp} to string according to the given {@code 
format}.
    */
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index b59bf574a7..d60c6fa72c 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -364,6 +364,9 @@ public abstract class OperandTypes {
           // Second operand optional (operand index 0, 1)
           number -> number == 1);
 
+  public static final SqlSingleOperandTypeChecker NUMERIC_CHARACTER =
+      family(SqlTypeFamily.NUMERIC, SqlTypeFamily.CHARACTER);
+
   public static final SqlSingleOperandTypeChecker NUMERIC_INTEGER =
       family(SqlTypeFamily.NUMERIC, SqlTypeFamily.INTEGER);
 
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index 53edb703e4..057045c656 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -429,6 +429,7 @@ public enum BuiltInMethod {
   INITCAP(SqlFunctions.class, "initcap", String.class),
   SUBSTRING(SqlFunctions.class, "substring", String.class, int.class,
       int.class),
+  FORMAT_NUMBER(SqlFunctions.class, "formatNumber", long.class, int.class),
   LPAD(SqlFunctions.class, "lpad", String.class, int.class, String.class),
   RPAD(SqlFunctions.class, "rpad", String.class, int.class, String.class),
   STARTS_WITH(SqlFunctions.class, "startsWith", String.class, String.class),
diff --git 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 88c36db887..1033899a30 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -295,6 +295,7 @@ DialectDoesNotSupportFeature=Dialect does not support 
feature: ''{0}''
 IllegalNegativePadLength=Second argument for LPAD/RPAD must not be negative
 IllegalEmptyPadPattern=Third argument (pad pattern) for LPAD/RPAD must not be 
empty
 IllegalNegativeSubstringLength=Substring error: negative substring length not 
allowed
+IllegalNegativeDecimalValue=Illegal arguments for 'FORMAT_NUMBER' function: 
negative decimal value not allowed
 IllegalArgumentsInMapFromArraysFunc=Illegal arguments: The length of the keys 
array {0,number,#} is not equal to the length of the values array {1,number,#} 
in MAP_FROM_ARRAYS function
 TrimError=Trim error: trim character must be exactly 1 character
 InvalidTypesForArithmetic=Invalid types for arithmetic: {0} {1} {2}
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 904f30df24..6b2f517469 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2731,6 +2731,8 @@ BigQuery's type system uses confusingly different names 
for types and functions:
 | b | FLOOR(value)                                   | Similar to standard 
`FLOOR(value)` except if *value* is an integer type, the return type is a double
 | b | FORMAT_DATE(string, date)                      | Formats *date* 
according to the specified format *string*
 | b | FORMAT_DATETIME(string, timestamp)             | Formats *timestamp* 
according to the specified format *string*
+| h s | FORMAT_NUMBER(value, decimalVal)             | Formats the number 
*value* like '#,###,###.##', rounded to decimal places *decimalVal*. If 
*decimalVal* is 0, the result has no decimal point or fractional part
+| h s | FORMAT_NUMBER(value, format)                 | Formats the number 
*value* to MySQL's FORMAT *format*, like '#,###,###.##0.00'
 | b | FORMAT_TIME(string, time)                      | Formats *time* 
according to the specified format *string*
 | b | FORMAT_TIMESTAMP(string timestamp)             | Formats *timestamp* 
according to the specified format *string*
 | s | GETBIT(value, position)                        | Equivalent to 
`BIT_GET(value, position)`
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index e5ff71d424..4d251614ce 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -8605,6 +8605,105 @@ public class SqlOperatorTest {
     }
   }
 
+  @Test void testFormatNumber() {
+    final SqlOperatorFixture f0 = 
fixture().setFor(SqlLibraryOperators.FORMAT_NUMBER);
+    f0.checkFails("^format_number(123, 2)^",
+        "No match found for function signature FORMAT_NUMBER\\(<NUMERIC>, 
<NUMERIC>\\)",
+        false);
+    final Consumer<SqlOperatorFixture> consumer = f -> {
+      // test with tinyint type
+      f.checkString("format_number(cast(1 as tinyint), 4)", "1.0000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1 as tinyint), 
'#,###,###,###,###,###,##0.0000')",
+          "1.0000",
+          "VARCHAR NOT NULL");
+
+      // test with smallint type
+      f.checkString("format_number(cast(1 as smallint), 4)", "1.0000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1234 as smallint), 
'#,###,###,###,###,###,##0.0000000')",
+          "1,234.0000000",
+          "VARCHAR NOT NULL");
+
+      // test with integer type
+      f.checkString("format_number(cast(1 as integer), 4)", "1.0000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1234 as integer), 
'#,###,###,###,###,###,##0.0000000')",
+          "1,234.0000000",
+          "VARCHAR NOT NULL");
+
+      // test with bigint type
+      f.checkString("format_number(cast(0 as bigint), 0)", "0",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1 as bigint), 4)", "1.0000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1234 as bigint), 7)", "1,234.0000000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1234 as bigint), 
'#,###,###,###,###,###,##0.0000000')",
+          "1,234.0000000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(-1 as bigint), 4)", "-1.0000",
+          "VARCHAR NOT NULL");
+
+      // test with float type
+      f.checkString("format_number(cast(12332.123456 as float), 4)", 
"12,332.1235",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(123456.123456789 as float), 
'########.###')",
+          "123456.123",
+          "VARCHAR NOT NULL");
+
+      // test with double type
+      f.checkString("format_number(cast(1234567.123456789 as double), 7)", 
"1,234,567.1234568",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(1234567.123456789 as double), 
'##,###,###.##')",
+          "1,234,567.12",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(-0.123456789 as double), 15)", 
"-0.123456789000000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(-0.123456789 as double),"
+              + " '#,###,###,###,###,###,##0.000000000000000')",
+          "-0.123456789000000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(0.000000 as double), 1)", "0.0",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(cast(0.000000 as double), 
'#,###,###,###,###,###,##0.0')",
+          "0.0",
+          "VARCHAR NOT NULL");
+
+      // test with decimal type
+      f.checkString("format_number(1234567.123456789, 7)", "1,234,567.1234568",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(1234567.123456789, '##,###,###.##')",
+          "1,234,567.12",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(-0.123456789, 15)", "-0.123456789000000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(-0.123456789,"
+              + " '#,###,###,###,###,###,##0.000000000000000')",
+          "-0.123456789000000",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(0.000000, 1)", "0.0",
+          "VARCHAR NOT NULL");
+      f.checkString("format_number(0.0, '#,###,###,###,###,###,##0.0000')",
+          "0.0000",
+          "VARCHAR NOT NULL");
+
+      // test with illegal argument
+      f.checkFails("format_number(12332.123456, -1)",
+          "Illegal arguments for 'FORMAT_NUMBER' function:"
+              + " negative decimal value not allowed",
+          true);
+
+      // test with null values
+      f.checkNull("format_number(cast(null as integer), 1)");
+      f.checkNull("format_number(0, cast(null as integer))");
+      f.checkNull("format_number(0, cast(null as varchar))");
+      f.checkNull("format_number(cast(null as integer), cast(null as 
integer))");
+      f.checkNull("format_number(cast(null as integer), cast(null as 
varchar))");
+    };
+    f0.forEachLibrary(list(SqlLibrary.HIVE, SqlLibrary.SPARK), consumer);
+  }
+
   @Test void testTrimFunc() {
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.TRIM, VmName.EXPAND);

Reply via email to