TimedSchedule

TimedSchedule(id) :: # (opaque)

Identified appointment spans in source-label order. The window selects starts; duration may extend beyond it. IDs preserve the series and source. Start and end interpretation share one zone-work budget per call. new_with_endings supports source-specific duration or explicit-end overrides.

Example

Use this layer when each recurrence start should become an identified span. Coordinate measures POSIX microseconds; Calendar applies calendar components, resolves the end, then adds its coordinate tail. The window selects starts, not every appointment overlapping that window.

This example supplies a finite UTC rules snapshot. Applications using named zones can pass rules from the optional zone-data package instead. For a complete application with calendar-month durations and an extra booking, see equipment loans.

import time.TimedSchedule
import time.TimedRecurrence
import time.TimedOccurrence
import time.CalendarPattern
import time.GregorianDate
import time.Calendar.Date
import time.ClockTime
import time.LocalDateTime
import time.ZoneRules
import time.FixedOffset
import time.PosixSpan
import time.PosixBoundary
import time.PosixDelta

expect {
    date = GregorianDate.from_fields({ year: 1970, month: 1, day: 1 })?
    end_date = GregorianDate.from_fields({ year: 1970, month: 1, day: 4 })?
    clock = ClockTime.from_microseconds_since_midnight(0)?
    start = LocalDateTime.new(Calendar.Date.from_gregorian(date), clock)
    end = LocalDateTime.new(Calendar.Date.from_gregorian(end_date), clock)
    validity = PosixSpan.from_seconds(-1, 345600, RejectSubmicrosecond)?
    rules = ZoneRules.new_bounded(
        "UTC", "fixed", validity,
        FixedOffset.from_seconds(0), [],
        { minimum: 0, maximum: 0 },
    )?
    rule = TimedRecurrence.new({ date, clock }, {
        calendar: CalendarPattern.defaults(Daily),
        clocks: { hours: [], minutes: [], seconds: [] },
        termination: Count(2),
        by_set_pos: [],
    })?
    duration = Coordinate(PosixDelta.from_microseconds(3600000000))
    schedule = TimedSchedule.new(
        42.U64, rule, { start, end }, duration,
        { rules, occurrence: RequireUnique, gap: RejectGap },
    )?
    work = {
        max_steps: 100, max_buffered: 1,
        max_zone_segments: 10, max_zone_candidates: 1,
    }
    result = TimedSchedule.collect(schedule, {
        work, max_occurrences: 3,
    })
    match result {
        Err(_) => Bool.False
        Ok(batch) => match batch.status {
            Complete => batch.occurrences.len() == 2
            Limited(_) => Bool.False
        }
    }
}

When a batch returns Limited, process its partial output and resume the returned cursor with sufficient budgets. Increasing only the output cap does not fix a work or buffer limit.

new : id, TimedRecurrence, Window, Duration, Context -> Try(TimedSchedule(id), [InvalidDuration, EmptyWindow, ReversedWindow, OutOfRange, ..])

Start an identified appointment query with one default duration and explicit start context. The window selects source starts; endings may extend beyond it. Invalid duration/window errors are checked before consumption.

new_with_overrides : id, TimedRecurrence, Window, Duration, List(Override), Context -> Try(TimedSchedule(id), [InvalidDuration, EmptyWindow, ReversedWindow, OutOfRange, TooManyOverrides, ConflictingDuration(LocalDateTime), ..])

Override durations at original source positions, including explicit inclusions. This does not add starts: configure those on the rule. Exclusions still win; overrides do not replenish COUNT or change IDs. At most 4096 inputs, sorted once in O(n log n), then O(log n) lookup. Identical definitions coalesce; conflicting definitions at one position return ConflictingDuration, even outside the query or at excluded starts. Calendar and coordinate definitions remain distinct despite equal widths.

new_with_endings : id, TimedRecurrence, Window, Duration, List(EndOverride), Context -> Try(TimedSchedule(id), [InvalidDuration, EmptyWindow, ReversedWindow, OutOfRange, TooManyOverrides, ConflictingEnding(LocalDateTime), ..])

Override a source with a duration or explicit end, without adding starts. The same 4096-entry limit and positional lookup apply. Equal definitions coalesce; different definitions at one position return ConflictingEnding. Endpoint order is checked after the start and end are interpreted.

