Skip to content
yarunoka.dev

Reference

View Markdown

build(document: YrnkDocument): Record<string, unknown>

The mirror image of parse: the typed model back to the document’s array-and-object representation (JSON.stringify the result for the wire form). Round-tripping is the identity — building a document parsed from the DSL yields the original spelling, structurally.

ensureResolvable(document: YrnkDocument, schedules?: Iterable<YrnkSchedule>): void

Would every name these schedules write be answered by this document’s definitions and bindings? The same validation every query runs first, reachable on its own for a caller that wants a wiring mistake surfaced before a schedule is stored or a question is asked. Consults the definitions and the bindings’ names only and never invokes a resolver, so passing says the references are answerable, not what the answers will be.

hasMatchIn(document: YrnkDocument, schedule: YrnkSchedule, after: YrnkInstant, through: YrnkInstant): boolean

Is there a scheduled point after after, through through? The substance of a firing decision — “is there a scheduled point after the previous run, through now?” maps onto it directly. A point exactly at after does not count (it was the previous judgment’s “now”, already counted); a point exactly at through counts in this judgment. An all-day occurrence counts while its day overlaps the period, however late in the day it is asked: a day is due for as long as it lasts.

matches(document: YrnkDocument, schedule: YrnkSchedule, at: YrnkInstant): boolean

Is the given instant an occurrence of this schedule? For a timed occurrence the answer is instant equality — the given instant, ignoring anything finer than a second (no scheduled point is finer), equals the occurrence’s instant. An all-day occurrence matches on the day alone: yes for every instant whose local date, read in the document timezone, is that day.

Questions are asked per schedule; the top-level OR of the schedules list is composed by the caller (any for the judgments, a merge for the enumeration).

occurrencesIn(document: YrnkDocument, schedule: YrnkSchedule, from: YrnkInstant, through: YrnkInstant): YrnkOccurrence[]

Which occurrences lie from from through through (both boundary instants included)? Timed occurrences are answered as Temporal.ZonedDateTime on the document timezone’s clock, all-day occurrences as Temporal.PlainDate; the two kinds stay distinct, and the answer is in ascending order. Unlike the period judgment, an enumeration has no previous window: the caller names two instants, and both are part of what it names.

parse(input: string | unknown, options?: YrnkParseOptions): YrnkDocument

Parses a Yrnk document (a JSON string or a decoded value) into the typed model. Each schedule is delegated to the schedule parser; what can only be validated with the whole document and its definitions together — resolvability of every name, the data behind the built-in vocabulary, and the declarations the document makes — happens here. The returned document is deeply frozen: the model is data, and the queries trust it not to change underneath them.

class YrnkError extends Error

Every failure this library reports. instanceof YrnkError answers “did Yarunoka reject this”, and code answers what kind of rejection it was. Environment problems (a missing Temporal) are thrown as plain Errors instead: they are breakage of the runtime the library stands on, not an answer about the input.

  • override readonly name = 'YrnkError'
  • readonly code: YrnkErrorCode
  • constructor(code: YrnkErrorCode, message: string)

const SUPPORTED_VERSION = '1.0'

The spec version this implementation reads.

type YrnkCalendar = {
readonly holidays?: YrnkDateSet;
readonly businessHolidays?: YrnkDateSet;
readonly businessDays?: YrnkDateSet;
readonly workweek?: readonly YrnkDayName[];
readonly businessHours?: readonly (readonly [string, string])[];
readonly dateSets: Readonly<Record<string, readonly string[]>>;
};

The definitions part. The built-in definitions carry the layer-model semantics; dateSets is the open namespace. undefined means “not defined” — distinct from an explicit empty list (the statement that there are no such days). Only an undefined workweek means the default (Mon–Fri) instead.

type YrnkCalendarWord =
| 'weekday'
| 'weekend'
| 'holiday'
| 'business_day'
| 'business_holiday';

The five layer-model words. weekday / weekend ask the fixed calendar and consult no definition; holiday asks the holidays list alone; business_day / business_holiday are questions to the stacked conclusion of the layers.

type YrnkDateSet = readonly string[] | string;

A date-list position of the calendar: the list of date literals the document contains, or the name of what resolves it. The two forms are told apart by type, exactly as the DSL tells them apart by shape.

type YrnkDayAtom =
| YrnkDayCondition
| { readonly kind: 'day-cycle'; readonly interval: number };

An atom of the days enumeration.

type YrnkDayCondition =
| { readonly kind: 'month-day'; readonly day: number }
| { readonly kind: 'weekday'; readonly day: YrnkDayName }
| { readonly kind: 'ordinal-weekday'; readonly ordinal: YrnkOrdinal; readonly day: YrnkDayName }
| { readonly kind: 'last-day-of-month' }
| { readonly kind: 'calendar-word'; readonly word: YrnkCalendarWord }
| { readonly kind: 'name'; readonly name: string };

