From 7fc267a37adbc75db4861796d59e7ab1df39cf12 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Fri, 8 Aug 2025 13:47:35 -0700 Subject: [PATCH 1/8] initial commit --- .../classes/java/time/format/DateTimeFormatterBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..150cd314e7d23 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -956,7 +956,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. * From 5658f2556b6bc4141d66834ba9929af1a22bbddb Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Thu, 14 Aug 2025 16:24:40 -0700 Subject: [PATCH 2/8] allow hour-only offsets --- .../java/time/format/DateTimeFormatter.java | 6 ++-- .../time/format/DateTimeFormatterBuilder.java | 8 +++-- .../java/time/test/java/time/TestInstant.java | 36 +++++++++++++++++-- 3 files changed, 43 insertions(+), 7 deletions(-) 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..f5298acabbfcf 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -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 behaviour of + * {@link DateTimeFormatterBuilder#appendOffset(String, String) + * appendOffset("+HH:mm:ss", "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 150cd314e7d23..98dda3a3bbe84 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 behaviour of + * {@link DateTimeFormatterBuilder#appendOffset(String, String) + * appendOffset("+HH:mm:ss", "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)}. @@ -3887,7 +3889,7 @@ 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() + .appendOffset("+HH:mm:ss", "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..37f82586f1471 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,7 @@ import java.time.Duration; import java.time.Instant; +import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import org.testng.annotations.Test; @@ -70,7 +71,7 @@ /** * Test Instant. - * @bug 8273369 8331202 + * @bug 8273369 8331202 8364752 */ @Test public class TestInstant extends AbstractTest { @@ -151,4 +152,35 @@ public void test_until_1arg_NPE() { Instant.now().until(null); }); } + + @DataProvider + private Object[][] valid_instants() { + return new Object[][] { + {"2017-01-01T00:00:00.000+02"}, + {"2017-01-01T00:00:00.000+02:00"}, + {"2017-01-01T00:00:00.000+02:00:00"}, + {"2017-01-01T00:00:00.000Z"}, + }; + } + + @Test(dataProvider = "valid_instants") + public void test_parse_valid(String instant) { + Instant.parse(instant); + } + + @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+020"}, + {"2017-01-01T00:00:00.000+02:0"}, + {"2017-01-01T00:00:00.000UTC"}, + }; + } + + @Test(dataProvider = "invalid_instants") + public void test_parse_invalid(String instant) { + assertThrows(DateTimeParseException.class, () -> Instant.parse(instant)); + } } From 048f5eabe157e04a0ac615ccc236ba43598e5b14 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Thu, 14 Aug 2025 16:34:28 -0700 Subject: [PATCH 3/8] allow all ISO 8601 offsets --- .../share/classes/java/time/format/DateTimeFormatter.java | 2 +- .../classes/java/time/format/DateTimeFormatterBuilder.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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 f5298acabbfcf..bfba98780043c 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -1201,7 +1201,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) { * The nano-of-second outputs zero, three, six or nine digits as necessary. * When parsing, the behaviour of * {@link DateTimeFormatterBuilder#appendOffset(String, String) - * appendOffset("+HH:mm:ss", "Z")} will be used to parse the offset, + * appendOffset("+HH", "Z")} in lenient mode 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. 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 98dda3a3bbe84..64b9c5c124346 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -896,8 +896,8 @@ public Iterator> getTextIterator(TemporalField field, * When formatting, the instant will always be suffixed by 'Z' to indicate UTC. * When parsing, the behaviour of * {@link DateTimeFormatterBuilder#appendOffset(String, String) - * appendOffset("+HH:mm:ss", "Z")} will be used to parse the offset, converting - * the instant to UTC as necessary. + * appendOffset("+HH", "Z")} in lenient mode 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)}. @@ -3889,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) - .appendOffset("+HH:mm:ss", "Z") + .parseLenient() + .appendOffset("+HH", "Z") .toFormatter().toPrinterParser(false); DateTimeParseContext newContext = context.copy(); int pos = parser.parse(newContext, text, position); From fa198b882880058cbf50454cd53849089d0b1b25 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Thu, 14 Aug 2025 16:46:38 -0700 Subject: [PATCH 4/8] test cases --- test/jdk/java/time/test/java/time/TestInstant.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/jdk/java/time/test/java/time/TestInstant.java b/test/jdk/java/time/test/java/time/TestInstant.java index 37f82586f1471..4968c18654ec9 100644 --- a/test/jdk/java/time/test/java/time/TestInstant.java +++ b/test/jdk/java/time/test/java/time/TestInstant.java @@ -157,7 +157,9 @@ public void test_until_1arg_NPE() { private Object[][] valid_instants() { return new Object[][] { {"2017-01-01T00:00:00.000+02"}, + {"2017-01-01T00:00:00.000+0200"}, {"2017-01-01T00:00:00.000+02:00"}, + {"2017-01-01T00:00:00.000+020000"}, {"2017-01-01T00:00:00.000+02:00:00"}, {"2017-01-01T00:00:00.000Z"}, }; @@ -173,8 +175,18 @@ 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"}, }; } From 7523e34412b539b602b9791f444c679ef64c4308 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Fri, 15 Aug 2025 09:47:43 -0700 Subject: [PATCH 5/8] copyright year update --- .../share/classes/java/time/format/DateTimeFormatter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bfba98780043c..5e0ff3378ddd3 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 From 8bc222afb3efda11f82d9f420fcbdf7bef59b442 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Mon, 18 Aug 2025 12:09:23 -0700 Subject: [PATCH 6/8] Update src/java.base/share/classes/java/time/format/DateTimeFormatter.java Right. Changing to your suggested wording Co-authored-by: Roger Riggs --- .../share/classes/java/time/format/DateTimeFormatter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5e0ff3378ddd3..108f1e1eef893 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -1199,9 +1199,9 @@ 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 + * When parsing, the lenient mode behavior of * {@link DateTimeFormatterBuilder#appendOffset(String, String) - * appendOffset("+HH", "Z")} in lenient mode will be used to parse the offset, + * 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. From cd1507d8be64bc3f44b19daf1d58a321beaf49d1 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Mon, 18 Aug 2025 12:12:33 -0700 Subject: [PATCH 7/8] Added non-zero offset test cases --- .../java/time/test/java/time/TestInstant.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/test/jdk/java/time/test/java/time/TestInstant.java b/test/jdk/java/time/test/java/time/TestInstant.java index 4968c18654ec9..8dbd951bde1a9 100644 --- a/test/jdk/java/time/test/java/time/TestInstant.java +++ b/test/jdk/java/time/test/java/time/TestInstant.java @@ -61,6 +61,8 @@ 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; @@ -155,19 +157,30 @@ public void test_until_1arg_NPE() { @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"}, - {"2017-01-01T00:00:00.000+0200"}, - {"2017-01-01T00:00:00.000+02:00"}, - {"2017-01-01T00:00:00.000+020000"}, - {"2017-01-01T00:00:00.000+02:00:00"}, - {"2017-01-01T00:00:00.000Z"}, + {"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.parse(instant); + public void test_parse_valid(String instant, Instant expected) { + assertEquals(Instant.parse(instant), expected); } @DataProvider From 0d78a520a2849e09d48bb2c36f2f67dafb07984d Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Mon, 18 Aug 2025 12:14:04 -0700 Subject: [PATCH 8/8] DateTimeFormatterBuilder wording --- .../classes/java/time/format/DateTimeFormatterBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 64b9c5c124346..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,9 +894,9 @@ 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 + * When parsing, the lenient mode behaviour of * {@link DateTimeFormatterBuilder#appendOffset(String, String) - * appendOffset("+HH", "Z")} in lenient mode will be used to parse the offset, + * 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