diff --git a/docs/developer-guide/syntax-reference.rst b/docs/developer-guide/syntax-reference.rst index 84b236b98ff8..0f127c524ae2 100644 --- a/docs/developer-guide/syntax-reference.rst +++ b/docs/developer-guide/syntax-reference.rst @@ -1680,7 +1680,14 @@ Scalar functions | REPLACE | ``REPLACE(col1, 'foo', 'bar')`` | Replace all instances of a substring in a string | | | | with a new string. | +------------------------+---------------------------------------------------------------------------+---------------------------------------------------+ -| ROUND | ``ROUND(col1)`` | Round a value to the nearest BIGINT value. | +| ROUND | ``ROUND(col1)`` or ``ROUND(col1, scale)`` | Round a value to the number of decimal places | +| | | as specified by scale to the right of the decimal | +| | | point. If scale is negative then value is rounded | +| | | to the right of the decimal point. | +| | | Numbers equidistant to the nearest value are | +| | | rounded up (in the positive direction). | +| | | If the number of decimal places is not provided | +| | | it defaults to zero. | +------------------------+---------------------------------------------------------------------------+---------------------------------------------------+ | SIGN | ``SIGN(col1)`` | The sign of a numeric value as an INTEGER: | | | | * -1 if the argument is negative | diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java b/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java index fa5b2c8ee908..453d492cc4f5 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java @@ -28,7 +28,6 @@ import io.confluent.ksql.function.udf.json.JsonExtractStringKudf; import io.confluent.ksql.function.udf.math.CeilKudf; import io.confluent.ksql.function.udf.math.RandomKudf; -import io.confluent.ksql.function.udf.math.RoundKudf; import io.confluent.ksql.function.udf.string.ConcatKudf; import io.confluent.ksql.function.udf.string.IfNullKudf; import io.confluent.ksql.function.udf.string.LCaseKudf; @@ -224,12 +223,6 @@ private void addMathFunctions() { "CEIL", CeilKudf.class)); - addBuiltInFunction(KsqlFunction.createLegacyBuiltIn( - Schema.OPTIONAL_INT64_SCHEMA, - Collections.singletonList(Schema.OPTIONAL_FLOAT64_SCHEMA), - "ROUND", - RoundKudf.class)); - addBuiltInFunction(KsqlFunction.createLegacyBuiltIn( Schema.OPTIONAL_FLOAT64_SCHEMA, Collections.emptyList(), diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Abs.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Abs.java index 017f9d9c0980..bf4566a66a2d 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Abs.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Abs.java @@ -55,9 +55,6 @@ public BigDecimal abs(@UdfParameter final BigDecimal val) { @UdfSchemaProvider public SqlType provideSchema(final List params) { - if (params.size() != 1) { - throw new KsqlException("Abs udf accepts one parameter"); - } final SqlType s = params.get(0); if (s.baseType() != SqlBaseType.DECIMAL) { throw new KsqlException("The schema provider method for Abs expects a BigDecimal parameter" diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Round.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Round.java new file mode 100644 index 000000000000..3262075db3bf --- /dev/null +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/Round.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019 Confluent Inc. + * + * Licensed under the Confluent Community License (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.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.ksql.function.udf.math; + +import io.confluent.ksql.function.udf.Udf; +import io.confluent.ksql.function.udf.UdfDescription; +import io.confluent.ksql.function.udf.UdfParameter; +import io.confluent.ksql.function.udf.UdfSchemaProvider; +import io.confluent.ksql.schema.ksql.SqlBaseType; +import io.confluent.ksql.schema.ksql.types.SqlDecimal; +import io.confluent.ksql.schema.ksql.types.SqlType; +import io.confluent.ksql.util.KsqlException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +/* +The rounding behaviour implemented here follows that of java.lang.Math.round() - we do that +in order to provide compatibility with the previous ROUND() implementation which used +Math.round(). The BigDecimal HALF_UP rounding behaviour is a bit more sane and would be a better +choice if we were starting from scratch. + +It's an implementation of rounding "half up". This means we round to the nearest integer value and +in the case we are equidistant from the two nearest integers we round UP to the nearest integer. +This means: + +ROUND(1.1) -> 1 +ROUND(1.5) -> 2 +ROUND(1.9) -> 2 + +ROUND(-1.1) -> -1 +ROUND(-1.5) -> -1 Note this is not -2! We round up and up is in a positive direction. +ROUND(-1.9) -> -2 + +Unfortunately there is an inconsistency in the way that java.lang.Math and +BigDecimal work with respect to rounding: + +Math.round(1.5) --> 2 +Math.round(-1.5) -- -1 + +But: + +new BigDecimal("1.5").setScale(2, RoundingMode.HALF_UP) --> 2 +new BigDecimal("-1.5").setScale(2, RoundingMode.HALF_UP) --> -2 + +There isn't any BigDecimal rounding mode which captures the java.lang.Math behaviour so +we need to use different rounding modes on BigDecimal depending on whether the value +is +ve or -ve to get consistent behaviour. +*/ +@UdfDescription(name = "Round", description = Round.DESCRIPTION) +public class Round { + + static final String DESCRIPTION = + "Round a value to the number of decimal places as specified by scale to the right of the " + + "decimal point. If scale is negative then value is rounded to the right of the decimal " + + "point. Numbers equidistant to the nearest value are rounded up (in the positive" + + " direction). If the number of decimal places is not provided it defaults to zero."; + + @Udf + public Long round(@UdfParameter final long val) { + return val; + } + + @Udf + public Long round(@UdfParameter final int val) { + return (long)val; + } + + @Udf + public Long round(@UdfParameter final Double val) { + return val == null ? null : Math.round(val); + } + + @Udf + public Double round(@UdfParameter final Double val, @UdfParameter final Integer decimalPlaces) { + return val == null + ? null + : roundBigDecimal(BigDecimal.valueOf(val), decimalPlaces).doubleValue(); + } + + @Udf(schemaProvider = "provideDecimalSchema") + public BigDecimal round(@UdfParameter final BigDecimal val) { + return round(val, 0); + } + + @Udf(schemaProvider = "provideDecimalSchemaWithDecimalPlaces") + public BigDecimal round( + @UdfParameter final BigDecimal val, + @UdfParameter final Integer decimalPlaces + ) { + return val == null ? null : roundBigDecimal(val, decimalPlaces); + } + + @UdfSchemaProvider + public SqlType provideDecimalSchemaWithDecimalPlaces(final List params) { + final SqlType s0 = params.get(0); + if (s0.baseType() != SqlBaseType.DECIMAL) { + throw new KsqlException("The schema provider method for round expects a BigDecimal parameter" + + "type as first parameter."); + } + final SqlType s1 = params.get(1); + if (s1.baseType() != SqlBaseType.INTEGER) { + throw new KsqlException("The schema provider method for round expects an Integer parameter" + + "type as second parameter."); + } + return s0; + } + + @UdfSchemaProvider + public SqlType provideDecimalSchema(final List params) { + final SqlType s0 = params.get(0); + if (s0.baseType() != SqlBaseType.DECIMAL) { + throw new KsqlException("The schema provider method for round expects a BigDecimal parameter" + + "type as a parameter."); + } + final SqlDecimal param = (SqlDecimal)s0; + return SqlDecimal.of(param.getPrecision() - param.getScale(), 0); + } + + private BigDecimal roundBigDecimal(final BigDecimal val, final int decimalPlaces) { + final RoundingMode roundingMode = val.compareTo(BigDecimal.ZERO) > 0 + ? RoundingMode.HALF_UP : RoundingMode.HALF_DOWN; + return val.setScale(decimalPlaces, roundingMode); + } +} \ No newline at end of file diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/RoundKudf.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/RoundKudf.java deleted file mode 100644 index 02273e10f52a..000000000000 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/math/RoundKudf.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2018 Confluent Inc. - * - * Licensed under the Confluent Community License (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.confluent.io/confluent-community-license - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.confluent.ksql.function.udf.math; - -import io.confluent.ksql.function.KsqlFunctionException; -import io.confluent.ksql.function.udf.Kudf; - -public class RoundKudf implements Kudf { - - @Override - public Object evaluate(final Object... args) { - if (args.length != 1) { - throw new KsqlFunctionException("Len udf should have one input argument."); - } - return Math.round((Double) args[0]); - } -} diff --git a/ksql-engine/src/test/java/io/confluent/ksql/function/InternalFunctionRegistryTest.java b/ksql-engine/src/test/java/io/confluent/ksql/function/InternalFunctionRegistryTest.java index 2aa257263abd..5467fad761c2 100644 --- a/ksql-engine/src/test/java/io/confluent/ksql/function/InternalFunctionRegistryTest.java +++ b/ksql-engine/src/test/java/io/confluent/ksql/function/InternalFunctionRegistryTest.java @@ -379,7 +379,7 @@ public void shouldHaveBuiltInUDFRegistered() { // String UDF "LCASE", "UCASE", "CONCAT", "TRIM", "IFNULL", "LEN", // Math UDF - "CEIL", "ROUND", "RANDOM", + "CEIL", "RANDOM", // JSON UDF "EXTRACTJSONFIELD", "ARRAYCONTAINS", // Struct UDF diff --git a/ksql-engine/src/test/java/io/confluent/ksql/function/udf/math/RoundTest.java b/ksql-engine/src/test/java/io/confluent/ksql/function/udf/math/RoundTest.java new file mode 100644 index 000000000000..c921340578ee --- /dev/null +++ b/ksql-engine/src/test/java/io/confluent/ksql/function/udf/math/RoundTest.java @@ -0,0 +1,238 @@ +/* + * Copyright 2018 Confluent Inc. + * + * Licensed under the Confluent Community License (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.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.ksql.function.udf.math; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import org.junit.Before; +import org.junit.Test; + +public class RoundTest { + + private Round udf; + + @Before + public void setUp() { + udf = new Round(); + } + + @Test + public void shouldRoundSimpleDoublePositive() { + assertThat(udf.round(0.0d), is(0L)); + assertThat(udf.round(1.23d), is(1L)); + assertThat(udf.round(1.0d), is(1L)); + assertThat(udf.round(1.5d), is(2L)); + assertThat(udf.round(1.75d), is(2L)); + assertThat(udf.round(1.53e6d), is(1530000L)); + assertThat(udf.round(10.01d), is(10L)); + assertThat(udf.round(12345.5d), is(12346L)); + assertThat(udf.round(9.99d), is(10L)); + assertThat(udf.round(110.1), is(110L)); + assertThat(udf.round(1530000.01d), is(1530000L)); + assertThat(udf.round(9999999.99d), is(10000000L)); + } + + @Test + public void shouldRoundSimpleDoubleNegative() { + assertThat(udf.round(-1.23d), is(-1L)); + assertThat(udf.round(-1.0d), is(-1L)); + assertThat(udf.round(-1.5d), is(-1L)); + assertThat(udf.round(-1.75d), is(-2L)); + assertThat(udf.round(-1.53e6d), is(-1530000L)); + assertThat(udf.round(-10.01d), is(-10L)); + assertThat(udf.round(-12345.5d), is(-12345L)); + assertThat(udf.round(-9.99d), is(-10L)); + assertThat(udf.round(-110.1), is(-110L)); + assertThat(udf.round(-1530000.01d), is(-1530000L)); + assertThat(udf.round(-9999999.99d), is(-10000000L)); + } + + @Test + public void shouldRoundSimpleBigDecimalPositive() { + assertThat(udf.round(new BigDecimal("0.0")), is(new BigDecimal("0"))); + assertThat(udf.round(new BigDecimal("1.23")), is(new BigDecimal("1"))); + assertThat(udf.round(new BigDecimal("1.0")), is(new BigDecimal("1"))); + assertThat(udf.round(new BigDecimal("1.5")), is(new BigDecimal("2"))); + assertThat(udf.round(new BigDecimal("1.75")), is(new BigDecimal("2"))); + assertThat(udf.round(new BigDecimal("1530000")), is(new BigDecimal("1530000"))); + assertThat(udf.round(new BigDecimal("10.1")), is(new BigDecimal("10"))); + assertThat(udf.round(new BigDecimal("12345.5")), is(new BigDecimal("12346"))); + assertThat(udf.round(new BigDecimal("9.99")), is(new BigDecimal("10"))); + assertThat(udf.round(new BigDecimal("110.1")), is(new BigDecimal("110"))); + assertThat(udf.round(new BigDecimal("1530000.01")), is(new BigDecimal("1530000"))); + assertThat(udf.round(new BigDecimal("9999999.99")), is(new BigDecimal("10000000"))); + } + + @Test + public void shouldRoundSimpleBigDecimalNegative() { + assertThat(udf.round(new BigDecimal("-1.23")), is(new BigDecimal("-1"))); + assertThat(udf.round(new BigDecimal("-1.0")), is(new BigDecimal("-1"))); + assertThat(udf.round(new BigDecimal("-1.5")), is(new BigDecimal("-1"))); + assertThat(udf.round(new BigDecimal("-1530000")), is(new BigDecimal("-1530000"))); + assertThat(udf.round(new BigDecimal("-10.1")), is(new BigDecimal("-10"))); + assertThat(udf.round(new BigDecimal("-12345.5")), is(new BigDecimal("-12345"))); + assertThat(udf.round(new BigDecimal("-9.99")), is(new BigDecimal("-10"))); + assertThat(udf.round(new BigDecimal("-110.1")), is(new BigDecimal("-110"))); + assertThat(udf.round(new BigDecimal("-1530000.01")), is(new BigDecimal("-1530000"))); + assertThat(udf.round(new BigDecimal("-9999999.99")), is(new BigDecimal("-10000000"))); + } + + @Test + public void shouldRoundDoubleWithDecimalPlacesPositive() { + assertThat(udf.round(0d, 0), is(0d)); + assertThat(udf.round(1.0d, 0), is(1.0d)); + assertThat(udf.round(1.1d, 0), is(1.0d)); + assertThat(udf.round(1.5d, 0), is(2.0d)); + assertThat(udf.round(1.75d, 0), is(2.0d)); + assertThat(udf.round(100.1d, 0), is(100.0d)); + assertThat(udf.round(100.5d, 0), is(101.0d)); + assertThat(udf.round(100.75d, 0), is(101.0d)); + assertThat(udf.round(100.10d, 1), is(100.1d)); + assertThat(udf.round(100.11d, 1), is(100.1d)); + assertThat(udf.round(100.15d, 1), is(100.2d)); + assertThat(udf.round(100.17d, 1), is(100.2d)); + assertThat(udf.round(100.110d, 2), is(100.11d)); + assertThat(udf.round(100.111d, 2), is(100.11d)); + assertThat(udf.round(100.115d, 2), is(100.12d)); + assertThat(udf.round(100.117d, 2), is(100.12d)); + assertThat(udf.round(100.1110d, 3), is(100.111d)); + assertThat(udf.round(100.1111d, 3), is(100.111d)); + assertThat(udf.round(100.1115d, 3), is(100.112d)); + assertThat(udf.round(100.1117d, 3), is(100.112d)); + assertThat(udf.round(12345.67d, -1), is(12350d)); + assertThat(udf.round(12345.67d, -2), is(12300d)); + assertThat(udf.round(12345.67d, -3), is(12000d)); + assertThat(udf.round(12345.67d, -4), is(10000d)); + assertThat(udf.round(12345.67d, -5), is(0d)); + } + + @Test + public void shouldRoundDoubleWithDecimalPlacesNegative() { + assertThat(udf.round(-1.0d, 0), is(-1.0d)); + assertThat(udf.round(-1.1d, 0), is(-1.0d)); + assertThat(udf.round(-1.5d, 0), is(-1.0d)); + assertThat(udf.round(-1.75d, 0), is(-2.0d)); + assertThat(udf.round(-100.1d, 0), is(-100.0d)); + assertThat(udf.round(-100.5d, 0), is(-100.0d)); + assertThat(udf.round(-100.75d, 0), is(-101.0d)); + assertThat(udf.round(-100.10d, 1), is(-100.1d)); + assertThat(udf.round(-100.11d, 1), is(-100.1d)); + assertThat(udf.round(-100.15d, 1), is(-100.1d)); + assertThat(udf.round(-100.17d, 1), is(-100.2d)); + assertThat(udf.round(-100.110d, 2), is(-100.11d)); + assertThat(udf.round(-100.111d, 2), is(-100.11d)); + assertThat(udf.round(-100.115d, 2), is(-100.11d)); + assertThat(udf.round(-100.117d, 2), is(-100.12d)); + assertThat(udf.round(-100.1110d, 3), is(-100.111d)); + assertThat(udf.round(-100.1111d, 3), is(-100.111d)); + assertThat(udf.round(-100.1115d, 3), is(-100.111d)); + assertThat(udf.round(-100.1117d, 3), is(-100.112d)); + assertThat(udf.round(-12345.67d, -1), is(-12350d)); + assertThat(udf.round(-12345.67d, -2), is(-12300d)); + assertThat(udf.round(-12345.67d, -3), is(-12000d)); + assertThat(udf.round(-12345.67d, -4), is(-10000d)); + assertThat(udf.round(-12345.67d, -5), is(0d)); + } + + @Test + public void shouldRoundBigDecimalWithDecimalPlacesPositive() { + assertThat(udf.round(new BigDecimal("0"), 0), is(new BigDecimal("0"))); + assertThat(udf.round(new BigDecimal("1.0"), 0), is(new BigDecimal("1"))); + assertThat(udf.round(new BigDecimal("1.1"), 0), is(new BigDecimal("1"))); + assertThat(udf.round(new BigDecimal("1.5"), 0), is(new BigDecimal("2"))); + assertThat(udf.round(new BigDecimal("1.75"), 0), is(new BigDecimal("2"))); + assertThat(udf.round(new BigDecimal("100.1"), 0),is(new BigDecimal("100"))); + assertThat(udf.round(new BigDecimal("100.5"), 0), is(new BigDecimal("101"))); + assertThat(udf.round(new BigDecimal("100.75"), 0), is(new BigDecimal("101"))); + assertThat(udf.round(new BigDecimal("100.10"), 1), is(new BigDecimal("100.1"))); + assertThat(udf.round(new BigDecimal("100.11"), 1), is(new BigDecimal("100.1"))); + assertThat(udf.round(new BigDecimal("100.15"), 1), is(new BigDecimal("100.2"))); + assertThat(udf.round(new BigDecimal("100.17"), 1), is(new BigDecimal("100.2"))); + assertThat(udf.round(new BigDecimal("100.110"), 2), is(new BigDecimal("100.11"))); + assertThat(udf.round(new BigDecimal("100.111"), 2), is(new BigDecimal("100.11"))); + assertThat(udf.round(new BigDecimal("100.115"), 2), is(new BigDecimal("100.12"))); + assertThat(udf.round(new BigDecimal("100.117"), 2), is(new BigDecimal("100.12"))); + assertThat(udf.round(new BigDecimal("100.1110"), 3), is(new BigDecimal("100.111"))); + assertThat(udf.round(new BigDecimal("100.1111"), 3), is(new BigDecimal("100.111"))); + assertThat(udf.round(new BigDecimal("100.1115"), 3), is(new BigDecimal("100.112"))); + assertThat(udf.round(new BigDecimal("100.1117"), 3), is(new BigDecimal("100.112"))); + assertThat(udf.round(new BigDecimal("12345.67"), -1), is(new BigDecimal("1.235E4"))); + assertThat(udf.round(new BigDecimal("12345.67"), -2), is(new BigDecimal("1.23E4"))); + assertThat(udf.round(new BigDecimal("12345.67"), -3), is(new BigDecimal("1.2E4"))); + assertThat(udf.round(new BigDecimal("12345.67"), -4), is(new BigDecimal("1E4"))); + assertThat(udf.round(new BigDecimal("12345.67"), -5), is(new BigDecimal("0E5"))); + } + + @Test + public void shouldRoundBigDecimalWithDecimalPlacesNegative() { + assertThat(udf.round(new BigDecimal("-1.0"), 0), is(new BigDecimal("-1"))); + assertThat(udf.round(new BigDecimal("-1.1"), 0), is(new BigDecimal("-1"))); + assertThat(udf.round(new BigDecimal("-1.5"), 0), is(new BigDecimal("-1"))); + assertThat(udf.round(new BigDecimal("-1.75"), 0), is(new BigDecimal("-2"))); + assertThat(udf.round(new BigDecimal("-100.1"), 0), is(new BigDecimal("-100"))); + assertThat(udf.round(new BigDecimal("-100.5"), 0), is(new BigDecimal("-100"))); + assertThat(udf.round(new BigDecimal("-100.75"), 0), is(new BigDecimal("-101"))); + assertThat(udf.round(new BigDecimal("-100.10"), 1), is(new BigDecimal("-100.1"))); + assertThat(udf.round(new BigDecimal("-100.11"), 1), is(new BigDecimal("-100.1"))); + assertThat(udf.round(new BigDecimal("-100.15"), 1), is(new BigDecimal("-100.1"))); + assertThat(udf.round(new BigDecimal("-100.17"), 1), is(new BigDecimal("-100.2"))); + assertThat(udf.round(new BigDecimal("-100.110"), 2), is(new BigDecimal("-100.11"))); + assertThat(udf.round(new BigDecimal("-100.111"), 2), is(new BigDecimal("-100.11"))); + assertThat(udf.round(new BigDecimal("-100.115"), 2), is(new BigDecimal("-100.11"))); + assertThat(udf.round(new BigDecimal("-100.117"), 2), is(new BigDecimal("-100.12"))); + assertThat(udf.round(new BigDecimal("-100.1110"), 3), is(new BigDecimal("-100.111"))); + assertThat(udf.round(new BigDecimal("-100.1111"), 3), is(new BigDecimal("-100.111"))); + assertThat(udf.round(new BigDecimal("-100.1115"), 3), is(new BigDecimal("-100.111"))); + assertThat(udf.round(new BigDecimal("-100.1117"), 3), is(new BigDecimal("-100.112"))); + assertThat(udf.round(new BigDecimal("-12345.67"), -1), is(new BigDecimal("-1.235E4"))); + assertThat(udf.round(new BigDecimal("-12345.67"), -2), is(new BigDecimal("-1.23E4"))); + assertThat(udf.round(new BigDecimal("-12345.67"), -3), is(new BigDecimal("-1.2E4"))); + assertThat(udf.round(new BigDecimal("-12345.67"), -4), is(new BigDecimal("-1E4"))); + assertThat(udf.round(new BigDecimal("-12345.67"), -5), is(new BigDecimal("-0E5"))); + } + + @Test + public void shouldHandleDoubleLiteralsEndingWith5ThatCannotBeRepresentedExactylyAsDoubles() { + assertThat(udf.round(new BigDecimal("265.335"), 2), is(new BigDecimal("265.34"))); + assertThat(udf.round(new BigDecimal("-265.335"), 2), is(new BigDecimal("-265.33"))); + + assertThat(udf.round(new BigDecimal("265.365"), 2), is(new BigDecimal("265.37"))); + assertThat(udf.round(new BigDecimal("-265.365"), 2), is(new BigDecimal("-265.36"))); + } + + @Test + public void shoulldHandleNullValues() { + assertThat(udf.round((Double)null), is((Long)null)); + assertThat(udf.round((BigDecimal) null), is((BigDecimal) null)); + assertThat(udf.round((Double)null, 2), is((Long)null)); + assertThat(udf.round((BigDecimal) null, 2), is((BigDecimal) null)); + } + + @Test + public void shouldRoundInt() { + assertThat(udf.round(123), is(123L)); + } + + @Test + public void shouldRoundLong() { + assertThat(udf.round(123L), is(123L)); + } + +} \ No newline at end of file diff --git a/ksql-functional-tests/src/test/resources/query-validation-tests/math.json b/ksql-functional-tests/src/test/resources/query-validation-tests/math.json index ec50d307067b..f48ce60b5ea3 100644 --- a/ksql-functional-tests/src/test/resources/query-validation-tests/math.json +++ b/ksql-functional-tests/src/test/resources/query-validation-tests/math.json @@ -87,15 +87,58 @@ ], "inputs": [ {"topic": "input", "value": {"i": null, "l": null, "d": null}}, - {"topic": "input", "value": {"i": -1, "l": -2, "d": -3.1, "b": "-3.1"}}, {"topic": "input", "value": {"i": 0, "l": 0, "d": 0.0, "b": "0.0"}}, - {"topic": "input", "value": {"i": 1, "l": 2, "d": 3.1, "b": "3.1"}} + {"topic": "input", "value": {"i": 1, "l": 1, "d": 1.0, "b": "1.0"}}, + {"topic": "input", "value": {"i": 1, "l": 1, "d": 1.1, "b": "1.1"}}, + {"topic": "input", "value": {"i": 1, "l": 1, "d": 1.5, "b": "1.5"}}, + {"topic": "input", "value": {"i": 1, "l": 1, "d": 1.7, "b": "1.7"}}, + {"topic": "input", "value": {"i": 1, "l": 1, "d": 2.0, "b": "2.0"}}, + + {"topic": "input", "value": {"i": -1, "l": -1, "d": -1.0, "b": "-1.0"}}, + {"topic": "input", "value": {"i": -1, "l": -1, "d": -1.1, "b": "-1.1"}}, + {"topic": "input", "value": {"i": -1, "l": -1, "d": -1.5, "b": "-1.5"}}, + {"topic": "input", "value": {"i": -1, "l": -1, "d": -1.7, "b": "-1.7"}}, + {"topic": "input", "value": {"i": -1, "l": -1, "d": -2.0, "b": "-2.0"}} ], "outputs": [ {"topic": "OUTPUT", "value": {"I": null, "L": null, "D": null, "B": null}}, - {"topic": "OUTPUT", "value": {"I": -1.0, "L": -2.0, "D": -4.0, "B": -4.0}}, {"topic": "OUTPUT", "value": {"I": 0.0, "L": 0.0, "D": 0.0, "B": 0.0}}, - {"topic": "OUTPUT", "value": {"I": 1.0, "L": 2.0, "D": 3.0, "B": 3.0}} + {"topic": "OUTPUT", "value": {"I": 1.0, "L": 1.0, "D": 1.0, "B": 1.0}}, + {"topic": "OUTPUT", "value": {"I": 1.0, "L": 1.0, "D": 1.0, "B": 1.0}}, + {"topic": "OUTPUT", "value": {"I": 1.0, "L": 1.0, "D": 1.0, "B": 1.0}}, + {"topic": "OUTPUT", "value": {"I": 1.0, "L": 1.0, "D": 1.0, "B": 1.0}}, + {"topic": "OUTPUT", "value": {"I": 1.0, "L": 1.0, "D": 2.0, "B": 2.0}}, + + {"topic": "OUTPUT", "value": {"I": -1.0, "L": -1.0, "D": -1.0, "B": -1.0}}, + {"topic": "OUTPUT", "value": {"I": -1.0, "L": -1.0, "D": -2.0, "B": -2.0}}, + {"topic": "OUTPUT", "value": {"I": -1.0, "L": -1.0, "D": -2.0, "B": -2.0}}, + {"topic": "OUTPUT", "value": {"I": -1.0, "L": -1.0, "D": -2.0, "B": -2.0}}, + {"topic": "OUTPUT", "value": {"I": -1.0, "L": -1.0, "D": -2.0, "B": -2.0}} + ] + }, + { + "name": "calculate CEIL function", + "statements": [ + "CREATE STREAM test (v DOUBLE) WITH (kafka_topic='test_topic', value_format='JSON');", + "CREATE STREAM OUTPUT AS SELECT CEIL(v) as C0 FROM test;" + ], + "inputs": [ + {"topic": "test_topic", "value": {"v" : 1.2}}, + {"topic": "test_topic", "value": {"v" : 1.7}}, + {"topic": "test_topic", "value": {"v" : 1.5}}, + {"topic": "test_topic", "value": {"v" : 3}}, + {"topic": "test_topic", "value": {"v" : 1.234567}}, + {"topic": "test_topic", "value": {"v" : 0}}, + {"topic": "test_topic", "value": {"v" : null}} + ], + "outputs": [ + {"topic": "OUTPUT", "value": {"C0" : 2.0}}, + {"topic": "OUTPUT", "value": {"C0" : 2.0}}, + {"topic": "OUTPUT", "value": {"C0" : 2.0}}, + {"topic": "OUTPUT", "value": {"C0" : 3.0}}, + {"topic": "OUTPUT", "value": {"C0" : 2.0}}, + {"topic": "OUTPUT", "value": {"C0" : 0.0}}, + {"topic": "OUTPUT", "value": {"C0" : null}} ] }, { @@ -116,6 +159,68 @@ {"topic": "OUTPUT", "value": {"I": 0.0, "L": 0.0, "D": 0.0, "B": "0.0"}}, {"topic": "OUTPUT", "value": {"I": 1.0, "L": 2.0, "D": 3.3, "B": "3.4"}} ] + }, + { + "name": "round", + "statements": [ + "CREATE STREAM test (v DOUBLE) WITH (kafka_topic='test_topic', value_format='JSON');", + "CREATE STREAM OUTPUT AS SELECT ROUND(v) as R0, ROUND(v, 0) as R00, ROUND(v, 1) as R1, ROUND(v, 2) as R2, ROUND(v, 10) as R10, ROUND(v , -1) as 1R , ROUND(v , -2) as 2R FROM test;" + ], + "inputs": [ + {"topic": "test_topic", "value": {"v" : 1.2}}, + {"topic": "test_topic", "value": {"v" : 1.7}}, + {"topic": "test_topic", "value": {"v" : 1.5}}, + {"topic": "test_topic", "value": {"v" : 3}}, + {"topic": "test_topic", "value": {"v" : 1.234567}}, + {"topic": "test_topic", "value": {"v" : 0}}, + {"topic": "test_topic", "value": {"v" : 111}}, + {"topic": "test_topic", "value": {"v" : null}}, + {"topic": "test_topic", "value": {"v" : -1.1}}, + {"topic": "test_topic", "value": {"v" : -1.5}}, + {"topic": "test_topic", "value": {"v" : -1.7}}, + {"topic": "test_topic", "value": {"v" : 12345.678}}, + {"topic": "test_topic", "value": {"v" : -12345.678}} + ], + "outputs": [ + {"topic": "OUTPUT", "value": {"R0" : 1, "R00" : 1.0, "R1" : 1.2, "R2" : 1.2, "R10" : 1.2, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 2, "R00" : 2.0, "R1" : 1.7, "R2" : 1.7, "R10" : 1.7, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 2, "R00" : 2.0, "R1" : 1.5, "R2" : 1.5, "R10" : 1.5, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 3, "R00" : 3.0, "R1" : 3.0, "R2" : 3.0, "R10" : 3.0, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 1, "R00" : 1.0, "R1" : 1.2, "R2" : 1.23, "R10" : 1.234567,"1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 0, "R00" : 0.0, "R1" : 0.0, "R2" : 0.0, "R10" : 0.0, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 111, "R00" : 111.0, "R1" : 111.0, "R2" : 111.0, "R10" : 111.0, "1R" : 110.0 ,"2R" : 100.0 }}, + {"topic": "OUTPUT", "value": {"R0" : null, "R00" : null, "R1" : null, "R2" : null, "R10" : null,"1R" : null ,"2R" : null }}, + {"topic": "OUTPUT", "value": {"R0" : -1, "R00" : -1.0, "R1" : -1.1, "R2" : -1.1, "R10" : -1.1, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : -1, "R00" : -1.0, "R1" : -1.5, "R2" : -1.5, "R10" : -1.5, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : -2, "R00" : -2.0, "R1" : -1.7, "R2" : -1.7, "R10" : -1.7, "1R" : 0.0 ,"2R" : 0.0 }}, + {"topic": "OUTPUT", "value": {"R0" : 12346, "R00" : 12346.0, "R1" : 12345.7, "R2" : 12345.68, "R10" : 12345.678, "1R" : 12350.0 ,"2R" : 12300.0 }}, + {"topic": "OUTPUT", "value": {"R0" : -12346, "R00" : -12346.0, "R1" : -12345.7, "R2" : -12345.68, "R10" : -12345.678, "1R" : -12350.0 ,"2R" : -12300.0 }} + ] + }, + { + "name": "round with large DECIMAL values", + "statements": [ + "CREATE STREAM test (v DECIMAL(33, 16)) WITH (kafka_topic='test_topic', value_format='DELIMITED');", + "CREATE STREAM OUTPUT AS SELECT ROUND(v) as R0, ROUND(v, 0) as R00, ROUND(v, 1) as R1, ROUND(v, 2) as R2, ROUND(v, 10) as R10, ROUND(v , -1) as 1R , ROUND(v , -2) as 2R FROM test;" + ], + "inputs": [ + {"topic": "test_topic", "value": "12345678987654321.2345678987654321"} + ], + "outputs": [ + { + "topic": "OUTPUT", + "value": "\"12,345,678,987,654,321\",\"12,345,678,987,654,321.0000000000000000\",\"12,345,678,987,654,321.2000000000000000\",\"12,345,678,987,654,321.2300000000000000\",\"12,345,678,987,654,321.2345678988000000\",\"12,345,678,987,654,320.0000000000000000\",\"12,345,678,987,654,300.0000000000000000\"" + } + ], + "post": { + "sources": [ + { + "name": "OUTPUT", + "type": "stream", + "valueSchema": "STRUCT" + } + ] + } } ] } \ No newline at end of file