// // DateResolver.swift // Poche // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import Foundation /// Dates are validated by the app, never trusted from the model /// (CLAUDE.md §4). The model supplies ISO 8601; Swift resolves and the /// resolved date is displayed in clear text on the confirmation card. enum DateResolutionError: LocalizedError { case unparseable(String) case inPast(Date) case unreasonablyFar(Date) var errorDescription: String? { switch self { case .unparseable(let raw): "la date « \(raw) » n'est pas au format ISO 8601. Redemande la date à l'utilisateur si nécessaire." case .inPast: "cette date est déjà passée. Demande à l'utilisateur la bonne date." case .unreasonablyFar: "cette date est à plus de cinq ans. Vérifie avec l'utilisateur." } } } enum DateResolver { private static let maxHorizon: TimeInterval = 5 * 365.25 * 24 * 3600 /// Accepts "2026-08-12T09:00:00" (with or without seconds/timezone) and /// bare dates "2026-08-12" (resolved to 09:00 local — always shown on /// the confirmation card before anything is written). static func resolve(iso raw: String, now: Date = .now, calendar: Calendar = .current) throws -> Date { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard let date = parse(trimmed, calendar: calendar) else { throw DateResolutionError.unparseable(trimmed) } guard date > now else { throw DateResolutionError.inPast(date) } guard date < now.addingTimeInterval(maxHorizon) else { throw DateResolutionError.unreasonablyFar(date) } return date } static func display(_ date: Date) -> String { date.formatted(date: .complete, time: .shortened) } private static func parse(_ text: String, calendar: Calendar) -> Date? { let withTZ = ISO8601DateFormatter() withTZ.formatOptions = [.withInternetDateTime] if let date = withTZ.date(from: text) { return date } // Local wall-clock time, no timezone suffix — the common model output. for format in ["yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm", "yyyy-MM-dd HH:mm"] { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.calendar = calendar formatter.timeZone = calendar.timeZone formatter.dateFormat = format if let date = formatter.date(from: text) { return date } } // Bare date: default to 09:00 local. let dateOnly = DateFormatter() dateOnly.locale = Locale(identifier: "en_US_POSIX") dateOnly.calendar = calendar dateOnly.timeZone = calendar.timeZone dateOnly.dateFormat = "yyyy-MM-dd" if let day = dateOnly.date(from: text) { return calendar.date(bySettingHour: 9, minute: 0, second: 0, of: day) } return nil } }