Skip to content

Commit c6d209b

Browse files
rpatil-openjdkIvan Gerasimov
authored and
Ivan Gerasimov
committed
8066982: ZonedDateTime.parse() returns wrong ZoneOffset around DST fall transition
In Parsed.java the method resolveInstant() is altered such that, the offset (if present) will be given priority over zone. Reviewed-by: rriggs, scolebourne
1 parent 741ed62 commit c6d209b

File tree

4 files changed

+241
-8
lines changed

4 files changed

+241
-8
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import java.time.Period;
8181
import java.time.ZoneId;
8282
import java.time.ZoneOffset;
83+
import java.time.chrono.ChronoLocalDateTime;
8384
import java.time.chrono.Chronology;
8485
import java.time.chrono.IsoChronology;
8586
import java.time.format.DateTimeFormatterBuilder.CompositePrinterParser;
@@ -473,6 +474,17 @@
473474
* day-of-week was valid for the date.
474475
* <li>If an {@linkplain #parsedExcessDays() excess number of days}
475476
* was parsed then it is added to the date if a date is available.
477+
* <li> If a second-based field is present, but {@code LocalTime} was not parsed,
478+
* then the resolver ensures that milli, micro and nano second values are
479+
* available to meet the contract of {@link ChronoField}.
480+
* These will be set to zero if missing.
481+
* <li>If both date and time were parsed and either an offset or zone is present,
482+
* the field {@link ChronoField#INSTANT_SECONDS} is created.
483+
* If an offset was parsed then the offset will be combined with the
484+
* {@code LocalDateTime} to form the instant, with any zone ignored.
485+
* If a {@code ZoneId} was parsed without an offset then the zone will be
486+
* combined with the {@code LocalDateTime} to form the instant using the rules
487+
* of {@link ChronoLocalDateTime#atZone(ZoneId)}.
476488
* </ol>
477489
*
478490
* @implSpec

jdk/src/java.base/share/classes/java/time/format/Parsed.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2015, 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
@@ -594,15 +594,16 @@ private void resolveFractional() {
594594

595595
private void resolveInstant() {
596596
// add instant seconds if we have date, time and zone
597+
// Offset (if present) will be given priority over the zone.
597598
if (date != null && time != null) {
598-
if (zone != null) {
599-
long instant = date.atTime(time).atZone(zone).getLong(ChronoField.INSTANT_SECONDS);
599+
Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
600+
if (offsetSecs != null) {
601+
ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
602+
long instant = date.atTime(time).atZone(offset).toEpochSecond();
600603
fieldValues.put(INSTANT_SECONDS, instant);
601604
} else {
602-
Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
603-
if (offsetSecs != null) {
604-
ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
605-
long instant = date.atTime(time).atZone(offset).getLong(ChronoField.INSTANT_SECONDS);
605+
if (zone != null) {
606+
long instant = date.atTime(time).atZone(zone).toEpochSecond();
606607
fieldValues.put(INSTANT_SECONDS, instant);
607608
}
608609
}

jdk/test/java/time/tck/java/time/TCKZonedDateTime.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ Object[][] data_parseAdditional() {
751751
{"2012-06-30T12:30:40Z[GMT]", 2012, 6, 30, 12, 30, 40, 0, "GMT"},
752752
{"2012-06-30T12:30:40Z[UT]", 2012, 6, 30, 12, 30, 40, 0, "UT"},
753753
{"2012-06-30T12:30:40Z[UTC]", 2012, 6, 30, 12, 30, 40, 0, "UTC"},
754-
{"2012-06-30T12:30:40+01:00[Z]", 2012, 6, 30, 12, 30, 40, 0, "Z"},
754+
{"2012-06-30T12:30:40+01:00[Z]", 2012, 6, 30, 11, 30, 40, 0, "Z"},
755755
{"2012-06-30T12:30:40+01:00[+01:00]", 2012, 6, 30, 12, 30, 40, 0, "+01:00"},
756756
{"2012-06-30T12:30:40+01:00[GMT+01:00]", 2012, 6, 30, 12, 30, 40, 0, "GMT+01:00"},
757757
{"2012-06-30T12:30:40+01:00[UT+01:00]", 2012, 6, 30, 12, 30, 40, 0, "UT+01:00"},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
package tck.java.time.format;
24+
25+
import static org.testng.AssertJUnit.assertEquals;
26+
27+
import java.time.LocalDateTime;
28+
import java.time.OffsetDateTime;
29+
import java.time.ZoneId;
30+
import java.time.ZoneOffset;
31+
import java.time.ZonedDateTime;
32+
import java.time.format.DateTimeFormatter;
33+
34+
import org.testng.annotations.BeforeMethod;
35+
import org.testng.annotations.DataProvider;
36+
import org.testng.annotations.Test;
37+
38+
/**
39+
* Testing DateTimeFormatter Parsing with 4 different test conditions:
40+
* 1. When Zone and Offset not provided
41+
* 2. When Zone and Offset provided
42+
* 3. When Offset is not provided and Zone is provided
43+
* 4. When Zone is not provided and Offset is provided
44+
*/
45+
46+
@Test
47+
public class TCKDTFParsedInstant {
48+
49+
private static final ZoneId EUROPE_BERLIN = ZoneId.of("Europe/Berlin");
50+
private static final ZoneId ASIA_ISTANBUL = ZoneId.of("Asia/Istanbul");
51+
52+
private DateTimeFormatter dtFormatter;
53+
private ZonedDateTime zdt1, zdt2;
54+
private LocalDateTime ldt1;
55+
private OffsetDateTime odt1;
56+
57+
@BeforeMethod
58+
public void setUp() throws Exception {
59+
dtFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME;
60+
}
61+
62+
@DataProvider(name="parseWithoutZoneWithoutOffset")
63+
Object[][] data_parse_WithoutOffset_WithoutZone() {
64+
return new Object[][] {
65+
{"1966-12-31T00:01:10", LocalDateTime.of(1966, 12, 31, 0, 1, 10)},
66+
{"1970-01-01T00:00:00", LocalDateTime.of(1970, 1, 1, 0, 0, 0)},
67+
{"2004-02-29T00:30:00", LocalDateTime.of(2004, 2, 29, 0, 30, 0)},
68+
{"2015-12-31T23:59:59", LocalDateTime.of(2015, 12, 31, 23, 59, 59)}
69+
};
70+
}
71+
72+
@Test(dataProvider="parseWithoutZoneWithoutOffset")
73+
public void testWithoutZoneWithoutOffset(String ldtString, LocalDateTime expectedLDT) {
74+
dtFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
75+
ldt1 = LocalDateTime.parse(ldtString, dtFormatter);
76+
assertEquals(expectedLDT, ldt1);
77+
}
78+
79+
@DataProvider(name="parseWithZoneWithOffset")
80+
Object[][] data_parse_WithZone_WithOffset() {
81+
return new Object[][] {
82+
{"2012-10-28T01:45:00-02:30[Europe/Berlin]",
83+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("-02:30"), EUROPE_BERLIN},
84+
{"2012-10-28T01:45:00-01:00[Europe/Berlin]",
85+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("-01:00"), EUROPE_BERLIN},
86+
{"2012-10-28T01:45:00-00:00[Europe/Berlin]",
87+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("-00:00"), EUROPE_BERLIN},
88+
{"2012-10-28T01:45:00+00:00[Europe/Berlin]",
89+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("+00:00"), EUROPE_BERLIN},
90+
{"2012-10-28T01:45:00+01:00[Europe/Berlin]",
91+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("+01:00"), EUROPE_BERLIN},
92+
{"2012-10-28T01:45:00+02:00[Europe/Berlin]",
93+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("+02:00"), EUROPE_BERLIN},
94+
{"2012-10-28T01:45:00+03:00[Europe/Berlin]",
95+
LocalDateTime.of(2012, 10, 28, 1, 45, 0, 0), ZoneOffset.of("+03:00"), EUROPE_BERLIN},
96+
{"2012-10-28T02:45:00-02:30[Europe/Berlin]",
97+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("-02:30"), EUROPE_BERLIN},
98+
{"2012-10-28T02:45:00-01:00[Europe/Berlin]",
99+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("-01:00"), EUROPE_BERLIN},
100+
{"2012-10-28T02:45:00-00:00[Europe/Berlin]",
101+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("-00:00"), EUROPE_BERLIN},
102+
{"2012-10-28T02:45:00+00:00[Europe/Berlin]",
103+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+00:00"), EUROPE_BERLIN},
104+
{"2012-10-28T02:45:00+01:00[Europe/Berlin]",
105+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+01:00"), EUROPE_BERLIN},
106+
{"2012-10-28T02:45:00+02:00[Europe/Berlin]",
107+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+02:00"), EUROPE_BERLIN},
108+
{"2012-10-28T02:45:00+03:00[Europe/Berlin]",
109+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+03:00"), EUROPE_BERLIN},
110+
{"2012-10-28T03:45:00-02:30[Europe/Berlin]",
111+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("-02:30"), EUROPE_BERLIN},
112+
{"2012-10-28T03:45:00-01:00[Europe/Berlin]",
113+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("-01:00"), EUROPE_BERLIN},
114+
{"2012-10-28T03:45:00-00:00[Europe/Berlin]",
115+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("-00:00"), EUROPE_BERLIN},
116+
{"2012-10-28T03:45:00+00:00[Europe/Berlin]",
117+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+00:00"), EUROPE_BERLIN},
118+
{"2012-10-28T03:45:00+01:00[Europe/Berlin]",
119+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+01:00"), EUROPE_BERLIN},
120+
{"2012-10-28T03:45:00+02:00[Europe/Berlin]",
121+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+02:00"), EUROPE_BERLIN},
122+
{"2012-10-28T03:45:00+03:00[Europe/Berlin]",
123+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+03:00"), EUROPE_BERLIN},
124+
125+
{"2012-10-28T02:45:00-02:30[Asia/Istanbul]",
126+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("-02:30"), ASIA_ISTANBUL},
127+
{"2012-10-28T02:45:00-01:00[Asia/Istanbul]",
128+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("-01:00"), ASIA_ISTANBUL},
129+
{"2012-10-28T02:45:00-00:00[Asia/Istanbul]",
130+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("-00:00"), ASIA_ISTANBUL},
131+
{"2012-10-28T02:45:00+00:00[Asia/Istanbul]",
132+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+00:00"), ASIA_ISTANBUL},
133+
{"2012-10-28T02:45:00+01:00[Asia/Istanbul]",
134+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+01:00"), ASIA_ISTANBUL},
135+
{"2012-10-28T02:45:00+02:00[Asia/Istanbul]",
136+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+02:00"), ASIA_ISTANBUL},
137+
{"2012-10-28T02:45:00+03:00[Asia/Istanbul]",
138+
LocalDateTime.of(2012, 10, 28, 2, 45, 0, 0), ZoneOffset.of("+03:00"), ASIA_ISTANBUL},
139+
{"2012-10-28T03:45:00-02:30[Asia/Istanbul]",
140+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("-02:30"), ASIA_ISTANBUL},
141+
{"2012-10-28T03:45:00-01:00[Asia/Istanbul]",
142+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("-01:00"), ASIA_ISTANBUL},
143+
{"2012-10-28T03:45:00-00:00[Asia/Istanbul]",
144+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("-00:00"), ASIA_ISTANBUL},
145+
{"2012-10-28T03:45:00+00:00[Asia/Istanbul]",
146+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+00:00"), ASIA_ISTANBUL},
147+
{"2012-10-28T03:45:00+01:00[Asia/Istanbul]",
148+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+01:00"), ASIA_ISTANBUL},
149+
{"2012-10-28T03:45:00+02:00[Asia/Istanbul]",
150+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+02:00"), ASIA_ISTANBUL},
151+
{"2012-10-28T03:45:00+03:00[Asia/Istanbul]",
152+
LocalDateTime.of(2012, 10, 28, 3, 45, 0, 0), ZoneOffset.of("+03:00"), ASIA_ISTANBUL},
153+
{"2012-10-28T04:45:00-02:30[Asia/Istanbul]",
154+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("-02:30"), ASIA_ISTANBUL},
155+
{"2012-10-28T04:45:00-01:00[Asia/Istanbul]",
156+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("-01:00"), ASIA_ISTANBUL},
157+
{"2012-10-28T04:45:00-00:00[Asia/Istanbul]",
158+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("-00:00"), ASIA_ISTANBUL},
159+
{"2012-10-28T04:45:00+00:00[Asia/Istanbul]",
160+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("+00:00"), ASIA_ISTANBUL},
161+
{"2012-10-28T04:45:00+01:00[Asia/Istanbul]",
162+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("+01:00"), ASIA_ISTANBUL},
163+
{"2012-10-28T04:45:00+02:00[Asia/Istanbul]",
164+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("+02:00"), ASIA_ISTANBUL},
165+
{"2012-10-28T04:45:00+03:00[Asia/Istanbul]",
166+
LocalDateTime.of(2012, 10, 28, 4, 45, 0, 0), ZoneOffset.of("+03:00"), ASIA_ISTANBUL}
167+
};
168+
}
169+
170+
@Test(dataProvider="parseWithZoneWithOffset")
171+
public void testWithZoneWithOffset(String zdtString, LocalDateTime ldt, ZoneOffset offset, ZoneId zone) {
172+
dtFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME;
173+
zdt1 = ZonedDateTime.ofInstant(ldt, offset, zone);
174+
zdt2 = ZonedDateTime.parse(zdtString, dtFormatter);
175+
assertEquals(zdt1, zdt2);
176+
}
177+
178+
@DataProvider(name="parseWithZoneWithoutOffset")
179+
Object[][] data_parse_WithZone_WithoutOffset() {
180+
return new Object[][] {
181+
{"28 Oct 00:45:00 2012 Europe/Berlin", ZonedDateTime.of(2012, 10, 28, 0, 45, 0, 0, EUROPE_BERLIN)},
182+
{"28 Oct 01:45:00 2012 Europe/Berlin", ZonedDateTime.of(2012, 10, 28, 1, 45, 0, 0, EUROPE_BERLIN)},
183+
{"28 Oct 02:45:00 2012 Europe/Berlin", ZonedDateTime.of(2012, 10, 28, 2, 45, 0, 0, EUROPE_BERLIN)},
184+
{"28 Oct 03:45:00 2012 Europe/Berlin", ZonedDateTime.of(2012, 10, 28, 3, 45, 0, 0, EUROPE_BERLIN)},
185+
{"28 Oct 04:45:00 2012 Europe/Berlin", ZonedDateTime.of(2012, 10, 28, 4, 45, 0, 0, EUROPE_BERLIN)},
186+
187+
{"28 Oct 01:45:00 2012 Asia/Istanbul", ZonedDateTime.of(2012, 10, 28, 1, 45, 0, 0, ASIA_ISTANBUL)},
188+
{"28 Oct 02:45:00 2012 Asia/Istanbul", ZonedDateTime.of(2012, 10, 28, 2, 45, 0, 0, ASIA_ISTANBUL)},
189+
{"28 Oct 03:45:00 2012 Asia/Istanbul", ZonedDateTime.of(2012, 10, 28, 3, 45, 0, 0, ASIA_ISTANBUL)},
190+
{"28 Oct 04:45:00 2012 Asia/Istanbul", ZonedDateTime.of(2012, 10, 28, 4, 45, 0, 0, ASIA_ISTANBUL)},
191+
{"28 Oct 05:45:00 2012 Asia/Istanbul", ZonedDateTime.of(2012, 10, 28, 5, 45, 0, 0, ASIA_ISTANBUL)}
192+
};
193+
}
194+
195+
@Test(dataProvider="parseWithZoneWithoutOffset")
196+
public void testWithZoneWithoutOffset(String withZoneWithoutOffset, ZonedDateTime expectedZDT) {
197+
dtFormatter = DateTimeFormatter.ofPattern("d MMM HH:mm:ss uuuu VV");
198+
zdt1 = ZonedDateTime.parse(withZoneWithoutOffset, dtFormatter);
199+
assertEquals(expectedZDT, zdt1);
200+
}
201+
202+
@DataProvider(name="parseWithOffsetWithoutZone")
203+
Object[][] data_parse_WithOffset_WithoutZone() {
204+
return new Object[][] {
205+
{"2015-12-14T00:45:00-11:30", OffsetDateTime.of(2015, 12, 14, 0, 45, 0, 0, ZoneOffset.of("-11:30"))},
206+
{"2015-12-14T01:45:00-05:00", OffsetDateTime.of(2015, 12, 14, 1, 45, 0, 0, ZoneOffset.of("-05:00"))},
207+
{"2015-12-14T02:45:00-00:00", OffsetDateTime.of(2015, 12, 14, 2, 45, 0, 0, ZoneOffset.of("-00:00"))},
208+
{"2015-12-14T03:45:00+00:00", OffsetDateTime.of(2015, 12, 14, 3, 45, 0, 0, ZoneOffset.of("+00:00"))},
209+
{"2015-12-14T04:45:00+03:30", OffsetDateTime.of(2015, 12, 14, 4, 45, 0, 0, ZoneOffset.of("+03:30"))},
210+
{"2015-12-14T05:45:00+10:00", OffsetDateTime.of(2015, 12, 14, 5, 45, 0, 0, ZoneOffset.of("+10:00"))}
211+
};
212+
}
213+
214+
@Test(dataProvider="parseWithOffsetWithoutZone")
215+
public void testWithOffsetWithoutZone(String odtString, OffsetDateTime expectedOTD) {
216+
dtFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
217+
odt1 = OffsetDateTime.parse(odtString, dtFormatter);
218+
assertEquals(expectedOTD, odt1);
219+
}
220+
}

0 commit comments

Comments
 (0)