The five rules
These are Onset's own rules of thumb, not facts about sleep. Each number is a cutoff I chose so the app ends up with one sensible night per morning.
- Join pieces that are close together. Onset joins asleep samples up to 90 minutes apart into one session, so a wake-up in the middle of the night doesn't split it in two.
- Ignore short sessions. Onset only counts a session as the main sleep if it has at least three hours of sleep. The cutoff is mostly there to leave out naps.
- One night per morning. Sessions are grouped by the day you woke up, and the longest one wins. That also handles two sources, like a watch and a phone, recording the same night.
- Ignore very short awake segments. Onset doesn't count awake time under two minutes as a wake-up. The threshold filters out brief blips between recorded stages.
- In bed means shortly before sleep. Onset only uses an in-bed record for time to fall asleep if it starts within three hours before sleep. It treats anything earlier as something else.
The code
enum SleepSessionBuilder {
static let mergeGap: TimeInterval = 90 * 60
static let minimumMainSleep: TimeInterval = 3 * 60 * 60
static let inBedLookback: TimeInterval = 3 * 60 * 60
static let minimumWakeUp: TimeInterval = 2 * 60
}
// Rule 1: join asleep samples that are close together.
var clusters: [[SleepSampleInterval]] = []
var current = [sorted[0]]
var currentEnd = sorted[0].end
for interval in sorted.dropFirst() {
if interval.start.timeIntervalSince(currentEnd) <= mergeGap {
current.append(interval)
currentEnd = max(currentEnd, interval.end)
} else {
clusters.append(current)
current = [interval]
currentEnd = interval.end
}
}
clusters.append(current)
// Rules 2 and 3: drop naps, then keep the longest session per wake day.
let candidates = clusters.compactMap { summary(for: $0, context: context) }
.filter { TimeInterval($0.totalSleepMinutes * 60) >= minimumMainSleep }
let byWakeDay = Dictionary(grouping: candidates) {
calendar.startOfDay(for: $0.wakeTime)
}
let nights = byWakeDay.values.compactMap {
$0.max { $0.totalSleepMinutes < $1.totalSleepMinutes }
}
Samples are sorted by start time first, and anything where the end isn't after the start is dropped before clustering.
What Onset reads
Onset only reads sleep data, and only if you choose to connect Apple Health. It works on your iPhone, doesn't store copies of the samples, and never writes anything back. The privacy policy has the details.