import re from typing import List, Optional from .dates_add_days import add_days, days_in_month from .dates_add_months import add_months from .dates_day_of_week import day_of_week _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