import Foundation /// Teilt die Terminliste in Tage. /// /// **Ohne Tagesüberschrift ist die Liste unlesbar.** Zwei Zeilen „Nina /// arbeiten, 8:00" untereinander sind zwei verschiedene Tage, und nichts /// verrät welche. Die Uhrzeit allein beantwortet nur die halbe Frage. public enum AgendaGrouping { public struct Day: Identifiable, Equatable, Sendable { /// Der Tagesanfang — er ist zugleich die Kennung. public let date: Date public let events: [OnyxEvent] public var id: Date { date } } /// Gruppiert nach Anfangstag, in der Reihenfolge, in der die Tage kommen. public static func group(_ events: [OnyxEvent], calendar: Calendar = .autoupdatingCurrent) -> [Day] { var order: [Date] = [] var byDay: [Date: [OnyxEvent]] = [:] for event in events.sorted(by: Self.isEarlier) { let day = calendar.startOfDay(for: event.start) if byDay[day] == nil { order.append(day) } byDay[day, default: []].append(event) } return order.map { Day(date: $0, events: byDay[$0] ?? []) } } /// Ganztägige zuerst, dann nach Uhrzeit. /// /// Ein ganztägiger Termin beginnt um Mitternacht und stünde sonst zwischen /// den frühen Terminen — dabei gilt er für den ganzen Tag und gehört über /// sie. private static func isEarlier(_ left: OnyxEvent, _ right: OnyxEvent) -> Bool { if left.isAllDay != right.isAllDay { return left.isAllDay } return left.start < right.start } /// Welche Art von Tag — die Entscheidung getrennt von ihrer Beschriftung, /// damit sie ohne Sprachkatalog prüfbar bleibt. public enum DayKind: Equatable, Sendable { case today case tomorrow case later } public static func kind(for day: Date, today: Date = Date(), calendar: Calendar = .autoupdatingCurrent) -> DayKind { let start = calendar.startOfDay(for: day) let reference = calendar.startOfDay(for: today) switch calendar.dateComponents([.day], from: reference, to: start).day ?? 0 { case 0: return .today case 1: return .tomorrow default: return .later } } /// „Heute", „Morgen" oder das Datum. /// /// Für die nächsten beiden Tage die Wörter: sie sind kürzer als jedes /// Datum und beantworten die Frage sofort. Danach Wochentag und Datum — /// der Wochentag allein wäre in drei Wochen mehrdeutig. public static func label(for day: Date, today: Date = Date(), calendar: Calendar = .autoupdatingCurrent) -> String { switch kind(for: day, today: today, calendar: calendar) { case .today: return String(localized: "widget.calendar.today", bundle: .module) case .tomorrow: return String(localized: "widget.calendar.tomorrow", bundle: .module) case .later: return calendar.startOfDay(for: day) .formatted(.dateTime.weekday(.abbreviated).day().month(.defaultDigits)) } } }