Skip to content

Commit

Permalink
[CALCITE-6551] Add DATE_FORMAT function (enabled in MySQL library)
Browse files Browse the repository at this point in the history
  • Loading branch information
suibianwanwank committed Aug 28, 2024
1 parent 99a0df1 commit a9118e5
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATEADD;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATETIME;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATETIME_TRUNC;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_FORMAT;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_FROM_UNIX_DATE;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_PART;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_TRUNC;
Expand Down Expand Up @@ -833,6 +834,7 @@ Builder populate2() {
// Datetime formatting methods
defineReflective(TO_CHAR, BuiltInMethod.TO_CHAR.method);
defineReflective(TO_CHAR_PG, BuiltInMethod.TO_CHAR_PG.method);
defineReflective(DATE_FORMAT, BuiltInMethod.DATE_FORMAT.method);
defineReflective(TO_DATE, BuiltInMethod.TO_DATE.method);
defineReflective(TO_DATE_PG, BuiltInMethod.TO_DATE_PG.method);
defineReflective(TO_TIMESTAMP, BuiltInMethod.TO_TIMESTAMP.method);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4313,6 +4313,14 @@ public String toCharPg(long timestamp, String pattern) {
return PostgresqlDateTimeFormatter.toChar(pattern, zonedDateTime).trim();
}

public String dateFormat(long timestamp, String pattern) {
final Timestamp sqlTimestamp = internalToTimestamp(timestamp);
sb.setLength(0);
withElements(FormatModels.MYSQL, pattern, elements ->
elements.forEach(element -> element.format(sb, sqlTimestamp)));
return sb.toString().trim();
}

public int toDate(String dateString, String fmtString) {
return toInt(
new java.sql.Date(internalToDateTime(dateString, fmtString)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,16 @@ private static RelDataType deriveTypeMapFromEntries(SqlOperatorBinding opBinding
OperandHandlers.DEFAULT, OperandTypes.TIMESTAMP_STRING, 0,
SqlFunctionCategory.TIMEDATE, call -> SqlMonotonicity.NOT_MONOTONIC, false) { };


/** The "DATE_FORMAT(timestamp, format)" function;
* converts {@code timestamp} to string according to the given {@code format}. */
@LibraryOperator(libraries = {MYSQL})
public static final SqlFunction DATE_FORMAT =
SqlBasicFunction.create("DATE_FORMAT",
ReturnTypes.VARCHAR_NULLABLE,
OperandTypes.TIMESTAMP_STRING,
SqlFunctionCategory.TIMEDATE);

/** The "TO_DATE(string1, string2)" function; casts string1
* to a DATE using the format specified in string2. */
@LibraryOperator(libraries = {ORACLE, REDSHIFT})
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,8 @@ public enum BuiltInMethod {
String.class),
TO_CHAR_PG(SqlFunctions.DateFormatFunction.class, "toCharPg", long.class,
String.class),
DATE_FORMAT(SqlFunctions.DateFormatFunction.class, "dateFormat", long.class,
String.class),
TO_DATE(SqlFunctions.DateFormatFunction.class, "toDate", String.class,
String.class),
TO_DATE_PG(SqlFunctions.DateFormatFunction.class, "toDatePg", String.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
Expand Down Expand Up @@ -59,6 +61,13 @@ public enum FormatElementEnum implements FormatElement {
sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_WEEK)));
}
},
D0("", "The weekday (Monday as the first day of the week) as a decimal number (0-6)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_WEEK) - 1));
}
},
DAY("EEEE", "The full weekday name, in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
Expand All @@ -77,6 +86,21 @@ public enum FormatElementEnum implements FormatElement {
sb.append(work.getDayFromDate(date, TextStyle.FULL).toLowerCase(Locale.ROOT));
}
},
DS("", "The month as a decimal number with suffix. (1st-31th)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
int d = calendar.get(Calendar.DAY_OF_MONTH);
sb.append(String.format(Locale.ROOT, "%d%s", d, getNumericSuffix(d)));
}
},
D1("dd", "The day of the month as a decimal number (1-31)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_MONTH)));
}
},
DD("dd", "The day of the month as a decimal number (01-31)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
Expand Down Expand Up @@ -221,6 +245,33 @@ public enum FormatElementEnum implements FormatElement {
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)));
}
},
V("", "The number of the year (Monday as the first day of the week),"
+ "If the first day of the week belongs to the previous year, "
+ "calculate the week of the previous year"
+ "as a decimal number (01-53)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setFirstDayOfWeek(Calendar.SUNDAY);
// Week 1 is the first week with a Sunday in it
int minimalDaysInFirstWeek =
getMinimalDaysInFirstWeekByWeekdayName(DayOfWeek.MONDAY, calendar.get(Calendar.YEAR));
calendar.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)));
}
},
v("", "The number of the year (Sunday as the first day of the week),"
+ "If the first day of the week belongs to the previous year, "
+ "calculate the week of the previous year"
+ "as a decimal number (01-53)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setFirstDayOfWeek(Calendar.MONDAY);
calendar.setMinimalDaysInFirstWeek(4);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)));
}
},
MI("m", "The minute as a decimal number (00-59)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
Expand Down Expand Up @@ -326,7 +377,16 @@ public enum FormatElementEnum implements FormatElement {
sb.append(String.format(Locale.ROOT, "%03d", calendar.get(Calendar.MILLISECOND)));
}
},
SS("s", "The second as a decimal number (00-60)") {
MCS("", "The millisecond as a decimal number (000000-999999)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
// It exceeds the precision of Calendar, we fill it with 0.
// So the actual range is (000000-999000)
sb.append(String.format(Locale.ROOT, "%06d", calendar.get(Calendar.MILLISECOND) * 1000));
}
},
SS("s", "The second as a decimal number (00-59)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
Expand Down Expand Up @@ -369,10 +429,31 @@ public enum FormatElementEnum implements FormatElement {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setMinimalDaysInFirstWeek(1);
calendar.setFirstDayOfWeek(Calendar.SUNDAY);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)));
}
},
WW1("w", "The week number of the year (Sunday as the first day of the week) as a decimal "
+ "number (00-53)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setMinimalDaysInFirstWeek(1);
calendar.setFirstDayOfWeek(Calendar.SUNDAY);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR) - 1));
}
},
WW2("", "The week number of the year (Monday as the first day of the week) as a decimal "
+ "number (01-53)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setMinimalDaysInFirstWeek(1);
calendar.setFirstDayOfWeek(Calendar.MONDAY);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)));
}
},
Y("y", "Last digit of year") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
Expand All @@ -399,6 +480,27 @@ public enum FormatElementEnum implements FormatElement {
sb.append(work.yyyyFormat.format(date));
}
},
WFY("", "The year for the week where Sunday is the first day of the week") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setFirstDayOfWeek(Calendar.SUNDAY);
// Week 1 is the first week with a Sunday in it
int minimalDaysInFirstWeek =
getMinimalDaysInFirstWeekByWeekdayName(DayOfWeek.MONDAY, calendar.get(Calendar.YEAR));
calendar.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
sb.append(String.format(Locale.ROOT, "%04d", calendar.getWeekYear()));
}
},
WFY0("", "The year for the week where Monday is the first day of the week") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
calendar.setFirstDayOfWeek(Calendar.MONDAY);
calendar.setMinimalDaysInFirstWeek(4);
sb.append(String.format(Locale.ROOT, "%04d", calendar.getWeekYear()));
}
},
pctY("yyyy", "The year with century as a decimal number") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
Expand Down Expand Up @@ -464,4 +566,32 @@ private String getDayFromDate(Date date, TextStyle style) {
return ld.getDayOfWeek().getDisplayName(style, Locale.ENGLISH);
}
}

