Skip to content

Commit 1ff73cb

Browse files
committed
8364752: java.time.Instant should be able to parse ISO 8601 offsets of the form HH:mm:ss
Reviewed-by: rriggs, vyazici, scolebourne
1 parent 69645fd commit 1ff73cb

File tree

3 files changed

+71
-9
lines changed

3 files changed

+71
-9
lines changed

src/java.base/share/classes/java/time/format/DateTimeFormatter.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -1199,8 +1199,10 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
11991199
* When formatting, the instant will always be suffixed by 'Z' to indicate UTC.
12001200
* The second-of-minute is always output.
12011201
* The nano-of-second outputs zero, three, six or nine digits as necessary.
1202-
* When parsing, the behaviour of {@link DateTimeFormatterBuilder#appendOffsetId()}
1203-
* will be used to parse the offset, converting the instant to UTC as necessary.
1202+
* When parsing, the lenient mode behavior of
1203+
* {@link DateTimeFormatterBuilder#appendOffset(String, String)
1204+
* appendOffset("+HH", "Z")} will be used to parse the offset,
1205+
* converting the instant to UTC as necessary.
12041206
* The time to at least the seconds field is required.
12051207
* Fractional seconds from zero to nine are parsed.
12061208
* The localized decimal style is not used.

src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -894,8 +894,10 @@ public Iterator<Entry<String, Long>> getTextIterator(TemporalField field,
894894
* {@link DateTimeFormatter#parsedLeapSecond()} for full details.
895895
* <p>
896896
* When formatting, the instant will always be suffixed by 'Z' to indicate UTC.
897-
* When parsing, the behaviour of {@link DateTimeFormatterBuilder#appendOffsetId()}
898-
* will be used to parse the offset, converting the instant to UTC as necessary.
897+
* When parsing, the lenient mode behaviour of
898+
* {@link DateTimeFormatterBuilder#appendOffset(String, String)
899+
* appendOffset("+HH", "Z")} will be used to parse the offset,
900+
* converting the instant to UTC as necessary.
899901
* <p>
900902
* An alternative to this method is to format/parse the instant as a single
901903
* epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}.
@@ -956,7 +958,7 @@ public DateTimeFormatterBuilder appendInstant(int fractionalDigits) {
956958
* Appends the zone offset, such as '+01:00', to the formatter.
957959
* <p>
958960
* This appends an instruction to format/parse the offset ID to the builder.
959-
* This is equivalent to calling {@code appendOffset("+HH:mm:ss", "Z")}.
961+
* This is equivalent to calling {@code appendOffset("+HH:MM:ss", "Z")}.
960962
* See {@link #appendOffset(String, String)} for details on formatting
961963
* and parsing.
962964
*
@@ -3887,7 +3889,8 @@ public int parse(DateTimeParseContext context, CharSequence text, int position)
38873889
.appendValue(MINUTE_OF_HOUR, 2).appendLiteral(':')
38883890
.appendValue(SECOND_OF_MINUTE, 2)
38893891
.appendFraction(NANO_OF_SECOND, minDigits, maxDigits, true)
3890-
.appendOffsetId()
3892+
.parseLenient()
3893+
.appendOffset("+HH", "Z")
38913894
.toFormatter().toPrinterParser(false);
38923895
DateTimeParseContext newContext = context.copy();
38933896
int pos = parser.parse(newContext, text, position);

test/jdk/java/time/test/java/time/TestInstant.java

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -61,6 +61,9 @@
6161

6262
import java.time.Duration;
6363
import java.time.Instant;
64+
import java.time.OffsetDateTime;
65+
import java.time.ZoneOffset;
66+
import java.time.format.DateTimeParseException;
6467
import java.time.temporal.ChronoUnit;
6568

6669
import org.testng.annotations.Test;
@@ -70,7 +73,7 @@
7073

7174
/**
7275
* Test Instant.
73-
* @bug 8273369 8331202
76+
* @bug 8273369 8331202 8364752
7477
*/
7578
@Test
7679
public class TestInstant extends AbstractTest {
@@ -151,4 +154,58 @@ public void test_until_1arg_NPE() {
151154
Instant.now().until(null);
152155
});
153156
}
157+
158+
@DataProvider
159+
private Object[][] valid_instants() {
160+
var I1 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("+02")).toInstant();
161+
var I2 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("+02:02")).toInstant();
162+
var I3 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("+02:02:02")).toInstant();
163+
var I4 = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.of("Z")).toInstant();
164+
return new Object[][] {
165+
{"2017-01-01T00:00:00.000+02", I1},
166+
{"2017-01-01T00:00:00.000+0200", I1},
167+
{"2017-01-01T00:00:00.000+02:00", I1},
168+
{"2017-01-01T00:00:00.000+020000", I1},
169+
{"2017-01-01T00:00:00.000+02:00:00", I1},
170+
171+
{"2017-01-01T00:00:00.000+0202", I2},
172+
{"2017-01-01T00:00:00.000+02:02", I2},
173+
174+
{"2017-01-01T00:00:00.000+020202", I3},
175+
{"2017-01-01T00:00:00.000+02:02:02", I3},
176+
177+
{"2017-01-01T00:00:00.000Z", I4},
178+
};
179+
}
180+
181+
@Test(dataProvider = "valid_instants")
182+
public void test_parse_valid(String instant, Instant expected) {
183+
assertEquals(Instant.parse(instant), expected);
184+
}
185+
186+
@DataProvider
187+
private Object[][] invalid_instants() {
188+
return new Object[][] {
189+
{"2017-01-01T00:00:00.000"},
190+
{"2017-01-01T00:00:00.000+0"},
191+
{"2017-01-01T00:00:00.000+0:"},
192+
{"2017-01-01T00:00:00.000+02:"},
193+
{"2017-01-01T00:00:00.000+020"},
194+
{"2017-01-01T00:00:00.000+02:0"},
195+
{"2017-01-01T00:00:00.000+02:0:"},
196+
{"2017-01-01T00:00:00.000+02:00:"},
197+
{"2017-01-01T00:00:00.000+02:000"},
198+
{"2017-01-01T00:00:00.000+02:00:0"},
199+
{"2017-01-01T00:00:00.000+02:00:0:"},
200+
{"2017-01-01T00:00:00.000+0200000"},
201+
{"2017-01-01T00:00:00.000+02:00:000"},
202+
{"2017-01-01T00:00:00.000+02:00:00:"},
203+
{"2017-01-01T00:00:00.000UTC"},
204+
};
205+
}
206+
207+
@Test(dataProvider = "invalid_instants")
208+
public void test_parse_invalid(String instant) {
209+
assertThrows(DateTimeParseException.class, () -> Instant.parse(instant));
210+
}
154211
}

0 commit comments

Comments
 (0)