CalendrierOuvre.kt 72 lignes · 2515 octets
package fr.ebii.card2vcf.planning

import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.DateTimeFormatter

/**
 * Calendrier ouvré d'un projet — port de `Server/src/services/calendrier.rs`.
 *
 * 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 deux implémentations sont tenues par les mêmes cas de test :
 * `Server/tests/calendrier.rs` et `CalendrierOuvreTest`.
 */
class CalendrierOuvre(joursOuvres: Int) {
    /** Un rythme hors de 5-7 (donnée abîmée) retombe sur 7, comportement historique. */
    private val rythme: Int = when (joursOuvres) {
        5 -> 5
        6 -> 6
        else -> 7
    }

    fun estOuvre(jour: LocalDate): Boolean = when (jour.dayOfWeek) {
        DayOfWeek.SATURDAY -> rythme >= 6
        DayOfWeek.SUNDAY -> rythme >= 7
        else -> true
    }

    /** Premier jour ouvré à partir de [jour] inclus. */
    fun caler(jour: LocalDate): LocalDate {
        var j = jour
        while (!estOuvre(j)) j = j.plusDays(1)
        return j
    }

    /** Avance de [n] jours ouvrés depuis [jour], calé au préalable. */
    fun avancer(jour: LocalDate, n: Int): LocalDate {
        var j = caler(jour)
        repeat(maxOf(n, 0)) { j = caler(j.plusDays(1)) }
        return j
    }

    /** Nombre de jours ouvrés dans `[debut, fin]` inclus ; 0 si l'intervalle est inversé. */
    fun compter(debut: LocalDate, fin: LocalDate): Int {
        var n = 0
        var j = debut
        while (!j.isAfter(fin)) {
            if (estOuvre(j)) n++
            j = j.plusDays(1)
        }
        return n
    }

    /** Dernier jour inclus. Une durée nulle vaut un jour : jamais de période vide. */
    fun fin(debut: LocalDate, dureeJours: Int): LocalDate =
        avancer(debut, maxOf(dureeJours, 1) - 1)

    /** « 09/01 → 13/01 », ou le jour seul si la tâche tient sur une journée. */
    fun periodeAffichee(debut: LocalDate, dureeJours: Int): String {
        val f = fin(debut, dureeJours)
        val d = caler(debut)
        return if (d == f) d.format(FORMAT) else "${d.format(FORMAT)} → ${f.format(FORMAT)}"
    }

    private companion object {
        val FORMAT: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM")
    }
}