diff --git a/LoopFollow/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/LoopFollow/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index c928251a4..dc4a1901f 100644 --- a/LoopFollow/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/LoopFollow/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -4,7 +4,6 @@ "filename" : "1024.png", "idiom" : "universal", "platform" : "ios", - "scale" : "1x", "size" : "1024x1024" }, { @@ -17,7 +16,6 @@ "filename" : "1024-dark.png", "idiom" : "universal", "platform" : "ios", - "scale" : "1x", "size" : "1024x1024" }, { @@ -30,7 +28,6 @@ "filename" : "1024-tinted.png", "idiom" : "universal", "platform" : "ios", - "scale" : "1x", "size" : "1024x1024" } ], diff --git a/LoopFollow/Stats/SimpleStatsViewModel.swift b/LoopFollow/Stats/SimpleStatsViewModel.swift index 841f4ca2a..8b027b4f4 100644 --- a/LoopFollow/Stats/SimpleStatsViewModel.swift +++ b/LoopFollow/Stats/SimpleStatsViewModel.swift @@ -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 { diff --git a/LoopFollow/Stats/StatsDataFetcher.swift b/LoopFollow/Stats/StatsDataFetcher.swift index 18353dce0..a40dbcc32 100644 --- a/LoopFollow/Stats/StatsDataFetcher.swift +++ b/LoopFollow/Stats/StatsDataFetcher.swift @@ -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 diff --git a/LoopFollow/Stats/StatsDateRange.swift b/LoopFollow/Stats/StatsDateRange.swift index e2e00458b..0cbac59f2 100644 --- a/LoopFollow/Stats/StatsDateRange.swift +++ b/LoopFollow/Stats/StatsDateRange.swift @@ -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) + } +} diff --git a/Tests/StatsCalendarDayTests.swift b/Tests/StatsCalendarDayTests.swift new file mode 100644 index 000000000..de5f94825 --- /dev/null +++ b/Tests/StatsCalendarDayTests.swift @@ -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() + + 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) + } +}