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 c94b123b26c9d..063b6e14a00f5 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, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2021, 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 @@ -81,6 +81,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.chrono.ChronoLocalDateTime; +import java.time.chrono.ChronoZonedDateTime; import java.time.chrono.Chronology; import java.time.chrono.IsoChronology; import java.time.format.DateTimeFormatterBuilder.CompositePrinterParser; @@ -373,15 +374,15 @@ * letters throws {@code IllegalArgumentException}. *
* Zone names: This outputs the display name of the time-zone ID. If the - * pattern letter is 'z' the output is the daylight savings aware zone name. + * pattern letter is 'z' the output is the daylight saving aware zone name. * If there is insufficient information to determine whether DST applies, - * the name ignoring daylight savings time will be used. + * the name ignoring daylight saving time will be used. * If the count of letters is one, two or three, then the short name is output. * If the count of letters is four, then the full name is output. * Five or more letters throws {@code IllegalArgumentException}. *
* If the pattern letter is 'v' the output provides the zone name ignoring - * daylight savings time. If the count of letters is one, then the short name is output. + * daylight saving time. If the count of letters is one, then the short name is output. * If the count of letters is four, then the full name is output. * Two, three and five or more letters throw {@code IllegalArgumentException}. *
@@ -502,7 +503,10 @@
* {@code LocalDateTime} to form the instant, with any zone ignored.
* If a {@code ZoneId} was parsed without an offset then the zone will be
* combined with the {@code LocalDateTime} to form the instant using the rules
- * of {@link ChronoLocalDateTime#atZone(ZoneId)}.
+ * of {@link ChronoLocalDateTime#atZone(ZoneId)}. If the {@code ZoneId} was
+ * parsed from a zone name that indicates whether daylight saving time is in
+ * operation or not, then that fact will be used to select the correct offset
+ * at the local time-line overlap.
*
*
* @implSpec
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 7af964f106b8a..40c0de53d3f67 100644
--- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java
+++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java
@@ -4324,9 +4324,10 @@ static final class ZoneTextPrinterParser extends ZoneIdPrinterParser {
}
}
- private static final int STD = 0;
- private static final int DST = 1;
- private static final int GENERIC = 2;
+ static final int UNDEFINED = -1;
+ static final int STD = 0;
+ static final int DST = 1;
+ static final int GENERIC = 2;
private static final Map
+ * This stores the zone name type that has been parsed.
+ * The parsed type should either be;
+ *
+ *
+ *
+ * @param type the parsed zone name type
+ */
+ void setParsedZoneNameType(int type) {
+ currentParsed().zoneNameType = type;
+ }
+
/**
* Stores the parsed leap second.
*/
diff --git a/src/java.base/share/classes/java/time/format/Parsed.java b/src/java.base/share/classes/java/time/format/Parsed.java
index 567c2700a1558..1ec956dfa2cd3 100644
--- a/src/java.base/share/classes/java/time/format/Parsed.java
+++ b/src/java.base/share/classes/java/time/format/Parsed.java
@@ -87,6 +87,7 @@
import java.time.Period;
import java.time.ZoneId;
import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
@@ -132,6 +133,10 @@ final class Parsed implements TemporalAccessor {
* The parsed zone.
*/
ZoneId zone;
+ /**
+ * The parsed zone name type.
+ */
+ int zoneNameType = DateTimeFormatterBuilder.ZoneTextPrinterParser.UNDEFINED;
/**
* The parsed chronology.
*/
@@ -175,6 +180,7 @@ Parsed copy() {
Parsed cloned = new Parsed();
cloned.fieldValues.putAll(this.fieldValues);
cloned.zone = this.zone;
+ cloned.zoneNameType = this.zoneNameType;
cloned.chrono = this.chrono;
cloned.leapSecond = this.leapSecond;
cloned.dayPeriod = this.dayPeriod;
@@ -652,8 +658,12 @@ private void resolveInstant() {
fieldValues.put(INSTANT_SECONDS, instant);
} else {
if (zone != null) {
- long instant = date.atTime(time).atZone(zone).toEpochSecond();
- fieldValues.put(INSTANT_SECONDS, instant);
+ var czdt = date.atTime(time).atZone(zone);
+ if (zoneNameType == DateTimeFormatterBuilder.ZoneTextPrinterParser.STD ||
+ zoneNameType == DateTimeFormatterBuilder.ZoneTextPrinterParser.GENERIC) {
+ czdt = czdt.withLaterOffsetAtOverlap();
+ }
+ fieldValues.put(INSTANT_SECONDS, czdt.toEpochSecond());
}
}
}
@@ -718,6 +728,7 @@ public String toString() {
buf.append(fieldValues).append(',').append(chrono);
if (zone != null) {
buf.append(',').append(zone);
+ buf.append(',').append(zoneNameType);
}
if (date != null || time != null) {
buf.append(" resolved to ");
diff --git a/test/jdk/java/time/test/java/time/format/TestZoneTextPrinterParser.java b/test/jdk/java/time/test/java/time/format/TestZoneTextPrinterParser.java
index 77ccf37ad5673..7981d7699077b 100644
--- a/test/jdk/java/time/test/java/time/format/TestZoneTextPrinterParser.java
+++ b/test/jdk/java/time/test/java/time/format/TestZoneTextPrinterParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021, 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
@@ -24,10 +24,12 @@
package test.java.time.format;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
import java.text.DateFormatSymbols;
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
import java.time.format.DecimalStyle;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
@@ -49,7 +51,7 @@
/*
* @test
- * @bug 8081022 8151876 8166875 8189784 8206980
+ * @bug 8081022 8151876 8166875 8177819 8189784 8206980 8277049
* @key randomness
*/
@@ -236,4 +238,37 @@ private DateTimeFormatter getFormatter(Locale locale, TextStyle style, boolean c
.withDecimalStyle(DecimalStyle.of(locale));
}
+ @DataProvider(name="roundTripAtOverlap")
+ Object[][] data_roundTripAtOverlap() {
+ return new Object[][] {
+ {"yyyy-MM-dd HH:mm:ss.SSS z", "2021-10-31 02:30:00.000 CET"},
+ {"yyyy-MM-dd HH:mm:ss.SSS z", "2021-10-31 02:30:00.000 CEST"},
+ {"yyyy-MM-dd HH:mm:ss.SSS z", "2021-11-07 01:30:00.000 EST"},
+ {"yyyy-MM-dd HH:mm:ss.SSS z", "2021-11-07 01:30:00.000 EDT"},
+ {"yyyy-MM-dd HH:mm:ss.SSS zzzz", "2021-10-31 02:30:00.000 Central European Standard Time"},
+ {"yyyy-MM-dd HH:mm:ss.SSS zzzz", "2021-10-31 02:30:00.000 Central European Summer Time"},
+ {"yyyy-MM-dd HH:mm:ss.SSS zzzz", "2021-11-07 01:30:00.000 Eastern Standard Time"},
+ {"yyyy-MM-dd HH:mm:ss.SSS zzzz", "2021-11-07 01:30:00.000 Eastern Daylight Time"},
+
+ {"yyyy-MM-dd HH:mm:ss.SSS v", "2021-10-31 02:30:00.000 CET"},
+ {"yyyy-MM-dd HH:mm:ss.SSS v", "2021-11-07 01:30:00.000 ET"},
+ {"yyyy-MM-dd HH:mm:ss.SSS vvvv", "2021-10-31 02:30:00.000 Central European Time"},
+ {"yyyy-MM-dd HH:mm:ss.SSS vvvv", "2021-11-07 01:30:00.000 Eastern Time"},
+ };
+ }
+
+ @Test(dataProvider="roundTripAtOverlap")
+ public void test_roundTripAtOverlap(String pattern, String input) {
+ var dtf = DateTimeFormatter.ofPattern(pattern);
+ assertEquals(dtf.format(ZonedDateTime.parse(input, dtf)), input);
+ var lc = input.toLowerCase(Locale.ROOT);
+ try {
+ ZonedDateTime.parse(lc, dtf);
+ fail("Should throw DateTimeParseException");
+ } catch (DateTimeParseException ignore) {}
+
+ dtf = new DateTimeFormatterBuilder().parseCaseInsensitive().appendPattern(pattern).toFormatter();
+ assertEquals(dtf.format(ZonedDateTime.parse(input, dtf)), input);
+ assertEquals(dtf.format(ZonedDateTime.parse(lc, dtf)), input);
+ }
}