field_ios

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

TeraCivilDateFormat.swift (3598B)


      1 import Foundation
      2 
      3 /// Locale field order and symbols applied directly to Gregorian components.
      4 /// No civil date is represented by an instant, even temporarily.
      5 struct TeraCivilDateFormat {
      6   let locale: Locale
      7 
      8   func string(_ value: TeraCivilDate) -> String {
      9     guard let pattern = DateFormatter.dateFormat(fromTemplate: "yMMMd", options: 0, locale: locale),
     10           pattern.utf8.count <= 128
     11     else { return value.canonical }
     12     let symbols = DateFormatter()
     13     symbols.locale = locale
     14     symbols.calendar = Calendar(identifier: .gregorian)
     15     return render(pattern, value: value, symbols: symbols) ?? value.canonical
     16   }
     17 
     18   private func render(_ pattern: String, value: TeraCivilDate, symbols: DateFormatter) -> String? {
     19     let characters = Array(pattern)
     20     var index = 0
     21     var quoted = false
     22     var output = ""
     23     while index < characters.count {
     24       let character = characters[index]
     25       if character == "'" {
     26         if index + 1 < characters.count, characters[index + 1] == "'" {
     27           output.append("'")
     28           index += 2
     29         } else {
     30           quoted.toggle()
     31           index += 1
     32         }
     33       } else if !quoted, character.isASCII, character.isLetter {
     34         let start = index
     35         while index < characters.count, characters[index] == character {
     36           index += 1
     37         }
     38         guard let field = field(character, count: index - start, value: value, symbols: symbols) else { return nil }
     39         output += field
     40       } else {
     41         output.append(character)
     42         index += 1
     43       }
     44     }
     45     return !quoted && output.utf8.count <= 512 ? output : nil
     46   }
     47 
     48   private func field(_ field: Character, count: Int, value: TeraCivilDate, symbols: DateFormatter) -> String? {
     49     switch field {
     50     case "y": number(Int(value.year), minimumDigits: count == 2 ? 1 : min(count, 4))
     51     case "d": number(Int(value.day), minimumDigits: min(count, 2))
     52     case "M", "L": month(Int(value.month), count: count, standalone: field == "L", symbols: symbols)
     53     case "G": symbols.eraSymbols.last
     54     default: nil
     55     }
     56   }
     57 
     58   private func month(_ month: Int, count: Int, standalone: Bool, symbols: DateFormatter) -> String? {
     59     if count <= 2 {
     60       return number(month, minimumDigits: count)
     61     }
     62     let names: [String] = switch count {
     63     case 3: standalone ? symbols.shortStandaloneMonthSymbols : symbols.shortMonthSymbols
     64     case 4: standalone ? symbols.standaloneMonthSymbols : symbols.monthSymbols
     65     default: standalone ? symbols.veryShortStandaloneMonthSymbols : symbols.veryShortMonthSymbols
     66     }
     67     return names.indices.contains(month - 1) ? names[month - 1] : nil
     68   }
     69 
     70   private func number(_ value: Int, minimumDigits: Int) -> String? {
     71     let formatter = NumberFormatter()
     72     formatter.locale = locale
     73     formatter.numberStyle = .decimal
     74     formatter.usesGroupingSeparator = false
     75     formatter.minimumIntegerDigits = minimumDigits
     76     formatter.maximumFractionDigits = 0
     77     return formatter.string(from: NSNumber(value: value))
     78   }
     79 }
     80 
     81 extension TeraCivilDate {
     82   var previousDay: Self? {
     83     if day > 1 {
     84       return try? Self(year: year, month: month, day: day - 1)
     85     }
     86     guard year > 1 || month > 1 else { return nil }
     87     let previousYear = month == 1 ? year - 1 : year
     88     let previousMonth = month == 1 ? 12 : month - 1
     89     // At most four validated component candidates; no timezone arithmetic.
     90     for previousDay: UInt8 in stride(from: 31, through: 28, by: -1) {
     91       if let value = try? Self(year: previousYear, month: previousMonth, day: previousDay) {
     92         return value
     93       }
     94     }
     95     return nil
     96   }
     97 }