Functional Weave
Code in TypeScript

todo.parse-quick-add@1.0.0

impl/python.py

4,963 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, Tuple

from .dates_add_days import add_days  ← from dates.add-days ^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
from .todo_item import Recurrence, TodoDraft  ← from todo.item ^1.0.0 · built alongside by fune
from .todo_normalise_tags import normalise_tags  ← from todo.normalise-tags ^1.0.0 · built alongside by fune
from .todo_parse_date_phrase import parse_date_phrase  ← from todo.parse-date-phrase ^1.0.0 · built alongside by fune

_PRIORITIES = {
    "!high": "high", "!h": "high", "!1": "high", "!!!": "high",
    "!medium": "medium", "!med": "medium", "!m": "medium", "!2": "medium", "!!": "medium",
    "!low": "low", "!l": "low", "!3": "low",
    "!none": "none",
}

# Short words that are also ordinary title words ("Read the sun"): on their
# own they stay in the title; "on sun" or "due sat" still sets the date.
_GUARDED = {"mon", "tue", "tues", "wed", "thu", "thur", "thurs", "fri", "sat", "sun", "tod", "weekend"}

_WEEKDAY_WORDS = {
    "monday", "mon", "tuesday", "tue", "tues", "wednesday", "wed", "thursday", "thu", "thur", "thurs",
    "friday", "fri", "saturday", "sat", "sunday", "sun",
}

_UNIT_FREQUENCIES = {
    "day": "daily", "days": "daily", "week": "weekly", "weeks": "weekly",
    "month": "monthly", "months": "monthly", "year": "yearly", "years": "yearly",
}

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

# The longest phrase todo.parse-date-phrase accepts: "due in 3 days".
_MAX_DATE_WORDS = 4

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

# (frequency, interval, weekday word or None, words used)
_Repeat = Tuple[str, int, Optional[str], int]


def _lower_ascii(text: str) -> str:
    return "".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in text)


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


def _repeat_at(words: List[str], i: int) -> Optional[_Repeat]:
    """The repeat phrase starting with "every" at words[i], or None."""
    if i + 1 >= len(words):
        return None
    a = _lower_ascii(words[i + 1])
    if a in ("weekday", "weekdays"):
        return ("weekdays", 1, None, 2)
    if a in _UNIT_FREQUENCIES:
        return (_UNIT_FREQUENCIES[a], 1, None, 2)
    if a in _WEEKDAY_WORDS:
        return ("weekly", 1, a, 2)
    if i + 2 >= len(words):
        return None
    every = _UNIT_FREQUENCIES.get(_lower_ascii(words[i + 2]))
    if every is None:
        return None
    n = 2 if a == "other" else _count(a)
    return None if n is None else (every, n, None, 3)


def parse_quick_add(text: str, today: str) -> TodoDraft:
    """One typed line into a TodoDraft: #tags, a !priority, the first date
    phrase and the first "every ..." repeat come out; every other word stays in
    the title, in order, single-spaced. The title can come out empty."""
    # add_days(x, 0) is the date check: it raises dates.add-days's message.
    add_days(today, 0)
    words = [w for w in _SPACES.split(text) if w != ""]
    title: List[str] = []
    tag_words: List[str] = []
    priority: Optional[str] = None
    due: Optional[str] = None
    repeat: Optional[_Repeat] = None

    i = 0
    while i < len(words):
        word = words[i]
        lower = _lower_ascii(word)
        if repeat is None and lower == "every":
            found = _repeat_at(words, i)
            if found is not None:
                repeat = found
                i += found[3]
                continue
        if priority is None and lower in _PRIORITIES:
            priority = _PRIORITIES[lower]
            i += 1
            continue
        if len(word) > 1 and word[0] == "#" and len(normalise_tags([word])) > 0:
            tag_words.append(word)
            i += 1
            continue
        if due is None:
            taken = 0
            for length in range(min(_MAX_DATE_WORDS, len(words) - i), 0, -1):
                if length == 1 and lower in _GUARDED:
                    continue
                date = parse_date_phrase(" ".join(words[i:i + length]), today)
                if date is not None:
                    due = date
                    taken = length
                    break
            if taken > 0:
                i += taken
                continue
        title.append(word)
        i += 1

    recurrence: Optional[Recurrence] = None
    if repeat is not None:
        frequency, interval, weekday, _ = repeat
        if due is None:
            if frequency == "weekdays":
                dow = day_of_week(today)
                due = today if dow <= 5 else add_days(today, 8 - dow)
            elif weekday is not None:
                due = parse_date_phrase(weekday, today)
            else:
                due = today
        recurrence = Recurrence(frequency=frequency, interval=interval, anchor=due)
    return TodoDraft(
        title=" ".join(title),
        notes=None,
        priority=priority if priority is not None else "none",
        due=due,
        tags=normalise_tags(tag_words),
        recurrence=recurrence,
    )