clusterIntoSessions<T> function

List<List<T>> clusterIntoSessions<T>(
  1. List<T> items, {
  2. required DateTime timestamp(
    1. T
    ),
  3. required Duration maxGap,
})

Splits items into chronological sessions, starting a new session whenever the gap between an item and its predecessor exceeds maxGap.

Items are sorted by timestamp first, so unsorted input is handled. Equal timestamps stay in the same session (a zero gap never exceeds a non-negative maxGap). Returns an empty list for empty input and one single-item session for one item.

Example:

final List<List<DateTime>> sessions = clusterIntoSessions<DateTime>(
  stamps,
  timestamp: (DateTime d) => d,
  maxGap: const Duration(minutes: 30),
);

Audited: 2026-06-12 11:26 EDT

Implementation

List<List<T>> clusterIntoSessions<T>(
  List<T> items, {
  required DateTime Function(T) timestamp,
  required Duration maxGap,
}) {
  final List<T> sorted = _sortedByTimestamp<T>(items, timestamp);
  // firstOrNull keeps the empty case explicit: no items means no sessions.
  final T? head = sorted.firstOrNull;
  if (head == null) {
    return <List<T>>[];
  }
  final List<List<T>> sessions = <List<T>>[];
  // Hold the open session in a local so we never index back into `sessions`
  // with a throwing accessor; it is appended to `sessions` as it is opened.
  List<T> currentSession = <T>[head];
  sessions.add(currentSession);
  DateTime previous = timestamp(head);
  // Walk the rest; a gap larger than maxGap opens a fresh session bucket.
  for (int i = 1; i < sorted.length; i++) {
    final T current = sorted[i];
    final DateTime currentStamp = timestamp(current);
    if (currentStamp.difference(previous) > maxGap) {
      currentSession = <T>[current];
      sessions.add(currentSession);
    } else {
      currentSession.add(current);
    }
    previous = currentStamp;
  }
  return sessions;
}