in utils/src/main/java/com/google/borg/borgcron/GrocTimeSpecification.java [583:689]
private Date getMatchSpecificTime(Date start) {
// Convert start to local time.
Calendar calendar = Calendar.getInstance(timezone);
calendar.setTime(start);
int startYear = calendar.get(Calendar.YEAR);
int startMonth = calendar.get(Calendar.MONTH) + 1; // Calendar is 0-based
int startDayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int startHour = calendar.get(Calendar.HOUR_OF_DAY);
int startMinute = calendar.get(Calendar.MINUTE);
int startDstOffset = calendar.get(Calendar.DST_OFFSET);
// Consider at least 48 months before giving up on finding a matching date,
// to ensure we consider a leap year.
final int maxMonthsToConsider = 48;
NextGenerator monthGen = new NextGenerator(startMonth, months);
int monthsConsidered = 0;
while (true) {
// Get the next matching month - monthGen will produce an endless series
// of months that match this specification and a count of year wraps.
IntegerPair nextMonthWrapPair = monthGen.next();
int nextMonth = nextMonthWrapPair.first;
int yearWraps = nextMonthWrapPair.second;
++monthsConsidered;
calendar.set(startYear + yearWraps, nextMonth - 1, 1);
List<Integer> dayMatches = findDays(calendar);
if (dayMatches.isEmpty()) {
if (monthsConsidered >= maxMonthsToConsider) {
throw new AssertionError("no matching days");
} else {
continue;
}
}
if (calendar.get(Calendar.YEAR) == startYear && nextMonth == startMonth) {
// we're working with the current month, remove any days earlier than
// today
while (!dayMatches.isEmpty() && dayMatches.get(0) < startDayOfMonth) {
dayMatches.remove(0);
}
if (!dayMatches.isEmpty() && dayMatches.get(0) == startDayOfMonth) {
// We're working with the current day: remove first entry if it's before the starting
// hour. Note that this may not work if we encounter a jurisdiction which uses a
// 2-hour offset for daylight savings time (DST).
if (startHour > hour) {
dayMatches.remove(0);
} else if (startHour == hour) {
// We're working with the current hour, which *may* be the DST "fall back" hour
// before we've fallen back (i.e., we're still in DST). Remove first entry if it's
// before the starting minute *unless* we're in the "fall back" hour (in which case
// we leave the dayMatch alone, since it may match after we have fallen back to
// standard time). The behavior of java.util.Calendar currently seems to be to
// prefer standard time over daylight savings so, by "resetting" the calendar to
// the current time, it will switch from daylight to standard time if we are in the
// "fall back" hour that gets repeated when we fall back. (And, remember that
// java.util.Calendar months are 0-based!)
if (startMinute >= minute) {
boolean inTheFallbackHour = false;
if (startDstOffset > 0) { // Currently in DST.
calendar.set(startYear, startMonth - 1, startDayOfMonth, startHour, startMinute, 0);
if (calendar.get(Calendar.DST_OFFSET) == 0) { // We've fallen back to standard time.
inTheFallbackHour = true;
}
}
if (!inTheFallbackHour) {
// No in the "fall back" hour: drop this match, as we've already done it.
dayMatches.remove(0);
}
}
}
}
}
while (!dayMatches.isEmpty()) {
// Yay, we have a matching date and time.
int candidateDay = dayMatches.get(0);
dayMatches.remove(0);
int beforeDstOffsetMillis = calendar.get(Calendar.DST_OFFSET);
// Following will switch to standard time if it can; also it will convert a non-existent
// 02:30 into a (sprung forward) 3:30...
calendar.set(startYear + yearWraps, nextMonth - 1, candidateDay, hour, minute, 0);
calendar.set(Calendar.MILLISECOND, 0);
int dstOffsetDifferenceMillis = beforeDstOffsetMillis - calendar.get(Calendar.DST_OFFSET);
// As mentioned above, java.util.Calendar's implementation appears to be aggressive
// about "falling back" from daylight time to standard time: if 'start' is in daylight
// time and our candidate time is in that "magic hour" that gets repeated when we fall
// back (e.g., 01:30 on the last Sunday in October), it will switch to standard time
// and deliver (only) the second occurrence of the target time. If we were in daylight
// time and discover that we've switched to standard time, we try backing up the clock
// by the DST offset (a.k.a., one hour); if this switches us back to daylight time at
// some point after 'start', we will go with that time. Otherwise (e.g., backing up
// stayed in standard time), move the time back forwards to where it was.
if (dstOffsetDifferenceMillis > 0) { // We "fell back" between 'start' and now.
calendar.add(Calendar.MILLISECOND, -dstOffsetDifferenceMillis); // Back up.
if (calendar.get(Calendar.DST_OFFSET) == 0 || start.after(calendar.getTime())) {
calendar.add(Calendar.MILLISECOND, +dstOffsetDifferenceMillis);
}
}
int calhour = calendar.get(Calendar.HOUR_OF_DAY);
if (calhour != hour) {
continue;
}
return calendar.getTime();
}
}
}