diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/source/static.py b/custom_components/waste_collection_schedule/waste_collection_schedule/source/static.py index ab80e0de..86f9ae47 100644 --- a/custom_components/waste_collection_schedule/waste_collection_schedule/source/static.py +++ b/custom_components/waste_collection_schedule/waste_collection_schedule/source/static.py @@ -1,61 +1,59 @@ -from dateutil.rrule import rrule -from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU +import datetime from collections import OrderedDict - -import datetime from dateutil import parser - +from dateutil.rrule import FR, MO, SA, SU, TH, TU, WE, rrule from waste_collection_schedule import Collection # type: ignore[attr-defined] TITLE = "Static Source" DESCRIPTION = "Source for static waste collection schedules." URL = None TEST_CASES = { - "Dates only": {"type": "Dates only", "dates": {"2022-01-01", "2022-02-28"}}, - "Same date twice": {"type": "Dates only", "dates": {"2022-01-01", "2022-01-01"}}, - "Recurrence only": { - "type": "Recurrence only", + "Dates only": {"type": "Dates only", "dates": ["2022-01-01", "2022-02-28"]}, + "Same date twice": {"type": "Dates only", "dates": ["2022-01-01", "2022-01-01"]}, + "Recurrence monthly by date": { + "type": "First day of month", "frequency": "MONTHLY", "interval": 1, "start": "2022-01-01", "until": "2022-12-31", }, - "Recurrence with exception": { - "type": "Recurrence with exception", + "Recurrence monthly by date with date list": { + "type": "First day of month excluding 01-Jan, including 02-Jan", "frequency": "MONTHLY", "interval": 1, "start": "2022-01-01", "until": "2022-12-31", - "excludes": {"2022-01-01"}, - "dates": {"2022-01-02"}, + "excludes": ["2022-01-01"], + "dates": ["2022-01-02"], }, - "Recurrence with Weekday and count": { - "type": "Recurrence with Weekday", + "Recurrence with weekday dict (day + byweekday)": { + "type": "First Monday and second Tuesday of the month", "frequency": "MONTHLY", "start": "2022-01-01", - "until": "2022-12-31", - "weekdays": {"MO": 1, 1: 2}, + "weekdays": {"MO": 1, "TU": 2}, }, - "Recurrence with Weekday without count": { - "type": "Recurrence with Weekday without count", + "Recurrence with first Saturday of the month": { + "type": "First Saturday of the month", "frequency": "MONTHLY", "start": "2022-01-01", - "until": "2022-12-31", - "weekdays": {"MO", 5}, + "weekdays": "SA", }, - "Recurrence with Weekday with and without count": { - "type": "Recurrence with Weekday with and without count", + "Recurrence with last Saturday of the month": { + "type": "Last Saturday of the month", "frequency": "MONTHLY", "start": "2022-01-01", - "until": "2022-12-31", - "weekdays": {"SA": -1, "MO": None, "TU": "Every"}, + "weekdays": {"SA": -1}, + }, + "Recurrence weekly specified by weekday": { + "type": "Every Friday", + "frequency": "WEEKLY", + "weekdays": "FR", }, } FREQNAMES = ["YEARLY", "MONTHLY", "WEEKLY", "DAILY"] -WEEKDAYNAME = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] -WEEKDAYS = [MO, TU, WE, TH, FR, SA, SU] +WEEKDAY_MAP = {"MO": MO, "TU": TU, "WE": WE, "TH": TH, "FR": FR, "SA": SA, "SU": SU} class Source: @@ -67,6 +65,7 @@ class Source: interval: int = 1, start: datetime.date = None, until: datetime.date = None, + count: int = None, excludes: list[str] = None, weekdays: list[str | int] | dict[str | int, int | str | None] = None, ): @@ -74,23 +73,16 @@ class Source: if weekdays is not None: self._weekdays = [] if isinstance(weekdays, dict | OrderedDict): - [self.add_weekday(weekday, count) - for weekday, count in weekdays.items()] + [ + self.add_weekday(weekday, count) + for weekday, count in weekdays.items() + ] - elif isinstance(weekdays, list | set): - for weekday in weekdays or []: - if isinstance(weekday, int): - self._weekdays.append(weekday) - elif isinstance(weekday, str): - self._weekdays.append(WEEKDAYNAME.index(weekday)) - elif isinstance(weekday, dict | OrderedDict): - [self.add_weekday(weekday, count) - for weekday, count in weekday.items()] - else: - raise Exception("Invalid weekdays format") + elif isinstance(weekdays, str): + self.add_weekday(weekdays, 1) else: - raise Exception("Invalid weekdays format") + raise Exception(f"Invalid weekdays format: {weekdays}") if self._weekdays == []: self._weekdays = None @@ -98,31 +90,22 @@ class Source: self._type = type self._dates = [parser.isoparse(d).date() for d in dates or []] - self._recurrence = FREQNAMES.index( - frequency) if frequency is not None else None + self._recurrence = FREQNAMES.index(frequency) if frequency is not None else None self._interval = interval self._start = parser.isoparse(start).date() if start else None - self._until = parser.isoparse(until).date() if until else None + if until: + self._until = parser.isoparse(until).date() + self._count = None + else: + self._until = None + self._count = count if count else 10 self._excludes = [parser.isoparse(d).date() for d in excludes or []] - def add_weekday(self, weekday, count=None): - weekday_index = None - if isinstance(weekday, int): - weekday_index = weekday - elif isinstance(weekday, str): - weekday_index = WEEKDAYNAME.index(weekday) + def add_weekday(self, weekday, count: int): + if weekday not in WEEKDAY_MAP: + raise Exception(f"invalid weekday: {weekday}") - if weekday_index > 6 or weekday_index < 0: - return - - if isinstance(count, str): - count = int(count) if count.isdigit() else "every" - - if count is None or count == "every": - [self._weekdays.append(WEEKDAYS[weekday_index](x)) - for x in range(1, 7)] - return - self._weekdays.append(WEEKDAYS[weekday_index](count)) + self._weekdays.append(WEEKDAY_MAP[weekday](count)) def fetch(self): dates = [] @@ -133,6 +116,7 @@ class Source: interval=self._interval, dtstart=self._start, until=self._until, + count=self._count, byweekday=self._weekdays, ) diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/test/test_sources.py b/custom_components/waste_collection_schedule/waste_collection_schedule/test/test_sources.py index f48928aa..b7882688 100755 --- a/custom_components/waste_collection_schedule/waste_collection_schedule/test/test_sources.py +++ b/custom_components/waste_collection_schedule/waste_collection_schedule/test/test_sources.py @@ -25,6 +25,8 @@ def main(): parser.add_argument( "-i", "--icon", action="store_true", help="Show waste type icon" ) + parser.add_argument("--sorted", action="store_true", help="Sort output by date") + parser.add_argument("--weekday", action="store_true", help="Show weekday") parser.add_argument( "-t", "--traceback", @@ -106,9 +108,15 @@ def main(): ) if args.list: + result = ( + sorted(result, key=lambda x: x.date) if args.sorted else result + ) for x in result: icon_str = f" [{x.icon}]" if args.icon else "" - print(f" {x.date.isoformat()}: {x.type}{icon_str}") + weekday_str = x.date.strftime("%a ") if args.weekday else "" + print( + f" {x.date.isoformat()} {weekday_str}: {x.type}{icon_str}" + ) except KeyboardInterrupt: exit() except Exception as exc: diff --git a/doc/source/static.md b/doc/source/static.md index cb7eb4e9..aae3a794 100644 --- a/doc/source/static.md +++ b/doc/source/static.md @@ -15,7 +15,9 @@ waste_collection_schedule: interval: INTERVAL start: START until: UNTIL + count: COUNT excludes: EXCLUDES + weekdays: WEEKDAYS ``` ### Configuration Variables @@ -51,7 +53,11 @@ Required if *FREQUENCY* is set. *(string) (optional)* Defines the end of the recurrence in the format "YYYY-MM-DD". -Required if *FREQUENCY* is set. + +**COUNT** +*(int) (optional)* + +Defines the (maximum) number of returned dates. Only used if `until` is not specified. Defaults to 10. **EXCLUDES** *(list) (optional)* @@ -59,11 +65,33 @@ Required if *FREQUENCY* is set. A list of dates in format "YYYY-MM-DD" which should be excluded from the recurrence. **WEEKDAYS** -*(list | dictionary) (optional)* +*(weekday | list of weekdays | dictionary of weekday and occurrence) (optional)* -Can either be used to define the week day for weekly frequency (can also be done by setting the start date and leaving this field empty). Or to specify weekday events in Monthly frequency. Should be in format MO, TU, WE, TH, FR, SA, SU or numbers 0-6. in Monthly frequency you may want to give additional parameters like 1 for first 2 for second -1 for last -2 for second to last or "every"(or any String) for all +Used to define the weekday for weekly or monthly frequencies. A weekday is specified by the following weekday constants: `MO, TU, WE, TH, FR, SA, SU`. -## Example +`WEEKDAYS` can be specified in one of the following 3 formats: + +1. Single Weekday: + + ```yaml + weekdays: MO + ``` + +2. List of Weekdays: + + ```yaml + weekdays: [MO, TU, SA] + ``` + +3. Dictionary: + + ```yaml + weekdays: { MO:1, FR:-2 } + ``` + + The additional numerical argument means the nth occurrence of this weekday in the specified frequency (normally only MONTHLY makes sense here). If frequency is set to `MONTHLY`, `MO:1` represents the first Monday of the month. `FR:-2` represents the 2nd last Friday of the month. + +## Examples This example defines a schedule, every 4 weeks starting on Friday, January 14, 2022 until the end of the year. Two days are removed from the schedule and two days are added instead, which are outside of the recurrence. @@ -72,7 +100,6 @@ Two days are removed from the schedule and two days are added instead, which are waste_collection_schedule: sources: - name: static - calendar_title: Altpapier args: type: Altpapier frequency: WEEKLY @@ -82,32 +109,50 @@ waste_collection_schedule: excludes: # Add exception for the recurrence - '2022-07-29' - '2022-09-23' - dates: # Manually define dates that are not part of the recurrence + dates: # Manually add dates that are not part of the recurrence - '2022-07-28' - '2022-09-22' ``` -This example defines a schedule, last Friday of the month and every second Monday of the month, and every Tuesday of the month. From January 14, 2022 until the end of the year. -Two days are removed from the schedule and two days are added instead, which are outside of the recurrence. +--- + +Defines a weekly schedule on Wednesday. + +```yaml +waste_collection_schedule: + sources: + - name: static + args: + type: Altpapier + frequency: WEEKLY + weekdays: WE +``` + +--- + +Defines a schedule for the 2nd last Thursday of a month. ```yaml waste_collection_schedule: sources: - name: static - calendar_title: Altpapier args: type: Altpapier frequency: MONTHLY - weekdays: # add the weekdays - - FR: -1 - - MO: 2 - - TU: Every - start: '2022-01-14' - until: '2022-12-31' - excludes: # Add exception for the recurrence - - '2022-07-29' - - '2022-09-23' - dates: # Manually define dates that are not part of the recurrence - - '2022-07-28' - - '2022-09-22' + weekdays: {TH:-1} +``` + +--- + +Defines a bi-weekly schedule for starting on the 01-Jan-2023. + +```yaml +waste_collection_schedule: + sources: + - name: static + args: + type: Altpapier + frequency: WEEKLY + interval: 2 + start: 2023-01-01 ```