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>> cache = new ConcurrentHashMap<>(); @@ -4433,11 +4434,11 @@ protected PrefixTree getTree(DateTimeParseContext context) { nonRegionIds.add(zid); continue; } - tree.add(zid, zid); // don't convert zid -> metazone + tree.add(zid, zid, UNDEFINED); // don't convert zid -> metazone zid = ZoneName.toZid(zid, locale); int i = textStyle == TextStyle.FULL ? 1 : 2; for (; i < names.length; i += 2) { - tree.add(names[i], zid); + tree.add(names[i], zid, (i - 1) / 2); } } @@ -4450,7 +4451,7 @@ protected PrefixTree getTree(DateTimeParseContext context) { int i = textStyle == TextStyle.FULL ? 1 : 2; for (; i < cidNames.length; i += 2) { if (cidNames[i] != null && !cidNames[i].isEmpty()) { - t.add(cidNames[i], cid); + t.add(cidNames[i], cid, (i - 1) / 2); } } }); @@ -4465,7 +4466,7 @@ protected PrefixTree getTree(DateTimeParseContext context) { } int i = textStyle == TextStyle.FULL ? 1 : 2; for (; i < names.length; i += 2) { - tree.add(names[i], zid); + tree.add(names[i], zid, (i - 1) / 2); } } } @@ -4571,15 +4572,16 @@ public int parse(DateTimeParseContext context, CharSequence text, int position) // parse PrefixTree tree = getTree(context); ParsePosition ppos = new ParsePosition(position); - String parsedZoneId = tree.match(text, ppos); - if (parsedZoneId == null) { + PrefixTree parsedZoneId = tree.match(text, ppos); + if (parsedZoneId.value == null) { if (context.charEquals(nextChar, 'Z')) { context.setParsed(ZoneOffset.UTC); return position + 1; } return ~position; } - context.setParsed(ZoneId.of(parsedZoneId)); + context.setParsed(ZoneId.of(parsedZoneId.value)); + context.setParsedZoneNameType(parsedZoneId.type); return ppos.getIndex(); } @@ -4641,14 +4643,16 @@ public String toString() { static class PrefixTree { protected String key; protected String value; + protected int type; protected char c0; // performance optimization to avoid the // boundary check cost of key.charat(0) protected PrefixTree child; protected PrefixTree sibling; - private PrefixTree(String k, String v, PrefixTree child) { + private PrefixTree(String k, String v, int type, PrefixTree child) { this.key = k; this.value = v; + this.type = type; this.child = child; if (k.isEmpty()) { c0 = 0xffff; @@ -4664,13 +4668,10 @@ private PrefixTree(String k, String v, PrefixTree child) { * @return the tree, not null */ public static PrefixTree newTree(DateTimeParseContext context) { - //if (!context.isStrict()) { - // return new LENIENT("", null, null); - //} if (context.isCaseSensitive()) { - return new PrefixTree("", null, null); + return new PrefixTree("", null, ZoneTextPrinterParser.UNDEFINED, null); } - return new CI("", null, null); + return new CI("", null, ZoneTextPrinterParser.UNDEFINED, null); } /** @@ -4683,7 +4684,7 @@ public static PrefixTree newTree(DateTimeParseContext context) { public static PrefixTree newTree(Set keys, DateTimeParseContext context) { PrefixTree tree = newTree(context); for (String k : keys) { - tree.add0(k, k); + tree.add0(k, k, ZoneTextPrinterParser.UNDEFINED); } return tree; } @@ -4692,7 +4693,7 @@ public static PrefixTree newTree(Set keys, DateTimeParseContext context * Clone a copy of this tree */ public PrefixTree copyTree() { - PrefixTree copy = new PrefixTree(key, value, null); + PrefixTree copy = new PrefixTree(key, value, type, null); if (child != null) { copy.child = child.copyTree(); } @@ -4710,11 +4711,11 @@ public PrefixTree copyTree() { * @param v the value, not null * @return true if the pair is added successfully */ - public boolean add(String k, String v) { - return add0(k, v); + public boolean add(String k, String v, int t) { + return add0(k, v, t); } - private boolean add0(String k, String v) { + private boolean add0(String k, String v, int t) { k = toKey(k); int prefixLen = prefixLength(k); if (prefixLen == key.length()) { @@ -4723,12 +4724,12 @@ private boolean add0(String k, String v) { PrefixTree c = child; while (c != null) { if (isEqual(c.c0, subKey.charAt(0))) { - return c.add0(subKey, v); + return c.add0(subKey, v, t); } c = c.sibling; } // add the node as the child of the current node - c = newNode(subKey, v, null); + c = newNode(subKey, v, t, null); c.sibling = child; child = c; return true; @@ -4738,18 +4739,20 @@ private boolean add0(String k, String v) { // return false; //} value = v; + type = t; return true; } // split the existing node - PrefixTree n1 = newNode(key.substring(prefixLen), value, child); + PrefixTree n1 = newNode(key.substring(prefixLen), value, type, child); key = k.substring(0, prefixLen); child = n1; if (prefixLen < k.length()) { - PrefixTree n2 = newNode(k.substring(prefixLen), v, null); + PrefixTree n2 = newNode(k.substring(prefixLen), v, t, null); child.sibling = n2; value = null; } else { value = v; + type = t; } return true; } @@ -4760,9 +4763,9 @@ private boolean add0(String k, String v) { * @param text the input text to parse, not null * @param off the offset position to start parsing at * @param end the end position to stop parsing - * @return the resulting string, or null if no match found. + * @return the resulting tree, or null if no match found. */ - public String match(CharSequence text, int off, int end) { + public PrefixTree match(CharSequence text, int off, int end) { if (!prefixOf(text, off, end)){ return null; } @@ -4770,16 +4773,16 @@ public String match(CharSequence text, int off, int end) { PrefixTree c = child; do { if (isEqual(c.c0, text.charAt(off))) { - String found = c.match(text, off, end); + PrefixTree found = c.match(text, off, end); if (found != null) { return found; } - return value; + return this; } c = c.sibling; } while (c != null); } - return value; + return this; } /** @@ -4789,9 +4792,9 @@ public String match(CharSequence text, int off, int end) { * @param pos the position to start parsing at, from 0 to the text * length. Upon return, position will be updated to the new parse * position, or unchanged, if no match found. - * @return the resulting string, or null if no match found. + * @return the resulting tree, or null if no match found. */ - public String match(CharSequence text, ParsePosition pos) { + public PrefixTree match(CharSequence text, ParsePosition pos) { int off = pos.getIndex(); int end = text.length(); if (!prefixOf(text, off, end)){ @@ -4803,7 +4806,7 @@ public String match(CharSequence text, ParsePosition pos) { do { if (isEqual(c.c0, text.charAt(off))) { pos.setIndex(off); - String found = c.match(text, pos); + PrefixTree found = c.match(text, pos); if (found != null) { return found; } @@ -4813,15 +4816,15 @@ public String match(CharSequence text, ParsePosition pos) { } while (c != null); } pos.setIndex(off); - return value; + return this; } protected String toKey(String k) { return k; } - protected PrefixTree newNode(String k, String v, PrefixTree child) { - return new PrefixTree(k, v, child); + protected PrefixTree newNode(String k, String v, int t, PrefixTree child) { + return new PrefixTree(k, v, t, child); } protected boolean isEqual(char c1, char c2) { @@ -4861,13 +4864,13 @@ private int prefixLength(String k) { */ private static class CI extends PrefixTree { - private CI(String k, String v, PrefixTree child) { - super(k, v, child); + private CI(String k, String v, int t, PrefixTree child) { + super(k, v, t, child); } @Override - protected CI newNode(String k, String v, PrefixTree child) { - return new CI(k, v, child); + protected CI newNode(String k, String v, int t, PrefixTree child) { + return new CI(k, v, t, child); } @Override @@ -4890,86 +4893,6 @@ protected boolean prefixOf(CharSequence text, int off, int end) { return true; } } - - /** - * Lenient prefix tree. Case insensitive and ignores characters - * like space, underscore and slash. - */ - private static class LENIENT extends CI { - - private LENIENT(String k, String v, PrefixTree child) { - super(k, v, child); - } - - @Override - protected CI newNode(String k, String v, PrefixTree child) { - return new LENIENT(k, v, child); - } - - private boolean isLenientChar(char c) { - return c == ' ' || c == '_' || c == '/'; - } - - protected String toKey(String k) { - for (int i = 0; i < k.length(); i++) { - if (isLenientChar(k.charAt(i))) { - StringBuilder sb = new StringBuilder(k.length()); - sb.append(k, 0, i); - i++; - while (i < k.length()) { - if (!isLenientChar(k.charAt(i))) { - sb.append(k.charAt(i)); - } - i++; - } - return sb.toString(); - } - } - return k; - } - - @Override - public String match(CharSequence text, ParsePosition pos) { - int off = pos.getIndex(); - int end = text.length(); - int len = key.length(); - int koff = 0; - while (koff < len && off < end) { - if (isLenientChar(text.charAt(off))) { - off++; - continue; - } - if (!isEqual(key.charAt(koff++), text.charAt(off++))) { - return null; - } - } - if (koff != len) { - return null; - } - if (child != null && off != end) { - int off0 = off; - while (off0 < end && isLenientChar(text.charAt(off0))) { - off0++; - } - if (off0 < end) { - PrefixTree c = child; - do { - if (isEqual(c.c0, text.charAt(off0))) { - pos.setIndex(off0); - String found = c.match(text, pos); - if (found != null) { - return found; - } - break; - } - c = c.sibling; - } while (c != null); - } - } - pos.setIndex(off); - return value; - } - } } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/format/DateTimeParseContext.java b/src/java.base/share/classes/java/time/format/DateTimeParseContext.java index 32eefcecc3009..0a4c7e825a3b6 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeParseContext.java +++ b/src/java.base/share/classes/java/time/format/DateTimeParseContext.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 @@ -417,6 +417,24 @@ void setParsed(ZoneId zone) { currentParsed().zone = zone; } + /** + * Stores the parsed zone name type. + *

+ * 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); + } }