Skip to content

Commit

Permalink
appscheduler: support weekdays and today/tomorrow in dates
Browse files Browse the repository at this point in the history
Closes #31
  • Loading branch information
eras committed Oct 31, 2023
1 parent 47b96f7 commit aaf6213
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 27 deletions.
125 changes: 102 additions & 23 deletions teslabot/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ def parse(self, args: List[str]) -> ParseResult[T]:
class Regex(Parser[Tuple[Optional[str], ...]]):
regex: "re.Pattern[str]"

def __init__(self, regex: str) -> None:
self.regex = re.compile(regex)
def __init__(self, regex: str, flags: int = 0) -> None:
self.regex = re.compile(regex, flags=flags)

def parse(self, args: List[str]) -> ParseResult[Tuple[Optional[str], ...]]:
if len(args) == 0:
Expand Down Expand Up @@ -592,6 +592,26 @@ def parse(self, args: List[str]) -> ParseResult[str]:
valid_values = ", ".join(self.strings)
return ParseFail(f"Expected one of {valid_values}", processed=0)

class OneOfStringsIndex(Parser[int]):
strings: List[str]

def __init__(self, strings: List[str]) -> None:
self.strings = strings

def parse(self, args: List[str]) -> ParseResult[int]:
if len(args) == 0:
return ParseFail("No argument provided", processed=0)
index: Optional[int] = None
try:
index = [str.lower() for str in self.strings].index(args[0].lower())
except ValueError:
pass
if index is not None:
return ParseOK(index, processed=1)
else:
valid_values = ", ".join(self.strings)
return ParseFail(f"Expected one of {valid_values}", processed=0)

TEnum = TypeVar('TEnum', bound=Enum)

class OneOfEnumValue(Generic[TEnum], Parser[TEnum]):
Expand Down Expand Up @@ -841,14 +861,20 @@ def parse(self, args: List[str]) -> ParseResult[float]:
@dataclass
class _DateWrapped:
yyyymmdd: Optional[Tuple[Optional[str], ...]] = None
today_tomorrow: Optional[Tuple[Optional[str], ...]] = None

class Date(Parser[datetime.date]):
regex: Parser[_DateWrapped]
today: Optional[datetime.date] # future use to support relative dates

def __init__(self, today: Optional[datetime.date] = None) -> None:
self.regex = Map(map=lambda x: _DateWrapped(yyyymmdd=x),
parser=Regex(r"^([0-9]{4})-(0*[0-9]{1,2})-(0*[0-9]{1,2})$"))
self.regex = \
OneOf(
Map(map=lambda x: _DateWrapped(yyyymmdd=x),
parser=Regex(r"^([0-9]{4})-(0*[0-9]{1,2})-(0*[0-9]{1,2})$")),
Map(map=lambda x: _DateWrapped(today_tomorrow=x),
parser=Regex(r"^(today|tomorrow)$")),
)
self.today = today

def parse(self, args: List[str]) -> ParseResult[datetime.date]:
Expand All @@ -859,14 +885,22 @@ def parse(self, args: List[str]) -> ParseResult[datetime.date]:
return ParseFail("Failed to parse date", processed=0)
else:
assert isinstance(result, ParseOK)
yyyymmdd: Tuple[Union[str, None], ...] = assert_some(result.value.yyyymmdd)
try:
return ParseOK(datetime.date(year=int(assert_some(yyyymmdd[0])),
month=int(assert_some(yyyymmdd[1])),
day=int(assert_some(yyyymmdd[2]))),
processed=result.processed)
except ValueError as exc:
return ParseFail(exc.args[0], processed=0)
today = self.today if self.today else datetime.date.today()
if result.value.yyyymmdd is not None:
yyyymmdd: Tuple[Union[str, None], ...] = assert_some(result.value.yyyymmdd)
try:
return ParseOK(datetime.date(year=int(assert_some(yyyymmdd[0])),
month=int(assert_some(yyyymmdd[1])),
day=int(assert_some(yyyymmdd[2]))),
processed=result.processed)
except ValueError as exc:
return ParseFail(exc.args[0], processed=0)
elif result.value.today_tomorrow == ("today",):
return ParseOK(today, processed=result.processed)
elif result.value.today_tomorrow == ("tomorrow",):
return ParseOK(today + datetime.timedelta(days=1), processed=result.processed)
else:
assert False

@dataclass
class _TimeWrapped:
Expand Down Expand Up @@ -933,21 +967,67 @@ def get(x: SuffixInfo[int]) -> int:
assert False
return ParseOK(time, processed=result.processed)

class Weekday(Parser[int]):
"""Parses a weekday; Monday is 0, Sunday is 6, like in datetime.date.weekday"""

parser: Parser[int]
weekdays_short = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
weekdays_long = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]

def __init__(self) -> None:
all_weekdays = Weekday.weekdays_short.copy()
all_weekdays += Weekday.weekdays_long
self.parser = OneOfStringsIndex(strings=all_weekdays)

def parse(self, args: List[str]) -> ParseResult[int]:
result = self.parser(args)
if isinstance(result, ParseOK):
return ParseOK(result.value % 7, processed=result.processed)
else:
assert isinstance(result, ParseFail)
return result.forward(processed=0)

