TimedRecurrence

TimedRecurrence :: # (opaque)

Native Gregorian timed series, in source-label order. Distinct source labels retain their identity even if gap adjustment maps them to one boundary. Windows select source labels, not timeline overlaps. This is not an RRULE parser. COUNT includes chosen source occurrences before the query window; UNTIL is inclusive in local position. Daily through yearly periods use Gregorian selectors via new; new_subdaily supplies hourly, minutely or secondly periods. Source exclusions are applied after interpretation and COUNT; they do not bypass gap/fold validation. Inclusions do not consume COUNT and merge by source position; durations are supplied by TimedSchedule. Clock candidates are ordered by source label. BYSETPOS indexes that full period after explicit interpretation, without deduplicating equal boundaries. Construction retains at most 4096 positions and bounded field selectors. Cursor work follows examined days/clocks and separately budgeted zone segments. Position selection scans at most 4096 supplied positions per item. Without positions, buffering holds one occurrence. With positions it holds the whole interpreted period, subject to max_buffered. Retained cursors can share buffers; resuming a shared buffer can copy it on append.

new : { date : GregorianDate, clock : ClockTime }, Spec -> Try(TimedRecurrence, [InvalidInterval, TooManySelectors, InvalidSelector(Str), InvalidCombination(Str), OutOfRange, InvalidHour, InvalidMinute, InvalidSecond, UnsupportedLeapSecond, InvalidCount, InvalidUntil, InvalidSetPosition, UnsynchronizedStart, ..])
new_subdaily : { date : GregorianDate, clock : ClockTime }, SubdailySpec -> Try(TimedRecurrence, [InvalidInterval, TooManySelectors, InvalidSelector(Str), InvalidCombination(Str), OutOfRange, InvalidHour, InvalidMinute, InvalidSecond, UnsupportedLeapSecond, InvalidCount, InvalidUntil, InvalidSetPosition, UnsynchronizedStart, ..])
with_exclusions : TimedRecurrence, List(LocalDateTime) -> Try(TimedRecurrence, [TooManySelectors, ..])

Replace the source-position exclusion set; an empty list clears it. Equal local positions match across supported calendar descriptions. Do not confuse source equality with equal resolved boundaries in a gap. At most 4096 inputs; O(n log n) normalization, O(log n) membership. Existing cursors retain their original immutable rule and exclusion set.

with_inclusions : TimedRecurrence, List({ date : GregorianDate, clock : ClockTime }) -> Try(TimedRecurrence, [TooManySelectors, ..])

Replace at most 4096 explicit Gregorian starts, normalized by position. They may precede the anchor or follow COUNT/UNTIL; the query still applies. Duplicate starts merge with the rule, and exclusions win over both. Existing cursors retain their input set. Construction is O(n log n).

add_inclusions : TimedRecurrence, List({ date : GregorianDate, clock : ClockTime }) -> Try(TimedRecurrence, [TooManySelectors, ..])

Add explicit starts while retaining existing inclusions and exclusions. Existing normalized entries plus supplied inputs must total at most 4096 before deduplication. Construction sorts the combined bounded collection.

fact_count : TimedRecurrence -> U64

Effective declaration facts, without clock products or zone evaluation. Clock selectors include normalized defaults; original spelling is absent.

fact_at : TimedRecurrence, U64 -> [End, Item(SemanticFact)]
cursor : TimedRecurrence, Window, Context -> Try(Cursor, [EmptyWindow, ReversedWindow, OutOfRange, ..])
Termination : [Forever, Count(U64), Until(LocalDateTime), UntilBoundary(PosixBoundary)]

Until is inclusive in source-label order. UntilBoundary is inclusive on the POSIX axis after full-period BYSETPOS selection. Explicit inclusions remain outside rule termination. A boundary before all starts yields no rule occurrences; it cannot be compared to the unresolved anchor at new.

Spec : { calendar : Spec, clocks : Spec, termination : Termination, by_set_pos : List(I16) }
Window : { start : LocalDateTime, end : LocalDateTime }
Context : { rules : ZoneRules, occurrence : OccurrencePolicy, gap : [RejectGap, UseOffsetBeforeGap] }
Limits : { max_steps : U64, max_buffered : U64, max_zone_segments : U64, max_zone_candidates : U64 }
Limit : [WorkLimit, BufferLimit, ZoneWorkLimit, ZoneBufferLimit, OutputLimit]
Next : { steps : U64, zone_segments : U64, buffered : U64, zone_buffered : U64, status : [End, Item({ occurrence : Occurrence, cursor : Cursor }), Limited({ cursor : Cursor, reason : Limit })] }
Batch : { occurrences : List(Occurrence), steps : U64, zone_segments : U64, buffered : U64, zone_buffered : U64, status : [Complete, Limited({ cursor : Cursor, reason : Limit })] }

Cursor

TimedRecurrence.Cursor :: # (opaque)
next : Cursor, Limits -> Try(Next, [OutOfRange, OutsideValidity, Gap, Ambiguous, AmbiguousGap, OffsetConflict, UnsynchronizedStart, ..])

Work counts date tests, clock candidates, period boundaries and emitted/filtered candidates. Zone work has a separate total budget. BYSETPOS buffers the whole period and validates DTSTART's position before any first-period output. Without BYSETPOS, only one pending occurrence is retained. Caller-retained cursors may share buffers. Merge explicit starts after rule selection/COUNT, retaining at most one rule lookahead and one pending inclusion classification. Looking ahead may examine a full BYSETPOS period under the same budgets.

collect : Cursor, CollectLimits -> Try(Batch, [OutOfRange, OutsideValidity, Gap, Ambiguous, AmbiguousGap, OffsetConflict, UnsynchronizedStart, ..])

Budgets apply to the entire batch, not separately to each output. Output capacity is a separate bound from pending candidate buffers. At capacity return Limited(OutputLimit) without looking ahead; even an exactly full final batch needs resumption to prove completion.

outcomes : Cursor, Limits -> Iter(Try(Next, [OutOfRange, OutsideValidity, Gap, Ambiguous, AmbiguousGap, OffsetConflict, UnsynchronizedStart]))

A lazy stream of next outcomes. Budgets apply per advancement. End, errors and Limited are terminal items. Resume a Limited cursor explicitly; iterating a zero-work stream cannot retry forever.