Functional Weave
Code in TypeScript

todo.parse-repeat@1.0.0

impl/python.py

2,160 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 dataclasses import replace
from typing import Optional

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  ← from todo.item ^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
from .todo_parse_quick_add import parse_quick_add  ← from todo.parse-quick-add ^1.0.0 · built alongside by fune

_SPACE = re.compile(r"[ \t\n\r\x0b\x0c]+")


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


def parse_repeat(text: str, due: Optional[str], today: str) -> Optional[Recurrence]:
    """A Repeat box: an "every ..." phrase as todo.parse-quick-add reads it, then optionally a start date."""
    # add_days(x, 0) is the date check: it raises dates.add-days's message.
    add_days(today, 0)
    if due is not None:
        add_days(due, 0)
    words = [w for w in _SPACE.split(text) if w != ""]
    if len(words) < 2 or _lower_ascii(words[0]) != "every":
        return None

    # The repeat is 2 words ("every week") or 3 ("every 2 weeks"); no 2-word
    # form starts a 3-word one, so the first that parses as a bare repeat wins.
    repeat: Optional[Recurrence] = None
    length = 0
    for n in (2, 3):
        if n > len(words):
            break
        draft = parse_quick_add(" ".join(words[:n]), today)
        if draft.recurrence is not None and draft.title == "":
            repeat = draft.recurrence
            length = n
            break
    if repeat is None:
        return None

    start = due
    if len(words) > length:
        start = parse_date_phrase(" ".join(words[length:]), today)
        if start is None:
            return None
    if start is None:
        return repeat

    if repeat.frequency == "weekdays":
        day = day_of_week(start)
        if day >= 6:
            start = add_days(start, 8 - day)
    elif repeat.frequency == "weekly" and length == 2 and parse_date_phrase(words[1], today) is not None:
        # "every monday": quick-add anchored it on the next Monday, so that
        # anchor's weekday is the one the series keeps.
        start = add_days(start, (day_of_week(repeat.anchor) - day_of_week(start) + 7) % 7)
    return replace(repeat, anchor=start)