computeNextCronRun function

DateTime? computeNextCronRun(
  1. CronFields fields,
  2. DateTime from
)

Compute the next DateTime strictly after from that matches the cron fields, using the process's local timezone. Walks forward minute-by-minute. Bounded at 366 days; returns null if no match.

Standard cron semantics: when both dayOfMonth and dayOfWeek are constrained (neither is the full range), a date matches if EITHER matches.

DST: fixed-hour crons targeting a spring-forward gap skip the transition day. Wildcard-hour crons fire at the first valid minute after the gap.

Implementation

DateTime? computeNextCronRun(CronFields fields, DateTime from) {
  final minuteSet = fields.minute.toSet();
  final hourSet = fields.hour.toSet();
  final domSet = fields.dayOfMonth.toSet();
  final monthSet = fields.month.toSet();
  final dowSet = fields.dayOfWeek.toSet();

  // Is the field wildcarded (full range)?
  final domWild = fields.dayOfMonth.length == 31;
  final dowWild = fields.dayOfWeek.length == 7;

  // Round up to the next whole minute (strictly after `from`).
  var t = DateTime(
    from.year,
    from.month,
    from.day,
    from.hour,
    from.minute,
  ).add(const Duration(minutes: 1));

  const maxIter = 366 * 24 * 60;
  for (int i = 0; i < maxIter; i++) {
    final month = t.month;
    if (!monthSet.contains(month)) {
      // Jump to start of next month.
      if (t.month == 12) {
        t = DateTime(t.year + 1, 1, 1);
      } else {
        t = DateTime(t.year, t.month + 1, 1);
      }
      continue;
    }

    final dom = t.day;
    final dow = t.weekday % 7; // DateTime weekday: Mon=1..Sun=7, cron: Sun=0.
    final dayMatches = domWild && dowWild
        ? true
        : domWild
        ? dowSet.contains(dow)
        : dowWild
        ? domSet.contains(dom)
        : domSet.contains(dom) || dowSet.contains(dow);

    if (!dayMatches) {
      // Jump to start of next day.
      t = DateTime(t.year, t.month, t.day + 1);
      continue;
    }

    if (!hourSet.contains(t.hour)) {
      t = DateTime(t.year, t.month, t.day, t.hour + 1);
      continue;
    }

    if (!minuteSet.contains(t.minute)) {
      t = t.add(const Duration(minutes: 1));
      continue;
    }

    return t;
  }

  return null;
}