From ff21b5a430444a5760d67e6764a6abf3c4d47da1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 17 May 2022 10:45:29 +0200 Subject: [PATCH] Polishing. Add support for offset representation variants (short and full). [#509][resolves #510] Signed-off-by: Mark Paluch --- .../codec/AbstractTemporalCodec.java | 6 +- .../codec/PostgresqlDateTimeFormatter.java | 56 ++++++++++++++++++- .../codec/PostgresqlTimeFormatter.java | 48 +++++++++++++++- .../codec/OffsetTimeCodecUnitTests.java | 6 ++ 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/r2dbc/postgresql/codec/AbstractTemporalCodec.java b/src/main/java/io/r2dbc/postgresql/codec/AbstractTemporalCodec.java index 993e4bf0..80c02309 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/AbstractTemporalCodec.java +++ b/src/main/java/io/r2dbc/postgresql/codec/AbstractTemporalCodec.java @@ -118,7 +118,7 @@ private Temporal decodeTemporal(ByteBuf buffer, PostgresqlObjectId dataType, @Nu return EpochTime.fromLong(buffer.readLong()).toLocalDateTime(); } - return PostgresqlDateTimeFormatter.INSTANCE.parse(ByteBufUtils.decode(buffer), LocalDateTime::from); + return PostgresqlDateTimeFormatter.parse(ByteBufUtils.decode(buffer), LocalDateTime::from); case DATE: case DATE_ARRAY: if (FORMAT_BINARY == format) { @@ -139,7 +139,7 @@ private Temporal decodeTemporal(ByteBuf buffer, PostgresqlObjectId dataType, @Nu return EpochTime.fromLong(buffer.readLong()).toInstant().atOffset(OffsetDateTime.now().getOffset()); } - return PostgresqlDateTimeFormatter.INSTANCE.parse(ByteBufUtils.decode(buffer), ZonedDateTime::from); + return PostgresqlDateTimeFormatter.parse(ByteBufUtils.decode(buffer), ZonedDateTime::from); case TIMETZ: case TIMETZ_ARRAY: if (FORMAT_BINARY == format) { @@ -148,7 +148,7 @@ private Temporal decodeTemporal(ByteBuf buffer, PostgresqlObjectId dataType, @Nu return OffsetTime.of(LocalTime.ofNanoOfDay(timeNano), ZoneOffset.ofTotalSeconds(offsetSec)); } - return PostgresqlTimeFormatter.INSTANCE.parse(ByteBufUtils.decode(buffer), OffsetTime::from); + return PostgresqlTimeFormatter.parse(ByteBufUtils.decode(buffer), OffsetTime::from); } throw new UnsupportedOperationException(String.format("Cannot decode value for type %s, format %s", dataType, format)); diff --git a/src/main/java/io/r2dbc/postgresql/codec/PostgresqlDateTimeFormatter.java b/src/main/java/io/r2dbc/postgresql/codec/PostgresqlDateTimeFormatter.java index ca96eaca..08ec4741 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/PostgresqlDateTimeFormatter.java +++ b/src/main/java/io/r2dbc/postgresql/codec/PostgresqlDateTimeFormatter.java @@ -18,7 +18,9 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; import java.time.format.SignStyle; +import java.time.temporal.TemporalQuery; import static java.time.temporal.ChronoField.DAY_OF_MONTH; import static java.time.temporal.ChronoField.HOUR_OF_DAY; @@ -30,7 +32,7 @@ class PostgresqlDateTimeFormatter { - static final DateTimeFormatter INSTANCE = + private static final DateTimeFormatter FULL_OFFSET = new DateTimeFormatterBuilder() .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) .appendLiteral('-') @@ -48,8 +50,58 @@ class PostgresqlDateTimeFormatter { .appendFraction(NANO_OF_SECOND, 0, 9, true) .optionalEnd() .optionalStart() - .appendOffset("+HH:mm:ss", "+00:00:00") + .appendOffset("+HH:MM:ss", "+00:00:00") .optionalEnd() .toFormatter(); + private static final DateTimeFormatter SHORT_OFFSET = + new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(DAY_OF_MONTH, 2) + .appendLiteral(' ') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .optionalStart() + .appendFraction(NANO_OF_SECOND, 0, 9, true) + .optionalEnd() + .optionalStart() + .appendOffset("+HH:mm", "+00:00") + .optionalEnd() + .toFormatter(); + + /** + * Fully parses the text producing an object of the specified type. + * Attempts to parse the text using {@link #SHORT_OFFSET} first, then {@link #FULL_OFFSET}. + *

+ * Most applications should use this method for parsing. + * It parses the entire text to produce the required date-time. + * For example: + *

+     *  LocalDateTime dt = parse(str, LocalDateTime::from);
+     * 
+ * If the parse completes without reading the entire length of the text, + * or a problem occurs during parsing or merging, then an exception is thrown. + * + * @param the type of the parsed date-time + * @param text the text to parse, not null + * @param query the query defining the type to parse to, not null + * @return the parsed date-time, not null + * @throws DateTimeParseException if unable to parse the requested result + */ + static T parse(CharSequence text, TemporalQuery query) { + + try { + return SHORT_OFFSET.parse(text, query); + } catch (DateTimeParseException e) { + return FULL_OFFSET.parse(text, query); + } + } + } diff --git a/src/main/java/io/r2dbc/postgresql/codec/PostgresqlTimeFormatter.java b/src/main/java/io/r2dbc/postgresql/codec/PostgresqlTimeFormatter.java index 77f7ab67..9cf760db 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/PostgresqlTimeFormatter.java +++ b/src/main/java/io/r2dbc/postgresql/codec/PostgresqlTimeFormatter.java @@ -18,6 +18,8 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalQuery; import static java.time.temporal.ChronoField.HOUR_OF_DAY; import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; @@ -26,7 +28,7 @@ class PostgresqlTimeFormatter { - static final DateTimeFormatter INSTANCE = + private static final DateTimeFormatter SHORT_OFFSET = new DateTimeFormatterBuilder() .appendValue(HOUR_OF_DAY, 2) .appendLiteral(':') @@ -42,4 +44,48 @@ class PostgresqlTimeFormatter { .optionalEnd() .toFormatter(); + private static final DateTimeFormatter FULL_OFFSET = + new DateTimeFormatterBuilder() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .optionalStart() + .appendFraction(NANO_OF_SECOND, 0, 9, true) + .optionalEnd() + .optionalStart() + .appendOffset("+HH:MM:ss", "+00:00") + .optionalEnd() + .toFormatter(); + + /** + * Fully parses the text producing an object of the specified type. + * Attempts to parse the text using {@link #SHORT_OFFSET} first, then {@link #FULL_OFFSET}. + *

+ * Most applications should use this method for parsing. + * It parses the entire text to produce the required date-time. + * For example: + *

+     *  OffsetTime dt = parse(str, OffsetTime::from);
+     * 
+ * If the parse completes without reading the entire length of the text, + * or a problem occurs during parsing or merging, then an exception is thrown. + * + * @param the type of the parsed date-time + * @param text the text to parse, not null + * @param query the query defining the type to parse to, not null + * @return the parsed date-time, not null + * @throws DateTimeParseException if unable to parse the requested result + */ + static T parse(CharSequence text, TemporalQuery query) { + + try { + return SHORT_OFFSET.parse(text, query); + } catch (DateTimeParseException e) { + return FULL_OFFSET.parse(text, query); + } + } + } diff --git a/src/test/java/io/r2dbc/postgresql/codec/OffsetTimeCodecUnitTests.java b/src/test/java/io/r2dbc/postgresql/codec/OffsetTimeCodecUnitTests.java index 8f898766..e742c7e3 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/OffsetTimeCodecUnitTests.java +++ b/src/test/java/io/r2dbc/postgresql/codec/OffsetTimeCodecUnitTests.java @@ -52,6 +52,12 @@ void decode() { assertThat(new OffsetTimeCodec(TEST).decode(encode(TEST, "21:16:41.123456-09:00"), dataType, FORMAT_TEXT, OffsetTime.class)) .isEqualTo(offsetTime); + + assertThat(new OffsetTimeCodec(TEST).decode(encode(TEST, "21:16:41.123456-09"), dataType, FORMAT_TEXT, OffsetTime.class)) + .isEqualTo(offsetTime); + + assertThat(new OffsetTimeCodec(TEST).decode(encode(TEST, "21:16:41.123456-09:00:00"), dataType, FORMAT_TEXT, OffsetTime.class)) + .isEqualTo(offsetTime); } @Test