diff --git a/teslabot/appscheduler.py b/teslabot/appscheduler.py index d45c57b..5b0e13e 100644 --- a/teslabot/appscheduler.py +++ b/teslabot/appscheduler.py @@ -92,7 +92,7 @@ def valid_schedulable(app_scheduler: "AppScheduler[T]", ScheduleAtArgs = Tuple[datetime.datetime, CommandWithArgs] def valid_schedule_at(app_scheduler: "AppScheduler[T]") -> p.Parser[ScheduleAtArgs]: - return p.Remaining(p.Adjacent(p.Time(), valid_schedulable(app_scheduler, include_every=True, include_until=True))) + return p.Remaining(p.Adjacent(p.TimeOrDateTime(), valid_schedulable(app_scheduler, include_every=True, include_until=True))) ScheduleEveryArgs = Tuple[Tuple[datetime.timedelta, Optional[datetime.datetime]], @@ -103,14 +103,14 @@ def valid_schedule_every(app_scheduler: "AppScheduler[T]", include_until: bool) "until", p.Optional_( p.Conditional(lambda: include_until, - p.Keyword("until", p.Time()))))), + p.Keyword("until", p.TimeOrDateTime()))))), valid_schedulable(app_scheduler, include_every=False, include_until=include_until))) ScheduleUntilArgs = Tuple[Tuple[datetime.datetime, Optional[datetime.timedelta]], CommandWithArgs] def valid_schedule_until(app_scheduler: "AppScheduler[T]", include_every: bool) -> p.Parser[ScheduleUntilArgs]: - return p.Remaining(p.Adjacent(p.Adjacent(p.Time(), + return p.Remaining(p.Adjacent(p.Adjacent(p.TimeOrDateTime(), p.Labeled( "every", p.Optional_( diff --git a/teslabot/parser.py b/teslabot/parser.py index 01c363f..edfac3c 100644 --- a/teslabot/parser.py +++ b/teslabot/parser.py @@ -838,6 +838,36 @@ def parse(self, args: List[str]) -> ParseResult[float]: meters = value.value * multiplier return ParseOK(meters, processed=result.processed) +@dataclass +class _DateWrapped: + yyyymmdd: 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.today = today + + def parse(self, args: List[str]) -> ParseResult[datetime.date]: + if len(args) == 0: + return ParseFail("No argument provided", processed=0) + result = self.regex.parse(args) + if isinstance(result, ParseFail): + return ParseFail("Failed to parse date", processed=0) + else: + assert isinstance(result, ParseOK) + yyyymmdd: Tuple[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) + @dataclass class _TimeWrapped: hhmm: Optional[Tuple[Optional[str], ...]] = None @@ -902,3 +932,39 @@ def get(x: SuffixInfo[int]) -> int: else: assert False return ParseOK(time, processed=result.processed) + +class DateTime(Parser[datetime.datetime]): + today: Optional[datetime.date] + parser: Adjacent[datetime.date, Tuple[int, int]] + + def __init__(self, today: Optional[datetime.date] = None) -> None: + self.today = today + self.parser = Adjacent(Date(today=self.today), 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) + else: + assert isinstance(result, ParseFail) + return ParseFail(message=result.message, + processed=result.processed) + +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)), + Labeled("time", Time(now=now))) + + def parse(self, args: List[str]) -> ParseResult[datetime.datetime]: + result = self.parser.parse(args) + if isinstance(result, ParseOK): + return ParseOK(result.value, processed=result.processed) + else: + assert isinstance(result, ParseFail) + return result.forward(processed=0) diff --git a/tests/test_parser.py b/tests/test_parser.py index 9d9c653..114e8e2 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -508,6 +508,66 @@ def test_hhmm(self) -> None: self.assertEqual(p.HhMm().parse(["10:10"]), p.ParseOK((10, 10), processed=1)) + def test_date(self) -> None: + now = datetime.datetime.fromisoformat("2022-02-22 01:00") + today = now.date() + with self.subTest(): + self.assertEqual(p.Date(today=today).parse([]), + p.ParseFail("No argument provided", processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["2022-03-03"]), + p.ParseOK(datetime.date(year=2022, + month=3, + day=3), + processed=1)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["2022-3-0003"]), + p.ParseOK(datetime.date(year=2022, + month=3, + day=3), + processed=1)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["2022-3-99"]), + p.ParseFail("day is out of range for month", + processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["2022-99-3"]), + p.ParseFail("month must be in 1..12", + processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["0000-9-3"]), + p.ParseFail("year 0 is out of range", + processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["2022-03-"]), + p.ParseFail("Failed to parse date", + processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["-03-03"]), + p.ParseFail("Failed to parse date", + processed=0)) + with self.subTest(): + self.assertEqual(p.Date(today=today).parse(["2022"]), + p.ParseFail("Failed to parse date", + processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse([""]), + p.ParseFail("Failed to parse date", + processed=0)) + + with self.subTest(): + self.assertEqual(p.Date(today=today).parse([" "]), + p.ParseFail("Failed to parse date", + processed=0)) + def test_time(self) -> None: now = datetime.datetime.fromisoformat("2022-02-22 01:00") today = now.date() @@ -591,6 +651,93 @@ def test_time(self) -> None: datetime.time(11, 10)), processed=3)) + 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([]), + p.ParseFail("No adjacent arguments parsed completely", processed=0)) + + with self.subTest(): + self.assertEqual(p.DateTime(today=today).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"]), + p.ParseOK(datetime.datetime(year=2022, + month=3, + day=3, + hour=1, + minute=00), + processed=2)) + + def test_time_or_datetime(self) -> None: + now = datetime.datetime.fromisoformat("2022-02-22 01:00") + today = now.date() + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse([]), + p.ParseFail("No argument provided", processed=0)) + + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["2022-03-03"]), + p.ParseFail("No argument provided while parsing right argument", processed=1)) + + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(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.TimeOrDateTime(now=now).parse(["00:00"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(0, 0)) + + datetime.timedelta(days=1), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["01:00"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(1, 0)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["02:00"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(2, 0)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["0m"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(1, 0)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["10m"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(1, 10)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["60m"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(2, 0)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["0h"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(1, 0)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["12h"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(13, 0)), + processed=1)) + with self.subTest(): + self.assertEqual(p.TimeOrDateTime(now=now).parse(["12h12m"]), + p.ParseOK(datetime.datetime.combine(today, + datetime.time(13, 12)), + processed=1)) + def test_rest_as_list(self) -> None: with self.subTest(): self.assertEqual(p.List_(p.AnyStr()).parse([]),