spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1//2// DateResolver.swift3// Poche4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//89import Foundation1011/// Dates are validated by the app, never trusted from the model12/// (CLAUDE.md §4). The model supplies ISO 8601; Swift resolves and the13/// resolved date is displayed in clear text on the confirmation card.14enum DateResolutionError: LocalizedError {15 case unparseable(String)16 case inPast(Date)17 case unreasonablyFar(Date)1819 var errorDescription: String? {20 switch self {21 case .unparseable(let raw):22 "la date « \(raw) » n'est pas au format ISO 8601. Redemande la date à l'utilisateur si nécessaire."23 case .inPast:24 "cette date est déjà passée. Demande à l'utilisateur la bonne date."25 case .unreasonablyFar:26 "cette date est à plus de cinq ans. Vérifie avec l'utilisateur."27 }28 }29}3031enum DateResolver {32 private static let maxHorizon: TimeInterval = 5 * 365.25 * 24 * 36003334 /// Accepts "2026-08-12T09:00:00" (with or without seconds/timezone) and35 /// bare dates "2026-08-12" (resolved to 09:00 local — always shown on36 /// the confirmation card before anything is written).37 static func resolve(iso raw: String, now: Date = .now, calendar: Calendar = .current) throws -> Date {38 let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)39 guard let date = parse(trimmed, calendar: calendar) else {40 throw DateResolutionError.unparseable(trimmed)41 }42 guard date > now else {43 throw DateResolutionError.inPast(date)44 }45 guard date < now.addingTimeInterval(maxHorizon) else {46 throw DateResolutionError.unreasonablyFar(date)47 }48 return date49 }5051 static func display(_ date: Date) -> String {52 date.formatted(date: .complete, time: .shortened)53 }5455 private static func parse(_ text: String, calendar: Calendar) -> Date? {56 let withTZ = ISO8601DateFormatter()57 withTZ.formatOptions = [.withInternetDateTime]58 if let date = withTZ.date(from: text) { return date }5960 // Local wall-clock time, no timezone suffix — the common model output.61 for format in ["yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm", "yyyy-MM-dd HH:mm"] {62 let formatter = DateFormatter()63 formatter.locale = Locale(identifier: "en_US_POSIX")64 formatter.calendar = calendar65 formatter.timeZone = calendar.timeZone66 formatter.dateFormat = format67 if let date = formatter.date(from: text) { return date }68 }6970 // Bare date: default to 09:00 local.71 let dateOnly = DateFormatter()72 dateOnly.locale = Locale(identifier: "en_US_POSIX")73 dateOnly.calendar = calendar74 dateOnly.timeZone = calendar.timeZone75 dateOnly.dateFormat = "yyyy-MM-dd"76 if let day = dateOnly.date(from: text) {77 return calendar.date(bySettingHour: 9, minute: 0, second: 0, of: day)78 }79 return nil80 }81}82