Skip to content

dates

dates

Deterministic date resolution shared by the Succès API and DIA tools.

Functions:

resolve_date_expression

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.

Source code in src/diapason/succes/dates.py
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")