FormatElementEnum.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.util.format;
import org.apache.calcite.avatica.util.DateTimeUtils;
import org.apache.calcite.util.TryThreadLocal;
import org.apache.commons.lang3.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import static java.util.Objects.requireNonNull;
/**
* Implementation of {@link FormatElement} containing the standard format
* elements. These are based on Oracle's format model documentation.
*
* <p>See
* <a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlqr/Format-Models.html">
* Oracle format model reference.</a>
*
* @see FormatModels#DEFAULT
*/
public enum FormatElementEnum implements FormatElement {
CC("cc", "century (2 digits) (the twenty-first century starts on 2001-01-01)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%2d", calendar.get(Calendar.YEAR) / 100 + 1));
}
},
D("", "The weekday (Sunday as the first day of the week) as a decimal number (1-7)") {
@Override public void toPattern(StringBuilder sb) throws UnsupportedOperationException {
throwToPatternNotImplemented();
}
@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)));
}
},
DAY("EEEE", "The full weekday name, in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.getDayFromDate(date, TextStyle.FULL).toUpperCase(Locale.ROOT));
}
},
Day("EEEE", "The full weekday name, capitalized") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.getDayFromDate(date, TextStyle.FULL));
}
},
day("EEEE", "The full weekday name, in lowercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.getDayFromDate(date, TextStyle.FULL).toLowerCase(Locale.ROOT));
}
},
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;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
}
},
DDD("D", "The day of the year as a decimal number (001-366)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%03d", calendar.get(Calendar.DAY_OF_YEAR)));
}
},
DY("EEE", "The abbreviated weekday name, in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.getDayFromDate(date, TextStyle.SHORT).toUpperCase(Locale.ROOT));
}
},
Dy("EEE", "The abbreviated weekday name, capitalized") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.getDayFromDate(date, TextStyle.SHORT));
}
},
dy("EEE", "The abbreviated weekday name, in lowercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.getDayFromDate(date, TextStyle.SHORT).toLowerCase(Locale.ROOT));
}
},
E("d", "The day of the month as a decimal number (1-31); "
+ "single digits are left-padded with space.") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%2d", calendar.get(Calendar.DAY_OF_MONTH)));
}
},
FF1("S", "Fractional seconds to 1 digit") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Extracting 1 decimal place as SimpleDateFormat returns precision with 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(work.sssFormat.format(date).charAt(0));
}
},
FF2("S", "Fractional seconds to 2 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Extracting 2 decimal places as SimpleDateFormat returns precision with 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(work.sssFormat.format(date), 0, 2);
}
},
FF3("S", "Fractional seconds to 3 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.sssFormat.format(date));
}
},
FF4("S", "Fractional seconds to 4 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Padding zeroes to right as SimpleDateFormat supports precision only up to 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(StringUtils.rightPad(work.sssFormat.format(date), 4, "0"));
}
},
FF5("S", "Fractional seconds to 5 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Padding zeroes to right as SimpleDateFormat supports precision only up to 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(StringUtils.rightPad(work.sssFormat.format(date), 5, "0"));
}
},
FF6("S", "Fractional seconds to 6 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Padding zeroes to right as SimpleDateFormat supports precision only up to 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(StringUtils.rightPad(work.sssFormat.format(date), 6, "0"));
}
},
FF7("S", "Fractional seconds to 6 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Padding zeroes to right as SimpleDateFormat supports precision only up to 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(StringUtils.rightPad(work.sssFormat.format(date), 7, "0"));
}
},
FF8("S", "Fractional seconds to 6 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Padding zeroes to right as SimpleDateFormat supports precision only up to 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(StringUtils.rightPad(work.sssFormat.format(date), 8, "0"));
}
},
FF9("S", "Fractional seconds to 6 digits") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
// Padding zeroes to right as SimpleDateFormat supports precision only up to 3 places.
// Refer to <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269">
// [CALCITE-6269] Fix missing/broken BigQuery date-time format elements</a>.
sb.append(StringUtils.rightPad(work.sssFormat.format(date), 9, "0"));
}
},
HH12("h", "The hour (12-hour clock) as a decimal number (01-12)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
int hour = calendar.get(Calendar.HOUR);
sb.append(String.format(Locale.ROOT, "%02d", hour == 0 ? 12 : hour));
}
},
HH24("H", "The hour (24-hour clock) as a decimal number (00-23)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
}
},
ID("u", "The weekday (Monday as the first day of the week) as a decimal number (1-7)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
int weekDay = calendar.get(Calendar.DAY_OF_WEEK);
// Converting Sun(1)...Sat(7) to Mon(1)...Sun(7)
sb.append(weekDay == 1 ? 7 : weekDay - 1);
}
},
IW("", "The ISO 8601 week number of the year (Monday as the first day of the week) "
+ "as a decimal number (01-53). If the week containing January 1 has four or more days "
+ "in the new year, then it is week 1; otherwise it is week 53 of the previous year, "
+ "and the next week is week 1.") {
@Override public void toPattern(StringBuilder sb) throws UnsupportedOperationException {
throwToPatternNotImplemented();
}
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().iso8601Calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)));
}
},
IYY("YY", "The ISO 8601 year without century as a decimal number. Each ISO year begins on "
+ "the Monday before the first Thursday of the Gregorian calendar year.") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().iso8601Calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.getWeekYear() % 100));
}
},
IYYYY("YYYY", "The ISO 8601 year with century as a decimal number. Each ISO year begins on "
+ "the Monday before the first Thursday of the Gregorian calendar year.") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().iso8601Calendar;
calendar.setTime(date);
sb.append(calendar.getWeekYear());
}
},
MI("m", "The minute as a decimal number (00-59)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.MINUTE)));
}
},
MM("MM", "The month as a decimal number (01-12)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.MONTH) + 1));
}
},
MON("MMM", "The abbreviated month name, in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.mmmFormat.format(date).toUpperCase(Locale.ROOT));
}
},
Mon("MMM", "The abbreviated month name, capitalized") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.mmmFormat.format(date));
}
},
mon("MMM", "The abbreviated month name, in lowercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.mmmFormat.format(date).toLowerCase(Locale.ROOT));
}
},
MONTH("MMMM", "The full month name (English), in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.mmmmFormat.format(date).toUpperCase(Locale.ROOT));
}
},
Month("MMMM", "The full month name (English), capitalized") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.mmmmFormat.format(date));
}
},
month("MMMM", "The full month name (English), in lowercase") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.mmmmFormat.format(date).toLowerCase(Locale.ROOT));
}
},
// PM can represent both AM and PM
PM("a", "Meridian indicator without periods") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
String meridian = calendar.get(Calendar.HOUR_OF_DAY) < 12 ? "AM" : "PM";
sb.append(meridian);
}
},
Q("", "The quarter as a decimal number (1-4)") {
@Override public void toPattern(StringBuilder sb) throws UnsupportedOperationException {
throwToPatternNotImplemented();
}
@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.MONTH) / 3) + 1));
}
},
AMPM("", "The time as Meridian Indicator in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(calendar.get(Calendar.AM_PM) == Calendar.AM ? "AM" : "PM");
}
},
AM_PM("", "The time as Meridian Indicator in uppercase with dot") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(calendar.get(Calendar.AM_PM) == Calendar.AM ? "A.M." : "P.M.");
}
},
ampm("", "The time as Meridian Indicator in lowercase") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(calendar.get(Calendar.AM_PM) == Calendar.AM ? "am" : "pm");
}
},
am_pm("", "The time as Meridian Indicator in uppercase") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(calendar.get(Calendar.AM_PM) == Calendar.AM ? "a.m." : "p.m.");
}
},
MS("SSS", "The millisecond as a decimal number (000-999)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%03d", calendar.get(Calendar.MILLISECOND)));
}
},
SS("s", "The second as a decimal number (00-60)") {
@Override public void format(StringBuilder sb, Date date) {
final Calendar calendar = Work.get().calendar;
calendar.setTime(date);
sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.SECOND)));
}
},
SSSSS("s", "The seconds of the day (00000-86400)") {
@Override public void format(StringBuilder sb, Date date) {
Calendar calendar = Work.get().calendar;
calendar.setTime(date);
long timeInMillis = calendar.getTimeInMillis();
// Set calendar to start of day for input date
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
long dayStartInMillis = calendar.getTimeInMillis();
// Get seconds of the day as difference from day start time
long secondsPassed = (timeInMillis - dayStartInMillis) / 1000;
sb.append(String.format(Locale.ROOT, "%05d", secondsPassed));
}
},
TZR("z", "The time zone name") {
@Override public void format(StringBuilder sb, Date date) {
// TODO: how to support timezones?
}
},
W("W", "The week number of the month (Sunday as the first day of the week) as a decimal "
+ "number (1-5)") {
@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.WEEK_OF_MONTH)));
}
},
WW("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);
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();
String formattedYear = work.yyFormat.format(date);
sb.append(formattedYear.substring(formattedYear.length() - 1));
}
},
YY("yy", "Last 2 digits of year") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.yyFormat.format(date));
}
},
YYY("yyy", "Last 3 digits of year") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
String formattedYear = work.yyyyFormat.format(date);
sb.append(formattedYear.substring(formattedYear.length() - 3));
}
},
YYYY("yyyy", "The year with century as a decimal number") {
@Override public void format(StringBuilder sb, Date date) {
final Work work = Work.get();
sb.append(work.yyyyFormat.format(date));
}
},
pctY("yyyy", "The year with century as a decimal number") {
@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.YEAR)));
}
};
private final String description;
final String javaFmt;
// TODO: be sure to deal with TZ
FormatElementEnum(String javaFmt, String description) {
this.javaFmt = requireNonNull(javaFmt, "javaFmt");
this.description = requireNonNull(description, "description");
}
@Override public String getDescription() {
return description;
}
@Override public void toPattern(StringBuilder sb) {
sb.append(this.javaFmt);
}
final void throwToPatternNotImplemented() {
throw new UnsupportedOperationException("Cannot convert '"
+ this.name().toUpperCase(Locale.ROOT) + "' FormatElement to Java pattern");
}
/** Work space. Provides a value for each mutable data structure that might
* be needed by a format element. Ensures thread-safety. */
static class Work {
private static final TryThreadLocal<Work> THREAD_WORK =
TryThreadLocal.withInitial(Work::new);
/** Returns an instance of Work for this thread. */
static Work get() {
return THREAD_WORK.get();
}
final Calendar calendar = new Calendar.Builder()
.setWeekDefinition(Calendar.SUNDAY, 1)
.setTimeZone(DateTimeUtils.DEFAULT_ZONE)
.setLocale(Locale.ROOT).build();
final Calendar iso8601Calendar = new Calendar.Builder()
.setCalendarType("iso8601")
.setTimeZone(DateTimeUtils.DEFAULT_ZONE)
.setLocale(Locale.ROOT).build();
final DateFormat mmmFormat = new SimpleDateFormat(MON.javaFmt, Locale.US);
/* Need to sse Locale.US instead of Locale.ROOT, because Locale.ROOT
* may actually return the *short* month name instead of the long name.
* See [CALCITE-6252] BigQuery FORMAT_DATE uses the wrong calendar for Julian dates:
* https://issues.apache.org/jira/browse/CALCITE-6252. This may be
* specific to Java 11. */
final DateFormat mmmmFormat = new SimpleDateFormat(MONTH.javaFmt, Locale.US);
final DateFormat sssFormat = new SimpleDateFormat(FF3.javaFmt, Locale.ROOT);
final DateFormat yyFormat = new SimpleDateFormat(YY.javaFmt, Locale.ROOT);
final DateFormat yyyyFormat = new SimpleDateFormat(YYYY.javaFmt, Locale.ROOT);
/** Util to return the full or abbreviated weekday name from date and expected TextStyle. */
private String getDayFromDate(Date date, TextStyle style) {
calendar.setTime(date);
// The Calendar and SimpleDateFormatter do not seem to give correct results
// for the day of the week prior to the Julian to Gregorian date change.
// So we resort to using a LocalDate representation.
LocalDate ld =
LocalDate.of(calendar.get(Calendar.YEAR),
// Calendar months are numbered from 0
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH));
return ld.getDayOfWeek().getDisplayName(style, Locale.ENGLISH);
}
}
}