"""Montant en toutes lettres, en français — remplit l'encadré « Somme en toutes lettres » du cerfa (voir `pdf.py`). Fonction pure, testée séparément de la génération PDF : l'orthographe des nombres en français a suffisamment d'exceptions (accord de "quatre-vingts"/"cents", "et un" irrégulier à 71) pour mériter ses propres cas de test. """ _ONES = [ "zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf", ] _TENS_PREFIX = {2: "vingt", 3: "trente", 4: "quarante", 5: "cinquante", 6: "soixante"} def _under_hundred(n: int) -> str: """0 <= n <= 99.""" if n < 20: return _ONES[n] if n < 70: tens, unit = divmod(n, 10) prefix = _TENS_PREFIX[tens] if unit == 0: return prefix if unit == 1: return f"{prefix} et un" return f"{prefix}-{_ONES[unit]}" if n < 80: # 70-79 : soixante + dix..dix-neuf, « et onze » irrégulier à 71 (le # seul cas des années 70 avec « et » — 72..79 n'en prennent pas). rem = n - 60 if rem == 11: return "soixante et onze" return f"soixante-{_ONES[rem]}" # 80-99 : quatre-vingt(s) + ... — jamais de « et », y compris à 91. rem = n - 80 if rem == 0: return "quatre-vingts" return f"quatre-vingt-{_ONES[rem]}" def _under_thousand(n: int) -> str: """0 <= n <= 999.""" if n < 100: return _under_hundred(n) hundreds, rem = divmod(n, 100) prefix = "cent" if hundreds == 1 else f"{_ONES[hundreds]} cent" if rem == 0: # « cents » ne prend un accord au pluriel que sur un multiple exact # (deux cents, mais deux cent un). return f"{prefix}s" if hundreds > 1 else prefix return f"{prefix} {_under_hundred(rem)}" def _number_to_words(n: int) -> str: """0 <= n <= 999 999 999 — largement suffisant pour une cotisation ou un don, jamais invariable au-delà (« mille » ne prend jamais de « s »).""" if n == 0: return "zéro" parts = [] millions, n = divmod(n, 1_000_000) if millions: parts.append("un million" if millions == 1 else f"{_under_thousand(millions)} millions") thousands, n = divmod(n, 1000) if thousands: parts.append("mille" if thousands == 1 else f"{_under_thousand(thousands)} mille") if n: parts.append(_under_thousand(n)) return " ".join(parts) def amount_in_words(amount: float) -> str: """« Cent cinquante euros », « cinquante centimes », « un euro et dix centimes »… — texte de l'encadré « Somme en toutes lettres » du cerfa, à partir d'un montant en euros (`Volunteer.fee_amount`).""" cents_total = round(amount * 100) euros, cents = divmod(cents_total, 100) parts = [] if euros or not cents: parts.append(f"{_number_to_words(euros)} {'euro' if euros <= 1 else 'euros'}") if cents: parts.append(f"{_number_to_words(cents)} {'centime' if cents <= 1 else 'centimes'}") text = " et ".join(parts) return text[:1].upper() + text[1:]