Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TIME_FORMAT() Function To SQL Plugin #1301

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions core/src/main/java/org/opensearch/sql/expression/DSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,10 @@ public static FunctionExpression timestamp(Expression... expressions) {
return compile(FunctionProperties.None, 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) {
Expand Down Expand Up @@ -810,6 +812,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<Integer, String> SUFFIX_CONVERTER =
ImmutableMap.<Integer, String>builder()
.put(1, "st").put(2, "nd").put(3, "rd").build();
Expand All @@ -33,7 +38,7 @@ interface DateTimeFormatHandler {
String getFormat(LocalDateTime date);
}

private static final Map<String, DateTimeFormatHandler> HANDLERS =
private static final Map<String, DateTimeFormatHandler> DATE_HANDLERS =
ImmutableMap.<String, DateTimeFormatHandler>builder()
.put("%a", (date) -> "EEE") // %a => EEE - Abbreviated weekday name (Sun..Sat)
.put("%b", (date) -> "LLL") // %b => LLL - Abbreviated month name (Jan..Dec)
Expand Down Expand Up @@ -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)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it - but I feel like we should use a constant for all of the surrounding format strings.

.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
Expand All @@ -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<String, DateTimeFormatHandler> TIME_HANDLERS =
ImmutableMap.<String, DateTimeFormatHandler>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("(?<!%)[a-zA-Z&&[^aydmshiHIMYDSEL]]+");
Expand All @@ -87,38 +131,76 @@ 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.
* Helper function to format a DATETIME according to a provided handler and matcher.
* @param formatExpr ExprValue containing the format expression
* @param handler Map of character patterns to their associated datetime format
* @param datetime The datetime argument being formatted
* @return A formatted string expression
*/
static ExprValue getFormattedDate(ExprValue dateExpr, ExprValue formatExpr) {
final LocalDateTime date = dateExpr.datetimeValue();
static ExprValue getFormattedString(ExprValue formatExpr,
Map<String, DateTimeFormatHandler> 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()));
}
m.appendTail(cleanFormat);

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,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());
Expand Down Expand Up @@ -853,6 +854,7 @@ private DefaultFunctionResolver year() {
* (STRING, STRING) -> STRING
* (DATE, STRING) -> STRING
* (DATETIME, STRING) -> STRING
* (TIME, STRING) -> STRING
* (TIMESTAMP, STRING) -> STRING
*/
private DefaultFunctionResolver date_format() {
Expand All @@ -863,6 +865,12 @@ private DefaultFunctionResolver date_format() {
STRING, DATE, STRING),
impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate),
STRING, DATETIME, STRING),
implWithProperties(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update javadoc also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 1af5666

nullMissingHandlingWithProperties(
(functionProperties, time, formatString)
-> DateTimeFormatterUtil.getFormattedDateOfToday(
formatString, time, functionProperties.getQueryStartClock())),
STRING, TIME, STRING),
impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate),
STRING, TIMESTAMP, STRING)
);
Expand Down Expand Up @@ -921,6 +929,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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")),
Expand Down Expand Up @@ -98,7 +99,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")),
Expand Down
Loading