diff --git a/babel/src/test/resources/sql/postgresql.iq b/babel/src/test/resources/sql/postgresql.iq index dc8a98d7ca78..af6b09744cbd 100644 --- a/babel/src/test/resources/sql/postgresql.iq +++ b/babel/src/test/resources/sql/postgresql.iq @@ -263,17 +263,17 @@ EXPR$0 a.d. !ok -select to_char(timestamp '2022-06-03 12:15:48.678', 'MONTH'); +select to_char(timestamp '2022-06-03 12:15:48.678', 'FMMONTH'); EXPR$0 JUNE !ok -select to_char(timestamp '2022-06-03 12:15:48.678', 'Month'); +select to_char(timestamp '2022-06-03 12:15:48.678', 'FMMonth'); EXPR$0 June !ok -select to_char(timestamp '2022-06-03 12:15:48.678', 'month'); +select to_char(timestamp '2022-06-03 12:15:48.678', 'FMmonth'); EXPR$0 june !ok @@ -293,17 +293,17 @@ EXPR$0 jun !ok -select to_char(timestamp '2022-06-03 12:15:48.678', 'DAY'); +select to_char(timestamp '2022-06-03 12:15:48.678', 'FMDAY'); EXPR$0 FRIDAY !ok -select to_char(timestamp '2022-06-03 12:15:48.678', 'Day'); +select to_char(timestamp '2022-06-03 12:15:48.678', 'FMDay'); EXPR$0 Friday !ok -select to_char(timestamp '2022-06-03 12:15:48.678', 'day'); +select to_char(timestamp '2022-06-03 12:15:48.678', 'FMday'); EXPR$0 friday !ok diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index f7b208c0a849..81ac8b364cfe 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -50,6 +50,7 @@ import org.apache.calcite.util.format.FormatElement; import org.apache.calcite.util.format.FormatModel; import org.apache.calcite.util.format.FormatModels; +import org.apache.calcite.util.format.postgresql.CompiledDateTimeFormat; import org.apache.calcite.util.format.postgresql.PostgresqlDateTimeFormatter; import org.apache.commons.codec.DecoderException; @@ -4372,6 +4373,11 @@ private static class Key extends MapEntry { .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value()) .build(CacheLoader.from(key -> key.t.parseNoCache(key.u))); + private static final LoadingCache FORMAT_CACHE_PG = + CacheBuilder.newBuilder() + .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value()) + .build(CacheLoader.from(PostgresqlDateTimeFormatter::compilePattern)); + /** Given a format string and a format model, calls an action with the * list of elements obtained by parsing that format string. */ protected final void withElements(FormatModel formatModel, String format, @@ -4405,10 +4411,11 @@ public String toChar(long timestamp, String pattern) { public static String toCharPg(DataContext root, long timestamp, String pattern) { final ZoneId zoneId = DataContext.Variable.TIME_ZONE.get(root).toZoneId(); final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(root)); + final CompiledDateTimeFormat dateTimeFormat = FORMAT_CACHE_PG.getUnchecked(pattern); final Timestamp sqlTimestamp = internalToTimestamp(timestamp); final ZonedDateTime zonedDateTime = ZonedDateTime.of(sqlTimestamp.toLocalDateTime(), zoneId); - return PostgresqlDateTimeFormatter.toChar(pattern, zonedDateTime, locale).trim(); + return dateTimeFormat.formatDateTime(zonedDateTime, locale); } public int toDate(String dateString, String fmtString) { @@ -4419,9 +4426,9 @@ public int toDate(String dateString, String fmtString) { public static int toDatePg(DataContext root, String dateString, String fmtString) { try { final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(root)); - return (int) PostgresqlDateTimeFormatter.toTimestamp(dateString, fmtString, LOCAL_ZONE, - locale) - .getLong(ChronoField.EPOCH_DAY); + final CompiledDateTimeFormat dateTimeFormat = FORMAT_CACHE_PG.getUnchecked(fmtString); + return (int) dateTimeFormat.parseDateTime(dateString, LOCAL_ZONE, locale).getLong( + ChronoField.EPOCH_DAY); } catch (Exception e) { SQLException sqlEx = new SQLException( @@ -4440,9 +4447,10 @@ public long toTimestamp(String timestampString, String fmtString) { public static long toTimestampPg(DataContext root, String timestampString, String fmtString) { try { final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(root)); - return PostgresqlDateTimeFormatter.toTimestamp(timestampString, fmtString, LOCAL_ZONE, - locale) - .toInstant().toEpochMilli(); + final CompiledDateTimeFormat dateTimeFormat = FORMAT_CACHE_PG.getUnchecked(fmtString); + return dateTimeFormat.parseDateTime(timestampString, LOCAL_ZONE, locale) + .toInstant() + .toEpochMilli(); } catch (Exception e) { SQLException sqlEx = new SQLException( diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/CompiledDateTimeFormat.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/CompiledDateTimeFormat.java new file mode 100644 index 000000000000..0941f8f6cb10 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/CompiledDateTimeFormat.java @@ -0,0 +1,391 @@ +/* + * 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.postgresql; + +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledItem; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.LiteralCompiledItem; + +import com.google.common.collect.ImmutableList; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.IsoFields; +import java.time.temporal.JulianFields; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Contains a parsed date/time format. Able to parse a string into a date/time value, + * or convert a date/time value to a string. + */ +public class CompiledDateTimeFormat { + private final ImmutableList compiledItems; + + public CompiledDateTimeFormat(ImmutableList compiledItems) { + this.compiledItems = compiledItems; + } + + /** + * Parses a date/time value from a string. The format used is in compiledItems. + * + * @param input the String to parse + * @param zoneId timezone to convert the result to + * @param locale Locale to use for parsing day or month names if the TM modifier was present + * @return the parsed date/time value + * @throws ParseException if the string to parse did not meet the required format + */ + public ZonedDateTime parseDateTime(String input, ZoneId zoneId, Locale locale) + throws ParseException { + final ParsePosition parsePosition = new ParsePosition(0); + final Map dateTimeParts = new HashMap<>(); + + for (int i = 0; i < compiledItems.size(); i++) { + final CompiledItem currentFormatItem = compiledItems.get(i); + final boolean nextItemNumeric = isItemNumeric(i + 1); + + if (currentFormatItem instanceof LiteralCompiledItem) { + final LiteralCompiledItem literal = (LiteralCompiledItem) currentFormatItem; + parsePosition.setIndex(parsePosition.getIndex() + literal.getFormatPatternLength()); + } else if (currentFormatItem instanceof CompiledPattern) { + final CompiledPattern pattern = (CompiledPattern) currentFormatItem; + dateTimeParts.put( + pattern.getChronoUnit(), pattern.parseValue( + parsePosition, input, nextItemNumeric, locale)); + } + } + + return constructDateTimeFromParts(dateTimeParts, zoneId); + } + + private boolean isItemNumeric(int index) { + if (index < compiledItems.size() && compiledItems.get(index) instanceof CompiledPattern) { + return ((CompiledPattern) compiledItems.get(index)).isNumeric(); + } + + return false; + } + + /** + * Converts a date/time value to a string. The output format is in compiledItems. + * + * @param dateTime date/time value to convert + * @param locale Locale to use for outputting day and month names if the TM modifier was present + * @return the date/time value formatted as a String + */ + public String formatDateTime(ZonedDateTime dateTime, Locale locale) { + final StringBuilder outputBuilder = new StringBuilder(); + + for (CompiledItem compiledItem : compiledItems) { + outputBuilder.append(compiledItem.convertToString(dateTime, locale)); + } + + return outputBuilder.toString(); + } + + private static ZonedDateTime constructDateTimeFromParts(Map dateParts, + ZoneId zoneId) { + LocalDateTime constructedDateTime = LocalDateTime.now(zoneId) + .truncatedTo(ChronoUnit.DAYS); + + DateCalendarEnum calendar = DateCalendarEnum.NONE; + boolean containsCentury = false; + for (ChronoUnitEnum unit : dateParts.keySet()) { + if (unit.getCalendars().size() == 1) { + DateCalendarEnum unitCalendar = unit.getCalendars().iterator().next(); + if (unitCalendar != DateCalendarEnum.NONE) { + calendar = unitCalendar; + break; + } + } else if (unit == ChronoUnitEnum.CENTURIES) { + containsCentury = true; + } + } + + if (calendar == DateCalendarEnum.NONE && containsCentury) { + calendar = DateCalendarEnum.GREGORIAN; + } + + switch (calendar) { + case NONE: + constructedDateTime = constructedDateTime + .withYear(1) + .withMonth(1) + .withDayOfMonth(1); + break; + case GREGORIAN: + constructedDateTime = updateWithGregorianFields(constructedDateTime, dateParts); + break; + case ISO_8601: + constructedDateTime = updateWithIso8601Fields(constructedDateTime, dateParts); + break; + case JULIAN: + final Integer julianDays = dateParts.get(ChronoUnitEnum.DAYS_JULIAN); + if (julianDays != null) { + constructedDateTime = constructedDateTime.with(JulianFields.JULIAN_DAY, julianDays); + } + break; + } + + constructedDateTime = updateWithTimeFields(constructedDateTime, dateParts); + + if (dateParts.containsKey(ChronoUnitEnum.TIMEZONE_HOURS) + || dateParts.containsKey(ChronoUnitEnum.TIMEZONE_MINUTES)) { + final int hours = dateParts.getOrDefault(ChronoUnitEnum.TIMEZONE_HOURS, 0); + final int minutes = dateParts.getOrDefault(ChronoUnitEnum.TIMEZONE_MINUTES, 0); + + return ZonedDateTime.of(constructedDateTime, ZoneOffset.ofHoursMinutes(hours, minutes)) + .withZoneSameInstant(zoneId); + } + + return ZonedDateTime.of(constructedDateTime, zoneId); + } + + private static LocalDateTime updateWithGregorianFields(LocalDateTime dateTime, + Map dateParts) { + LocalDateTime updatedDateTime = dateTime.withYear(getGregorianYear(dateParts)).withDayOfYear(1); + + if (dateParts.containsKey(ChronoUnitEnum.MONTHS_IN_YEAR)) { + updatedDateTime = + updatedDateTime.withMonth(dateParts.get(ChronoUnitEnum.MONTHS_IN_YEAR)); + } + + if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_MONTH)) { + updatedDateTime = + updatedDateTime.withDayOfMonth(dateParts.get(ChronoUnitEnum.DAYS_IN_MONTH)); + } + + if (dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_MONTH)) { + updatedDateTime = + updatedDateTime.withDayOfMonth( + dateParts.get(ChronoUnitEnum.WEEKS_IN_MONTH) * 7 - 6); + } + + if (dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_YEAR)) { + updatedDateTime = + updatedDateTime.withDayOfYear( + dateParts.get(ChronoUnitEnum.WEEKS_IN_YEAR) * 7 - 6); + } + + if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_YEAR)) { + updatedDateTime = + updatedDateTime.withDayOfYear(dateParts.get(ChronoUnitEnum.DAYS_IN_YEAR)); + } + + return updatedDateTime; + } + + private static int getGregorianYear(Map dateParts) { + int year = + getYear( + dateParts.get(ChronoUnitEnum.ERAS), + dateParts.get(ChronoUnitEnum.YEARS), + dateParts.get(ChronoUnitEnum.CENTURIES), + dateParts.get(ChronoUnitEnum.YEARS_IN_MILLENIA), + dateParts.get(ChronoUnitEnum.YEARS_IN_CENTURY)); + return year == 0 ? 1 : year; + } + + private static LocalDateTime updateWithIso8601Fields(LocalDateTime dateTime, + Map dateParts) { + final int year = getIso8601Year(dateParts); + + if (!dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601) + && !dateParts.containsKey(ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601)) { + return dateTime.withYear(year).withDayOfYear(1); + } + + LocalDateTime updatedDateTime = dateTime + .with(ChronoField.DAY_OF_WEEK, 1) + .with(IsoFields.WEEK_BASED_YEAR, year) + .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 1); + + if (dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601)) { + updatedDateTime = + updatedDateTime.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, + dateParts.get(ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601)); + + if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_WEEK)) { + updatedDateTime = + updatedDateTime.with(ChronoField.DAY_OF_WEEK, + dateParts.get(ChronoUnitEnum.DAYS_IN_WEEK)); + } + } else if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601)) { + updatedDateTime = + updatedDateTime.plusDays(dateParts.get(ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601) - 1); + } + + return updatedDateTime; + } + + private static int getIso8601Year(Map dateParts) { + int year = + getYear( + dateParts.get(ChronoUnitEnum.ERAS), + dateParts.get(ChronoUnitEnum.YEARS_ISO_8601), + dateParts.get(ChronoUnitEnum.CENTURIES), + dateParts.get(ChronoUnitEnum.YEARS_IN_MILLENIA_ISO_8601), + dateParts.get(ChronoUnitEnum.YEARS_IN_CENTURY_ISO_8601)); + return year == 0 ? 1 : year; + } + + private static int getYear(@Nullable Integer era, @Nullable Integer years, + @Nullable Integer centuries, @Nullable Integer yearsInMillenia, + @Nullable Integer yearsInCentury) { + int yearSign = 1; + if (era != null) { + if (era == 0) { + yearSign = -1; + } + } + + if (yearsInMillenia != null) { + int year = yearsInMillenia; + if (year < 520) { + year += 2000; + } else { + year += 1000; + } + + return yearSign * year; + } + + if (centuries != null) { + int year = 100 * (centuries - 1); + + if (yearsInCentury != null) { + year += yearsInCentury; + } else { + year += 1; + } + + return yearSign * year; + } + + if (years != null) { + return yearSign * years; + } + + if (yearsInCentury != null) { + int year = yearsInCentury; + if (year < 70) { + year += 2000; + } else if (year < 100) { + year += 1900; + } + + return yearSign * year; + } + + return yearSign; + } + + private static LocalDateTime updateWithTimeFields(LocalDateTime dateTime, + Map dateParts) { + LocalDateTime updatedDateTime = dateTime; + + if (dateParts.containsKey(ChronoUnitEnum.HOURS_IN_DAY)) { + updatedDateTime = + updatedDateTime.withHour(dateParts.get(ChronoUnitEnum.HOURS_IN_DAY)); + } + + if (dateParts.containsKey(ChronoUnitEnum.HALF_DAYS) + && dateParts.containsKey(ChronoUnitEnum.HOURS_IN_HALF_DAY)) { + updatedDateTime = + updatedDateTime.withHour(dateParts.get(ChronoUnitEnum.HALF_DAYS) * 12 + + dateParts.get(ChronoUnitEnum.HOURS_IN_HALF_DAY)); + } else if (dateParts.containsKey(ChronoUnitEnum.HOURS_IN_HALF_DAY)) { + updatedDateTime = + updatedDateTime.withHour(dateParts.get(ChronoUnitEnum.HOURS_IN_HALF_DAY)); + } + + if (dateParts.containsKey(ChronoUnitEnum.MINUTES_IN_HOUR)) { + updatedDateTime = + updatedDateTime.withMinute(dateParts.get(ChronoUnitEnum.MINUTES_IN_HOUR)); + } + + if (dateParts.containsKey(ChronoUnitEnum.SECONDS_IN_DAY)) { + updatedDateTime = + updatedDateTime.with(ChronoField.SECOND_OF_DAY, + dateParts.get(ChronoUnitEnum.SECONDS_IN_DAY)); + } + + if (dateParts.containsKey(ChronoUnitEnum.SECONDS_IN_MINUTE)) { + updatedDateTime = + updatedDateTime.withSecond(dateParts.get(ChronoUnitEnum.SECONDS_IN_MINUTE)); + } + + if (dateParts.containsKey(ChronoUnitEnum.MILLIS)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MILLI_OF_SECOND, + dateParts.get(ChronoUnitEnum.MILLIS)); + } + + if (dateParts.containsKey(ChronoUnitEnum.MICROS)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MICRO_OF_SECOND, + dateParts.get(ChronoUnitEnum.MICROS)); + } + + if (dateParts.containsKey(ChronoUnitEnum.TENTHS_OF_SECOND)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MILLI_OF_SECOND, + 100L * dateParts.get(ChronoUnitEnum.TENTHS_OF_SECOND)); + } + + if (dateParts.containsKey(ChronoUnitEnum.HUNDREDTHS_OF_SECOND)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MILLI_OF_SECOND, + 10L * dateParts.get(ChronoUnitEnum.HUNDREDTHS_OF_SECOND)); + } + + if (dateParts.containsKey(ChronoUnitEnum.THOUSANDTHS_OF_SECOND)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MILLI_OF_SECOND, + dateParts.get(ChronoUnitEnum.THOUSANDTHS_OF_SECOND)); + } + + if (dateParts.containsKey(ChronoUnitEnum.TENTHS_OF_MS)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MICRO_OF_SECOND, + 100L * dateParts.get(ChronoUnitEnum.TENTHS_OF_MS)); + } + + if (dateParts.containsKey(ChronoUnitEnum.HUNDREDTHS_OF_MS)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MICRO_OF_SECOND, + 10L * dateParts.get(ChronoUnitEnum.HUNDREDTHS_OF_MS)); + } + + if (dateParts.containsKey(ChronoUnitEnum.THOUSANDTHS_OF_MS)) { + updatedDateTime = + updatedDateTime.with(ChronoField.MICRO_OF_SECOND, + dateParts.get(ChronoUnitEnum.THOUSANDTHS_OF_MS)); + } + + return updatedDateTime; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/DateStringFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/DateStringFormatPattern.java deleted file mode 100644 index 7055cc00d067..000000000000 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/DateStringFormatPattern.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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.postgresql; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.text.ParseException; -import java.text.ParsePosition; -import java.time.DayOfWeek; -import java.time.Month; -import java.time.ZonedDateTime; -import java.time.format.TextStyle; -import java.util.Locale; - -/** - * Converts a non numeric value from a string to a datetime component and can generate - * a string representation of of a datetime component from a datetime. An example is - * converting to and from month names. - * - * @param a type used by java.time to represent a datetime component - * that has a string representation - */ -public class DateStringFormatPattern extends StringFormatPattern { - /** - * Provides an abstraction over datetime components that have string representations. - * - * @param a type used by java.time to represent a datetime component - * that has a string representation - */ - private interface DateStringConverter { - /** - * Get the ChronoUnitEnum value that this converter handles. - * - * @return a ChronoUnitEnum value - */ - ChronoUnitEnum getChronoUnit(); - - /** - * Extract the value of this datetime component from the provided value. - * - * @param dateTime where to extract the value from - * @return the extracted value - */ - T getValueFromDateTime(ZonedDateTime dateTime); - - /** - * An array of the possible string values. - * - * @return array of possible string values - */ - T[] values(); - - /** - * Generate the string representation of a value. This may involve formatting as well - * as translating to the provided locale. - * - * @param value value to convert - * @param textStyle how to format the value - * @param haveFillMode if false add padding spaces to the correct length - * @param locale locale to translate to - * @return converted string value - */ - String getDisplayName(T value, TextStyle textStyle, boolean haveFillMode, Locale locale); - - /** - * Get the int value for the provided value (such as a month). - * - * @param value value to convert to an int - * @return result of converting the value to an int - */ - int getValue(T value); - } - - /** - * Can convert between a day of week name and the corresponding datetime component value. - */ - private static class DayOfWeekConverter implements DateStringConverter { - @Override public ChronoUnitEnum getChronoUnit() { - return ChronoUnitEnum.DAYS_IN_WEEK; - } - - @Override public DayOfWeek getValueFromDateTime(ZonedDateTime dateTime) { - return dateTime.getDayOfWeek(); - } - - @Override public DayOfWeek[] values() { - return DayOfWeek.values(); - } - - @Override public String getDisplayName(DayOfWeek value, TextStyle textStyle, - boolean haveFillMode, Locale locale) { - final String formattedValue = value.getDisplayName(textStyle, locale); - - if (!haveFillMode && textStyle == TextStyle.FULL) { - // Pad the day name to 9 characters - // See the description for DAY, Day or day in the PostgreSQL documentation for TO_CHAR - return String.format(locale, "%-9s", formattedValue); - } else { - return formattedValue; - } - } - - @Override public int getValue(DayOfWeek value) { - return value.getValue(); - } - } - - /** - * Can convert between a month name and the corresponding datetime component value. - */ - private static class MonthConverter implements DateStringConverter { - @Override public ChronoUnitEnum getChronoUnit() { - return ChronoUnitEnum.MONTHS_IN_YEAR; - } - - @Override public Month getValueFromDateTime(ZonedDateTime dateTime) { - return dateTime.getMonth(); - } - - @Override public Month[] values() { - return Month.values(); - } - - @Override public String getDisplayName(Month value, TextStyle textStyle, boolean haveFillMode, - Locale locale) { - final String formattedValue = value.getDisplayName(textStyle, locale); - - if (!haveFillMode && textStyle == TextStyle.FULL) { - // Pad the month name to 9 characters - // See the description for MONTH, Month or month in the PostgreSQL documentation for - // TO_CHAR - return String.format(locale, "%-9s", formattedValue); - } else { - return formattedValue; - } - } - - @Override public int getValue(Month value) { - return value.getValue(); - } - } - - private static final DateStringConverter DAY_OF_WEEK = new DayOfWeekConverter(); - private static final DateStringConverter MONTH = new MonthConverter(); - - private final DateStringConverter dateStringEnum; - private final CapitalizationEnum capitalization; - private final TextStyle textStyle; - - private DateStringFormatPattern( - ChronoUnitEnum chronoUnit, DateStringConverter dateStringEnum, - TextStyle textStyle, CapitalizationEnum capitalization, String... patterns) { - super(chronoUnit, patterns); - this.dateStringEnum = dateStringEnum; - this.capitalization = capitalization; - this.textStyle = textStyle; - } - - public static DateStringFormatPattern forDayOfWeek(TextStyle textStyle, - CapitalizationEnum capitalization, String... patterns) { - return new DateStringFormatPattern<>( - DAY_OF_WEEK.getChronoUnit(), - DAY_OF_WEEK, - textStyle, - capitalization, - patterns); - } - - public static DateStringFormatPattern forMonth(TextStyle textStyle, - CapitalizationEnum capitalization, String... patterns) { - return new DateStringFormatPattern<>( - MONTH.getChronoUnit(), - MONTH, - textStyle, - capitalization, - patterns); - } - - @Override protected int parseValue(ParsePosition inputPosition, String input, - Locale locale, boolean haveFillMode, boolean enforceLength) throws ParseException { - final String inputTrimmed = input.substring(inputPosition.getIndex()); - - for (T value : dateStringEnum.values()) { - final String formattedValue = - capitalization.apply(dateStringEnum.getDisplayName(value, textStyle, false, locale), - locale); - if (inputTrimmed.startsWith(formattedValue.trim())) { - inputPosition.setIndex(inputPosition.getIndex() + formattedValue.trim().length()); - return dateStringEnum.getValue(value); - } - } - - throw new ParseException("Unable to parse value", inputPosition.getIndex()); - } - - @Override public String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - return capitalization.apply( - dateStringEnum.getDisplayName( - dateStringEnum.getValueFromDateTime(dateTime), - textStyle, - haveFillMode, - locale), locale); - } -} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/EnumStringFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/EnumStringFormatPattern.java deleted file mode 100644 index 26f91d2ac3c2..000000000000 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/EnumStringFormatPattern.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.postgresql; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.text.ParseException; -import java.text.ParsePosition; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoField; -import java.util.Locale; - -/** - * Uses an array of string values to convert between a string representation and the - * datetime component value. Examples of this would be AM/PM or BCE/CE. The index - * of the string in the array is the value. - */ -public class EnumStringFormatPattern extends StringFormatPattern { - private final ChronoField chronoField; - private final String[] enumValues; - - /** - * Constructs a new EnumStringFormatPattern for the provide list of pattern strings and - * ChronoUnitEnum value. - * - * @param chronoUnit ChronoUnitEnum value that this pattern parses - * @param patterns array of pattern strings - */ - public EnumStringFormatPattern(ChronoUnitEnum chronoUnit, ChronoField chronoField, - String... patterns) { - super(chronoUnit, patterns); - this.chronoField = chronoField; - this.enumValues = patterns; - } - - @Override protected int parseValue(ParsePosition inputPosition, String input, Locale locale, - boolean haveFillMode, boolean enforceLength) throws ParseException { - final String inputTrimmed = input.substring(inputPosition.getIndex()); - - for (int i = 0; i < enumValues.length; i++) { - if (inputTrimmed.startsWith(enumValues[i])) { - inputPosition.setIndex(inputPosition.getIndex() + enumValues[i].length()); - return i; - } - } - - throw new ParseException("Unable to parse value", inputPosition.getIndex()); - } - - @Override public String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - final int value = dateTime.get(chronoField); - if (value >= 0 && value < enumValues.length) { - return enumValues[value]; - } - - throw new IllegalArgumentException(); - } -} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/FormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/FormatPattern.java deleted file mode 100644 index c2f306060137..000000000000 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/FormatPattern.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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.postgresql; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.text.ParseException; -import java.text.ParsePosition; -import java.time.ZonedDateTime; -import java.util.Locale; - -/** - * A format element that is able to produce a string from a date. - */ -public abstract class FormatPattern { - private final String[] patterns; - - /** - * Creates a new FormatPattern with the provided pattern strings. Child classes - * must call this constructor. - * - * @param patterns array of patterns - */ - protected FormatPattern(String[] patterns) { - this.patterns = patterns; - } - - /** - * Get the array of pattern strings. - * - * @return array of pattern strings - */ - public String[] getPatterns() { - return patterns; - } - - /** - * Checks if this pattern matches the substring starting at the parsePosition - * in the formatString. If it matches, then the dateTime is - * converted to a string based on this pattern. For example, "YYYY" will get the year of - * the dateTime and convert it to a string. - * - * @param parsePosition current position in the format string - * @param formatString input format string - * @param dateTime datetime to convert - * @return the string representation of the datetime based on the format pattern - */ - public abstract @Nullable String convert(ParsePosition parsePosition, String formatString, - ZonedDateTime dateTime, Locale locale); - - /** - * Get the ChronoUnitEnum value that this format pattern represents. For example, the - * pattern YYYY is for YEAR. - * - * @return a ChronoUnitEnum value - */ - protected abstract ChronoUnitEnum getChronoUnit(); - - /** - * Attempts to parse a single value from the input for this pattern. It will start parsing - * from inputPosition. The format string and position are also provided in case - * they have any flags applied such as FM or TM. - * - * @param inputPosition where to start parsing the input from - * @param input string that is getting parsed - * @param formatPosition where this format pattern starts in the format string - * @param formatString full format string that the user provided - * @param enforceLength should parsing stop once a fixed number of characters have been - * parsed. Some patterns like YYYY can match more than 4 digits, while - * others like HH24 must match exactly two digits. - * @return the long value of the datetime component that was parsed - * @throws ParseException if the pattern could not be applied to the input - */ - public long parse(ParsePosition inputPosition, String input, ParsePosition formatPosition, - String formatString, boolean enforceLength, Locale locale) throws ParseException { - - boolean haveFillMode = false; - boolean haveTranslateMode = false; - - String formatTrimmed = formatString.substring(formatPosition.getIndex()); - if (formatTrimmed.startsWith("FMTM") || formatTrimmed.startsWith("TMFM")) { - haveFillMode = true; - haveTranslateMode = true; - formatTrimmed = formatTrimmed.substring(4); - } else if (formatTrimmed.startsWith("FM")) { - haveFillMode = true; - formatTrimmed = formatTrimmed.substring(2); - } else if (formatTrimmed.startsWith("TM")) { - haveTranslateMode = true; - formatTrimmed = formatTrimmed.substring(2); - } - - for (String pattern : patterns) { - if (formatTrimmed.startsWith(pattern)) { - formatTrimmed = formatTrimmed.substring(pattern.length()); - break; - } - } - - try { - final Locale localeToUse = haveTranslateMode ? locale : Locale.US; - long parsedValue = parseValue(inputPosition, input, localeToUse, haveFillMode, enforceLength); - formatPosition.setIndex(formatString.length() - formatTrimmed.length()); - - return parsedValue; - } catch (ParseException e) { - inputPosition.setErrorIndex(inputPosition.getIndex()); - throw e; - } - } - - /** - * Attempts to parse a single value from the input for this pattern. It will start parsing - * from inputPosition. - * - * @param inputPosition where to start parsing the input from - * @param input string that is getting parsed - * @param locale Locale to use when parsing text values, such as month names - * @param haveFillMode is fill mode enabled - * @param enforceLength should parsing stop once a fixed number of characters have been - * parsed. Some patterns like YYYY can match more than 4 digits, while - * others like HH24 must match exactly two digits. - * @return the int value of the datetime component that was parsed - * @throws ParseException if the pattern could not be applied to the input - */ - protected abstract int parseValue(ParsePosition inputPosition, String input, Locale locale, - boolean haveFillMode, boolean enforceLength) throws ParseException; - - /** - * Get the length of this format pattern from the full pattern. This will include any - * modifiers on the pattern. - * - * @param formatString the full format pattern from the user with all characters before this - * pattern removed - * @return length of this format pattern - */ - int getFormatLength(final String formatString) { - int length = 0; - - for (String prefix : new String[] {"FM", "TM"}) { - if (formatString.substring(length).startsWith(prefix)) { - length += 2; - } - } - - String formatTrimmed = formatString.substring(length); - for (String pattern : patterns) { - if (formatTrimmed.startsWith(pattern)) { - length += pattern.length(); - break; - } - } - - formatTrimmed = formatString.substring(length); - if (formatTrimmed.startsWith("TH") || formatTrimmed.startsWith("th")) { - length += 2; - } - - return length; - } - - /** - * Get the length of this format pattern from the full pattern. This will include any - * prefix modifiers on the pattern. - * - * @param formatString the full format pattern from the user - * @param formatParsePosition where to start reading in the format string - * @return length of this format pattern with any prefixes - */ - int matchedPatternLength(final String formatString, final ParsePosition formatParsePosition) { - String formatTrimmed = formatString.substring(formatParsePosition.getIndex()); - - int prefixLength = 0; - for (String prefix : new String[] {"FM", "TM"}) { - if (formatTrimmed.startsWith(prefix)) { - formatTrimmed = formatTrimmed.substring(prefix.length()); - prefixLength += prefix.length(); - } - } - - for (String pattern : patterns) { - if (formatTrimmed.startsWith(pattern)) { - return prefixLength + pattern.length(); - } - } - - return -1; - } - - /** - * Checks if the format pattern is for a numeric value. - * - * @return true if the format pattern is for a numeric value - */ - protected abstract boolean isNumeric(); -} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/NumberFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/NumberFormatPattern.java deleted file mode 100644 index a0a53a9d892e..000000000000 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/NumberFormatPattern.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * 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.postgresql; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.text.ParseException; -import java.text.ParsePosition; -import java.time.ZonedDateTime; -import java.util.Locale; -import java.util.function.Function; - -import static java.lang.Integer.parseInt; - -/** - * A format element that will produce a number. Numbers can have leading zeroes - * removed and can have ordinal suffixes. - */ -public class NumberFormatPattern extends FormatPattern { - private final ChronoUnitEnum chronoUnit; - private final long minValue; - private final long maxValue; - private final int preferredLength; - private final Function converter; - private final @Nullable Function valueAdjuster; - - /** - * Constructs a new NumberFormatPattern for the provided values. - * - * @param chronoUnit ChronoUnitEnum value that this pattern parses - * @param minValue minimum allowed value - * @param maxValue maximum allowed value - * @param preferredLength the number input characters that would normally be consumed by this - * pattern. For example YYYY would normally consume 4 characters, but - * can actually consume more or less. - * @param converter a Function that will extract the value from a datetime and format it - * @param patterns array of pattern strings - */ - public NumberFormatPattern(ChronoUnitEnum chronoUnit, long minValue, long maxValue, - int preferredLength, Function converter, String... patterns) { - super(patterns); - this.chronoUnit = chronoUnit; - this.converter = converter; - this.valueAdjuster = null; - this.minValue = minValue; - this.maxValue = maxValue; - this.preferredLength = preferredLength; - } - - /** - * Constructs a new NumberFormatPattern for the provided values. - * - * @param chronoUnit ChronoUnitEnum value that this pattern parses - * @param minValue minimum allowed value - * @param maxValue maximum allowed value - * @param preferredLength the number input characters that would normally be consumed by this - * pattern. For example YYYY would normally consume 4 characters, but - * can actually consume more or less. - * @param converter a Function that will extract the value from a datetime and format it - * @param valueAdjuster a Function that can convert the extracted value to the expected - * datetime value. - * @param patterns array of pattern strings - */ - protected NumberFormatPattern(ChronoUnitEnum chronoUnit, int minValue, - int maxValue, int preferredLength, Function converter, - Function valueAdjuster, String... patterns) { - super(patterns); - this.chronoUnit = chronoUnit; - this.converter = converter; - this.valueAdjuster = valueAdjuster; - this.minValue = minValue; - this.maxValue = maxValue; - this.preferredLength = preferredLength; - } - - @Override public @Nullable String convert(ParsePosition parsePosition, String formatString, - ZonedDateTime dateTime, Locale locale) { - String formatStringTrimmed = formatString.substring(parsePosition.getIndex()); - - boolean haveFillMode = false; - boolean haveTranslationMode = false; - if (formatStringTrimmed.startsWith("FMTM") || formatStringTrimmed.startsWith("TMFM")) { - haveFillMode = true; - haveTranslationMode = true; - formatStringTrimmed = formatStringTrimmed.substring(4); - } else if (formatStringTrimmed.startsWith("FM")) { - haveFillMode = true; - formatStringTrimmed = formatStringTrimmed.substring(2); - } else if (formatStringTrimmed.startsWith("TM")) { - haveTranslationMode = true; - formatStringTrimmed = formatStringTrimmed.substring(2); - } - - String patternToUse = null; - for (String pattern : getPatterns()) { - if (formatStringTrimmed.startsWith(pattern)) { - patternToUse = pattern; - break; - } - } - - if (patternToUse == null) { - return null; - } - - parsePosition.setIndex(parsePosition.getIndex() + patternToUse.length() - + (haveFillMode ? 2 : 0) + (haveTranslationMode ? 2 : 0)); - - formatStringTrimmed = formatString.substring(parsePosition.getIndex()); - - String ordinalSuffix = null; - if (formatStringTrimmed.startsWith("TH")) { - ordinalSuffix = "TH"; - parsePosition.setIndex(parsePosition.getIndex() + 2); - } else if (formatStringTrimmed.startsWith("th")) { - ordinalSuffix = "th"; - parsePosition.setIndex(parsePosition.getIndex() + 2); - } - - String formattedValue = converter.apply(dateTime); - if (haveFillMode) { - formattedValue = trimLeadingZeros(formattedValue); - } - - if (ordinalSuffix != null) { - String suffix; - - if (formattedValue.length() >= 2 - && formattedValue.charAt(formattedValue.length() - 2) == '1') { - suffix = "th"; - } else { - switch (formattedValue.charAt(formattedValue.length() - 1)) { - case '1': - suffix = "st"; - break; - case '2': - suffix = "nd"; - break; - case '3': - suffix = "rd"; - break; - default: - suffix = "th"; - break; - } - } - - if ("th".equals(ordinalSuffix)) { - suffix = suffix.toLowerCase(Locale.ROOT); - } else { - suffix = suffix.toUpperCase(Locale.ROOT); - } - - formattedValue += suffix; - parsePosition.setIndex(parsePosition.getIndex() + 2); - } - - return formattedValue; - } - - /** - * Removes leading zeros from string, while preserving the negative sign if present. - * - * @param value String to remove leading zeros from - * @return input string without leading zeros - */ - protected String trimLeadingZeros(String value) { - if (value.isEmpty()) { - return value; - } - - boolean isNegative = value.charAt(0) == '-'; - int offset = isNegative ? 1 : 0; - boolean trimmed = false; - for (; offset < value.length() - 1; offset++) { - if (value.charAt(offset) != '0') { - break; - } - - trimmed = true; - } - - if (trimmed) { - return isNegative ? "-" + value.substring(offset) : value.substring(offset); - } else { - return value; - } - } - - @Override public ChronoUnitEnum getChronoUnit() { - return chronoUnit; - } - - @Override protected int parseValue(final ParsePosition inputPosition, final String input, - Locale locale, boolean haveFillMode, boolean enforceLength) throws ParseException { - int endIndex = inputPosition.getIndex(); - for (; endIndex < input.length(); endIndex++) { - if (input.charAt(endIndex) < '0' || input.charAt(endIndex) > '9') { - break; - } else if (enforceLength && endIndex == inputPosition.getIndex() + preferredLength) { - break; - } - } - - if (endIndex == inputPosition.getIndex()) { - throw new ParseException("Unable to parse value", inputPosition.getIndex()); - } - - int value = parseInt(input.substring(inputPosition.getIndex(), endIndex)); - if (value < minValue || value > maxValue) { - throw new ParseException("Parsed value outside of valid range", inputPosition.getIndex()); - } - - if (valueAdjuster != null) { - value = valueAdjuster.apply(value); - } - - inputPosition.setIndex(endIndex); - return value; - } - - @Override protected boolean isNumeric() { - return true; - } -} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/PatternModifier.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/PatternModifier.java new file mode 100644 index 000000000000..fc7d44b29240 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/PatternModifier.java @@ -0,0 +1,53 @@ +/* + * 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.postgresql; + +/** + * Flags that can be added to a date/time format component. + */ +public enum PatternModifier { + FM(true, "FM"), + TH_UPPER(false, "TH"), + TH_LOWER(false, "th"), + TM(true, "TM"); + + private final boolean prefix; + private final String modifierString; + + PatternModifier(boolean prefix, String modifierString) { + this.prefix = prefix; + this.modifierString = modifierString; + } + + /** + * Is this modifier placed before or after the pattern. + * + * @return true if this modifier is placed before the pattern + */ + public boolean isPrefix() { + return prefix; + } + + /** + * Get the text for this modifier. + * + * @return text for this modifier + */ + public String getModifierString() { + return modifierString; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java index 015663206608..8ec2316194ad 100644 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java @@ -16,24 +16,33 @@ */ package org.apache.calcite.util.format.postgresql; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledItem; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.LiteralCompiledItem; +import org.apache.calcite.util.format.postgresql.format.AmPmFormatPattern; +import org.apache.calcite.util.format.postgresql.format.BcAdFormatPattern; +import org.apache.calcite.util.format.postgresql.format.DayOfWeekFormatPattern; +import org.apache.calcite.util.format.postgresql.format.FormatPattern; +import org.apache.calcite.util.format.postgresql.format.MonthFormatPattern; +import org.apache.calcite.util.format.postgresql.format.NumberFormatPattern; +import org.apache.calcite.util.format.postgresql.format.RomanNumeralsFormatPattern; +import org.apache.calcite.util.format.postgresql.format.TimeZoneFormatPattern; +import org.apache.calcite.util.format.postgresql.format.TimeZoneHoursFormatPattern; +import org.apache.calcite.util.format.postgresql.format.TimeZoneMinutesFormatPattern; +import org.apache.calcite.util.format.postgresql.format.TimeZoneOffsetFormatPattern; +import org.apache.calcite.util.format.postgresql.format.YearWithCommasFormatPattern; + +import com.google.common.collect.ImmutableList; + import org.checkerframework.checker.nullness.qual.Nullable; import java.text.ParseException; import java.text.ParsePosition; -import java.time.LocalDateTime; import java.time.Month; -import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.TextStyle; import java.time.temporal.ChronoField; -import java.time.temporal.ChronoUnit; import java.time.temporal.IsoFields; import java.time.temporal.JulianFields; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; /** * Provides an implementation of toChar that matches PostgreSQL behaviour. @@ -43,400 +52,367 @@ public class PostgresqlDateTimeFormatter { * The format patterns that are supported. Order is very important, since some patterns * are prefixes of other patterns. */ - @SuppressWarnings("TemporalAccessorGetChronoField") - private static final FormatPattern[] FORMAT_PATTERNS = new FormatPattern[] { - new NumberFormatPattern( - ChronoUnitEnum.HOURS_IN_DAY, - 0, - 23, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.getHour()), - "HH24"), - new NumberFormatPattern( - ChronoUnitEnum.HOURS_IN_HALF_DAY, - 1, - 12, - 2, - dt -> { - final int hour = dt.get(ChronoField.HOUR_OF_AMPM); - return String.format(Locale.ROOT, "%02d", hour == 0 ? 12 : hour); - }, - "HH12", "HH"), - new NumberFormatPattern( - ChronoUnitEnum.MINUTES_IN_HOUR, - 0, - 59, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.getMinute()), - "MI"), - new NumberFormatPattern( - ChronoUnitEnum.SECONDS_IN_DAY, - 0, - 24 * 60 * 60 - 1, - 5, - dt -> Integer.toString(dt.get(ChronoField.SECOND_OF_DAY)), - "SSSSS", "SSSS"), - new NumberFormatPattern( - ChronoUnitEnum.SECONDS_IN_MINUTE, - 0, - 59, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.getSecond()), - "SS"), - new NumberFormatPattern( - ChronoUnitEnum.MILLIS, - 0, - 999, - 3, - dt -> String.format(Locale.ROOT, "%03d", dt.get(ChronoField.MILLI_OF_SECOND)), - "MS"), - new NumberFormatPattern( - ChronoUnitEnum.MICROS, - 0, - 999_999, - 6, - dt -> String.format(Locale.ROOT, "%06d", dt.get(ChronoField.MICRO_OF_SECOND)), - "US"), - new NumberFormatPattern( - ChronoUnitEnum.TENTHS_OF_SECOND, - 0, - 9, - 1, - dt -> Integer.toString(dt.get(ChronoField.MILLI_OF_SECOND) / 100), - "FF1"), - new NumberFormatPattern( - ChronoUnitEnum.HUNDREDTHS_OF_SECOND, - 0, - 99, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.get(ChronoField.MILLI_OF_SECOND) / 10), - "FF2"), - new NumberFormatPattern( - ChronoUnitEnum.THOUSANDTHS_OF_SECOND, - 0, - 999, - 3, - dt -> String.format(Locale.ROOT, "%03d", dt.get(ChronoField.MILLI_OF_SECOND)), - "FF3"), - new NumberFormatPattern( - ChronoUnitEnum.TENTHS_OF_MS, - 0, - 9_999, - 4, - dt -> String.format(Locale.ROOT, "%04d", dt.get(ChronoField.MICRO_OF_SECOND) / 100), - "FF4"), - new NumberFormatPattern( - ChronoUnitEnum.HUNDREDTHS_OF_MS, - 0, - 99_999, - 5, - dt -> String.format(Locale.ROOT, "%05d", dt.get(ChronoField.MICRO_OF_SECOND) / 10), - "FF5"), - new NumberFormatPattern( - ChronoUnitEnum.THOUSANDTHS_OF_MS, - 0, - 999_999, - 6, - dt -> String.format(Locale.ROOT, "%06d", dt.get(ChronoField.MICRO_OF_SECOND)), - "FF6"), - new EnumStringFormatPattern( - ChronoUnitEnum.HALF_DAYS, - ChronoField.AMPM_OF_DAY, - "AM", "PM"), - new EnumStringFormatPattern( - ChronoUnitEnum.HALF_DAYS, - ChronoField.AMPM_OF_DAY, - "am", "pm"), - new EnumStringFormatPattern( - ChronoUnitEnum.HALF_DAYS, - ChronoField.AMPM_OF_DAY, - "A.M.", "P.M."), - new EnumStringFormatPattern( - ChronoUnitEnum.HALF_DAYS, - ChronoField.AMPM_OF_DAY, - "a.m.", "p.m."), - new YearWithCommasFormatPattern(), - new NumberFormatPattern( - ChronoUnitEnum.YEARS, - 0, - Integer.MAX_VALUE, - 4, - dt -> String.format(Locale.ROOT, "%04d", dt.getYear()), - "YYYY"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_ISO_8601, - 0, - Integer.MAX_VALUE, - 4, - dt -> Integer.toString(dt.get(IsoFields.WEEK_BASED_YEAR)), - "IYYY"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_IN_MILLENIA_ISO_8601, - 0, - Integer.MAX_VALUE, - 3, - dt -> { - final String yearString = - String.format(Locale.ROOT, "%03d", dt.get(IsoFields.WEEK_BASED_YEAR)); - return yearString.substring(yearString.length() - 3); - }, - "IYY"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_IN_CENTURY_ISO_8601, - 0, - Integer.MAX_VALUE, - 2, - dt -> { - final String yearString = - String.format(Locale.ROOT, "%02d", dt.get(IsoFields.WEEK_BASED_YEAR)); - return yearString.substring(yearString.length() - 2); - }, - "IY"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_IN_MILLENIA, - 0, - Integer.MAX_VALUE, - 3, - dt -> { - final String formattedYear = String.format(Locale.ROOT, "%03d", dt.getYear()); - if (formattedYear.length() > 3) { - return formattedYear.substring(formattedYear.length() - 3); - } else { - return formattedYear; - } - }, - "YYY"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_IN_CENTURY, - 0, - Integer.MAX_VALUE, - 2, - dt -> { - final String formattedYear = String.format(Locale.ROOT, "%02d", dt.getYear()); - if (formattedYear.length() > 2) { - return formattedYear.substring(formattedYear.length() - 2); - } else { - return formattedYear; - } - }, - "YY"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_IN_CENTURY, - 0, - Integer.MAX_VALUE, - 1, - dt -> { - final String formattedYear = Integer.toString(dt.getYear()); - if (formattedYear.length() > 1) { - return formattedYear.substring(formattedYear.length() - 1); - } else { - return formattedYear; - } - }, - "Y"), - new NumberFormatPattern( - ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601, - 1, - 53, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR)), - "IW"), - new NumberFormatPattern( - ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601, - 0, - 371, - 3, - dt -> { - final Month month = dt.getMonth(); - final int dayOfMonth = dt.getDayOfMonth(); - final int weekNumber = dt.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR); - - if (month == Month.JANUARY && dayOfMonth < 4) { - if (weekNumber == 1) { - return String.format(Locale.ROOT, "%03d", dt.getDayOfWeek().getValue()); - } - } else if (month == Month.DECEMBER && dayOfMonth >= 29) { - if (weekNumber == 1) { - return String.format(Locale.ROOT, "%03d", dt.getDayOfWeek().getValue()); - } - } - - return String.format(Locale.ROOT, "%03d", - (weekNumber - 1) * 7 + dt.getDayOfWeek().getValue()); - }, - "IDDD"), - new NumberFormatPattern( - ChronoUnitEnum.DAYS_IN_WEEK, - 1, - 7, - 1, - dt -> Integer.toString(dt.getDayOfWeek().getValue()), - "ID"), - new NumberFormatPattern( - ChronoUnitEnum.YEARS_IN_CENTURY_ISO_8601, - 0, - Integer.MAX_VALUE, - 1, - dt -> { - final String yearString = Integer.toString(dt.get(IsoFields.WEEK_BASED_YEAR)); - return yearString.substring(yearString.length() - 1); - }, - "I"), - new EnumStringFormatPattern(ChronoUnitEnum.ERAS, ChronoField.ERA, "BC", "AD"), - new EnumStringFormatPattern(ChronoUnitEnum.ERAS, ChronoField.ERA, "bc", "ad"), - new EnumStringFormatPattern(ChronoUnitEnum.ERAS, ChronoField.ERA, "B.C.", "A.D."), - new EnumStringFormatPattern(ChronoUnitEnum.ERAS, ChronoField.ERA, "b.c.", "a.d."), - DateStringFormatPattern.forMonth(TextStyle.FULL, CapitalizationEnum.ALL_UPPER, "MONTH"), - DateStringFormatPattern.forMonth(TextStyle.FULL, CapitalizationEnum.CAPITALIZED, "Month"), - DateStringFormatPattern.forMonth(TextStyle.FULL, CapitalizationEnum.ALL_LOWER, "month"), - DateStringFormatPattern.forMonth(TextStyle.SHORT, CapitalizationEnum.ALL_UPPER, "MON"), - DateStringFormatPattern.forMonth(TextStyle.SHORT, CapitalizationEnum.CAPITALIZED, "Mon"), - DateStringFormatPattern.forMonth(TextStyle.SHORT, CapitalizationEnum.ALL_LOWER, "mon"), - new NumberFormatPattern( - ChronoUnitEnum.MONTHS_IN_YEAR, - 1, - 12, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.getMonthValue()), - "MM"), - DateStringFormatPattern.forDayOfWeek(TextStyle.FULL, CapitalizationEnum.ALL_UPPER, "DAY"), - DateStringFormatPattern.forDayOfWeek(TextStyle.FULL, CapitalizationEnum.CAPITALIZED, "Day"), - DateStringFormatPattern.forDayOfWeek(TextStyle.FULL, CapitalizationEnum.ALL_LOWER, "day"), - DateStringFormatPattern.forDayOfWeek(TextStyle.SHORT, CapitalizationEnum.ALL_UPPER, "DY"), - DateStringFormatPattern.forDayOfWeek(TextStyle.SHORT, CapitalizationEnum.CAPITALIZED, "Dy"), - DateStringFormatPattern.forDayOfWeek(TextStyle.SHORT, CapitalizationEnum.ALL_LOWER, "dy"), - new NumberFormatPattern( - ChronoUnitEnum.DAYS_IN_YEAR, - 1, - 366, - 3, - dt -> String.format(Locale.ROOT, "%03d", dt.getDayOfYear()), - "DDD"), - new NumberFormatPattern( - ChronoUnitEnum.DAYS_IN_MONTH, - 1, - 31, - 2, - dt -> String.format(Locale.ROOT, "%02d", dt.getDayOfMonth()), - "DD"), - new NumberFormatPattern( - ChronoUnitEnum.DAYS_IN_WEEK, - 1, - 7, - 1, - dt -> { - int dayOfWeek = dt.getDayOfWeek().getValue() + 1; - if (dayOfWeek == 8) { - dayOfWeek = 1; - } - return Integer.toString(dayOfWeek); - }, - v -> v < 7 ? v + 1 : 1, - "D"), - new NumberFormatPattern( - ChronoUnitEnum.WEEKS_IN_YEAR, - 1, - 53, - 2, - dt -> Integer.toString((int) Math.ceil((double) dt.getDayOfYear() / 7)), - "WW"), - new NumberFormatPattern( - ChronoUnitEnum.WEEKS_IN_MONTH, - 1, - 5, - 1, - dt -> Integer.toString((int) Math.ceil((double) dt.getDayOfMonth() / 7)), - "W"), - new NumberFormatPattern( - ChronoUnitEnum.CENTURIES, - 0, - Integer.MAX_VALUE, - 2, - dt -> { - if (dt.get(ChronoField.ERA) == 0) { - return String.format(Locale.ROOT, "-%02d", Math.abs(dt.getYear() / 100 - 1)); - } else { - return String.format(Locale.ROOT, "%02d", dt.getYear() / 100 + 1); - } - }, - "CC"), - new NumberFormatPattern( - ChronoUnitEnum.DAYS_JULIAN, - 0, - Integer.MAX_VALUE, - 1, - dt -> { - final long julianDays = dt.getLong(JulianFields.JULIAN_DAY); - if (dt.getYear() < 0) { - return Long.toString(365L + julianDays); - } else { - return Long.toString(julianDays); - } - }, - "J"), - new NumberFormatPattern( - ChronoUnitEnum.MONTHS_IN_YEAR, - 1, - 4, - 1, - dt -> Integer.toString(dt.get(IsoFields.QUARTER_OF_YEAR)), - "Q"), - new RomanNumeralMonthFormatPattern(true, "RM"), - new RomanNumeralMonthFormatPattern(false, "rm"), - new TimeZoneHoursFormatPattern(), - new TimeZoneMinutesFormatPattern(), - new StringFormatPattern(ChronoUnitEnum.TIMEZONE_MINUTES, "TZ") { - @Override protected int parseValue(ParsePosition inputPosition, String input, - Locale locale, boolean haveFillMode, boolean enforceLength) throws ParseException { - throw new ParseException("TZ pattern is not supported in parsing datetime values", - inputPosition.getIndex()); - } - - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - return String.format( - locale, - "%3s", - dateTime.getZone().getDisplayName(TextStyle.SHORT, locale).toUpperCase(locale)); - } - }, - new StringFormatPattern(ChronoUnitEnum.TIMEZONE_MINUTES, "tz") { - @Override protected int parseValue(ParsePosition inputPosition, String input, Locale locale, - boolean haveFillMode, boolean enforceLength) throws ParseException { - throw new ParseException("tz pattern is not supported in parsing datetime values", - inputPosition.getIndex()); - } - - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - return String.format( - locale, - "%3s", - dateTime.getZone().getDisplayName(TextStyle.SHORT, locale).toLowerCase(locale)); - } - }, - new StringFormatPattern(ChronoUnitEnum.TIMEZONE_MINUTES, "OF") { - @Override protected int parseValue(ParsePosition inputPosition, String input, Locale locale, - boolean haveFillMode, boolean enforceLength) throws ParseException { - throw new ParseException("OF pattern is not supported in parsing datetime values", - inputPosition.getIndex()); - } - - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - final int hours = dateTime.getOffset().get(ChronoField.HOUR_OF_DAY); - final int minutes = dateTime.getOffset().get(ChronoField.MINUTE_OF_HOUR); - - String formattedHours = - String.format(Locale.ROOT, "%s%02d", hours < 0 ? "-" : "+", hours); - if (minutes == 0) { - return formattedHours; - } else { - return String.format(Locale.ROOT, "%s:%02d", formattedHours, minutes); - } - } - } - }; + private static final FormatPattern[] FORMAT_PATTERNS = + new FormatPattern[] { + new NumberFormatPattern( + "HH24", + ChronoUnitEnum.HOURS_IN_DAY, + ZonedDateTime::getHour, + 2, + 2, + 0, + 23), + new NumberFormatPattern( + "HH12", + ChronoUnitEnum.HOURS_IN_HALF_DAY, + dt -> { + final int value = dt.get(ChronoField.HOUR_OF_AMPM); + return value > 0 ? value : 12; + }, + 2, + 2, + 0, + 12), + new NumberFormatPattern( + "HH", + ChronoUnitEnum.HOURS_IN_HALF_DAY, + dt -> { + final int value = dt.get(ChronoField.HOUR_OF_AMPM); + return value > 0 ? value : 12; + }, + 2, + 2, + 0, + 12), + new NumberFormatPattern( + "MI", + ChronoUnitEnum.MINUTES_IN_HOUR, + ZonedDateTime::getMinute, + 2, + 2, + 0, + 59), + new NumberFormatPattern( + "SSSSS", + ChronoUnitEnum.SECONDS_IN_DAY, + dt -> dt.get(ChronoField.SECOND_OF_DAY), + -1, + 5, + 0, + 24 * 60 * 60 - 1), + new NumberFormatPattern( + "SSSS", + ChronoUnitEnum.SECONDS_IN_DAY, + dt -> dt.get(ChronoField.SECOND_OF_DAY), + 1, + 5, + 0, + 24 * 60 * 60 - 1), + new NumberFormatPattern( + "SS", + ChronoUnitEnum.SECONDS_IN_MINUTE, + ZonedDateTime::getSecond, + 2, + 2, + 0, + 59), + new NumberFormatPattern( + "MS", + ChronoUnitEnum.MILLIS, + dt -> dt.get(ChronoField.MILLI_OF_SECOND), + 3, + 3, + 0, + 999), + new NumberFormatPattern( + "US", + ChronoUnitEnum.MICROS, + dt -> dt.get(ChronoField.MICRO_OF_SECOND), + 6, + 6, + 0, + 999_999), + new NumberFormatPattern( + "FF1", + ChronoUnitEnum.TENTHS_OF_SECOND, + dt -> dt.get(ChronoField.MILLI_OF_SECOND) / 100, + 1, + 1, + 0, + 9), + new NumberFormatPattern( + "FF2", + ChronoUnitEnum.HUNDREDTHS_OF_SECOND, + dt -> dt.get(ChronoField.MILLI_OF_SECOND) / 10, + 2, + 2, + 0, + 99), + new NumberFormatPattern( + "FF3", + ChronoUnitEnum.MILLIS, + dt -> dt.get(ChronoField.MILLI_OF_SECOND), + 3, + 3, + 0, + 999), + new NumberFormatPattern( + "FF4", + ChronoUnitEnum.TENTHS_OF_MS, + dt -> dt.get(ChronoField.MICRO_OF_SECOND) / 100, + 4, + 4, + 0, + 9_999), + new NumberFormatPattern( + "FF5", + ChronoUnitEnum.HUNDREDTHS_OF_MS, + dt -> dt.get(ChronoField.MICRO_OF_SECOND) / 10, + 5, + 5, + 0, + 99_999), + new NumberFormatPattern( + "FF6", + ChronoUnitEnum.THOUSANDTHS_OF_MS, + dt -> dt.get(ChronoField.MICRO_OF_SECOND), + 6, + 6, + 0, + 999_999), + new AmPmFormatPattern("AM"), + new AmPmFormatPattern("PM"), + new AmPmFormatPattern("A.M."), + new AmPmFormatPattern("P.M."), + new AmPmFormatPattern("am"), + new AmPmFormatPattern("pm"), + new AmPmFormatPattern("a.m."), + new AmPmFormatPattern("p.m."), + new YearWithCommasFormatPattern(), + new NumberFormatPattern( + "YYYY", + ChronoUnitEnum.YEARS, + ZonedDateTime::getYear, + 4, + -1, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "IYYY", + ChronoUnitEnum.YEARS_ISO_8601, + dt -> dt.get(IsoFields.WEEK_BASED_YEAR), + 4, + -1, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "IYY", + ChronoUnitEnum.YEARS_IN_MILLENIA_ISO_8601, + dt -> dt.get(IsoFields.WEEK_BASED_YEAR) % 1000, + 3, + 3, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "IY", + ChronoUnitEnum.YEARS_IN_CENTURY_ISO_8601, + dt -> dt.get(IsoFields.WEEK_BASED_YEAR) % 100, + 2, + 2, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "YYY", + ChronoUnitEnum.YEARS_IN_MILLENIA, + dt -> dt.getYear() % 1000, + 3, + 3, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "YY", + ChronoUnitEnum.YEARS_IN_CENTURY, + dt -> dt.getYear() % 100, + 2, + 2, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "Y", + ChronoUnitEnum.YEARS_IN_CENTURY, + ZonedDateTime::getYear, + 1, + 1, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "IW", + ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601, + dt -> dt.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR), + 2, + 2, + 1, + 53), + new NumberFormatPattern( + "IDDD", + ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601, + dt -> { + final Month month = dt.getMonth(); + final int dayOfMonth = dt.getDayOfMonth(); + final int weekNumber = dt.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR); + + if (month == Month.JANUARY && dayOfMonth < 4) { + if (weekNumber == 1) { + return dt.getDayOfWeek().getValue(); + } + } else if (month == Month.DECEMBER && dayOfMonth >= 29) { + if (weekNumber == 1) { + return dt.getDayOfWeek().getValue(); + } + } + + return (weekNumber - 1) * 7 + dt.getDayOfWeek().getValue(); + }, + 3, + 3, + 0, + 371), + new NumberFormatPattern( + "ID", + ChronoUnitEnum.DAYS_IN_WEEK, + dt -> dt.getDayOfWeek().getValue(), + 1, + 1, + 1, + 7), + new NumberFormatPattern( + "I", + ChronoUnitEnum.YEARS_IN_CENTURY_ISO_8601, + dt -> dt.get(IsoFields.WEEK_BASED_YEAR), + 1, + 1, + 0, + Integer.MAX_VALUE), + new BcAdFormatPattern("BC"), + new BcAdFormatPattern("AD"), + new BcAdFormatPattern("B.C."), + new BcAdFormatPattern("A.D."), + new BcAdFormatPattern("bc"), + new BcAdFormatPattern("ad"), + new BcAdFormatPattern("b.c."), + new BcAdFormatPattern("a.d."), + new MonthFormatPattern("MONTH"), + new MonthFormatPattern("Month"), + new MonthFormatPattern("month"), + new MonthFormatPattern("MON"), + new MonthFormatPattern("Mon"), + new MonthFormatPattern("mon"), + new NumberFormatPattern( + "MM", + ChronoUnitEnum.MONTHS_IN_YEAR, + dt -> dt.getMonth().getValue(), + 2, + 2, + 1, + 12), + new DayOfWeekFormatPattern("DAY"), + new DayOfWeekFormatPattern("Day"), + new DayOfWeekFormatPattern("day"), + new DayOfWeekFormatPattern("DY"), + new DayOfWeekFormatPattern("Dy"), + new DayOfWeekFormatPattern("dy"), + new NumberFormatPattern( + "DDD", + ChronoUnitEnum.DAYS_IN_YEAR, + ZonedDateTime::getDayOfYear, + 3, + 3, + 1, + 366), + new NumberFormatPattern( + "DD", + ChronoUnitEnum.DAYS_IN_MONTH, + ZonedDateTime::getDayOfMonth, + 2, + 2, + 1, + 31), + new NumberFormatPattern( + "D", + ChronoUnitEnum.DAYS_IN_WEEK, + dt -> { + int dayOfWeek = dt.getDayOfWeek().getValue() + 1; + if (dayOfWeek == 8) { + return 1; + } + return dayOfWeek; + }, + v -> v < 7 ? v + 1 : 1, + 1, + 1, + 1, + 7), + new NumberFormatPattern( + "WW", + ChronoUnitEnum.WEEKS_IN_YEAR, + dt -> (int) Math.ceil((double) dt.getDayOfYear() / 7), + -1, + 2, + 1, + 53), + new NumberFormatPattern( + "W", + ChronoUnitEnum.WEEKS_IN_MONTH, + dt -> (int) Math.ceil((double) dt.getDayOfMonth() / 7), + 1, + 1, + 1, + 5), + new NumberFormatPattern( + "CC", + ChronoUnitEnum.CENTURIES, + dt -> { + if (dt.get(ChronoField.ERA) == 0) { + return dt.getYear() / 100 - 1; + } else { + return dt.getYear() / 100 + 1; + } + }, + 2, + Integer.MAX_VALUE, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "J", + ChronoUnitEnum.DAYS_JULIAN, + dt -> { + final int julianDays = (int) dt.getLong(JulianFields.JULIAN_DAY); + if (dt.getYear() < 0) { + return 365 + julianDays; + } else { + return julianDays; + } + }, + -1, + -1, + 0, + Integer.MAX_VALUE), + new NumberFormatPattern( + "Q", + ChronoUnitEnum.MONTHS_IN_YEAR, + dt -> dt.get(IsoFields.QUARTER_OF_YEAR), + 1, + 1, + 1, + 4), + new RomanNumeralsFormatPattern("RM"), + new RomanNumeralsFormatPattern("rm"), + new TimeZoneHoursFormatPattern(), + new TimeZoneMinutesFormatPattern(), + new TimeZoneFormatPattern("TZ"), + new TimeZoneFormatPattern("tz"), + new TimeZoneOffsetFormatPattern() + }; /** * Remove access to the default constructor. @@ -444,405 +420,66 @@ public class PostgresqlDateTimeFormatter { private PostgresqlDateTimeFormatter() { } - /** - * Converts a format string such as "YYYY-MM-DD" with a datetime to a string representation. - * - * @see PostgreSQL - * - * @param formatString input format string - * @param dateTime datetime to convert - * @return formatted string representation of the datetime - */ - public static String toChar(String formatString, ZonedDateTime dateTime, Locale locale) { + public static CompiledDateTimeFormat compilePattern(String format) { + final ImmutableList.Builder compiledPatterns = ImmutableList.builder(); final ParsePosition parsePosition = new ParsePosition(0); - final StringBuilder sb = new StringBuilder(); - - while (parsePosition.getIndex() < formatString.length()) { - boolean matched = false; - - for (FormatPattern formatPattern : FORMAT_PATTERNS) { - final String formattedString = - formatPattern.convert(parsePosition, formatString, dateTime, locale); - if (formattedString != null) { - sb.append(formattedString); - matched = true; - break; - } + final ParsePosition nextPatternPosition = new ParsePosition(parsePosition.getIndex()); + + FormatPattern nextPattern = getNextPattern(format, nextPatternPosition); + while (nextPattern != null) { + // Add the literal if one exists before the next pattern + if (parsePosition.getIndex() < nextPatternPosition.getIndex()) { + compiledPatterns.add( + new LiteralCompiledItem( + format.substring(parsePosition.getIndex(), nextPatternPosition.getIndex()))); } - if (!matched) { - sb.append(formatString.charAt(parsePosition.getIndex())); - parsePosition.setIndex(parsePosition.getIndex() + 1); + try { + final CompiledPattern compiledPattern = + nextPattern.compilePattern(format, nextPatternPosition); + compiledPatterns.add(compiledPattern); + parsePosition.setIndex( + nextPatternPosition.getIndex() + compiledPattern.getFormatPatternLength()); + nextPatternPosition.setIndex(parsePosition.getIndex()); + nextPattern = getNextPattern(format, nextPatternPosition); + } catch (ParseException e) { + throw new IllegalArgumentException(); } } - return sb.toString(); - } - - public static ZonedDateTime toTimestamp(String input, String formatString, ZoneId zoneId, - Locale locale) throws Exception { - final ParsePosition inputParsePosition = new ParsePosition(0); - final ParsePosition formatParsePosition = new ParsePosition(0); - final Map dateTimeParts = new HashMap<>(); - - while (inputParsePosition.getIndex() < input.length() - && formatParsePosition.getIndex() < formatString.length()) { - if (input.charAt(inputParsePosition.getIndex()) == ' ' - && formatString.charAt(formatParsePosition.getIndex()) == ' ') { - inputParsePosition.setIndex(inputParsePosition.getIndex() + 1); - formatParsePosition.setIndex(formatParsePosition.getIndex() + 1); - continue; - } else if (input.charAt(inputParsePosition.getIndex()) == ' ') { - inputParsePosition.setIndex(inputParsePosition.getIndex() + 1); - continue; - } else if (formatString.charAt(formatParsePosition.getIndex()) == ' ') { - formatParsePosition.setIndex(formatParsePosition.getIndex() + 1); - continue; - } - - long parsedValue = 0L; - FormatPattern matchedPattern = null; - - for (FormatPattern formatPattern : FORMAT_PATTERNS) { - int matchedPatternLength = - formatPattern.matchedPatternLength(formatString, formatParsePosition); - if (matchedPatternLength > 0) { - final FormatPattern nextPattern = - getNextPattern(formatString, formatParsePosition.getIndex() + matchedPatternLength); - - final boolean enforceLength = nextPattern != null && formatPattern.isNumeric() - && nextPattern.isNumeric(); - - parsedValue = - formatPattern.parse(inputParsePosition, input, formatParsePosition, formatString, - enforceLength, locale); - matchedPattern = formatPattern; - break; - } - } - - if (matchedPattern == null) { - if (Character.isLetter(formatString.charAt(formatParsePosition.getIndex()))) { - throw new IllegalArgumentException(); - } else { - inputParsePosition.setIndex(inputParsePosition.getIndex() + 1); - formatParsePosition.setIndex(formatParsePosition.getIndex() + 1); - } - } else { - final Set units = dateTimeParts.keySet(); - if (!matchedPattern.getChronoUnit().isCompatible(units)) { - throw new IllegalArgumentException(); - } - - dateTimeParts.put(matchedPattern.getChronoUnit(), parsedValue); - } + if (parsePosition.getIndex() < format.length()) { + compiledPatterns.add(new LiteralCompiledItem(format.substring(parsePosition.getIndex()))); } - return constructDateTimeFromParts(dateTimeParts, zoneId); + return new CompiledDateTimeFormat(compiledPatterns.build()); } - private static @Nullable FormatPattern getNextPattern(String formatString, - int formatPosition) { - String formatTrimmed = formatString.substring(formatPosition); - for (String prefix : new String[] {"FM", "TM"}) { - if (formatTrimmed.startsWith(prefix)) { - formatTrimmed = formatString.substring(prefix.length()); - } - } + private static @Nullable FormatPattern getNextPattern(String format, + ParsePosition parsePosition) { + while (parsePosition.getIndex() < format.length()) { + String formatTrimmed = format.substring(parsePosition.getIndex()); - for (FormatPattern pattern : FORMAT_PATTERNS) { - for (String patternString : pattern.getPatterns()) { - if (formatTrimmed.startsWith(patternString)) { - return pattern; - } - } - } + boolean prefixFound = true; + while (prefixFound) { + prefixFound = false; - return null; - } - - private static ZonedDateTime constructDateTimeFromParts(Map dateParts, - ZoneId zoneId) { - LocalDateTime constructedDateTime = LocalDateTime.now(zoneId) - .truncatedTo(ChronoUnit.DAYS); - - DateCalendarEnum calendar = DateCalendarEnum.NONE; - boolean containsCentury = false; - for (ChronoUnitEnum unit : dateParts.keySet()) { - if (unit.getCalendars().size() == 1) { - DateCalendarEnum unitCalendar = unit.getCalendars().iterator().next(); - if (unitCalendar != DateCalendarEnum.NONE) { - calendar = unitCalendar; - break; + for (PatternModifier modifier : PatternModifier.values()) { + if (modifier.isPrefix() && formatTrimmed.startsWith(modifier.getModifierString())) { + formatTrimmed = formatTrimmed.substring(modifier.getModifierString().length()); + prefixFound = true; + } } - } else if (unit == ChronoUnitEnum.CENTURIES) { - containsCentury = true; - } - } - - if (calendar == DateCalendarEnum.NONE && containsCentury) { - calendar = DateCalendarEnum.GREGORIAN; - } - - switch (calendar) { - case NONE: - constructedDateTime = constructedDateTime - .withYear(1) - .withMonth(1) - .withDayOfMonth(1); - break; - case GREGORIAN: - constructedDateTime = updateWithGregorianFields(constructedDateTime, dateParts); - break; - case ISO_8601: - constructedDateTime = updateWithIso8601Fields(constructedDateTime, dateParts); - break; - case JULIAN: - final Long julianDays = dateParts.get(ChronoUnitEnum.DAYS_JULIAN); - if (julianDays != null) { - constructedDateTime = constructedDateTime.with(JulianFields.JULIAN_DAY, julianDays); - } - break; - } - - constructedDateTime = updateWithTimeFields(constructedDateTime, dateParts); - - if (dateParts.containsKey(ChronoUnitEnum.TIMEZONE_HOURS) - || dateParts.containsKey(ChronoUnitEnum.TIMEZONE_MINUTES)) { - final int hours = dateParts.getOrDefault(ChronoUnitEnum.TIMEZONE_HOURS, 0L) - .intValue(); - final int minutes = dateParts.getOrDefault(ChronoUnitEnum.TIMEZONE_MINUTES, 0L) - .intValue(); - - return ZonedDateTime.of(constructedDateTime, ZoneOffset.ofHoursMinutes(hours, minutes)) - .withZoneSameInstant(zoneId); - } - - return ZonedDateTime.of(constructedDateTime, zoneId); - } - - private static LocalDateTime updateWithGregorianFields(LocalDateTime dateTime, - Map dateParts) { - LocalDateTime updatedDateTime = dateTime.withYear(getGregorianYear(dateParts)).withDayOfYear(1); - - if (dateParts.containsKey(ChronoUnitEnum.MONTHS_IN_YEAR)) { - updatedDateTime = - updatedDateTime.withMonth(dateParts.get(ChronoUnitEnum.MONTHS_IN_YEAR).intValue()); - } - - if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_MONTH)) { - updatedDateTime = - updatedDateTime.withDayOfMonth(dateParts.get(ChronoUnitEnum.DAYS_IN_MONTH).intValue()); - } - - if (dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_MONTH)) { - updatedDateTime = - updatedDateTime.withDayOfMonth( - dateParts.get(ChronoUnitEnum.WEEKS_IN_MONTH).intValue() * 7 - 6); - } - - if (dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_YEAR)) { - updatedDateTime = - updatedDateTime.withDayOfYear( - dateParts.get(ChronoUnitEnum.WEEKS_IN_YEAR).intValue() * 7 - 6); - } - - if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_YEAR)) { - updatedDateTime = - updatedDateTime.withDayOfYear(dateParts.get(ChronoUnitEnum.DAYS_IN_YEAR).intValue()); - } - - return updatedDateTime; - } - - private static int getGregorianYear(Map dateParts) { - int year = - getYear( - dateParts.get(ChronoUnitEnum.ERAS), - dateParts.get(ChronoUnitEnum.YEARS), - dateParts.get(ChronoUnitEnum.CENTURIES), - dateParts.get(ChronoUnitEnum.YEARS_IN_MILLENIA), - dateParts.get(ChronoUnitEnum.YEARS_IN_CENTURY)); - return year == 0 ? 1 : year; - } - - private static LocalDateTime updateWithIso8601Fields(LocalDateTime dateTime, - Map dateParts) { - final int year = getIso8601Year(dateParts); - - if (!dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601) - && !dateParts.containsKey(ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601)) { - return dateTime.withYear(year).withDayOfYear(1); - } - - LocalDateTime updatedDateTime = dateTime - .with(ChronoField.DAY_OF_WEEK, 1) - .with(IsoFields.WEEK_BASED_YEAR, year) - .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 1); - - if (dateParts.containsKey(ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601)) { - updatedDateTime = - updatedDateTime.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, - dateParts.get(ChronoUnitEnum.WEEKS_IN_YEAR_ISO_8601)); - - if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_WEEK)) { - updatedDateTime = - updatedDateTime.with(ChronoField.DAY_OF_WEEK, - dateParts.get(ChronoUnitEnum.DAYS_IN_WEEK)); } - } else if (dateParts.containsKey(ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601)) { - updatedDateTime = - updatedDateTime.plusDays(dateParts.get(ChronoUnitEnum.DAYS_IN_YEAR_ISO_8601) - 1); - } - - return updatedDateTime; - } - - private static int getIso8601Year(Map dateParts) { - int year = - getYear( - dateParts.get(ChronoUnitEnum.ERAS), - dateParts.get(ChronoUnitEnum.YEARS_ISO_8601), - dateParts.get(ChronoUnitEnum.CENTURIES), - dateParts.get(ChronoUnitEnum.YEARS_IN_MILLENIA_ISO_8601), - dateParts.get(ChronoUnitEnum.YEARS_IN_CENTURY_ISO_8601)); - return year == 0 ? 1 : year; - } - - private static int getYear(@Nullable Long era, @Nullable Long years, - @Nullable Long centuries, @Nullable Long yearsInMillenia, - @Nullable Long yearsInCentury) { - int yearSign = 1; - if (era != null) { - if (era == 0) { - yearSign = -1; - } - } - - if (yearsInMillenia != null) { - int year = yearsInMillenia.intValue(); - if (year < 520) { - year += 2000; - } else { - year += 1000; - } - - return yearSign * year; - } - - if (centuries != null) { - int year = 100 * (centuries.intValue() - 1); - - if (yearsInCentury != null) { - year += yearsInCentury.intValue(); - } else { - year += 1; - } - - return yearSign * year; - } - - if (years != null) { - return yearSign * years.intValue(); - } - if (yearsInCentury != null) { - int year = yearsInCentury.intValue(); - if (year < 70) { - year += 2000; - } else { - year += 1900; + for (FormatPattern formatPattern : FORMAT_PATTERNS) { + if (formatTrimmed.startsWith(formatPattern.getPattern())) { + return formatPattern; + } } - return yearSign * year; + parsePosition.setIndex(parsePosition.getIndex() + 1); } - return yearSign; - } - - private static LocalDateTime updateWithTimeFields(LocalDateTime dateTime, - Map dateParts) { - LocalDateTime updatedDateTime = dateTime; - - if (dateParts.containsKey(ChronoUnitEnum.HOURS_IN_DAY)) { - updatedDateTime = - updatedDateTime.withHour(dateParts.get(ChronoUnitEnum.HOURS_IN_DAY).intValue()); - } - - if (dateParts.containsKey(ChronoUnitEnum.HALF_DAYS) - && dateParts.containsKey(ChronoUnitEnum.HOURS_IN_HALF_DAY)) { - updatedDateTime = - updatedDateTime.withHour(dateParts.get(ChronoUnitEnum.HALF_DAYS).intValue() * 12 - + dateParts.get(ChronoUnitEnum.HOURS_IN_HALF_DAY).intValue()); - } else if (dateParts.containsKey(ChronoUnitEnum.HOURS_IN_HALF_DAY)) { - updatedDateTime = - updatedDateTime.withHour(dateParts.get(ChronoUnitEnum.HOURS_IN_HALF_DAY).intValue()); - } - - if (dateParts.containsKey(ChronoUnitEnum.MINUTES_IN_HOUR)) { - updatedDateTime = - updatedDateTime.withMinute(dateParts.get(ChronoUnitEnum.MINUTES_IN_HOUR).intValue()); - } - - if (dateParts.containsKey(ChronoUnitEnum.SECONDS_IN_DAY)) { - updatedDateTime = - updatedDateTime.with(ChronoField.SECOND_OF_DAY, - dateParts.get(ChronoUnitEnum.SECONDS_IN_DAY)); - } - - if (dateParts.containsKey(ChronoUnitEnum.SECONDS_IN_MINUTE)) { - updatedDateTime = - updatedDateTime.withSecond(dateParts.get(ChronoUnitEnum.SECONDS_IN_MINUTE).intValue()); - } - - if (dateParts.containsKey(ChronoUnitEnum.MILLIS)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MILLI_OF_SECOND, - dateParts.get(ChronoUnitEnum.MILLIS)); - } - - if (dateParts.containsKey(ChronoUnitEnum.MICROS)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MICRO_OF_SECOND, - dateParts.get(ChronoUnitEnum.MICROS)); - } - - if (dateParts.containsKey(ChronoUnitEnum.TENTHS_OF_SECOND)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MILLI_OF_SECOND, - 100 * dateParts.get(ChronoUnitEnum.TENTHS_OF_SECOND)); - } - - if (dateParts.containsKey(ChronoUnitEnum.HUNDREDTHS_OF_SECOND)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MILLI_OF_SECOND, - 10 * dateParts.get(ChronoUnitEnum.HUNDREDTHS_OF_SECOND)); - } - - if (dateParts.containsKey(ChronoUnitEnum.THOUSANDTHS_OF_SECOND)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MILLI_OF_SECOND, - dateParts.get(ChronoUnitEnum.THOUSANDTHS_OF_SECOND)); - } - - if (dateParts.containsKey(ChronoUnitEnum.TENTHS_OF_MS)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MICRO_OF_SECOND, - 100 * dateParts.get(ChronoUnitEnum.TENTHS_OF_MS)); - } - - if (dateParts.containsKey(ChronoUnitEnum.HUNDREDTHS_OF_MS)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MICRO_OF_SECOND, - 10 * dateParts.get(ChronoUnitEnum.HUNDREDTHS_OF_MS)); - } - - if (dateParts.containsKey(ChronoUnitEnum.THOUSANDTHS_OF_MS)) { - updatedDateTime = - updatedDateTime.with(ChronoField.MICRO_OF_SECOND, - dateParts.get(ChronoUnitEnum.THOUSANDTHS_OF_MS)); - } - - return updatedDateTime; + return null; } } diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/StringFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/StringFormatPattern.java deleted file mode 100644 index 65d75359f322..000000000000 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/StringFormatPattern.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.postgresql; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.text.ParsePosition; -import java.time.ZonedDateTime; -import java.util.Locale; - -/** - * A format element that will produce a string. The "FM" prefix and "TH"/"th" suffixes - * will be silently consumed when the pattern matches. - */ -public abstract class StringFormatPattern extends FormatPattern { - private final ChronoUnitEnum chronoUnit; - - /** - * Constructs a new StringFormatPattern for the provide list of pattern strings and - * ChronoUnitEnum value. Child classes must use this constructor. - * - * @param chronoUnit ChronoUnitEnum value that this pattern parses - * @param patterns array of pattern strings - */ - protected StringFormatPattern(ChronoUnitEnum chronoUnit, String... patterns) { - super(patterns); - this.chronoUnit = chronoUnit; - } - - @Override public @Nullable String convert(ParsePosition parsePosition, String formatString, - ZonedDateTime dateTime, Locale locale) { - String formatStringTrimmed = formatString.substring(parsePosition.getIndex()); - - boolean haveFillMode = false; - boolean haveTranslationMode = false; - if (formatStringTrimmed.startsWith("FMTM") || formatStringTrimmed.startsWith("TMFM")) { - haveFillMode = true; - haveTranslationMode = true; - formatStringTrimmed = formatStringTrimmed.substring(4); - } else if (formatStringTrimmed.startsWith("FM")) { - haveFillMode = true; - formatStringTrimmed = formatStringTrimmed.substring(2); - } else if (formatStringTrimmed.startsWith("TM")) { - haveTranslationMode = true; - formatStringTrimmed = formatStringTrimmed.substring(2); - } - - String patternToUse = null; - for (String pattern : getPatterns()) { - if (formatStringTrimmed.startsWith(pattern)) { - patternToUse = pattern; - break; - } - } - - if (patternToUse == null) { - return null; - } - - formatStringTrimmed = formatStringTrimmed.substring(patternToUse.length()); - final String suffix; - if (formatStringTrimmed.startsWith("TH") || formatStringTrimmed.startsWith("th")) { - suffix = formatStringTrimmed.substring(0, 2); - } else { - suffix = null; - } - - parsePosition.setIndex(parsePosition.getIndex() + patternToUse.length() - + (haveFillMode ? 2 : 0) + (haveTranslationMode ? 2 : 0) - + (suffix != null ? suffix.length() : 0)); - return dateTimeToString( - dateTime, - haveFillMode, - suffix, - haveTranslationMode ? locale : Locale.US); - } - - @Override public ChronoUnitEnum getChronoUnit() { - return chronoUnit; - } - - /** - * Extracts the datetime component from the provided datetime and formats it. This may - * also involve translation to the provided locale. - * - * @param dateTime extract the datetime component from here - * @param haveFillMode is fill mode enabled - * @param suffix suffix modifier if any (TH or th) - * @param locale locale to translate to - * @return formatted string representation of datetime component - */ - protected abstract String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale); - - @Override protected boolean isNumeric() { - return false; - } -} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/AmPmFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/AmPmFormatPattern.java new file mode 100644 index 000000000000..971c44ab0cac --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/AmPmFormatPattern.java @@ -0,0 +1,60 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.AmPmCompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for AM/PM (first or second half of day). + */ +public class AmPmFormatPattern extends FormatPattern { + private final boolean upperCase; + private final boolean includePeriods; + + public AmPmFormatPattern(String pattern) { + super(pattern); + switch (pattern) { + case "am": + case "pm": + this.upperCase = false; + this.includePeriods = false; + break; + case "AM": + case "PM": + this.upperCase = true; + this.includePeriods = false; + break; + case "a.m.": + case "p.m.": + this.upperCase = false; + this.includePeriods = true; + break; + default: + this.upperCase = true; + this.includePeriods = true; + break; + } + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new AmPmCompiledPattern(modifiers, upperCase, includePeriods); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/BcAdFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/BcAdFormatPattern.java new file mode 100644 index 000000000000..6234356f9a98 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/BcAdFormatPattern.java @@ -0,0 +1,60 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.BcAdCompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for BC/AD (era, ie. BCE/CE). + */ +public class BcAdFormatPattern extends FormatPattern { + private final boolean upperCase; + private final boolean includePeriods; + + public BcAdFormatPattern(String pattern) { + super(pattern); + switch (pattern) { + case "bc": + case "ad": + this.upperCase = false; + this.includePeriods = false; + break; + case "BC": + case "AD": + this.upperCase = true; + this.includePeriods = false; + break; + case "b.c.": + case "a.d.": + this.upperCase = false; + this.includePeriods = true; + break; + default: + this.upperCase = true; + this.includePeriods = true; + break; + } + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new BcAdCompiledPattern(modifiers, upperCase, includePeriods); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/DayOfWeekFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/DayOfWeekFormatPattern.java new file mode 100644 index 000000000000..8a9e5a3a73d6 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/DayOfWeekFormatPattern.java @@ -0,0 +1,67 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.CapitalizationEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.DayOfWeekCompiledPattern; + +import java.time.format.TextStyle; +import java.util.Set; + +/** + * The date/time format component for a text representation of a day of the week. + */ +public class DayOfWeekFormatPattern extends FormatPattern { + private final CapitalizationEnum capitalization; + private final TextStyle textStyle; + + public DayOfWeekFormatPattern(String pattern) { + super(pattern); + switch (pattern) { + case "DAY": + capitalization = CapitalizationEnum.ALL_UPPER; + textStyle = TextStyle.FULL; + break; + case "Day": + capitalization = CapitalizationEnum.CAPITALIZED; + textStyle = TextStyle.FULL; + break; + case "day": + capitalization = CapitalizationEnum.ALL_LOWER; + textStyle = TextStyle.FULL; + break; + case "DY": + capitalization = CapitalizationEnum.ALL_UPPER; + textStyle = TextStyle.SHORT; + break; + case "Dy": + capitalization = CapitalizationEnum.CAPITALIZED; + textStyle = TextStyle.SHORT; + break; + default: + capitalization = CapitalizationEnum.ALL_LOWER; + textStyle = TextStyle.SHORT; + break; + } + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new DayOfWeekCompiledPattern(modifiers, capitalization, textStyle); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/FormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/FormatPattern.java new file mode 100644 index 000000000000..c9bce3466423 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/FormatPattern.java @@ -0,0 +1,114 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; + +import com.google.common.collect.ImmutableSet; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Set; + +/** + * A single component of a date/time format pattern such as YYYY or MI. Each + * component may have some flags applied. If the flags are not used by a component, + * then they are ignored. + */ +public abstract class FormatPattern { + protected final String pattern; + + /** + * Base class constructor that stores the pattern. + * + * @param pattern the date/time component taht this FormatPattern represents + */ + protected FormatPattern(String pattern) { + this.pattern = pattern; + } + + /** + * Creates the compiled version of the parsed date/time component along with + * any flags that it had. It is expected that the formatString has the date/time + * component (with its flags) at the position indicated in formatParsePosition. + * + * @param formatString the full date/time format string + * @param formatParsePosition starting position in formatString with this + * pattern is located + * @return the compiled version of the parsed date/time component + * @throws ParseException If the date/time format component was not found + */ + public CompiledPattern compilePattern(String formatString, + ParsePosition formatParsePosition) throws ParseException { + String formatTrimmed = formatString.substring(formatParsePosition.getIndex()); + int charsConsumed = 0; + final ImmutableSet.Builder modifiers = ImmutableSet.builder(); + + // Find the prefix modifiers + boolean modifierFound = true; + while (modifierFound) { + modifierFound = false; + + for (PatternModifier modifier : PatternModifier.values()) { + if (modifier.isPrefix() && formatTrimmed.startsWith(modifier.getModifierString())) { + modifiers.add(modifier); + formatTrimmed = formatTrimmed.substring(modifier.getModifierString().length()); + charsConsumed += modifier.getModifierString().length(); + modifierFound = true; + break; + } + } + } + + if (formatTrimmed.startsWith(pattern)) { + charsConsumed += pattern.length(); + formatTrimmed = formatString.substring(formatParsePosition.getIndex() + charsConsumed); + + // Find the suffix modifiers + modifierFound = true; + while (modifierFound) { + modifierFound = false; + + for (PatternModifier modifier : PatternModifier.values()) { + if (!modifier.isPrefix() && formatTrimmed.startsWith(modifier.getModifierString())) { + modifiers.add(modifier); + formatTrimmed = formatTrimmed.substring(modifier.getModifierString().length()); + modifierFound = true; + break; + } + } + } + + return buildCompiledPattern(modifiers.build()); + } + + throw new ParseException("Pattern not found", formatParsePosition.getIndex()); + } + + /** + * Creates a new instance of the compiled version of this date/time component. + * + * @param modifiers the set of flags that were parsed + * @return a new instance of the compiled version of this date/time component + */ + protected abstract CompiledPattern buildCompiledPattern(Set modifiers); + + public String getPattern() { + return pattern; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/MonthFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/MonthFormatPattern.java new file mode 100644 index 000000000000..8bd06ddd5864 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/MonthFormatPattern.java @@ -0,0 +1,67 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.CapitalizationEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.MonthCompiledPattern; + +import java.time.format.TextStyle; +import java.util.Set; + +/** + * The date/time format component for a text representation of a month. + */ +public class MonthFormatPattern extends FormatPattern { + private final CapitalizationEnum capitalization; + private final TextStyle textStyle; + + public MonthFormatPattern(String pattern) { + super(pattern); + switch (pattern) { + case "MONTH": + capitalization = CapitalizationEnum.ALL_UPPER; + textStyle = TextStyle.FULL; + break; + case "Month": + capitalization = CapitalizationEnum.CAPITALIZED; + textStyle = TextStyle.FULL; + break; + case "month": + capitalization = CapitalizationEnum.ALL_LOWER; + textStyle = TextStyle.FULL; + break; + case "MON": + capitalization = CapitalizationEnum.ALL_UPPER; + textStyle = TextStyle.SHORT; + break; + case "Mon": + capitalization = CapitalizationEnum.CAPITALIZED; + textStyle = TextStyle.SHORT; + break; + default: + capitalization = CapitalizationEnum.ALL_LOWER; + textStyle = TextStyle.SHORT; + break; + } + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new MonthCompiledPattern(modifiers, capitalization, textStyle); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/NumberFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/NumberFormatPattern.java new file mode 100644 index 000000000000..41d49d4f82b3 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/NumberFormatPattern.java @@ -0,0 +1,74 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.NumberCompiledPattern; + +import java.time.ZonedDateTime; +import java.util.Set; +import java.util.function.Function; + +/** + * A date/time format component that will parse a sequence of digits into a value + * (such as "DD"). + */ +public class NumberFormatPattern extends FormatPattern { + private final ChronoUnitEnum chronoUnit; + private final Function dateTimeToIntFunction; + private final Function valueAdjuster; + private final int minCharacters; + private final int maxCharacters; + private final int minValue; + private final int maxValue; + + public NumberFormatPattern(String pattern, ChronoUnitEnum chronoUnit, + Function dateTimeToIntConverter, + Function valueAdjuster, int minCharacters, int maxCharacters, int minValue, + int maxValue) { + super(pattern); + this.chronoUnit = chronoUnit; + this.dateTimeToIntFunction = dateTimeToIntConverter; + this.valueAdjuster = valueAdjuster; + this.minCharacters = minCharacters; + this.maxCharacters = maxCharacters; + this.minValue = minValue; + this.maxValue = maxValue; + } + + public NumberFormatPattern(String pattern, ChronoUnitEnum chronoUnit, + Function dateTimeToIntConverter, int minCharacters, + int maxCharacters, int minValue, int maxValue) { + this(pattern, chronoUnit, dateTimeToIntConverter, v -> v, minCharacters, maxCharacters, + minValue, maxValue); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new NumberCompiledPattern( + chronoUnit, + dateTimeToIntFunction, + valueAdjuster, + minCharacters, + maxCharacters, + minValue, + maxValue, + pattern, + modifiers); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/RomanNumeralsFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/RomanNumeralsFormatPattern.java new file mode 100644 index 000000000000..55f64dbb8cd6 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/RomanNumeralsFormatPattern.java @@ -0,0 +1,39 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.RomanNumeralsCompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for the roman numeral representation of a month. + */ +public class RomanNumeralsFormatPattern extends FormatPattern { + private final boolean upperCase; + + public RomanNumeralsFormatPattern(String pattern) { + super(pattern); + this.upperCase = "RM".equals(pattern); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new RomanNumeralsCompiledPattern(modifiers, upperCase); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneFormatPattern.java new file mode 100644 index 000000000000..a50b6379fe88 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneFormatPattern.java @@ -0,0 +1,40 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.TimeZoneCompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for the 3 letter timezone code (such as UTC). + * This is only supported when converting a date/time value to a string. + */ +public class TimeZoneFormatPattern extends FormatPattern { + private final boolean upperCase; + + public TimeZoneFormatPattern(String pattern) { + super(pattern); + upperCase = "TZ".equals(pattern); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new TimeZoneCompiledPattern(modifiers, upperCase); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneHoursFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneHoursFormatPattern.java new file mode 100644 index 000000000000..3364e2312dc7 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneHoursFormatPattern.java @@ -0,0 +1,36 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.TimeZoneHoursCompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for the hours of the timezone offset. + */ +public class TimeZoneHoursFormatPattern extends FormatPattern { + public TimeZoneHoursFormatPattern() { + super("TZH"); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new TimeZoneHoursCompiledPattern(modifiers); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneMinutesFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneMinutesFormatPattern.java new file mode 100644 index 000000000000..251603f7e83f --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneMinutesFormatPattern.java @@ -0,0 +1,36 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.TimeZoneMinutesCompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for the minutes of the timezone offset. + */ +public class TimeZoneMinutesFormatPattern extends FormatPattern { + public TimeZoneMinutesFormatPattern() { + super("TZM"); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new TimeZoneMinutesCompiledPattern(modifiers); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneOffsetFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneOffsetFormatPattern.java new file mode 100644 index 000000000000..7bcf69eaa590 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/TimeZoneOffsetFormatPattern.java @@ -0,0 +1,38 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.TimeZoneOffsetCompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for the hours and minutes of the timezone offset. Can be + * just two digits plus sign if the UTC offset is a whole number of hours. Otherwise, + * it is a value similar to "+07:30". + */ +public class TimeZoneOffsetFormatPattern extends FormatPattern { + public TimeZoneOffsetFormatPattern() { + super("OF"); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new TimeZoneOffsetCompiledPattern(modifiers); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/YearWithCommasFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/YearWithCommasFormatPattern.java new file mode 100644 index 000000000000..29ef1da22b3c --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/YearWithCommasFormatPattern.java @@ -0,0 +1,37 @@ +/* + * 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.postgresql.format; + +import org.apache.calcite.util.format.postgresql.PatternModifier; +import org.apache.calcite.util.format.postgresql.format.compiled.CompiledPattern; +import org.apache.calcite.util.format.postgresql.format.compiled.YearWithCommasCompiledPattern; + +import java.util.Set; + +/** + * The date/time format component for the year formatted with a comma after the thousands + * (such as "2,024"). + */ +public class YearWithCommasFormatPattern extends FormatPattern { + public YearWithCommasFormatPattern() { + super("Y,YYY"); + } + + @Override protected CompiledPattern buildCompiledPattern(Set modifiers) { + return new YearWithCommasCompiledPattern(modifiers); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/AmPmCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/AmPmCompiledPattern.java new file mode 100644 index 000000000000..264d378d61bb --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/AmPmCompiledPattern.java @@ -0,0 +1,83 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.util.Locale; +import java.util.Set; + +/** + * The date/time format compiled component for AM/PM (first or second half of day). + */ +public class AmPmCompiledPattern extends CompiledPattern { + private final boolean upperCase; + private final boolean includePeriods; + + public AmPmCompiledPattern(Set modifiers, boolean upperCase, + boolean includePeriods) { + super(ChronoUnitEnum.HALF_DAYS, modifiers); + this.upperCase = upperCase; + this.includePeriods = includePeriods; + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final int intValue = dateTime.get(ChronoField.AMPM_OF_DAY); + final String stringValue; + if (intValue == 0) { + stringValue = includePeriods ? "a.m." : "am"; + } else { + stringValue = includePeriods ? "p.m." : "pm"; + } + + if (upperCase) { + return stringValue.toUpperCase(Locale.ROOT); + } + + return stringValue; + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + String amValue = includePeriods ? "a.m." : "am"; + String pmValue = includePeriods ? "p.m." : "pm"; + if (upperCase) { + amValue = amValue.toUpperCase(Locale.ROOT); + pmValue = pmValue.toUpperCase(Locale.ROOT); + } + + final String inputTrimmed = input.substring(inputPosition.getIndex()); + if (inputTrimmed.startsWith(amValue)) { + inputPosition.setIndex(inputPosition.getIndex() + amValue.length()); + return 0; + } else if (inputTrimmed.startsWith(pmValue)) { + inputPosition.setIndex(inputPosition.getIndex() + pmValue.length()); + return 1; + } + + throw new ParseException("Unable to parse value", inputPosition.getIndex()); + } + + @Override protected int getBaseFormatPatternLength() { + return includePeriods ? 4 : 2; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/BcAdCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/BcAdCompiledPattern.java new file mode 100644 index 000000000000..900260bdcdc5 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/BcAdCompiledPattern.java @@ -0,0 +1,83 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.util.Locale; +import java.util.Set; + +/** + * The date/time format compiled component for BC/AD (era, ie. BCE/CE). + */ +public class BcAdCompiledPattern extends CompiledPattern { + private final boolean upperCase; + private final boolean includePeriods; + + public BcAdCompiledPattern(Set modifiers, boolean upperCase, + boolean includePeriods) { + super(ChronoUnitEnum.ERAS, modifiers); + this.upperCase = upperCase; + this.includePeriods = includePeriods; + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final int intValue = dateTime.get(ChronoField.ERA); + final String stringValue; + if (intValue == 0) { + stringValue = includePeriods ? "b.c." : "bc"; + } else { + stringValue = includePeriods ? "a.d." : "ad"; + } + + if (upperCase) { + return stringValue.toUpperCase(Locale.ROOT); + } + + return stringValue; + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + String bcValue = includePeriods ? "b.c." : "bc"; + String pmValue = includePeriods ? "a.d." : "ad"; + if (upperCase) { + bcValue = bcValue.toUpperCase(Locale.ROOT); + pmValue = pmValue.toUpperCase(Locale.ROOT); + } + + final String inputTrimmed = input.substring(inputPosition.getIndex()); + if (inputTrimmed.startsWith(bcValue)) { + inputPosition.setIndex(inputPosition.getIndex() + bcValue.length()); + return 0; + } else if (inputTrimmed.startsWith(pmValue)) { + inputPosition.setIndex(inputPosition.getIndex() + pmValue.length()); + return 1; + } + + throw new ParseException("Unable to parse value", inputPosition.getIndex()); + } + + @Override protected int getBaseFormatPatternLength() { + return includePeriods ? 4 : 2; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/CompiledItem.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/CompiledItem.java new file mode 100644 index 000000000000..a2f41b601eec --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/CompiledItem.java @@ -0,0 +1,43 @@ +/* + * 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.postgresql.format.compiled; + +import java.time.ZonedDateTime; +import java.util.Locale; + +/** + * A single component of a parsed date/time format. Can be a pattern such as + * "YYYY" or a literal string. Able to output the string representation of a + * date/time component. + */ +public interface CompiledItem { + /** + * Creates one portion of the formatted date/time. + * + * @param dateTime date/time value to format + * @param locale Locale to use for day or month names if the TM modifier was present + * @return the formatted String value of this date/time component + */ + String convertToString(ZonedDateTime dateTime, Locale locale); + + /** + * Returns the length of the format pattern. + * + * @return length of the format pattern + */ + int getFormatPatternLength(); +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/CompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/CompiledPattern.java new file mode 100644 index 000000000000..9a4101bbfe73 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/CompiledPattern.java @@ -0,0 +1,82 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Locale; +import java.util.Set; + +/** + * A single date/time format compiled component such as "YYYY" or "MI". A compiled component + * is a format component along with any flags it had. Adds the ability to parse a string. + */ +public abstract class CompiledPattern implements CompiledItem { + private final ChronoUnitEnum chronoUnit; + protected final Set modifiers; + + protected CompiledPattern(ChronoUnitEnum chronoUnit, Set modifiers) { + this.chronoUnit = chronoUnit; + this.modifiers = modifiers; + } + + /** + * Get the ChronoUnitEnum value that this pattern is for. + * + * @return a ChronoUnitEnum value + */ + public ChronoUnitEnum getChronoUnit() { + return chronoUnit; + } + + /** + * Parse this date/time component from a String. + * + * @param inputPosition starting position for parsing + * @param input full string that will be parsed + * @param enforceLength whether to limit the length of characters read. Needed when one + * sequence of digits is followed by another (such as YYYYDD). + * @param locale Locale to use for parsing day and month names if the TM flag was present + * @return the integer value of the parsed date/time component + * @throws ParseException if unable to parse a value from input + */ + public abstract int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException; + + @Override public int getFormatPatternLength() { + return 2 * modifiers.size() + getBaseFormatPatternLength(); + } + + /** + * Returns the length of the format pattern without modifiers. + * + * @return length of the format pattern + */ + protected abstract int getBaseFormatPatternLength(); + + /** + * Does this pattern match a sequence of digits. + * + * @return true if this pattern matches a sequence of digits + */ + public boolean isNumeric() { + return false; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/DayOfWeekCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/DayOfWeekCompiledPattern.java new file mode 100644 index 000000000000..2de0ddfcf846 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/DayOfWeekCompiledPattern.java @@ -0,0 +1,81 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.CapitalizationEnum; +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.format.TextStyle; +import java.util.Locale; +import java.util.Set; + +/** + * The date/time format compiled component for a text representation of a day of + * the week. + */ +public class DayOfWeekCompiledPattern extends CompiledPattern { + private final CapitalizationEnum capitalization; + private final TextStyle textStyle; + + public DayOfWeekCompiledPattern(Set modifiers, + CapitalizationEnum capitalization, TextStyle textStyle) { + super(ChronoUnitEnum.DAYS_IN_WEEK, modifiers); + this.capitalization = capitalization; + this.textStyle = textStyle; + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final Locale localeToUse = modifiers.contains(PatternModifier.TM) ? locale : Locale.US; + final int intValue = dateTime.getDayOfWeek().getValue(); + final String stringValue = + capitalization.apply( + DayOfWeek.of(intValue).getDisplayName(textStyle, localeToUse), localeToUse); + + if (textStyle == TextStyle.FULL && !modifiers.contains(PatternModifier.FM) + && !modifiers.contains(PatternModifier.TM)) { + return String.format(locale, "%-9s", stringValue); + } + + return stringValue; + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + final Locale localeToUse = modifiers.contains(PatternModifier.TM) ? locale : Locale.US; + final String inputTrimmed = input.substring(inputPosition.getIndex()); + + for (DayOfWeek dayOfWeek : DayOfWeek.values()) { + final String expectedValue = + capitalization.apply(dayOfWeek.getDisplayName(textStyle, localeToUse), localeToUse); + if (inputTrimmed.startsWith(expectedValue)) { + inputPosition.setIndex(inputPosition.getIndex() + expectedValue.length()); + return dayOfWeek.getValue(); + } + } + + throw new ParseException("Unable to parse value", inputPosition.getIndex()); + } + + @Override protected int getBaseFormatPatternLength() { + return textStyle == TextStyle.FULL ? 3 : 2; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/LiteralCompiledItem.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/LiteralCompiledItem.java new file mode 100644 index 000000000000..3ef72cf3668d --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/LiteralCompiledItem.java @@ -0,0 +1,42 @@ +/* + * 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.postgresql.format.compiled; + +import java.time.ZonedDateTime; +import java.util.Locale; + +/** + * A literal string in a date/time format. When converting a date/time to a string, + * the literal value is output exactly. When parsing a string into a date/time value, + * just need to consume a string of equal length to the literal (can be a different + * string). + */ +public class LiteralCompiledItem implements CompiledItem { + private final String literalValue; + + public LiteralCompiledItem(String literalValue) { + this.literalValue = literalValue; + } + + @Override public String convertToString(final ZonedDateTime dateTime, final Locale locale) { + return literalValue; + } + + @Override public int getFormatPatternLength() { + return literalValue.length(); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/MonthCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/MonthCompiledPattern.java new file mode 100644 index 000000000000..fd7a8f3cabcb --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/MonthCompiledPattern.java @@ -0,0 +1,80 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.CapitalizationEnum; +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.Month; +import java.time.ZonedDateTime; +import java.time.format.TextStyle; +import java.util.Locale; +import java.util.Set; + +/** + * The date/time format compiled component for a text representation of a month. + */ +public class MonthCompiledPattern extends CompiledPattern { + private final CapitalizationEnum capitalization; + private final TextStyle textStyle; + + public MonthCompiledPattern(Set modifiers, CapitalizationEnum capitalization, + TextStyle textStyle) { + super(ChronoUnitEnum.MONTHS_IN_YEAR, modifiers); + this.capitalization = capitalization; + this.textStyle = textStyle; + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final Locale localeToUse = modifiers.contains(PatternModifier.TM) ? locale : Locale.US; + final int intValue = dateTime.getMonthValue(); + final String stringValue = + capitalization.apply( + Month.of(intValue).getDisplayName(textStyle, localeToUse), localeToUse); + + if (textStyle == TextStyle.FULL && !modifiers.contains(PatternModifier.FM) + && !modifiers.contains(PatternModifier.TM)) { + return String.format(locale, "%-9s", stringValue); + } + + return stringValue; + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + final Locale localeToUse = modifiers.contains(PatternModifier.TM) ? locale : Locale.US; + final String inputTrimmed = input.substring(inputPosition.getIndex()); + + for (Month month : Month.values()) { + final String expectedValue = + capitalization.apply(month.getDisplayName(textStyle, localeToUse), localeToUse); + if (inputTrimmed.startsWith(expectedValue)) { + inputPosition.setIndex(inputPosition.getIndex() + expectedValue.length()); + return month.getValue(); + } + } + + throw new ParseException("Unable to parse value", inputPosition.getIndex()); + } + + @Override protected int getBaseFormatPatternLength() { + return textStyle == TextStyle.FULL ? 5 : 3; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/NumberCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/NumberCompiledPattern.java new file mode 100644 index 000000000000..58e96e080d7f --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/NumberCompiledPattern.java @@ -0,0 +1,142 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.ZonedDateTime; +import java.util.Locale; +import java.util.Set; +import java.util.function.Function; + +import static java.lang.Integer.parseInt; + +/** + * A date/time format compiled component that will parse a sequence of digits into + * a value (such as "DD"). + */ +public class NumberCompiledPattern extends CompiledPattern { + private final Function dateTimeToIntConverter; + private final Function valueAdjuster; + private final int minCharacters; + private final int maxCharacters; + private final int minValue; + private final int maxValue; + private final String pattern; + + public NumberCompiledPattern(ChronoUnitEnum chronoUnit, + Function dateTimeToIntConverter, + Function valueAdjuster, int minCharacters, int maxCharacters, int minValue, + int maxValue, String pattern, Set modifiers) { + super(chronoUnit, modifiers); + this.dateTimeToIntConverter = dateTimeToIntConverter; + this.valueAdjuster = valueAdjuster; + this.minCharacters = minCharacters; + this.maxCharacters = maxCharacters; + this.minValue = minValue; + this.maxValue = maxValue; + this.pattern = pattern; + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final long intValue = dateTimeToIntConverter.apply(dateTime); + final String signPrefix = intValue < 0 ? "-" : ""; + String stringValue; + if (!modifiers.contains(PatternModifier.FM) && minCharacters > 0) { + stringValue = + String.format(Locale.ROOT, signPrefix + "%0" + minCharacters + "d", Math.abs(intValue)); + } else { + stringValue = Long.toString(intValue); + } + + if (maxCharacters > 0 && stringValue.length() - signPrefix.length() > maxCharacters) { + stringValue = stringValue.substring(stringValue.length() - maxCharacters); + } + + if (modifiers.contains(PatternModifier.TH_UPPER)) { + return stringValue + getOrdinalSuffix(stringValue).toUpperCase(Locale.ROOT); + } else if (modifiers.contains(PatternModifier.TH_LOWER)) { + return stringValue + getOrdinalSuffix(stringValue); + } + + return stringValue; + } + + private String getOrdinalSuffix(String stringValue) { + // 10 through 19 have a th suffix + if (stringValue.length() >= 2 && stringValue.charAt(stringValue.length() - 2) == '1') { + return "th"; + } + + switch (stringValue.charAt(stringValue.length() - 1)) { + case '1': + return "st"; + case '2': + return "nd"; + case '3': + return "rd"; + default: + return "th"; + } + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + int endIndex = inputPosition.getIndex(); + for (; endIndex < input.length(); endIndex++) { + if (input.charAt(endIndex) < '0' || input.charAt(endIndex) > '9') { + break; + } else if (enforceLength && endIndex == inputPosition.getIndex() + minCharacters) { + break; + } + } + + if (modifiers.contains(PatternModifier.TH_UPPER) + || modifiers.contains(PatternModifier.TH_LOWER)) { + if (endIndex < input.length() - 1) { + endIndex += 2; + } else if (endIndex < input.length()) { + endIndex++; + } + } + + if (endIndex == inputPosition.getIndex()) { + throw new ParseException("Unable to parse value", inputPosition.getIndex()); + } + + int value = parseInt(input.substring(inputPosition.getIndex(), endIndex)); + if (value < minValue || value > maxValue) { + throw new ParseException("Parsed value outside of valid range", inputPosition.getIndex()); + } + + value = valueAdjuster.apply(value); + + inputPosition.setIndex(endIndex); + return value; + } + + @Override protected int getBaseFormatPatternLength() { + return pattern.length(); + } + + @Override public boolean isNumeric() { + return true; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/RomanNumeralMonthFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/RomanNumeralsCompiledPattern.java similarity index 80% rename from core/src/main/java/org/apache/calcite/util/format/postgresql/RomanNumeralMonthFormatPattern.java rename to core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/RomanNumeralsCompiledPattern.java index 6e18215ce58d..f3575763f8cd 100644 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/RomanNumeralMonthFormatPattern.java +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/RomanNumeralsCompiledPattern.java @@ -14,28 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.format.postgresql; +package org.apache.calcite.util.format.postgresql.format.compiled; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; import java.text.ParseException; import java.text.ParsePosition; import java.time.ZonedDateTime; import java.util.Locale; +import java.util.Set; /** - * Converts a Roman numeral value (between 1 and 12) to a month value and back. + * The date/time format compiled component for the roman numeral representation of a + * month. */ -public class RomanNumeralMonthFormatPattern extends StringFormatPattern { +public class RomanNumeralsCompiledPattern extends CompiledPattern { private final boolean upperCase; - public RomanNumeralMonthFormatPattern(boolean upperCase, String... patterns) { - super(ChronoUnitEnum.MONTHS_IN_YEAR, patterns); + public RomanNumeralsCompiledPattern(Set modifiers, boolean upperCase) { + super(ChronoUnitEnum.MONTHS_IN_YEAR, modifiers); this.upperCase = upperCase; } - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { final String romanNumeral; switch (dateTime.getMonth().getValue()) { @@ -80,12 +82,12 @@ public RomanNumeralMonthFormatPattern(boolean upperCase, String... patterns) { if (upperCase) { return romanNumeral; } else { - return romanNumeral.toLowerCase(locale); + return romanNumeral.toLowerCase(Locale.US); } } - @Override protected int parseValue(ParsePosition inputPosition, String input, Locale locale, - boolean haveFillMode, boolean enforceLength) throws ParseException { + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { final String inputTrimmed = input.substring(inputPosition.getIndex()); if (inputTrimmed.startsWith(upperCase ? "III" : "iii")) { @@ -128,4 +130,8 @@ public RomanNumeralMonthFormatPattern(boolean upperCase, String... patterns) { throw new ParseException("Unable to parse value", inputPosition.getIndex()); } + + @Override protected int getBaseFormatPatternLength() { + return 2; + } } diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneCompiledPattern.java new file mode 100644 index 000000000000..ca37846d289a --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneCompiledPattern.java @@ -0,0 +1,64 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.ZonedDateTime; +import java.time.format.TextStyle; +import java.util.Locale; +import java.util.Set; + +/** + * The date/time format compiled component for the 3 letter timezone code (such as UTC). + * This is only supported when converting a date/time value to a string. + */ +public class TimeZoneCompiledPattern extends CompiledPattern { + private final boolean upperCase; + + public TimeZoneCompiledPattern(Set modifiers, boolean upperCase) { + super(ChronoUnitEnum.TIMEZONE_MINUTES, modifiers); + this.upperCase = upperCase; + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final String stringValue = + String.format( + locale, + "%3s", + dateTime.getZone().getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase(locale)); + + if (upperCase) { + return stringValue.toUpperCase(Locale.US); + } + + return stringValue; + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + throw new ParseException("TZ pattern is not supported in parsing datetime values", + inputPosition.getIndex()); + } + + @Override protected int getBaseFormatPatternLength() { + return 2; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneHoursFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneHoursCompiledPattern.java similarity index 73% rename from core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneHoursFormatPattern.java rename to core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneHoursCompiledPattern.java index 906c6d6777f5..38a015c7f4bb 100644 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneHoursFormatPattern.java +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneHoursCompiledPattern.java @@ -14,32 +14,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.format.postgresql; +package org.apache.calcite.util.format.postgresql.format.compiled; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; import java.text.ParseException; import java.text.ParsePosition; import java.time.ZonedDateTime; import java.time.temporal.ChronoField; import java.util.Locale; +import java.util.Set; import static java.lang.Integer.parseInt; /** - * Able to parse timezone hours from string and to generate a string of the timezone - * hours from a datetime. Timezone hours always have a sign (+/-) and are between - * -15 and +15. + * The date/time format compiled component for the hours of the timezone offset. */ -public class TimeZoneHoursFormatPattern extends StringFormatPattern { - public TimeZoneHoursFormatPattern() { - super(ChronoUnitEnum.TIMEZONE_HOURS, "TZH"); +public class TimeZoneHoursCompiledPattern extends CompiledPattern { + public TimeZoneHoursCompiledPattern(Set modifiers) { + super(ChronoUnitEnum.TIMEZONE_HOURS, modifiers); } - @Override protected int parseValue(final ParsePosition inputPosition, final String input, - final Locale locale, final boolean haveFillMode, boolean enforceLength) - throws ParseException { + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + return String.format( + Locale.ROOT, + "%+02d", + dateTime.getOffset().get(ChronoField.OFFSET_SECONDS) / 3600); + } + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { int inputOffset = inputPosition.getIndex(); String inputTrimmed = input.substring(inputOffset); @@ -76,15 +81,11 @@ public TimeZoneHoursFormatPattern() { return isPositive ? timezoneHours : -1 * timezoneHours; } - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - return String.format( - Locale.ROOT, - "%+02d", - dateTime.getOffset().get(ChronoField.OFFSET_SECONDS) / 3600); + @Override protected int getBaseFormatPatternLength() { + return 3; } - @Override protected boolean isNumeric() { + @Override public boolean isNumeric() { return true; } } diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneMinutesFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneMinutesCompiledPattern.java similarity index 69% rename from core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneMinutesFormatPattern.java rename to core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneMinutesCompiledPattern.java index bd1a75895501..5d2585302f58 100644 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneMinutesFormatPattern.java +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneMinutesCompiledPattern.java @@ -14,32 +14,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.format.postgresql; +package org.apache.calcite.util.format.postgresql.format.compiled; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; import java.text.ParseException; import java.text.ParsePosition; import java.time.ZonedDateTime; import java.time.temporal.ChronoField; import java.util.Locale; +import java.util.Set; import static java.lang.Integer.parseInt; /** - * Able to parse timezone minutes from string and to generate a string of the timezone - * minutes from a datetime. Timezone minutes always have two digits and are between - * 00 and 59. + * The date/time format compiled component for the minutes of the timezone offset. */ -public class TimeZoneMinutesFormatPattern extends StringFormatPattern { - public TimeZoneMinutesFormatPattern() { - super(ChronoUnitEnum.TIMEZONE_MINUTES, "TZM"); +public class TimeZoneMinutesCompiledPattern extends CompiledPattern { + public TimeZoneMinutesCompiledPattern(Set modifiers) { + super(ChronoUnitEnum.TIMEZONE_MINUTES, modifiers); } - @Override protected int parseValue(final ParsePosition inputPosition, final String input, - final Locale locale, final boolean haveFillMode, boolean enforceLength) - throws ParseException { + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + return String.format( + Locale.ROOT, + "%02d", + (dateTime.getOffset().get(ChronoField.OFFSET_SECONDS) % 3600) / 60); + } + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { if (inputPosition.getIndex() + 2 > input.length()) { throw new ParseException("Unable to parse value", inputPosition.getIndex()); } @@ -62,15 +67,11 @@ public TimeZoneMinutesFormatPattern() { return timezoneMinutes; } - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - return String.format( - Locale.ROOT, - "%02d", - (dateTime.getOffset().get(ChronoField.OFFSET_SECONDS) % 3600) / 60); + @Override protected int getBaseFormatPatternLength() { + return 3; } - @Override protected boolean isNumeric() { + @Override public boolean isNumeric() { return true; } } diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneOffsetCompiledPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneOffsetCompiledPattern.java new file mode 100644 index 000000000000..a8ea63c5a500 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/TimeZoneOffsetCompiledPattern.java @@ -0,0 +1,65 @@ +/* + * 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.postgresql.format.compiled; + +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.util.Locale; +import java.util.Set; + +/** + * The date/time format compiled component for the hours and minutes of the timezone offset. + * Can be just two digits plus sign if the UTC offset is a whole number of hours. Otherwise, + * it is a value similar to "+07:30". + */ +public class TimeZoneOffsetCompiledPattern extends CompiledPattern { + public TimeZoneOffsetCompiledPattern(Set modifiers) { + super(ChronoUnitEnum.TIMEZONE_MINUTES, modifiers); + } + + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + final int hours = dateTime.getOffset().get(ChronoField.HOUR_OF_DAY); + final int minutes = dateTime.getOffset().get(ChronoField.MINUTE_OF_HOUR); + + String formattedHours = + String.format(Locale.ROOT, "%s%02d", hours < 0 ? "-" : "+", hours); + if (minutes == 0) { + return formattedHours; + } else { + return String.format(Locale.ROOT, "%s:%02d", formattedHours, minutes); + } + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { + throw new ParseException("OF pattern is not supported in parsing datetime values", + inputPosition.getIndex()); + } + + @Override protected int getBaseFormatPatternLength() { + return 2; + } + + @Override public boolean isNumeric() { + return true; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/YearWithCommasFormatPattern.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/YearWithCommasCompiledPattern.java similarity index 55% rename from core/src/main/java/org/apache/calcite/util/format/postgresql/YearWithCommasFormatPattern.java rename to core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/YearWithCommasCompiledPattern.java index ff3711128cbb..3cbe442713ba 100644 --- a/core/src/main/java/org/apache/calcite/util/format/postgresql/YearWithCommasFormatPattern.java +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/YearWithCommasCompiledPattern.java @@ -14,29 +14,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.format.postgresql; +package org.apache.calcite.util.format.postgresql.format.compiled; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.apache.calcite.util.format.postgresql.ChronoUnitEnum; +import org.apache.calcite.util.format.postgresql.PatternModifier; import java.text.ParseException; import java.text.ParsePosition; import java.time.ZonedDateTime; import java.util.Locale; +import java.util.Set; import static java.lang.Integer.parseInt; /** - * Able to parse and generate string of years with commas to separate the thousands. An - * example year is "1,997". + * The date/time format compiled component for the year formatted with a comma after + * the thousands (such as "2,024"). */ -public class YearWithCommasFormatPattern extends StringFormatPattern { - public YearWithCommasFormatPattern() { - super(ChronoUnitEnum.YEARS, "Y,YYY"); +public class YearWithCommasCompiledPattern extends CompiledPattern { + public YearWithCommasCompiledPattern(Set modifiers) { + super(ChronoUnitEnum.YEARS, modifiers); } - @Override protected int parseValue(ParsePosition inputPosition, String input, Locale locale, - boolean haveFillMode, boolean enforceLength) throws ParseException { + @Override public String convertToString(ZonedDateTime dateTime, Locale locale) { + String formattedValue = String.format(Locale.ROOT, "%04d", dateTime.getYear()); + formattedValue = formattedValue.substring(0, formattedValue.length() - 3) + "," + + formattedValue.substring(formattedValue.length() - 3); + if (modifiers.contains(PatternModifier.TH_UPPER) + || modifiers.contains(PatternModifier.TH_LOWER)) { + String suffix; + switch (formattedValue.charAt(formattedValue.length() - 1)) { + case '1': + suffix = "st"; + break; + case '2': + suffix = "nd"; + break; + case 3: + suffix = "rd"; + break; + default: + suffix = "th"; + break; + } + + if (modifiers.contains(PatternModifier.TH_UPPER)) { + return formattedValue + suffix.toUpperCase(Locale.ROOT); + } + + return formattedValue + suffix; + } + + return formattedValue; + } + + @Override public int parseValue(ParsePosition inputPosition, String input, boolean enforceLength, + Locale locale) throws ParseException { final String inputTrimmed = input.substring(inputPosition.getIndex()); final int commaIndex = inputTrimmed.indexOf(','); @@ -66,43 +100,27 @@ public YearWithCommasFormatPattern() { } } - inputPosition.setIndex(inputPosition.getIndex() + endIndex); - final String remainingDigits = inputTrimmed.substring(commaIndex + 1, endIndex); - return parseInt(thousands) * 1000 + parseInt(remainingDigits); - } - - @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean haveFillMode, - @Nullable String suffix, Locale locale) { - final String stringValue = String.format(locale, "%04d", dateTime.getYear()); - String outputSuffix = ""; - if (suffix != null) { - switch (stringValue.charAt(stringValue.length() - 1)) { - case '1': - outputSuffix = "st"; - break; - case '2': - outputSuffix = "nd"; - break; - case '3': - outputSuffix = "rd"; - break; - default: - outputSuffix = "th"; - break; - } - - if ("TH".equals(suffix)) { - outputSuffix = outputSuffix.toUpperCase(locale); + if (modifiers.contains(PatternModifier.TH_UPPER) + || modifiers.contains(PatternModifier.TH_LOWER)) { + if (endIndex < inputTrimmed.length() - 1) { + endIndex += 2; + } else if (endIndex < inputTrimmed.length()) { + endIndex++; } } - return stringValue.substring(0, stringValue.length() - 3) + "," - + stringValue.substring(stringValue.length() - 3) + outputSuffix; + inputPosition.setIndex(inputPosition.getIndex() + endIndex); + + return parseInt(thousands) * 1000 + parseInt(remainingDigits); + } + + @Override protected int getBaseFormatPatternLength() { + return 5; } - @Override protected boolean isNumeric() { + @Override public boolean isNumeric() { return true; } } diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/package-info.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/package-info.java new file mode 100644 index 000000000000..c0b9a1518fc5 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/compiled/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Classes for that represent components of a parsed date/time format. + */ +package org.apache.calcite.util.format.postgresql.format.compiled; diff --git a/core/src/main/java/org/apache/calcite/util/format/postgresql/format/package-info.java b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/package-info.java new file mode 100644 index 000000000000..e6da6fdf895a --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/postgresql/format/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Classes used to build up a list of supported date/time format components. + */ +package org.apache.calcite.util.format.postgresql.format; diff --git a/core/src/test/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatterTest.java b/core/src/test/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatterTest.java index 77456250054f..5c4c0afd1e8d 100644 --- a/core/src/test/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatterTest.java +++ b/core/src/test/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatterTest.java @@ -50,15 +50,21 @@ public class PostgresqlDateTimeFormatterTest { private static final ZonedDateTime JAN_1_2024 = createDateTime(2024, 1, 1, 0, 0, 0, 0); private String toCharUs(String pattern, ZonedDateTime dateTime) { - return PostgresqlDateTimeFormatter.toChar(pattern, dateTime, Locale.US); + final CompiledDateTimeFormat dateTimeFormat = + PostgresqlDateTimeFormatter.compilePattern(pattern); + return dateTimeFormat.formatDateTime(dateTime, Locale.US); } private String toCharFrench(String pattern, ZonedDateTime dateTime) { - return PostgresqlDateTimeFormatter.toChar(pattern, dateTime, Locale.FRENCH); + final CompiledDateTimeFormat dateTimeFormat = + PostgresqlDateTimeFormatter.compilePattern(pattern); + return dateTimeFormat.formatDateTime(dateTime, Locale.FRENCH); } private ZonedDateTime toTimestamp(String input, String format) throws Exception { - return PostgresqlDateTimeFormatter.toTimestamp(input, format, TIME_ZONE, Locale.US); + final CompiledDateTimeFormat dateTimeFormat = + PostgresqlDateTimeFormatter.compilePattern(format); + return dateTimeFormat.parseDateTime(input, TIME_ZONE, Locale.US); } @ParameterizedTest @@ -838,9 +844,9 @@ void testEraLowerCaseWithDots(String pattern) { final ZonedDateTime date2 = createDateTime(2024, 3, 1, 23, 0, 0, 0); final ZonedDateTime date3 = createDateTime(2024, 11, 1, 23, 0, 0, 0); - assertEquals("JANVIER ", toCharFrench("TMMONTH", date1)); - assertEquals("MARS ", toCharFrench("TMMONTH", date2)); - assertEquals("NOVEMBRE ", toCharFrench("TMMONTH", date3)); + assertEquals("JANVIER", toCharFrench("TMMONTH", date1)); + assertEquals("MARS", toCharFrench("TMMONTH", date2)); + assertEquals("NOVEMBRE", toCharFrench("TMMONTH", date3)); } @Test void testMonthFullUpperCaseNoPadding() { @@ -950,9 +956,9 @@ void testEraLowerCaseWithDots(String pattern) { final ZonedDateTime date2 = createDateTime(2024, 3, 1, 23, 0, 0, 0); final ZonedDateTime date3 = createDateTime(2024, 10, 1, 23, 0, 0, 0); - assertEquals("LUNDI ", toCharFrench("TMDAY", date1)); - assertEquals("VENDREDI ", toCharFrench("TMDAY", date2)); - assertEquals("MARDI ", toCharFrench("TMDAY", date3)); + assertEquals("LUNDI", toCharFrench("TMDAY", date1)); + assertEquals("VENDREDI", toCharFrench("TMDAY", date2)); + assertEquals("MARDI", toCharFrench("TMDAY", date3)); } @Test void testDayFullCapitalized() { @@ -1669,10 +1675,11 @@ void testEraLowerCaseWithDots(String pattern) { @Test void testToTimestampWithTimezone() throws Exception { final ZoneId utcZone = ZoneId.of("UTC"); + final CompiledDateTimeFormat dateTimeFormat = + PostgresqlDateTimeFormatter.compilePattern("YYYY-MM-DD HH24:MI:SSTZH:TZM"); assertEquals( APR_17_2024.plusHours(7).withZoneSameLocal(utcZone), - PostgresqlDateTimeFormatter.toTimestamp("2024-04-17 00:00:00-07:00", - "YYYY-MM-DD HH24:MI:SSTZH:TZM", utcZone, Locale.US)); + dateTimeFormat.parseDateTime("2024-04-17 00:00:00-07:00", utcZone, Locale.US)); } protected static ZonedDateTime createDateTime(int year, int month, int dayOfMonth, int hour, diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index 4b7c5972c961..f9f330170591 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -4915,10 +4915,10 @@ void testBitGetFunc(SqlOperatorFixture f, String functionName) { "2022-06-03 12:15:48.678", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'Day')", - "Friday", + "Friday ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '0001-01-01 00:00:00.000', 'Day')", - "Monday", + "Monday ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'DY')", "FRI", @@ -5050,13 +5050,13 @@ void testBitGetFunc(SqlOperatorFixture f, String functionName) { "a.d.", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'MONTH')", - "JUNE", + "JUNE ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'Month')", - "June", + "June ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'month')", - "june", + "june ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'MON')", "JUN", @@ -5068,13 +5068,13 @@ void testBitGetFunc(SqlOperatorFixture f, String functionName) { "jun", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'DAY')", - "FRIDAY", + "FRIDAY ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'Day')", - "Friday", + "Friday ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'day')", - "friday", + "friday ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 12:15:48.678', 'DY')", "FRI", @@ -5134,7 +5134,7 @@ void testBitGetFunc(SqlOperatorFixture f, String functionName) { "22", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 13:15:48.678', 'Month')", - "June", + "June ", "VARCHAR NOT NULL"); f.checkString("to_char(timestamp '2022-06-03 13:15:48.678', 'Mon')", "Jun", @@ -5232,9 +5232,6 @@ void testBitGetFunc(SqlOperatorFixture f, String functionName) { f.checkFails("to_date('ABCD', 'YYYY-MM-DD')", "java.sql.SQLException: Invalid format: 'YYYY-MM-DD' for datetime string: 'ABCD'.", true); - f.checkFails("to_date('2022-06-03', 'Invalid')", - "java.sql.SQLException: Invalid format: 'Invalid' for datetime string: '2022-06-03'.", - true); f.checkNull("to_date(NULL, 'YYYY-MM-DD')"); f.checkNull("to_date('2022-06-03', NULL)"); f.checkNull("to_date(NULL, NULL)"); @@ -5303,10 +5300,6 @@ void testBitGetFunc(SqlOperatorFixture f, String functionName) { "java.sql.SQLException: Invalid format: 'YYYY-MM-DD HH24:MI:SS' for timestamp " + "string: 'ABCD'.", true); - f.checkFails("to_timestamp('2022-06-03 18:34:56', 'Invalid')", - "java.sql.SQLException: Invalid format: 'Invalid' for timestamp string: " - + "'2022-06-03 18:34:56'.", - true); f.checkNull("to_timestamp(NULL, 'YYYY-MM-DD HH24:MI:SS')"); f.checkNull("to_timestamp('2022-06-03 18:34:56', NULL)"); f.checkNull("to_timestamp(NULL, NULL)");