diff --git a/core/src/main/java/org/elasticsearch/common/rounding/Rounding.java b/core/src/main/java/org/elasticsearch/common/rounding/Rounding.java index ad9f926e881..50461d6184a 100644 --- a/core/src/main/java/org/elasticsearch/common/rounding/Rounding.java +++ b/core/src/main/java/org/elasticsearch/common/rounding/Rounding.java @@ -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; diff --git a/core/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java b/core/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java index 159b8693b84..2c1c480c6a5 100644 --- a/core/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java +++ b/core/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java @@ -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)