normanj-bitquill commented on code in PR #3833:
URL: https://github.com/apache/calcite/pull/3833#discussion_r1659389863


##########
core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java:
##########
@@ -4061,11 +4074,40 @@ public int toDate(String dateString, String fmtString) {
           new java.sql.Date(internalToDateTime(dateString, fmtString)));
     }
 
+    public int toDatePg(String dateString, String fmtString) {
+      try {
+        return (int) PostgresqlDateTimeFormatter.toTimestamp(dateString, 
fmtString,
+                LOCAL_ZONE)
+            .getLong(ChronoField.EPOCH_DAY);
+      } catch (Exception e) {
+        SQLException sqlEx =
+            new SQLException(
+                String.format(Locale.ROOT,
+                    "Invalid format: '%s' for datetime string: '%s'.", 
fmtString,
+                    dateString));
+        throw Util.toUnchecked(sqlEx);
+      }
+    }
+
     public long toTimestamp(String timestampString, String fmtString) {
       return toLong(
           new java.sql.Timestamp(internalToDateTime(timestampString, 
fmtString)));
     }
 
+    public long toTimestampPg(String timestampString, String fmtString) {
+      try {
+        return PostgresqlDateTimeFormatter.toTimestamp(timestampString, 
fmtString, LOCAL_ZONE)

Review Comment:
   PostgreSQL will parse the timestamp in the default timezone unless the 
format and input have timezone information.
   
   From my understanding, I need to eventually return a long value (millis 
since epoch in UTC) for a timestamp value. Is it possible to return a timestamp 
with timezone information?
   
https://github.com/apache/calcite/blob/main/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java#L4064
   
   Since I am returning a long value (millis since epoch in UTC), I am using 
UTC as the default timezone.



##########
core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java:
##########
@@ -1690,22 +1690,40 @@ private static RelDataType 
deriveTypeMapFromEntries(SqlOperatorBinding opBinding
 
   /** The "TO_DATE(string1, string2)" function; casts string1
    * to a DATE using the format specified in string2. */
-  @LibraryOperator(libraries = {POSTGRESQL, ORACLE})
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction TO_DATE =
       SqlBasicFunction.create("TO_DATE",
           ReturnTypes.DATE_NULLABLE,
           OperandTypes.STRING_STRING,
           SqlFunctionCategory.TIMEDATE);
 
+  /** The "TO_DATE(string1, string2)" function for PostgreSQL; casts string1
+   * to a DATE using the format specified in string2. */
+  @LibraryOperator(libraries = {POSTGRESQL})
+  public static final SqlFunction TO_DATE_PG =
+      new SqlBasicFunction("TO_DATE", SqlKind.OTHER_FUNCTION,
+          SqlSyntax.FUNCTION, true, ReturnTypes.DATE_NULLABLE, null,
+          OperandHandlers.DEFAULT, OperandTypes.STRING_STRING, 0,
+          SqlFunctionCategory.TIMEDATE, call -> SqlMonotonicity.NOT_MONOTONIC, 
false) { };
+
   /** The "TO_TIMESTAMP(string1, string2)" function; casts string1
    * to a TIMESTAMP using the format specified in string2. */
-  @LibraryOperator(libraries = {POSTGRESQL, ORACLE})
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction TO_TIMESTAMP =
       SqlBasicFunction.create("TO_TIMESTAMP",
           ReturnTypes.TIMESTAMP_NULLABLE,
           OperandTypes.STRING_STRING,
           SqlFunctionCategory.TIMEDATE);
 
+  /** The "TO_TIMESTAMP(string1, string2)" function for PostgreSQL; casts 
string1
+   * to a TIMESTAMP using the format specified in string2. */
+  @LibraryOperator(libraries = {POSTGRESQL})
+  public static final SqlFunction TO_TIMESTAMP_PG =
+      new SqlBasicFunction("TO_TIMESTAMP", SqlKind.OTHER_FUNCTION,
+          SqlSyntax.FUNCTION, true, ReturnTypes.TIMESTAMP_NULLABLE, null,

Review Comment:
   You are correct. At this point in the code, am I able to return a timestamp 
with timezone?
   
https://github.com/apache/calcite/blob/main/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java#L4064



##########
core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java:
##########
@@ -308,4 +481,362 @@ public static String toChar(String formatString, 
ZonedDateTime dateTime) {
 
     return sb.toString();
   }
+
+  public static ZonedDateTime toTimestamp(String input, String formatString, 
ZoneId zoneId)
+      throws Exception {
+    final ParsePosition inputParsePosition = new ParsePosition(0);
+    final ParsePosition formatParsePosition = new ParsePosition(0);
+    final Map<ChronoUnitEnum, Long> 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);
+          matchedPattern = formatPattern;
+          break;
+        }
+      }
+
+      if (matchedPattern == null) {
+        inputParsePosition.setIndex(inputParsePosition.getIndex() + 1);
+        formatParsePosition.setIndex(formatParsePosition.getIndex() + 1);
+      } else {
+        final Set<ChronoUnitEnum> units = dateTimeParts.keySet();
+        if (!matchedPattern.getChronoUnit().isCompatible(units)) {
+          throw new IllegalArgumentException();
+        }
+
+        dateTimeParts.put(matchedPattern.getChronoUnit(), parsedValue);
+      }
+    }
+
+    return constructDateTimeFromParts(dateTimeParts, zoneId);
+  }
+
+  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());
+      }
+    }
+
+    for (FormatPattern pattern : FORMAT_PATTERNS) {
+      for (String patternString : pattern.getPatterns()) {
+        if (formatTrimmed.startsWith(patternString)) {
+          return pattern;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  private static ZonedDateTime constructDateTimeFromParts(Map<ChronoUnitEnum, 
Long> dateParts,
+      ZoneId zoneId) {
+    LocalDateTime constructedDateTime = 
LocalDateTime.now(ZoneId.systemDefault())
+        .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 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));
+    }
+
+    return ZonedDateTime.of(constructedDateTime, zoneId);
+  }
+
+  private static LocalDateTime updateWithGregorianFields(LocalDateTime 
dateTime,
+      Map<ChronoUnitEnum, Long> 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<ChronoUnitEnum, Long> dateParts) {
+    return 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));
+  }
+
+  private static LocalDateTime updateWithIso8601Fields(LocalDateTime dateTime,
+      Map<ChronoUnitEnum, Long> 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<ChronoUnitEnum, Long> dateParts) {
+    return 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));
+  }
+
+  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();

