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 17a28ae871..6bed705ac8 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -419,8 +419,10 @@ public static FunctionExpression timestamp(FunctionProperties functionProperties return compile(functionProperties, BuiltinFunctionName.TIMESTAMP, expressions); } - public static FunctionExpression date_format(Expression... expressions) { - return compile(FunctionProperties.None, BuiltinFunctionName.DATE_FORMAT, expressions); + public static FunctionExpression date_format( + FunctionProperties functionProperties, + Expression... expressions) { + return compile(functionProperties, BuiltinFunctionName.DATE_FORMAT, expressions); } public static FunctionExpression to_days(Expression... expressions) { @@ -823,6 +825,11 @@ public static FunctionExpression current_date(FunctionProperties functionPropert return compile(functionProperties, BuiltinFunctionName.CURRENT_DATE, args); } + public static FunctionExpression time_format(FunctionProperties functionProperties, + Expression... expressions) { + return compile(functionProperties, BuiltinFunctionName.TIME_FORMAT, expressions); + } + public static FunctionExpression utc_date(FunctionProperties functionProperties, Expression... args) { return compile(functionProperties, BuiltinFunctionName.UTC_DATE, args); diff --git a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFormatterUtil.java b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFormatterUtil.java index d351a8808a..c5efb2dc5f 100644 --- a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFormatterUtil.java +++ b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFormatterUtil.java @@ -6,12 +6,15 @@ package org.opensearch.sql.expression.datetime; import com.google.common.collect.ImmutableMap; +import java.time.Clock; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.opensearch.sql.data.model.ExprNullValue; import org.opensearch.sql.data.model.ExprStringValue; import org.opensearch.sql.data.model.ExprValue; @@ -23,6 +26,8 @@ class DateTimeFormatterUtil { private static final int SUFFIX_SPECIAL_START_TH = 11; private static final int SUFFIX_SPECIAL_END_TH = 13; private static final String SUFFIX_SPECIAL_TH = "th"; + + private static final String NANO_SEC_FORMAT = "'%06d'"; private static final Map SUFFIX_CONVERTER = ImmutableMap.builder() .put(1, "st").put(2, "nd").put(3, "rd").build(); @@ -33,7 +38,7 @@ interface DateTimeFormatHandler { String getFormat(LocalDateTime date); } - private static final Map HANDLERS = + private static final Map DATE_HANDLERS = ImmutableMap.builder() .put("%a", (date) -> "EEE") // %a => EEE - Abbreviated weekday name (Sun..Sat) .put("%b", (date) -> "LLL") // %b => LLL - Abbreviated month name (Jan..Dec) @@ -61,7 +66,7 @@ interface DateTimeFormatHandler { .put("%D", (date) -> // %w - Day of month with English suffix String.format("'%d%s'", date.getDayOfMonth(), getSuffix(date.getDayOfMonth()))) .put("%f", (date) -> // %f - Microseconds - String.format("'%d'", (date.getNano() / 1000))) + String.format(NANO_SEC_FORMAT, (date.getNano() / 1000))) .put("%w", (date) -> // %w - Day of week (0 indexed) String.format("'%d'", date.getDayOfWeek().getValue())) .put("%U", (date) -> // %U Week where Sunday is the first day - WEEK() mode 0 @@ -78,6 +83,45 @@ interface DateTimeFormatHandler { String.format("'%d'", CalendarLookup.getYearNumber(3, date.toLocalDate()))) .build(); + //Handlers for the time_format function. + //Some format specifiers return 0 or null to align with MySQL. + //https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_time-format + private static final Map TIME_HANDLERS = + ImmutableMap.builder() + .put("%a", (date) -> null) + .put("%b", (date) -> null) + .put("%c", (date) -> "0") + .put("%d", (date) -> "00") + .put("%e", (date) -> "0") + .put("%H", (date) -> "HH") // %H => HH - (00..23) + .put("%h", (date) -> "hh") // %h => hh - (01..12) + .put("%I", (date) -> "hh") // %I => hh - (01..12) + .put("%i", (date) -> "mm") // %i => mm - Minutes, numeric (00..59) + .put("%j", (date) -> null) + .put("%k", (date) -> "H") // %k => H - (0..23) + .put("%l", (date) -> "h") // %l => h - (1..12) + .put("%p", (date) -> "a") // %p => a - AM or PM + .put("%M", (date) -> null) + .put("%m", (date) -> "00") + .put("%r", (date) -> "hh:mm:ss a") // %r => hh:mm:ss a - hh:mm:ss followed by AM or PM + .put("%S", (date) -> "ss") // %S => ss - Seconds (00..59) + .put("%s", (date) -> "ss") // %s => ss - Seconds (00..59) + .put("%T", (date) -> "HH:mm:ss") // %T => HH:mm:ss + .put("%W", (date) -> null) + .put("%Y", (date) -> "0000") + .put("%y", (date) -> "00") + .put("%D", (date) -> null) + .put("%f", (date) -> // %f - Microseconds + String.format(NANO_SEC_FORMAT, (date.getNano() / 1000))) + .put("%w", (date) -> null) + .put("%U", (date) -> null) + .put("%u", (date) -> null) + .put("%V", (date) -> null) + .put("%v", (date) -> null) + .put("%X", (date) -> null) + .put("%x", (date) -> null) + .build(); + private static final Pattern pattern = Pattern.compile("%."); private static final Pattern CHARACTERS_WITH_NO_MOD_LITERAL_BEHIND_PATTERN = Pattern.compile("(? handler, + LocalDateTime datetime) { final StringBuffer cleanFormat = new StringBuffer(); final Matcher m = CHARACTERS_WITH_NO_MOD_LITERAL_BEHIND_PATTERN - .matcher(formatExpr.stringValue()); + .matcher(formatExpr.stringValue()); + while (m.find()) { m.appendReplacement(cleanFormat,String.format("'%s'", m.group())); } @@ -104,21 +151,56 @@ static ExprValue getFormattedDate(ExprValue dateExpr, ExprValue formatExpr) { final Matcher matcher = pattern.matcher(cleanFormat.toString()); final StringBuffer format = new StringBuffer(); - while (matcher.find()) { - matcher.appendReplacement(format, - HANDLERS.getOrDefault(matcher.group(), (d) -> - String.format("'%s'", matcher.group().replaceFirst(MOD_LITERAL, ""))) - .getFormat(date)); + try { + while (matcher.find()) { + matcher.appendReplacement(format, + handler.getOrDefault(matcher.group(), (d) -> + String.format("'%s'", matcher.group().replaceFirst(MOD_LITERAL, ""))) + .getFormat(datetime)); + } + } catch (Exception e) { + return ExprNullValue.of(); } matcher.appendTail(format); // English Locale matches SQL requirements. // 'AM'/'PM' instead of 'a.m.'/'p.m.' // 'Sat' instead of 'Sat.' etc - return new ExprStringValue(date.format( + return new ExprStringValue(datetime.format( DateTimeFormatter.ofPattern(format.toString(), Locale.ENGLISH))); } + /** + * Format the date using the date format String. + * @param dateExpr the date ExprValue of Date/Datetime/Timestamp/String type. + * @param formatExpr the format ExprValue of String type. + * @return Date formatted using format and returned as a String. + */ + static ExprValue getFormattedDate(ExprValue dateExpr, ExprValue formatExpr) { + final LocalDateTime date = dateExpr.datetimeValue(); + return getFormattedString(formatExpr, DATE_HANDLERS, date); + } + + static ExprValue getFormattedDateOfToday(ExprValue formatExpr, ExprValue time, Clock current) { + final LocalDateTime date = LocalDateTime.of(LocalDate.now(current), time.timeValue()); + + return getFormattedString(formatExpr, DATE_HANDLERS, date); + } + + /** + * Format the date using the date format String. + * @param timeExpr the date ExprValue of Date/Datetime/Timestamp/String type. + * @param formatExpr the format ExprValue of String type. + * @return Date formatted using format and returned as a String. + */ + static ExprValue getFormattedTime(ExprValue timeExpr, ExprValue formatExpr) { + //Initializes DateTime with LocalDate.now(). This is safe because the date is ignored. + //The time_format function will only return 0 or null for invalid string format specifiers. + final LocalDateTime time = LocalDateTime.of(LocalDate.now(), timeExpr.timeValue()); + + return getFormattedString(formatExpr, TIME_HANDLERS, time); + } + /** * Returns English suffix of incoming value. * @param val Incoming value. 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 7c7749ee17..5a35aadcae 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 @@ -166,6 +166,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(subtime()); repository.register(sysdate()); repository.register(time()); + repository.register(time_format()); repository.register(time_to_sec()); repository.register(timediff()); repository.register(timestamp()); @@ -876,6 +877,7 @@ private DefaultFunctionResolver year() { * (STRING, STRING) -> STRING * (DATE, STRING) -> STRING * (DATETIME, STRING) -> STRING + * (TIME, STRING) -> STRING * (TIMESTAMP, STRING) -> STRING */ private DefaultFunctionResolver date_format() { @@ -886,6 +888,12 @@ private DefaultFunctionResolver date_format() { STRING, DATE, STRING), impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), STRING, DATETIME, STRING), + implWithProperties( + nullMissingHandlingWithProperties( + (functionProperties, time, formatString) + -> DateTimeFormatterUtil.getFormattedDateOfToday( + formatString, time, functionProperties.getQueryStartClock())), + STRING, TIME, STRING), impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), STRING, TIMESTAMP, STRING) ); @@ -944,6 +952,30 @@ private ExprValue exprDateApplyInterval(FunctionProperties functionProperties, var dt = extractDateTime(datetime, functionProperties); return new ExprDatetimeValue(isAdd ? dt.plus(interval) : dt.minus(interval)); } + + /** + * Formats date according to format specifier. First argument is time, second is format. + * Detailed supported signatures: + * (STRING, STRING) -> STRING + * (DATE, STRING) -> STRING + * (DATETIME, STRING) -> STRING + * (TIME, STRING) -> STRING + * (TIMESTAMP, STRING) -> STRING + */ + private DefaultFunctionResolver time_format() { + return define(BuiltinFunctionName.TIME_FORMAT.getName(), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedTime), + STRING, STRING, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedTime), + STRING, DATE, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedTime), + STRING, DATETIME, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedTime), + STRING, TIME, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedTime), + STRING, TIMESTAMP, STRING) + ); + } /** * ADDDATE function 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 ce657d11e9..5f3bfa7a3e 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 @@ -66,6 +66,7 @@ public enum BuiltinFunctionName { DATEDIFF(FunctionName.of("datediff")), DATETIME(FunctionName.of("datetime")), DATE_ADD(FunctionName.of("date_add")), + DATE_FORMAT(FunctionName.of("date_format")), DATE_SUB(FunctionName.of("date_sub")), DAY(FunctionName.of("day")), DAYNAME(FunctionName.of("dayname")), @@ -100,7 +101,7 @@ public enum BuiltinFunctionName { TIMEDIFF(FunctionName.of("timediff")), TIME_TO_SEC(FunctionName.of("time_to_sec")), TIMESTAMP(FunctionName.of("timestamp")), - DATE_FORMAT(FunctionName.of("date_format")), + TIME_FORMAT(FunctionName.of("time_format")), TO_DAYS(FunctionName.of("to_days")), UTC_DATE(FunctionName.of("utc_date")), UTC_TIME(FunctionName.of("utc_time")), 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 be8d512834..b993964b26 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 @@ -27,6 +27,8 @@ import com.google.common.collect.ImmutableList; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.stream.Stream; import lombok.AllArgsConstructor; @@ -41,6 +43,7 @@ import org.opensearch.sql.data.model.ExprDateValue; import org.opensearch.sql.data.model.ExprDatetimeValue; 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; @@ -78,7 +81,7 @@ public void setup() { "%W","%D","%Y","%y","%a","%b","%j","%m","%d","%h","%s","%w","%f", "%q","%"), ImmutableList.of("13","01","13","1","14","PM","01:14:15 PM","15","13:14:15"," January", - "Saturday","31st","1998","98","Sat","Jan","031","01","31","01","15","6","12345", + "Saturday","31st","1998","98","Sat","Jan","031","01","31","01","15","6","012345", "q","%") ), new DateFormatTester("1999-12-01", @@ -162,7 +165,7 @@ String getFormatted() { } FunctionExpression getDateFormatExpression() { - return DSL.date_format(DSL.literal(date), DSL.literal(getFormatter())); + return DSL.date_format(functionProperties, DSL.literal(date), DSL.literal(getFormatter())); } } @@ -1701,34 +1704,67 @@ public void date_format() { String timestamp = "1998-01-31 13:14:15.012345"; String timestampFormat = "%a %b %c %D %d %e %f %H %h %I %i %j %k %l %M " + "%m %p %r %S %s %T %% %P"; - String timestampFormatted = "Sat Jan 01 31st 31 31 12345 13 01 01 14 031 13 1 " + String timestampFormatted = "Sat Jan 01 31st 31 31 012345 13 01 01 14 031 13 1 " + "January 01 PM 01:14:15 PM 15 15 13:14:15 % P"; - FunctionExpression expr = DSL.date_format(DSL.literal(timestamp), DSL.literal(timestampFormat)); + FunctionExpression expr = DSL.date_format( + functionProperties, + DSL.literal(timestamp), + DSL.literal(timestampFormat)); assertEquals(STRING, expr.type()); assertEquals(timestampFormatted, eval(expr).stringValue()); when(nullRef.type()).thenReturn(DATE); when(missingRef.type()).thenReturn(DATE); - assertEquals(nullValue(), eval(DSL.date_format(nullRef, DSL.literal("")))); - assertEquals(missingValue(), eval(DSL.date_format(missingRef, DSL.literal("")))); + assertEquals(nullValue(), eval(DSL.date_format( + functionProperties, + nullRef, + DSL.literal("")))); + assertEquals(missingValue(), eval(DSL.date_format( + functionProperties, + missingRef, + DSL.literal("")))); when(nullRef.type()).thenReturn(DATETIME); when(missingRef.type()).thenReturn(DATETIME); - assertEquals(nullValue(), eval(DSL.date_format(nullRef, DSL.literal("")))); - assertEquals(missingValue(), eval(DSL.date_format(missingRef, DSL.literal("")))); + assertEquals(nullValue(), eval(DSL.date_format( + functionProperties, + nullRef, + DSL.literal("")))); + assertEquals(missingValue(), eval(DSL.date_format( + functionProperties, + missingRef, + DSL.literal("")))); when(nullRef.type()).thenReturn(TIMESTAMP); when(missingRef.type()).thenReturn(TIMESTAMP); - assertEquals(nullValue(), eval(DSL.date_format(nullRef, DSL.literal("")))); - assertEquals(missingValue(), eval(DSL.date_format(missingRef, DSL.literal("")))); + assertEquals(nullValue(), eval(DSL.date_format( + functionProperties, + nullRef, + DSL.literal("")))); + assertEquals(missingValue(), eval(DSL.date_format( + functionProperties, + missingRef, + DSL.literal("")))); when(nullRef.type()).thenReturn(STRING); when(missingRef.type()).thenReturn(STRING); - assertEquals(nullValue(), eval(DSL.date_format(nullRef, DSL.literal("")))); - assertEquals(missingValue(), eval(DSL.date_format(missingRef, DSL.literal("")))); - assertEquals(nullValue(), eval(DSL.date_format(DSL.literal(""), nullRef))); - assertEquals(missingValue(), eval(DSL.date_format(DSL.literal(""), missingRef))); + assertEquals(nullValue(), eval(DSL.date_format( + functionProperties, + nullRef, + DSL.literal("")))); + assertEquals(missingValue(), eval(DSL.date_format( + functionProperties, + missingRef, + DSL.literal("")))); + assertEquals(nullValue(), eval(DSL.date_format( + functionProperties, + DSL.literal(""), + nullRef))); + assertEquals(missingValue(), eval(DSL.date_format( + functionProperties, + DSL.literal(""), + missingRef))); } void testDateFormat(DateFormatTester dft) { @@ -1737,6 +1773,220 @@ void testDateFormat(DateFormatTester dft) { assertEquals(dft.getFormatted(), eval(expr).stringValue()); } + @Test + public void testDateFormatWithTimeType() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + FunctionExpression expr = DSL.date_format( + functionProperties, + DSL.literal(new ExprTimeValue("12:23:34")), + DSL.literal(new ExprStringValue("%m %d"))); + + assertEquals( + expr.toString(), + "date_format(TIME '12:23:34', \"%m %d\")" + ); + assertEquals( + LocalDateTime.now( + functionProperties.getQueryStartClock()).format( + DateTimeFormatter.ofPattern("\"MM dd\"")), + eval(expr).toString() + ); + } + + @Test + public void testTimeFormatWithDateType() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + FunctionExpression expr = DSL.time_format( + functionProperties, + DSL.literal(new ExprDateValue("2023-01-16")), + DSL.literal(new ExprStringValue("%h %s"))); + + assertEquals( + expr.toString(), + "time_format(DATE '2023-01-16', \"%h %s\")" + ); + assertEquals( + "\"12 00\"", + eval(expr).toString() + ); + } + + private static Stream getTestDataForTimeFormat() { + return Stream.of( + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%f"), + "012345"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.002345"), + DSL.literal("%f"), + "002345"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012300"), + DSL.literal("%f"), + "012300"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%H"), + "13"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%h"), + "01"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%I"), + "01"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%i"), + "14"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%k"), + "13"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%l"), + "1"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%p"), + "PM"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%r"), + "01:14:15 PM"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%S"), + "15"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%s"), + "15"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%T"), + "13:14:15"), + Arguments.of( + DSL.literal("1998-01-31 13:14:15.012345"), + DSL.literal("%f %H %h %I %i %k %l %p %r %S %s %T"), + "012345 13 01 01 14 13 1 PM 01:14:15 PM 15 15 13:14:15") + ); + } + + private void timeFormatQuery(LiteralExpression arg, + LiteralExpression format, + String expectedResult) { + FunctionExpression expr = DSL.time_format(functionProperties, arg, format); + assertEquals(STRING, expr.type()); + assertEquals(expectedResult, eval(expr).stringValue()); + } + + @ParameterizedTest(name = "{0}{1}") + @MethodSource("getTestDataForTimeFormat") + public void testTimeFormat(LiteralExpression arg, + LiteralExpression format, + String expectedResult) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + timeFormatQuery(arg, format, expectedResult); + } + + private static Stream getInvalidTestDataForTimeFormat() { + return Stream.of( + Arguments.of( + DSL.literal("asdfasdf"), + DSL.literal("%f")), + Arguments.of( + DSL.literal("12345"), + DSL.literal("%h")), + Arguments.of( + DSL.literal("10:11:61"), + DSL.literal("%h")), + Arguments.of( + DSL.literal("10:61:12"), + DSL.literal("%h")), + Arguments.of( + DSL.literal("61:11:12"), + DSL.literal("%h")) + ); + } + + @ParameterizedTest(name = "{0}{1}") + @MethodSource("getInvalidTestDataForTimeFormat") + public void testInvalidTimeFormat(LiteralExpression arg, LiteralExpression format) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.time_format(functionProperties, arg, format); + assertThrows(SemanticCheckException.class, () -> eval(expr)); + } + + private static Stream getInvalidTimeFormatHandlers() { + return Stream.of( + Arguments.of("%a"), + Arguments.of("%b"), + Arguments.of("%j"), + Arguments.of("%M"), + Arguments.of("%W"), + Arguments.of("%D"), + Arguments.of("%w"), + Arguments.of("%U"), + Arguments.of("%u"), + Arguments.of("%V"), + Arguments.of("%v"), + Arguments.of("%X"), + Arguments.of("%x") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getInvalidTimeFormatHandlers") + public void testTimeFormatWithInvalidHandlers(String handler) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.time_format( + functionProperties, + DSL.literal("12:23:34"), + DSL.literal(handler)); + assertEquals(ExprNullValue.of(), eval(expr)); + } + + @Test + public void testTimeFormatWithDateHandlers() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.time_format( + functionProperties, + DSL.literal(new ExprDateValue("2023-01-17")), + DSL.literal("%c %d %e %m %Y %y")); + assertEquals( + "0 00 0 00 0000 00", + eval(expr).stringValue()); + } + + @Test + public void testTimeFormatAndDateFormatReturnSameResult() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression timeFormatExpr = DSL.time_format( + functionProperties, + DSL.literal(new ExprDateValue("1998-01-31 13:14:15.012345")), + DSL.literal("%f %H %h %I %i %k %l %p %r %S %s %T")); + FunctionExpression dateFormatExpr = DSL.date_format( + functionProperties, + DSL.literal(new ExprDateValue("1998-01-31 13:14:15.012345")), + DSL.literal("%f %H %h %I %i %k %l %p %r %S %s %T")); + + assertEquals(eval(dateFormatExpr), eval(timeFormatExpr)); + } + private ExprValue eval(Expression expression) { return expression.valueOf(env); } diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 581a68409b..e72175788d 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1361,6 +1361,7 @@ Description >>>>>>>>>>> Usage: date_format(date, format) formats the date argument using the specifiers in the format argument. +If an argument of type TIME is provided, the local date is used. .. list-table:: The following table describes the available specifier arguments. :widths: 20 80 @@ -1437,19 +1438,19 @@ Usage: date_format(date, format) formats the date argument using the specifiers * - x - x, for any smallcase/uppercase alphabet except [aydmshiHIMYDSEL] -Argument type: STRING/DATE/DATETIME/TIMESTAMP, STRING +Argument type: STRING/DATE/DATETIME/TIME/TIMESTAMP, STRING Return type: STRING Example:: - >od SELECT DATE_FORMAT('1998-01-31 13:14:15.012345', '%T.%f'), DATE_FORMAT(TIMESTAMP('1998-01-31 13:14:15.012345'), '%Y-%b-%D %r') + os> SELECT DATE_FORMAT('1998-01-31 13:14:15.012345', '%T.%f'), DATE_FORMAT(TIMESTAMP('1998-01-31 13:14:15.012345'), '%Y-%b-%D %r') fetched rows / total rows = 1/1 - +-----------------------------------------------+----------------------------------------------------------------+ - | DATE('1998-01-31 13:14:15.012345', '%T.%f') | DATE(TIMESTAMP('1998-01-31 13:14:15.012345'), '%Y-%b-%D %r') | - |-----------------------------------------------+----------------------------------------------------------------| - | '13:14:15.012345' | '1998-Jan-31st 01:14:15 PM' | - +-----------------------------------------------+----------------------------------------------------------------+ + +------------------------------------------------------+-----------------------------------------------------------------------+ + | DATE_FORMAT('1998-01-31 13:14:15.012345', '%T.%f') | DATE_FORMAT(TIMESTAMP('1998-01-31 13:14:15.012345'), '%Y-%b-%D %r') | + |------------------------------------------------------+-----------------------------------------------------------------------| + | 13:14:15.012345 | 1998-Jan-31st 01:14:15 PM | + +------------------------------------------------------+-----------------------------------------------------------------------+ DATE_SUB @@ -2307,6 +2308,60 @@ Example:: | 13:49:00 | 13:49:00 | 13:49:00 | 13:49:00 | +--------------------+-----------------+------------------------------------------+-------------------------------+ +TIME_FORMAT +----------- + +Description +>>>>>>>>>>> + +Usage: time_format(time, format) formats the time argument using the specifiers in the format argument. +This supports a subset of the time format specifiers available for the `date_format`_ function. +Using date format specifiers supported by `date_format`_ will return 0 or null. +Acceptable format specifiers are listed in the table below. +If an argument of type DATE is passed in, it is treated as a DATETIME at midnight (i.e., 00:00:00). + +.. list-table:: The following table describes the available specifier arguments. + :widths: 20 80 + :header-rows: 1 + + * - Specifier + - Description + * - %f + - Microseconds (000000..999999) + * - %H + - Hour (00..23) + * - %h + - Hour (01..12) + * - %I + - Hour (01..12) + * - %i + - Minutes, numeric (00..59) + * - %p + - AM or PM + * - %r + - Time, 12-hour (hh:mm:ss followed by AM or PM) + * - %S + - Seconds (00..59) + * - %s + - Seconds (00..59) + * - %T + - Time, 24-hour (hh:mm:ss) + + +Argument type: STRING/DATE/DATETIME/TIME/TIMESTAMP, STRING + +Return type: STRING + +Example:: + + os> SELECT TIME_FORMAT('1998-01-31 13:14:15.012345', '%f %H %h %I %i %p %r %S %s %T') + fetched rows / total rows = 1/1 + +------------------------------------------------------------------------------+ + | TIME_FORMAT('1998-01-31 13:14:15.012345', '%f %H %h %I %i %p %r %S %s %T') | + |------------------------------------------------------------------------------| + | 012345 13 01 01 14 PM 01:14:15 PM 15 15 13:14:15 | + +------------------------------------------------------------------------------+ + TIME_TO_SEC ----------- diff --git a/docs/user/ppl/functions/datetime.rst b/docs/user/ppl/functions/datetime.rst index 429ebafe64..646b7d65ad 100644 --- a/docs/user/ppl/functions/datetime.rst +++ b/docs/user/ppl/functions/datetime.rst @@ -425,6 +425,7 @@ Description >>>>>>>>>>> Usage: date_format(date, format) formats the date argument using the specifiers in the format argument. +If an argument of type TIME is provided, the local date is used. .. list-table:: The following table describes the available specifier arguments. :widths: 20 80 @@ -501,7 +502,7 @@ Usage: date_format(date, format) formats the date argument using the specifiers * - x - x, for any smallcase/uppercase alphabet except [aydmshiHIMYDSEL] -Argument type: STRING/DATE/DATETIME/TIMESTAMP, STRING +Argument type: STRING/DATE/DATETIME/TIME/TIMESTAMP, STRING Return type: STRING 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 0ebfd73265..b83924bc26 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 @@ -789,7 +789,7 @@ public void testDateFormat() throws IOException { String timestamp = "1998-01-31 13:14:15.012345"; String timestampFormat = "%a %b %c %D %d %e %f %H %h %I %i %j %k %l %M " + "%m %p %r %S %s %T %% %P"; - String timestampFormatted = "Sat Jan 01 31st 31 31 12345 13 01 01 14 031 13 1 " + String timestampFormatted = "Sat Jan 01 31st 31 31 012345 13 01 01 14 031 13 1 " + "January 01 PM 01:14:15 PM 15 15 13:14:15 % P"; verifyDateFormat(timestamp, "timestamp", timestampFormat, timestampFormatted); 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 bec3a1a731..c24b3ef0c5 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 @@ -967,7 +967,7 @@ public void testDateFormat() throws IOException { String timestamp = "1998-01-31 13:14:15.012345"; String timestampFormat = "%a %b %c %D %d %e %f %H %h %I %i %j %k %l %M " + "%m %p %r %S %s %T %% %P"; - String timestampFormatted = "Sat Jan 01 31st 31 31 12345 13 01 01 14 031 13 1 " + String timestampFormatted = "Sat Jan 01 31st 31 31 012345 13 01 01 14 031 13 1 " + "January 01 PM 01:14:15 PM 15 15 13:14:15 % P"; verifyDateFormat(timestamp, "timestamp", timestampFormat, timestampFormatted); @@ -1288,6 +1288,26 @@ public void testTimeDiff() throws IOException { verifyDataRows(result, rows("10:59:59")); } + void verifyTimeFormat(String time, String type, String format, String formatted) throws IOException { + String query = String.format("time_format(%s('%s'), '%s')", type, time, format); + JSONObject result = executeQuery("select " + query); + verifySchema(result, schema(query, null, "keyword")); + verifyDataRows(result, rows(formatted)); + + query = String.format("time_format('%s', '%s')", time, format); + result = executeQuery("select " + query); + verifySchema(result, schema(query, null, "keyword")); + verifyDataRows(result, rows(formatted)); + } + + @Test + public void testTimeFormat() throws IOException { + String timestamp = "1998-01-31 13:14:15.012345"; + String timestampFormat = "%f %H %h %I %i %p %r %S %s %T"; + String timestampFormatted = "012345 13 01 01 14 PM 01:14:15 PM 15 15 13:14:15"; + verifyTimeFormat(timestamp, "timestamp", timestampFormat, timestampFormatted); + } + 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/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index c278d735b5..695abf5e69 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -259,6 +259,7 @@ SYSDATE: 'SYSDATE'; TAN: 'TAN'; TIME: 'TIME'; TIMEDIFF: 'TIMEDIFF'; +TIME_FORMAT: 'TIME_FORMAT'; TIME_TO_SEC: 'TIME_TO_SEC'; TIMESTAMP: 'TIMESTAMP'; TRUNCATE: 'TRUNCATE'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 042e1bd44c..20ec64545d 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -471,6 +471,7 @@ dateTimeFunctionName | SUBTIME | SYSDATE | TIME + | TIME_FORMAT | TIME_TO_SEC | TIMEDIFF | TIMESTAMP