Skip to content

Commit cd4634b

Browse files
committed
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
1 parent f03723a commit cd4634b

File tree

2 files changed

+70
-9
lines changed

2 files changed

+70
-9
lines changed

core/src/main/java/org/elasticsearch/common/rounding/Rounding.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,38 @@ public byte id() {
128128
@Override
129129
public long round(long utcMillis) {
130130
long rounded = field.roundFloor(utcMillis);
131-
if (timeZone.isFixed() == false && timeZone.getOffset(utcMillis) != timeZone.getOffset(rounded)) {
132-
// in this case, we crossed a time zone transition. In some edge
133-
// cases this will
134-
// result in a value that is not a rounded value itself. We need
135-
// to round again
136-
// to make sure. This will have no affect in cases where
137-
// 'rounded' was already a proper
138-
// rounded value
139-
rounded = field.roundFloor(rounded);
131+
if (timeZone.isFixed() == false) {
132+
// special cases for non-fixed time zones with dst transitions
133+
if (timeZone.getOffset(utcMillis) != timeZone.getOffset(rounded)) {
134+
/*
135+
* the offset change indicates a dst transition. In some
136+
* edge cases this will result in a value that is not a
137+
* rounded value before the transition. We round again to
138+
* make sure we really return a rounded value. This will
139+
* have no effect in cases where we already had a valid
140+
* rounded value
141+
*/
142+
rounded = field.roundFloor(rounded);
143+
} else {
144+
/*
145+
* check if the current time instant is at a start of a DST
146+
* overlap by comparing the offset of the instant and the
147+
* previous millisecond. We want to detect negative offset
148+
* changes that result in an overlap
149+
*/
150+
if (timeZone.getOffset(rounded) < timeZone.getOffset(rounded - 1)) {
151+
/*
152+
* we are rounding a date just after a DST overlap. if
153+
* the overlap is smaller than the time unit we are
154+
* rounding to, we want to add the overlapping part to
155+
* the following rounding interval
156+
*/
157+
long previousRounded = field.roundFloor(rounded - 1);
158+
if (rounded - previousRounded < field.getDurationField().getUnitMillis()) {
159+
rounded = previousRounded;
160+
}
161+
}
162+
}
140163
}
141164
assert rounded == field.roundFloor(rounded);
142165
return rounded;

core/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,44 @@ public void testEdgeCasesTransition() {
514514
}
515515
}
516516

517+
/**
518+
* tests for dst transition with overlaps and day roundings.
519+
*/
520+
public void testDST_END_Edgecases() {
521+
// First case, dst happens at 1am local time, switching back one hour.
522+
// We want the overlapping hour to count for the next day, making it a 25h interval
523+
524+
DateTimeUnit timeUnit = DateTimeUnit.DAY_OF_MONTH;
525+
DateTimeZone tz = DateTimeZone.forID("Atlantic/Azores");
526+
Rounding rounding = new Rounding.TimeUnitRounding(timeUnit, tz);
527+
528+
// Sunday, 29 October 2000, 01:00:00 clocks were turned backward 1 hour
529+
// to Sunday, 29 October 2000, 00:00:00 local standard time instead
530+
531+
long midnightBeforeTransition = time("2000-10-29T00:00:00", tz);
532+
long nextMidnight = time("2000-10-30T00:00:00", tz);
533+
534+
assertInterval(midnightBeforeTransition, nextMidnight, rounding, 25 * 60, tz);
535+
536+
// Second case, dst happens at 0am local time, switching back one hour to 23pm local time.
537+
// We want the overlapping hour to count for the previous day here
538+
539+
tz = DateTimeZone.forID("America/Lima");
540+
rounding = new Rounding.TimeUnitRounding(timeUnit, tz);
541+
542+
// Sunday, 1 April 1990, 00:00:00 clocks were turned backward 1 hour to
543+
// Saturday, 31 March 1990, 23:00:00 local standard time instead
544+
545+
midnightBeforeTransition = time("1990-03-31T00:00:00.000-04:00");
546+
nextMidnight = time("1990-04-01T00:00:00.000-05:00");
547+
assertInterval(midnightBeforeTransition, nextMidnight, rounding, 25 * 60, tz);
548+
549+
// make sure the next interval is 24h long again
550+
long midnightAfterTransition = time("1990-04-01T00:00:00.000-05:00");
551+
nextMidnight = time("1990-04-02T00:00:00.000-05:00");
552+
assertInterval(midnightAfterTransition, nextMidnight, rounding, 24 * 60, tz);
553+
}
554+
517555
/**
518556
* Test that time zones are correctly parsed. There is a bug with
519557
* Joda 2.9.4 (see https://github.com/JodaOrg/joda-time/issues/373)

0 commit comments

Comments
 (0)