Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List
from .dates_add_days import days_from_civil, days_in_month, epoch_day_from_iso, iso_from_epoch_day, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_of_week ← from dates.day-of-week ^1.0.0 · built alongside by fune
from .dates_recurrence_types import RecurrenceRule
def _is_int(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def _check_range(name: str, value: object, low: int, high: int, kind: str) -> None:
if not _is_int(value) or value < low or value > high: # type: ignore[operator]
raise ValueError("%s must be %s-%s for %s, received %s" % (name, low, high, kind, value))
def _date_in_month(index: int, rule: RecurrenceRule) -> int:
"""The date of the ``day``-th (clamped) or last ``weekday`` in a month index counted from year 0."""
year = index // 12
month = index - year * 12 + 1
if year > 9999:
raise ValueError("the schedule runs past 9999-12-31, outside the supported range 0001-01-01 to 9999-12-31")
length = days_in_month(year, month)
if rule.kind == "monthly-on-day":
return days_from_civil(year, month, min(rule.day, length))
last = days_from_civil(year, month, length)
last_weekday = day_of_week(iso_from_epoch_day(last))
return last - (last_weekday - rule.weekday) % 7
def recurrence(rule: RecurrenceRule, start_iso: str, count: int) -> List[str]:
"""The first ``count`` dates of a rule on or after ``start_iso``.
Monthly dates are clamped from the rule's day every time, never from the
previous date, so the 31st does not decay to the 28th after February.
"""
if not _is_int(count) or count < 0:
raise ValueError("count must be a non-negative integer, received %s" % (count,))
if not _is_int(rule.interval) or rule.interval < 1:
raise ValueError("interval must be a positive integer, received %s" % (rule.interval,))
kind = rule.kind
if kind == "monthly-on-day":
if rule.day is None:
raise ValueError("day must be 1-31 for monthly-on-day, received null")
_check_range("day", rule.day, 1, 31, kind)
if rule.weekday is not None:
raise ValueError("weekday does not apply to monthly-on-day")
elif kind == "last-weekday-of-month":
if rule.weekday is None:
raise ValueError("weekday must be 1-7 for last-weekday-of-month, received null")
_check_range("weekday", rule.weekday, 1, 7, kind)
if rule.day is not None:
raise ValueError("day does not apply to last-weekday-of-month")
elif kind in ("every-days", "every-weeks", "yearly"):
if rule.day is not None:
raise ValueError("day does not apply to %s" % (kind,))
if rule.weekday is not None:
raise ValueError("weekday does not apply to %s" % (kind,))
else:
raise ValueError(
'unknown recurrence kind "%s": expected monthly-on-day, last-weekday-of-month, every-days, every-weeks or yearly'
% (kind,)
)
start = epoch_day_from_iso(start_iso)
out: List[str] = []
if kind in ("every-days", "every-weeks"):
step = 7 * rule.interval if kind == "every-weeks" else rule.interval
for k in range(count):
out.append(iso_from_epoch_day(start + k * step))
elif kind == "yearly":
# Always from the anchor, so 29 February comes back in leap years.
for k in range(count):
out.append(add_months(start_iso, 12 * rule.interval * k))
else:
date = parse_iso_date(start_iso)
first = date.year * 12 + (date.month - 1)
if count > 0 and _date_in_month(first, rule) < start:
first += 1
for k in range(count):
out.append(iso_from_epoch_day(_date_in_month(first + k * rule.interval, rule)))
return out