110 lines
4.7 KiB
Swift
110 lines
4.7 KiB
Swift
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)
|
|
}
|
|
}
|