Fix time zone rounding edge case for DST overlaps

When using TimeUnitRounding with a DAY_OF_MONTH unit, failing tests in #20833
uncovered an issue when the DST shift happenes just one hour after midnight
local time and sets back the clock to midnight, leading to an overlap.
Previously this would lead to two different rounding values, depending on
whether a date before or after the transition was rounded. This change detects
this special case and correct for it by using the previous rounding date for
both cases.

Closes #20833
This commit is contained in:
Christoph Büscher 2016-11-14 18:34:18 +01:00
parent f03723a812
commit cd4634bdc6
2 changed files with 70 additions and 9 deletions

View File

@ -128,15 +128,38 @@ public abstract class Rounding implements Streamable {
@Override
public long round(long utcMillis) {
long rounded = field.roundFloor(utcMillis);
if (timeZone.isFixed() == false && timeZone.getOffset(utcMillis) != timeZone.getOffset(rounded)) {
// in this case, we crossed a time zone transition. In some edge
// cases this will
// result in a value that is not a rounded value itself. We need
// to round again
// to make sure. This will have no affect in cases where
// 'rounded' was already a proper
// rounded value
rounded = field.roundFloor(rounded);
if (timeZone.isFixed() == false) {
// special cases for non-fixed time zones with dst transitions
if (timeZone.getOffset(utcMillis) != timeZone.getOffset(rounded)) {
/*
* the offset change indicates a dst transition. In some
* edge cases this will result in a value that is not a
* rounded value before the transition. We round again to
* make sure we really return a rounded value. This will
* have no effect in cases where we already had a valid
* rounded value
*/
rounded = field.roundFloor(rounded);
} else {
/*
* check if the current time instant is at a start of a DST
* overlap by comparing the offset of the instant and the
* previous millisecond. We want to detect negative offset
* changes that result in an overlap
*/
if (timeZone.getOffset(rounded) < timeZone.getOffset(rounded - 1)) {
/*
* we are rounding a date just after a DST overlap. if
* the overlap is smaller than the time unit we are
* rounding to, we want to add the overlapping part to
* the following rounding interval
*/
long previousRounded = field.roundFloor(rounded - 1);
if (rounded - previousRounded < field.getDurationField().getUnitMillis()) {
rounded = previousRounded;
}
}
}
}
assert rounded == field.roundFloor(rounded);
return rounded;

View File

@ -514,6 +514,44 @@ public class TimeZoneRoundingTests extends ESTestCase {
}
}
/**
* tests for dst transition with overlaps and day roundings.
*/
public void testDST_END_Edgecases() {
// First case, dst happens at 1am local time, switching back one hour.
// We want the overlapping hour to count for the next day, making it a 25h interval
DateTimeUnit timeUnit = DateTimeUnit.DAY_OF_MONTH;
DateTimeZone tz = DateTimeZone.forID("Atlantic/Azores");
Rounding rounding = new Rounding.TimeUnitRounding(timeUnit, tz);
// Sunday, 29 October 2000, 01:00:00 clocks were turned backward 1 hour
// to Sunday, 29 October 2000, 00:00:00 local standard time instead
long midnightBeforeTransition = time("2000-10-29T00:00:00", tz);
long nextMidnight = time("2000-10-30T00:00:00", tz);
assertInterval(midnightBeforeTransition, nextMidnight, rounding, 25 * 60, tz);
// Second case, dst happens at 0am local time, switching back one hour to 23pm local time.
// We want the overlapping hour to count for the previous day here
tz = DateTimeZone.forID("America/Lima");
rounding = new Rounding.TimeUnitRounding(timeUnit, tz);
// Sunday, 1 April 1990, 00:00:00 clocks were turned backward 1 hour to
// Saturday, 31 March 1990, 23:00:00 local standard time instead
midnightBeforeTransition = time("1990-03-31T00:00:00.000-04:00");
nextMidnight = time("1990-04-01T00:00:00.000-05:00");
assertInterval(midnightBeforeTransition, nextMidnight, rounding, 25 * 60, tz);
// make sure the next interval is 24h long again
long midnightAfterTransition = time("1990-04-01T00:00:00.000-05:00");
nextMidnight = time("1990-04-02T00:00:00.000-05:00");
assertInterval(midnightAfterTransition, nextMidnight, rounding, 24 * 60, tz);
}
/**
* Test that time zones are correctly parsed. There is a bug with
* Joda 2.9.4 (see https://github.com/JodaOrg/joda-time/issues/373)