108 lines
3.8 KiB
Swift
108 lines
3.8 KiB
Swift
import Foundation
|
||
import MacToolsPluginKit
|
||
|
||
enum CalendarEventGrouper {
|
||
static func group(
|
||
events: [CalendarEventInput],
|
||
visibleDates: [Date],
|
||
calendar: Calendar,
|
||
localization: PluginLocalization = PluginLocalization(bundle: .main)
|
||
) -> [Date: [CalendarEventSummary]] {
|
||
let dayStarts = visibleDates.map { calendar.startOfDay(for: $0) }
|
||
var grouped = Dictionary(uniqueKeysWithValues: dayStarts.map { ($0, [CalendarEventSummary]()) })
|
||
|
||
for event in events {
|
||
for dayStart in dayStarts where overlaps(event: event, dayStart: dayStart, calendar: calendar) {
|
||
grouped[dayStart, default: []].append(
|
||
summary(
|
||
for: event,
|
||
dayStart: dayStart,
|
||
calendar: calendar,
|
||
localization: localization
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
return grouped.mapValues { summaries in
|
||
summaries.sorted { lhs, rhs in
|
||
if lhs.isAllDay != rhs.isAllDay {
|
||
return lhs.isAllDay && !rhs.isAllDay
|
||
}
|
||
|
||
if lhs.startDate != rhs.startDate {
|
||
return lhs.startDate < rhs.startDate
|
||
}
|
||
|
||
return lhs.title.localizedCompare(rhs.title) == .orderedAscending
|
||
}
|
||
}
|
||
}
|
||
|
||
private static func overlaps(event: CalendarEventInput, dayStart: Date, calendar: Calendar) -> Bool {
|
||
guard let nextDayStart = calendar.date(byAdding: .day, value: 1, to: dayStart) else {
|
||
return false
|
||
}
|
||
|
||
let eventEnd = normalizedEndDate(for: event, calendar: calendar)
|
||
return event.startDate < nextDayStart && eventEnd > dayStart
|
||
}
|
||
|
||
private static func summary(
|
||
for event: CalendarEventInput,
|
||
dayStart: Date,
|
||
calendar: Calendar,
|
||
localization: PluginLocalization
|
||
) -> CalendarEventSummary {
|
||
CalendarEventSummary(
|
||
id: "\(event.id)-\(CalendarComponentCalendars.dayID(for: dayStart, calendar: calendar))",
|
||
title: event.title.isEmpty
|
||
? localization.string("event.untitled", defaultValue: "未命名日程")
|
||
: event.title,
|
||
timeText: timeText(for: event, dayStart: dayStart, calendar: calendar, localization: localization),
|
||
startDate: event.startDate,
|
||
endDate: normalizedEndDate(for: event, calendar: calendar),
|
||
isAllDay: event.isAllDay,
|
||
color: event.color
|
||
)
|
||
}
|
||
|
||
private static func normalizedEndDate(for event: CalendarEventInput, calendar: Calendar) -> Date {
|
||
if event.endDate > event.startDate {
|
||
return event.endDate
|
||
}
|
||
|
||
return calendar.date(byAdding: .minute, value: 1, to: event.startDate) ?? event.startDate
|
||
}
|
||
|
||
private static func timeText(
|
||
for event: CalendarEventInput,
|
||
dayStart: Date,
|
||
calendar: Calendar,
|
||
localization: PluginLocalization
|
||
) -> String {
|
||
guard !event.isAllDay else {
|
||
return localization.string("event.allDay", defaultValue: "全天")
|
||
}
|
||
|
||
let endDate = normalizedEndDate(for: event, calendar: calendar)
|
||
let nextDayStart = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart
|
||
let visibleStart = max(event.startDate, dayStart)
|
||
let visibleEnd = min(endDate, nextDayStart)
|
||
|
||
if visibleStart >= visibleEnd {
|
||
return formatTime(event.startDate)
|
||
}
|
||
|
||
return "\(formatTime(visibleStart))–\(formatTime(visibleEnd))"
|
||
}
|
||
|
||
private static func formatTime(_ date: Date) -> String {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = PluginRuntimeLocalization.locale
|
||
formatter.dateStyle = .none
|
||
formatter.timeStyle = .short
|
||
return formatter.string(from: date)
|
||
}
|
||
}
|