RfcPeriod

RfcPeriod :: # (opaque)

RFC 5545 section 3.3.9 PERIOD values, profile period-values-v1. Parse an extracted start/end or start/positive-duration value. Endpoint values share RfcDateTime's year, precision and leap-second profile. Both explicit endpoints must use the same UTC/local form; mixed forms are outside this profile. Property parameters, comma lists and ICS content lines belong to the calling adapter. Source spelling is not retained.

Parsing visits at most 273 bytes and does no zone work. UTC endpoint order is checked immediately. Local endpoint order is checked after interpretation: increasing labels can resolve to equal or reversed boundaries near a gap. Canonical text preserves explicit-end versus duration intent. See the reservation application for native rule execution with parsed period additions.

import time.RfcPeriod
expect {
    value = RfcPeriod.parse("19970101T180000Z/PT5H30M")?
    RfcPeriod.to_text(value) == "19970101T180000Z/PT19800S"
}
parser_for : encoding -> (state -> Try({ value : RfcPeriod, rest : state }, [InvalidRfcPeriod(Error), Encoding(err), ..]))
    where [
        encoding.parse_str : encoding, state -> Try({ value : Str, rest : state }, err),
    ]

Generic encodings carry canonical text, never the opaque backing record. Encoding failures remain distinct from this profile's validation errors. The encoding owns framing and its work limits; parse bounds the decoded text.

encoder_for : encoding -> (RfcPeriod, state -> Try(state, err))
    where [
        encoding.encode_str : Str, state -> Try(state, err),
    ]
from_quote : Str -> Try(RfcPeriod, [BadQuotedBytes(Str)])

Typed quoted literals use the same checked profile at compile time. Runtime interpolation remains Str followed by an explicit parse call.

start : RfcPeriod -> RfcDateTime
ending : RfcPeriod -> Ending
fact_count : RfcPeriod -> U64

These are source-declaration facts, not a resolved span. Local endpoint ordering remains unresolved; a duration ending is not added to its start.

fact_at : RfcPeriod, U64 -> [End, Item(SemanticFact)]
schedule : id, TimedRecurrence, Window, RfcDuration, List(RfcPeriod), Context -> Try(TimedSchedule(id), [TooManyPeriods, IncompatibleContext, TooManySelectors, InvalidDuration, EmptyWindow, ReversedWindow, OutOfRange, TooManyOverrides, ConflictingEnding(LocalDateTime), ..])

Add at most 4096 PERIOD inclusions to a native rule, preserving its existing inclusions, COUNT/UNTIL and source exclusions. Native inclusion capacity also bounds the combined inputs before deduplication. Repeated equal definitions coalesce; conflicting endings at one source fail even when excluded or outside the query. This profile never picks a winner. The context explicitly assigns meaning to the native rule's source labels and window. All periods must match that UTC/local form. Local rules are supplied immutable data, not a TZID lookup. Both starts and local ends use first-fold/before-gap RFC policies. UTC uses fixed zero-offset POSIX rules on [I64.lowest, I64.highest), without leap-second positions. Native rule ranges remain native ranges; the text parser limits only supplied periods. No recurrence or zone work is done here; the returned TimedSchedule uses its ordinary budgets and resumptions. Construction is O(n log n), with bounded input buffers; not a full RRULE parser or an ICS property adapter. Existing duration overrides must be supplied together as periods rather than merged from another schedule.

Ending : [End(RfcDateTime), Duration(RfcDuration)]
Context : [Utc, Local(ZoneRules)]
Error : [Malformed, TooLarge, Start(Error), End(Error), Duration(Error), MixedForms, InvalidPeriod]