Valider les conflits de planning iOS avec des intervalles semi-ouverts
La liste des réservations affiche deux réunions qui s’enchaînent sans se chevaucher, mais la validation signale un conflit. Ailleurs, une réservation englobe entièrement trois créneaux courts, alors que la page de statistiques n’indique qu’une seule réservation simultanée. Dans les deux cas, inutile de commencer par inspecter l’interface. Sur un Mac dans le cloud, il suffit d’isoler les fonctions de calcul d’intervalles en Swift pur, de fixer les règles aux bornes et les résultats attendus pour ramener le problème à des entrées reproductibles. Nous traitons ici les périodes d’occupation d’une seule ressource, sans aborder l’affichage des dates ni les requêtes réseau.
Définir d’abord le contrat des intervalles semi-ouverts
Chaque réservation est représentée par [start, end) : le début est inclus, la fin est exclue. Ainsi, [0, 10) et [10, 20) ne sont pas en conflit, contrairement à [0, 10) et [9, 20). Les unités peuvent être des secondes ou des minutes, mais elles doivent rester identiques dans un même calcul : on ne peut pas mélanger directement deux précisions différentes.
Le point d’entrée doit rejeter toute entrée dont la fin n’est pas strictement postérieure au début. Un rappel de durée nulle n’est pas une réservation qui occupe une ressource et doit relever d’un autre modèle de données. Ne lui ajoutez pas discrètement une seconde pour le faire entrer dans le tri.
La « fusion des plages occupées » et le « nombre maximal d’occupations simultanées » sont deux problèmes distincts. La fusion fait perdre le nombre de chevauchements : on ne peut donc pas fusionner les intervalles avant de calculer la simultanéité.
Si les règles métier imposent un temps de préparation entre deux réunions, intégrez d’abord ce temps aux intervalles d’occupation, puis appliquez les mêmes règles. Un contrôle supplémentaire limité à l’interface produirait des réponses différentes entre la liste et l’API de validation.
Écrire une implémentation centrale exécutable seule
Enregistrez le contenu suivant dans Intervals.swift. Le code utilise uniquement la bibliothèque standard. Lors de la fusion, il réunit seulement les intervalles qui se chevauchent réellement, sans assimiler deux intervalles contigus à un chevauchement. Les valeurs entières servent ici à isoler l’algorithme des conversions de dates.
typealias Slot = Range<Int>
func merged(_ input: [Slot]) -> [Slot] {
precondition(input.allSatisfy { !$0.isEmpty })
let sorted = input.sorted {
if $0.lowerBound == $1.lowerBound {
return $0.upperBound < $1.upperBound
}
return $0.lowerBound < $1.lowerBound
}
var result: [Slot] = []
for slot in sorted {
if let last = result.last,
slot.lowerBound < last.upperBound {
result[result.count - 1] =
last.lowerBound..<max(last.upperBound, slot.upperBound)
} else {
result.append(slot)
}
}
return result
}
func peak(_ input: [Slot]) -> Int {
precondition(input.allSatisfy { !$0.isEmpty })
let events = input.flatMap {
[(time: $0.lowerBound, delta: 1),
(time: $0.upperBound, delta: -1)]
}.sorted {
if $0.time == $1.time {
return $0.delta < $1.delta
}
return $0.time < $1.time
}
var active = 0
var maximum = 0
for event in events {
active += event.delta
maximum = max(maximum, active)
}
return maximum
}
precondition(merged([]).isEmpty)
precondition(peak([]) == 0)
precondition(merged([0..<10, 10..<20]) == [0..<10, 10..<20])
precondition(peak([0..<10, 10..<20]) == 1)
precondition(merged([0..<10, 5..<15, 10..<20]) == [0..<20])
precondition(peak([0..<10, 5..<15, 10..<20]) == 2)
precondition(merged([0..<30, 5..<10, 8..<12]) == [0..<30])
precondition(peak([0..<30, 5..<10, 8..<12]) == 3)
precondition(peak([0..<10, 0..<10]) == 2)
Exécutez cette commande dans le terminal :
xcrun swift Intervals.swift
Si les vérifications réussissent, le script n’affiche rien et se termine avec un code de sortie nul. L’emploi de precondition, plutôt que de simples appels à assert, évite que les tests deviennent inopérants lorsque les assertions sont désactivées dans une compilation optimisée ordinaire. Le script de validation ne doit pas non plus utiliser d’options de compilation qui désactivent les contrôles de sécurité.
Vérifier les événements au même instant et les intervalles imbriqués
L’algorithme de balayage associe un incrément de un à chaque début et un décrément de un à chaque fin. À un même instant, il faut traiter les décréments avant les incréments ; sinon, deux réservations contiguës seront brièvement comptées comme simultanées. Trier uniquement par instant, sans définir cet ordre, est une source classique d’erreurs aux bornes.
Conservez au minimum cinq catégories de cas fixes : liste vide, intervalles contigus, chevauchement partiel, inclusion complète et doublons exacts. C’est aux données métier de déterminer si deux intervalles identiques représentent deux réservations indépendantes. Ici, chaque élément compte comme une occupation distincte : deux réservations strictement identiques donnent donc une simultanéité de deux.
Un autre détail de la fusion passe facilement inaperçu : pour mettre à jour la borne de fin, il faut retenir la plus grande valeur. Si on la remplace directement par la fin de l’intervalle courant, une réservation longue qui en contient une courte peut se retrouver raccourcie après fusion. Des entrées dans le désordre doivent également produire le même résultat ; l’algorithme ne doit pas dépendre d’un tri effectué par hasard côté serveur.
Recouper les résultats avec un calcul indépendant
Les réponses attendues des cas fixes peuvent être écrites avec la même erreur que l’implémentation. Pour de petits échantillons d’intervalles à valeurs entières, on peut compter à chaque unité les intervalles qui la contiennent et utiliser ce calcul comme référence indépendante. Ajoutez le code suivant au même fichier pour l’exécuter. Il sert aux tests et ne remplace pas l’algorithme de production.
func referencePeak(_ input: [Slot]) -> Int {
guard let start = input.map(\.lowerBound).min(),
let end = input.map(\.upperBound).max() else {
return 0
}
return (start..<end).map { tick in
input.filter { $0.contains(tick) }.count
}.max() ?? 0
}
let candidates: [Slot] = (0..<5).flatMap { start in
((start + 1)...5).map { start..<$0 }
}
for first in candidates {
for second in candidates {
let sample = [first, second]
precondition(peak(sample) == referencePeak(sample))
precondition(merged(sample) ==
merged(Array(sample.reversed())))
precondition(merged(merged(sample)) == merged(sample))
}
}
Ces vérifications portent respectivement sur l’exactitude du nombre maximal d’occupations simultanées, l’indépendance vis-à-vis de l’ordre des entrées et l’idempotence de la fusion. L’algorithme de référence discret ne s’applique qu’au contrat d’unités entières défini ici ; il ne permet pas d’affirmer que tous les cas de temps continu sont couverts. Si vous ajoutez des tests aléatoires, enregistrez les entrées qui échouent, et pas seulement la graine aléatoire, afin de pouvoir reproduire le problème après un changement de chaîne d’outils.
Renforcer la validation des entrées et les traces d’échec avant l’intégration
Lors de l’intégration au projet, séparez l’analyse des données, leur normalisation et le calcul des intervalles en trois couches. Obtenez d’abord des valeurs de début et de fin sur une même référence temporelle, harmonisez ensuite leur précision, puis vérifiez que la fin est strictement postérieure au début. Une réservation initialement très courte peut devenir de durée nulle après conversion de précision : une validation effectuée uniquement avant cette conversion ne suffit donc pas.
Si plusieurs salles partagent une liste de réservations, regroupez d’abord les entrées par identifiant de ressource, puis effectuez les calculs séparément. Des réunions simultanées dans des salles différentes ne doivent pas entrer en conflit. Si une même personne ne peut pas non plus participer à plusieurs réunions à la fois, définissez un autre ensemble de contraintes de ressources pour les personnes, au lieu de regrouper sans distinction toutes les réservations dans un seul balayage.
Pour documenter un échec, il suffit de conserver l’identifiant de la ressource, les intervalles normalisés, la valeur attendue, la valeur obtenue et la version de la chaîne d’outils. Les informations sur les participants ne sont pas nécessaires. Avant de corriger le problème, ajoutez l’entrée concernée aux cas fixes, puis exécutez le script complet. La validation finale doit exiger la cohérence des cas limites, du calcul de référence et du regroupement métier, et non simplement la disparition d’un message de conflit dans une interface.
Questions fréquentes
Deux réservations qui se suivent sont-elles en conflit ?
Non, avec les intervalles semi-ouverts retenus ici. À heure égale, traitez les fins avant les débuts. Si une préparation est nécessaire, élargissez les plages occupées avant de rechercher les conflits.
La fusion permet-elle de calculer le pic de simultanéité ?
Non. Elle conserve les périodes occupées, mais perd le nombre de chevauchements. Calculez le pic à partir des événements de début et de fin des intervalles d’origine.
Pourquoi commencer avec des unités de temps entières ?
Elles isolent l’algorithme des problèmes de lecture des dates. En production, normalisez la référence temporelle et la précision, puis refusez toute fin antérieure ou égale au début.
Exécutez vos projets sur un Mac physique dédié
Utilisez un Mac dans le cloud pour compiler, tester et travailler à distance sur le bureau. Choisissez la configuration et la période de facturation adaptées à votre projet. La disponibilité effective est indiquée en temps réel dans la console.