JDK-8066982 : ZonedDateTime.parse() returns wrong ZoneOffset around DST fall transition
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.time
  • Affected Version: 8u20
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_7
  • CPU: x86_64
  • Submitted: 2014-08-25
  • Updated: 2018-11-22
  • Resolved: 2015-12-25
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8-poolUnresolved 9 b100Fixed
Related Reports
Duplicate :  
Relates :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.8.0_20"
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 6.1.7601]

A DESCRIPTION OF THE PROBLEM :
Parsing a string with ZonedDateTime.parse() that contains zone offset and zone ID "Europe/Berlin" returns a wrong ZonedDateAndTime (different offset). This error starts exactly at the transition time (included) and ends one hour later (excluded).


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. create a ZonedDateTime with date/time of the DST transition in fall and a ZoneId different from UTC (e.g. "Europe/Berlin")
2. apply ZonedDateTime.format() with ISO_ZONED_DATE_TIME formatter
3. apply ZonedDateTime.parse() to the formatted string with same formatter
4. compare the result to the starting value

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The parsed string should represent the same instant on the time line.
ACTUAL -
Starting at the fall DST transition time (included) and ending one hour later (excluded) the offset is one hour too high. Lies 2..5 show wrong result, 1st and last line are correct.

2012-10-28T02:45:00+02:00[Europe/Berlin] -> 2012-10-28T02:45+02:00[Europe/Berlin]
2012-10-28T02:00:00+01:00[Europe/Berlin] -> 2012-10-28T02:00+02:00[Europe/Berlin]
2012-10-28T02:15:00+01:00[Europe/Berlin] -> 2012-10-28T02:15+02:00[Europe/Berlin]
2012-10-28T02:30:00+01:00[Europe/Berlin] -> 2012-10-28T02:30+02:00[Europe/Berlin]
2012-10-28T02:45:00+01:00[Europe/Berlin] -> 2012-10-28T02:45+02:00[Europe/Berlin]
2012-10-28T03:00:00+01:00[Europe/Berlin] -> 2012-10-28T03:00+01:00[Europe/Berlin]


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class LocalDateTimeIssue {
  public static void main(String[] args) {
    ZonedDateTime zdt = ZonedDateTime.of(2012, 10, 28, 2, 45, 0, 0, ZoneId.of("Europe/Berlin"));
    DateTimeFormatter fmt = DateTimeFormatter.ISO_ZONED_DATE_TIME;
    for (int i = 0; i < 6; i++) {
      String s = zdt.format(fmt);
      System.out.println(s + " -> " + ZonedDateTime.parse(s, fmt));
      zdt = zdt.plus(15, ChronoUnit.MINUTES);
    }
  }
}

---------- END SOURCE ----------


Comments
[~scolebourne] Thank you for your clarifications on why offset should be given priority over zone. I am providing the mail extract here for reference: The code deals correctly with the "good" case where the offset is valid for the zone. The question is what should the code do if the offset is not valid for the zone, eg. 2012-10-28T02:45+03:00[Europe/Berlin] There are three options: 1) parse error 2) prioritise the zone 3) prioritise the offset The key thing to factor into the decision is that time-zones change. Thus, the parser needs to allow for the fact that when the date-time was written to a string, +03:00 actually was a valid offset. (Imagine the situation where Germany changes from +01:00 in winter and +02:00 in summer to +02:00 in winter and +03:00 in summer). This kind of change to time-zones happens all the time. Given the fact that time-zone rules change, option 1 is too harsh. There is no way to know if the input actually is invalid. It might have come from another system with a more accurate time-zone database, or an older one. Options 2 and 3 both handle the case where the time-zone database has changed, option 2 changes to offset to make the result valid, option 3 changes the local time to make the result valid. As a general rule, when transmitting dates and times across a network boundary, the time-zone is not sent at all. As such, the receiving party necessarily places all their trust in the offset. The same should apply here, placing trust in the offset, not the zone (as the offset is a fixed known element, but the zone varies over time). Making the offset always take priority is a change to the existing results for invalid offsets, but it will be the right choice.
09-12-2015

