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