def resolve_date_expression(
expression: str | None,
*,
timezone: str = DEFAULT_TIMEZONE,
now: datetime | date | None = None,
) -> DateResolution:
"""Resolve common French, Haitian Creole and English date expressions.
Ambiguous phrases such as ``vendredi prochain`` deliberately return two
choices and never write data. This ports the legacy PHP assistant rule.
"""
raw = (expression or "").strip()
if not raw:
return DateResolution("unrecognized")
folded = _fold(raw)
today = _today(timezone, now)
try:
parsed = date.fromisoformat(raw)
except ValueError:
parsed = None
if parsed is not None:
return DateResolution("exact", parsed.isoformat())
french = re.fullmatch(r"(\d{1,2})/(\d{1,2})/(\d{4})", folded)
if french:
try:
parsed = date(
int(french.group(3)), int(french.group(2)), int(french.group(1))
)
except ValueError:
return DateResolution("unrecognized")
return DateResolution("exact", parsed.isoformat())
relative = {
"aujourd'hui": 0,
"aujourdhui": 0,
"today": 0,
"jodi a": 0,
"jodia": 0,
"demain": 1,
"tomorrow": 1,
"demen": 1,
"apres-demain": 2,
"apres demain": 2,
"day after tomorrow": 2,
"apre demen": 2,
"hier": -1,
"yesterday": -1,
"ye": -1,
}
if folded in relative:
return DateResolution(
"exact", (today + timedelta(days=relative[folded])).isoformat()
)
plus = re.fullmatch(r"\+(\d{1,4})\s*j", folded)
if plus:
return DateResolution(
"exact", (today + timedelta(days=int(plus.group(1)))).isoformat()
)
in_amount = re.fullmatch(
r"(?:dans|in)\s+(\d{1,4})\s+(jour|jours|day|days|semaine|semaines|week|weeks)",
folded,
)
if in_amount:
amount = int(in_amount.group(1))
if "semaine" in in_amount.group(2) or "week" in in_amount.group(2):
amount *= 7
return DateResolution("exact", (today + timedelta(days=amount)).isoformat())
if folded in {"ce weekend", "ce week-end", "this weekend"}:
saturday = today + timedelta(days=(5 - today.weekday()) % 7)
return DateResolution("exact", saturday.isoformat())
if folded in {"semaine prochaine", "la semaine prochaine", "next week"}:
monday = today + timedelta(days=(7 - today.weekday()))
return DateResolution("exact", monday.isoformat())
weekday_match = re.fullmatch(r"([a-z]+)(?:\s+(prochain|prochaine|next))?", folded)
if weekday_match and weekday_match.group(1) in _WEEKDAYS:
first = _next_weekday(today, _WEEKDAYS[weekday_match.group(1)])
if weekday_match.group(2):
return DateResolution(
"ambiguous",
options=(first.isoformat(), (first + timedelta(days=7)).isoformat()),
)
return DateResolution("exact", first.isoformat())
return DateResolution("unrecognized")