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 16d7193c556..9368cf54afd 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, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -817,6 +817,7 @@ public final class DateTimeFormatter { *
  • The {@link #ISO_LOCAL_DATE} *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. * Parsing is case insensitive. * *

    @@ -829,7 +830,9 @@ public final class DateTimeFormatter { ISO_OFFSET_DATE = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(ISO_LOCAL_DATE) + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE); } @@ -846,6 +849,7 @@ public final class DateTimeFormatter { *

  • If the offset is not available then the format is complete. *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. * Parsing is case insensitive. * *

    @@ -862,7 +866,9 @@ public final class DateTimeFormatter { .parseCaseInsensitive() .append(ISO_LOCAL_DATE) .optionalStart() + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE); } @@ -919,6 +925,7 @@ public final class DateTimeFormatter { *

  • The {@link #ISO_LOCAL_TIME} *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. * Parsing is case insensitive. * *

    @@ -930,7 +937,9 @@ public final class DateTimeFormatter { ISO_OFFSET_TIME = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(ISO_LOCAL_TIME) + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, null); } @@ -947,6 +956,7 @@ public final class DateTimeFormatter { *

  • If the offset is not available then the format is complete. *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. * Parsing is case insensitive. * *

    @@ -962,7 +972,9 @@ public final class DateTimeFormatter { .parseCaseInsensitive() .append(ISO_LOCAL_TIME) .optionalStart() + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, null); } @@ -1075,6 +1087,7 @@ public final class DateTimeFormatter { *

  • If the offset is not available to format or parse then the format is complete. *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. *
  • If the zone ID is not available or is a {@code ZoneOffset} then the format is complete. *
  • An open square bracket '['. *
  • The {@link ZoneId#getId() zone ID}. This is not part of the ISO-8601 standard. @@ -1094,7 +1107,9 @@ public final class DateTimeFormatter { ISO_DATE_TIME = new DateTimeFormatterBuilder() .append(ISO_LOCAL_DATE_TIME) .optionalStart() + .parseLenient() .appendOffsetId() + .parseStrict() .optionalStart() .appendLiteral('[') .parseCaseSensitive() @@ -1121,6 +1136,7 @@ public final class DateTimeFormatter { *
  • If the offset is not available to format or parse then the format is complete. *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. * Parsing is case insensitive. * *

    @@ -1139,7 +1155,9 @@ public final class DateTimeFormatter { .appendLiteral('-') .appendValue(DAY_OF_YEAR, 3) .optionalStart() + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE); } @@ -1165,6 +1183,7 @@ public final class DateTimeFormatter { *

  • If the offset is not available to format or parse then the format is complete. *
  • The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then * they will be handled even though this is not part of the ISO-8601 standard. + * The offset parsing is lenient, which allows the minutes and seconds to be optional. * Parsing is case insensitive. * *

    @@ -1185,7 +1204,9 @@ public final class DateTimeFormatter { .appendLiteral('-') .appendValue(DAY_OF_WEEK, 1) .optionalStart() + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE); } diff --git a/test/jdk/java/time/tck/java/time/TCKOffsetTime.java b/test/jdk/java/time/tck/java/time/TCKOffsetTime.java index 7ea9504edbc..4c16ce48247 100644 --- a/test/jdk/java/time/tck/java/time/TCKOffsetTime.java +++ b/test/jdk/java/time/tck/java/time/TCKOffsetTime.java @@ -421,7 +421,6 @@ public class TCKOffsetTime extends AbstractDateTimeTest { {"00;00"}, {"12-00"}, {"-01:00"}, - {"00:00:00-09"}, {"00:00:00,09"}, {"00:00:abs"}, {"11"}, diff --git a/test/jdk/java/time/test/java/time/format/TestDateTimeFormatter.java b/test/jdk/java/time/test/java/time/format/TestDateTimeFormatter.java index 2ccc0da8f58..7ca7a6ab559 100644 --- a/test/jdk/java/time/test/java/time/format/TestDateTimeFormatter.java +++ b/test/jdk/java/time/test/java/time/format/TestDateTimeFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -59,6 +59,7 @@ */ package test.java.time.format; +import static java.time.format.DateTimeFormatter.*; import static java.time.temporal.ChronoField.DAY_OF_MONTH; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -93,15 +94,19 @@ import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; import java.util.Locale; import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * Test DateTimeFormatter. - * @bug 8085887 8293146 + * @bug 8085887 8293146 8210336 */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestDateTimeFormatter { @@ -333,4 +338,27 @@ public class TestDateTimeFormatter { assertThrows(DateTimeException.class, () -> LocalDate.parse(weekDate, f)); } } + + private static Stream data_iso_short_offset_parse() { + return Stream.of( + Arguments.of("20260123-01", BASIC_ISO_DATE), + Arguments.of("2026-01-23-01", ISO_DATE), + Arguments.of("2026-01-23T11:30:59-01", ISO_DATE_TIME), + Arguments.of("11:30:59-01", ISO_TIME), + Arguments.of("2026-01-23-01", ISO_OFFSET_DATE), + Arguments.of("2026-01-23T11:30:59-01", ISO_OFFSET_DATE_TIME), + Arguments.of("11:30:59-01", ISO_OFFSET_TIME), + Arguments.of("2026-023-01", ISO_ORDINAL_DATE), + Arguments.of("2026-W04-5-01", ISO_WEEK_DATE) + ); + } + + // Checks if predefined ISO formatters can parse hour-only offsets + @ParameterizedTest + @MethodSource("data_iso_short_offset_parse") + public void test_iso_short_offset_parse(String text, DateTimeFormatter formatter) { + var formatted = formatter.format(formatter.parse(text)); + var expected = text + (formatter == BASIC_ISO_DATE ? "00" : ":00"); + assertEquals(expected, formatted); + } }