A day atom legal as a shift landing condition or an if condition — every atom except the day cycle, which counts from the schedule’s from and is allowed only in the days enumeration.

type YrnkDayName = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
type YrnkDirection = 'prev' | 'next';
type YrnkDocument = {
readonly version: string;
readonly timezone: string;
/** The names this document leaves to its host; empty when none */
readonly resolvers: readonly string[];
readonly calendar: YrnkCalendar;
readonly schedules: readonly YrnkSchedule[];
readonly label?: string;
readonly description?: string;
readonly [parsedDocument]: true;
};

A parsed Yrnk document. The brand marks that the value went through parse — validated, normalized, and with its resolver bindings registered — telling it apart from raw JSON of the same shape at the type level.

type YrnkErrorCode =
/** The structure or a value of a Yrnk document violates the language */
| 'invalid-document'
/** The document declares a spec version this implementation does not know */
| 'unsupported-version'
/** A name collides with a reserved word or looks like a literal */
| 'reserved-name'
/** A name is neither a date_sets entry nor declared under resolvers */
| 'undefined-name'
/** A declared name has no resolver bound to it */
| 'unregistered-resolver'
/** A value handed to the API violates its contract */
| 'invalid-value'
/** What a resolver returned violates its contract */
| 'invalid-calendar-data'
/** A calendar definition required by the vocabulary in use is missing */
| 'missing-calendar-data';

What went wrong, as a closed set of kinds. One error class with a code rather than a class per kind: instanceof is brittle across realms and duplicated installs, so the discriminant is data and the class is only the catch-all handle.

type YrnkIf = {
readonly direction: YrnkDirection | null;
readonly negated: boolean;
readonly condition: YrnkDayCondition;
};

The if modifier — filtering by the base day itself or a neighbour. shift moves the day; if filters without moving. A null direction means “the day itself”.

type YrnkInstant = Temporal.Instant | Temporal.ZonedDateTime | Date | string;

An instant a query accepts: a Temporal value, a Date, or an ISO 8601 string with a UTC offset. The wire carries moments — a zone-name-only string names no moment and is rejected.

type YrnkOccurrence = Temporal.PlainDate | Temporal.ZonedDateTime;

An occurrence as a query answers it: a whole day (all-day) or an instant on the document timezone’s clock (timed). The two kinds never merge — a day and a timed point at its 00:00 are distinct occurrences, and the types carry that distinction.

type YrnkOrdinal = '1st' | '2nd' | '3rd' | '4th' | '5th' | 'last';
type YrnkParseOptions = {
/** What the host binds the document's declared resolver names to */
readonly resolvers?: Readonly<Record<string, YrnkResolver>>;
};
type YrnkResolver = (range: {
readonly from: Temporal.PlainDate;
readonly through: Temporal.PlainDate;
}) => readonly string[];

What the host binds a declared name to. Asked with the date range the answer has to cover; dates outside it are ignored, and dates missing inside it read as “not in this set”. The contract is synchronous on purpose: an async source is pre-fetched by the caller and wrapped as a resolver returning a static list.

type YrnkSchedule = {
readonly label?: string;
readonly description?: string;
readonly from?: string;
readonly until?: string;
readonly years?: readonly number[];
readonly months?: readonly number[];
readonly days?: readonly YrnkDayAtom[];
readonly shift?: YrnkShift;
readonly if?: YrnkIf;
readonly time: YrnkTimeSpec;
};

One element of the DSL’s schedules[]. The date axes (years / months / days) combine with AND; an absent axis means no restriction. from / until is the validity range — a boundary clipping the schedule’s set of points to [from, until), spelled “YYYY-MM-DD HH:MM” on the document timezone’s clock.

type YrnkShift = {
readonly direction: YrnkDirection;
readonly orSame: boolean;
readonly condition: YrnkDayCondition;
};

The shift modifier — rounding. Takes each base day selected by the days condition and moves it in a fixed direction until the landing condition holds. orSame is the inclusive / exclusive distinction.

type YrnkTimeSpec =
/** An enumeration of fixed times, in written order */
| { readonly kind: 'times'; readonly times: readonly string[] }
/** A clock grid; null between means the whole day [00:00, 24:00) */
| {
readonly kind: 'grid';
readonly every: readonly [number, YrnkTimeUnit];
readonly between: readonly [string, string] | 'business_hour' | null;
}
/** A day-level occurrence that carries no time */
| { readonly kind: 'allday' }
/** The from-anchored interval sequence, counting across days */
| { readonly kind: 'sequence'; readonly every: readonly [number, YrnkTimeUnit] };

The time part of a schedule — exactly one of the three forms the DSL offers, plus the grid’s own shape. Time literals stay as written (zero-padded HH:MM), so round-tripping is the identity.

type YrnkTimeUnit = 'hour' | 'minute' | 'second';