fillMissing function
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;
}