Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"filename" : "1024.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "1x",
"size" : "1024x1024"
},
{
Expand All @@ -17,7 +16,6 @@
"filename" : "1024-dark.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "1x",
"size" : "1024x1024"
},
{
Expand All @@ -30,7 +28,6 @@
"filename" : "1024-tinted.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "1x",
"size" : "1024x1024"
}
],
Expand Down
6 changes: 3 additions & 3 deletions LoopFollow/Stats/SimpleStatsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ class SimpleStatsViewModel: ObservableObject {

var currentTime = startTime
while currentTime < endTime {
let currentDate = Date(timeIntervalSince1970: currentTime)
let dayStart = calendar.startOfDay(for: currentDate).timeIntervalSince1970
let nextDayStart = dayStart + 24 * 60 * 60
guard let day = StatsCalendarDay.bounds(containing: currentTime, calendar: calendar) else { break }
let dayStart = day.start
let nextDayStart = day.next

// Process each profile segment for this day
for i in 0 ..< sortedProfile.count {
Expand Down
6 changes: 3 additions & 3 deletions LoopFollow/Stats/StatsDataFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -539,9 +539,9 @@ class StatsDataFetcher {

var currentTime = startTime
while currentTime < endTime {
let currentDate = Date(timeIntervalSince1970: currentTime)
let dayStart = calendar.startOfDay(for: currentDate).timeIntervalSince1970
let nextDayStart = dayStart + 24 * 60 * 60
guard let day = StatsCalendarDay.bounds(containing: currentTime, calendar: calendar) else { break }
let dayStart = day.start
let nextDayStart = day.next

for i in 0 ..< sortedProfile.count {
let segmentRate = sortedProfile[i].value
Expand Down
12 changes: 12 additions & 0 deletions LoopFollow/Stats/StatsDateRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ enum StatsDateRange {
return (start: calendar.startOfDay(for: start), end: end)
}
}

/// Calendar-day progression for historical stats; a local day can be 23 or 25 hours.
/// Shared by basal gap reconstruction and temp-basal adjustment calculations.
enum StatsCalendarDay {
static func bounds(containing timestamp: TimeInterval, calendar: Calendar) -> (start: TimeInterval, next: TimeInterval)? {
let dayStart = calendar.startOfDay(for: Date(timeIntervalSince1970: timestamp))
guard let nextDayStart = calendar.date(byAdding: .day, value: 1, to: dayStart) else {
return nil
}
return (start: dayStart.timeIntervalSince1970, next: nextDayStart.timeIntervalSince1970)
}
}
59 changes: 59 additions & 0 deletions Tests/StatsCalendarDayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// LoopFollow
// StatsCalendarDayTests.swift

import Foundation
@testable import LoopFollow
import Testing

struct StatsCalendarDayTests {
private func stockholmCalendar() throws -> Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(identifier: "Europe/Stockholm"))
return calendar
}

@Test("Autumn clock change has a 25-hour day and always advances")
func autumnClockChange() throws {
let calendar = try stockholmCalendar()
let midnight = try #require(calendar.date(from: DateComponents(year: 2026, month: 10, day: 25)))
let day = try #require(StatsCalendarDay.bounds(containing: midnight.timeIntervalSince1970, calendar: calendar))
#expect(day.next - day.start == 25 * 60 * 60)

// During the repeated hour, adding 24h to midnight would remain on 25 Oct.
let finalHour = day.next - 30 * 60
let nearEnd = try #require(StatsCalendarDay.bounds(containing: finalHour, calendar: calendar))
#expect(nearEnd.start == day.start)
#expect(nearEnd.next > finalHour)

let next = try #require(StatsCalendarDay.bounds(containing: nearEnd.next, calendar: calendar))
#expect(next.start == day.next)
#expect(next.next > next.start)
}

@Test("Spring clock change has a 23-hour day")
func springClockChange() throws {
let calendar = try stockholmCalendar()
let midnight = try #require(calendar.date(from: DateComponents(year: 2026, month: 3, day: 29)))
let day = try #require(StatsCalendarDay.bounds(containing: midnight.timeIntervalSince1970, calendar: calendar))
#expect(day.next - day.start == 23 * 60 * 60)
}

@Test("Seven statistics calendar days progress across the autumn transition")
func sevenDayHistory() throws {
let calendar = try stockholmCalendar()
let start = try #require(calendar.date(from: DateComponents(year: 2026, month: 10, day: 22)))
var cursor = start.timeIntervalSince1970
var seenDays = Set<TimeInterval>()

for _ in 0 ..< 7 {
let day = try #require(StatsCalendarDay.bounds(containing: cursor, calendar: calendar))
#expect(day.start == cursor)
#expect(day.next > cursor)
#expect(seenDays.insert(day.start).inserted)
cursor = day.next
}
#expect(seenDays.count == 7)
let expected = try #require(calendar.date(from: DateComponents(year: 2026, month: 10, day: 29)))
#expect(cursor == expected.timeIntervalSince1970)
}
}