@@ -514,6 +514,101 @@ public void testEdgeCasesTransition() {
514514 }
515515 }
516516
517+ public void testDST_Europe_Rome () {
518+ // time zone "Europe/Rome", rounding to days. Rome had two midnights on the day the clocks went back in 1978, and
519+ // timeZone.convertLocalToUTC() gives the later of the two because Rome is east of UTC, whereas we want the earlier.
520+
521+ DateTimeUnit timeUnit = DateTimeUnit .DAY_OF_MONTH ;
522+ DateTimeZone tz = DateTimeZone .forID ("Europe/Rome" );
523+ Rounding rounding = new TimeUnitRounding (timeUnit , tz );
524+
525+ {
526+ long timeBeforeFirstMidnight = time ("1978-09-30T23:59:00+02:00" );
527+ long floor = rounding .round (timeBeforeFirstMidnight );
528+ assertThat (floor , isDate (time ("1978-09-30T00:00:00+02:00" ), tz ));
529+ }
530+
531+ {
532+ long timeBetweenMidnights = time ("1978-10-01T00:30:00+02:00" );
533+ long floor = rounding .round (timeBetweenMidnights );
534+ assertThat (floor , isDate (time ("1978-10-01T00:00:00+02:00" ), tz ));
535+ }
536+
537+ {
538+ long timeAfterSecondMidnight = time ("1978-10-01T00:30:00+01:00" );
539+ long floor = rounding .round (timeAfterSecondMidnight );
540+ assertThat (floor , isDate (time ("1978-10-01T00:00:00+02:00" ), tz ));
541+
542+ long prevFloor = rounding .round (floor - 1 );
543+ assertThat (prevFloor , lessThan (floor ));
544+ assertThat (prevFloor , isDate (time ("1978-09-30T00:00:00+02:00" ), tz ));
545+ }
546+ }
547+
548+ /**
549+ * Test for a time zone whose days overlap because the clocks are set back across midnight at the end of DST.
550+ */
551+ public void testDST_America_St_Johns () {
552+ // time zone "America/St_Johns", rounding to days.
553+ DateTimeUnit timeUnit = DateTimeUnit .DAY_OF_MONTH ;
554+ DateTimeZone tz = DateTimeZone .forID ("America/St_Johns" );
555+ Rounding rounding = new TimeUnitRounding (timeUnit , tz );
556+
557+ // 29 October 2006 - Daylight Saving Time ended, changing the UTC offset from -02:30 to -03:30.
558+ // This happened at 02:31 UTC, 00:01 local time, so the clocks were set back 1 hour to 23:01 on the 28th.
559+ // This means that 2006-10-29 has _two_ midnights, one in the -02:30 offset and one in the -03:30 offset.
560+ // Only the first of these is considered "rounded". Moreover, the extra time between 23:01 and 23:59
561+ // should be considered as part of the 28th even though it comes after midnight on the 29th.
562+
563+ {
564+ // Times before the first midnight should be rounded up to the first midnight.
565+ long timeBeforeFirstMidnight = time ("2006-10-28T23:30:00.000-02:30" );
566+ long floor = rounding .round (timeBeforeFirstMidnight );
567+ assertThat (floor , isDate (time ("2006-10-28T00:00:00.000-02:30" ), tz ));
568+ long ceiling = rounding .nextRoundingValue (timeBeforeFirstMidnight );
569+ assertThat (ceiling , isDate (time ("2006-10-29T00:00:00.000-02:30" ), tz ));
570+ assertInterval (floor , timeBeforeFirstMidnight , ceiling , rounding , tz );
571+ }
572+
573+ {
574+ // Times between the two midnights which are on the later day should be rounded down to the later day's midnight.
575+ long timeBetweenMidnights = time ("2006-10-29T00:00:30.000-02:30" );
576+ // (this is halfway through the last minute before the clocks changed, in which local time was ambiguous)
577+
578+ long floor = rounding .round (timeBetweenMidnights );
579+ assertThat (floor , isDate (time ("2006-10-29T00:00:00.000-02:30" ), tz ));
580+
581+ long ceiling = rounding .nextRoundingValue (timeBetweenMidnights );
582+ assertThat (ceiling , isDate (time ("2006-10-30T00:00:00.000-03:30" ), tz ));
583+
584+ assertInterval (floor , timeBetweenMidnights , ceiling , rounding , tz );
585+ }
586+
587+ {
588+ // Times between the two midnights which are on the earlier day should be rounded down to the earlier day's midnight.
589+ long timeBetweenMidnights = time ("2006-10-28T23:30:00.000-03:30" );
590+ // (this is halfway through the hour after the clocks changed, in which local time was ambiguous)
591+
592+ long floor = rounding .round (timeBetweenMidnights );
593+ assertThat (floor , isDate (time ("2006-10-28T00:00:00.000-02:30" ), tz ));
594+
595+ long ceiling = rounding .nextRoundingValue (timeBetweenMidnights );
596+ assertThat (ceiling , isDate (time ("2006-10-29T00:00:00.000-02:30" ), tz ));
597+
598+ assertInterval (floor , timeBetweenMidnights , ceiling , rounding , tz );
599+ }
600+
601+ {
602+ // Times after the second midnight should be rounded down to the first midnight.
603+ long timeAfterSecondMidnight = time ("2006-10-29T06:00:00.000-03:30" );
604+ long floor = rounding .round (timeAfterSecondMidnight );
605+ assertThat (floor , isDate (time ("2006-10-29T00:00:00.000-02:30" ), tz ));
606+ long ceiling = rounding .nextRoundingValue (timeAfterSecondMidnight );
607+ assertThat (ceiling , isDate (time ("2006-10-30T00:00:00.000-03:30" ), tz ));
608+ assertInterval (floor , timeAfterSecondMidnight , ceiling , rounding , tz );
609+ }
610+ }
611+
517612 /**
518613 * tests for dst transition with overlaps and day roundings.
519614 */
@@ -527,12 +622,17 @@ public void testDST_END_Edgecases() {
527622
528623 // Sunday, 29 October 2000, 01:00:00 clocks were turned backward 1 hour
529624 // to Sunday, 29 October 2000, 00:00:00 local standard time instead
625+ // which means there were two midnights that day.
530626
531627 long midnightBeforeTransition = time ("2000-10-29T00:00:00" , tz );
628+ long midnightOfTransition = time ("2000-10-29T00:00:00-01:00" );
629+ assertEquals (60L * 60L * 1000L , midnightOfTransition - midnightBeforeTransition );
532630 long nextMidnight = time ("2000-10-30T00:00:00" , tz );
533631
534632 assertInterval (midnightBeforeTransition , nextMidnight , rounding , 25 * 60 , tz );
535633
634+ assertThat (rounding .round (time ("2000-10-29T06:00:00-01:00" )), isDate (time ("2000-10-29T00:00:00Z" ), tz ));
635+
536636 // Second case, dst happens at 0am local time, switching back one hour to 23pm local time.
537637 // We want the overlapping hour to count for the previous day here
538638
@@ -584,26 +684,52 @@ private static void assertInterval(long rounded, long nextRoundingValue, Roundin
584684 * @param nextRoundingValue the expected upper end of the rounding interval
585685 * @param rounding the rounding instance
586686 */
587- private static void assertInterval (long rounded , long unrounded , long nextRoundingValue , Rounding rounding ,
588- DateTimeZone tz ) {
589- assert rounded <= unrounded && unrounded <= nextRoundingValue ;
687+ private static void assertInterval (long rounded , long unrounded , long nextRoundingValue , Rounding rounding , DateTimeZone tz ) {
590688 assertThat ("rounding should be idempotent " , rounding .round (rounded ), isDate (rounded , tz ));
591689 assertThat ("rounded value smaller or equal than unrounded" + rounding , rounded , lessThanOrEqualTo (unrounded ));
592690 assertThat ("values less than rounded should round further down" + rounding , rounding .round (rounded - 1 ), lessThan (rounded ));
593- assertThat ("nextRounding value should be greater than date" + rounding , nextRoundingValue , greaterThan (unrounded ));
594691 assertThat ("nextRounding value should be a rounded date" , rounding .round (nextRoundingValue ), isDate (nextRoundingValue , tz ));
595692 assertThat ("values above nextRounding should round down there" , rounding .round (nextRoundingValue + 1 ),
596693 isDate (nextRoundingValue , tz ));
597694
598- long dateBetween = dateBetween (rounded , nextRoundingValue );
599- assertThat ("dateBetween should round down to roundedDate" , rounding .round (dateBetween ), isDate (rounded , tz ));
600- assertThat ("dateBetween should round up to nextRoundingValue" , rounding .nextRoundingValue (dateBetween ),
601- isDate (nextRoundingValue , tz ));
695+ if (isTimeWithWellDefinedRounding (tz , unrounded )) {
696+ assertThat ("nextRounding value should be greater than date" + rounding , nextRoundingValue , greaterThan (unrounded ));
697+
698+ long dateBetween = dateBetween (rounded , nextRoundingValue );
699+ assertThat ("dateBetween [" + new DateTime (dateBetween , tz ) + "] should round down to roundedDate" ,
700+ rounding .round (dateBetween ), isDate (rounded , tz ));
701+ assertThat ("dateBetween [" + new DateTime (dateBetween , tz ) + "] should round up to nextRoundingValue" ,
702+ rounding .nextRoundingValue (dateBetween ), isDate (nextRoundingValue , tz ));
703+ }
704+ }
705+
706+ private static boolean isTimeWithWellDefinedRounding (DateTimeZone tz , long t ) {
707+ if (tz .getID ().equals ("America/St_Johns" )
708+ || tz .getID ().equals ("America/Goose_Bay" )
709+ || tz .getID ().equals ("America/Moncton" )
710+ || tz .getID ().equals ("Canada/Newfoundland" )) {
711+
712+ // Clocks went back at 00:01 between 1987 and 2010, causing overlapping days.
713+ // These timezones are otherwise uninteresting, so just skip this period.
714+
715+ return t <= time ("1987-10-01T00:00:00Z" )
716+ || t >= time ("2010-12-01T00:00:00Z" );
717+ }
718+
719+ if (tz .getID ().equals ("Antarctica/Casey" )) {
720+
721+ // Clocks went back 3 hours at 02:00 on 2010-03-05, causing overlapping days.
722+
723+ return t <= time ("2010-03-03T00:00:00Z" )
724+ || t >= time ("2010-03-07T00:00:00Z" );
725+ }
726+
727+ return true ;
602728 }
603729
604730 private static long dateBetween (long lower , long upper ) {
605- long dateBetween = lower + Math . abs (( randomLong () % ( upper - lower )) );
606- assert lower <= dateBetween && dateBetween <= upper ;
731+ long dateBetween = randomLongBetween ( lower , upper - 1 );
732+ assert lower <= dateBetween && dateBetween < upper ;
607733 return dateBetween ;
608734 }
609735
@@ -629,7 +755,7 @@ public boolean matchesSafely(final Long item) {
629755
630756 @ Override
631757 public void describeTo (Description description ) {
632- description .appendText ("Expected: " + new DateTime (expected , tz ) + " [" + expected + "] " );
758+ description .appendText (new DateTime (expected , tz ) + " [" + expected + "] " );
633759 }
634760
635761 @ Override
0 commit comments