TeraCalendarEditing.swift (4127B)
1 import Foundation 2 3 enum TeraCalendarEditing { 4 static func civilDate(at seconds: UInt64, timeZone: TimeZone) -> TeraCivilDate? { 5 guard let instant = TeraCalendarTiming.presentationInstant(seconds) else { return nil } 6 var calendar = Calendar(identifier: .gregorian) 7 calendar.timeZone = timeZone 8 let parts = calendar.dateComponents([.year, .month, .day], from: instant) 9 guard let year = parts.year.flatMap(UInt16.init(exactly:)), 10 let month = parts.month.flatMap(UInt8.init(exactly:)), 11 let day = parts.day.flatMap(UInt8.init(exactly:)) 12 else { return nil } 13 return try? TeraCivilDate(year: year, month: month, day: day) 14 } 15 16 static func initialize(_ form: inout TeraAddForm, now: UInt64, timeZone: TimeZone) { 17 form.eventTimezone = timeZone.identifier 18 let civil = civilDate(at: now, timeZone: timeZone) 19 form.eventStartDate = civil?.canonical 20 form.eventEndDate = civil?.nextDay?.canonical 21 let (start, startOverflow) = now.addingReportingOverflow(3600) 22 let (end, endOverflow) = start.addingReportingOverflow(3600) 23 if !startOverflow, TeraCalendarTiming.presentationInstant(start) != nil { 24 form.eventStartUnixSeconds = start 25 } 26 if !startOverflow, !endOverflow, TeraCalendarTiming.presentationInstant(end) != nil { 27 form.eventEndUnixSeconds = end 28 } 29 } 30 31 static func pickerInstant(_ value: UInt64?) -> Date? { 32 guard let value, value > 0 else { return nil } 33 return TeraCalendarTiming.presentationInstant(value) 34 } 35 36 static func pickerSeconds(_ value: Date) -> UInt64? { 37 guard let seconds = try? TeraClock.unixSeconds(from: value, requirePositive: true), 38 TeraCalendarTiming.presentationInstant(seconds) != nil 39 else { return nil } 40 return seconds 41 } 42 } 43 44 /// A wall time exists only in conjunction with an explicit zone. Strict matching 45 /// returns no instant for a gap and both distinct instants for an overlap. 46 struct TeraWallTime { 47 let date: TeraCivilDate 48 var hour: Int 49 var minute: Int 50 var second: Int = 0 51 52 func instants(in timeZone: TimeZone) -> [UInt64] { 53 guard (0 ... 23).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return [] } 54 var calendar = Calendar(identifier: .gregorian) 55 calendar.timeZone = timeZone 56 let components = DateComponents(year: Int(date.year), month: Int(date.month), day: Int(date.day), 57 hour: hour, minute: minute, second: second) 58 guard let noon = calendar.date(from: DateComponents(year: Int(date.year), month: Int(date.month), 59 day: Int(date.day), hour: 12)), 60 let anchor = calendar.date(byAdding: .day, value: -1, to: noon) 61 else { return [] } 62 let candidates = [Calendar.RepeatedTimePolicy.first, .last].compactMap { policy -> UInt64? in 63 guard let found = calendar.nextDate(after: anchor, matching: components, matchingPolicy: .strict, 64 repeatedTimePolicy: policy, direction: .forward), 65 matches(found, components: components, calendar: calendar) 66 else { return nil } 67 return TeraCalendarEditing.pickerSeconds(found) 68 } 69 return Array(Set(candidates)).sorted() 70 } 71 72 private func matches(_ instant: Date, components: DateComponents, calendar: Calendar) -> Bool { 73 let fields: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second] 74 let actual = calendar.dateComponents(fields, from: instant) 75 return fields.allSatisfy { actual.value(for: $0) == components.value(for: $0) } 76 } 77 78 static func from(_ seconds: UInt64, timeZone: TimeZone) -> Self? { 79 guard let instant = TeraCalendarEditing.pickerInstant(seconds), 80 let date = TeraCalendarEditing.civilDate(at: seconds, timeZone: timeZone) 81 else { return nil } 82 var calendar = Calendar(identifier: .gregorian) 83 calendar.timeZone = timeZone 84 return Self(date: date, hour: calendar.component(.hour, from: instant), 85 minute: calendar.component(.minute, from: instant), second: calendar.component(.second, from: instant)) 86 } 87 }