from_prepared : id, TimedRecurrence, Window, Endings, Context -> Try(TimedSchedule(id), [EmptyWindow, ReversedWindow, OutOfRange, ..])

Advanced construction from checked ending definitions. Duration validation and override normalization are not repeated; query state is fresh. This carries no persistence semantics. Use ScheduleDefinition for ordinary reusable declarations that also retain their explicit context and origin.

next : TimedSchedule(id), Limits -> Try(Next(id), Error)

At most one start advancement and one end advancement. A pending end resumes before consuming another start. Steps count source-candidate work; constructing a duration adds only bounded calendar arithmetic. Start buffers and the pending end's zone candidates have separate caps.

collect : TimedSchedule(id), { work : Limits, max_occurrences : U64 } -> Try(Batch(id), Error)

Output capacity returns Limited without looking ahead. Candidate and zone budgets are totals across every start and end in this batch.

outcomes : TimedSchedule(id), Limits -> Iter(Try(Next(id), Error))

End, errors and Limited are terminal items; resume a limited cursor explicitly with new budgets. Each advancement uses the supplied limits.

to_inspect : TimedSchedule(id) -> 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.

Endings

TimedSchedule.Endings :: # (opaque)

Checked normalized ending definitions for advanced prepared scheduling. Construction validates duration and normalizes at most 4096 overrides once; no zone resolution or recurrence expansion. This is not a storage format.

normalize : List(Entry) -> Try(List(Entry), [InvalidDuration, TooManyOverrides, ConflictingEnding(LocalDateTime), ..])

Check and normalize up to 4096 source-specific ending overrides. Duplicate equal endings collapse; conflicting endings for one source return ConflictingEnding.

new : Duration, List(Entry) -> Try(Endings, [InvalidDuration, TooManyOverrides, ConflictingEnding(LocalDateTime), ..])

Validate the positive default duration and normalize ending overrides once for reuse across queries. Does not resolve zones or enumerate starts.

is_eq : Endings, Endings -> Bool

Compare retained duration and normalized source-ending intent. This is declaration equality, not equivalence of resulting appointment spans.

to_hash : Endings, Hasher -> Hasher

Hash the same declaration fields used by equality, including policies and original override sources, for dictionary keys.

definition : Endings -> { duration : Duration, overrides : List(Entry) }

Read the checked default duration and normalized overrides for editing or inspection. The returned fields are not a persistence format.

EndOverride : { source : LocalDateTime, ending : Ending }

Replace the ending of one original source label with duration, explicit boundary or explicit local-end intent. Equal resolved boundaries do not merge source identities.

Override : { source : LocalDateTime, duration : Duration }

Duration-only override for an original source label. Use EndOverride when an explicit endpoint must be retained.

Limits : Limits

Candidate and zone budgets shared by start and end interpretation during a call; output capacity is supplied separately to collect.

Limit : [StartWorkLimit, StartBufferLimit, StartZoneWorkLimit, StartZoneBufferLimit, EndZoneWorkLimit, EndZoneBufferLimit, OutputLimit]

Which start, end or output budget stopped execution. Resume the returned cursor to retain source-series and pending-ending state.

Next : { steps : U64, zone_segments : U64, start_buffered : U64, start_zone_buffered : U64, end_buffered : U64, status : [End, Item({ occurrence : TimedOccurrence({ series : id, source : LocalDateTime }), cursor : TimedSchedule(id) }), Limited({ cursor : TimedSchedule(id), reason : Limit })] }

One complete identified appointment with its continuation, End, or a resumable limit; counters include start and end work.

Batch : { occurrences : List(TimedOccurrence({ series : id, source : LocalDateTime })), steps : U64, zone_segments : U64, status : [Complete, Limited({ cursor : TimedSchedule(id), reason : Limit })] }

Complete appointments emitted so far, work counters and Complete/Limited status. A Limited batch is valid partial output, not a complete schedule.

Error : [InvalidDuration, OutOfRange, OutsideValidity, InvalidDestination(Fields), Gap, Ambiguous, AmbiguousGap, OffsetConflict, UnsynchronizedStart]

Malformed duration, invalid window, unavailable zone interpretation or range failure. Resource exhaustion is represented separately by a Limited result.