That comment refers to the various cases where things are ambiguous. The whole part (including bits not quoted above) indicate that if there is a "previous offset" (such as when adding a Period to a ZonedDateTime) then it should be used. However, if there is no "previous offset" than summer time should be used. Note that the whole section refers to the ambiguous cases.In the bug report, the input is not ambiguous, so that documentation section does not apply. The documentation section that should apply to this bug is the ordered list in the "Resolving" section of `DateTimeFormatter`. Unfortunately, the ordered list is missing steps 9 and 10 that should define the last two steps of resolving. Step 9 should be a description of the `Parsed.resolveFractional()` method. Step 10 should be a description of the `Parsed.resolveInstant()` method. It would make sense to add this documentation with this change (although doing so makes the bug fix harder to backport). Proposed docs: 9. If a second-based field is present, but `LocalTime` was not parsed, then the resolver ensures that milli, micro and nano second values are available to meet the contract of `ChronoField`. These will be set to zero if missing. 10. If both a date and time were parsed and either an offset or zone is present, the field `INSTANT_SECONDS` is created. If an offset was parsed then the offset will be combined with the local date-time to form the instant, with any zone ignored. If a `ZoneId` was parsed without an offset then the zone will be combined with the local date-time to form the instant using the rules of `ChronoZonedDateTime.atZone()`.
27-11-2015

@Stephen - After incorporating your suggested fix , is the following line about " previous offset" still valid ? "For Overlaps, the general strategy is that if the local date-time falls in the middle of an Overlap, then the previous offset will be retained"
27-11-2015

Let me be a little clearer. This is a terrible bug, and must be fixed. If a user supplies a valid string with both an offset and time-zone, it must be fully parsed. There is a round-trip behaviour that is not being met right now. ZonedDateTime zdt1 = ... ZonedDateTime zdt2 = ZonedDateTime.parse(zdt1.toString()); assert zd1.equals(zdt2) It is absolutely vital that this round trip behaviour is maintained, including through the Overlap. The whole point of holding the offset is to handle the Overlap! This comment "if we give the priority to offset over zone, then for the each Instant in the overlap period we will end up having two different LocalDateTime(with different offset) which may be wrong and confusing.(without knowing the actual corresponding "UTC" time or if the clock has been set back by an hour or not)" appears to misunderstand the problem. Specifically, the combination of a LocalDateTime and an offset fully defines an instant (the UTC time). As such, the parser has full knowledge of whether the input time is in summer or winter. On the ZDT Javadoc, the second sentence is actually clear. It says that the offset is *vital* during a DST overlap. By contrast, the current parser is ignoring the vital information.
24-11-2015

I still feel this is the correct implementation. Here are few more points to consider: 1. Even though the string contains the information to decide it is +1 or +2, sometime it may contain any other offset too (+3 or anything). Because of that it becomes important to check for the valid offsets from the Zone. That is the reason you will see the below outputs for the Strings with invalid offsets before and after overlap respectively: String: "2012-10-28T01:00:00+01:00[Europe/Berlin]" Parsed Output: 2012-10-28T01:00+02:00 String: "2012-10-28T04:00:00+02:00[Europe/Berlin]" Parsed Output: 2012-10-28T04:00+01:00 And during overlap period the earlier mentioned rule applies, ignoring the provided offset and retaining the previous offset. As suggested by Stephen above, during overlap if we give the priority to offset over zone, then for the each Instant in the overlap period we will end up having two different LocalDateTime(with different offset) which may be wrong and confusing.(without knowing the actual corresponding "UTC" time or if the clock has been set back by an hour or not). This should clear why the rule of keeping the same offset as previous was made for the Overlap period. 2. Further to clarify why offset should not take priority over zone, please consider below line, from the javadoc for ZonedDateTime: "In terms of design, this class should be viewed primarily as the combination of a LocalDateTime and a ZoneId. The ZoneOffset is a vital, but secondary, piece of information, used to ensure that the class represents an instant, especially during a daylight savings overlap." I hope these 2 points will throw more light in the analysis and understanding of this behavior. We can always debate on both the side, because in the overlap period there are 2 valid offsets. But considering above complications and the rules in the java-doc, the current behavior looks fine.
17-11-2015

