field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }