8364752: java.time.Instant should be able to parse ISO 8601 offsets of the form HH:mm:ss

Reviewed-by: rriggs, vyazici, scolebourne
This commit is contained in:
Naoto Sato 2025-08-26 21:51:53 +00:00
parent 69645fd4ba
commit 1ff73cb2ec
3 changed files with 71 additions and 9 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -1199,8 +1199,10 @@ public final class DateTimeFormatter {
* When formatting, the instant will always be suffixed by 'Z' to indicate UTC.
* The second-of-minute is always output.
* The nano-of-second outputs zero, three, six or nine digits as necessary.
* When parsing, the behaviour of {@link DateTimeFormatterBuilder#appendOffsetId()}
* will be used to parse the offset, converting the instant to UTC as necessary.
* When parsing, the lenient mode behavior of
* {@link DateTimeFormatterBuilder#appendOffset(String, String)
* appendOffset("+HH", "Z")} will be used to parse the offset,
* converting the instant to UTC as necessary.
* The time to at least the seconds field is required.
* Fractional seconds from zero to nine are parsed.
* The localized decimal style is not used.

View File

@ -894,8 +894,10 @@ public final class DateTimeFormatterBuilder {
* {@link DateTimeFormatter#parsedLeapSecond()} for full details.
* <p>
* When formatting, the instant will always be suffixed by 'Z' to indicate UTC.
* When parsing, the behaviour of {@link DateTimeFormatterBuilder#appendOffsetId()}
* will be used to parse the offset, converting the instant to UTC as necessary.
* When parsing, the lenient mode behaviour of
* {@link DateTimeFormatterBuilder#appendOffset(String, String)
* appendOffset("+HH", "Z")} will be used to parse the offset,
* converting the instant to UTC as necessary.
* <p>
* An alternative to this method is to format/parse the instant as a single
* epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}.
@ -956,7 +958,7 @@ public final class DateTimeFormatterBuilder {
* Appends the zone offset, such as '+01:00', to the formatter.
* <p>
* This appends an instruction to format/parse the offset ID to the builder.
* This is equivalent to calling {@code appendOffset("+HH:mm:ss", "Z")}.
* This is equivalent to calling {@code appendOffset("+HH:MM:ss", "Z")}.
* See {@link #appendOffset(String, String)} for details on formatting
* and parsing.
*
@ -3887,7 +3889,8 @@ public final class DateTimeFormatterBuilder {
.appendValue(MINUTE_OF_HOUR, 2).appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
.appendFraction(NANO_OF_SECOND, minDigits, maxDigits, true)
.appendOffsetId()
.parseLenient()
.appendOffset("+HH", "Z")
.toFormatter().toPrinterParser(false);
DateTimeParseContext newContext = context.copy();
int pos = parser.parse(newContext, text, position);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -61,6 +61,9 @@ package test.java.time;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import org.testng.annotations.Test;
@ -70,7 +73,7 @@ import static org.testng.Assert.assertThrows;
/**
* Test Instant.
* @bug 8273369 8331202
* @bug 8273369 8331202 8364752
*/
@Test
public class TestInstant extends AbstractTest {
@ -151,4 +154,58 @@ public class TestInstant extends AbstractTest {
Instant.now().until(null);
});
}
@DataProvider
private Object[][] valid_instants() {
var I1 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("+02")).toInstant();
var I2 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("+02:02")).toInstant();
var I3 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("+02:02:02")).toInstant();
var I4 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("Z")).toInstant();
return new Object[][] {
{"2017-01-01T00:00:00.000+02", I1},
{"2017-01-01T00:00:00.000+0200", I1},
{"2017-01-01T00:00:00.000+02:00", I1},
{"2017-01-01T00:00:00.000+020000", I1},
{"2017-01-01T00:00:00.000+02:00:00", I1},
{"2017-01-01T00:00:00.000+0202", I2},
{"2017-01-01T00:00:00.000+02:02", I2},
{"2017-01-01T00:00:00.000+020202", I3},
{"2017-01-01T00:00:00.000+02:02:02", I3},
{"2017-01-01T00:00:00.000Z", I4},
};
}
@Test(dataProvider = "valid_instants")
public void test_parse_valid(String instant, Instant expected) {
assertEquals(Instant.parse(instant), expected);
}
@DataProvider
private Object[][] invalid_instants() {
return new Object[][] {
{"2017-01-01T00:00:00.000"},
{"2017-01-01T00:00:00.000+0"},
{"2017-01-01T00:00:00.000+0:"},
{"2017-01-01T00:00:00.000+02:"},
{"2017-01-01T00:00:00.000+020"},
{"2017-01-01T00:00:00.000+02:0"},
{"2017-01-01T00:00:00.000+02:0:"},
{"2017-01-01T00:00:00.000+02:00:"},
{"2017-01-01T00:00:00.000+02:000"},
{"2017-01-01T00:00:00.000+02:00:0"},
{"2017-01-01T00:00:00.000+02:00:0:"},
{"2017-01-01T00:00:00.000+0200000"},
{"2017-01-01T00:00:00.000+02:00:000"},
{"2017-01-01T00:00:00.000+02:00:00:"},
{"2017-01-01T00:00:00.000UTC"},
};
}
@Test(dataProvider = "invalid_instants")
public void test_parse_invalid(String instant) {
assertThrows(DateTimeParseException.class, () -> Instant.parse(instant));
}
}