While the analysis above is excellent it misses one key point - the input string contains the information to decide whether it is +01:00 or +02:00. This is a bug, around line 590 in class 'Parsed'. The code is as follows: // add instant seconds if we have date, time and zone if (date != null && time != null) { if (zone != null) { // the following line cause the bug: long instant = date.atTime(time).atZone(zone).getLong(ChronoField.INSTANT_SECONDS); fieldValues.put(INSTANT_SECONDS, instant); } else { Long offsetSecs = fieldValues.get(OFFSET_SECONDS); if (offsetSecs != null) { ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue()); long instant = date.atTime(time).atZone(offset).getLong(ChronoField.INSTANT_SECONDS); fieldValues.put(INSTANT_SECONDS, instant); } } } The code does not take into account the possibility that *both* the zone and OFFSET_SECONDS can be present. I believe the correct fix is to reverse the two inner "if" statements. ie. offset should take priority over zone. I'm not sure why no test covered this area, so some need adding. The cases to consider are: * neither offset nor zone - only LocalDateTime available * only offset available - INSTANT_SECONDS calculated from LocalDateTime and OFFSET_SECONDS * only zone available - INSTANT_SECONDS calculated from LocalDateTime and zone, using summer time in DST Overlap * both zone and offset available - INSTANT_SECONDS calculated from LocalDateTime and OFFSET_SECONDS (zone ignored). The zone has to be ignored in the last case as the date-time might have been produce on a system with incorrect time-zone data. In situations like that, the offset is likely to be the most correct thing.
17-11-2015

Thank you Aleksej and Roger for your feedback. I am removing the duplicate link and closing this issue as "Not an Issue".
17-11-2015

Ramanand, thank you for the analysis and great explanation. I agree that you can close it as "Not and Issue". Also I suggest to remove the JDK-8032051 duplicate link - these two issues looks different.
16-11-2015

[[~aefimov] please correct me if I have made any mistake or my understanding is wrong]. This is ���Not an Issue��� and expected behavior of the ZonedDateTime class. Please read below, my lengthy explanation for the same. :) As per the Javadoc of ZonedDateTime(http://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html): "This class handles conversion from the local time-line of LocalDateTime to the instant time-line of Instant. The difference between the two time-lines is the offset from UTC/Greenwich, represented by a ZoneOffset. Converting between the two time-lines involves calculating the offset using the rules accessed from the ZoneId. Obtaining the offset for an instant is simple, as there is exactly one valid offset for each instant. By contrast, obtaining the offset for a local date-time is not straightforward. There are three cases: ��� Normal, with one valid offset. For the vast majority of the year, the normal case applies, where there is a single valid offset for the local date-time. ��� Gap, with zero valid offsets. This is when clocks jump forward typically due to the spring daylight savings change from "winter" to "summer". In a gap there are local date-time values with no valid offset. ��� Overlap, with two valid offsets. This is when clocks are set back typically due to the autumn daylight savings change from "summer" to "winter". In an overlap there are local date-time values with two valid offsets." Now, coming back to our example of ZonedDateTime -zdt: 28th October 2012 which is actually (DST transition in fall) and comes under 3rd case ��� Overlap, where two valid offsets are present.(+1 and +2 in this case). Please consider output only for: String s = zdt.format(fmt); 2012-10-28T02:45:00+02:00[Europe/Berlin] 2012-10-28T02:00:00+01:00[Europe/Berlin] 2012-10-28T02:15:00+01:00[Europe/Berlin] 2012-10-28T02:30:00+01:00[Europe/Berlin] 2012-10-28T02:45:00+01:00[Europe/Berlin] 2012-10-28T03:00:00+01:00[Europe/Berlin] On 2nd output line you can see that, since we are performing ���plus��� on zdt object, when the time reaches 3:00:00 it has set its hour field of LocalDateTime to 2:00:00 instead of 3:00:00 and hence setting the correct zone offset (+1) after overlap, which is also called as laterOffsetAtOverlap. Since zdt object already has this information of time and offset, formatting has resulted the above shown outputs in every loop. Now Consider the output only for the parsing part: ZonedDateTime.parse(s, fmt); 2012-10-28T02:45+02:00[Europe/Berlin] 2012-10-28T02:00+02:00[Europe/Berlin] 2012-10-28T02:15+02:00[Europe/Berlin] 2012-10-28T02:30+02:00[Europe/Berlin] 2012-10-28T02:45+02:00[Europe/Berlin] 2012-10-28T03:00+01:00[Europe/Berlin] Here parse method just accepts a formatted String and doesn���t know whether the actual set back of hours(1 hour in this case) has happened or not.(Because we are in the Overlap case where both the offsets are valid). And because of this reason here the previous offset is retained (i.e. +2 in this case is retained) as per the below javadoc lines: ���For Overlaps, the general strategy is that if the local date-time falls in the middle of an Overlap, then the previous offset will be retained. If there is no previous offset, or the previous offset is invalid, then the earlier offset is used, typically "summer" time..Two additional methods, withEarlierOffsetAtOverlap() and withLaterOffsetAtOverlap(), help manage the case of an overlap." I hope that the above information will be useful while understanding why the reported bug is "Not and Issue".
16-11-2015

The parsing errors is different than JDK-8032051.
30-10-2015