diff --git a/Bedtime/Bedtime/ContentView.swift b/Bedtime/Bedtime/ContentView.swift index 1819c41..6582649 100644 --- a/Bedtime/Bedtime/ContentView.swift +++ b/Bedtime/Bedtime/ContentView.swift @@ -45,7 +45,8 @@ struct ContentView: View { ViewModel.calculateSleepBank( sleepSessions: healthKitManager.sleepSessions, goalHours: userPreferences.sleepGoalHours, - recentDays: userPreferences.sleepBankDays + recentDays: userPreferences.sleepBankDays, + wakeTime: userPreferences.wakeTime ) } @@ -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 diff --git a/Bedtime/Bedtime/Models/SleepInsights.swift b/Bedtime/Bedtime/Models/SleepInsights.swift index d721814..a861564 100644 --- a/Bedtime/Bedtime/Models/SleepInsights.swift +++ b/Bedtime/Bedtime/Models/SleepInsights.swift @@ -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 } @@ -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) } diff --git a/Bedtime/Bedtime/Models/ViewModel.swift b/Bedtime/Bedtime/Models/ViewModel.swift index 381d9e4..9cae4b4 100644 --- a/Bedtime/Bedtime/Models/ViewModel.swift +++ b/Bedtime/Bedtime/Models/ViewModel.swift @@ -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 diff --git a/Bedtime/Bedtime/Utils/SleepWindow.swift b/Bedtime/Bedtime/Utils/SleepWindow.swift index f9c6551..b3a9c89 100644 --- a/Bedtime/Bedtime/Utils/SleepWindow.swift +++ b/Bedtime/Bedtime/Utils/SleepWindow.swift @@ -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 + } }