import Foundation /// Woher die Termine kommen sollen. public enum CalendarSourceKind: String, CaseIterable, Sendable { case appleCalendar case calendarr case both public var localizedName: String { switch self { case .appleCalendar: String(localized: "calendar.source.apple", bundle: .module) case .calendarr: String(localized: "calendar.source.calendarr", bundle: .module) case .both: String(localized: "calendar.source.both", bundle: .module) } } /// Fasst die Zustände mehrerer Quellen zu einem zusammen. /// /// Solange **eine** Quelle liefert, wird geliefert. Calendarr nie geöffnet /// zu haben darf den Apple-Kalender nicht mit ausknipsen — sonst wäre /// „beide" schlechter als jede einzelne Quelle für sich. public static func merge(_ states: [CalendarSourceState]) -> CalendarSourceState { guard !states.isEmpty else { return .ready(.empty) } let windows = states.compactMap { state -> EventWindow? in if case .ready(let window) = state { return window } return nil } // Keine einzige Quelle liefert: dann ist der erste Grund die Antwort. // Irgendeine Begründung ist besser als eine leere Liste, die aussieht, // als wäre nichts los. guard !windows.isEmpty else { return states[0] } return .ready(.merged(windows)) } } public extension EventWindow { /// Führt mehrere Fenster zu einem zusammen. static func merged(_ windows: [EventWindow]) -> EventWindow { guard !windows.isEmpty else { return .empty } guard windows.count > 1 else { return windows[0] } let events = windows.flatMap(\.events).sorted { $0.start < $1.start } // Die Abdeckung ist der **Schnitt**, nicht die Vereinigung: ein Tag // gilt nur als bekannt, wenn ihn jede beteiligte Quelle abdeckt. Deckt // Calendarr den März nicht ab, ist der März unbekannt — auch wenn der // Apple-Kalender dafür Auskunft geben könnte. Sonst zeigte das Raster // einen halben März als „frei", der in Wahrheit nur halb bekannt ist. var coverage: DateInterval? for window in windows { // `nil` heißt unbegrenzt und schränkt daher nichts ein. guard let next = window.coverage else { continue } coverage = coverage.map { current in let start = max(current.start, next.start) let end = min(current.end, next.end) // Ohne Überschneidung bleibt nichts übrig — und das ist die // richtige Antwort, nicht ein beliebiger der beiden Zeiträume. return DateInterval(start: start, end: max(start, end)) } ?? next } // Der älteste Stand begrenzt die Verlässlichkeit des Ganzen. Den // neueren zu zeigen behauptete eine Aktualität, die nicht alle haben. let asOf = windows.compactMap(\.asOf).min() return EventWindow(events: events, coverage: coverage, asOf: asOf) } }