Functional Weave
Code in Python

todo.parse-date-phrase@1.0.0

impl/python.py

3,747 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import List, Optional

from .dates_add_days import add_days, days_in_month  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_of_week  ← from dates.day-of-week ^1.0.0 · built alongside by fune

_WEEKDAYS = {
    "monday": 1, "mon": 1,
    "tuesday": 2, "tue": 2, "tues": 2,
    "wednesday": 3, "wed": 3,
    "thursday": 4, "thu": 4, "thur": 4, "thurs": 4,
    "friday": 5, "fri": 5,
    "saturday": 6, "sat": 6,
    "sunday": 7, "sun": 7,
}

_NUMBER_WORDS = {
    "a": 1, "an": 1, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
    "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
}

_UNITS = {
    "day": "day", "days": "day", "week": "week", "weeks": "week",
    "month": "month", "months": "month", "year": "year", "years": "year",
}

_SPACES = re.compile(r"[ \t\n\r\x0b\x0c]+")
_COUNT = re.compile(r"[1-9][0-9]{0,2}")
_ISO = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _lower_ascii(text: str) -> str:
    """Upper-case ASCII letters only, so no other script can spell a keyword."""
    return "".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in text)


def _split_words(text: str) -> List[str]:
    return [w for w in _SPACES.split(text) if w != ""]


def _is_real_date(word: str) -> bool:
    if not _ISO.fullmatch(word):
        return False
    year, month, day = int(word[0:4]), int(word[5:7]), int(word[8:10])
    return year >= 1 and 1 <= month <= 12 and 1 <= day <= days_in_month(year, month)


def _count(word: str) -> Optional[int]:
    if _COUNT.fullmatch(word):
        return int(word)
    return _NUMBER_WORDS.get(word)


def _days_until(weekday: int, today: int) -> int:
    """Days from today to the next given weekday, 1 to 7: never today itself."""
    return (weekday - today + 6) % 7 + 1


def parse_date_phrase(text: str, today: str) -> Optional[str]:
    """The date a typed phrase names, counted from today, or None when the
    whole text is not one of the phrases in the README."""
    # add_days(x, 0) is the date check: it raises dates.add-days's message.
    add_days(today, 0)
    words = _split_words(_lower_ascii(text))
    if len(words) > 1 and words[0] in ("on", "due"):
        words = words[1:]
    dow = day_of_week(today)

    if len(words) == 1:
        word = words[0]
        if word in ("today", "tod"):
            return today
        if word in ("tomorrow", "tmr", "tmrw"):
            return add_days(today, 1)
        if word == "yesterday":
            return add_days(today, -1)
        if word == "weekend":
            return today if dow >= 6 else add_days(today, 6 - dow)
        if word in _WEEKDAYS:
            return add_days(today, _days_until(_WEEKDAYS[word], dow))
        return word if _is_real_date(word) else None
    if len(words) == 2:
        first, second = words
        if first == "this" and second == "weekend":
            return today if dow >= 6 else add_days(today, 6 - dow)
        if first != "next":
            return None
        next_monday = 8 - dow
        if second == "week":
            return add_days(today, next_monday)
        if second == "month":
            return add_months(today[0:8] + "01", 1)
        if second == "year":
            return add_months(today[0:5] + "01-01", 12)
        if second in _WEEKDAYS:
            return add_days(today, next_monday + _WEEKDAYS[second] - 1)
        return None
    if len(words) == 3 and words[0] == "in":
        n = _count(words[1])
        unit = _UNITS.get(words[2])
        if n is None or unit is None:
            return None
        if unit == "day":
            return add_days(today, n)
        if unit == "week":
            return add_days(today, 7 * n)
        if unit == "month":
            return add_months(today, n)
        return add_months(today, 12 * n)
    return None