fillMissing function

List<DateTime> fillMissing(
  1. List<DateTime> timestamps,
  2. Duration interval
)

The complete regular grid of timestamps from the earliest to the latest of timestamps, stepping by interval; callers compare it against the input to see which slots were missing.

timestamps is copied and sorted first. With 0 or 1 samples there is nothing to fill, so the input is returned sorted as-is. The last emitted grid point is the final tick that does not pass the max timestamp.

Example:

fillMissing(
  <DateTime>[DateTime(2026, 1, 1, 0), DateTime(2026, 1, 1, 3)],
  const Duration(hours: 1),
); // [00:00, 01:00, 02:00, 03:00]

Audited: 2026-06-12 11:26 EDT

Implementation

List<DateTime> fillMissing(List<DateTime> timestamps, Duration interval) {
  // A non-positive interval would never advance `current` past `last`, so the
  // fill loop would spin forever and grow `grid` until OOM. There is no grid to
  // build without a positive step; return the input sorted as-is.
  if (interval <= Duration.zero) {
    return List<DateTime>.of(timestamps)..sort((DateTime a, DateTime b) => a.compareTo(b));
  }
  // Fewer than two points can't define a grid span; return them sorted as-is.
  if (timestamps.length < 2) {
    return List<DateTime>.of(timestamps)..sort((DateTime a, DateTime b) => a.compareTo(b));
  }
  final List<DateTime> sorted = List<DateTime>.of(timestamps)
    ..sort((DateTime a, DateTime b) => a.compareTo(b));

  // sorted has >= 2 entries (guarded above), so firstOrNull/lastOrNull are
  // non-null; the early return keeps flow analysis honest without an unsafe .first.
  final DateTime? first = sorted.firstOrNull;
  final DateTime? last = sorted.lastOrNull;
  if (first == null || last == null) {
    return sorted;
  }

  final List<DateTime> grid = <DateTime>[];
  DateTime current = first;
  // Emit every tick up to and including [last]; isAfter stops one past the end.
  while (!current.isAfter(last)) {
    grid.add(current);
    current = current.add(interval);
  }
  return grid;
}