diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java index e33eb315e7..34a648b385 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java @@ -56,6 +56,21 @@ public static LiteralExpression literal(ExprValue value) { return new LiteralExpression(value); } + /** + * Wrap a number to {@link LiteralExpression}. + */ + public static LiteralExpression literal(Number value) { + if (value instanceof Integer) { + return new LiteralExpression(ExprValueUtils.integerValue(value.intValue())); + } else if (value instanceof Long) { + return new LiteralExpression(ExprValueUtils.longValue(value.longValue())); + } else if (value instanceof Float) { + return new LiteralExpression(ExprValueUtils.floatValue(value.floatValue())); + } else { + return new LiteralExpression(ExprValueUtils.doubleValue(value.doubleValue())); + } + } + public static ReferenceExpression ref(String ref, ExprType type) { return new ReferenceExpression(ref, type); } @@ -144,6 +159,46 @@ public FunctionExpression truncate(Expression... expressions) { return function(BuiltinFunctionName.TRUNCATE, expressions); } + public FunctionExpression acos(Expression... expressions) { + return function(BuiltinFunctionName.ACOS, expressions); + } + + public FunctionExpression asin(Expression... expressions) { + return function(BuiltinFunctionName.ASIN, expressions); + } + + public FunctionExpression atan(Expression... expressions) { + return function(BuiltinFunctionName.ATAN, expressions); + } + + public FunctionExpression atan2(Expression... expressions) { + return function(BuiltinFunctionName.ATAN2, expressions); + } + + public FunctionExpression cos(Expression... expressions) { + return function(BuiltinFunctionName.COS, expressions); + } + + public FunctionExpression cot(Expression... expressions) { + return function(BuiltinFunctionName.COT, expressions); + } + + public FunctionExpression degrees(Expression... expressions) { + return function(BuiltinFunctionName.DEGREES, expressions); + } + + public FunctionExpression radians(Expression... expressions) { + return function(BuiltinFunctionName.RADIANS, expressions); + } + + public FunctionExpression sin(Expression... expressions) { + return function(BuiltinFunctionName.SIN, expressions); + } + + public FunctionExpression tan(Expression... expressions) { + return function(BuiltinFunctionName.TAN, expressions); + } + public FunctionExpression add(Expression... expressions) { return function(BuiltinFunctionName.ADD, expressions); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java index bf39c5005f..a17b5d65d2 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java @@ -37,6 +37,17 @@ public enum BuiltinFunctionName { SQRT(FunctionName.of("sqrt")), TRUNCATE(FunctionName.of("truncate")), + ACOS(FunctionName.of("acos")), + ASIN(FunctionName.of("asin")), + ATAN(FunctionName.of("atan")), + ATAN2(FunctionName.of("atan2")), + COS(FunctionName.of("cos")), + COT(FunctionName.of("cot")), + DEGREES(FunctionName.of("degrees")), + RADIANS(FunctionName.of("radians")), + SIN(FunctionName.of("sin")), + TAN(FunctionName.of("tan")), + /** * Text Functions. */ diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunction.java index 69972d3255..0ba0b6dbcb 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunction.java @@ -69,6 +69,16 @@ public static void register(BuiltinFunctionRepository repository) { repository.register(truncate()); repository.register(pi()); repository.register(rand()); + repository.register(acos()); + repository.register(asin()); + repository.register(atan()); + repository.register(atan2()); + repository.register(cos()); + repository.register(cot()); + repository.register(degrees()); + repository.register(radians()); + repository.register(sin()); + repository.register(tan()); } /** @@ -487,6 +497,154 @@ private static FunctionResolver truncate() { .build()); } + /** + * Definition of acos(x) function. + * Calculates the arc cosine of x, that is, the value whose cosine is x. + * Returns NULL if x is not in the range -1 to 1. + * The supported signature of acos function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver acos() { + FunctionName functionName = BuiltinFunctionName.ACOS.getName(); + return new FunctionResolver( + functionName, + singleArgumentFunction(functionName, v -> v < -1 || v > 1 ? null : Math.acos(v))); + } + + /** + * Definition of asin(x) function. + * Calculates the arc sine of x, that is, the value whose sine is x. + * Returns NULL if x is not in the range -1 to 1. + * The supported signature of asin function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver asin() { + FunctionName functionName = BuiltinFunctionName.ASIN.getName(); + return new FunctionResolver( + functionName, + singleArgumentFunction(functionName, v -> v < -1 || v > 1 ? null : Math.asin(v))); + } + + /** + * Definition of atan(x) and atan(y, x) function. + * atan(x) calculates the arc tangent of x, that is, the value whose tangent is x. + * atan(y, x) calculates the arc tangent of y / x, except that the signs of both arguments + * are used to determine the quadrant of the result. + * The supported signature of atan function is + * (x: INTEGER/LONG/FLOAT/DOUBLE, y: INTEGER/LONG/FLOAT/DOUBLE) -> DOUBLE + */ + private static FunctionResolver atan() { + FunctionName functionName = BuiltinFunctionName.ATAN.getName(); + return new FunctionResolver(functionName, + new ImmutableMap.Builder() + .put( + new FunctionSignature(functionName, Arrays.asList(ExprCoreType.DOUBLE)), + unaryOperator( + functionName, Math::atan, ExprValueUtils::getDoubleValue, ExprCoreType.DOUBLE)) + .put( + new FunctionSignature( + functionName, Arrays.asList(ExprCoreType.DOUBLE, ExprCoreType.DOUBLE)), + doubleArgFunc(functionName, + Math::atan2, ExprValueUtils::getDoubleValue, ExprValueUtils::getDoubleValue, + ExprCoreType.DOUBLE)) + .build()); + } + + /** + * Definition of atan2(y, x) function. + * Calculates the arc tangent of y / x, except that the signs of both arguments + * are used to determine the quadrant of the result. + * The supported signature of atan2 function is + * (x: INTEGER/LONG/FLOAT/DOUBLE, y: INTEGER/LONG/FLOAT/DOUBLE) -> DOUBLE + */ + private static FunctionResolver atan2() { + FunctionName functionName = BuiltinFunctionName.ATAN2.getName(); + return new FunctionResolver(functionName, + new ImmutableMap.Builder() + .put( + new FunctionSignature( + functionName, Arrays.asList(ExprCoreType.DOUBLE, ExprCoreType.DOUBLE)), + doubleArgFunc(functionName, + Math::atan2, ExprValueUtils::getDoubleValue, ExprValueUtils::getDoubleValue, + ExprCoreType.DOUBLE)) + .build()); + } + + /** + * Definition of cos(x) function. + * Calculates the cosine of X, where X is given in radians + * The supported signature of cos function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver cos() { + FunctionName functionName = BuiltinFunctionName.COS.getName(); + return new FunctionResolver(functionName, singleArgumentFunction(functionName, Math::cos)); + } + + /** + * Definition of cot(x) function. + * Calculates the cotangent of x + * The supported signature of cot function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver cot() { + FunctionName functionName = BuiltinFunctionName.COT.getName(); + return new FunctionResolver( + functionName, + singleArgumentFunction(functionName, v -> { + if (v == 0) { + throw new ArithmeticException(String.format("Out of range value for cot(%s)", v)); + } + return 1 / Math.tan(v); + })); + } + + /** + * Definition of degrees(x) function. + * Converts x from radians to degrees + * The supported signature of degrees function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver degrees() { + FunctionName functionName = BuiltinFunctionName.DEGREES.getName(); + return new FunctionResolver( + functionName, singleArgumentFunction(functionName, Math::toDegrees)); + } + + /** + * Definition of radians(x) function. + * Converts x from degrees to radians + * The supported signature of radians function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver radians() { + FunctionName functionName = BuiltinFunctionName.RADIANS.getName(); + return new FunctionResolver( + functionName, singleArgumentFunction(functionName, Math::toRadians)); + } + + /** + * Definition of sin(x) function. + * Calculates the sine of x, where x is given in radians + * The supported signature of sin function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver sin() { + FunctionName functionName = BuiltinFunctionName.SIN.getName(); + return new FunctionResolver(functionName, singleArgumentFunction(functionName, Math::sin)); + } + + /** + * Definition of tan(x) function. + * Calculates the tangent of x, where x is given in radians + * The supported signature of tan function is + * INTEGER/LONG/FLOAT/DOUBLE -> DOUBLE + */ + private static FunctionResolver tan() { + FunctionName functionName = BuiltinFunctionName.TAN.getName(); + return new FunctionResolver(functionName, singleArgumentFunction(functionName, Math::tan)); + } + /** * Util method to generate single argument function bundles. Applicable for INTEGER -> INTEGER * LONG -> LONG FLOAT -> FLOAT DOUBLE -> DOUBLE diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunctionTest.java index 6e5f958a34..bf1cf97ead 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/arthmetic/MathematicalFunctionTest.java @@ -34,6 +34,7 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.closeTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; @@ -74,6 +75,20 @@ private static Stream testLogDoubleArguments() { return builder.add(Arguments.of(2D, 2D)).build(); } + private static Stream trigonometricArguments() { + Stream.Builder builder = Stream.builder(); + return builder + .add(Arguments.of(1)).add(Arguments.of(1L)).add(Arguments.of(1F)).add(Arguments.of(1D)) + .build(); + } + + private static Stream trigonometricDoubleArguments() { + Stream.Builder builder = Stream.builder(); + return builder + .add(Arguments.of(1, 2)).add(Arguments.of(1L, 2L)).add(Arguments.of(1F, 2F)) + .add(Arguments.of(1D, 2D)).build(); + } + /** * Test abs with integer value. */ @@ -1767,4 +1782,462 @@ public void rand_null_value() { assertEquals(FLOAT, rand.type()); assertTrue(rand.valueOf(valueEnv()).isNull()); } + + /** + * Test acos with integer, long, float, double values. + */ + @ParameterizedTest(name = "acos({0})") + @MethodSource("trigonometricArguments") + public void test_acos(Number value) { + FunctionExpression acos = dsl.acos(DSL.literal(value)); + assertThat( + acos.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.acos(value.doubleValue())))); + assertEquals(String.format("acos(%s)", value), acos.toString()); + } + + /** + * Test acos with illegal values. + */ + @ParameterizedTest(name = "acos({0})") + @ValueSource(doubles = {2D, -2D}) + public void acos_with_illegal_value(Number value) { + FunctionExpression acos = dsl.acos(DSL.literal(value)); + assertEquals(DOUBLE, acos.type()); + assertTrue(acos.valueOf(valueEnv()).isNull()); + } + + /** + * Test acos with null value. + */ + @Test + public void acos_null_value() { + FunctionExpression acos = dsl.acos(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, acos.type()); + assertTrue(acos.valueOf(valueEnv()).isNull()); + } + + /** + * Test acos with missing value. + */ + @Test + public void acos_missing_value() { + FunctionExpression acos = dsl.acos(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, acos.type()); + assertTrue(acos.valueOf(valueEnv()).isMissing()); + } + + /** + * Test asin with integer, long, float, double values. + */ + @ParameterizedTest(name = "asin({0})") + @MethodSource("trigonometricArguments") + public void test_asin(Number value) { + FunctionExpression asin = dsl.asin(DSL.literal(value)); + assertThat( + asin.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.asin(value.doubleValue())))); + assertEquals(String.format("asin(%s)", value), asin.toString()); + } + + /** + * Test asin with illegal value. + */ + @ParameterizedTest(name = "asin({0})") + @ValueSource(doubles = {2D, -2D}) + public void asin_with_illegal_value(Number value) { + FunctionExpression asin = dsl.asin(DSL.literal(value)); + assertEquals(DOUBLE, asin.type()); + assertTrue(asin.valueOf(valueEnv()).isNull()); + } + + /** + * Test asin with null value. + */ + @Test + public void asin_null_value() { + FunctionExpression asin = dsl.asin(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, asin.type()); + assertTrue(asin.valueOf(valueEnv()).isNull()); + } + + /** + * Test asin with missing value. + */ + @Test + public void asin_missing_value() { + FunctionExpression asin = dsl.asin(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, asin.type()); + assertTrue(asin.valueOf(valueEnv()).isMissing()); + } + + /** + * Test atan with one argument integer, long, float, double values. + */ + @ParameterizedTest(name = "atan({0})") + @MethodSource("trigonometricArguments") + public void atan_one_arg(Number value) { + FunctionExpression atan = dsl.atan(DSL.literal(value)); + assertThat( + atan.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.atan(value.doubleValue())))); + assertEquals(String.format("atan(%s)", value), atan.toString()); + } + + /** + * Test atan with two arguments of integer, long, float, double values. + */ + @ParameterizedTest(name = "atan({0}, {1})") + @MethodSource("trigonometricDoubleArguments") + public void atan_two_args(Number v1, Number v2) { + FunctionExpression atan = + dsl.atan(DSL.literal(v1), DSL.literal(v2)); + assertThat( + atan.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.atan2(v1.doubleValue(), v2.doubleValue())))); + assertEquals(String.format("atan(%s, %s)", v1, v2), atan.toString()); + } + + /** + * Test atan with null value. + */ + @Test + public void atan_null_value() { + FunctionExpression atan = dsl.atan(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isNull()); + + atan = dsl.atan(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE), DSL.literal(1)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isNull()); + + atan = dsl.atan(DSL.literal(1), DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isNull()); + + atan = dsl.atan(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isNull()); + } + + /** + * Test atan with missing value. + */ + @Test + public void atan_missing_value() { + FunctionExpression atan = dsl.atan(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isMissing()); + + atan = dsl.atan(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE), DSL.literal(1)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isMissing()); + + atan = dsl.atan(DSL.literal(1), DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isMissing()); + + atan = dsl.atan(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isMissing()); + } + + /** + * Test atan with missing value. + */ + @Test + public void atan_null_missing() { + FunctionExpression atan = dsl.atan( + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isMissing()); + + atan = dsl.atan(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan.type()); + assertTrue(atan.valueOf(valueEnv()).isMissing()); + } + + /** + * Test atan2 with integer, long, float, double values. + */ + @ParameterizedTest(name = "atan2({0}, {1})") + @MethodSource("trigonometricDoubleArguments") + public void test_atan2(Number v1, Number v2) { + FunctionExpression atan2 = dsl.atan2(DSL.literal(v1), DSL.literal(v2)); + assertThat( + atan2.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.atan2(v1.doubleValue(), v2.doubleValue())))); + assertEquals(String.format("atan2(%s, %s)", v1, v2), atan2.toString()); + } + + /** + * Test atan2 with null value. + */ + @Test + public void atan2_null_value() { + FunctionExpression atan2 = dsl.atan2( + DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE), DSL.literal(1)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isNull()); + + atan2 = dsl.atan2(DSL.literal(1), DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isNull()); + + atan2 = dsl.atan2(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isNull()); + } + + /** + * Test atan2 with missing value. + */ + @Test + public void atan2_missing_value() { + FunctionExpression atan2 = dsl.atan2( + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE), DSL.literal(1)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isMissing()); + + atan2 = dsl.atan2(DSL.literal(1), DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isMissing()); + + atan2 = dsl.atan2(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isMissing()); + } + + /** + * Test atan2 with missing value. + */ + @Test + public void atan2_null_missing() { + FunctionExpression atan2 = dsl.atan2( + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isMissing()); + + atan2 = dsl.atan2(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE), + DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, atan2.type()); + assertTrue(atan2.valueOf(valueEnv()).isMissing()); + } + + /** + * Test cos with integer, long, float, double values. + */ + @ParameterizedTest(name = "cos({0})") + @MethodSource("trigonometricArguments") + public void test_cos(Number value) { + FunctionExpression cos = dsl.cos(DSL.literal(value)); + assertThat( + cos.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.cos(value.doubleValue())))); + assertEquals(String.format("cos(%s)", value), cos.toString()); + } + + /** + * Test cos with null value. + */ + @Test + public void cos_null_value() { + FunctionExpression cos = dsl.cos(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, cos.type()); + assertTrue(cos.valueOf(valueEnv()).isNull()); + } + + /** + * Test cos with missing value. + */ + @Test + public void cos_missing_value() { + FunctionExpression cos = dsl.cos(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, cos.type()); + assertTrue(cos.valueOf(valueEnv()).isMissing()); + } + + /** + * Test cot with integer, long, float, double values. + */ + @ParameterizedTest(name = "cot({0})") + @MethodSource("trigonometricArguments") + public void test_cot(Number value) { + FunctionExpression cot = dsl.cot(DSL.literal(value)); + assertThat( + cot.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(1 / Math.tan(value.doubleValue())))); + assertEquals(String.format("cot(%s)", value), cot.toString()); + } + + /** + * Test cot with out-of-range value 0. + */ + @ParameterizedTest(name = "cot({0})") + @ValueSource(doubles = {0}) + public void cot_with_zero(Number value) { + FunctionExpression cot = dsl.cot(DSL.literal(value)); + assertThrows( + ArithmeticException.class, () -> cot.valueOf(valueEnv()), + String.format("Out of range value for cot(%s)", value)); + } + + /** + * Test cot with null value. + */ + @Test + public void cot_null_value() { + FunctionExpression cot = dsl.cot(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, cot.type()); + assertTrue(cot.valueOf(valueEnv()).isNull()); + } + + /** + * Test cot with missing value. + */ + @Test + public void cot_missing_value() { + FunctionExpression cot = dsl.cot(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, cot.type()); + assertTrue(cot.valueOf(valueEnv()).isMissing()); + } + + /** + * Test degrees with integer, long, float, double values. + */ + @ParameterizedTest(name = "degrees({0})") + @MethodSource("trigonometricArguments") + public void test_degrees(Number value) { + FunctionExpression degrees = dsl.degrees(DSL.literal(value)); + assertThat( + degrees.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.toDegrees(value.doubleValue())))); + assertEquals(String.format("degrees(%s)", value), degrees.toString()); + } + + /** + * Test degrees with null value. + */ + @Test + public void degrees_null_value() { + FunctionExpression degrees = dsl.degrees(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, degrees.type()); + assertTrue(degrees.valueOf(valueEnv()).isNull()); + } + + /** + * Test degrees with missing value. + */ + @Test + public void degrees_missing_value() { + FunctionExpression degrees = dsl.degrees(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, degrees.type()); + assertTrue(degrees.valueOf(valueEnv()).isMissing()); + } + + /** + * Test radians with integer, long, float, double values. + */ + @ParameterizedTest(name = "radians({0})") + @MethodSource("trigonometricArguments") + public void test_radians(Number value) { + FunctionExpression radians = dsl.radians(DSL.literal(value)); + assertThat( + radians.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.toRadians(value.doubleValue())))); + assertEquals(String.format("radians(%s)", value), radians.toString()); + } + + /** + * Test radians with null value. + */ + @Test + public void radians_null_value() { + FunctionExpression radians = dsl.radians(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, radians.type()); + assertTrue(radians.valueOf(valueEnv()).isNull()); + } + + /** + * Test radians with missing value. + */ + @Test + public void radians_missing_value() { + FunctionExpression radians = dsl.radians(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, radians.type()); + assertTrue(radians.valueOf(valueEnv()).isMissing()); + } + + /** + * Test sin with integer, long, float, double values. + */ + @ParameterizedTest(name = "sin({0})") + @MethodSource("trigonometricArguments") + public void test_sin(Number value) { + FunctionExpression sin = dsl.sin(DSL.literal(value)); + assertThat( + sin.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.sin(value.doubleValue())))); + assertEquals(String.format("sin(%s)", value), sin.toString()); + } + + /** + * Test sin with null value. + */ + @Test + public void sin_null_value() { + FunctionExpression sin = dsl.sin(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, sin.type()); + assertTrue(sin.valueOf(valueEnv()).isNull()); + } + + /** + * Test sin with missing value. + */ + @Test + public void sin_missing_value() { + FunctionExpression sin = dsl.sin(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, sin.type()); + assertTrue(sin.valueOf(valueEnv()).isMissing()); + } + + /** + * Test tan with integer, long, float, double values. + */ + @ParameterizedTest(name = "tan({0})") + @MethodSource("trigonometricArguments") + public void test_tan(Number value) { + FunctionExpression tan = dsl.tan(DSL.literal(value)); + assertThat( + tan.valueOf(valueEnv()), + allOf(hasType(DOUBLE), hasValue(Math.tan(value.doubleValue())))); + assertEquals(String.format("tan(%s)", value), tan.toString()); + } + + /** + * Test tan with null value. + */ + @Test + public void tan_null_value() { + FunctionExpression tan = dsl.tan(DSL.ref(DOUBLE_TYPE_NULL_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, tan.type()); + assertTrue(tan.valueOf(valueEnv()).isNull()); + } + + /** + * Test tan with missing value. + */ + @Test + public void tan_missing_value() { + FunctionExpression tan = dsl.tan(DSL.ref(DOUBLE_TYPE_MISSING_VALUE_FIELD, DOUBLE)); + assertEquals(DOUBLE, tan.type()); + assertTrue(tan.valueOf(valueEnv()).isMissing()); + } } diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index dea0bbcf09..a8d97fde7c 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -33,9 +33,21 @@ ACOS Description ----------- -Specifications: +Usage: acos(x) calculate the arc cosine of x. Returns NULL if x is not in the range -1 to 1. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE -1. ACOS(NUMBER T) -> DOUBLE +Return type: DOUBLE + +Example:: + + od> SELECT ACOS(0) + fetched rows / total rows = 1/1 + +--------------------+ + | acos(0) | + |--------------------| + | 1.5707963267948966 | + +--------------------+ ADD @@ -66,9 +78,21 @@ ASIN Description ----------- -Specifications: +Usage: asin(x) calculate the arc sine of x. Returns NULL if x is not in the range -1 to 1. -1. ASIN(NUMBER T) -> DOUBLE +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type: DOUBLE + +Example:: + + od> SELECT ASIN(0) + fetched rows / total rows = 1/1 + +-----------+ + | asin(0) | + |-----------| + | 0 | + +-----------+ ATAN @@ -77,9 +101,21 @@ ATAN Description ----------- -Specifications: +Usage: atan(x) calculates the arc tangent of x. atan(y, x) calculates the arc tangent of y / x, except that the signs of both arguments are used to determine the quadrant of the result. -1. ATAN(NUMBER T) -> DOUBLE +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type: DOUBLE + +Example:: + + od> SELECT ATAN(2), ATAN(2, 3) + fetched rows / total rows = 1/1 + +--------------------+--------------------+ + | atan(2) | atan(2, 3) | + |--------------------+--------------------| + | 1.1071487177940904 | 0.5880026035475675 | + +--------------------+--------------------+ ATAN2 @@ -88,9 +124,21 @@ ATAN2 Description ----------- -Specifications: +Usage: atan2(y, x) calculates the arc tangent of y / x, except that the signs of both arguments are used to determine the quadrant of the result. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type: DOUBLE + +Example:: -1. ATAN2(NUMBER T, NUMBER) -> DOUBLE + od> SELECT ATAN2(2, 3) + fetched rows / total rows = 1/1 + +--------------------+ + | atan2(2, 3) | + |--------------------| + | 0.5880026035475675 | + +--------------------+ CAST @@ -168,9 +216,21 @@ COS Description ----------- -Specifications: +Usage: cos(x) calculate the cosine of x, where x is given in radians. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type: DOUBLE -1. COS(NUMBER T) -> DOUBLE +Example:: + + od> SELECT COS(0) + fetched rows / total rows = 1/1 + +----------+ + | cos(0) | + |----------| + | 1 | + +----------+ COSH @@ -190,9 +250,21 @@ COT Description ----------- -Specifications: +Usage: cot(x) calculate the cotangent of x. Returns out-of-range error if x equals to 0. -1. COT(NUMBER T) -> DOUBLE +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type: DOUBLE + +Example:: + + od> SELECT COT(1) + fetched rows / total rows = 1/1 + +--------------------+ + | cot(1) | + |--------------------| + | 0.6420926159343306 | + +--------------------+ CRC32 @@ -269,9 +341,21 @@ DEGREES Description ----------- -Specifications: +Usage: degrees(x) converts x from radians to degrees. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE -1. DEGREES(NUMBER T) -> DOUBLE +Return type: DOUBLE + +Example:: + + od> SELECT DEGREES(1.57) + fetched rows / total rows = 1/1 + +-------------------+ + | degrees(1.57) | + |-------------------| + | 89.95437383553924 | + +-------------------+ DIVIDE @@ -625,9 +709,21 @@ RADIANS Description ----------- -Specifications: +Usage: radians(x) converts x from degrees to radians. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE -1. RADIANS(NUMBER T) -> DOUBLE +Return type: DOUBLE + +Example:: + + od> SELECT RADIANS(90) + fetched rows / total rows = 1/1 + +--------------------+ + | radians(90) | + |--------------------| + | 1.5707963267948966 | + +--------------------+ RAND @@ -763,9 +859,21 @@ SIN Description ----------- -Specifications: +Usage: sin(x) calculate the sine of x, where x is given in radians. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type: DOUBLE + +Example:: -1. SIN(NUMBER T) -> DOUBLE + od> SELECT SIN(0) + fetched rows / total rows = 1/1 + +----------+ + | sin(0) | + |----------| + | 0 | + +----------+ SINH @@ -833,9 +941,21 @@ TAN Description ----------- -Specifications: +Usage: tan(x) calculate the tangent of x, where x is given in radians. + +Argument type: INTEGER/LONG/FLOAT/DOUBLE -1. TAN(NUMBER T) -> DOUBLE +Return type: DOUBLE + +Example:: + + od> SELECT TAN(0) + fetched rows / total rows = 1/1 + +----------+ + | tan(0) | + |----------| + | 0 | + +----------+ TIMESTAMP diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/MathematicalFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/MathematicalFunctionIT.java index 0afe4be28d..dfd1b1df42 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/MathematicalFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/MathematicalFunctionIT.java @@ -21,6 +21,7 @@ import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.schema; import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifyDataRows; import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifySchema; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifySome; import java.io.IOException; import org.json.JSONObject; @@ -315,4 +316,101 @@ public void testRand() throws IOException { "source=%s | eval f = rand(5) | fields f", TEST_INDEX_BANK)); verifySchema(result, schema("f", null, "float")); } + + @Test + public void testAcos() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = acos(0) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.acos(0))); + } + + @Test + public void testAsin() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = asin(1) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.asin(1))); + } + + @Test + public void testAtan() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = atan(2) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.atan(2))); + + result = + executeQuery( + String.format( + "source=%s | eval f = atan(2, 3) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.atan2(2, 3))); + } + + @Test + public void testAtan2() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = atan2(2, 3) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.atan2(2, 3))); + } + + @Test + public void testCos() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = cos(1.57) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.cos(1.57))); + } + + @Test + public void testCot() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = cot(2) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), closeTo(1 / Math.tan(2))); + } + + @Test + public void testDegrees() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = degrees(1.57) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.toDegrees(1.57))); + } + + @Test + public void testRadians() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = radians(90) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.toRadians(90))); + } + + @Test + public void testSin() throws IOException { + JSONObject result = + executeQuery( + String.format( + "source=%s | eval f = sin(1.57) | fields f", TEST_INDEX_BANK)); + verifySchema(result, schema("f", null, "double")); + verifySome(result.getJSONArray("datarows"), rows(Math.sin(1.57))); + } } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java index 746f81dc01..5bfadb3c6f 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java @@ -136,6 +136,13 @@ public void testTruncate() throws IOException { verifyDataRows(result, rows(-50)); } + @Test + public void testAtan() throws IOException { + JSONObject result = executeQuery("select atan(2, 3)"); + verifySchema(result, schema("atan(2, 3)", null, "double")); + verifyDataRows(result, rows(Math.atan2(2, 3))); + } + protected JSONObject executeQuery(String query) throws IOException { Request request = new Request("POST", QUERY_API_ENDPOINT); request.setJsonEntity(String.format(Locale.ROOT, "{\n" + " \"query\": \"%s\"\n" + "}", query)); diff --git a/integ-test/src/test/resources/correctness/expressions/functions.txt b/integ-test/src/test/resources/correctness/expressions/functions.txt index 0fe9cc4697..fc6120d0ce 100644 --- a/integ-test/src/test/resources/correctness/expressions/functions.txt +++ b/integ-test/src/test/resources/correctness/expressions/functions.txt @@ -38,4 +38,38 @@ sign(abs(1)) sqrt(0) sqrt(1) sqrt(1.1) -sqrt(abs(1)) \ No newline at end of file +sqrt(abs(1)) +acos(0) +acos(0.5) +acos(-0.5) +acos(1) +acos(-1) +asin(0) +asin(0.5) +asin(-0.5) +asin(1) +asin(-1) +atan(0) +atan(1) +atan(-1) +atan2(2, 1) +atan2(-2, 1) +atan2(2, -1) +atan2(-2, -1) +cos(0) +cos(1.57) +cos(-1.57) +cot(1) +cot(-1) +degrees(0) +degrees(1.57) +degrees(-1.57) +radians(0) +radians(90) +radians(-90) +sin(0) +sin(1.57) +sin(-1.57) +tan(0) +tan(1.57) +tan(-1.57) diff --git a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 index f7a68559e3..16564a6e21 100644 --- a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 @@ -156,6 +156,18 @@ SIGN: 'SIGN'; SQRT: 'SQRT'; TRUNCATE: 'TRUNCATE'; +// TRIGONOMETRIC FUNCTIONS +ACOS: 'ACOS'; +ASIN: 'ASIN'; +ATAN: 'ATAN'; +ATAN2: 'ATAN2'; +COS: 'COS'; +COT: 'COT'; +DEGREES: 'DEGREES'; +RADIANS: 'RADIANS'; +SIN: 'SIN'; +TAN: 'TAN'; + // LITERALS AND VALUES //STRING_LITERAL: DQUOTA_STRING | SQUOTA_STRING | BQUOTA_STRING; ID: ID_LITERAL; diff --git a/ppl/src/main/antlr/OpenDistroPPLParser.g4 b/ppl/src/main/antlr/OpenDistroPPLParser.g4 index 325fec1b86..d6ee05fc93 100644 --- a/ppl/src/main/antlr/OpenDistroPPLParser.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLParser.g4 @@ -205,6 +205,11 @@ functionArg mathematicalFunctionBase : ABS | CEIL | CEILING | CONV | CRC32 | E | EXP | FLOOR | LN | LOG | LOG10 | LOG2 | MOD | PI |POW | POWER | RAND | ROUND | SIGN | SQRT | TRUNCATE + | trigonometricFunctionName + ; + +trigonometricFunctionName + : ACOS | ASIN | ATAN | ATAN2 | COS | COT | DEGREES | RADIANS | SIN | TAN ; dateAndTimeFunctionBase diff --git a/sql/src/main/antlr/OpenDistroSQLParser.g4 b/sql/src/main/antlr/OpenDistroSQLParser.g4 index ce72d9a670..c9b3babc4e 100644 --- a/sql/src/main/antlr/OpenDistroSQLParser.g4 +++ b/sql/src/main/antlr/OpenDistroSQLParser.g4 @@ -172,6 +172,11 @@ scalarFunctionName mathematicalFunctionName : ABS | CEIL | CEILING | CONV | CRC32 | E | EXP | FLOOR | LN | LOG | LOG10 | LOG2 | MOD | PI | POW | POWER | RAND | ROUND | SIGN | SQRT | TRUNCATE + | trigonometricFunctionName + ; + +trigonometricFunctionName + : ACOS | ASIN | ATAN | ATAN2 | COS | COT | DEGREES | RADIANS | SIN | TAN ; dateTimeFunctionName