Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 1 addition & 2 deletions Bedtime/Bedtime/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ struct ContentView: View {
wakeTime: userPreferences.wakeTime,
sleepGoal: userPreferences.sleepGoalHours,
sleepBank: sleepBank,
maxSleepHours: userPreferences.maxSleepHoursPerNight,
minSleepHours: userPreferences.minSleepHoursPerNight
maxSleepHours: userPreferences.effectiveMaxSleepHours
)
}

Expand Down
81 changes: 74 additions & 7 deletions Bedtime/Bedtime/Models/UserPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,88 @@ final class UserPreferences {
var wakeTime: Date
var sleepBankDays: Int
var lastUpdated: Date
var maxSleepHoursPerNight: Double
var minSleepHoursPerNight: Double

var earliestReasonableBedtime: Date

init(
sleepGoalHours: Double = 8.0,
wakeTime: Date = Calendar.current.date(from: DateComponents(hour: 7, minute: 0)) ?? Date(),
sleepBankDays: Int = 7,
maxSleepHoursPerNight: Double = 10,
minSleepHoursPerNight: Double = 5
earliestReasonableBedtime: Date = Calendar.current.date(from: DateComponents(hour: 21, minute: 0)) ?? Date()
) {
self.sleepGoalHours = sleepGoalHours
self.wakeTime = wakeTime
self.sleepBankDays = sleepBankDays
self.lastUpdated = Date()
self.maxSleepHoursPerNight = maxSleepHoursPerNight
self.minSleepHoursPerNight = minSleepHoursPerNight
self.earliestReasonableBedtime = earliestReasonableBedtime
}

/// Real hours available on the current night — DST-aware, for the main-screen
/// recommendation where "how much can I actually get tonight" is what matters.
var effectiveMaxSleepHours: Double {
Self.maxSleepHours(earliestBedtime: earliestReasonableBedtime, wakeTime: wakeTime)
}

/// Nominal wall-clock length of the sleep window (a stable 10h for a 9pm–7am
/// schedule, ignoring DST). For settings/schedule display, where a value that
/// wobbles ±1h twice a year would just be confusing.
var nominalMaxSleepHours: Double {
Self.nominalWindowHours(earliestBedtime: earliestReasonableBedtime, wakeTime: wakeTime)
}

/// Real hours between the earliest reasonable bedtime and wake time for the
/// night ending at the next wake from `referenceDate`. Anchoring on the wake
/// (forward from now) then searching backward for the bedtime keeps the
/// window correct even when the app is opened mid-sleep, and lets `Calendar`
/// absorb the ±1h of a DST transition instead of assuming a 24h day.
static func maxSleepHours(
earliestBedtime: Date,
wakeTime: Date,
referenceDate: Date = Date(),
calendar: Calendar = .current
) -> Double {
let wakeComponents = calendar.dateComponents([.hour, .minute], from: wakeTime)
let bedComponents = calendar.dateComponents([.hour, .minute], from: earliestBedtime)

guard
let wake = calendar.nextDate(
after: referenceDate,
matching: wakeComponents,
matchingPolicy: .strict
),
let bed = calendar.nextDate(
after: wake,
matching: bedComponents,
matchingPolicy: .strict,
direction: .backward
)
else {
// Fall back to a nominal wall-clock window if a match can't be found.
return nominalWindowHours(earliestBedtime: earliestBedtime, wakeTime: wakeTime, calendar: calendar)
}

return wake.timeIntervalSince(bed) / 3600.0
Comment thread
gsbernstein marked this conversation as resolved.
Outdated
}

static func nominalWindowHours(
earliestBedtime: Date,
wakeTime: Date,
calendar: Calendar = .current
) -> Double {
let wakeMinutes = minutesSinceMidnight(wakeTime, calendar: calendar)
let earliestMinutes = minutesSinceMidnight(earliestBedtime, calendar: calendar)

let sleepMinutes: Int
if earliestMinutes > wakeMinutes {
sleepMinutes = (24 * 60 - earliestMinutes) + wakeMinutes
} else {
sleepMinutes = wakeMinutes - earliestMinutes
}
Comment thread
gsbernstein marked this conversation as resolved.
Outdated

return Double(sleepMinutes) / 60.0
Comment thread
gsbernstein marked this conversation as resolved.
Outdated
}

private static func minutesSinceMidnight(_ date: Date, calendar: Calendar) -> Int {
let components = calendar.dateComponents([.hour, .minute], from: date)
return (components.hour ?? 0) * 60 + (components.minute ?? 0)
}
}
9 changes: 4 additions & 5 deletions Bedtime/Bedtime/Models/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ class ViewModel {
wakeTime: Date,
sleepGoal: Double,
sleepBank: SleepBank,
maxSleepHours: Double,
minSleepHours: Double
maxSleepHours: Double
) -> BedtimeRecommendation {
let calendar = Calendar.current

Expand All @@ -62,12 +61,12 @@ class ViewModel {
} else if totalSleepNeeded > maxSleepHours {
totalSleepNeeded = maxSleepHours
reason = "You can't catch up in one night, so just get as much as possible."
} else if totalSleepNeeded < minSleepHours {
totalSleepNeeded = minSleepHours
reason = "You're way ahead!"
} else if sleepBank.isInDebt {
let debtHours = sleepBank.debtHours
reason = "You need \(String(format: "%.1f", totalSleepNeeded)) hours tonight to catch up on your \(String(format: "%.1f", debtHours))-hour sleep debt."
} else if totalSleepNeeded < sleepGoal {
totalSleepNeeded = sleepGoal
reason = "You're ahead of the game! Aim for at least \(String(format: "%.1f", sleepGoal)) hours tonight."
} else {
reason = "You're ahead of the game! Aim for at least \(String(format: "%.1f", sleepGoal)) hours tonight."
}
Comment thread
gsbernstein marked this conversation as resolved.
Comment thread
gsbernstein marked this conversation as resolved.
Outdated
Expand Down
38 changes: 13 additions & 25 deletions Bedtime/Bedtime/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ struct SettingsView: View {
@State private var debugMessage: String?
#endif

private var earliestBedtimeBinding: Binding<Date> {
$preferences.earliestReasonableBedtime
}

var body: some View {
NavigationStack {
Form {
Expand Down Expand Up @@ -76,31 +80,15 @@ struct SettingsView: View {
}

Section("Sleep Limits") {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Max sleep hours per night")
Spacer()
Text("\(String(format: "%.0f", preferences.maxSleepHoursPerNight)) hours")
.foregroundColor(.secondary)
}

Slider(value: $preferences.maxSleepHoursPerNight, in: 8...16, step: 1) {
Text("Max")
}
.accentColor(.blue)

HStack {
Text("Min sleep hours per night")
Spacer()
Text("\(String(format: "%.0f", preferences.minSleepHoursPerNight)) hours")
.foregroundColor(.secondary)
}

Slider(value: $preferences.minSleepHoursPerNight, in: 2...10, step: 1) {
Text("Min")
}
.accentColor(.blue)
}
DatePicker(
"Earliest reasonable bedtime",
selection: earliestBedtimeBinding,
displayedComponents: .hourAndMinute
)

Text("Won't recommend sleeping before this time — allowing up to \(String(format: "%.1f", preferences.nominalMaxSleepHours)) hours per night based on your wake time (aside from DST adjustments).")
.font(.caption)
.foregroundColor(.secondary)
}

Section("Data Sources") {
Expand Down