diff --git a/quartz/src/main/java/org/quartz/impl/triggers/CalendarIntervalTriggerImpl.java b/quartz/src/main/java/org/quartz/impl/triggers/CalendarIntervalTriggerImpl.java
index f1609af0f..c64805595 100644
--- a/quartz/src/main/java/org/quartz/impl/triggers/CalendarIntervalTriggerImpl.java
+++ b/quartz/src/main/java/org/quartz/impl/triggers/CalendarIntervalTriggerImpl.java
@@ -19,9 +19,14 @@
package org.quartz.impl.triggers;
+import java.time.DateTimeException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
+import java.util.function.Supplier;
import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.CalendarIntervalTrigger;
@@ -36,13 +41,12 @@
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
-
/**
*
The trigger will fire every N (see {@link #setRepeatInterval(int)} ) units of calendar time
- * (see {@link #setRepeatIntervalUnit(org.quartz.DateBuilder.IntervalUnit)}) as specified in the trigger's definition.
+ * (see {@link #setRepeatIntervalUnit(IntervalUnit)}) as specified in the trigger's definition.
* This trigger can achieve schedules that are not possible with {@link SimpleTrigger} (e.g
* because months are not a fixed number of seconds) or {@link CronTrigger} (e.g. because
* "every 5 months" is not an even divisor of 12).
@@ -78,7 +82,7 @@ public class CalendarIntervalTriggerImpl extends AbstractTrigger YEAR_TO_GIVEUP_SCHEDULING_AT) {
+ if (c.get(Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
nextFireTime = null;
}
}
@@ -552,9 +556,9 @@ public void updateWithNewCalendar(org.quartz.Calendar calendar, long misfireThre
break;
//avoid infinite loop
- java.util.Calendar c = java.util.Calendar.getInstance();
+ Calendar c = Calendar.getInstance();
c.setTime(nextFireTime);
- if (c.get(java.util.Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
+ if (c.get(Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
nextFireTime = null;
}
@@ -596,9 +600,9 @@ public Date computeFirstFireTime(org.quartz.Calendar calendar) {
break;
//avoid infinite loop
- java.util.Calendar c = java.util.Calendar.getInstance();
+ Calendar c = Calendar.getInstance();
c.setTime(nextFireTime);
- if (c.get(java.util.Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
+ if (c.get(Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
return null;
}
}
@@ -662,6 +666,52 @@ public void setPreviousFireTime(Date previousFireTime) {
this.previousFireTime = previousFireTime;
}
+ private ChronoUnit intervalUnitToChronoUnit() {
+ switch (getRepeatIntervalUnit()) {
+ case MILLISECOND:
+ return ChronoUnit.MILLIS;
+ case SECOND:
+ return ChronoUnit.SECONDS;
+ case MINUTE:
+ return ChronoUnit.MINUTES;
+ case HOUR:
+ return ChronoUnit.HOURS;
+ case DAY:
+ return ChronoUnit.DAYS;
+ case WEEK:
+ return ChronoUnit.WEEKS;
+ case MONTH:
+ return ChronoUnit.MONTHS;
+ case YEAR:
+ return ChronoUnit.YEARS;
+ default:
+ throw new IllegalStateException("Unknown repeat interval unit: " + getRepeatIntervalUnit());
+ }
+ }
+
+ private int intervalUnitToCalendarField() {
+ switch (getRepeatIntervalUnit()) {
+ case MILLISECOND:
+ return Calendar.MILLISECOND;
+ case SECOND:
+ return Calendar.SECOND;
+ case MINUTE:
+ return Calendar.MINUTE;
+ case HOUR:
+ return Calendar.HOUR_OF_DAY;
+ case DAY:
+ return Calendar.DAY_OF_MONTH;
+ case WEEK:
+ return Calendar.WEEK_OF_YEAR;
+ case MONTH:
+ return Calendar.MONTH;
+ case YEAR:
+ return Calendar.YEAR;
+ default:
+ throw new IllegalStateException("Unknown repeat interval unit: " + getRepeatIntervalUnit());
+ }
+ }
+
/**
*
* Returns the next time at which the DateIntervalTrigger will
@@ -698,148 +748,41 @@ protected Date getFireTimeAfter(Date afterTime, boolean ignoreEndTime) {
return new Date(startMillis);
}
-
- long secondsAfterStart = 1 + (afterMillis - startMillis) / 1000L;
+ final ChronoUnit chronoUnit = intervalUnitToChronoUnit();
- Date time = null;
- long repeatLong = getRepeatInterval();
-
- Calendar aTime = Calendar.getInstance();
- aTime.setTime(afterTime);
+ final ZoneId zoneId = timeZone == null ? ZoneId.systemDefault() : timeZone.toZoneId();
+ final ZonedDateTime aDateTime = ZonedDateTime.ofInstant(afterTime.toInstant(), zoneId);
+ final ZonedDateTime sDateTime = ZonedDateTime.ofInstant(startTime.toInstant(), zoneId);
+ final long unitsSinceStart = chronoUnit.between(sDateTime, aDateTime);
- Calendar sTime = Calendar.getInstance();
- if(timeZone != null)
- sTime.setTimeZone(timeZone);
- sTime.setTime(getStartTime());
- sTime.setLenient(true);
-
- if(getRepeatIntervalUnit().equals(IntervalUnit.SECOND)) {
- long jumpCount = secondsAfterStart / repeatLong;
- if(secondsAfterStart % repeatLong != 0)
- jumpCount++;
- sTime.add(Calendar.SECOND, getRepeatInterval() * (int)jumpCount);
- time = sTime.getTime();
+ Supplier advancer = new Supplier<>() {
+ private int intervalUnitsSinceStart = getRepeatInterval() * (int)(unitsSinceStart / getRepeatInterval());
+ @Override
+ public ZonedDateTime get() {
+ ZonedDateTime result = sDateTime.plus(intervalUnitsSinceStart, chronoUnit);
+ if(result.getYear() >= YEAR_TO_GIVEUP_SCHEDULING_AT) {
+ return null;
+ }
+ intervalUnitsSinceStart += getRepeatInterval();
+ return result;
+ }
+ };
+
+ ZonedDateTime nextFireDateTime = advancer.get();
+ while(nextFireDateTime != null && !nextFireDateTime.isAfter(aDateTime)) {
+ nextFireDateTime = advancer.get();
}
- else if(getRepeatIntervalUnit().equals(IntervalUnit.MINUTE)) {
- long jumpCount = secondsAfterStart / (repeatLong * 60L);
- if(secondsAfterStart % (repeatLong * 60L) != 0)
- jumpCount++;
- sTime.add(Calendar.MINUTE, getRepeatInterval() * (int)jumpCount);
- time = sTime.getTime();
+
+ if(nextFireDateTime != null && chronoUnit.compareTo(ChronoUnit.DAYS) >= 0) {
+ nextFireDateTime = advanceIfNeeded(nextFireDateTime, sDateTime, aDateTime, advancer);
}
- else if(getRepeatIntervalUnit().equals(IntervalUnit.HOUR)) {
- long jumpCount = secondsAfterStart / (repeatLong * 60L * 60L);
- if(secondsAfterStart % (repeatLong * 60L * 60L) != 0)
- jumpCount++;
- sTime.add(Calendar.HOUR_OF_DAY, getRepeatInterval() * (int)jumpCount);
- time = sTime.getTime();
+
+ if(nextFireDateTime == null) {
+ return null;
}
- else { // intervals a day or greater ...
- int initialHourOfDay = sTime.get(Calendar.HOUR_OF_DAY);
-
- if(getRepeatIntervalUnit().equals(IntervalUnit.DAY)) {
- sTime.setLenient(true);
-
- // Because intervals greater than an hour have an non-fixed number
- // of seconds in them (due to daylight savings, variation number of
- // days in each month, leap year, etc. ) we can't jump forward an
- // exact number of seconds to calculate the fire time as we can
- // with the second, minute and hour intervals. But, rather
- // than slowly crawling our way there by iteratively adding the
- // increment to the start time until we reach the "after time",
- // we can first make a big leap most of the way there...
-
- long jumpCount = secondsAfterStart / (repeatLong * 24L * 60L * 60L);
- // if we need to make a big jump, jump most of the way there,
- // but not all the way because in some cases we may over-shoot or under-shoot
- if(jumpCount > 20) {
- if(jumpCount < 50)
- jumpCount = (long) (jumpCount * 0.80);
- else if(jumpCount < 500)
- jumpCount = (long) (jumpCount * 0.90);
- else
- jumpCount = (long) (jumpCount * 0.95);
- sTime.add(java.util.Calendar.DAY_OF_YEAR, (int) (getRepeatInterval() * jumpCount));
- }
-
- // now baby-step the rest of the way there...
- while(!sTime.getTime().after(afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.DAY_OF_YEAR, getRepeatInterval());
- }
- while(daylightSavingHourShiftOccurredAndAdvanceNeeded(sTime, initialHourOfDay, afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.DAY_OF_YEAR, getRepeatInterval());
- }
- time = sTime.getTime();
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.WEEK)) {
- sTime.setLenient(true);
-
- // Because intervals greater than an hour have an non-fixed number
- // of seconds in them (due to daylight savings, variation number of
- // days in each month, leap year, etc. ) we can't jump forward an
- // exact number of seconds to calculate the fire time as we can
- // with the second, minute and hour intervals. But, rather
- // than slowly crawling our way there by iteratively adding the
- // increment to the start time until we reach the "after time",
- // we can first make a big leap most of the way there...
-
- long jumpCount = secondsAfterStart / (repeatLong * 7L * 24L * 60L * 60L);
- // if we need to make a big jump, jump most of the way there,
- // but not all the way because in some cases we may over-shoot or under-shoot
- if(jumpCount > 20) {
- if(jumpCount < 50)
- jumpCount = (long) (jumpCount * 0.80);
- else if(jumpCount < 500)
- jumpCount = (long) (jumpCount * 0.90);
- else
- jumpCount = (long) (jumpCount * 0.95);
- sTime.add(java.util.Calendar.WEEK_OF_YEAR, (int) (getRepeatInterval() * jumpCount));
- }
-
- while(!sTime.getTime().after(afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.WEEK_OF_YEAR, getRepeatInterval());
- }
- while(daylightSavingHourShiftOccurredAndAdvanceNeeded(sTime, initialHourOfDay, afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.WEEK_OF_YEAR, getRepeatInterval());
- }
- time = sTime.getTime();
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.MONTH)) {
- sTime.setLenient(true);
-
- // because of the large variation in size of months, and
- // because months are already large blocks of time, we will
- // just advance via brute-force iteration.
-
- while(!sTime.getTime().after(afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.MONTH, getRepeatInterval());
- }
- while(daylightSavingHourShiftOccurredAndAdvanceNeeded(sTime, initialHourOfDay, afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.MONTH, getRepeatInterval());
- }
- time = sTime.getTime();
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.YEAR)) {
-
- while(!sTime.getTime().after(afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.YEAR, getRepeatInterval());
- }
- while(daylightSavingHourShiftOccurredAndAdvanceNeeded(sTime, initialHourOfDay, afterTime) &&
- (sTime.get(java.util.Calendar.YEAR) < YEAR_TO_GIVEUP_SCHEDULING_AT)) {
- sTime.add(java.util.Calendar.YEAR, getRepeatInterval());
- }
- time = sTime.getTime();
- }
- } // case of interval of a day or greater
-
+ Date time = Date.from(nextFireDateTime.toInstant());
+
if (!ignoreEndTime && (endMillis <= time.getTime())) {
return null;
}
@@ -847,16 +790,36 @@ else if(getRepeatIntervalUnit().equals(IntervalUnit.YEAR)) {
return time;
}
- private boolean daylightSavingHourShiftOccurredAndAdvanceNeeded(Calendar newTime, int initialHourOfDay, Date afterTime) {
- if(isPreserveHourOfDayAcrossDaylightSavings() && newTime.get(Calendar.HOUR_OF_DAY) != initialHourOfDay) {
- newTime.set(Calendar.HOUR_OF_DAY, initialHourOfDay);
- if (newTime.get(Calendar.HOUR_OF_DAY) != initialHourOfDay) {
- return isSkipDayIfHourDoesNotExist();
- } else {
- return !newTime.getTime().after(afterTime);
+ private ZonedDateTime advanceIfNeeded(ZonedDateTime nextFireDateTime,
+ ZonedDateTime startDateTime,
+ ZonedDateTime afterDateTime,
+ Supplier advancer) {
+ if(isPreserveHourOfDayAcrossDaylightSavings()) {
+ int initialHourOfDay = startDateTime.getHour();
+ // This may iterate multiple times in the case of skipDayIfHourDoesNotExist and yearly intervals
+ while(nextFireDateTime != null && nextFireDateTime.getHour() != initialHourOfDay) {
+ ZonedDateTime adjustedDateTime;
+ try {
+ adjustedDateTime = nextFireDateTime.withHour(initialHourOfDay);
+ } catch (DateTimeException ignored) {
+ adjustedDateTime = nextFireDateTime;
+ }
+ if (adjustedDateTime.getHour() == initialHourOfDay) {
+ if (adjustedDateTime.isAfter(afterDateTime)) {
+ return adjustedDateTime;
+ } else {
+ nextFireDateTime = advancer.get();
+ }
+ } else {
+ if (isSkipDayIfHourDoesNotExist()) {
+ nextFireDateTime = advancer.get();
+ } else {
+ return nextFireDateTime;
+ }
+ }
}
}
- return false;
+ return nextFireDateTime;
}
/**
@@ -891,28 +854,8 @@ public Date getFinalFireTime() {
lTime.setTimeZone(timeZone);
lTime.setTime(fTime);
lTime.setLenient(true);
-
- if(getRepeatIntervalUnit().equals(IntervalUnit.SECOND)) {
- lTime.add(java.util.Calendar.SECOND, -1 * getRepeatInterval());
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.MINUTE)) {
- lTime.add(java.util.Calendar.MINUTE, -1 * getRepeatInterval());
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.HOUR)) {
- lTime.add(java.util.Calendar.HOUR_OF_DAY, -1 * getRepeatInterval());
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.DAY)) {
- lTime.add(java.util.Calendar.DAY_OF_YEAR, -1 * getRepeatInterval());
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.WEEK)) {
- lTime.add(java.util.Calendar.WEEK_OF_YEAR, -1 * getRepeatInterval());
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.MONTH)) {
- lTime.add(java.util.Calendar.MONTH, -1 * getRepeatInterval());
- }
- else if(getRepeatIntervalUnit().equals(IntervalUnit.YEAR)) {
- lTime.add(java.util.Calendar.YEAR, -1 * getRepeatInterval());
- }
+
+ lTime.add(intervalUnitToCalendarField(), -1 * getRepeatInterval());
return lTime.getTime();
}
diff --git a/quartz/src/test/java/org/quartz/CalendarIntervalTriggerTest.java b/quartz/src/test/java/org/quartz/CalendarIntervalTriggerTest.java
index fdbdae378..a31a5ea96 100644
--- a/quartz/src/test/java/org/quartz/CalendarIntervalTriggerTest.java
+++ b/quartz/src/test/java/org/quartz/CalendarIntervalTriggerTest.java
@@ -24,10 +24,15 @@
import java.text.ParseException;
+import java.time.*;
+import java.time.temporal.ChronoUnit;
+import java.time.zone.ZoneOffsetTransition;
+import java.time.zone.ZoneRules;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
+import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.quartz.DateBuilder.IntervalUnit;
@@ -105,6 +110,29 @@ void testYearlyIntervalGetFireTimeAfter() {
assertEquals(targetCalendar.getTime(), secondTime, "Year increment result not as expected.");
}
+ @Test
+ void testYearlyIntervalGetFireTimeAfterLeap() {
+ Calendar startCalendar = Calendar.getInstance();
+ final int leapYear = 4 * (startCalendar.get(Calendar.YEAR) / 4);
+ startCalendar.set(leapYear, Calendar.FEBRUARY, 29, 9, 30, 17);
+
+ CalendarIntervalTriggerImpl yearlyTrigger = new CalendarIntervalTriggerImpl();
+ yearlyTrigger.setStartTime(startCalendar.getTime());
+ yearlyTrigger.setRepeatIntervalUnit(IntervalUnit.YEAR);
+ yearlyTrigger.setRepeatInterval(1);
+
+ List fireTimes = TriggerUtils.computeFireTimes(yearlyTrigger, null, 5);
+ Calendar testCal = Calendar.getInstance();
+ assertEquals(List.of(leapYear, leapYear + 1,leapYear + 2,leapYear + 3,leapYear + 4), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.YEAR))
+ .collect(Collectors.toList()));
+ assertEquals(List.of(29,28,28,28,29), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.DAY_OF_MONTH))
+ .collect(Collectors.toList()));
+ }
+
@Test
void testMonthlyIntervalGetFireTimeAfter() {
@@ -128,6 +156,85 @@ void testMonthlyIntervalGetFireTimeAfter() {
assertEquals(targetCalendar.getTime(), fifthTime, "Month increment result not as expected.");
}
+
+ @Test
+ void testMonthIntervalGetFireTimeAfterOn31() {
+ Calendar startCalendar = Calendar.getInstance();
+ int year = startCalendar.get(Calendar.YEAR);
+ startCalendar.set(year, Calendar.MARCH, 31, 9, 30, 17);
+
+ CalendarIntervalTriggerImpl yearlyTrigger = new CalendarIntervalTriggerImpl();
+ yearlyTrigger.setStartTime(startCalendar.getTime());
+ yearlyTrigger.setRepeatIntervalUnit(IntervalUnit.MONTH);
+ yearlyTrigger.setRepeatInterval(1);
+
+ List fireTimes = TriggerUtils.computeFireTimes(yearlyTrigger, null, 3);
+ Calendar testCal = Calendar.getInstance();
+ assertEquals(List.of(31,30,31), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.DAY_OF_MONTH))
+ .collect(Collectors.toList()));
+ }
+
+ private Calendar get30MinBeforeSpringForward() {
+ Calendar cal = Calendar.getInstance();
+ ZoneId zoneId = ZoneId.of("America/New_York");
+ ZoneRules zoneRules = zoneId.getRules();
+ Instant now = Instant.now();
+ ZoneOffsetTransition transition = zoneRules.nextTransition(now);
+ if(transition == null) {
+ // Should not happen
+ return cal;
+ }
+
+ if(transition.isOverlap()) {
+ transition = zoneRules.previousTransition(now);
+ }
+ Instant transitionInstant = transition.getInstant();
+ cal.setTimeZone(TimeZone.getTimeZone(zoneId));
+ cal.setTimeInMillis(transitionInstant.minus(30, ChronoUnit.MINUTES).toEpochMilli());
+ cal.setLenient(true);
+ return cal;
+ }
+
+ @Test
+ void testMonthlyIntervalGetFireTimeAfterSpringForward() {
+ Calendar startCalendar = get30MinBeforeSpringForward();
+
+ startCalendar.add(Calendar.MONTH, - 1);
+ startCalendar.add(Calendar.HOUR_OF_DAY, 1);
+ int month = startCalendar.get(Calendar.MONTH);
+ int day = startCalendar.get(Calendar.DAY_OF_MONTH);
+ int hour = startCalendar.get(Calendar.HOUR_OF_DAY);
+
+ CalendarIntervalTriggerImpl yearlyTrigger = new CalendarIntervalTriggerImpl();
+ yearlyTrigger.setStartTime(startCalendar.getTime());
+ yearlyTrigger.setRepeatIntervalUnit(IntervalUnit.MONTH);
+ yearlyTrigger.setRepeatInterval(1);
+ yearlyTrigger.setTimeZone(startCalendar.getTimeZone());
+
+ List fireTimes = TriggerUtils.computeFireTimes(yearlyTrigger, null, 3);
+
+ Calendar testCal = Calendar.getInstance(startCalendar.getTimeZone());
+ ZonedDateTime localDateTime = startCalendar.toInstant().atZone(startCalendar.getTimeZone().toZoneId());
+ String hint = "Failed for " + localDateTime;
+ // Check that no month is skipped
+ assertEquals(List.of(month, month + 1, month + 2), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.MONTH))
+ .collect(Collectors.toList()), hint);
+ // Check that day of month is preserved
+ assertEquals(List.of(day, day, day), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.DAY_OF_MONTH))
+ .collect(Collectors.toList()), hint);
+ // Check that spring forward gap does not affect the following trigger times
+ assertEquals(List.of(hour, hour+1, hour), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.HOUR_OF_DAY))
+ .collect(Collectors.toList()), hint);
+ }
+
@Test
void testWeeklyIntervalGetFireTimeAfter() {
@@ -174,6 +281,76 @@ void testDailyIntervalGetFireTimeAfter() {
assertEquals(targetCalendar.getTime(), fifthTime, "Day increment result not as expected.");
}
+
+ @Test
+ void testDailyIntervalGetFireTimeAfterSpringForward() {
+ Calendar startCalendar = get30MinBeforeSpringForward();
+
+ startCalendar.add(Calendar.DAY_OF_MONTH, - 1);
+ startCalendar.add(Calendar.HOUR_OF_DAY, 1);
+ startCalendar.add(Calendar.MINUTE, 10);
+ int day = startCalendar.get(Calendar.DAY_OF_YEAR);
+ int hour = startCalendar.get(Calendar.HOUR_OF_DAY);
+
+ CalendarIntervalTriggerImpl yearlyTrigger = new CalendarIntervalTriggerImpl();
+ yearlyTrigger.setStartTime(startCalendar.getTime());
+ yearlyTrigger.setRepeatIntervalUnit(IntervalUnit.DAY);
+ yearlyTrigger.setRepeatInterval(1);
+ yearlyTrigger.setTimeZone(startCalendar.getTimeZone());
+
+ List fireTimes = TriggerUtils.computeFireTimes(yearlyTrigger, null, 3);
+
+ Calendar testCal = Calendar.getInstance(startCalendar.getTimeZone());
+ ZonedDateTime localDateTime = startCalendar.toInstant().atZone(startCalendar.getTimeZone().toZoneId());
+ String hint = "Failed for " + localDateTime;
+ // Check that no day is skipped
+ assertEquals(List.of(day, day + 1, day + 2), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.DAY_OF_YEAR))
+ .collect(Collectors.toList()), hint);
+ // Check that spring forward gap does not affect the following trigger times
+ assertEquals(List.of(hour, hour+1, hour), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.HOUR_OF_DAY))
+ .collect(Collectors.toList()), hint);
+ }
+
+ @Test
+ void testDailyIntervalGetFireTimeAfterSpringForwardPreserveHour() {
+ Calendar startCalendar = get30MinBeforeSpringForward();
+
+ startCalendar.add(Calendar.DAY_OF_MONTH, - 1);
+ startCalendar.add(Calendar.HOUR_OF_DAY, 1);
+ startCalendar.add(Calendar.MINUTE, 10);
+ int day = startCalendar.get(Calendar.DAY_OF_YEAR);
+ int hour = startCalendar.get(Calendar.HOUR_OF_DAY);
+
+ CalendarIntervalTriggerImpl yearlyTrigger = new CalendarIntervalTriggerImpl();
+ yearlyTrigger.setStartTime(startCalendar.getTime());
+ yearlyTrigger.setRepeatIntervalUnit(IntervalUnit.DAY);
+ yearlyTrigger.setRepeatInterval(1);
+ yearlyTrigger.setTimeZone(startCalendar.getTimeZone());
+
+ yearlyTrigger.setPreserveHourOfDayAcrossDaylightSavings(true);
+ yearlyTrigger.setSkipDayIfHourDoesNotExist(true);
+
+ List fireTimes = TriggerUtils.computeFireTimes(yearlyTrigger, null, 3);
+
+ Calendar testCal = Calendar.getInstance(startCalendar.getTimeZone());
+ ZonedDateTime localDateTime = startCalendar.toInstant().atZone(startCalendar.getTimeZone().toZoneId());
+ String hint = "Failed for " + localDateTime;
+ // Check that a DTS day got skipped
+ assertEquals(List.of(day, day + 2, day + 3), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.DAY_OF_YEAR))
+ .collect(Collectors.toList()), hint);
+ // Check that the hour is preserved
+ assertEquals(List.of(hour, hour, hour), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.HOUR_OF_DAY))
+ .collect(Collectors.toList()), hint);
+ }
+
@Test
void testHourlyIntervalGetFireTimeAfter() {
@@ -197,6 +374,31 @@ void testHourlyIntervalGetFireTimeAfter() {
assertEquals(targetCalendar.getTime(), fifthTime, "Hour increment result not as expected.");
}
+
+ @Test
+ void testHourlyIntervalGetFireTimeAfterSpringForward() {
+ Calendar startCalendar = get30MinBeforeSpringForward();
+
+ int hour = startCalendar.get(Calendar.HOUR_OF_DAY);
+
+ CalendarIntervalTriggerImpl yearlyTrigger = new CalendarIntervalTriggerImpl();
+ yearlyTrigger.setStartTime(startCalendar.getTime());
+ yearlyTrigger.setTimeZone(startCalendar.getTimeZone());
+ yearlyTrigger.setRepeatIntervalUnit(DateBuilder.IntervalUnit.HOUR);
+ yearlyTrigger.setRepeatInterval(1);
+
+ List fireTimes = TriggerUtils.computeFireTimes(yearlyTrigger, null, 4);
+ ZonedDateTime localDateTime = startCalendar.toInstant().atZone(startCalendar.getTimeZone().toZoneId());
+ String hint = "Failed for " + localDateTime;
+
+ Calendar testCal = Calendar.getInstance(startCalendar.getTimeZone());
+ // Check that hourly firings are continuous across the spring forward gap
+ assertEquals(List.of(hour, hour + 2, hour + 3, hour + 4), fireTimes.stream()
+ .peek(testCal::setTime)
+ .map(d -> testCal.get(Calendar.HOUR_OF_DAY))
+ .collect(Collectors.toList()), hint);
+ }
+
@Test
void testMinutelyIntervalGetFireTimeAfter() {