/** How an occasion repeats. `annual` is the ordinary case; `once` never returns. */ export type OccasionRecurrence = 'annual' | 'once'; /** * A parsed occasion date. * * `recurring` has no year because the year is meaningless for an annual date and * carrying one invites arithmetic that uses it. `dated` has one because a `once` * occasion without a year is not a date at all. */ export type OccasionDate = { readonly kind: 'recurring'; readonly month: number; readonly day: number; } | { readonly kind: 'dated'; readonly year: number; readonly month: number; readonly day: number; }; /** `YYYY-MM-DD`, the only date representation that crosses a module boundary here. */ export type IsoDate = string; /** Days in a month, 1-based, honouring the leap rule. */ export declare function daysInMonth(year: number, month: number): number; export declare function isLeapYear(year: number): boolean; /** * Parse `MM-DD` or `YYYY-MM-DD`, or `null` for anything else. * * The calendar is checked, not just the shape: `02-30` and `2026-13-01` are * refused. That matters because an occasion whose date does not exist would * otherwise sit in the file looking healthy and never fire, and "it never * reminded me" is the failure this whole feature exists to prevent. */ export declare function parseOccasionDate(value: string): OccasionDate | null; /** Render a date back to the form it is written in. Inverse of the parser. */ export declare function renderOccasionDate(date: OccasionDate): string; export declare function toIsoDate(year: number, month: number, day: number): IsoDate; /** * The occurrence of `date` in `year`, with 29 February landing on the 28th. * * Owner-facing consequence: in a non-leap year a 29 February birthday is raised * on the 28th rather than skipped. Skipping is the behaviour a naive * implementation produces (the date simply does not exist, so nothing matches) * and it means the feature silently does nothing three years in four for the * person it was built for. Landing early is a day out; landing never is the * whole failure. */ export declare function occurrenceInYear(date: OccasionDate, year: number): IsoDate; /** * The next occurrence on or after `today`, or `null` when there is not one. * * A `once` occasion that has passed returns `null`, it is over, and an * occasion that keeps proposing a date in the past would nudge forever. * An `annual` occasion always has a next one. */ export declare function nextOccurrence(date: OccasionDate, recurrence: OccasionRecurrence, today: IsoDate): IsoDate | null; /** * Whole days from `from` to `to`, negative when `to` is earlier. * * Computed through `Date.UTC` on the parsed parts rather than by parsing the * strings as instants: `new Date('2026-03-14')` is UTC midnight while * `new Date('2026-03-14T00:00')` is local midnight, and mixing the two puts the * count a day out for half the planet. Both ends are calendar dates already in * the owner's zone by the time they reach here, so UTC is simply the arithmetic * frame and carries no zone meaning at all. */ export declare function daysBetween(from: IsoDate, to: IsoDate): number; /** `iso` shifted by `days`, still a calendar date. */ export declare function addDays(iso: IsoDate, days: number): IsoDate; /** True when `iso` parses as a calendar date that exists. */ export declare function isIsoDate(value: string): boolean; /** The calendar day it is where the owner is, from `daemon.timezone`. */ export declare function todayInZone(nowMs: number, timezone: string): IsoDate; /** * Minutes past midnight where the owner is. * * `hourCycle: 'h23'` and `formatToParts` together, rather than slicing a * formatted string: an `en-US` time format is 12-hour by default, so a naive * slice reads 10pm as minute 600 and puts the quiet-hours boundary fourteen * hours out, in the direction that sends a message at 10pm believing it is * 10am. */ export declare function minutesOfDayInZone(nowMs: number, timezone: string): number; //# sourceMappingURL=dates.d.ts.map