TeraCalendarTiming.swift (3058B)
1 import Foundation 2 import TeraKitBindings 3 4 /// Gregorian components, independent of timezone or a Foundation instant. 5 struct TeraCivilDate: Sendable, Equatable, Hashable, Comparable { 6 let year: UInt16 7 let month: UInt8 8 let day: UInt8 9 10 init(year: UInt16, month: UInt8, day: UInt8) throws { 11 guard (1 ... 9999).contains(year), (1 ... 12).contains(month) else { 12 throw TeraCalendarTiming.unsupported 13 } 14 let leap = year.isMultiple(of: 4) && (!year.isMultiple(of: 100) || year.isMultiple(of: 400)) 15 let lengths: [UInt8] = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 16 guard (1 ... lengths[Int(month) - 1]).contains(day) else { 17 throw TeraCalendarTiming.unsupported 18 } 19 self.year = year 20 self.month = month 21 self.day = day 22 } 23 24 var canonical: String { 25 String(format: "%04d-%02d-%02d", Int(year), Int(month), Int(day)) 26 } 27 28 static func < (lhs: Self, rhs: Self) -> Bool { 29 (lhs.year, lhs.month, lhs.day) < (rhs.year, rhs.month, rhs.day) 30 } 31 } 32 33 enum TeraCalendarTiming: Sendable, Equatable, Hashable { 34 case dateBased(start: TeraCivilDate, endExclusive: TeraCivilDate?) 35 case timeBased( 36 startUnixSeconds: UInt64, endExclusiveUnixSeconds: UInt64?, 37 startTimezoneID: String?, endTimezoneID: String? 38 ) 39 40 static var unsupported: TeraRuntimeFailure { 41 TeraRuntimeFailure( 42 schemaVersion: 1, code: "today_reader_unsupported", category: "today", retryable: false, 43 recoveryActions: ["upgrade_application"], operationID: nil, capabilityID: nil, 44 safeMessage: "Calendar data requires a supported application version." 45 ) 46 } 47 48 /// The wire domain is wider than native date presentation. Keep the original 49 /// UInt64 in the model and check the Gregorian display domain before conversion. 50 static func presentationInstant(_ seconds: UInt64) -> Date? { 51 guard seconds <= 253_402_300_799 else { return nil } 52 return Date(timeIntervalSince1970: TimeInterval(seconds)) 53 } 54 } 55 56 extension FfiCivilDate { 57 func appValue() throws -> TeraCivilDate { 58 try TeraCivilDate(year: year, month: month, day: day) 59 } 60 } 61 62 extension FfiCalendarTiming { 63 func appValue() throws -> TeraCalendarTiming { 64 switch self { 65 case let .dateBased(start, endExclusive): 66 let first = try start.appValue() 67 let end = try endExclusive?.appValue() 68 guard end == nil || end.map({ $0 > first }) == true else { 69 throw TeraCalendarTiming.unsupported 70 } 71 return .dateBased(start: first, endExclusive: end) 72 case let .timeBased(startUnixS, endExclusiveUnixS, startTzid, endTzid): 73 guard endExclusiveUnixS == nil || endExclusiveUnixS.map({ $0 > startUnixS }) == true, 74 [startTzid, endTzid].compactMap(\.self).allSatisfy({ 75 $0.utf8.count <= 255 && TimeZone(identifier: $0) != nil 76 }) 77 else { throw TeraCalendarTiming.unsupported } 78 return .timeBased( 79 startUnixSeconds: startUnixS, endExclusiveUnixSeconds: endExclusiveUnixS, 80 startTimezoneID: startTzid, endTimezoneID: endTzid 81 ) 82 } 83 } 84 }