CalendrierOuvre.swift 70 lignes · 2496 octets
import Foundation

/// Calendrier ouvré d'un projet — port de `Server/src/services/calendrier.rs`,
/// décalqué du Kotlin `planning/CalendrierOuvre.kt`.
///
/// Les durées de tâches se comptent en jours **ouvrés** selon le rythme du
/// projet (`joursOuvres` : 5 = lun-ven, 6 = lun-sam, 7 = tous les jours). Une
/// tâche ne peut donc pas calculer sa fin seule : il lui faut le calendrier de
/// son projet. Sans ce port, le téléphone afficherait une fin différente de
/// celle du diagramme web.
///
/// Les trois implémentations sont tenues par les mêmes cas de test :
/// `Server/tests/calendrier.rs`, `CalendrierOuvreTest.kt` et `CalendrierOuvreTest.swift`.
struct CalendrierOuvre {
    /// Un rythme hors de 5-7 (donnée abîmée) retombe sur 7, comportement historique.
    private let rythme: Int

    init(joursOuvres: Int) {
        switch joursOuvres {
        case 5: rythme = 5
        case 6: rythme = 6
        default: rythme = 7
        }
    }

    func estOuvre(_ jour: DateCivile) -> Bool {
        switch jour.jourDeSemaine {
        case 6: return rythme >= 6   // samedi
        case 7: return rythme >= 7   // dimanche
        default: return true
        }
    }

    /// Premier jour ouvré à partir de `jour` inclus.
    func caler(_ jour: DateCivile) -> DateCivile {
        var j = jour
        while !estOuvre(j) { j = j.plusJours(1) }
        return j
    }

    /// Avance de `n` jours ouvrés depuis `jour`, calé au préalable.
    func avancer(_ jour: DateCivile, _ n: Int) -> DateCivile {
        var j = caler(jour)
        for _ in 0..<max(n, 0) { j = caler(j.plusJours(1)) }
        return j
    }

    /// Nombre de jours ouvrés dans `[debut, fin]` inclus ; 0 si l'intervalle est inversé.
    func compter(_ debut: DateCivile, _ fin: DateCivile) -> Int {
        var n = 0
        var j = debut
        while j <= fin {
            if estOuvre(j) { n += 1 }
            j = j.plusJours(1)
        }
        return n
    }

    /// Dernier jour inclus. Une durée nulle vaut un jour : jamais de période vide.
    func fin(_ debut: DateCivile, _ dureeJours: Int) -> DateCivile {
        avancer(debut, max(dureeJours, 1) - 1)
    }

    /// « 09/01 → 13/01 », ou le jour seul si la tâche tient sur une journée.
    func periodeAffichee(_ debut: DateCivile, _ dureeJours: Int) -> String {
        let f = fin(debut, dureeJours)
        let d = caler(debut)
        return d == f ? d.jourMois : "\(d.jourMois) → \(f.jourMois)"
    }
}