Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/130054.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 130054
summary: Add support for parsing ISO date time with zone-id (RFC 9557)
area: Mapping
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -462,6 +462,9 @@ private ZoneId parseZoneId(CharSequence str, int pos) {
}
}
pos++; // read the + or -
if (str.charAt(pos) == '[' && str.charAt(len - 1) == ']') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could lead to some malformed strings being parsed, like 2022-07-08T00:14:07+[Europe/London]

return parseRawZoneId(str, pos);
}

Integer hours = parseInt(str, pos, pos += 2);
if (hours == null || hours > 23) return null;
Expand All @@ -477,6 +480,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) {
Expand All @@ -487,10 +494,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about unknown suffixes, or suffixes representing features like e.g. calendars? (for example, 2022-07-08T00:14:07+01:00[knort=blargel]?)
I think that in that case we would want to ignore that, and keep the parsed offset. Here instead I think we end up ignoring both.

}
}

/*
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,16 @@ 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]"));
formatter.format(formatter.parse("2031-12-03T10:15:30.123456789Z[UTC]"));
}

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
Expand Down
Loading