Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Bedtime/Bedtime/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ struct ContentView: View {
ViewModel.calculateSleepBank(
sleepSessions: healthKitManager.sleepSessions,
goalHours: userPreferences.sleepGoalHours,
recentDays: userPreferences.sleepBankDays
recentDays: userPreferences.sleepBankDays,
wakeTime: userPreferences.wakeTime
)
}

Expand All @@ -62,6 +63,7 @@ struct ContentView: View {
SleepInsightsEngine.generateInsight(
sleepSessions: healthKitManager.sleepSessions,
goalHours: userPreferences.sleepGoalHours,
wakeTime: userPreferences.wakeTime,
maxSleepHours: SleepWindow.maxSleepHours(
earliestBedtime: userPreferences.earliestReasonableBedtime,
wakeTime: userPreferences.wakeTime
Expand Down
13 changes: 10 additions & 3 deletions Bedtime/Bedtime/Models/SleepInsights.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ enum SleepInsightsEngine {
static func generateInsight(
sleepSessions: [Date: [SleepSession]],
goalHours: Double,
wakeTime: Date,
maxSleepHours: Double
) -> SleepBankInsight? {
let snapshots = windowBalances(sleepSessions: sleepSessions, goalHours: goalHours)
let snapshots = windowBalances(
sleepSessions: sleepSessions,
goalHours: goalHours,
wakeTime: wakeTime
)
guard snapshots.contains(where: { $0.sleepBank.averageHours != nil }) else {
return nil
}
Expand Down Expand Up @@ -71,13 +76,15 @@ enum SleepInsightsEngine {

static func windowBalances(
sleepSessions: [Date: [SleepSession]],
goalHours: Double
goalHours: Double,
wakeTime: Date
) -> [SleepWindowBalance] {
windowRange.map { days in
let bank = ViewModel.calculateSleepBank(
sleepSessions: sleepSessions,
goalHours: goalHours,
recentDays: days
recentDays: days,
wakeTime: wakeTime
)
return SleepWindowBalance(days: days, balance: bank.currentBalance, sleepBank: bank)
}
Expand Down
11 changes: 9 additions & 2 deletions Bedtime/Bedtime/Models/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ class ViewModel {
static func calculateSleepBank(
sleepSessions: [Date: [SleepSession]],
goalHours: Double,
recentDays: Int
recentDays: Int,
wakeTime: Date,
referenceDate: Date = Date()
) -> SleepBank {
let calendar = Calendar.current
let endDate = Date()
let endDate = SleepWindow.effectiveSleepBankEndDate(
now: referenceDate,
wakeTime: wakeTime,
sleepSessions: sleepSessions,
calendar: calendar
)
let startDate = calendar.date(byAdding: .day, value: -recentDays, to: endDate) ?? endDate

// Filter sessions from the last N days
Expand Down
32 changes: 32 additions & 0 deletions Bedtime/Bedtime/Utils/SleepWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,36 @@ struct SleepWindow {
let components = calendar.dateComponents([.hour, .minute], from: date)
return (components.hour ?? 0) * 60 + (components.minute ?? 0)
}

/// End of the sleep-bank lookback window. Before today's wake time, if today has no
/// recorded sleep yet, the current calendar day is still in progress — use end of
/// yesterday so midnight doesn't shift the lookback window.
static func effectiveSleepBankEndDate(
now: Date = Date(),
wakeTime: Date,
sleepSessions: [Date: [SleepSession]],
calendar: Calendar = .current
) -> Date {
let today = calendar.startOfDay(for: now)
let todayHasData = !(sleepSessions[today]?.isEmpty ?? true)
if todayHasData {
return now
}

let wakeComponents = calendar.dateComponents([.hour, .minute], from: wakeTime)
guard let todayWake = calendar.date(
bySettingHour: wakeComponents.hour ?? 0,
minute: wakeComponents.minute ?? 0,
second: 0,
of: today
) else {
return now
}

if now < todayWake {
return calendar.date(byAdding: .second, value: -1, to: today) ?? now
}

return now
}
}