private Date getMatchSpecificTime()

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();
      }
    }
  }