This is an automated email from the ASF dual-hosted git repository.
mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 374091b0a7 [CALCITE-6449] Enable PostgreSQL implementations of
to_date/to_timestamp
374091b0a7 is described below
commit 374091b0a7c24e61a5850e763c20587ff79e04ac
Author: Norman Jordan <[email protected]>
AuthorDate: Wed May 15 11:34:58 2024 -0700
[CALCITE-6449] Enable PostgreSQL implementations of to_date/to_timestamp
* Supports all date/time format patterns of PostreSQL 14
* Oracle will use the existing implementations
* Added to_date/to_timestamp tests to PostgreSQL iq file
* PostgreSQL to_timestamp function returns TIMESTAMP_TZ that is nullable
---
babel/src/test/resources/sql/postgresql.iq | 710 ++++++++++++++++
.../calcite/adapter/enumerable/RexImpTable.java | 4 +
.../org/apache/calcite/runtime/SqlFunctions.java | 43 +
.../calcite/sql/fun/SqlLibraryOperators.java | 22 +-
.../org/apache/calcite/sql/type/ReturnTypes.java | 7 +
.../org/apache/calcite/util/BuiltInMethod.java | 4 +
.../util/format/postgresql/ChronoUnitEnum.java | 242 ++++++
...ursFormatPattern.java => DateCalendarEnum.java} | 37 +-
.../format/postgresql/DateStringFormatPattern.java | 96 ++-
.../format/postgresql/EnumStringFormatPattern.java | 28 +-
.../util/format/postgresql/FormatPattern.java | 173 +++-
.../format/postgresql/NumberFormatPattern.java | 101 ++-
.../postgresql/PostgresqlDateTimeFormatter.java | 601 ++++++++++++-
.../postgresql/RomanNumeralMonthFormatPattern.java | 51 +-
.../format/postgresql/StringFormatPattern.java | 38 +-
.../format/postgresql/TimeZoneFormatPattern.java | 46 -
.../postgresql/TimeZoneHoursFormatPattern.java | 50 +-
.../postgresql/TimeZoneMinutesFormatPattern.java | 36 +-
.../postgresql/YearWithCommasFormatPattern.java | 106 +++
.../org/apache/calcite/test/SqlValidatorTest.java | 4 +-
.../PostgresqlDateTimeFormatterTest.java | 937 ++++++++++++++++++++-
.../org/apache/calcite/test/SqlOperatorTest.java | 72 +-
22 files changed, 3276 insertions(+), 132 deletions(-)
diff --git a/babel/src/test/resources/sql/postgresql.iq
b/babel/src/test/resources/sql/postgresql.iq
index 492c6a4d51..d2e0b53224 100644
--- a/babel/src/test/resources/sql/postgresql.iq
+++ b/babel/src/test/resources/sql/postgresql.iq
@@ -408,11 +408,721 @@ EXPR$0
null
!ok
+select to_timestamp('01', 'HH');
+EXPR$0
+0001-01-01 01:00:00
+!ok
+
+select to_timestamp('1', 'HH');
+EXPR$0
+0001-01-01 01:00:00
+!ok
+
+select to_timestamp('11', 'HH');
+EXPR$0
+0001-01-01 11:00:00
+!ok
+
+select to_timestamp('01', 'HH12');
+EXPR$0
+0001-01-01 01:00:00
+!ok
+
+select to_timestamp('1', 'HH12');
+EXPR$0
+0001-01-01 01:00:00
+!ok
+
+select to_timestamp('11', 'HH12');
+EXPR$0
+0001-01-01 11:00:00
+!ok
+
+select to_timestamp('01', 'HH24');
+EXPR$0
+0001-01-01 01:00:00
+!ok
+
+select to_timestamp('1', 'HH24');
+EXPR$0
+0001-01-01 01:00:00
+!ok
+
+select to_timestamp('18', 'HH24');
+EXPR$0
+0001-01-01 18:00:00
+!ok
+
+select to_timestamp('01', 'MI');
+EXPR$0
+0001-01-01 00:01:00
+!ok
+
+select to_timestamp('1', 'MI');
+EXPR$0
+0001-01-01 00:01:00
+!ok
+
+select to_timestamp('57', 'MI');
+EXPR$0
+0001-01-01 00:57:00
+!ok
+
+select to_timestamp('01', 'SS');
+EXPR$0
+0001-01-01 00:00:01
+!ok
+
+select to_timestamp('1', 'SS');
+EXPR$0
+0001-01-01 00:00:01
+!ok
+
+select to_timestamp('57', 'SS');
+EXPR$0
+0001-01-01 00:00:57
+!ok
+
+select to_timestamp('03AM', 'HH12AM');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03AM', 'HH12PM');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03PM', 'HH12AM');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03PM', 'HH12PM');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03A.M.', 'HH12A.M.');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03A.M.', 'HH12P.M.');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03P.M.', 'HH12A.M.');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03P.M.', 'HH12P.M.');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03am', 'HH12am');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03am', 'HH12pm');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03pm', 'HH12am');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03pm', 'HH12pm');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03a.m.', 'HH12a.m.');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03a.m.', 'HH12p.m.');
+EXPR$0
+0001-01-01 03:00:00
+!ok
+
+select to_timestamp('03p.m.', 'HH12a.m.');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('03p.m.', 'HH12p.m.');
+EXPR$0
+0001-01-01 15:00:00
+!ok
+
+select to_timestamp('0,001', 'Y,YYY');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('2,024', 'Y,YYY');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('0001', 'YYYY');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'YYYY');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('2024', 'YYYY');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('001', 'YYY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'YYY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('987', 'YYY');
+EXPR$0
+1987-01-01 00:00:00
+!ok
+
+select to_timestamp('01', 'YY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'YY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('24', 'YY');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'Y');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('4', 'YY');
+EXPR$0
+2004-01-01 00:00:00
+!ok
+
+select to_timestamp('0001', 'IYYY');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'IYYY');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('2024', 'IYYY');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('001', 'IYY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'IYY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('987', 'IYY');
+EXPR$0
+1987-01-01 00:00:00
+!ok
+
+select to_timestamp('01', 'IY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'IY');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('24', 'IY');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'I');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('4', 'I');
+EXPR$0
+2004-01-01 00:00:00
+!ok
+
+select to_timestamp('JANUARY', 'MONTH');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('MARCH', 'MONTH');
+EXPR$0
+0001-03-01 00:00:00
+!ok
+
+select to_timestamp('NOVEMBER', 'MONTH');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('January', 'Month');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('March', 'Month');
+EXPR$0
+0001-03-01 00:00:00
+!ok
+
+select to_timestamp('November', 'Month');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('january', 'month');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('march', 'month');
+EXPR$0
+0001-03-01 00:00:00
+!ok
+
+select to_timestamp('november', 'month');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('JAN', 'MON');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('MAR', 'MON');
+EXPR$0
+0001-03-01 00:00:00
+!ok
+
+select to_timestamp('NOV', 'MON');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('Jan', 'Mon');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('Mar', 'Mon');
+EXPR$0
+0001-03-01 00:00:00
+!ok
+
+select to_timestamp('Nov', 'Mon');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('jan', 'mon');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('mar', 'mon');
+EXPR$0
+0001-03-01 00:00:00
+!ok
+
+select to_timestamp('nov', 'mon');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('01', 'MM');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'MM');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('11', 'MM');
+EXPR$0
+0001-11-01 00:00:00
+!ok
+
+select to_timestamp('1982 23 MONDAY', 'IYYY IW DAY');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 THURSDAY', 'IYYY IW DAY');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 FRIDAY', 'IYYY IW DAY');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('1982 23 Monday', 'IYYY IW Day');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 Thursday', 'IYYY IW Day');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 Friday', 'IYYY IW Day');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('1982 23 monday', 'IYYY IW day');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 thursday', 'IYYY IW day');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 friday', 'IYYY IW day');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('1982 23 MON', 'IYYY IW DY');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 THU', 'IYYY IW DY');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 FRI', 'IYYY IW DY');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('1982 23 Mon', 'IYYY IW Dy');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 Thu', 'IYYY IW Dy');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 Fri', 'IYYY IW Dy');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('1982 23 mon', 'IYYY IW dy');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 thu', 'IYYY IW dy');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 fri', 'IYYY IW dy');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('2024 001', 'YYYY DDD');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('2024 1', 'YYYY DDD');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('2024 137', 'YYYY DDD');
+EXPR$0
+2024-05-16 00:00:00
+!ok
+
+select to_timestamp('01', 'DD');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'DD');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('23', 'DD');
+EXPR$0
+0001-01-23 00:00:00
+!ok
+
+select to_timestamp('2020 001', 'IYYY IDDD');
+EXPR$0
+2019-12-30 00:00:00
+!ok
+
+select to_timestamp('2020 1', 'IYYY IDDD');
+EXPR$0
+2019-12-30 00:00:00
+!ok
+
+select to_timestamp('2020 137', 'IYYY IDDD');
+EXPR$0
+2020-05-14 00:00:00
+!ok
+
+select to_timestamp('1982 23 1', 'IYYY IW ID');
+EXPR$0
+1982-06-07 00:00:00
+!ok
+
+select to_timestamp('1982 23 4', 'IYYY IW ID');
+EXPR$0
+1982-06-10 00:00:00
+!ok
+
+select to_timestamp('1982 23 5', 'IYYY IW ID');
+EXPR$0
+1982-06-11 00:00:00
+!ok
+
+select to_timestamp('2024 1 1', 'YYYY MM W');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('2024 4 2', 'YYYY MM W');
+EXPR$0
+2024-04-08 00:00:00
+!ok
+
+select to_timestamp('2024 11 4', 'YYYY MM W');
+EXPR$0
+2024-11-22 00:00:00
+!ok
+
+select to_timestamp('2024 01', 'YYYY WW');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('2024 1', 'YYYY WW');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('2024 51', 'YYYY WW');
+EXPR$0
+2024-12-16 00:00:00
+!ok
+
+select to_timestamp('2020 01', 'IYYY IW');
+EXPR$0
+2019-12-30 00:00:00
+!ok
+
+select to_timestamp('2020 1', 'IYYY IW');
+EXPR$0
+2019-12-30 00:00:00
+!ok
+
+select to_timestamp('2020 51', 'IYYY IW');
+EXPR$0
+2020-12-14 00:00:00
+!ok
+
+select to_timestamp('21', 'CC');
+EXPR$0
+2001-01-01 00:00:00
+!ok
+
+select to_timestamp('16', 'CC');
+EXPR$0
+1501-01-01 00:00:00
+!ok
+
+select to_timestamp('1', 'CC');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('2460311', 'J');
+EXPR$0
+2024-01-01 00:00:00
+!ok
+
+select to_timestamp('2445897', 'J');
+EXPR$0
+1984-07-15 00:00:00
+!ok
+
+select to_timestamp('1806606', 'J');
+EXPR$0
+0234-03-21 00:00:00
+!ok
+
+select to_timestamp('I', 'RM');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('IV', 'RM');
+EXPR$0
+0001-04-01 00:00:00
+!ok
+
+select to_timestamp('IX', 'RM');
+EXPR$0
+0001-09-01 00:00:00
+!ok
+
+select to_timestamp('i', 'rm');
+EXPR$0
+0001-01-01 00:00:00
+!ok
+
+select to_timestamp('iv', 'rm');
+EXPR$0
+0001-04-01 00:00:00
+!ok
+
+select to_timestamp('ix', 'rm');
+EXPR$0
+0001-09-01 00:00:00
+!ok
+
select to_date('2022-06-03', 'YYYY-MM-DD');
EXPR$0
2022-06-03
!ok
+select to_date('2,024-04-17', 'Y,YYY-MM-DD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('24-04-17', 'YYY-MM-DD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('24-04-17', 'YY-MM-DD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('2124-04-17', 'CCYY-MM-DD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('20240417', 'YYYYMMDD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('2,0240417', 'Y,YYYMMDD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('2024-16-3', 'IYYY-IW-ID');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('2024-16 Wednesday', 'IYYY-IW Day');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('2024-108', 'IYYY-IDDD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('April 17, 2024', 'Month DD, YYYY');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('IV 17, 2024', 'RM DD, YYYY');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('APR 17, 2024', 'MON DD, YYYY');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('2024-16', 'YYYY-WW');
+EXPR$0
+2024-04-15
+!ok
+
+select to_date('2024-108', 'YYYY-DDD');
+EXPR$0
+2024-04-17
+!ok
+
+select to_date('0000-01-01', 'YYYY-MM-DD');
+EXPR$0
+0001-01-01
+!ok
+
select to_timestamp('18:46:32 2022-06-03', 'HH24:MI:SS YYYY-MM-DD');
EXPR$0
2022-06-03 18:46:32
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index ce70c6cebe..3e6088d626 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -288,8 +288,10 @@ import static
org.apache.calcite.sql.fun.SqlLibraryOperators.TO_CHAR;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_CHAR_PG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_CODE_POINTS;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_DATE;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_DATE_PG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_HEX;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_TIMESTAMP;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_TIMESTAMP_PG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRANSLATE3;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRUNC_BIG_QUERY;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRY_CAST;
@@ -801,7 +803,9 @@ public class RexImpTable {
defineReflective(TO_CHAR, BuiltInMethod.TO_CHAR.method);
defineReflective(TO_CHAR_PG, BuiltInMethod.TO_CHAR_PG.method);
defineReflective(TO_DATE, BuiltInMethod.TO_DATE.method);
+ defineReflective(TO_DATE_PG, BuiltInMethod.TO_DATE_PG.method);
defineReflective(TO_TIMESTAMP, BuiltInMethod.TO_TIMESTAMP.method);
+ defineReflective(TO_TIMESTAMP_PG, BuiltInMethod.TO_TIMESTAMP_PG.method);
final FormatDatetimeImplementor datetimeFormatImpl =
new FormatDatetimeImplementor();
map.put(FORMAT_DATE, datetimeFormatImpl);
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 30fb4d54ee..6625e14a02 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -4033,6 +4033,20 @@ public class SqlFunctions {
* {@code FORMAT_DATETIME}, {@code FORMAT_TIME}, {@code TO_CHAR} functions.
*/
@Deterministic
public static class DateFormatFunction {
+ // Timezone to use for PostgreSQL parsing of timestamps
+ private static final ZoneId LOCAL_ZONE;
+ static {
+ ZoneId zoneId;
+ try {
+ // Currently the parsed timestamps are expected to be the number of
+ // milliseconds since the epoch in UTC, with no timezone information
+ zoneId = ZoneId.of("UTC");
+ } catch (Exception e) {
+ zoneId = ZoneId.systemDefault();
+ }
+ LOCAL_ZONE = zoneId;
+ }
+
/** Work space for various functions. Clear it before you use it. */
final StringBuilder sb = new StringBuilder();
@@ -4090,11 +4104,40 @@ public class SqlFunctions {
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)
+ .toInstant().toEpochMilli();
+ } catch (Exception e) {
+ SQLException sqlEx =
+ new SQLException(
+ String.format(Locale.ROOT,
+ "Invalid format: '%s' for timestamp string: '%s'.",
fmtString,
+ timestampString));
+ throw Util.toUnchecked(sqlEx);
+ }
+ }
+
private long internalToDateTime(String dateString, String fmtString) {
final ParsePosition pos = new ParsePosition(0);
diff --git
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index 642b74690a..5c876e0614 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -1726,22 +1726,40 @@ public abstract class SqlLibraryOperators {
/** The "TO_DATE(string1, string2)" function; casts string1
* to a DATE using the format specified in string2. */
- @LibraryOperator(libraries = {ORACLE, POSTGRESQL})
+ @LibraryOperator(libraries = {ORACLE, REDSHIFT})
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}, exceptLibraries = {REDSHIFT})
+ 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 = {ORACLE, POSTGRESQL})
+ @LibraryOperator(libraries = {ORACLE, REDSHIFT})
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}, exceptLibraries = {REDSHIFT})
+ public static final SqlFunction TO_TIMESTAMP_PG =
+ new SqlBasicFunction("TO_TIMESTAMP", SqlKind.OTHER_FUNCTION,
+ SqlSyntax.FUNCTION, true, ReturnTypes.TIMESTAMP_TZ_NULLABLE, null,
+ OperandHandlers.DEFAULT, OperandTypes.STRING_STRING, 0,
+ SqlFunctionCategory.TIMEDATE, call -> SqlMonotonicity.NOT_MONOTONIC,
false) { };
+
/**
* The "PARSE_TIME(string, string)" function (BigQuery);
* converts a string representation of time to a TIME value.
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 5153a18fb4..40b5353514 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -391,6 +391,13 @@ public abstract class ReturnTypes {
public static final SqlReturnTypeInference TIMESTAMP_LTZ_NULLABLE =
TIMESTAMP_LTZ.andThen(SqlTypeTransforms.TO_NULLABLE);
+ /**
+ * Type-inference strategy whereby the result type of a call is nullable
+ * TIMESTAMP WITH TIME ZONE.
+ */
+ public static final SqlReturnTypeInference TIMESTAMP_TZ_NULLABLE =
+ TIMESTAMP_TZ.andThen(SqlTypeTransforms.TO_NULLABLE);
+
/**
* Type-inference strategy whereby the result type of a call is Double.
*/
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index ff4a57b909..7b144167da 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -662,8 +662,12 @@ public enum BuiltInMethod {
String.class),
TO_DATE(SqlFunctions.DateFormatFunction.class, "toDate", String.class,
String.class),
+ TO_DATE_PG(SqlFunctions.DateFormatFunction.class, "toDatePg", String.class,
+ String.class),
TO_TIMESTAMP(SqlFunctions.DateFormatFunction.class, "toTimestamp",
String.class,
String.class),
+ TO_TIMESTAMP_PG(SqlFunctions.DateFormatFunction.class, "toTimestampPg",
String.class,
+ String.class),
FORMAT_DATE(SqlFunctions.DateFormatFunction.class, "formatDate",
String.class, int.class),
FORMAT_TIME(SqlFunctions.DateFormatFunction.class, "formatTime",
diff --git
a/core/src/main/java/org/apache/calcite/util/format/postgresql/ChronoUnitEnum.java
b/core/src/main/java/org/apache/calcite/util/format/postgresql/ChronoUnitEnum.java
new file mode 100644
index 0000000000..b1e52cfebf
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/util/format/postgresql/ChronoUnitEnum.java
@@ -0,0 +1,242 @@
+/*
+ * 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 com.google.common.collect.ImmutableSet;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Set;
+
+import static
org.apache.calcite.util.format.postgresql.DateCalendarEnum.GREGORIAN;
+import static
org.apache.calcite.util.format.postgresql.DateCalendarEnum.ISO_8601;
+import static
org.apache.calcite.util.format.postgresql.DateCalendarEnum.JULIAN;
+import static org.apache.calcite.util.format.postgresql.DateCalendarEnum.NONE;
+
+/**
+ * A component of a datetime. May belong to a type of calendar. Also contains
+ * a list of parent values. For example months are in a year. A datetime can be
+ * reconstructed from one or more <code>ChronUnitEnum</code> items along with
+ * their associated values.
+ *
+ * <p>Some <code>ChronoUnitEnum</code> items conflict with others.
+ */
+public enum ChronoUnitEnum {
+ ERAS(ChronoUnit.ERAS, NONE),
+ CENTURIES(
+ ChronoUnit.CENTURIES,
+ ImmutableSet.of(ISO_8601, GREGORIAN),
+ ERAS),
+ YEARS_ISO_8601(
+ ChronoUnit.YEARS,
+ ISO_8601,
+ CENTURIES),
+ YEARS_IN_MILLENIA_ISO_8601(
+ ChronoUnit.YEARS,
+ ISO_8601,
+ CENTURIES),
+ YEARS_IN_CENTURY_ISO_8601(
+ ChronoUnit.YEARS,
+ ISO_8601,
+ CENTURIES),
+ DAYS_IN_YEAR_ISO_8601(
+ ChronoUnit.DAYS,
+ ISO_8601,
+ YEARS_ISO_8601),
+ WEEKS_IN_YEAR_ISO_8601(
+ ChronoUnit.WEEKS,
+ ISO_8601,
+ YEARS_ISO_8601),
+ DAYS_JULIAN(
+ ChronoUnit.DAYS,
+ JULIAN),
+ YEARS(
+ ChronoUnit.YEARS,
+ GREGORIAN,
+ CENTURIES),
+ YEARS_IN_MILLENIA(
+ ChronoUnit.YEARS,
+ GREGORIAN,
+ CENTURIES),
+ YEARS_IN_CENTURY(
+ ChronoUnit.YEARS,
+ GREGORIAN,
+ CENTURIES),
+ MONTHS_IN_YEAR(
+ ChronoUnit.MONTHS,
+ GREGORIAN,
+ YEARS, YEARS_IN_CENTURY),
+ DAYS_IN_YEAR(
+ ChronoUnit.DAYS,
+ GREGORIAN,
+ YEARS, YEARS_IN_CENTURY),
+ DAYS_IN_MONTH(
+ ChronoUnit.DAYS,
+ GREGORIAN,
+ MONTHS_IN_YEAR),
+ WEEKS_IN_YEAR(
+ ChronoUnit.WEEKS,
+ GREGORIAN,
+ YEARS, YEARS_IN_CENTURY),
+ WEEKS_IN_MONTH(
+ ChronoUnit.WEEKS,
+ GREGORIAN,
+ MONTHS_IN_YEAR),
+ DAYS_IN_WEEK(
+ ChronoUnit.DAYS,
+ NONE,
+ YEARS_ISO_8601, WEEKS_IN_MONTH, WEEKS_IN_YEAR),
+ HOURS_IN_DAY(
+ ChronoUnit.HOURS,
+ NONE,
+ DAYS_IN_YEAR, DAYS_IN_MONTH, DAYS_IN_WEEK),
+ HALF_DAYS(
+ ChronoUnit.HALF_DAYS,
+ NONE,
+ DAYS_IN_YEAR, DAYS_IN_MONTH, DAYS_IN_WEEK),
+ HOURS_IN_HALF_DAY(
+ ChronoUnit.HOURS,
+ NONE,
+ HALF_DAYS),
+ MINUTES_IN_HOUR(
+ ChronoUnit.MINUTES,
+ NONE,
+ HOURS_IN_DAY, HOURS_IN_HALF_DAY),
+ SECONDS_IN_DAY(
+ ChronoUnit.SECONDS,
+ NONE,
+ DAYS_IN_YEAR, DAYS_IN_MONTH, DAYS_IN_WEEK),
+ SECONDS_IN_MINUTE(
+ ChronoUnit.SECONDS,
+ NONE,
+ MINUTES_IN_HOUR),
+ MILLIS(
+ ChronoUnit.MILLIS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ MICROS(
+ ChronoUnit.MICROS,
+ NONE,
+ MILLIS),
+ TENTHS_OF_SECOND(
+ ChronoUnit.MILLIS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ HUNDREDTHS_OF_SECOND(
+ ChronoUnit.MILLIS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ THOUSANDTHS_OF_SECOND(
+ ChronoUnit.MILLIS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ TENTHS_OF_MS(
+ ChronoUnit.MICROS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ HUNDREDTHS_OF_MS(
+ ChronoUnit.MICROS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ THOUSANDTHS_OF_MS(
+ ChronoUnit.MICROS,
+ NONE,
+ SECONDS_IN_DAY, SECONDS_IN_MINUTE),
+ TIMEZONE_HOURS(
+ ChronoUnit.HOURS,
+ NONE),
+ TIMEZONE_MINUTES(
+ ChronoUnit.MINUTES,
+ NONE,
+ TIMEZONE_HOURS);
+
+ private final ChronoUnit chronoUnit;
+ private final ImmutableSet<ChronoUnitEnum> parentUnits;
+ private final ImmutableSet<DateCalendarEnum> calendars;
+
+ ChronoUnitEnum(ChronoUnit chronoUnit, DateCalendarEnum calendar,
+ ChronoUnitEnum... parentUnits) {
+ this.chronoUnit = chronoUnit;
+ this.parentUnits = ImmutableSet.copyOf(parentUnits);
+ this.calendars = ImmutableSet.of(calendar);
+ }
+
+ ChronoUnitEnum(ChronoUnit chronoUnit, Set<DateCalendarEnum> calendars,
+ ChronoUnitEnum... parentUnits) {
+ this.chronoUnit = chronoUnit;
+ this.parentUnits = ImmutableSet.copyOf(parentUnits);
+ this.calendars =
ImmutableSet.<DateCalendarEnum>builder().addAll(calendars).build();
+ }
+
+ /**
+ * Get the ChronoUnit value that corresponds to this value.
+ *
+ * @return a ChronoUnit value
+ */
+ public ChronoUnit getChronoUnit() {
+ return chronoUnit;
+ }
+
+ /**
+ * Get the set of calendars that this value can be in.
+ *
+ * @return set of calendars that this value can be in
+ */
+ public Set<DateCalendarEnum> getCalendars() {
+ return calendars;
+ }
+
+ /**
+ * Checks if the current item can be added to <code>units</code> without
causing
+ * any conflicts.
+ *
+ * @param units a <code>Set</code> of items to test against
+ * @return <code>true</code> if this item does not conflict with
<code>units</code>
+ */
+ public boolean isCompatible(Set<ChronoUnitEnum> units) {
+ if (!calendars.isEmpty()) {
+ for (ChronoUnitEnum unit : units) {
+ boolean haveCompatibleCalendar = false;
+
+ for (DateCalendarEnum unitCalendar : unit.getCalendars()) {
+ for (DateCalendarEnum calendar : calendars) {
+ if (unitCalendar == NONE || calendar == NONE
+ || unitCalendar.isCalendarCompatible(calendar)) {
+ haveCompatibleCalendar = true;
+ break;
+ }
+ }
+
+ if (haveCompatibleCalendar) {
+ break;
+ }
+ }
+
+ if (!haveCompatibleCalendar) {
+ return false;
+ }
+ }
+ }
+
+ for (ChronoUnitEnum unit : units) {
+ if (parentUnits.equals(unit.parentUnits)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
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/DateCalendarEnum.java
similarity index 54%
copy from
core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneHoursFormatPattern.java
copy to
core/src/main/java/org/apache/calcite/util/format/postgresql/DateCalendarEnum.java
index 5c07559730..9b3d46dba7 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/DateCalendarEnum.java
@@ -16,27 +16,26 @@
*/
package org.apache.calcite.util.format.postgresql;
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-import java.time.ZonedDateTime;
-import java.time.temporal.ChronoField;
-import java.util.Locale;
-
/**
- * 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.
+ * Calendar types that can be used in PostgreSQL datetime formats.
*/
-public class TimeZoneHoursFormatPattern extends StringFormatPattern {
- public TimeZoneHoursFormatPattern() {
- super("TZH");
- }
+public enum DateCalendarEnum {
+ GREGORIAN,
+ JULIAN,
+ ISO_8601,
+ NONE;
+
+ /**
+ * Tests if the provided calendar is compatible with this calendar.
+ *
+ * @param otherCalendar calendar to test with this
+ * @return whether the provided calendar is compatible with this
+ */
+ public boolean isCalendarCompatible(DateCalendarEnum otherCalendar) {
+ if (this == NONE || otherCalendar == NONE) {
+ return true;
+ }
- @Override String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
- @Nullable String suffix, Locale locale) {
- return String.format(
- Locale.ROOT,
- "%+02d",
- dateTime.getOffset().get(ChronoField.OFFSET_SECONDS) / 3600);
+ return this == otherCalendar;
}
}
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
index 896fdcaca9..7055cc00d0 100644
---
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
@@ -18,6 +18,8 @@ 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;
@@ -40,19 +42,65 @@ public class DateStringFormatPattern<T> extends
StringFormatPattern {
* that has a string representation
*/
private interface DateStringConverter<T> {
+ /**
+ * 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<DayOfWeek> {
+ @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);
@@ -65,16 +113,28 @@ public class DateStringFormatPattern<T> extends
StringFormatPattern {
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<Month> {
+ @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);
@@ -88,19 +148,24 @@ public class DateStringFormatPattern<T> extends
StringFormatPattern {
return formattedValue;
}
}
+
+ @Override public int getValue(Month value) {
+ return value.getValue();
+ }
}
private static final DateStringConverter<DayOfWeek> DAY_OF_WEEK = new
DayOfWeekConverter();
private static final DateStringConverter<Month> MONTH = new MonthConverter();
- private final DateStringConverter<T> dateStringConverter;
+ private final DateStringConverter<T> dateStringEnum;
private final CapitalizationEnum capitalization;
private final TextStyle textStyle;
- private DateStringFormatPattern(DateStringConverter<T> dateStringConverter,
+ private DateStringFormatPattern(
+ ChronoUnitEnum chronoUnit, DateStringConverter<T> dateStringEnum,
TextStyle textStyle, CapitalizationEnum capitalization, String...
patterns) {
- super(patterns);
- this.dateStringConverter = dateStringConverter;
+ super(chronoUnit, patterns);
+ this.dateStringEnum = dateStringEnum;
this.capitalization = capitalization;
this.textStyle = textStyle;
}
@@ -108,6 +173,7 @@ public class DateStringFormatPattern<T> extends
StringFormatPattern {
public static DateStringFormatPattern<DayOfWeek> forDayOfWeek(TextStyle
textStyle,
CapitalizationEnum capitalization, String... patterns) {
return new DateStringFormatPattern<>(
+ DAY_OF_WEEK.getChronoUnit(),
DAY_OF_WEEK,
textStyle,
capitalization,
@@ -117,17 +183,35 @@ public class DateStringFormatPattern<T> extends
StringFormatPattern {
public static DateStringFormatPattern<Month> 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(
- dateStringConverter.getDisplayName(
- dateStringConverter.getValueFromDateTime(dateTime),
+ 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
index 29a643b784..26f91d2ac3 100644
---
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
@@ -18,6 +18,8 @@ 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;
@@ -31,12 +33,34 @@ public class EnumStringFormatPattern extends
StringFormatPattern {
private final ChronoField chronoField;
private final String[] enumValues;
- public EnumStringFormatPattern(ChronoField chronoField, String... patterns) {
- super(patterns);
+ /**
+ * 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);
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
index dbeb7ac6fe..f0194dd5b0 100644
---
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
@@ -18,13 +18,36 @@ 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 interface FormatPattern {
+public abstract class FormatPattern {
+ private 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
<code>parsePosition</code>
* in the <code>formatString</code>. If it matches, then the
<code>dateTime</code> is
@@ -36,6 +59,152 @@ public interface FormatPattern {
* @param dateTime datetime to convert
* @return the string representation of the datetime based on the format
pattern
*/
- @Nullable String convert(ParsePosition parsePosition, String formatString,
+ public abstract @Nullable String convert(ParsePosition parsePosition, String
formatString,
ZonedDateTime dateTime);
+
+ /**
+ * 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) 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 locale = haveTranslateMode ? Locale.getDefault() :
Locale.US;
+ long parsedValue = parseValue(inputPosition, input, locale,
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
index 57b4e8a428..776ed52098 100644
---
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
@@ -18,6 +18,7 @@ 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;
@@ -27,13 +28,61 @@ import java.util.function.Function;
* A format element that will produce a number. Numbers can have leading zeroes
* removed and can have ordinal suffixes.
*/
-public class NumberFormatPattern implements FormatPattern {
- private final String[] patterns;
+public class NumberFormatPattern extends FormatPattern {
+ private final ChronoUnitEnum chronoUnit;
+ private final long minValue;
+ private final long maxValue;
+ private final int preferredLength;
private final Function<ZonedDateTime, String> converter;
+ private final @Nullable Function<Integer, Integer> 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<ZonedDateTime, String> converter,
String... patterns) {
+ super(patterns);
+ this.chronoUnit = chronoUnit;
+ this.converter = converter;
+ this.valueAdjuster = null;
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ this.preferredLength = preferredLength;
+ }
- protected NumberFormatPattern(Function<ZonedDateTime, String> converter,
String... patterns) {
+ /**
+ * 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<ZonedDateTime, String>
converter,
+ Function<Integer, Integer> valueAdjuster, String... patterns) {
+ super(patterns);
+ this.chronoUnit = chronoUnit;
this.converter = converter;
- this.patterns = patterns;
+ this.valueAdjuster = valueAdjuster;
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ this.preferredLength = preferredLength;
}
@Override public @Nullable String convert(ParsePosition parsePosition,
String formatString,
@@ -55,7 +104,7 @@ public class NumberFormatPattern implements FormatPattern {
}
String patternToUse = null;
- for (String pattern : patterns) {
+ for (String pattern : getPatterns()) {
if (formatStringTrimmed.startsWith(pattern)) {
patternToUse = pattern;
break;
@@ -121,6 +170,12 @@ public class NumberFormatPattern implements FormatPattern {
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;
@@ -143,4 +198,40 @@ public class NumberFormatPattern implements FormatPattern {
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 = Integer.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/PostgresqlDateTimeFormatter.java
b/core/src/main/java/org/apache/calcite/util/format/postgresql/PostgresqlDateTimeFormatter.java
index b5d1851fb9..978bf578ed 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
@@ -18,14 +18,22 @@ 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.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.
@@ -38,76 +46,135 @@ public class PostgresqlDateTimeFormatter {
@SuppressWarnings("TemporalAccessorGetChronoField")
private static final FormatPattern[] FORMAT_PATTERNS = new FormatPattern[] {
new NumberFormatPattern(
- dt -> {
- final int hour = dt.get(ChronoField.HOUR_OF_AMPM);
- return String.format(Locale.ROOT, "%02d", hour == 0 ? 12 : hour);
- },
- "HH12"),
- 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);
},
- "HH"),
+ "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(ChronoField.AMPM_OF_DAY, "AM", "PM"),
- new EnumStringFormatPattern(ChronoField.AMPM_OF_DAY, "am", "pm"),
- new EnumStringFormatPattern(ChronoField.AMPM_OF_DAY, "A.M.", "P.M."),
- new EnumStringFormatPattern(ChronoField.AMPM_OF_DAY, "a.m.", "p.m."),
- new NumberFormatPattern(dt -> {
- final String formattedYear = String.format(Locale.ROOT, "%0,4d",
dt.getYear());
- if (formattedYear.length() == 4 && formattedYear.charAt(0) == '0') {
- return "0," + formattedYear.substring(1);
- } else {
- return formattedYear;
- }
- }, "Y,YYY") {
- @Override protected String trimLeadingZeros(String value) {
- return value;
- }
- },
- new NumberFormatPattern(
+ 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));
@@ -115,6 +182,10 @@ public class PostgresqlDateTimeFormatter {
},
"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));
@@ -122,6 +193,10 @@ public class PostgresqlDateTimeFormatter {
},
"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) {
@@ -132,6 +207,10 @@ public class PostgresqlDateTimeFormatter {
},
"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) {
@@ -142,6 +221,10 @@ public class PostgresqlDateTimeFormatter {
},
"YY"),
new NumberFormatPattern(
+ ChronoUnitEnum.YEARS_IN_CENTURY,
+ 0,
+ Integer.MAX_VALUE,
+ 1,
dt -> {
final String formattedYear = Integer.toString(dt.getYear());
if (formattedYear.length() > 1) {
@@ -152,9 +235,17 @@ public class PostgresqlDateTimeFormatter {
},
"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();
@@ -175,18 +266,26 @@ public class PostgresqlDateTimeFormatter {
},
"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(ChronoField.ERA, "BC", "AD"),
- new EnumStringFormatPattern(ChronoField.ERA, "bc", "ad"),
- new EnumStringFormatPattern(ChronoField.ERA, "B.C.", "A.D."),
- new EnumStringFormatPattern(ChronoField.ERA, "b.c.", "a.d."),
+ 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"),
@@ -194,6 +293,10 @@ public class PostgresqlDateTimeFormatter {
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"),
@@ -203,12 +306,24 @@ public class PostgresqlDateTimeFormatter {
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) {
@@ -216,14 +331,27 @@ public class PostgresqlDateTimeFormatter {
}
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));
@@ -233,6 +361,10 @@ public class PostgresqlDateTimeFormatter {
},
"CC"),
new NumberFormatPattern(
+ ChronoUnitEnum.DAYS_JULIAN,
+ 0,
+ Integer.MAX_VALUE,
+ 1,
dt -> {
final long julianDays = dt.getLong(JulianFields.JULIAN_DAY);
if (dt.getYear() < 0) {
@@ -243,16 +375,54 @@ public class PostgresqlDateTimeFormatter {
},
"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 TimeZoneFormatPattern(true, "TZ"),
- new TimeZoneFormatPattern(false, "tz"),
- new StringFormatPattern("OF") {
- @Override String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
+ 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);
@@ -308,4 +478,371 @@ public class PostgresqlDateTimeFormatter {
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) {
+ if
(Character.isLetter(formatString.charAt(formatParsePosition.getIndex()))) {
+ throw new IllegalArgumentException();
+ } else {
+ 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))
+ .withZoneSameInstant(zoneId);
+ }
+
+ 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) {
+ 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<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) {
+ 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;
+ }
+
+ return yearSign * year;
+ }
+
+ return yearSign;
+ }
+
+ private static LocalDateTime updateWithTimeFields(LocalDateTime dateTime,
+ Map<ChronoUnitEnum, Long> 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;
+ }
}
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/RomanNumeralMonthFormatPattern.java
index f2458896aa..6e18215ce5 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/RomanNumeralMonthFormatPattern.java
@@ -18,6 +18,8 @@ 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;
@@ -28,11 +30,11 @@ public class RomanNumeralMonthFormatPattern extends
StringFormatPattern {
private final boolean upperCase;
public RomanNumeralMonthFormatPattern(boolean upperCase, String... patterns)
{
- super(patterns);
+ super(ChronoUnitEnum.MONTHS_IN_YEAR, patterns);
this.upperCase = upperCase;
}
- @Override String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
+ @Override protected String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
@Nullable String suffix, Locale locale) {
final String romanNumeral;
@@ -81,4 +83,49 @@ public class RomanNumeralMonthFormatPattern extends
StringFormatPattern {
return romanNumeral.toLowerCase(locale);
}
}
+
+ @Override protected int parseValue(ParsePosition inputPosition, String
input, Locale locale,
+ boolean haveFillMode, boolean enforceLength) throws ParseException {
+ final String inputTrimmed = input.substring(inputPosition.getIndex());
+
+ if (inputTrimmed.startsWith(upperCase ? "III" : "iii")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 3);
+ return 3;
+ } else if (inputTrimmed.startsWith(upperCase ? "II" : "ii")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 2);
+ return 2;
+ } else if (inputTrimmed.startsWith(upperCase ? "IV" : "iv")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 2);
+ return 4;
+ } else if (inputTrimmed.startsWith(upperCase ? "IX" : "ix")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 2);
+ return 9;
+ } else if (inputTrimmed.startsWith(upperCase ? "I" : "i")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 1);
+ return 1;
+ } else if (inputTrimmed.startsWith(upperCase ? "VIII" : "viii")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 4);
+ return 8;
+ } else if (inputTrimmed.startsWith(upperCase ? "VII" : "vii")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 3);
+ return 7;
+ } else if (inputTrimmed.startsWith(upperCase ? "VI" : "vi")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 2);
+ return 6;
+ } else if (inputTrimmed.startsWith(upperCase ? "V" : "v")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 1);
+ return 5;
+ } else if (inputTrimmed.startsWith(upperCase ? "XII" : "xii")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 3);
+ return 12;
+ } else if (inputTrimmed.startsWith(upperCase ? "XI" : "xi")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 2);
+ return 11;
+ } else if (inputTrimmed.startsWith(upperCase ? "X" : "x")) {
+ inputPosition.setIndex(inputPosition.getIndex() + 1);
+ return 10;
+ }
+
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
}
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
index 4162c4deb8..d4ab0d3d90 100644
---
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
@@ -26,11 +26,19 @@ 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 implements FormatPattern {
- private final String[] patterns;
+public abstract class StringFormatPattern extends FormatPattern {
+ private final ChronoUnitEnum chronoUnit;
- protected StringFormatPattern(String... patterns) {
- this.patterns = patterns;
+ /**
+ * 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,
@@ -52,7 +60,7 @@ public abstract class StringFormatPattern implements
FormatPattern {
}
String patternToUse = null;
- for (String pattern : patterns) {
+ for (String pattern : getPatterns()) {
if (formatStringTrimmed.startsWith(pattern)) {
patternToUse = pattern;
break;
@@ -81,6 +89,24 @@ public abstract class StringFormatPattern implements
FormatPattern {
haveTranslationMode ? Locale.getDefault() : Locale.US);
}
- abstract String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
+ @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/TimeZoneFormatPattern.java
b/core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneFormatPattern.java
deleted file mode 100644
index 0193417820..0000000000
---
a/core/src/main/java/org/apache/calcite/util/format/postgresql/TimeZoneFormatPattern.java
+++ /dev/null
@@ -1,46 +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.time.ZonedDateTime;
-import java.time.format.TextStyle;
-import java.util.Locale;
-
-/**
- * Able to parse timezone codes from string and to get the timezone from a
datetime.
- * Timezone codes are 3 letters, such as PST or UTC.
- */
-public class TimeZoneFormatPattern extends StringFormatPattern {
- final boolean isUpperCase;
-
- public TimeZoneFormatPattern(boolean isUpperCase, String... patterns) {
- super(patterns);
- this.isUpperCase = isUpperCase;
- }
-
- @Override String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
- @Nullable String suffix, Locale locale) {
-
- final String zoneCode = dateTime.getZone().getDisplayName(TextStyle.SHORT,
locale);
- return String.format(
- locale,
- "%3s",
- isUpperCase ? zoneCode.toUpperCase(locale) :
zoneCode.toLowerCase(locale));
- }
-}
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/TimeZoneHoursFormatPattern.java
index 5c07559730..2b02d2e837 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/TimeZoneHoursFormatPattern.java
@@ -18,6 +18,8 @@ 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;
@@ -29,14 +31,58 @@ import java.util.Locale;
*/
public class TimeZoneHoursFormatPattern extends StringFormatPattern {
public TimeZoneHoursFormatPattern() {
- super("TZH");
+ super(ChronoUnitEnum.TIMEZONE_HOURS, "TZH");
}
- @Override String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
+ @Override protected int parseValue(final ParsePosition inputPosition, final
String input,
+ final Locale locale, final boolean haveFillMode, boolean enforceLength)
+ throws ParseException {
+
+ int inputOffset = inputPosition.getIndex();
+ String inputTrimmed = input.substring(inputOffset);
+
+ boolean isPositive = true;
+ if (inputTrimmed.charAt(0) == '-') {
+ isPositive = false;
+ } else if (inputTrimmed.charAt(0) != '+') {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ inputOffset++;
+ inputTrimmed = input.substring(inputOffset);
+
+ if (!Character.isDigit(inputTrimmed.charAt(0))) {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ int endIndex = inputOffset + 1;
+ if (endIndex > input.length() ||
!Character.isDigit(input.charAt(inputOffset))) {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ if (endIndex < input.length() &&
Character.isDigit(input.charAt(endIndex))) {
+ endIndex++;
+ }
+
+ int timezoneHours = Integer.parseInt(input.substring(inputOffset,
endIndex));
+
+ if (timezoneHours > 15) {
+ throw new ParseException("Value is outside of valid range",
inputPosition.getIndex());
+ }
+
+ inputPosition.setIndex(endIndex);
+ 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 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/TimeZoneMinutesFormatPattern.java
index f8d26426a0..18a28e7ad8 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/TimeZoneMinutesFormatPattern.java
@@ -18,6 +18,8 @@ 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;
@@ -29,14 +31,44 @@ import java.util.Locale;
*/
public class TimeZoneMinutesFormatPattern extends StringFormatPattern {
public TimeZoneMinutesFormatPattern() {
- super("TZM");
+ super(ChronoUnitEnum.TIMEZONE_MINUTES, "TZM");
}
- @Override String dateTimeToString(ZonedDateTime dateTime, boolean
haveFillMode,
+ @Override protected int parseValue(final ParsePosition inputPosition, final
String input,
+ final Locale locale, final boolean haveFillMode, boolean enforceLength)
+ throws ParseException {
+
+ if (inputPosition.getIndex() + 2 > input.length()) {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ if (!Character.isDigit(input.charAt(inputPosition.getIndex()))
+ || !Character.isDigit(input.charAt(inputPosition.getIndex() + 1))) {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ int timezoneMinutes =
+ Integer.parseInt(
+ input.substring(inputPosition.getIndex(),
+ inputPosition.getIndex() + 2));
+
+ if (timezoneMinutes >= 60) {
+ throw new ParseException("Value outside of valid range",
inputPosition.getIndex());
+ }
+
+ inputPosition.setIndex(inputPosition.getIndex() + 2);
+ 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 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/YearWithCommasFormatPattern.java
new file mode 100644
index 0000000000..0aa3dee35d
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/util/format/postgresql/YearWithCommasFormatPattern.java
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+/**
+ * Able to parse and generate string of years with commas to separate the
thousands. An
+ * example year is "1,997".
+ */
+public class YearWithCommasFormatPattern extends StringFormatPattern {
+ public YearWithCommasFormatPattern() {
+ super(ChronoUnitEnum.YEARS, "Y,YYY");
+ }
+
+ @Override protected int parseValue(ParsePosition inputPosition, String
input, Locale locale,
+ boolean haveFillMode, boolean enforceLength) throws ParseException {
+
+ final String inputTrimmed = input.substring(inputPosition.getIndex());
+ final int commaIndex = inputTrimmed.indexOf(',');
+
+ if (commaIndex <= 0 || commaIndex > 3) {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ final String thousands = inputTrimmed.substring(0, commaIndex);
+ int endIndex;
+ if (enforceLength) {
+ if (inputPosition.getIndex() + commaIndex + 4 > input.length()) {
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+
+ endIndex = commaIndex + 4;
+ } else {
+ endIndex = commaIndex + 1;
+ for (; endIndex < inputTrimmed.length(); endIndex++) {
+ if (!Character.isDigit(inputTrimmed.charAt(endIndex))) {
+ break;
+ }
+ }
+
+ if (endIndex == commaIndex + 1 || endIndex > commaIndex + 4) {
+ inputPosition.setErrorIndex(inputPosition.getIndex());
+ throw new ParseException("Unable to parse value",
inputPosition.getIndex());
+ }
+ }
+
+ inputPosition.setIndex(inputPosition.getIndex() + endIndex);
+
+ final String remainingDigits = inputTrimmed.substring(commaIndex + 1,
endIndex);
+ return Integer.parseInt(thousands) * 1000 +
Integer.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);
+ }
+ }
+
+ return stringValue.substring(0, stringValue.length() - 3) + ","
+ + stringValue.substring(stringValue.length() - 3) + outputSuffix;
+ }
+
+ @Override protected boolean isNumeric() {
+ return true;
+ }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index bb4dc7b783..2b44ef986f 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -1615,14 +1615,14 @@ public class SqlValidatorTest extends
SqlValidatorTestCase {
final SqlOperatorTable opTable = operatorTableFor(SqlLibrary.POSTGRESQL);
expr("TO_TIMESTAMP('2000-01-01 01:00:00', 'YYYY-MM-DD HH:MM:SS')")
.withOperatorTable(opTable)
- .columnType("TIMESTAMP(0) NOT NULL");
+ .columnType("TIMESTAMP_TZ(0) NOT NULL");
wholeExpr("TO_TIMESTAMP('2000-01-01 01:00:00')")
.withOperatorTable(opTable)
.fails("Invalid number of arguments to function 'TO_TIMESTAMP'. "
+ "Was expecting 2 arguments");
expr("TO_TIMESTAMP(2000, 'YYYY')")
.withOperatorTable(opTable)
- .columnType("TIMESTAMP(0) NOT NULL");
+ .columnType("TIMESTAMP_TZ(0) NOT NULL");
wholeExpr("TO_TIMESTAMP(2000, 'YYYY')")
.withOperatorTable(opTable)
.withTypeCoercion(false)
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 b36491620a..df4ced40c5 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
@@ -24,15 +24,24 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
import java.util.Locale;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* Unit test for {@link PostgresqlDateTimeFormatter}.
*/
@Isolated
public class PostgresqlDateTimeFormatterTest {
+ private static final ZoneId TIME_ZONE = ZoneId.systemDefault();
+
+ private static final ZonedDateTime DAY_1_CE = createDateTime(1, 1, 1, 0, 0,
0, 0);
+ private static final ZonedDateTime APR_17_2024 = createDateTime(2024, 4, 17,
0, 0, 0, 0);
+ private static final ZonedDateTime JAN_1_2001 = createDateTime(2001, 1, 1,
0, 0, 0, 0);
+ private static final ZonedDateTime JAN_1_2024 = createDateTime(2024, 1, 1,
0, 0, 0, 0);
+
@ParameterizedTest
@ValueSource(strings = {"HH12", "HH"})
void testHH12(String pattern) {
@@ -1311,8 +1320,932 @@ public class PostgresqlDateTimeFormatterTest {
assertEquals("xii", PostgresqlDateTimeFormatter.toChar("rm", date4));
}
- private ZonedDateTime createDateTime(int year, int month, int dayOfMonth,
int hour, int minute,
- int seconds, int nanoseconds) {
+ @Test void testToTimestampHH() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusHours(1),
+ PostgresqlDateTimeFormatter.toTimestamp("01", "HH", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(1),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "HH", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(11),
+ PostgresqlDateTimeFormatter.toTimestamp("11", "HH", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("72", "HH", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "HH", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampHH12() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusHours(1),
+ PostgresqlDateTimeFormatter.toTimestamp("01", "HH12", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(1),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "HH12", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(11),
+ PostgresqlDateTimeFormatter.toTimestamp("11", "HH12", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("72", "HH12", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "HH12", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampHH24() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusHours(1),
+ PostgresqlDateTimeFormatter.toTimestamp("01", "HH24", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(1),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "HH24", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(18),
+ PostgresqlDateTimeFormatter.toTimestamp("18", "HH24", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("72", "HH24", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "HH24", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampMI() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusMinutes(1),
+ PostgresqlDateTimeFormatter.toTimestamp("01", "MI", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMinutes(1),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "MI", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMinutes(57),
+ PostgresqlDateTimeFormatter.toTimestamp("57", "MI", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("72", "MI", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "MI", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampSS() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusSeconds(1),
+ PostgresqlDateTimeFormatter.toTimestamp("01", "SS", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusSeconds(1),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "SS", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusSeconds(57),
+ PostgresqlDateTimeFormatter.toTimestamp("57", "SS", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("72", "SS", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "SS", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampMS() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("001", "MS", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "MS", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(999_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("999", "MS", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("9999", "MS", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "MS", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampUS() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000),
+ PostgresqlDateTimeFormatter.toTimestamp("001", "US", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "US", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(999_000),
+ PostgresqlDateTimeFormatter.toTimestamp("999", "US", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("9999999", "US", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "US", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampFF1() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(100_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "FF1", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(900_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("9", "FF1", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("72", "FF1", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "FF1", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampFF2() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(10_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("01", "FF2", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(10_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "FF2", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(970_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("97", "FF2", TIME_ZONE));
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("999", "FF2", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+
+ try {
+ PostgresqlDateTimeFormatter.toTimestamp("abc", "FF2", TIME_ZONE);
+ fail();
+ } catch (Exception e) {
+ }
+ }
+
+ @Test void testToTimestampFF3() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("001", "FF3", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "FF3", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(976_000_000),
+ PostgresqlDateTimeFormatter.toTimestamp("976", "FF3", TIME_ZONE));
+ }
+
+ @Test void testToTimestampFF4() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(100_000),
+ PostgresqlDateTimeFormatter.toTimestamp("0001", "FF4", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(100_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "FF4", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(976_200_000),
+ PostgresqlDateTimeFormatter.toTimestamp("9762", "FF4", TIME_ZONE));
+ }
+
+ @Test void testToTimestampFF5() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(10_000),
+ PostgresqlDateTimeFormatter.toTimestamp("00001", "FF5", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(10_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "FF5", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(976_210_000),
+ PostgresqlDateTimeFormatter.toTimestamp("97621", "FF5", TIME_ZONE));
+ }
+
+ @Test void testToTimestampFF6() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000),
+ PostgresqlDateTimeFormatter.toTimestamp("000001", "FF6", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(1_000),
+ PostgresqlDateTimeFormatter.toTimestamp("1", "FF6", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusNanos(976_214_000),
+ PostgresqlDateTimeFormatter.toTimestamp("976214", "FF6", TIME_ZONE));
+ }
+
+ @Test void testToTimestampAMPM() throws Exception {
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03AM", "HH12AM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03AM", "HH12PM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03PM", "HH12AM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03PM", "HH12PM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03A.M.", "HH12A.M.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03A.M.", "HH12P.M.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03P.M.", "HH12A.M.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03P.M.", "HH12P.M.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03am", "HH12am", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03am", "HH12pm", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03pm", "HH12am", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03pm", "HH12pm", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03a.m.", "HH12a.m.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(3),
+ PostgresqlDateTimeFormatter.toTimestamp("03a.m.", "HH12p.m.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03p.m.", "HH12a.m.",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusHours(15),
+ PostgresqlDateTimeFormatter.toTimestamp("03p.m.", "HH12p.m.",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampYYYYWithCommas() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("0,001", "Y,YYY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2,024", "Y,YYY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampYYYY() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("0001", "YYYY", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "YYYY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024", "YYYY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampYYY() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("001", "YYY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "YYY", TIME_ZONE));
+ assertEquals(
+ createDateTime(1987, 1, 1, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("987", "YYY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampYY() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("01", "YY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "YY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("24", "YY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampY() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "Y", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001.plusYears(3),
+ PostgresqlDateTimeFormatter.toTimestamp("4", "Y", TIME_ZONE));
+ }
+
+ @Test void testToTimestampIYYY() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("0001", "IYYY", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "IYYY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024", "IYYY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampIYY() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("001", "IYY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "IYY", TIME_ZONE));
+ assertEquals(
+ createDateTime(1987, 1, 1, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("987", "IYY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampIY() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("01", "IY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "IY", TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("24", "IY", TIME_ZONE));
+ }
+
+ @Test void testToTimestampI() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "I", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "I", TIME_ZONE));
+ assertEquals(
+ JAN_1_2001.plusYears(3),
+ PostgresqlDateTimeFormatter.toTimestamp("4", "I", TIME_ZONE));
+ }
+
+ @Test void testToTimestampBCAD() throws Exception {
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920BC", "YYYYBC", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920BC", "YYYYAD", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920AD", "YYYYBC", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920AD", "YYYYAD", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920B.C.", "YYYYB.C.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920B.C.", "YYYYA.D.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920A.D.", "YYYYB.C.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920A.D.", "YYYYA.D.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920bc", "YYYYbc", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920bc", "YYYYad", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920ad", "YYYYbc", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920ad", "YYYYad", TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920b.c.", "YYYYb.c.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 0,
+ PostgresqlDateTimeFormatter.toTimestamp("1920b.c.", "YYYYa.d.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920a.d.", "YYYYb.c.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ assertEquals(
+ 1,
+ PostgresqlDateTimeFormatter.toTimestamp("1920a.d.", "YYYYa.d.",
TIME_ZONE)
+ .get(ChronoField.ERA));
+ }
+
+ @Test void testToTimestampMonthUpperCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("JANUARY", "MONTH",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(2),
+ PostgresqlDateTimeFormatter.toTimestamp("MARCH", "MONTH",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("NOVEMBER", "MONTH",
TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampMonthCapitalized() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("January", "Month",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(2),
+ PostgresqlDateTimeFormatter.toTimestamp("March", "Month",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("November", "Month",
TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampMonthLowerCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("january", "month",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(2),
+ PostgresqlDateTimeFormatter.toTimestamp("march", "month",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("november", "month",
TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampMonUpperCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("JAN", "MON", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(2),
+ PostgresqlDateTimeFormatter.toTimestamp("MAR", "MON", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("NOV", "MON", TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampMonCapitalized() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("Jan", "Mon", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(2),
+ PostgresqlDateTimeFormatter.toTimestamp("Mar", "Mon", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("Nov", "Mon", TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampMonLowerCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("jan", "mon", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(2),
+ PostgresqlDateTimeFormatter.toTimestamp("mar", "mon", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("nov", "mon", TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampMM() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("01", "MM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "MM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(10),
+ PostgresqlDateTimeFormatter.toTimestamp("11", "MM", TIME_ZONE));
+ }
+
+ @Test void testToTimestampDayUpperCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 MONDAY", "IYYY IW
DAY", TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 THURSDAY", "IYYY IW
DAY", TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 FRIDAY", "IYYY IW
DAY", TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampDayCapitalized() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 Monday", "IYYY IW
Day", TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 Thursday", "IYYY IW
Day", TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 Friday", "IYYY IW
Day", TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampDayLowerCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 monday", "IYYY IW
day", TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 thursday", "IYYY IW
day", TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 friday", "IYYY IW
day", TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampDyUpperCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 MON", "IYYY IW DY",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 THU", "IYYY IW DY",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 FRI", "IYYY IW DY",
TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampDyCapitalized() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 Mon", "IYYY IW Dy",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 Thu", "IYYY IW Dy",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 Fri", "IYYY IW Dy",
TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampDyLowerCase() throws Exception {
+ final Locale originalLocale = Locale.getDefault();
+
+ try {
+ Locale.setDefault(Locale.US);
+
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 mon", "IYYY IW dy",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 thu", "IYYY IW dy",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 fri", "IYYY IW dy",
TIME_ZONE));
+ } finally {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
+ @Test void testToTimestampDDD() throws Exception {
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024 001", "YYYY DDD",
TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024 1", "YYYY DDD",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2024, 5, 16, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2024 137", "YYYY DDD",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampDD() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("01", "DD", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "DD", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusDays(22),
+ PostgresqlDateTimeFormatter.toTimestamp("23", "DD", TIME_ZONE));
+ }
+
+ @Test void testToTimestampIDDD() throws Exception {
+ assertEquals(
+ createDateTime(2019, 12, 30, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2020 001", "IYYY IDDD",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2019, 12, 30, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2020 1", "IYYY IDDD",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2020, 5, 14, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2020 137", "IYYY IDDD",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampID() throws Exception {
+ assertEquals(
+ createDateTime(1982, 6, 7, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 1", "IYYY IW ID",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 10, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 4", "IYYY IW ID",
TIME_ZONE));
+ assertEquals(
+ createDateTime(1982, 6, 11, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1982 23 5", "IYYY IW ID",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampW() throws Exception {
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024 1 1", "YYYY MM W",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2024, 4, 8, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2024 4 2", "YYYY MM W",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2024, 11, 22, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2024 11 4", "YYYY MM W",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampWW() throws Exception {
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024 01", "YYYY WW",
TIME_ZONE));
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024 1", "YYYY WW",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2024, 12, 16, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2024 51", "YYYY WW",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampIW() throws Exception {
+ assertEquals(
+ createDateTime(2019, 12, 30, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2020 01", "IYYY IW",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2019, 12, 30, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2020 1", "IYYY IW",
TIME_ZONE));
+ assertEquals(
+ createDateTime(2020, 12, 14, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2020 51", "IYYY IW",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampCC() throws Exception {
+ assertEquals(
+ JAN_1_2001,
+ PostgresqlDateTimeFormatter.toTimestamp("21", "CC", TIME_ZONE));
+ assertEquals(
+ createDateTime(1501, 1, 1, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("16", "CC", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("1", "CC", TIME_ZONE));
+ }
+
+ @Test void testToTimestampJ() throws Exception {
+ assertEquals(
+ JAN_1_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2460311", "J", TIME_ZONE));
+ assertEquals(
+ createDateTime(1984, 7, 15, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2445897", "J", TIME_ZONE));
+ assertEquals(
+ createDateTime(234, 3, 21, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("1806606", "J", TIME_ZONE));
+ }
+
+ @Test void testToTimestampRMUpperCase() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("I", "RM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(3),
+ PostgresqlDateTimeFormatter.toTimestamp("IV", "RM", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(8),
+ PostgresqlDateTimeFormatter.toTimestamp("IX", "RM", TIME_ZONE));
+ }
+
+ @Test void testToTimestampRMLowerCase() throws Exception {
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("i", "rm", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(3),
+ PostgresqlDateTimeFormatter.toTimestamp("iv", "rm", TIME_ZONE));
+ assertEquals(
+ DAY_1_CE.plusMonths(8),
+ PostgresqlDateTimeFormatter.toTimestamp("ix", "rm", TIME_ZONE));
+ }
+
+ @Test void testToTimestampDateValidFormats() throws Exception {
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024-04-17", "YYYY-MM-DD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2,024-04-17", "Y,YYY-MM-DD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("24-04-17", "YYY-MM-DD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("24-04-17", "YY-MM-DD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2124-04-17", "CCYY-MM-DD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("20240417", "YYYYMMDD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2,0240417", "Y,YYYMMDD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024-16-3", "IYYY-IW-ID",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024-16 Wednesday", "IYYY-IW
Day", TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024-108", "IYYY-IDDD",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("April 17, 2024", "Month DD,
YYYY", TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("IV 17, 2024", "RM DD, YYYY",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("APR 17, 2024", "MON DD,
YYYY", TIME_ZONE));
+ assertEquals(
+ createDateTime(2024, 4, 15, 0, 0, 0, 0),
+ PostgresqlDateTimeFormatter.toTimestamp("2024-16", "YYYY-WW",
TIME_ZONE));
+ assertEquals(
+ APR_17_2024,
+ PostgresqlDateTimeFormatter.toTimestamp("2024-108", "YYYY-DDD",
TIME_ZONE));
+ assertEquals(
+ DAY_1_CE,
+ PostgresqlDateTimeFormatter.toTimestamp("0000-01-01", "YYYY-MM-DD",
TIME_ZONE));
+ }
+
+ @Test void testToTimestampWithTimezone() throws Exception {
+ final ZoneId utcZone = ZoneId.of("UTC");
+ 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));
+ }
+
+ private static ZonedDateTime createDateTime(int year, int month, int
dayOfMonth, int hour,
+ int minute, int seconds, int nanoseconds) {
return ZonedDateTime.of(
LocalDateTime.of(year, month, dayOfMonth, hour, minute, seconds,
nanoseconds),
ZoneId.systemDefault());
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 1c0b2cae2b..3bea58f537 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -5149,10 +5149,43 @@ public class SqlOperatorTest {
f.checkNull("to_date(NULL, NULL)");
};
final List<SqlLibrary> libraries =
- list(SqlLibrary.ORACLE, SqlLibrary.POSTGRESQL, SqlLibrary.REDSHIFT);
+ list(SqlLibrary.ORACLE, SqlLibrary.REDSHIFT);
f0.forEachLibrary(libraries, consumer);
}
+ @Test void testToDatePg() {
+ final SqlOperatorFixture f = fixture().withLibrary(SqlLibrary.POSTGRESQL)
+ .setFor(SqlLibraryOperators.TO_DATE_PG);
+
+ f.checkString("to_date('2022-06-03', 'YYYY-MM-DD')",
+ "2022-06-03",
+ "DATE NOT NULL");
+ f.checkString("to_date('0001-01-01', 'YYYY-MM-DD')",
+ "0001-01-01",
+ "DATE NOT NULL");
+ f.checkString("to_date('Jun 03, 2022', 'Mon DD, YYYY')",
+ "2022-06-03",
+ "DATE NOT NULL");
+ f.checkString("to_date('2022-June-03', 'YYYY-Month-DD')",
+ "2022-06-03",
+ "DATE NOT NULL");
+ f.checkString("to_date('2022-Jun-03', 'YYYY-Mon-DD')",
+ "2022-06-03",
+ "DATE NOT NULL");
+ f.checkString("to_date('2022-154', 'YYYY-DDD')",
+ "2022-06-03",
+ "DATE NOT NULL");
+ 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)");
+ }
+
@Test void testToTimestamp() {
final SqlOperatorFixture f0 =
fixture().setFor(SqlLibraryOperators.TO_TIMESTAMP);
@@ -5186,10 +5219,45 @@ public class SqlOperatorTest {
f.checkNull("to_timestamp(NULL, NULL)");
};
final List<SqlLibrary> libraries =
- list(SqlLibrary.ORACLE, SqlLibrary.POSTGRESQL, SqlLibrary.REDSHIFT);
+ list(SqlLibrary.ORACLE, SqlLibrary.REDSHIFT);
f0.forEachLibrary(libraries, consumer);
}
+ @Test void testToTimestampPg() {
+ final SqlOperatorFixture f = fixture().withLibrary(SqlLibrary.POSTGRESQL)
+ .setFor(SqlLibraryOperators.TO_TIMESTAMP_PG);
+
+ f.checkString("to_timestamp('2022-06-03 18:34:56', 'YYYY-MM-DD
HH24:MI:SS')",
+ "2022-06-03 18:34:56",
+ "TIMESTAMP_TZ(0) NOT NULL");
+ f.checkString("to_timestamp('0001-01-01 18:43:56', 'YYYY-MM-DD
HH24:MI:SS')",
+ "0001-01-01 18:43:56",
+ "TIMESTAMP_TZ(0) NOT NULL");
+ f.checkString("to_timestamp('18:34:56 Jun 03, 2022', 'HH24:MI:SS Mon DD,
YYYY')",
+ "2022-06-03 18:34:56",
+ "TIMESTAMP_TZ(0) NOT NULL");
+ f.checkString("to_timestamp('18:34:56 2022-June-03', 'HH24:MI:SS
YYYY-Month-DD')",
+ "2022-06-03 18:34:56",
+ "TIMESTAMP_TZ(0) NOT NULL");
+ f.checkString("to_timestamp('18:34:56 2022-Jun-03', 'HH24:MI:SS
YYYY-Mon-DD')",
+ "2022-06-03 18:34:56",
+ "TIMESTAMP_TZ(0) NOT NULL");
+ f.checkString("to_timestamp('18:34:56 2022-154', 'HH24:MI:SS YYYY-DDD')",
+ "2022-06-03 18:34:56",
+ "TIMESTAMP_TZ(0) NOT NULL");
+ f.checkFails("to_timestamp('ABCD', 'YYYY-MM-DD HH24:MI:SS')",
+ "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)");
+ }
+
@Test void testFromBase64() {
final SqlOperatorFixture f0 = fixture()
.setFor(SqlLibraryOperators.FROM_BASE64);