DateRecurrence

DateRecurrence :: # (opaque)

Gregorian date-only series. Dates identify occurrences within this series; applications pair them with their own series ID. They are not POSIX instants. Native CalendarPattern defaults apply; this is not an RFC text adapter.

Example

A monthly rule anchored on the 31st skips months without that day. Collect with explicit work and output limits. Limited carries the cursor to resume; even reaching the exact output capacity does not prove completion.

import time.GregorianDate
import time.CalendarPattern
import time.DateRecurrence

expect {
    start = GregorianDate.from_fields({ year: 2025, month: 1, day: 31 })?
    end = GregorianDate.from_fields({ year: 2025, month: 6, day: 1 })?
    rule = DateRecurrence.new(start, {
        pattern: CalendarPattern.defaults(Monthly),
        termination: Count(3),
        by_set_pos: [], inclusions: [], exclusions: [],
    })?
    cursor = DateRecurrence.cursor(rule, { start, end })?
    batch = DateRecurrence.Cursor.collect(cursor, {
        max_steps: 1000, max_buffered: 31, max_occurrences: 4,
    })?
    months = batch.dates.map(|date| GregorianDate.to_fields(date).month)
    months == [1.U8, 3, 5] and match batch.status {
        Complete => Bool.True
        Limited(_) => Bool.False
    }
}

Examples assume a package dependency named time.

definition : DateRecurrence -> Definition

Read the semantic definition without scanning periods or occurrences. Immutable selector/exception lists are shared. Reconstruct edits with new; inclusions and exclusions are already sorted and unique.

new : GregorianDate, Spec -> Try(DateRecurrence, [InvalidInterval, TooManySelectors, InvalidSelector(Str), InvalidCombination(Str), InvalidCount, InvalidUntil, UnsynchronizedStart, OutOfRange, ..])

O(366*s + n log n) construction, where s is selector count and n is the number of explicit dates. Each supplied list is limited to 4096 entries. Validate DTSTART against the whole first period, including BYSETPOS. Invalid generated dates are skipped; UNTIL is inclusive. COUNT applies before exclusions; explicit inclusions do not consume or replenish COUNT.

cursor : DateRecurrence, Window -> Try(Cursor, [EmptyWindow, ReversedWindow, ..])

Bind a fresh cursor to this exact immutable rule and a finite half-open date window. Evaluation starts at the series anchor, preserving COUNT.

fact_count : DateRecurrence -> U64

Declaration facts only: no cursor, period scan or occurrence generation.

fact_at : DateRecurrence, U64 -> [End, Item(SemanticFact)]

Read one semantic explanation fact by zero-based index; End means the index is past the declaration’s facts. Does not resolve zones or enumerate occurrences.

to_inspect : DateRecurrence -> Str

Return a bounded diagnostic summary without advancing cursors or enumerating occurrences. Use typed accessors for application logic; this text is not a storage format.

Termination : [Forever, Count(U64), Until(GregorianDate)]

Stop the generated series by source count or inclusive final date, or leave it unbounded. Exclusions do not replenish COUNT; explicit inclusions remain outside rule termination.

Spec : {
    pattern : Spec,
    termination : Termination,
    by_set_pos : List(I16),
    inclusions : List(GregorianDate),
    exclusions : List(GregorianDate),
}

Calendar candidates, termination, period positions and explicit date additions/exclusions. Construction checks selector combinations before any query begins.

Definition : { anchor : GregorianDate, spec : Spec }

The anchor and checked declaration fields, independent of query progress. Rebuilding a declaration creates no occurrences and does not save a cursor.

Window : { start : GregorianDate, end : GregorianDate }

Half-open source-date query: include starts at start, exclude starts at end. The original series anchor and COUNT are retained across windows.

Limits : { max_steps : U64, max_buffered : U64, max_occurrences : U64 }

Maximum candidate work, buffered dates and emitted dates for this call. A limit is an incomplete result, not proof that no more dates exist.

Limit : [WorkLimit, BufferLimit, OutputLimit]

The exhausted candidate-work, buffer or output budget. Continue from the returned cursor rather than recreating a recurrence from the last visible date.

Batch : {
    dates : List(GregorianDate),
    steps : U64,
    buffered : U64,
    status : [Complete, Limited({ cursor : Cursor, reason : Limit })],
}

Dates collected within the window, work accounting and Complete or Limited status. Limited preserves the continuation even if no dates were emitted.

FoldBatch : {
    value : acc,
    occurrences : U64,
    steps : U64,
    buffered : U64,
    status : [Complete, Limited({ cursor : Cursor, reason : Limit }), Stopped(Cursor)],
}

Accumulator and work accounting from bounded folding. A limited result preserves both the accumulator and continuation.

Next : {
    steps : U64,
    buffered : U64,
    status : [End, Item({ date : GregorianDate, cursor : Cursor }), Limited({ cursor : Cursor, reason : Limit })],
}

One date and continuation, End, or a resumable limit with the work performed. Always inspect the status before consuming a date.

Cursor

DateRecurrence.Cursor :: # (opaque)

Cursor representation is private. Resume accepts no replacement rule or window, so a cursor cannot accidentally be used with stale inputs.

collect : Cursor, Limits -> Try(Batch, [OutOfRange, ..])

One step examines one date, buffered candidate, explicit date, or period/state boundary. Each step has bounded selector/calendar work. Buffer capacity counts retained candidates (at most 366); it is not an allocation-byte claim. Output and immutable input lists are separate. A zero budget is valid and can return Limited without advancing. Reducing capacity below an already retained buffer returns BufferLimit without advancing; Batch.buffered reports that existing retained count. Shared cursors may copy a period buffer; no scan hides inside one step.

fold : Cursor, Limits, acc, (acc, GregorianDate -> [Continue(acc), Stop(acc)]) -> Try(FoldBatch(acc), [OutOfRange, ..])

Fold visits ordered, deduplicated dates after exclusions and query filtering. Stop(value) consumes that date and returns its exact next cursor, without searching ahead. Stopped never claims completeness. max_occurrences limits visitor calls; steps account for engine work, not the caller's visitor. No intermediate output list is constructed. Resume with the returned cursor and value to preserve the accumulator.

next : Cursor, { max_steps : U64, max_buffered : U64 } -> Try(Next, [OutOfRange, ..])

Return one date without looking for the following date. End is proven exhaustion; Limited needs more work/capacity and is not end-of-series.

chunks : Cursor, Limits -> Iter(Try(Batch, [OutOfRange]))

Lazy Roc iterator of bounded batches. Each advance performs one collect; no series expansion happens while constructing this iterator. Complete, Limited and errors remain visible items. Iter exhaustion is transport exhaustion, not proof that the temporal query completed. Positive work/output limits continue automatically. BufferLimit or a zero work/output budget emits one terminal Limited item; its cursor can be resumed explicitly with larger limits. Errors are emitted once. Retaining chunks/rest iterators can retain their captured buffers.

to_inspect : Cursor -> Str

Return a bounded diagnostic summary without advancing cursors or enumerating occurrences. Use typed accessors for application logic; this text is not a storage format.