Skip to content

Commit

Permalink
Add TIME_FORMAT() Function To SQL Plugin
Browse files Browse the repository at this point in the history
Adds the time_format function to the SQL plugin. The function takes a DATE/DATETIME/TIME/TIMESTAMP/STRING in a valid format, and a formatting string (e.g., "%h %p") and outputs a formatted string. If a DATE is passed in, it treats the argument as if it is taken at midnight (i.e., 00:00:00).

Signed-off-by: GabeFernandez310 <[email protected]>
  • Loading branch information
GabeFernandez310 authored Feb 15, 2023
1 parent de40f42 commit bae0f6b
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 42 deletions.
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 @@ -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) {
Expand Down Expand Up @@ -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);
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)))
.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 @@ -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());
Expand Down Expand Up @@ -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() {
Expand All @@ -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)
);
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down Expand Up @@ -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")),
Expand Down
Loading

0 comments on commit bae0f6b

Please sign in to comment.