"""Lecture des dates françaises telles qu'elles apparaissent dans le corpus. Le corpus mélange les formes : « 23 avr. 2026 », « 1er juill. 2026 », « 12 juin 2026 », « 24.07.2026 », « 22/07/2026 », « ~24 août 2026 », « Fin août 2026 », « Automne 2026 ». Les trois premières familles donnent une date exacte ; les suivantes ne donnent qu'une intention de calendrier, qu'on ne convertit jamais en date certaine — le libellé est conservé tel quel. """ from __future__ import annotations import re import unicodedata from datetime import date # Formes rencontrées dans le corpus, abrégées ou non, avec ou sans accent. MOIS: dict[str, int] = { "janvier": 1, "janv": 1, "fevrier": 2, "fevr": 2, "fev": 2, "mars": 3, "avril": 4, "avr": 4, "mai": 5, "juin": 6, "juillet": 7, "juill": 7, "juil": 7, "aout": 8, "septembre": 9, "sept": 9, "octobre": 10, "oct": 10, "novembre": 11, "nov": 11, "decembre": 12, "dec": 12, } # Découpages de saison utilisés pour situer une échéance sans date exacte. SAISONS: dict[str, tuple[int, int]] = { "printemps": (4, 1), "ete": (7, 1), "automne": (10, 1), "hiver": (1, 15), "rentree": (9, 1), } _LITTERALE = re.compile( r"\b(\d{1,2})\s*(?:er|ᵉʳ|re|ère)?\.?\s+" r"(janvier|janv|fevrier|fevr|fev|mars|avril|avr|mai|juin|juillet|juill|juil|" r"aout|septembre|sept|octobre|oct|novembre|nov|decembre|dec)\.?\s*(\d{4})?", re.IGNORECASE, ) _NUMERIQUE = re.compile(r"\b(\d{1,2})[./](\d{1,2})[./](\d{4})\b") _ISO = re.compile(r"\b(\d{4})-(\d{2})-(\d{2})\b") _MOIS_SEUL = re.compile( r"\b(debut|mi|fin|courant)?\s*-?\s*" r"(janvier|janv|fevrier|fevr|fev|mars|avril|avr|mai|juin|juillet|juill|juil|" r"aout|septembre|sept|octobre|oct|novembre|nov|decembre|dec)\.?" r"\s+(\d{4})\b", re.IGNORECASE, ) _SAISON = re.compile( r"\b(printemps|ete|automne|hiver|rentree)\s+(?:de\s+)?(\d{4})\b", re.IGNORECASE ) def sans_accents(texte: str) -> str: """Retire les diacritiques : « août » → « aout », « févr. » → « fevr. ».""" decompose = unicodedata.normalize("NFD", texte) return "".join(c for c in decompose if unicodedata.category(c) != "Mn") def lire_date(fragment: str, *, annee_defaut: int | None = None) -> date | None: """Extrait la première date **exacte** d'un fragment, sinon `None`. Une date exacte suppose un jour identifié. « Fin août 2026 » ou « Automne 2026 » renvoient `None` : voir `lire_echeance`. """ if not fragment: return None plat = sans_accents(fragment).lower() if m := _ISO.search(plat): return _construire(int(m.group(1)), int(m.group(2)), int(m.group(3))) if m := _NUMERIQUE.search(plat): return _construire(int(m.group(3)), int(m.group(2)), int(m.group(1))) if m := _LITTERALE.search(plat): jour = int(m.group(1)) mois = MOIS[m.group(2)] annee = int(m.group(3)) if m.group(3) else annee_defaut if annee is None: return None return _construire(annee, mois, jour) return None def lire_echeance(fragment: str) -> tuple[date | None, str]: """Interprète une échéance prévisionnelle. Retourne `(date approchée ou None, libellé nettoyé)`. Le libellé est toujours conservé : c'est lui qui est affiché, la date ne sert qu'au tri et au filtrage par période. """ libelle = " ".join(fragment.split()).strip(" .;") if not libelle: return None, "" if exacte := lire_date(libelle): return exacte, libelle plat = sans_accents(libelle).lower() if m := _MOIS_SEUL.search(plat): qualificatif, mois, annee = m.group(1), MOIS[m.group(2)], int(m.group(3)) # Un mois sans jour est situé conventionnellement : début = 5, fin = 25, # milieu ou absence de qualificatif = 15. La date sert au tri, jamais à # affirmer un fait — c'est le libellé qui est affiché. jour = {"debut": 5, "fin": 25}.get(qualificatif or "", 15) return _construire(annee, mois, jour), libelle if m := _SAISON.search(plat): mois, jour = SAISONS[m.group(1)] return _construire(int(m.group(2)), mois, jour), libelle # « Indéterminée », « Sans échéance », « Incertaine »… : aucune date. return None, libelle def _construire(annee: int, mois: int, jour: int) -> date | None: try: return date(annee, mois, jour) except ValueError: return None def toutes_les_dates(fragment: str, *, annee_defaut: int | None = None) -> list[date]: """Toutes les dates exactes d'un fragment, dans l'ordre d'apparition. Utile pour les cellules qui en contiennent plusieurs, par exemple « Adoptée (20-21 juill.) ; saisine du 24 juill. ». """ plat = sans_accents(fragment).lower() trouvees: list[tuple[int, date]] = [] for motif, ordre in ((_ISO, "ymd"), (_NUMERIQUE, "dmy"), (_LITTERALE, "litterale")): for m in motif.finditer(plat): if ordre == "ymd": d = _construire(int(m.group(1)), int(m.group(2)), int(m.group(3))) elif ordre == "dmy": d = _construire(int(m.group(3)), int(m.group(2)), int(m.group(1))) else: annee = int(m.group(3)) if m.group(3) else annee_defaut d = _construire(annee, MOIS[m.group(2)], int(m.group(1))) if annee else None if d: trouvees.append((m.start(), d)) return [d for _, d in sorted(trouvees)]