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 }