SPB Git

spb/poche Public

Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.

Swift 100%
3.0 KB · 82 lines swift
Raw Blame History
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