Skip to content

Core Concepts

Naive vs. zone-aware values

Like every reference library this is modeled on, cl-date-kit separates values that carry no time zone ("naive" or "local") from ones that do:

Naive Zone-aware
LOCAL-DATE, LOCAL-TIME, LOCAL-DATE-TIME INSTANT, ZONED-DATE-TIME

A LOCAL-DATE-TIME of 2024-03-10T02:30:00 is just four numbers; it does not become a real moment in history until you pair it with a ZONE-OFFSET or TIME-ZONE. Doing that pairing is RESOLVE-LOCAL-DATE-TIME / ZONED-DATE-TIME-OF-LOCAL, and it is the one place naive date-time arithmetic most commonly goes wrong in other libraries -- see Conditions for why it can come back with zero, one, or two answers.

Exact time vs. calendar time

DURATION is an exact elapsed time: DURATION-OF-HOURS 24 is always 86400 seconds. PERIOD is calendar-based: "1 day" means "the same wall-clock time tomorrow," which is 23, 24, or 25 real hours depending on whether a daylight-saving transition falls in between. ZONED-DATE-TIME keeps these as separate operations (-PLUS-DURATION vs. -PLUS-PERIOD) rather than one, because collapsing them is exactly how "add a day" bugs happen twice a year.

RFC 5545 Recurrence Rules

RRULE is an immutable RFC 5545 recurrence-rule value. It describes a local-calendar pattern; RRULE-SCHEDULE supplies a zoned DTSTART for a zone-aware timed event, a LOCAL-DATE-TIME DTSTART for a floating timed event, or a LOCAL-DATE DTSTART for an all-day event. Zoned timed schedules preserve wall-clock intent across daylight-saving transitions rather than treating a recurrence as a fixed elapsed duration. Use ZONED-DATE-TIME-PLUS-DURATION when the requirement is instead a fixed elapsed interval.

Evaluate an RRULE only through a schedule created with MAKE-RRULE-SCHEDULE. A zone-aware timed schedule requires a ZONED-DATE-TIME DTSTART; candidate local date-times are resolved strictly in that zone. A local time in a DST gap does not denote an occurrence and is skipped. When a local time occurs twice during an overlap, evaluation selects the earlier offset. This makes each generated zoned occurrence an unambiguous instant while retaining the rule's local-calendar meaning. A floating schedule uses a LOCAL-DATE-TIME DTSTART and emits LOCAL-DATE-TIME values directly, without zone or DST resolution; its UNTIL, when present, must also be a LOCAL-DATE-TIME. An all-day schedule requires a LOCAL-DATE DTSTART and emits LOCAL-DATE values without zone or DST resolution. Its UNTIL, when present, must also be a LOCAL-DATE; it permits only DAILY, WEEKLY, MONTHLY, and YEARLY frequencies and rejects time-of-day BY* parts.

COUNT bounds the number of emitted occurrences, while UNTIL supplies an inclusive final bound; RFC 5545 makes them mutually exclusive. COUNT is not a search bound: a rule whose filters never produce an occurrence would never reach its count. Therefore MAP-RRULE-OCCURRENCES, RRULE-OCCURRENCES, and DO-RRULE-OCCURRENCES require a positive :MAX-PERIODS argument whenever UNTIL is absent. This is a bound on evaluated frequency periods, not emitted values, and guarantees termination when filters produce no candidates. If this period limit is exhausted before COUNT is reached, evaluation returns the generated prefix. Callback cancellation and RETURN from the iteration macro still stop evaluation early.

ZoneOffset vs. TimeZone

ZONE-OFFSET is a fixed offset from UTC, like +09:00 -- it never changes and has no daylight-saving behavior of its own. TIME-ZONE is a named IANA zone ("Asia/Tokyo", "America/New_York") backed by the parsed rules from its TZif file, and its offset at a given instant can and does change through the year. Anywhere a ZONE-OFFSET is accepted, a fixed offset is fine to use for a value that will never need daylight-saving awareness (recorded UTC timestamps, for instance); anywhere a wall-clock reading needs to become a real moment in a specific place, use FIND-TIME-ZONE.

The Clock protocol

INSTANT-NOW (and, transitively, ZONED-DATE-TIME-NOW, LOCAL-DATE-TIME-NOW, LOCAL-DATE-NOW, and LOCAL-TIME-NOW) read the current time through CLOCK-NOW, a DEFGENERIC -- the one place in this library that dispatches with CLOS rather than a plain DEFSTRUCT and named functions. Pass a FIXED-CLOCK wherever a test needs "now" to be a specific, repeatable value instead of the real wall clock.

For a scoped operation, CALL-WITH-CLOCK invokes a function with a dynamically bound clock and (WITH-CLOCK (clock) ...) provides the equivalent macro form. Every *-NOW operation uses this contextual clock unless its optional CLOCK argument is supplied explicitly.