diff --git a/core/src/main/java/org/opensearch/sql/expression/DSL.java b/core/src/main/java/org/opensearch/sql/expression/DSL.java index 616f431283..43e7279798 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -461,6 +461,10 @@ public static FunctionExpression module(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.MODULES, expressions); } + public static FunctionExpression sec_to_time(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.SEC_TO_TIME, expressions); + } + public static FunctionExpression substr(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.SUBSTR, expressions); } diff --git a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java index fc8cdc93ef..9bd32cc02a 100644 --- a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java @@ -15,6 +15,7 @@ import static org.opensearch.sql.data.type.ExprCoreType.DATE; import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; +import static org.opensearch.sql.data.type.ExprCoreType.FLOAT; import static org.opensearch.sql.data.type.ExprCoreType.INTEGER; import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL; import static org.opensearch.sql.data.type.ExprCoreType.LONG; @@ -59,6 +60,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.opensearch.sql.data.model.ExprDateValue; import org.opensearch.sql.data.model.ExprDatetimeValue; @@ -176,6 +178,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(period_add()); repository.register(period_diff()); repository.register(quarter()); + repository.register(sec_to_time()); repository.register(second(BuiltinFunctionName.SECOND)); repository.register(second(BuiltinFunctionName.SECOND_OF_MINUTE)); repository.register(subdate()); @@ -688,6 +691,15 @@ private DefaultFunctionResolver quarter() { ); } + private DefaultFunctionResolver sec_to_time() { + return define(BuiltinFunctionName.SEC_TO_TIME.getName(), + impl((nullMissingHandling(DateTimeFunction::exprSecToTime)), TIME, INTEGER), + impl((nullMissingHandling(DateTimeFunction::exprSecToTime)), TIME, LONG), + impl((nullMissingHandling(DateTimeFunction::exprSecToTimeWithNanos)), TIME, DOUBLE), + impl((nullMissingHandling(DateTimeFunction::exprSecToTimeWithNanos)), TIME, FLOAT) + ); + } + /** * SECOND(STRING/TIME/DATETIME/TIMESTAMP). return the second value for time. */ @@ -1498,6 +1510,44 @@ private ExprValue exprQuarter(ExprValue date) { return new ExprIntegerValue((month / 3) + ((month % 3) == 0 ? 0 : 1)); } + /** + * Returns TIME value of sec_to_time function for an INTEGER or LONG arguments. + * @param totalSeconds The total number of seconds + * @return A TIME value + */ + private ExprValue exprSecToTime(ExprValue totalSeconds) { + return new ExprTimeValue(LocalTime.MIN.plus(Duration.ofSeconds(totalSeconds.longValue()))); + } + + /** + * Helper function which obtains the decimal portion of the seconds value passed in. + * Uses BigDecimal to prevent issues with math on floating point numbers. + * Return is formatted to be used with Duration.ofSeconds(); + * + * @param seconds and ExprDoubleValue or ExprFloatValue for the seconds + * @return A LONG representing the nanoseconds portion + */ + private long formatNanos(ExprValue seconds) { + //Convert ExprValue to BigDecimal + BigDecimal formattedNanos = BigDecimal.valueOf(seconds.doubleValue()); + //Extract only the nanosecond part + formattedNanos = formattedNanos.subtract(BigDecimal.valueOf(formattedNanos.intValue())); + + return formattedNanos.scaleByPowerOfTen(9).longValue(); + } + + /** + * Returns TIME value of sec_to_time function for FLOAT or DOUBLE arguments. + * @param totalSeconds The total number of seconds + * @return A TIME value + */ + private ExprValue exprSecToTimeWithNanos(ExprValue totalSeconds) { + long nanos = formatNanos(totalSeconds); + + return new ExprTimeValue( + LocalTime.MIN.plus(Duration.ofSeconds(totalSeconds.longValue(), nanos))); + } + /** * Second implementation for ExprValue. * diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index ec4a7bc140..d5195875d9 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -94,6 +94,7 @@ public enum BuiltinFunctionName { PERIOD_ADD(FunctionName.of("period_add")), PERIOD_DIFF(FunctionName.of("period_diff")), QUARTER(FunctionName.of("quarter")), + SEC_TO_TIME(FunctionName.of("sec_to_time")), SECOND(FunctionName.of("second")), SECOND_OF_MINUTE(FunctionName.of("second_of_minute")), SUBDATE(FunctionName.of("subdate")), diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java index fdb029ca90..4cbe515cde 100644 --- a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java @@ -42,6 +42,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.opensearch.sql.data.model.ExprDateValue; import org.opensearch.sql.data.model.ExprDatetimeValue; +import org.opensearch.sql.data.model.ExprDoubleValue; +import org.opensearch.sql.data.model.ExprIntegerValue; import org.opensearch.sql.data.model.ExprLongValue; import org.opensearch.sql.data.model.ExprNullValue; import org.opensearch.sql.data.model.ExprStringValue; @@ -56,6 +58,7 @@ import org.opensearch.sql.expression.FunctionExpression; import org.opensearch.sql.expression.LiteralExpression; import org.opensearch.sql.expression.env.Environment; +import org.opensearch.sql.expression.function.FunctionProperties; @ExtendWith(MockitoExtension.class) class DateTimeFunctionTest extends ExpressionTestBase { @@ -1308,6 +1311,62 @@ public void quarter() { assertEquals(integerValue(4), eval(expression)); } + private static Stream getTestDataForSecToTime() { + return Stream.of( + Arguments.of(1, "00:00:01"), + Arguments.of(2378, "00:39:38"), + Arguments.of(6897, "01:54:57"), + Arguments.of(-82800, "01:00:00"), + Arguments.of(-169200, "01:00:00"), + Arguments.of(3600, "01:00:00"), + Arguments.of(90000, "01:00:00"), + Arguments.of(176400, "01:00:00") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getTestDataForSecToTime") + public void testSecToTime(int seconds, String expected) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.sec_to_time( + DSL.literal(new ExprIntegerValue(seconds))); + + assertEquals(TIME, expr.type()); + assertEquals(new ExprTimeValue(expected), eval(expr)); + } + + private static Stream getTestDataForSecToTimeWithDecimal() { + return Stream.of( + Arguments.of(1.123, "00:00:01.123"), + Arguments.of(1.00123, "00:00:01.00123"), + Arguments.of(1.001023, "00:00:01.001023"), + Arguments.of(1.000000042, "00:00:01.000000042"), + Arguments.of(3.14, "00:00:03.14") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getTestDataForSecToTimeWithDecimal") + public void testSecToTimeWithDecimal(double arg, String expected) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.sec_to_time(DSL.literal(new ExprDoubleValue(arg))); + + assertEquals(TIME, expr.type()); + assertEquals(new ExprTimeValue(expected), eval(expr)); + } + + @Test + public void testSecToTimeWithNullValue() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.sec_to_time( + DSL.literal(nullValue())); + + assertEquals(nullValue(), eval(expr)); + } + @Test public void second() { when(nullRef.type()).thenReturn(TIME); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 5d5a3e1f96..077e1a81af 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2131,6 +2131,47 @@ Example:: | 3 | +-------------------------------+ +SEC_TO_TIME +----------- + +Description +>>>>>>>>>>> + +Usage: sec_to_time(number) returns the time in HH:mm:ssss[.nnnnnn] format. +Note that the function returns a time between 00:00:00 and 23:59:59. +If an input value is too large (greater than 86399), the function will wrap around and begin returning outputs starting from 00:00:00. +If an input value is too small (less than 0), the function will wrap around and begin returning outputs counting down from 23:59:59. + +Argument type: INTEGER, LONG, DOUBLE, FLOAT + +Return type: TIME + +Example:: + + os> SELECT SEC_TO_TIME(3601) + fetched rows / total rows = 1/1 + +---------------------+ + | SEC_TO_TIME(3601) | + |---------------------| + | 01:00:01 | + +---------------------+ + + os> SELECT sec_to_time(1234.123); + fetched rows / total rows = 1/1 + +-------------------------+ + | sec_to_time(1234.123) | + |-------------------------| + | 00:20:34.123 | + +-------------------------+ + + os> SELECT sec_to_time(NULL); + fetched rows / total rows = 1/1 + +---------------------+ + | sec_to_time(NULL) | + |---------------------| + | null | + +---------------------+ + SECOND ------ diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java index 4254641524..4a8b44c410 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java @@ -754,6 +754,16 @@ public void testQuarter() throws IOException { verifyDataRows(result, rows(3)); } + @Test + public void testSecToTime() throws IOException { + JSONObject result = executeQuery( + String.format("SELECT sec_to_time(balance) FROM %s LIMIT 3", TEST_INDEX_BANK)); + verifyDataRows(result, + rows("10:53:45"), + rows("01:34:46"), + rows("09:07:18")); + } + @Test public void testSecond() throws IOException { JSONObject result = executeQuery("select second(timestamp('2020-09-16 17:30:00'))"); diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 25f23a7bd6..040b10faf0 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -248,6 +248,7 @@ RINT: 'RINT'; ROUND: 'ROUND'; RTRIM: 'RTRIM'; REVERSE: 'REVERSE'; +SEC_TO_TIME: 'SEC_TO_TIME'; SIGN: 'SIGN'; SIGNUM: 'SIGNUM'; SIN: 'SIN'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index e5efeabba0..01ba25f601 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -466,6 +466,7 @@ dateTimeFunctionName | PERIOD_ADD | PERIOD_DIFF | QUARTER + | SEC_TO_TIME | SECOND | SECOND_OF_MINUTE | SUBDATE diff --git a/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java b/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java index 0b8e64d0bb..cdd53d956f 100644 --- a/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java +++ b/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java @@ -521,6 +521,13 @@ public void can_parse_minute_of_day_function() { assertNotNull(parser.parse("SELECT minute_of_day('2022-12-14 12:23:34');"));; } + @Test + public void can_parse_sec_to_time_function() { + assertNotNull(parser.parse("SELECT sec_to_time(-6897)")); + assertNotNull(parser.parse("SELECT sec_to_time(6897)")); + assertNotNull(parser.parse("SELECT sec_to_time(6897.123)")); + } + @Test public void can_parse_last_day_function() { assertNotNull(parser.parse("SELECT last_day(\"2017-06-20\")"));