"""French number to words conversion using num2words."""
import re
from collections.abc import Callable
# Try to import num2words
_num2words_fn: Callable[..., str] | None = None
try:
from num2words import num2words as _n2w
_num2words_fn = _n2w
NUM2WORDS_AVAILABLE = True
except ImportError:
NUM2WORDS_AVAILABLE = False
[docs]
def number_to_french(n: int, ordinal: bool = False) -> str:
"""Convert a number to French words using num2words.
Args:
n: Integer to convert.
ordinal: If True, return ordinal form (premier, deuxième, etc.)
Returns:
French word representation.
Raises:
ImportError: If num2words is not installed.
Example:
>>> number_to_french(42)
'quarante-deux'
>>> number_to_french(1, ordinal=True)
'premier'
"""
if _num2words_fn is None:
raise ImportError(
"num2words is required for number conversion. "
"Install with: pip install num2words"
)
if ordinal:
return _num2words_fn(n, lang="fr", to="ordinal")
return _num2words_fn(n, lang="fr")
[docs]
def expand_numbers(text: str, max_value: int = 1000000) -> str:
"""Expand numbers in text to French words.
Args:
text: Text containing numbers.
max_value: Maximum value to expand (larger numbers kept as-is).
Returns:
Text with numbers expanded.
Example:
>>> expand_numbers("J'ai 3 pommes et 42 oranges.")
"J'ai trois pommes et quarante-deux oranges."
"""
if not NUM2WORDS_AVAILABLE:
return text
def replace_match(match: re.Match[str]) -> str:
num = int(match.group(0))
if num <= max_value:
return number_to_french(num)
return match.group(0)
return re.sub(r"\b\d+\b", replace_match, text)
[docs]
def expand_time(text: str) -> str:
"""Expand time expressions like 14h30.
Args:
text: Text containing time expressions.
Returns:
Text with times expanded.
Example:
>>> expand_time("Le rendez-vous est à 14h30.")
'Le rendez-vous est à quatorze heures trente.'
"""
if not NUM2WORDS_AVAILABLE:
return text
def replace_time(match: re.Match[str]) -> str:
hours = int(match.group(1))
minutes = match.group(2)
result = number_to_french(hours) + " heure"
if hours > 1:
result += "s"
if minutes:
mins = int(minutes)
if mins > 0:
result += " " + number_to_french(mins)
return result
return re.sub(r"\b(\d{1,2})h(\d{2})?\b", replace_time, text)
[docs]
def expand_currency(text: str) -> str:
"""Expand currency amounts.
Args:
text: Text containing currency amounts.
Returns:
Text with currency expanded.
Example:
>>> expand_currency("Ça coûte 5€.")
'Ça coûte cinq euros.'
"""
if not NUM2WORDS_AVAILABLE:
return text
# Euro
text = re.sub(
r"(\d+)\s*€",
lambda m: number_to_french(int(m.group(1)))
+ (" euro" if int(m.group(1)) == 1 else " euros"),
text,
)
# Dollar
text = re.sub(
r"\$\s*(\d+)",
lambda m: number_to_french(int(m.group(1)))
+ (" dollar" if int(m.group(1)) == 1 else " dollars"),
text,
)
return text
[docs]
def expand_ordinal(text: str) -> str:
"""Expand ordinal numbers like 1er, 2ème, etc.
Args:
text: Text containing ordinal numbers.
Returns:
Text with ordinals expanded.
Example:
>>> expand_ordinal("Le 1er janvier")
'Le premier janvier'
"""
if not NUM2WORDS_AVAILABLE:
return text
# Match patterns like 1er, 1ère, 2e, 2ème, 2nd, 2nde, etc.
def replace_ordinal(match: re.Match[str]) -> str:
num = int(match.group(1))
suffix = match.group(2).lower()
# Handle feminine forms
if suffix in ("ère", "re", "nde"):
# For feminine, we need to modify the output
ordinal = number_to_french(num, ordinal=True)
# Convert masculine to feminine
if ordinal.endswith("premier"):
ordinal = ordinal[:-7] + "première"
elif ordinal.endswith("second"):
ordinal = ordinal[:-6] + "seconde"
return ordinal
return number_to_french(num, ordinal=True)
# Match ordinal patterns
text = re.sub(
r"\b(\d+)(er|ère|re|e|ème|nd|nde)\b",
replace_ordinal,
text,
flags=re.IGNORECASE,
)
return text
[docs]
def is_available() -> bool:
"""Check if num2words is available.
Returns:
True if num2words is installed.
"""
return NUM2WORDS_AVAILABLE