field_ios

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

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 }