Skip to content

Commit c1c543c

Browse files
committed
8210336: DateTimeFormatter predefined formatters should support short time zone offsets
Reviewed-by: jlu, rriggs
1 parent 96180b9 commit c1c543c

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2026, 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
@@ -817,6 +817,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
817817
* <li>The {@link #ISO_LOCAL_DATE}
818818
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
819819
* they will be handled even though this is not part of the ISO-8601 standard.
820+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
820821
* Parsing is case insensitive.
821822
* </ul>
822823
* <p>
@@ -829,7 +830,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
829830
ISO_OFFSET_DATE = new DateTimeFormatterBuilder()
830831
.parseCaseInsensitive()
831832
.append(ISO_LOCAL_DATE)
833+
.parseLenient()
832834
.appendOffsetId()
835+
.parseStrict()
833836
.toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
834837
}
835838

@@ -846,6 +849,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
846849
* <li>If the offset is not available then the format is complete.
847850
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
848851
* they will be handled even though this is not part of the ISO-8601 standard.
852+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
849853
* Parsing is case insensitive.
850854
* </ul>
851855
* <p>
@@ -862,7 +866,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
862866
.parseCaseInsensitive()
863867
.append(ISO_LOCAL_DATE)
864868
.optionalStart()
869+
.parseLenient()
865870
.appendOffsetId()
871+
.parseStrict()
866872
.toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
867873
}
868874

@@ -919,6 +925,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
919925
* <li>The {@link #ISO_LOCAL_TIME}
920926
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
921927
* they will be handled even though this is not part of the ISO-8601 standard.
928+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
922929
* Parsing is case insensitive.
923930
* </ul>
924931
* <p>
@@ -930,7 +937,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
930937
ISO_OFFSET_TIME = new DateTimeFormatterBuilder()
931938
.parseCaseInsensitive()
932939
.append(ISO_LOCAL_TIME)
940+
.parseLenient()
933941
.appendOffsetId()
942+
.parseStrict()
934943
.toFormatter(ResolverStyle.STRICT, null);
935944
}
936945

@@ -947,6 +956,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
947956
* <li>If the offset is not available then the format is complete.
948957
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
949958
* they will be handled even though this is not part of the ISO-8601 standard.
959+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
950960
* Parsing is case insensitive.
951961
* </ul>
952962
* <p>
@@ -962,7 +972,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
962972
.parseCaseInsensitive()
963973
.append(ISO_LOCAL_TIME)
964974
.optionalStart()
975+
.parseLenient()
965976
.appendOffsetId()
977+
.parseStrict()
966978
.toFormatter(ResolverStyle.STRICT, null);
967979
}
968980

@@ -1075,6 +1087,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
10751087
* <li>If the offset is not available to format or parse then the format is complete.
10761088
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
10771089
* they will be handled even though this is not part of the ISO-8601 standard.
1090+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
10781091
* <li>If the zone ID is not available or is a {@code ZoneOffset} then the format is complete.
10791092
* <li>An open square bracket '['.
10801093
* <li>The {@link ZoneId#getId() zone ID}. This is not part of the ISO-8601 standard.
@@ -1094,7 +1107,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
10941107
ISO_DATE_TIME = new DateTimeFormatterBuilder()
10951108
.append(ISO_LOCAL_DATE_TIME)
10961109
.optionalStart()
1110+
.parseLenient()
10971111
.appendOffsetId()
1112+
.parseStrict()
10981113
.optionalStart()
10991114
.appendLiteral('[')
11001115
.parseCaseSensitive()
@@ -1121,6 +1136,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
11211136
* <li>If the offset is not available to format or parse then the format is complete.
11221137
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
11231138
* they will be handled even though this is not part of the ISO-8601 standard.
1139+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
11241140
* Parsing is case insensitive.
11251141
* </ul>
11261142
* <p>
@@ -1139,7 +1155,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
11391155
.appendLiteral('-')
11401156
.appendValue(DAY_OF_YEAR, 3)
11411157
.optionalStart()
1158+
.parseLenient()
11421159
.appendOffsetId()
1160+
.parseStrict()
11431161
.toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
11441162
}
11451163

@@ -1165,6 +1183,7 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
11651183
* <li>If the offset is not available to format or parse then the format is complete.
11661184
* <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
11671185
* they will be handled even though this is not part of the ISO-8601 standard.
1186+
* The offset parsing is lenient, which allows the minutes and seconds to be optional.
11681187
* Parsing is case insensitive.
11691188
* </ul>
11701189
* <p>
@@ -1185,7 +1204,9 @@ public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
11851204
.appendLiteral('-')
11861205
.appendValue(DAY_OF_WEEK, 1)
11871206
.optionalStart()
1207+
.parseLenient()
11881208
.appendOffsetId()
1209+
.parseStrict()
11891210
.toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
11901211
}
11911212

