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 @Override
public long round(long utcMillis) { public long round(long utcMillis) {
long rounded = field.roundFloor(utcMillis); long rounded = field.roundFloor(utcMillis);
if (timeZone.isFixed() == false && timeZone.getOffset(utcMillis) != timeZone.getOffset(rounded)) { if (timeZone.isFixed() == false) {
// in this case, we crossed a time zone transition. In some edge // special cases for non-fixed time zones with dst transitions
// cases this will if (timeZone.getOffset(utcMillis) != timeZone.getOffset(rounded)) {
// result in a value that is not a rounded value itself. We need /*
// to round again * the offset change indicates a dst transition. In some
// to make sure. This will have no affect in cases where * edge cases this will result in a value that is not a
// 'rounded' was already a proper * rounded value before the transition. We round again to
// rounded value * 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); 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); assert rounded == field.roundFloor(rounded);
return 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 * 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) * Joda 2.9.4 (see https://github.com/JodaOrg/joda-time/issues/373)