diff --git a/Sources/CalendarrCore/Complications/ComplicationTimeline.swift b/Sources/CalendarrCore/Complications/ComplicationTimeline.swift index 06ae07f..c04aa1e 100644 --- a/Sources/CalendarrCore/Complications/ComplicationTimeline.swift +++ b/Sources/CalendarrCore/Complications/ComplicationTimeline.swift @@ -10,14 +10,22 @@ public enum ComplicationTimeline { /// Dates at which a complication should re-render. /// /// Always begins with `now` so the first entry renders immediately. Event - /// starts and ends within `horizon` become entries; hourly marks fill the - /// rest so a quiet calendar still advances its date and countdown. The - /// result is sorted, deduplicated to the second, and capped at + /// starts and ends within `horizon` become entries. + /// + /// The first `fineGrainedWindow` then gets a mark every minute, and the rest + /// of the horizon one every hour. The fine-grained part is what lets a + /// complication show a *compact* countdown — "23m" rather than "1:23:45" — + /// which is the only kind that fits an accessoryCorner or a circular gauge. + /// A live `Text(style: .timer)` needs no entries at all but is eight + /// characters wide, and overlaps the curved label it sits next to. + /// + /// The result is sorted, deduplicated to the second, and capped at /// `maxEntries`, keeping the earliest. public static func entryDates(from now: Date, in snapshot: CalendarrSnapshot, horizon: TimeInterval = 24 * 3600, - maxEntries: Int = 100) -> [Date] { + fineGrainedWindow: TimeInterval = 3600, + maxEntries: Int = 150) -> [Date] { let end = now.addingTimeInterval(horizon) var seen: Set = [] var dates: [Date] = [] @@ -36,6 +44,12 @@ public enum ComplicationTimeline { add(event.start) add(event.end) } + let fineEnd = min(end, now.addingTimeInterval(fineGrainedWindow)) + var minute = now.addingTimeInterval(60) + while minute <= fineEnd { + add(minute) + minute = minute.addingTimeInterval(60) + } var hourly = now.addingTimeInterval(3600) while hourly <= end { add(hourly) diff --git a/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift b/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift index bfe1594..eea5e88 100644 --- a/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift +++ b/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift @@ -73,6 +73,27 @@ struct ComplicationTimelineTests { #expect(dates.count >= 24) } + @Test("The first hour gets minute-granular entries, so a compact countdown stays honest") + func minuteGranularFirstHour() { + let dates = ComplicationTimeline.entryDates(from: Self.now, in: Self.snapshot([])) + let firstHour = dates.filter { $0 <= Self.now.addingTimeInterval(3600) } + + // 60 minute marks plus `now` itself. + #expect(firstHour.count >= 60) + #expect(firstHour.contains(Self.now.addingTimeInterval(60))) + #expect(firstHour.contains(Self.now.addingTimeInterval(1800))) + } + + @Test("Past the first hour the entries thin out to hourly") + func hourlyAfterFirstHour() { + let dates = ComplicationTimeline.entryDates(from: Self.now, in: Self.snapshot([])) + let later = dates.filter { $0 > Self.now.addingTimeInterval(3600) } + + // 23 further hours, not 23 * 60 minutes. + #expect(later.count < 40) + #expect(later.contains(Self.now.addingTimeInterval(2 * 3600))) + } + @Test("The entry count is capped, keeping the earliest entries") func capsEntries() { var events: [SnapshotEvent] = []