| /* |
| * 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.checkerframework.checker.nullness.qual.Nullable; |
| |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.Locale; |
| |
| import static org.apache.calcite.linq4j.Nullness.castNonNull; |
| |
| 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("F", "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); |
| sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_WEEK))); |
| } |
| }, |
| DAY("EEEE", "The full weekday name") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.eeeeFormat.format(date)); |
| } |
| }, |
| 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") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.eeeFormat.format(date)); |
| } |
| }, |
| 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(); |
| sb.append(work.sFormat.format(date)); |
| } |
| }, |
| FF2("SS", "Fractional seconds to 2 digits") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.ssFormat.format(date)); |
| } |
| }, |
| FF3("SSS", "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("SSSS", "Fractional seconds to 4 digits") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.ssssFormat.format(date)); |
| } |
| }, |
| FF5("SSSSS", "Fractional seconds to 5 digits") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.sssssFormat.format(date)); |
| } |
| }, |
| FF6("SSSSSS", "Fractional seconds to 6 digits") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.ssssssFormat.format(date)); |
| } |
| }, |
| 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))); |
| } |
| }, |
| // TODO: Ensure ISO 8601 for parsing |
| IW("w", "The ISO 8601 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) { |
| // TODO: ensure this is isoweek |
| final Calendar calendar = Work.get().calendar; |
| calendar.setTime(date); |
| calendar.setFirstDayOfWeek(Calendar.MONDAY); |
| 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; |
| 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") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.mmmFormat.format(date)); |
| } |
| }, |
| MONTH("MMMM", "The full month name (English)") { |
| @Override public void format(StringBuilder sb, Date date) { |
| final Work work = Work.get(); |
| sb.append(work.mmmmFormat.format(date)); |
| } |
| }, |
| // 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)") { |
| // TODO: Allow parsing of quarters. |
| @Override public void toPattern(StringBuilder sb) throws UnsupportedOperationException { |
| throw new UnsupportedOperationException("Cannot convert 'Q' FormatElement to Java pattern"); |
| } |
| @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)); |
| } |
| }, |
| 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))); |
| } |
| }, |
| 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); |
| calendar.setFirstDayOfWeek(Calendar.SUNDAY); |
| sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR))); |
| } |
| }, |
| 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)); |
| } |
| }, |
| YYYY("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); |
| } |
| |
| /** 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 ThreadLocal<@Nullable Work> THREAD_WORK = |
| ThreadLocal.withInitial(Work::new); |
| |
| /** Returns an instance of Work for this thread. */ |
| static Work get() { |
| return castNonNull(THREAD_WORK.get()); |
| } |
| |
| final Calendar calendar = |
| Calendar.getInstance(DateTimeUtils.DEFAULT_ZONE, Locale.ROOT); |
| |
| /** Uses Locale.US instead of Locale.ROOT to fix formatting in Java 11 */ |
| final DateFormat eeeeFormat = new SimpleDateFormat(DAY.javaFmt, Locale.US); |
| final DateFormat eeeFormat = new SimpleDateFormat(DY.javaFmt, Locale.ROOT); |
| final DateFormat mmmFormat = new SimpleDateFormat(MON.javaFmt, Locale.ROOT); |
| final DateFormat mmmmFormat = new SimpleDateFormat(MONTH.javaFmt, Locale.ROOT); |
| final DateFormat sFormat = new SimpleDateFormat(FF1.javaFmt, Locale.ROOT); |
| final DateFormat ssFormat = new SimpleDateFormat(FF2.javaFmt, Locale.ROOT); |
| final DateFormat sssFormat = new SimpleDateFormat(FF3.javaFmt, Locale.ROOT); |
| final DateFormat ssssFormat = new SimpleDateFormat(FF4.javaFmt, Locale.ROOT); |
| final DateFormat sssssFormat = new SimpleDateFormat(FF5.javaFmt, Locale.ROOT); |
| final DateFormat ssssssFormat = new SimpleDateFormat(FF6.javaFmt, Locale.ROOT); |
| final DateFormat yyFormat = new SimpleDateFormat(YY.javaFmt, Locale.ROOT); |
| } |
| } |