test/jdk/java/time/tck/java/time/TCKOffsetTime.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,6 @@ Object[][] provider_sampleBadParse() {
421421
{"00;00"},
422422
{"12-00"},
423423
{"-01:00"},
424-
{"00:00:00-09"},
425424
{"00:00:00,09"},
426425
{"00:00:abs"},
427426
{"11"},
@@ -436,6 +435,11 @@ public void factory_parse_invalidText(String unparsable) {
436435
Assertions.assertThrows(DateTimeParseException.class, () -> OffsetTime.parse(unparsable));
437436
}
438437

438+
@Test
439+
public void factory_parse_hourOnlyOffset() {
440+
Assertions.assertDoesNotThrow(() -> OffsetTime.parse("00:00:00-09"));
441+
}
442+
439443
//-----------------------------------------------------------------------s
440444
@Test
441445
public void factory_parse_illegalHour() {

test/jdk/java/time/test/java/time/format/TestDateTimeFormatter.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2026, 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
@@ -59,6 +59,7 @@
5959
*/
6060
package test.java.time.format;
6161

62+
import static java.time.format.DateTimeFormatter.*;
6263
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
6364

6465
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -93,15 +94,19 @@
9394
import java.time.temporal.TemporalAccessor;
9495
import java.util.Locale;
9596
import java.util.function.Function;
97+
import java.util.stream.Stream;
9698

9799
import org.junit.jupiter.api.Test;
98100
import org.junit.jupiter.api.TestInstance;
99101
import org.junit.jupiter.params.ParameterizedTest;
102+
import org.junit.jupiter.params.provider.Arguments;
100103
import org.junit.jupiter.params.provider.MethodSource;
101104

105+
import static org.junit.jupiter.api.Assertions.assertEquals;
106+
102107
/**
103108
* Test DateTimeFormatter.
104-
* @bug 8085887 8293146
109+
* @bug 8085887 8293146 8210336
105110
*/
106111
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
107112
public class TestDateTimeFormatter {
@@ -333,4 +338,49 @@ public void test_week_53(String weekDate, Locale locale, LocalDate expected) {
333338
assertThrows(DateTimeException.class, () -> LocalDate.parse(weekDate, f));
334339
}
335340
}
341+
342+
private static Stream<Arguments> data_iso_short_offset_parse() {
343+
return Stream.of(
344+
Arguments.of("20260123-01", BASIC_ISO_DATE, "20260123-0100"),
345+
Arguments.of("20260123+00", BASIC_ISO_DATE, "20260123Z"),
346+
Arguments.of("20260123-00", BASIC_ISO_DATE, "20260123Z"),
347+
Arguments.of("2026-01-23-01", ISO_DATE, "2026-01-23-01:00"),
348+
Arguments.of("2026-01-23+00", ISO_DATE, "2026-01-23Z"),
349+
Arguments.of("2026-01-23-00", ISO_DATE, "2026-01-23Z"),
350+
Arguments.of("2026-01-23T11:30:59-01", ISO_DATE_TIME, "2026-01-23T11:30:59-01:00"),
351+
Arguments.of("2026-01-23T11:30:59+00", ISO_DATE_TIME, "2026-01-23T11:30:59Z"),
352+
Arguments.of("2026-01-23T11:30:59-00", ISO_DATE_TIME, "2026-01-23T11:30:59Z"),
353+
Arguments.of("11:30:59-01", ISO_TIME, "11:30:59-01:00"),
354+
Arguments.of("11:30:59+00", ISO_TIME, "11:30:59Z"),
355+
Arguments.of("11:30:59-00", ISO_TIME, "11:30:59Z"),
356+
Arguments.of("2026-01-23-01", ISO_OFFSET_DATE, "2026-01-23-01:00"),
357+
Arguments.of("2026-01-23+00", ISO_OFFSET_DATE, "2026-01-23Z"),
358+
Arguments.of("2026-01-23-00", ISO_OFFSET_DATE, "2026-01-23Z"),
359+
Arguments.of("2026-01-23T11:30:59-01", ISO_OFFSET_DATE_TIME, "2026-01-23T11:30:59-01:00"),
360+
Arguments.of("2026-01-23T11:30:59+00", ISO_OFFSET_DATE_TIME, "2026-01-23T11:30:59Z"),
361+
Arguments.of("2026-01-23T11:30:59-00", ISO_OFFSET_DATE_TIME, "2026-01-23T11:30:59Z"),
362+
Arguments.of("11:30:59-01", ISO_OFFSET_TIME, "11:30:59-01:00"),
363+
Arguments.of("11:30:59+00", ISO_OFFSET_TIME, "11:30:59Z"),
364+
Arguments.of("11:30:59-00", ISO_OFFSET_TIME, "11:30:59Z"),
365+
Arguments.of("2026-023-01", ISO_ORDINAL_DATE, "2026-023-01:00"),
366+
Arguments.of("2026-023+00", ISO_ORDINAL_DATE, "2026-023Z"),
367+
Arguments.of("2026-023-00", ISO_ORDINAL_DATE, "2026-023Z"),
368+
Arguments.of("2026-W04-5-01", ISO_WEEK_DATE, "2026-W04-5-01:00"),
369+
Arguments.of("2026-W04-5+00", ISO_WEEK_DATE, "2026-W04-5Z"),
370+
Arguments.of("2026-W04-5-00", ISO_WEEK_DATE, "2026-W04-5Z"),
371+
Arguments.of("2026-01-23T11:30:59-01", ISO_ZONED_DATE_TIME, "2026-01-23T11:30:59-01:00"),
372+
Arguments.of("2026-01-23T11:30:59+00", ISO_ZONED_DATE_TIME, "2026-01-23T11:30:59Z"),
373+
Arguments.of("2026-01-23T11:30:59-00", ISO_ZONED_DATE_TIME, "2026-01-23T11:30:59Z"),
374+
Arguments.of("2026-01-23T11:30:59-01", ISO_INSTANT, "2026-01-23T12:30:59Z"),
375+
Arguments.of("2026-01-23T11:30:59+00", ISO_INSTANT, "2026-01-23T11:30:59Z"),
376+
Arguments.of("2026-01-23T11:30:59-00", ISO_INSTANT, "2026-01-23T11:30:59Z")
377+
);
378+
}
379+
380+
// Checks if predefined ISO formatters can parse hour-only offsets
381+
@ParameterizedTest
382+
@MethodSource("data_iso_short_offset_parse")
383+
public void test_iso_short_offset_parse(String text, DateTimeFormatter formatter, String expected) {
384+
assertEquals(expected, formatter.format(formatter.parse(text)));
385+
}
336386
}

0 commit comments

Comments
 (0)