@dataclass
class _DateWeekdayWrapped:
date: Optional[datetime.date] = None
weekday: Optional[int] = None

class DateTime(Parser[datetime.datetime]):
today: Optional[datetime.date]
parser: Adjacent[datetime.date, Tuple[int, int]]
now: Optional[datetime.datetime]
parser: Adjacent[_DateWeekdayWrapped, Tuple[int, int]]

def __init__(self, today: Optional[datetime.date] = None) -> None:
self.today = today
self.parser = Adjacent(Date(today=self.today), HhMm())
def __init__(self, now: Optional[datetime.datetime] = None) -> None:
self.now = now
today = map_optional(now, lambda x: x.date())
self.parser = \
Adjacent(
OneOf(
Map(lambda x: _DateWeekdayWrapped(date=x), Date(today=today)),
Map(lambda x: _DateWeekdayWrapped(weekday=x), Weekday()),
),
HhMm())

def parse(self, args: List[str]) -> ParseResult[datetime.datetime]:
result = self.parser.parse(args)
if isinstance(result, ParseOK):
now = datetime.datetime.combine(result.value[0],
datetime.time(hour=result.value[1][0],
minute=result.value[1][1]))
return ParseOK(now, processed=result.processed)
now = self.now if self.now is not None else datetime.datetime.now()
today = now.date()
time = datetime.time(hour=result.value[1][0], minute=result.value[1][1])
if result.value[0].date is not None:
result_time = datetime.datetime.combine(result.value[0].date,
time)
elif result.value[0].weekday is not None:
# Advanced current
while today.weekday() != result.value[0].weekday:
today += datetime.timedelta(days=1)
result_time = datetime.datetime.combine(today, time)
else:
assert False, "One of the fields should have been filled and checked"
if result_time < now:
return ParseFail(message="Cannot parse times in the past",
processed=0)
else:
return ParseOK(result_time, processed=result.processed)
else:
assert isinstance(result, ParseFail)
return ParseFail(message=result.message,
Expand All @@ -957,8 +1037,7 @@ class TimeOrDateTime(Parser[datetime.datetime]):
parser: Parser[datetime.datetime]

def __init__(self, now: Optional[datetime.datetime] = None) -> None:
today = map_optional(now, lambda x: x.date())
self.parser = OneOf(Labeled("datetime", DateTime(today=today)),
self.parser = OneOf(Labeled("datetime", DateTime(now=now)),
Labeled("time", Time(now=now)))

def parse(self, args: List[str]) -> ParseResult[datetime.datetime]:
Expand Down
86 changes: 82 additions & 4 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,20 @@ def test_date(self) -> None:
day=3),
processed=1))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["today"]),
p.ParseOK(datetime.date(year=2022,
month=2,
day=22),
processed=1))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["tomorrow"]),
p.ParseOK(datetime.date(year=2022,
month=2,
day=23),
processed=1))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022-3-99"]),
p.ParseFail("day is out of range for month",
Expand Down Expand Up @@ -653,25 +667,47 @@ def test_time(self) -> None:

def test_datetime(self) -> None:
now = datetime.datetime.fromisoformat("2022-02-22 01:00")
today = now.date()
with self.subTest():
self.assertEqual(p.DateTime(today=today).parse([]),
self.assertEqual(p.DateTime(now=now).parse([]),
p.ParseFail("No adjacent arguments parsed completely", processed=0))

with self.subTest():
self.assertEqual(p.DateTime(today=today).parse(["2022-03-03"]),
self.assertEqual(p.DateTime(now=now).parse(["2022-03-03"]),
p.ParseFail("No argument provided while parsing right argument", processed=1))

with self.subTest():
self.assertEqual(p.DateTime(today=today).parse(["2022-03-03", "01:00"]),
self.assertEqual(p.DateTime(now=now).parse(["2022-03-03", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=3,
day=3,
hour=1,
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.DateTime(now=now).parse(["today", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=2,
day=22,
hour=1,
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.DateTime(now=now).parse(["tomorrow", "00:30"]),
p.ParseOK(datetime.datetime(year=2022,
month=2,
day=23,
hour=0,
minute=30),
processed=2))

with self.subTest():
self.assertEqual(p.DateTime(now=now).parse(["today", "00:00"]),
p.ParseFail("Cannot parse times in the past", processed=0))

def test_time_or_datetime(self) -> None:
# 2022-02-22 is a Tuesday
now = datetime.datetime.fromisoformat("2022-02-22 01:00")
today = now.date()
with self.subTest():
Expand All @@ -691,6 +727,48 @@ def test_time_or_datetime(self) -> None:
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["today", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=2,
day=22,
hour=1,
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["tuesday", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=2,
day=22,
hour=1,
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["wed", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=2,
day=23,
hour=1,
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["mon", "00:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=2,
day=28,
hour=0,
minute=00),
processed=2))

with self.subTest():
# TODO: not the best error message, should say something about it being in the past
self.assertEqual(p.TimeOrDateTime(now=now).parse(["tuesday", "00:00"]),
p.ParseFail("Invalid value (expected one of datetime, time)",
processed=0))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["00:00"]),
p.ParseOK(datetime.datetime.combine(today,
Expand Down

0 comments on commit aaf6213

Please sign in to comment.