Zweimal „Nina arbeiten, 8:00" untereinander sind zwei verschiedene Tage, und nichts verriet welche. Die Uhrzeit beantwortet nur die halbe Frage. Jetzt steht über jedem Tag eine Überschrift: „Heute", „Morgen", danach Wochentag und Datum. Die Wörter für die nächsten beiden Tage, weil sie kürzer sind als jedes Datum und sofort beantworten, was man wissen will. Der Wochentag allein wäre in drei Wochen mehrdeutig, deshalb steht das Datum dabei. Ganztägige Termine stehen jetzt über den Uhrzeiten desselben Tages. Sie beginnen um Mitternacht und standen deshalb zwischen den frühen Terminen — dabei gelten sie für den ganzen Tag. Die Entscheidung „heute/morgen/später" liegt getrennt von ihrer Beschriftung, damit sie ohne Sprachkatalog prüfbar bleibt: im Testlauf steht sonst der rohe Schlüssel statt des Wortes, und der Test prüft die Übersetzung statt der Logik. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
3.0 KiB
Swift
76 lines
3.0 KiB
Swift
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))
|
|
}
|
|
}
|
|
}
|