Updated apps
This commit is contained in:
@@ -198,17 +198,34 @@ class Schedule:
|
||||
return result
|
||||
|
||||
def _roll_to_season(self, d: date | None) -> date | None:
|
||||
"""Roll a due date outside the seasonal window to the 1st of the next
|
||||
active month; a no-op when there's no window or the date is in season."""
|
||||
"""Roll a due date outside the seasonal window into the next active
|
||||
month; a no-op when there's no window or the date is in season.
|
||||
|
||||
Interval kinds land on the month's 1st ("due once the season starts").
|
||||
Calendar kinds PRESERVE their pattern inside the window — a "2nd
|
||||
Saturday" task must come due on the 2nd Saturday of the active month,
|
||||
not on the 1st (#83). If the pattern (or its ±offset) misses the
|
||||
active month, the search continues into the next one, bounded.
|
||||
"""
|
||||
if d is None or not self.season_months or d.month in self.season_months:
|
||||
return d
|
||||
year, month = d.year, d.month
|
||||
for _ in range(12):
|
||||
for _ in range(24):
|
||||
month += 1
|
||||
if month > 12:
|
||||
month, year = 1, year + 1
|
||||
if month in self.season_months:
|
||||
if month not in self.season_months:
|
||||
continue
|
||||
if self.kind not in _CALENDAR_KINDS:
|
||||
return date(year, month, 1)
|
||||
occ = self._calendar_occurrence(date(year, month, 1), inclusive=True)
|
||||
if occ is None:
|
||||
return date(year, month, 1)
|
||||
if occ.month in self.season_months:
|
||||
return occ
|
||||
# Pattern (e.g. a 5th Friday) or its offset fell outside the
|
||||
# window — keep searching from where the occurrence landed.
|
||||
year, month = occ.year, occ.month
|
||||
return d # season_months held only invalid values — leave the date as-is
|
||||
|
||||
def _compute_next_due(
|
||||
@@ -441,6 +458,50 @@ class Schedule:
|
||||
)
|
||||
|
||||
|
||||
|
||||
def preview_occurrences(
|
||||
schedule: Schedule,
|
||||
*,
|
||||
last_performed: date | None,
|
||||
times_performed: int = 0,
|
||||
today: date,
|
||||
count: int = 3,
|
||||
) -> tuple[list[date], bool]:
|
||||
"""The next ``count`` occurrences a schedule produces, plus whether the
|
||||
series ends within them.
|
||||
|
||||
Simulates an ON-TIME completion per step (last_performed and the
|
||||
planned anchor advance, times_performed increments), so completion-
|
||||
anchored intervals, calendar kinds, season windows, business-day rolls,
|
||||
±offsets and finite series all advance exactly as the engine would.
|
||||
Single source of truth for BOTH preview surfaces — the panel's
|
||||
``schedule/preview`` WS command and the options flow's next-dates line
|
||||
(#83; keep them DRY through this helper).
|
||||
"""
|
||||
occurrences: list[date] = []
|
||||
series_ended = False
|
||||
lp = last_performed
|
||||
lpd: date | None = None
|
||||
times = times_performed
|
||||
for _ in range(count):
|
||||
nxt = schedule.next_due(
|
||||
last_performed=lp,
|
||||
created_at=today,
|
||||
last_planned_due=lpd,
|
||||
today=today,
|
||||
times_performed=times,
|
||||
)
|
||||
if nxt is None:
|
||||
series_ended = True
|
||||
break
|
||||
if occurrences and nxt <= occurrences[-1]: # pragma: no cover
|
||||
break # safety net: the engine must advance — never loop
|
||||
occurrences.append(nxt)
|
||||
lp = nxt
|
||||
lpd = nxt
|
||||
times += 1
|
||||
return occurrences, series_ended
|
||||
|
||||
def is_recurring(task: Mapping[str, Any]) -> bool:
|
||||
"""True iff the task dict has a cycling schedule (interval or calendar kind).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user