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 c4de0e13ad..0fccacd136 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 @@ -8,6 +8,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.INTEGER; import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL; import static org.opensearch.sql.data.type.ExprCoreType.LONG; @@ -19,6 +20,8 @@ import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling; import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.time.format.TextStyle; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -27,6 +30,7 @@ import org.opensearch.sql.data.model.ExprDatetimeValue; 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; import org.opensearch.sql.data.model.ExprTimeValue; import org.opensearch.sql.data.model.ExprTimestampValue; @@ -64,6 +68,8 @@ public void register(BuiltinFunctionRepository repository) { repository.register(dayOfYear()); repository.register(from_days()); repository.register(hour()); + repository.register(makedate()); + repository.register(maketime()); repository.register(microsecond()); repository.register(minute()); repository.register(month()); @@ -236,6 +242,16 @@ private FunctionResolver hour() { ); } + private FunctionResolver makedate() { + return define(BuiltinFunctionName.MAKEDATE.getName(), + impl(nullMissingHandling(DateTimeFunction::exprMakeDate), DATE, DOUBLE, DOUBLE)); + } + + private FunctionResolver maketime() { + return define(BuiltinFunctionName.MAKETIME.getName(), + impl(nullMissingHandling(DateTimeFunction::exprMakeTime), TIME, DOUBLE, DOUBLE, DOUBLE)); + } + /** * MICROSECOND(STRING/TIME/DATETIME/TIMESTAMP). return the microsecond value for time. */ @@ -512,6 +528,50 @@ private ExprValue exprHour(ExprValue time) { return new ExprIntegerValue(time.timeValue().getHour()); } + /** + * Following MySQL, function receives arguments of type double and rounds them before use. + * Furthermore: + * - zero year interpreted as 2000 + * - negative year is not accepted + * - @dayOfYear should be greater than 1 + * - if @dayOfYear is greater than 365/366, calculation goes to the next year(s) + * + * @param yearExpr year + * @param dayOfYearExp day of the @year, starting from 1 + * @return Date - ExprDateValue object with LocalDate + */ + private ExprValue exprMakeDate(ExprValue yearExpr, ExprValue dayOfYearExp) { + var year = Math.round(yearExpr.doubleValue()); + var dayOfYear = Math.round(dayOfYearExp.doubleValue()); + // We need to do this to comply with MySQL + if (0 >= dayOfYear || 0 > year) { + return ExprNullValue.of(); + } + if (0 == year) { + year = 2000; + } + return new ExprDateValue(LocalDate.ofYearDay((int)year, 1).plusDays(dayOfYear - 1)); + } + + /** + * Following MySQL, function receives arguments of type double. @hour and @minute are rounded, + * while @second used as is, including fraction part. + * @param hourExpr hour + * @param minuteExpr minute + * @param secondExpr second + * @return Time - ExprTimeValue object with LocalTime + */ + private ExprValue exprMakeTime(ExprValue hourExpr, ExprValue minuteExpr, ExprValue secondExpr) { + var hour = Math.round(hourExpr.doubleValue()); + var minute = Math.round(minuteExpr.doubleValue()); + var second = secondExpr.doubleValue(); + if (0 > hour || 0 > minute || 0 > second) { + return ExprNullValue.of(); + } + return new ExprTimeValue(LocalTime.parse(String.format("%02d:%02d:%012.9f", + hour, minute, second), DateTimeFormatter.ISO_TIME)); + } + /** * Microsecond 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 cd34328453..b3821d6e41 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 @@ -68,6 +68,8 @@ public enum BuiltinFunctionName { DAYOFYEAR(FunctionName.of("dayofyear")), FROM_DAYS(FunctionName.of("from_days")), HOUR(FunctionName.of("hour")), + MAKEDATE(FunctionName.of("makedate")), + MAKETIME(FunctionName.of("maketime")), MICROSECOND(FunctionName.of("microsecond")), MINUTE(FunctionName.of("minute")), MONTH(FunctionName.of("month")), 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 79efa2a015..89415e0560 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 @@ -16,6 +16,7 @@ import static org.opensearch.sql.data.model.ExprValueUtils.stringValue; 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.INTEGER; import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL; import static org.opensearch.sql.data.type.ExprCoreType.LONG; @@ -24,7 +25,15 @@ import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Year; +import java.util.HashSet; import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.IntStream; import lombok.AllArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,7 +52,10 @@ import org.opensearch.sql.expression.Expression; import org.opensearch.sql.expression.ExpressionTestBase; import org.opensearch.sql.expression.FunctionExpression; +import org.opensearch.sql.expression.config.ExpressionConfig; import org.opensearch.sql.expression.env.Environment; +import org.opensearch.sql.expression.function.FunctionName; +import org.opensearch.sql.expression.function.FunctionSignature; @ExtendWith(MockitoExtension.class) class DateTimeFunctionTest extends ExpressionTestBase { diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/MakeDateTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/MakeDateTest.java new file mode 100644 index 0000000000..497e73ea51 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/MakeDateTest.java @@ -0,0 +1,174 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.expression.datetime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.opensearch.sql.data.model.ExprValueUtils.missingValue; +import static org.opensearch.sql.data.model.ExprValueUtils.nullValue; +import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; + +import java.time.LocalDate; +import java.time.Year; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.sql.data.model.ExprValue; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; +import org.opensearch.sql.expression.ExpressionTestBase; +import org.opensearch.sql.expression.FunctionExpression; +import org.opensearch.sql.expression.config.ExpressionConfig; +import org.opensearch.sql.expression.env.Environment; +import org.opensearch.sql.expression.function.FunctionName; +import org.opensearch.sql.expression.function.FunctionSignature; + +@ExtendWith(MockitoExtension.class) +public class MakeDateTest extends ExpressionTestBase { + + @Mock + Environment env; + + @Mock + Expression nullRef; + + @Mock + Expression missingRef; + + private FunctionExpression makedate(Expression year, Expression dayOfYear) { + var repo = new ExpressionConfig().functionRepository(); + var func = repo.resolve(new FunctionSignature(new FunctionName("makedate"), + List.of(DOUBLE, DOUBLE))); + return (FunctionExpression)func.apply(List.of(year, dayOfYear)); + } + + private LocalDate makedate(Double year, Double dayOfYear) { + return makedate(DSL.literal(year), DSL.literal(dayOfYear)).valueOf(null).dateValue(); + } + + @Test + public void checkEdgeCases() { + assertEquals(LocalDate.ofYearDay(2002, 1), makedate(2001., 366.), + "No switch to the next year on getting 366th day of a non-leap year"); + assertEquals(LocalDate.ofYearDay(2005, 1), makedate(2004., 367.), + "No switch to the next year on getting 367th day of a leap year"); + assertEquals(LocalDate.ofYearDay(2000, 42), makedate(0., 42.), + "0 year is not interpreted as 2000 as in MySQL"); + assertEquals(nullValue(), eval(makedate(DSL.literal(-1.), DSL.literal(42.))), + "Negative year doesn't produce NULL"); + assertEquals(nullValue(), eval(makedate(DSL.literal(42.), DSL.literal(-1.))), + "Negative dayOfYear doesn't produce NULL"); + assertEquals(nullValue(), eval(makedate(DSL.literal(42.), DSL.literal(0.))), + "Zero dayOfYear doesn't produce NULL"); + + assertEquals(LocalDate.of(1999, 3, 1), makedate(1999., 60.), + "Got Feb 29th of a non-lear year"); + assertEquals(LocalDate.of(1999, 12, 31), makedate(1999., 365.)); + assertEquals(LocalDate.of(2004, 12, 31), makedate(2004., 366.)); + } + + @Test + public void checkRounding() { + assertEquals(LocalDate.of(42, 1, 1), makedate(42.49, 1.49)); + assertEquals(LocalDate.of(43, 1, 2), makedate(42.50, 1.50)); + } + + @Test + public void checkNullValues() { + when(nullRef.valueOf(env)).thenReturn(nullValue()); + + assertEquals(nullValue(), eval(makedate(nullRef, DSL.literal(42.)))); + assertEquals(nullValue(), eval(makedate(DSL.literal(42.), nullRef))); + assertEquals(nullValue(), eval(makedate(nullRef, nullRef))); + } + + @Test + public void checkMissingValues() { + when(missingRef.valueOf(env)).thenReturn(missingValue()); + + assertEquals(missingValue(), eval(makedate(missingRef, DSL.literal(42.)))); + assertEquals(missingValue(), eval(makedate(DSL.literal(42.), missingRef))); + assertEquals(missingValue(), eval(makedate(missingRef, missingRef))); + } + + private static Stream getTestData() { + return Stream.of( + Arguments.of(3755.421154, 9.300720), + Arguments.of(3416.922084, 850.832172), + Arguments.of(498.717527, 590.831215), + Arguments.of(1255.402786, 846.041171), + Arguments.of(2491.200868, 832.929840), + Arguments.of(1140.775582, 345.592629), + Arguments.of(2087.208382, 110.392189), + Arguments.of(4582.515870, 763.629197), + Arguments.of(1654.431245, 476.360251), + Arguments.of(1342.494306, 70.108352), + Arguments.of(171.841206, 794.470738), + Arguments.of(5000.103926, 441.461842), + Arguments.of(2957.828371, 273.909052), + Arguments.of(2232.699033, 171.537097), + Arguments.of(4650.163672, 226.857148), + Arguments.of(495.943520, 735.062451), + Arguments.of(4568.187019, 552.394124), + Arguments.of(688.085482, 283.574200), + Arguments.of(4627.662672, 791.729059), + Arguments.of(2812.837393, 397.688304), + Arguments.of(3050.030341, 596.714966), + Arguments.of(3617.452566, 619.795467), + Arguments.of(2210.322073, 106.914268), + Arguments.of(675.757974, 147.702828), + Arguments.of(1101.801820, 40.055318) + ); + } + + /** + * Test function with given pseudo-random values. + * @param year year + * @param dayOfYear day of year + */ + @ParameterizedTest(name = "year = {0}, dayOfYear = {1}") + @MethodSource("getTestData") + public void checkRandomValues(double year, double dayOfYear) { + LocalDate actual = makedate(year, dayOfYear); + LocalDate expected = getReferenceValue(year, dayOfYear); + + assertEquals(expected, actual, + String.format("year = %f, dayOfYear = %f", year, dayOfYear)); + } + + /** + * Using another algorithm to get reference value. + * We should go to the next year until remaining @dayOfYear is bigger than 365/366. + * @param year Year. + * @param dayOfYear Day of the year. + * @return The calculated date. + */ + private LocalDate getReferenceValue(double year, double dayOfYear) { + var yearL = (int)Math.round(year); + var dayL = (int)Math.round(dayOfYear); + while (true) { + int daysInYear = Year.isLeap(yearL) ? 366 : 365; + if (dayL > daysInYear) { + dayL -= daysInYear; + yearL++; + } else { + break; + } + } + return LocalDate.ofYearDay(yearL, dayL); + } + + private ExprValue eval(Expression expression) { + return expression.valueOf(env); + } +} diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/MakeTimeTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/MakeTimeTest.java new file mode 100644 index 0000000000..8269f74ccc --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/MakeTimeTest.java @@ -0,0 +1,172 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.expression.datetime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import static org.opensearch.sql.data.model.ExprValueUtils.missingValue; +import static org.opensearch.sql.data.model.ExprValueUtils.nullValue; +import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; + +import java.time.Duration; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.sql.data.model.ExprValue; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; +import org.opensearch.sql.expression.ExpressionTestBase; +import org.opensearch.sql.expression.FunctionExpression; +import org.opensearch.sql.expression.config.ExpressionConfig; +import org.opensearch.sql.expression.env.Environment; +import org.opensearch.sql.expression.function.FunctionName; +import org.opensearch.sql.expression.function.FunctionSignature; + +@ExtendWith(MockitoExtension.class) +public class MakeTimeTest extends ExpressionTestBase { + + @Mock + Environment env; + + @Mock + Expression nullRef; + + @Mock + Expression missingRef; + + private FunctionExpression maketime(Expression hour, Expression minute, Expression second) { + var repo = new ExpressionConfig().functionRepository(); + var func = repo.resolve(new FunctionSignature(new FunctionName("maketime"), + List.of(DOUBLE, DOUBLE, DOUBLE))); + return (FunctionExpression)func.apply(List.of(hour, minute, second)); + } + + private LocalTime maketime(Double hour, Double minute, Double second) { + return maketime(DSL.literal(hour), DSL.literal(minute), DSL.literal(second)) + .valueOf(null).timeValue(); + } + + @Test + public void checkEdgeCases() { + assertEquals(nullValue(), eval(maketime(DSL.literal(-1.), DSL.literal(42.), DSL.literal(42.))), + "Negative hour doesn't produce NULL"); + assertEquals(nullValue(), eval(maketime(DSL.literal(42.), DSL.literal(-1.), DSL.literal(42.))), + "Negative minute doesn't produce NULL"); + assertEquals(nullValue(), eval(maketime(DSL.literal(12.), DSL.literal(42.), DSL.literal(-1.))), + "Negative second doesn't produce NULL"); + + assertThrows(DateTimeParseException.class, + () -> eval(maketime(DSL.literal(24.), DSL.literal(42.), DSL.literal(42.)))); + assertThrows(DateTimeParseException.class, + () -> eval(maketime(DSL.literal(12.), DSL.literal(60.), DSL.literal(42.)))); + assertThrows(DateTimeParseException.class, + () -> eval(maketime(DSL.literal(12.), DSL.literal(42.), DSL.literal(60.)))); + + assertEquals(LocalTime.of(23, 59, 59), maketime(23., 59., 59.)); + assertEquals(LocalTime.of(0, 0, 0), maketime(0., 0., 0.)); + } + + @Test + public void checkRounding() { + assertEquals(LocalTime.of(0, 0, 0), maketime(0.49, 0.49, 0.)); + assertEquals(LocalTime.of(1, 1, 0), maketime(0.50, 0.50, 0.)); + } + + @Test + public void checkSecondFraction() { + assertEquals(LocalTime.of(0, 0, 0).withNano(999999999), maketime(0., 0., 0.999999999)); + assertEquals(LocalTime.of(0, 0, 0).withNano(100502000), maketime(0., 0., 0.100502)); + } + + @Test + public void checkNullValues() { + when(nullRef.valueOf(env)).thenReturn(nullValue()); + + assertEquals(nullValue(), eval(maketime(nullRef, DSL.literal(42.), DSL.literal(42.)))); + assertEquals(nullValue(), eval(maketime(DSL.literal(42.), nullRef, DSL.literal(42.)))); + assertEquals(nullValue(), eval(maketime(DSL.literal(42.), DSL.literal(42.), nullRef))); + assertEquals(nullValue(), eval(maketime(nullRef, nullRef, DSL.literal(42.)))); + assertEquals(nullValue(), eval(maketime(nullRef, DSL.literal(42.), nullRef))); + assertEquals(nullValue(), eval(maketime(nullRef, nullRef, nullRef))); + assertEquals(nullValue(), eval(maketime(DSL.literal(42.), nullRef, nullRef))); + } + + @Test + public void checkMissingValues() { + when(missingRef.valueOf(env)).thenReturn(missingValue()); + + assertEquals(missingValue(), eval(maketime(missingRef, DSL.literal(42.), DSL.literal(42.)))); + assertEquals(missingValue(), eval(maketime(DSL.literal(42.), missingRef, DSL.literal(42.)))); + assertEquals(missingValue(), eval(maketime(DSL.literal(42.), DSL.literal(42.), missingRef))); + assertEquals(missingValue(), eval(maketime(missingRef, missingRef, DSL.literal(42.)))); + assertEquals(missingValue(), eval(maketime(missingRef, DSL.literal(42.), missingRef))); + assertEquals(missingValue(), eval(maketime(missingRef, missingRef, missingRef))); + assertEquals(missingValue(), eval(maketime(DSL.literal(42.), missingRef, missingRef))); + } + + private static Stream getTestData() { + return Stream.of( + Arguments.of(20., 30., 40.), + Arguments.of(18.392650, 32.625996, 52.877904), + Arguments.of(20.115442, 7.393619, 27.006809), + Arguments.of(1.231453, 36.462770, 28.736317), + Arguments.of(3.586288, 13.180347, 22.665265), + Arguments.of(4.284613, 40.426888, 19.631883), + Arguments.of(14.843040, 44.682624, 53.484064), + Arguments.of(19.797981, 41.826666, 2.635713), + Arguments.of(4.194618, 10.934165, 32.019225), + Arguments.of(13.240491, 53.625706, 34.506773), + Arguments.of(7.606246, 27.344016, 30.117284), + Arguments.of(13.922934, 26.936002, 42.599373), + Arguments.of(23.114911, 37.764516, 7.677971), + Arguments.of(7.388466, 31.973471, 35.131596), + Arguments.of(19.777173, 44.926077, 24.613693), + Arguments.of(5.773249, 52.693275, 10.190731), + Arguments.of(17.812324, 36.549285, 4.620326), + Arguments.of(9.774054, 41.955251, 23.995705), + Arguments.of(19.619894, 54.933941, 48.788633), + Arguments.of(18.731704, 48.510363, 50.444896), + Arguments.of(10.345095, 27.593594, 23.083821), + Arguments.of(22.925545, 25.113236, 10.645589), + Arguments.of(7.494112, 9.761983, 17.444988), + Arguments.of(17.867756, 10.313120, 36.391815), + Arguments.of(19.712155, 3.197562, 6.607233), + Arguments.of(2.385090, 41.761568, 33.342590) + ); + } + + /** + * Test function with given pseudo-random values. + * @param hour hour + * @param minute minute + * @param second second + */ + @ParameterizedTest(name = "hour = {0}, minute = {1}, second = {2}") + @MethodSource("getTestData") + public void checkRandomValues(double hour, double minute, double second) { + // results could have 1 nanosec diff because of rounding FP + var expected = LocalTime.of((int)Math.round(hour), (int)Math.round(minute), + // pick fraction second part as nanos + (int)Math.floor(second)).withNano((int)((second % 1) * 1E9)); + var delta = Duration.between(expected, maketime(hour, minute, second)).getNano(); + assertEquals(0, delta, 1, + String.format("hour = %f, minute = %f, second = %f", hour, minute, second)); + } + + private ExprValue eval(Expression expression) { + return expression.valueOf(env); + } +} diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index f1d4d987a3..736b1f148e 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1219,15 +1219,70 @@ Example:: +---------------------------+ +MAKEDATE +-------- + +Description +>>>>>>>>>>> + +Returns a date, given `year` and `day-of-year` values. `dayofyear` must be greater than 0 or the result is `NULL`. The result is also `NULL` if either argument is `NULL`. +Arguments are rounded to an integer. + +Limitations: +- Zero `year` interpreted as 2000; +- Negative `year` is not accepted; +- `day-of-year` should be greater than zero; +- `day-of-year` could be greater than 365/366, calculation switches to the next year(s) (see example). + +Specifications: + +1. MAKEDATE(DOUBLE, DOUBLE) -> DATE + +Argument type: DOUBLE + +Return type: DATE + +Example:: + + os> select MAKEDATE(1945, 5.9), MAKEDATE(1984, 1984) + fetched rows / total rows = 1/1 + +-----------------------+------------------------+ + | MAKEDATE(1945, 5.9) | MAKEDATE(1984, 1984) | + |-----------------------+------------------------| + | 1945-01-06 | 1989-06-06 | + +-----------------------+------------------------+ + + MAKETIME -------- Description >>>>>>>>>>> +Returns a time value calculated from the hour, minute, and second arguments. Returns `NULL` if any of its arguments are `NULL`. +The second argument can have a fractional part, rest arguments are rounded to an integer. + +Limitations: +- 24-hour clock is used, available time range is [00:00:00.0 - 23:59:59.(9)]; +- Up to 9 digits of second fraction part is taken (nanosecond precision). + Specifications: -1. MAKETIME(INTEGER, INTEGER, INTEGER) -> DATE +1. MAKETIME(DOUBLE, DOUBLE, DOUBLE) -> TIME + +Argument type: DOUBLE + +Return type: TIME + +Example:: + + os> select MAKETIME(20, 30, 40), MAKETIME(20.2, 49.5, 42.100502) + fetched rows / total rows = 1/1 + +------------------------+-----------------------------------+ + | MAKETIME(20, 30, 40) | MAKETIME(20.2, 49.5, 42.100502) | + |------------------------+-----------------------------------| + | 20:30:40 | 20:50:42.100502 | + +------------------------+-----------------------------------+ MICROSECOND diff --git a/docs/user/ppl/functions/datetime.rst b/docs/user/ppl/functions/datetime.rst index 5be5686c34..3680dc2272 100644 --- a/docs/user/ppl/functions/datetime.rst +++ b/docs/user/ppl/functions/datetime.rst @@ -388,15 +388,70 @@ Example:: +--------------------------+ +MAKEDATE +-------- + +Description +>>>>>>>>>>> + +Returns a date, given `year` and `day-of-year` values. `dayofyear` must be greater than 0 or the result is `NULL`. The result is also `NULL` if either argument is `NULL`. +Arguments are rounded to an integer. + +Limitations: +- Zero `year` interpreted as 2000; +- Negative `year` is not accepted; +- `day-of-year` should be greater than zero; +- `day-of-year` could be greater than 365/366, calculation switches to the next year(s) (see example). + +Specifications: + +1. MAKEDATE(DOUBLE, DOUBLE) -> DATE + +Argument type: DOUBLE + +Return type: DATE + +Example:: + + os> source=people | eval `MAKEDATE(1945, 5.9)` = MAKEDATE(1945, 5.9), `MAKEDATE(1984, 1984)` = MAKEDATE(1984, 1984) | fields `MAKEDATE(1945, 5.9)`, `MAKEDATE(1984, 1984)` + fetched rows / total rows = 1/1 + +-----------------------+------------------------+ + | MAKEDATE(1945, 5.9) | MAKEDATE(1984, 1984) | + |-----------------------+------------------------| + | 1945-01-06 | 1989-06-06 | + +-----------------------+------------------------+ + + MAKETIME -------- Description >>>>>>>>>>> +Returns a time value calculated from the hour, minute, and second arguments. Returns `NULL` if any of its arguments are `NULL`. +The second argument can have a fractional part, rest arguments are rounded to an integer. + +Limitations: +- 24-hour clock is used, available time range is [00:00:00.0 - 23:59:59.(9)]; +- Up to 9 digits of second fraction part is taken (nanosecond precision). + Specifications: -1. MAKETIME(INTEGER, INTEGER, INTEGER) -> DATE +1. MAKETIME(DOUBLE, DOUBLE, DOUBLE) -> TIME + +Argument type: DOUBLE + +Return type: TIME + +Example:: + + os> source=people | eval `MAKETIME(20, 30, 40)` = MAKETIME(20, 30, 40), `MAKETIME(20.2, 49.5, 42.100502)` = MAKETIME(20.2, 49.5, 42.100502) | fields `MAKETIME(20, 30, 40)`, `MAKETIME(20.2, 49.5, 42.100502)` + fetched rows / total rows = 1/1 + +------------------------+-----------------------------------+ + | MAKETIME(20, 30, 40) | MAKETIME(20.2, 49.5, 42.100502) | + |------------------------+-----------------------------------| + | 20:30:40 | 20:50:42.100502 | + +------------------------+-----------------------------------+ MICROSECOND diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java index fcbfc27710..7e0169d174 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java @@ -13,6 +13,8 @@ import static org.opensearch.sql.util.MatcherUtils.verifySome; import java.io.IOException; +import java.time.LocalTime; + import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.opensearch.sql.common.utils.StringUtils; @@ -463,4 +465,19 @@ public void testDateFormatISO8601() throws IOException { verifyDateFormat(date, "date", dateFormat, dateFormatted); } + @Test + public void testMakeTime() throws IOException { + var result = executeQuery(String.format( + "source=%s | eval f1 = MAKETIME(20, 30, 40), f2 = MAKETIME(20.2, 49.5, 42.100502) | fields f1, f2", TEST_INDEX_DATE)); + verifySchema(result, schema("f1", null, "time"), schema("f2", null, "time")); + verifySome(result.getJSONArray("datarows"), rows("20:30:40", "20:50:42.100502")); + } + + @Test + public void testMakeDate() throws IOException { + var result = executeQuery(String.format( + "source=%s | eval f1 = MAKEDATE(1945, 5.9), f2 = MAKEDATE(1984, 1984) | fields f1, f2", TEST_INDEX_DATE)); + verifySchema(result, schema("f1", null, "date"), schema("f2", null, "date")); + verifySome(result.getJSONArray("datarows"), rows("1945-01-06", "1989-06-06")); + } } 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 d19c3719b6..7c6bd7efe2 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 @@ -7,11 +7,13 @@ package org.opensearch.sql.sql; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE; import static org.opensearch.sql.legacy.plugin.RestSqlAction.QUERY_API_ENDPOINT; import static org.opensearch.sql.util.MatcherUtils.rows; import static org.opensearch.sql.util.MatcherUtils.schema; import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; import static org.opensearch.sql.util.MatcherUtils.verifySchema; +import static org.opensearch.sql.util.MatcherUtils.verifySome; import static org.opensearch.sql.util.TestUtils.getResponseBody; import java.io.IOException; @@ -452,6 +454,23 @@ public void testDateFormat() throws IOException { verifyDateFormat(date, "date", dateFormat, dateFormatted); } + + @Test + public void testMakeTime() throws IOException { + var result = executeQuery(String.format( + "select MAKETIME(20, 30, 40) as f1, MAKETIME(20.2, 49.5, 42.100502) as f2", TEST_INDEX_DATE)); + verifySchema(result, schema("MAKETIME(20, 30, 40)", "f1", "time"), schema("MAKETIME(20.2, 49.5, 42.100502)", "f2", "time")); + verifySome(result.getJSONArray("datarows"), rows("20:30:40", "20:50:42.100502")); + } + + @Test + public void testMakeDate() throws IOException { + var result = executeQuery(String.format( + "select MAKEDATE(1945, 5.9) as f1, MAKEDATE(1984, 1984) as f2", TEST_INDEX_DATE)); + verifySchema(result, schema("MAKEDATE(1945, 5.9)", "f1", "date"), schema("MAKEDATE(1984, 1984)", "f2", "date")); + verifySome(result.getJSONArray("datarows"), rows("1945-01-06", "1989-06-06")); + } + 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/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index e01d433778..93df64d0b3 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -231,6 +231,8 @@ DAYOFWEEK: 'DAYOFWEEK'; DAYOFYEAR: 'DAYOFYEAR'; DAYNAME: 'DAYNAME'; FROM_DAYS: 'FROM_DAYS'; +MAKEDATE: 'MAKEDATE'; +MAKETIME: 'MAKETIME'; MONTHNAME: 'MONTHNAME'; SUBDATE: 'SUBDATE'; TIME: 'TIME'; diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index 79d0b5f92e..c83297459d 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -375,7 +375,7 @@ trigonometricFunctionName dateAndTimeFunctionBase : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | HOUR | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | TIME | TIME_TO_SEC - | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT + | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT | MAKETIME | MAKEDATE ; /** condition function return boolean value */ diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 9fca2942cf..6d2d7d8a64 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -216,6 +216,7 @@ LOG10: 'LOG10'; LOG2: 'LOG2'; LOWER: 'LOWER'; LTRIM: 'LTRIM'; +MAKEDATE: 'MAKEDATE'; MAKETIME: 'MAKETIME'; MODULUS: 'MODULUS'; MONTHNAME: 'MONTHNAME'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index a9316b55a4..40207df82a 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -385,7 +385,7 @@ trigonometricFunctionName dateTimeFunctionName : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | HOUR | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | TIME | TIME_TO_SEC - | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT + | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT | MAKETIME | MAKEDATE ; textFunctionName