import Foundation /// Pure anchor/slicing logic mirroring `lib/widget_data/widget_data_mapper.dart` /// (resolveDayAnchor / resolveWeekAnchor). Keep both sides in sync — the Dart /// unit tests in test/widget_data/widget_data_mapper_test.dart double as the /// review checklist here: /// resolveDayAnchor(Wed 2026-05-06 10:00) == 2026-05-06 (before cutoff) /// resolveDayAnchor(Wed 2026-05-06 19:00) == 2026-05-07 (after cutoff) /// resolveDayAnchor(Fri 2026-05-08 18:00) == 2026-05-11 (Fri → Mon) /// resolveDayAnchor(Sat 2026-05-09 10:00) == 2026-05-11 /// resolveDayAnchor(Sun 2026-05-10 22:00) == 2026-05-11 /// resolveWeekAnchor(Tue 2026-05-05 10:00) == 2026-05-04 /// resolveWeekAnchor(Sun 2026-05-10 10:00) == 2026-05-11 enum TimetableAnchor { /// After 17:00 the user's question shifts from "what's left today" to /// "what's tomorrow", so the day widget rolls forward. static let dayCutoffHour = 17 static func resolveDayAnchor(_ now: Date, calendar: Calendar = .current) -> Date { var candidate = calendar.startOfDay(for: now) let shiftToTomorrow = calendar.component(.hour, from: now) >= dayCutoffHour || isWeekend(candidate, calendar: calendar) if shiftToTomorrow { candidate = nextDay(candidate, calendar: calendar) } while isWeekend(candidate, calendar: calendar) { candidate = nextDay(candidate, calendar: calendar) } return candidate } static func resolveWeekAnchor(_ now: Date, calendar: Calendar = .current) -> Date { let anchor = resolveDayAnchor(now, calendar: calendar) // Swift weekday: 1 = Sunday … 7 = Saturday → distance back to Monday. let daysFromMonday = (calendar.component(.weekday, from: anchor) + 5) % 7 let monday = calendar.date(byAdding: .day, value: -daysFromMonday, to: anchor) ?? anchor return calendar.startOfDay(for: monday) } /// Future instants at which the rendered anchor can change: each day's /// midnight (rollover) and 17:00 (cutoff), within the horizon. Sorted, /// strictly after `now`. static func boundaryDates( from now: Date, horizonDays: Int = 3, calendar: Calendar = .current ) -> [Date] { var result: [Date] = [] let today = calendar.startOfDay(for: now) for offset in 0...horizonDays { guard let day = calendar.date(byAdding: .day, value: offset, to: today) else { continue } let midnight = calendar.startOfDay(for: day) if midnight > now { result.append(midnight) } if let cutoff = calendar.date( bySettingHour: dayCutoffHour, minute: 0, second: 0, of: day ), cutoff > now { result.append(cutoff) } } return result.sorted() } /// Derives a day payload from the 14-day week payload — same shape the /// Dart buildDayData produces, since the week payload runs through the /// identical per-day merge/collision pipeline. static func slice( week: WidgetTimetableData, forDay anchor: Date, calendar: Calendar = .current ) -> WidgetTimetableData { let lessons = week.lessons.filter { calendar.isDate($0.start, inSameDayAs: anchor) } // Every v2 week payload carries `days` — the key bump guarantees it. let dayInfo = week.days?.first { calendar.isDate($0.date, inSameDayAs: anchor) } return WidgetTimetableData( fetchedAt: week.fetchedAt, anchorDate: anchor, lessons: lessons, periods: week.periods, isHoliday: dayInfo?.isHoliday ?? false, holidayName: dayInfo?.holidayName, days: nil ) } /// Week payload re-anchored to the week containing/following `anchorDate`. /// The week view filters its five columns off `anchorDate`, so this alone /// performs the Friday-evening/weekend jump to next week. static func retarget(week: WidgetTimetableData, anchorDate: Date) -> WidgetTimetableData { WidgetTimetableData( fetchedAt: week.fetchedAt, anchorDate: anchorDate, lessons: week.lessons, periods: week.periods, isHoliday: week.isHoliday, holidayName: week.holidayName, days: week.days ) } private static func isWeekend(_ date: Date, calendar: Calendar) -> Bool { let weekday = calendar.component(.weekday, from: date) return weekday == 1 || weekday == 7 } private static func nextDay(_ date: Date, calendar: Calendar) -> Date { let next = calendar.date(byAdding: .day, value: 1, to: date) ?? date return calendar.startOfDay(for: next) } }