Review Comment:
   Yeah, `YY`. You could do something like `CCYY` if you wanted to enter this 
year as `2124`.
   
   ```
   postgres=# select to_date('2124', 'CCYY');
     to_date
   ------------
    2024-01-01
   (1 row)
   ```



##########
core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java:
##########
@@ -308,4 +481,362 @@ public static String toChar(String formatString, 
ZonedDateTime dateTime) {
 
     return sb.toString();
   }
+
+  public static ZonedDateTime toTimestamp(String input, String formatString, 
ZoneId zoneId)
+      throws Exception {
+    final ParsePosition inputParsePosition = new ParsePosition(0);
+    final ParsePosition formatParsePosition = new ParsePosition(0);
+    final Map<ChronoUnitEnum, Long> 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);
+          matchedPattern = formatPattern;
+          break;
+        }
+      }
+
+      if (matchedPattern == null) {
+        inputParsePosition.setIndex(inputParsePosition.getIndex() + 1);
+        formatParsePosition.setIndex(formatParsePosition.getIndex() + 1);
+      } else {
+        final Set<ChronoUnitEnum> units = dateTimeParts.keySet();
+        if (!matchedPattern.getChronoUnit().isCompatible(units)) {
+          throw new IllegalArgumentException();
+        }
+
+        dateTimeParts.put(matchedPattern.getChronoUnit(), parsedValue);
+      }
+    }
+
+    return constructDateTimeFromParts(dateTimeParts, zoneId);
+  }
+
+  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());
+      }
+    }
+
+    for (FormatPattern pattern : FORMAT_PATTERNS) {
+      for (String patternString : pattern.getPatterns()) {
+        if (formatTrimmed.startsWith(patternString)) {
+          return pattern;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  private static ZonedDateTime constructDateTimeFromParts(Map<ChronoUnitEnum, 
Long> dateParts,
+      ZoneId zoneId) {
+    LocalDateTime constructedDateTime = 
LocalDateTime.now(ZoneId.systemDefault())
+        .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 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));
+    }
+
+    return ZonedDateTime.of(constructedDateTime, zoneId);
+  }
+
+  private static LocalDateTime updateWithGregorianFields(LocalDateTime 
dateTime,
+      Map<ChronoUnitEnum, Long> 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(

Review Comment:
   This is matching PostgreSQL behaviour.
   
   ```
   postgres=# select to_date('2024-03-2', 'YYYY-MM-W');
     to_date
   ------------
    2024-03-08
   (1 row)
   ```



##########
core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java:
##########
@@ -308,4 +481,362 @@ public static String toChar(String formatString, 
ZonedDateTime dateTime) {
 
     return sb.toString();
   }
+
+  public static ZonedDateTime toTimestamp(String input, String formatString, 
ZoneId zoneId)
+      throws Exception {
+    final ParsePosition inputParsePosition = new ParsePosition(0);
+    final ParsePosition formatParsePosition = new ParsePosition(0);
+    final Map<ChronoUnitEnum, Long> 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);
+          matchedPattern = formatPattern;
+          break;
+        }
+      }
+
+      if (matchedPattern == null) {
+        inputParsePosition.setIndex(inputParsePosition.getIndex() + 1);
+        formatParsePosition.setIndex(formatParsePosition.getIndex() + 1);
+      } else {
+        final Set<ChronoUnitEnum> units = dateTimeParts.keySet();
+        if (!matchedPattern.getChronoUnit().isCompatible(units)) {
+          throw new IllegalArgumentException();
+        }
+
+        dateTimeParts.put(matchedPattern.getChronoUnit(), parsedValue);
+      }
+    }
+
+    return constructDateTimeFromParts(dateTimeParts, zoneId);
+  }
+
+  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());
+      }
+    }
+
+    for (FormatPattern pattern : FORMAT_PATTERNS) {
+      for (String patternString : pattern.getPatterns()) {
+        if (formatTrimmed.startsWith(patternString)) {
+          return pattern;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  private static ZonedDateTime constructDateTimeFromParts(Map<ChronoUnitEnum, 
Long> dateParts,
+      ZoneId zoneId) {
+    LocalDateTime constructedDateTime = 
LocalDateTime.now(ZoneId.systemDefault())
+        .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 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));
+    }
+
+    return ZonedDateTime.of(constructedDateTime, zoneId);
+  }
+
+  private static LocalDateTime updateWithGregorianFields(LocalDateTime 
dateTime,
+      Map<ChronoUnitEnum, Long> 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(

Review Comment:
   This is also to match PostgreSQL behaviour.
   
   ```
   postgres=# select to_date('2024-15', 'YYYY-WW');
     to_date
   ------------
    2024-04-08
   (1 row)
   
   postgres=# select to_date('2024-99', 'YYYY-DDD');
     to_date
   ------------
    2024-04-08
   (1 row)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to