Feature the night underway instead of a 4am clock key - #42
Draft
gsbernstein wants to merge 6 commits into
Draft
Conversation
lastNightKey bucketed by startOfDay(now - 4h) while sessions bucket by midpoint + 6h, so between midnight and 4am the card looked under yesterday for sleep filed under today: a 2am wake-up showed "No sleep data" while Recent Sleep listed that same session, and the empty key also offered to sync data that was already present. Both now derive the day from one place, SleepDay. The card features the sleep day underway, so partial sleep shows as it accumulates, falling back to the previous night only in the small hours when nothing is recorded yet. After that an empty night stays empty so the sync prompt can appear. Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
The rule lived in a ContentView computed property, so it could only be exercised by running the app at the hours in question. Moving it beside the day math it depends on makes it a pure function over the nights that have data, with now and the calendar injectable. Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Twelve Swift Testing cases pin the 6pm sleep-day boundary, both DST transitions, and each branch of the featured-night rule: small-hours partial sleep, the small-hours fallback, the 4am cutoff, daytime staying empty so the sync prompt can appear, and an evening nap counting as the night underway. All inject a fixed calendar so they don't depend on the machine's time zone. The project file was edited outside Xcode, so it was re-parsed afterward to confirm both targets resolve with no dangling references. Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
smallHoursEndHour read awkwardly: "hours" meant the wee hours while "Hour" meant an hour of the day. A range says it once, matches sleepBankDaysRange, and reads as a membership test at the call site. Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
The guard's success path meant "don't fall back," so the branch that returned the previous night was the one that fell through it. An if states the case directly: nothing recorded yet and still the small hours means use the night before. Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two different rules decided which day a night belonged to. Sessions were bucketed by midpoint + 6h (
SleepSession.dateForGrouping), but the summary card looked them up understartOfDay(now - 4h). Between midnight and 4am those disagree.Concretely: sleep 10:00 PM Monday to 2:00 AM Tuesday, then open the app at 2:30 AM. The session's midpoint is midnight, so +6h files it under Tuesday, while the lookup key is
2:30 AM − 4h→ Monday. The card showed "No sleep data available" while Recent Sleep listed that same session one card below. The empty key also droverecentSourceAppLinks, so the app offered to "Open Oura to sync last night's sleep" for data it already had.A second case hit outside that window: a 9–11 PM block has a 10 PM midpoint, so it files under the next day. At 11:30 PM it was invisible to a card keyed to today.
Changes
One definition of a sleep day.
SleepDayowns the 6pm boundary, anddateForGroupingnow delegates to it. Applied to a session's midpoint it buckets the session; applied tonowit yields the sleep day underway — same arithmetic, two uses.Feature the night underway, don't hardcode 4am. The card now shows the sleep day underway, so waking briefly at 2am shows tonight's sleep so far rather than yesterday's total. That's deliberate: a partial night is a real answer to "where do I stand if I don't fall back asleep," and nothing is locked in.
Fallback is deliberately narrow. Only in the small hours — before
Constants.smallHoursEndHour— does an empty night fall back to the night before, covering sleep still in progress or a tracker that hasn't synced. Later in the day an empty night stays empty so the no-data state and the sync prompt can do their job instead of resurrecting older sleep. This is why the daytime needs no fallback at all: the sleep day underway already is last night's completed sleep.Made the rule testable. The selection lived in a
ContentViewcomputed property, reachable only by running the app at the relevant hours. It's now a pure function over the nights that have data, withnowand the calendar injected.Added a test target.
BedtimeTests(Swift Testing), 12 cases: the 6pm boundary either side, both DST transitions, and every branch of the rule including the 4am cutoff. All inject a fixedAmerica/New_Yorkcalendar so results don't depend on the machine's time zone.Verification
Not verifiable in this environment: per
AGENTS.md, Cursor Cloud VMs are Linux and this project can't be built, run, or tested without Xcode and the iOS SDK. The tests are written but have never been executed.project.pbxprojwas edited outside Xcode, so I re-parsed it afterward and confirmed both targets resolve, all object ids are well-formed and unique, and no references dangle. Still worth a sanity check on open:BedtimeTestsvisible in the navigator, and ⌘U runs the 12 tests.One deliberate omission: the card is still titled "Last Night" even when showing sleep in progress. A dynamic "Tonight" title needs a rule for when a night stops being tonight, and the obvious candidates all misfire on the evening-nap case, so I left it rather than invent one.