From 3d75f22c2c69aaf9782f236e716bb009fcb77d54 Mon Sep 17 00:00:00 2001 From: jycr Date: Thu, 26 Jun 2025 01:08:47 +0200 Subject: [PATCH 1/2] Add support for parsing time zones in DateFormatters and enhance Iso8601Parser We can now parse ISO date-time such as : 2029-05-15T17:14:56.123456789-08:00[America/Los_Angeles] or 2031-12-03T10:15:30.123456789+01:00[Europe/Paris] or with "short-id" (like `NST` for `Pacific/Auckland`) 2025-06-26T12:01:48.211+12:00[NST] --- docs/changelog/130054.yaml | 5 ++++ .../common/time/Iso8601Parser.java | 26 +++++++++++++++++++ .../common/time/DateFormattersTests.java | 9 +++++++ 3 files changed, 40 insertions(+) create mode 100644 docs/changelog/130054.yaml diff --git a/docs/changelog/130054.yaml b/docs/changelog/130054.yaml new file mode 100644 index 0000000000000..02c104c21056c --- /dev/null +++ b/docs/changelog/130054.yaml @@ -0,0 +1,5 @@ +pr: 130054 +summary: Add support for parsing ISO date time with zone-id (RFC 9557) +area: Mapping +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java b/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java index 86ecddff9c575..131ee74246b85 100644 --- a/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java +++ b/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java @@ -477,6 +477,10 @@ private ZoneId parseZoneId(CharSequence str, int pos) { if (minutes == null || minutes > 59) return null; if (len == pos) return ofHoursMinutesSeconds(hours, minutes, 0, positive); + if (str.charAt(pos) == '[' && str.charAt(len - 1) == ']') { + return parseRawZoneId(str, pos); + } + // either both dividers have a colon, or neither do if ((str.charAt(pos) == ':') != hasColon) return null; if (hasColon) { @@ -487,10 +491,32 @@ private ZoneId parseZoneId(CharSequence str, int pos) { if (seconds == null || seconds > 59) return null; if (len == pos) return ofHoursMinutesSeconds(hours, minutes, seconds, positive); + if (str.charAt(pos) == '[' && str.charAt(len - 1) == ']') { + return parseRawZoneId(str, pos); + } + // there's some text left over... return null; } + /** + * Parses a raw zone id, which is a string of the form [zoneId] (eg [Europe/Paris]). + * + * @param str The string to parse + * @param pos The position in the string where the zone id starts (the first character after the opening [) + * @return The parsed zone id, or {@code null} if the string is not a valid zone id. + */ + private ZoneId parseRawZoneId(CharSequence str, int pos) { + try { + String zoneId = str.subSequence(pos + 1, str.length() - 1).toString(); + // Try to resolve short zone ids to offsets. + zoneId = ZoneId.SHORT_IDS.getOrDefault(zoneId, zoneId); + return ZoneId.of(zoneId); + } catch (DateTimeException e) { + return null; + } + } + /* * ZoneOffset.ofTotalSeconds has a ConcurrentHashMap cache of offsets. This is fine, * but it does mean there's an expensive map lookup every time we call ofTotalSeconds. diff --git a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java index 463d0d4514bac..7cd94908e3037 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -608,6 +608,15 @@ public void testIso8601Parsing() { formatter.format(formatter.parse("2018-05-15T17:14:56,123456789+01:00")); } + public void testParsingDateTimeWithZoneId() { + DateFormatter formatter = DateFormatters.forPattern("strict_date_optional_time"); + formatter.format(formatter.parse("2018-05-15T17:14:56-08:00[America/Los_Angeles]")); + formatter.format(formatter.parse("2029-05-15T17:14:56.123456789-08:00[America/Los_Angeles]")); + formatter.format(formatter.parse("2031-12-03T10:15:30.123456789+01:00:00[Europe/Paris]")); + formatter.format(formatter.parse("2031-12-03T10:15:30.123456789+01:00:00[PST]")); + formatter.format(formatter.parse("2031-12-03T10:15:30.123456789+01:00:00[EST]")); + } + public void testRoundupFormatterWithEpochDates() { assertRoundupFormatter("epoch_millis", "1234567890", 1234567890L); // also check nanos of the epoch_millis formatter if it is rounded up to the nano second From 3efd3f6a466181011512ffe44e67544cc4bd53d1 Mon Sep 17 00:00:00 2001 From: jycr Date: Thu, 26 Jun 2025 02:14:22 +0200 Subject: [PATCH 2/2] Supports parsing dates with `UTC` as zone-id Such as: `2031-12-03T10:15:30.123456789Z[UTC]` --- .../java/org/elasticsearch/common/time/Iso8601Parser.java | 5 ++++- .../org/elasticsearch/common/time/DateFormattersTests.java | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java b/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java index 131ee74246b85..99c47f38ba6ae 100644 --- a/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java +++ b/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java @@ -450,7 +450,7 @@ private ZoneId parseZoneId(CharSequence str, int pos) { boolean positive; switch (first) { - case '+' -> positive = true; + case '+', 'Z' -> positive = true; case '-' -> positive = false; default -> { // non-trivial zone offset, fallback on the built-in java zoneid parser @@ -462,6 +462,9 @@ private ZoneId parseZoneId(CharSequence str, int pos) { } } pos++; // read the + or - + if (str.charAt(pos) == '[' && str.charAt(len - 1) == ']') { + return parseRawZoneId(str, pos); + } Integer hours = parseInt(str, pos, pos += 2); if (hours == null || hours > 23) return null; diff --git a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java index 7cd94908e3037..be1ddc0f61f75 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -615,6 +615,7 @@ public void testParsingDateTimeWithZoneId() { formatter.format(formatter.parse("2031-12-03T10:15:30.123456789+01:00:00[Europe/Paris]")); formatter.format(formatter.parse("2031-12-03T10:15:30.123456789+01:00:00[PST]")); formatter.format(formatter.parse("2031-12-03T10:15:30.123456789+01:00:00[EST]")); + formatter.format(formatter.parse("2031-12-03T10:15:30.123456789Z[UTC]")); } public void testRoundupFormatterWithEpochDates() {