diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java index 7127e27729449..108f1e1eef893 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -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 static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) { * 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. diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index f5eeeec563cf5..43a79cd85ee43 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -894,8 +894,10 @@ public Iterator> getTextIterator(TemporalField field, * {@link DateTimeFormatter#parsedLeapSecond()} for full details. *

* 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. *

* 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 DateTimeFormatterBuilder appendInstant(int fractionalDigits) { * Appends the zone offset, such as '+01:00', to the formatter. *

* 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 int parse(DateTimeParseContext context, CharSequence text, int position) .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); diff --git a/test/jdk/java/time/test/java/time/TestInstant.java b/test/jdk/java/time/test/java/time/TestInstant.java index 1879334cb2a4c..8dbd951bde1a9 100644 --- a/test/jdk/java/time/test/java/time/TestInstant.java +++ b/test/jdk/java/time/test/java/time/TestInstant.java @@ -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 @@ 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 @@ /** * Test Instant. - * @bug 8273369 8331202 + * @bug 8273369 8331202 8364752 */ @Test public class TestInstant extends AbstractTest { @@ -151,4 +154,58 @@ public void test_until_1arg_NPE() { 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)); + } }