from .dates_add_days import add_days from .dates_day_of_week import day_of_week from .subscriptions_next_billing_date import next_billing_date from .todo_item import Recurrence _STEPS = {"daily": "day", "weekly": "week", "monthly": "month", "yearly": "year"} def next_occurrence(recurrence: Recurrence, due: str, today: str) -> str: """The first date of the series strictly after both due and today. Every occurrence is the anchor plus whole intervals, so month ends stay month ends; finishing late skips the occurrences already missed. """ frequency, interval, anchor = recurrence.frequency, recurrence.interval, recurrence.anchor if frequency != "weekdays" and frequency not in _STEPS: raise ValueError(f'unknown repeat frequency "{frequency}": expected daily, weekdays, weekly, monthly or yearly') if isinstance(interval, bool) or not isinstance(interval, int) or interval < 1: raise ValueError(f"repeat interval must be 1 or more, received {interval}") if frequency == "weekdays" and interval != 1: raise ValueError(f"weekday repeats cannot skip weeks: interval must be 1, received {interval}") add_days(anchor, 0) add_days(due, 0) add_days(today, 0) if anchor > due: raise ValueError(f"repeat anchor {anchor} is after the due date {due}") after = due if due > today else today if frequency == "weekdays": nxt = add_days(after, 1) while day_of_week(nxt) > 5: nxt = add_days(nxt, 1) return nxt return next_billing_date(anchor, _STEPS[frequency], interval, after)