/** Calculate what day of the year the first weekdayName of the given year is. */
private static int getMinimalDaysInFirstWeekByWeekdayName(DayOfWeek weekdayName, int year) {
// get
return LocalDate.of(year, 1, 1)
.with(ChronoField.DAY_OF_WEEK, weekdayName.getValue())
.getDayOfYear();
}

/** Util to return the suffix of numeric. */
private static String getNumericSuffix(int numeric) {
String outputSuffix;
switch (numeric) {
case 1:
outputSuffix = "st";
break;
case 2:
outputSuffix = "nd";
break;
case 3:
outputSuffix = "rd";
break;
default:
outputSuffix = "th";
break;
}
return outputSuffix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@
import static org.apache.calcite.util.format.FormatElementEnum.AM_PM;
import static org.apache.calcite.util.format.FormatElementEnum.CC;
import static org.apache.calcite.util.format.FormatElementEnum.D;
import static org.apache.calcite.util.format.FormatElementEnum.D0;
import static org.apache.calcite.util.format.FormatElementEnum.D1;
import static org.apache.calcite.util.format.FormatElementEnum.DAY;
import static org.apache.calcite.util.format.FormatElementEnum.DD;
import static org.apache.calcite.util.format.FormatElementEnum.DDD;
import static org.apache.calcite.util.format.FormatElementEnum.DS;
import static org.apache.calcite.util.format.FormatElementEnum.DY;
import static org.apache.calcite.util.format.FormatElementEnum.Day;
import static org.apache.calcite.util.format.FormatElementEnum.Dy;
Expand All @@ -52,6 +55,7 @@
import static org.apache.calcite.util.format.FormatElementEnum.HH12;
import static org.apache.calcite.util.format.FormatElementEnum.HH24;
import static org.apache.calcite.util.format.FormatElementEnum.IW;
import static org.apache.calcite.util.format.FormatElementEnum.MCS;
import static org.apache.calcite.util.format.FormatElementEnum.MI;
import static org.apache.calcite.util.format.FormatElementEnum.MM;
import static org.apache.calcite.util.format.FormatElementEnum.MON;
Expand All @@ -64,8 +68,13 @@
import static org.apache.calcite.util.format.FormatElementEnum.SS;
import static org.apache.calcite.util.format.FormatElementEnum.SSSSS;
import static org.apache.calcite.util.format.FormatElementEnum.TZR;
import static org.apache.calcite.util.format.FormatElementEnum.V;
import static org.apache.calcite.util.format.FormatElementEnum.W;
import static org.apache.calcite.util.format.FormatElementEnum.WFY;
import static org.apache.calcite.util.format.FormatElementEnum.WFY0;
import static org.apache.calcite.util.format.FormatElementEnum.WW;
import static org.apache.calcite.util.format.FormatElementEnum.WW1;
import static org.apache.calcite.util.format.FormatElementEnum.WW2;
import static org.apache.calcite.util.format.FormatElementEnum.Y;
import static org.apache.calcite.util.format.FormatElementEnum.YY;
import static org.apache.calcite.util.format.FormatElementEnum.YYY;
Expand All @@ -77,6 +86,7 @@
import static org.apache.calcite.util.format.FormatElementEnum.mon;
import static org.apache.calcite.util.format.FormatElementEnum.month;
import static org.apache.calcite.util.format.FormatElementEnum.pctY;
import static org.apache.calcite.util.format.FormatElementEnum.v;

import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -110,6 +120,14 @@ private FormatModels() {
*/
public static final FormatModel POSTGRESQL;

/** Format model for MySQL.
*
* <p>MySQL format element reference:
* <a href="https://dev.mysql.com/doc/refman/8.4/en/date-and-time-functions.html#function_date-format">
* MySQL Standard SQL Format Elements</a>.
*/
public static final FormatModel MYSQL = createMysqlFormatModel();

static {
final Map<String, FormatElement> map = new LinkedHashMap<>();
for (FormatElementEnum fe : FormatElementEnum.values()) {
Expand Down Expand Up @@ -231,6 +249,47 @@ MI, literalElement(":"), SS, literalElement(" "),
POSTGRESQL = create(map);
}

private static FormatModel createMysqlFormatModel() {
final Map<String, FormatElement> map = new LinkedHashMap<>();
map.put("%a", Dy);
map.put("%b", Mon);
map.put("%c", MM);
map.put("%D", DS);
map.put("%d", DD);
map.put("%e", D1);
map.put("%f", MCS);
map.put("%H", HH24);
map.put("%h", HH12);
map.put("%I", HH12);
map.put("%i", MI);
map.put("%j", DDD);
map.put("%k", HH24);
map.put("%l", HH12);
map.put("%M", Month);
map.put("%m", MM);
map.put("%p", PM);
map.put("%r",
compositeElement("The date representation in hh:mm:ss am/pm format.",
HH12, literalElement(":"), MI, literalElement(":"), SS, literalElement(" "), PM));
map.put("%S", SS);
map.put("%s", SS);
map.put("%T",
compositeElement("The date representation in hh:mm:ss am/pm format.",
HH24, literalElement(":"), MI, literalElement(":"), SS));
map.put("%U", WW1);
map.put("%u", WW2);
map.put("%V", V);
map.put("%v", v);
map.put("%W", Day);
map.put("%w", D0);
map.put("%X", WFY);
map.put("%x", WFY0);
map.put("%Y", YYYY);
map.put("%y", YY);
map.put("%%", literalElement("%"));
return create(map);
}

/**
* Generates a {@link Pattern} using the keys of a {@link FormatModel} element
* map. This pattern is used in {@link FormatModel#parse(String)} to help
Expand Down
Loading

0 comments on commit a9118e5

Please sign in to comment.