diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/data/model/ExprStringValue.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/data/model/ExprStringValue.java index 42de804fe3..09761dcfe9 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/data/model/ExprStringValue.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/data/model/ExprStringValue.java @@ -21,9 +21,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.format.DateTimeParseException; import java.util.Objects; -import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; /** 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 1a816ad017..21a3096f25 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 @@ -329,6 +329,10 @@ public FunctionExpression timestamp(Expression... expressions) { return function(BuiltinFunctionName.TIMESTAMP, expressions); } + public FunctionExpression date_format(Expression... expressions) { + return function(BuiltinFunctionName.DATE_FORMAT, expressions); + } + public FunctionExpression to_days(Expression... expressions) { return function(BuiltinFunctionName.TO_DAYS, expressions); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java index 9956a93201..c6d71aaacc 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java @@ -47,9 +47,9 @@ private static Calendar getCalendar(int mode, LocalDate date) { /** * Set first day of week, minimal days in first week and date in calendar. - * @param firstDayOfWeek the given first day of the week. + * @param firstDayOfWeek the given first day of the week. * @param minimalDaysInWeek the given minimal days required in the first week of the year. - * @param date the given date. + * @param date the given date. */ private static Calendar getCalendar(int firstDayOfWeek, int minimalDaysInWeek, LocalDate date) { Calendar calendar = Calendar.getInstance(); @@ -74,4 +74,19 @@ static int getWeekNumber(int mode, LocalDate date) { } return weekNumber; } -} \ No newline at end of file + + /** + * Returns year for date according to mode. + * @param mode Integer for mode. Valid mode values are 0 to 7. + * @param date LocalDate for date. + */ + static int getYearNumber(int mode, LocalDate date) { + Calendar calendar = getCalendar(mode, date); + int weekNumber = getWeekNumber(mode, date); + int yearNumber = calendar.get(Calendar.YEAR); + if ((weekNumber > 51) && (calendar.get(Calendar.DAY_OF_MONTH) < 7)) { + yearNumber--; + } + return yearNumber; + } +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFormatterUtil.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFormatterUtil.java new file mode 100644 index 0000000000..ef0200a570 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFormatterUtil.java @@ -0,0 +1,120 @@ +package com.amazon.opendistroforelasticsearch.sql.expression.datetime; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.google.common.collect.ImmutableMap; + +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; + +/** + * This class converts a SQL style DATE_FORMAT format specifier and converts it to a + * Java SimpleDateTime format. + */ +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 Map SUFFIX_CONVERTER = + ImmutableMap.builder() + .put(1, "st").put(2, "nd").put(3, "rd").build(); + + // The following have special cases that need handling outside of the format options provided + // by the DateTimeFormatter class. + interface DateTimeFormatHandler { + String getFormat(LocalDateTime date); + } + + private static final Map 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) + .put("%c", (date) -> "MM") // %c => MM - Month, numeric (0..12) + .put("%d", (date) -> "dd") // %d => dd - Day of the month, numeric (00..31) + .put("%e", (date) -> "d") // %e => d - Day of the month, numeric (0..31) + .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) -> "DDD") // %j => DDD - (001..366) + .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) -> "LLLL") // %M => LLLL - Month name (January..December) + .put("%m", (date) -> "MM") // %m => MM - Month, numeric (00..12) + .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) -> "EEEE") // %W => EEEE - Weekday name (Sunday..Saturday) + .put("%Y", (date) -> "yyyy") // %Y => yyyy - Year, numeric, 4 digits + .put("%y", (date) -> "yy") // %y => yy - Year, numeric, 2 digits + // The following are not directly supported by DateTimeFormatter. + .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))) + .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 + String.format("'%d'", CalendarLookup.getWeekNumber(0, date.toLocalDate()))) + .put("%u", (date) -> // %u Week where Monday is the first day - WEEK() mode 1 + String.format("'%d'", CalendarLookup.getWeekNumber(1, date.toLocalDate()))) + .put("%V", (date) -> // %V Week where Sunday is the first day - WEEK() mode 2 used with %X + String.format("'%d'", CalendarLookup.getWeekNumber(2, date.toLocalDate()))) + .put("%v", (date) -> // %v Week where Monday is the first day - WEEK() mode 3 used with %x + String.format("'%d'", CalendarLookup.getWeekNumber(3, date.toLocalDate()))) + .put("%X", (date) -> // %X Year for week where Sunday is the first day, 4 digits used with %V + String.format("'%d'", CalendarLookup.getYearNumber(2, date.toLocalDate()))) + .put("%x", (date) -> // %x Year for week where Monday is the first day, 4 digits used with %v + String.format("'%d'", CalendarLookup.getYearNumber(3, date.toLocalDate()))) + .build(); + + private static final Pattern pattern = Pattern.compile("%."); + private static final String MOD_LITERAL = "%"; + + private DateTimeFormatterUtil() { + } + + /** + * 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(); + final Matcher matcher = pattern.matcher(formatExpr.stringValue()); + 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)); + } + 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( + DateTimeFormatter.ofPattern(format.toString(), Locale.ENGLISH))); + } + + /** + * Returns English suffix of incoming value. + * @param val Incoming value. + * @return English suffix as String (st, nd, rd, th) + */ + private static String getSuffix(int val) { + // The numbers 11, 12, and 13 do not follow general suffix rules. + if ((SUFFIX_SPECIAL_START_TH <= val) && (val <= SUFFIX_SPECIAL_END_TH)) { + return SUFFIX_SPECIAL_TH; + } + return SUFFIX_CONVERTER.getOrDefault(val % 10, SUFFIX_SPECIAL_TH); + } +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java index b0d145f9af..84628b3357 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java @@ -85,6 +85,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(time()); repository.register(time_to_sec()); repository.register(timestamp()); + repository.register(date_format()); repository.register(to_days()); repository.register(week()); repository.register(year()); @@ -400,6 +401,27 @@ private FunctionResolver year() { ); } + /** + * Formats date according to format specifier. First argument is date, second is format. + * Detailed supported signatures: + * (STRING, STRING) -> STRING + * (DATE, STRING) -> STRING + * (DATETIME, STRING) -> STRING + * (TIMESTAMP, STRING) -> STRING + */ + private FunctionResolver date_format() { + return define(BuiltinFunctionName.DATE_FORMAT.getName(), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), + STRING, STRING, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), + STRING, DATE, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), + STRING, DATETIME, STRING), + impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), + STRING, TIMESTAMP, STRING) + ); + } + /** * ADDDATE function implementation for ExprValue. * 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 c403ee88fc..98389f60f1 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 @@ -72,6 +72,7 @@ public enum BuiltinFunctionName { TIME(FunctionName.of("time")), TIME_TO_SEC(FunctionName.of("time_to_sec")), TIMESTAMP(FunctionName.of("timestamp")), + DATE_FORMAT(FunctionName.of("date_format")), TO_DAYS(FunctionName.of("to_days")), WEEK(FunctionName.of("week")), YEAR(FunctionName.of("year")), diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java index 88188cebb1..1bf450a208 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java @@ -47,6 +47,9 @@ import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import com.google.common.collect.ImmutableList; +import java.util.List; +import lombok.AllArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -71,6 +74,89 @@ public void setup() { when(missingRef.valueOf(env)).thenReturn(missingValue()); } + final List dateFormatTesters = ImmutableList.of( + new DateFormatTester("1998-01-31 13:14:15.012345", + ImmutableList.of("%H","%I","%k","%l","%i","%p","%r","%S","%T"," %M", + "%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", + "q","%") + ), + new DateFormatTester("1999-12-01", + ImmutableList.of("%D"), + ImmutableList.of("1st") + ), + new DateFormatTester("1999-12-02", + ImmutableList.of("%D"), + ImmutableList.of("2nd") + ), + new DateFormatTester("1999-12-03", + ImmutableList.of("%D"), + ImmutableList.of("3rd") + ), + new DateFormatTester("1999-12-04", + ImmutableList.of("%D"), + ImmutableList.of("4th") + ), + new DateFormatTester("1999-12-11", + ImmutableList.of("%D"), + ImmutableList.of("11th") + ), + new DateFormatTester("1999-12-12", + ImmutableList.of("%D"), + ImmutableList.of("12th") + ), + new DateFormatTester("1999-12-13", + ImmutableList.of("%D"), + ImmutableList.of("13th") + ), + new DateFormatTester("1999-12-31", + ImmutableList.of("%x","%v","%X","%V","%u","%U"), + ImmutableList.of("1999", "52", "1999", "52", "52", "52") + ), + new DateFormatTester("2000-01-01", + ImmutableList.of("%x","%v","%X","%V","%u","%U"), + ImmutableList.of("1999", "52", "1999", "52", "0", "0") + ), + new DateFormatTester("1998-12-31", + ImmutableList.of("%x","%v","%X","%V","%u","%U"), + ImmutableList.of("1998", "52", "1998", "52", "52", "52") + ), + new DateFormatTester("1999-01-01", + ImmutableList.of("%x","%v","%X","%V","%u","%U"), + ImmutableList.of("1998", "52", "1998", "52", "0", "0") + ), + new DateFormatTester("2020-01-04", + ImmutableList.of("%x","%X"), + ImmutableList.of("2020", "2019") + ), + new DateFormatTester("2008-12-31", + ImmutableList.of("%v","%V","%u","%U"), + ImmutableList.of("53","52","53","52") + ) + ); + + @AllArgsConstructor + private class DateFormatTester { + private final String date; + private final List formatterList; + private final List formattedList; + private static final String DELIMITER = "|"; + + String getFormatter() { + return String.join(DELIMITER, formatterList); + } + + String getFormatted() { + return String.join(DELIMITER, formattedList); + } + + FunctionExpression getDateFormatExpression() { + return dsl.date_format(DSL.literal(date), DSL.literal(getFormatter())); + } + } + @Test public void adddate() { FunctionExpression expr = dsl.adddate(dsl.date(DSL.literal("2020-08-26")), DSL.literal(7)); @@ -872,6 +958,48 @@ public void year() { assertEquals(integerValue(2020), eval(expression)); } + @Test + public void date_format() { + dateFormatTesters.forEach(this::testDateFormat); + 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 " + + "January 01 PM 01:14:15 PM 15 15 13:14:15 % P"; + + FunctionExpression expr = dsl.date_format(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("")))); + + 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("")))); + + 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("")))); + + 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))); + } + + void testDateFormat(DateFormatTester dft) { + FunctionExpression expr = dft.getDateFormatExpression(); + assertEquals(STRING, expr.type()); + assertEquals(dft.getFormatted(), eval(expr).stringValue()); + } + private ExprValue eval(Expression expression) { return expression.valueOf(env); } diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 72dd446594..7655668e20 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -835,17 +835,6 @@ Example:: +----------------------+------------------------------------------+ -DATE_FORMAT ------------ - -Description ->>>>>>>>>>> - -Specifications: - -1. DATE_FORMAT(DATE, STRING) -> STRING -2. DATE_FORMAT(DATE, STRING, STRING) -> STRING - DATE_ADD -------- @@ -877,6 +866,102 @@ Example:: +-------------------------------------------------+-----------------------------------+-------------------------------------------------+ +DATE_FORMAT +----------- + +Description +>>>>>>>>>>> + +Usage: date_format(date, format) formats the date argument using the specifiers in the format argument. + +.. list-table:: The following table describes the available specifier arguments. + :widths: 20 80 + :header-rows: 1 + + * - Specifier + - Description + * - %a + - Abbreviated weekday name (Sun..Sat) + * - %b + - Abbreviated month name (Jan..Dec) + * - %c + - Month, numeric (0..12) + * - %D + - Day of the month with English suffix (0th, 1st, 2nd, 3rd, …) + * - %d + - Day of the month, numeric (00..31) + * - %e + - Day of the month, numeric (0..31) + * - %f + - Microseconds (000000..999999) + * - %H + - Hour (00..23) + * - %h + - Hour (01..12) + * - %I + - Hour (01..12) + * - %i + - Minutes, numeric (00..59) + * - %j + - Day of year (001..366) + * - %k + - Hour (0..23) + * - %l + - Hour (1..12) + * - %M + - Month name (January..December) + * - %m + - Month, numeric (00..12) + * - %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) + * - %U + - Week (00..53), where Sunday is the first day of the week; WEEK() mode 0 + * - %u + - Week (00..53), where Monday is the first day of the week; WEEK() mode 1 + * - %V + - Week (01..53), where Sunday is the first day of the week; WEEK() mode 2; used with %X + * - %v + - Week (01..53), where Monday is the first day of the week; WEEK() mode 3; used with %x + * - %W + - Weekday name (Sunday..Saturday) + * - %w + - Day of the week (0=Sunday..6=Saturday) + * - %X + - Year for the week where Sunday is the first day of the week, numeric, four digits; used with %V + * - %x + - Year for the week, where Monday is the first day of the week, numeric, four digits; used with %v + * - %Y + - Year, numeric, four digits + * - %y + - Year, numeric (two digits) + * - %% + - A literal % character + * - %x + - x, for any “x” not listed above + +Argument type: STRING/DATE/DATETIME/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') + 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_SUB -------- diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java index 46891b482d..626f033bea 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java @@ -403,4 +403,33 @@ public void testYear() throws IOException { verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(2020)); } + + void verifyDateFormat(String date, String type, String format, String formatted) throws IOException { + JSONObject result = executeQuery(String.format( + "source=%s | eval f = date_format(%s('%s'), '%s') | fields f", + TEST_INDEX_DATE, type, date, format)); + verifySchema(result, schema("f", null, "string")); + verifySome(result.getJSONArray("datarows"), rows(formatted)); + + result = executeQuery(String.format( + "source=%s | eval f = date_format('%s', '%s') | fields f", + TEST_INDEX_DATE, date, format)); + verifySchema(result, schema("f", null, "string")); + verifySome(result.getJSONArray("datarows"), rows(formatted)); + } + + @Test + 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 " + + "January 01 PM 01:14:15 PM 15 15 13:14:15 % P"; + verifyDateFormat(timestamp, "timestamp", timestampFormat, timestampFormatted); + + String date = "1998-01-31"; + String dateFormat = "%U %u %V %v %W %w %X %x %Y %y"; + String dateFormatted = "4 4 4 4 Saturday 6 1998 1998 1998 98"; + verifyDateFormat(date, "date", dateFormat, dateFormatted); + } } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java index 52af0c80c4..69a6e997d2 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java @@ -373,6 +373,33 @@ public void testWeek() throws IOException { week("2000-01-01", 2, 52); } + void verifyDateFormat(String date, String type, String format, String formatted) throws IOException { + String query = String.format("date_format(%s('%s'), '%s')", type, date, format); + JSONObject result = executeQuery("select " + query); + verifySchema(result, schema(query, null, "string")); + verifyDataRows(result, rows(formatted)); + + query = String.format("date_format('%s', '%s')", date, format); + result = executeQuery("select " + query); + verifySchema(result, schema(query, null, "string")); + verifyDataRows(result, rows(formatted)); + } + + @Test + 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 " + + "January 01 PM 01:14:15 PM 15 15 13:14:15 % P"; + verifyDateFormat(timestamp, "timestamp", timestampFormat, timestampFormatted); + + String date = "1998-01-31"; + String dateFormat = "%U %u %V %v %W %w %X %x %Y %y"; + String dateFormatted = "4 4 4 4 Saturday 6 1998 1998 1998 98"; + verifyDateFormat(date, "date", dateFormat, dateFormatted); + } + 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/OpenDistroPPLLexer.g4 b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 index 55da2a2431..dbce42dc9d 100644 --- a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 @@ -213,6 +213,7 @@ SUBDATE: 'SUBDATE'; TIME: 'TIME'; TIME_TO_SEC: 'TIME_TO_SEC'; TIMESTAMP: 'TIMESTAMP'; +DATE_FORMAT: 'DATE_FORMAT'; TO_DAYS: 'TO_DAYS'; // TEXT FUNCTIONS diff --git a/ppl/src/main/antlr/OpenDistroPPLParser.g4 b/ppl/src/main/antlr/OpenDistroPPLParser.g4 index 1f822ac007..f04b21b2da 100644 --- a/ppl/src/main/antlr/OpenDistroPPLParser.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLParser.g4 @@ -233,9 +233,9 @@ trigonometricFunctionName ; dateAndTimeFunctionBase - : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS + : 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 | WEEK | YEAR + | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT ; textFunctionBase diff --git a/sql/src/main/antlr/OpenDistroSQLParser.g4 b/sql/src/main/antlr/OpenDistroSQLParser.g4 index eced4246a9..56194f0a6e 100644 --- a/sql/src/main/antlr/OpenDistroSQLParser.g4 +++ b/sql/src/main/antlr/OpenDistroSQLParser.g4 @@ -236,9 +236,9 @@ trigonometricFunctionName ; dateTimeFunctionName - : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS + : 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 | WEEK | YEAR + | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT ; textFunctionName