/** * A calendar date, and deliberately not an instant. * * ### The bug this file exists to make impossible * * A due date is not a moment. "This is due on the 19th" is true in * Denver and true in Tokyo, and it does not become the 18th because * a laptop crossed a meridian. The classic date-picker defect is * that the value is a `Date` — which is an instant, a count of * milliseconds — and every read of it goes through the *browser's* * timezone. Pick the 19th at 23:30 in Denver, store * `new Date("2026-08-19").toISOString()`, and the server, the next * device, and the same device after a flight all disagree about * which day you meant. It is a one-day error, it only appears near * midnight or after travel, and it is invisible in every test * written at noon. * * So the value here is a **plain date**: a year, a month, and a day, * with no time, no offset, and no zone. It is stored as an ISO * `YYYY-MM-DD` string — which is exactly ``'s own * `value` format, so a consumer can move between this component and * the native control without a migration. * * ### There is no `Date` arithmetic in here at all * * Not "carefully-written `Date` arithmetic" — none. Every operation * below is integer maths on a **day number**: the count of days * since 1970-01-01, computed with Howard Hinnant's `days_from_civil` * / `civil_from_days` (public domain, the algorithm C++20's * `` and Rust's `chrono` both use). Adding a day is `+ 1`. * That is a proleptic Gregorian calendar with no DST, no leap * seconds, and no zone, which is what a calendar date actually is. * * A `Date` appears in exactly two places, both of them named and * both of them fenced: * * - `getLocalPlainDate`, which is the **one** conversion from an * instant to a calendar date, and which requires you to say * which zone's calendar you mean. * - `formatPlainDate` and its callers, which hand `Intl` a date * built at UTC midnight and format it with `timeZone: "UTC"`, so * the formatter cannot shift the day either. * * `plainDate.test.ts` re-runs the whole suite under four `TZ` * values, including UTC+14 and UTC-11, and asserts every answer is * byte-identical. * * ### Why not `Temporal` * * `Temporal.PlainDate` is this type, done properly, and it is * available in the Node this repo builds on. It is not available in * every browser a published package has to run in, and the polyfill * is ~50 KB gz — an order of magnitude more than this entire * component. The functions below are the ~80 lines of it a date * picker needs. When `Temporal` is baseline, this file becomes a * thin adapter and the public API — ISO strings — does not change, * which is the reason the API is ISO strings. */ /** Year, month (1-12), day (1-31). No time, no zone. */ export type PlainDate = { day: number; month: number; year: number; }; /** * A start and an end, either of which may be absent — because a * half-picked range is a real state a user spends time in, not an * error. `start` with a null `end` is "I have clicked once". */ export type DateRange = { end: null | string; start: null | string; }; export declare const getIsLeapYear: (year: number) => boolean; export declare const getDaysInMonth: (year: number, month: number) => number; export declare const getIsValidPlainDate: ({ day, month, year, }: PlainDate) => boolean; /** * Days since 1970-01-01, by Hinnant's `days_from_civil`. * * Every other operation in this file is expressed through this and * its inverse, which is why "add a month, then add 40 days, then * ask what weekday it is" cannot drift: there is one representation * and it is an integer. */ export declare const getDayNumber: ({ day, month, year, }: PlainDate) => number; /** The inverse — Hinnant's `civil_from_days`. */ export declare const getPlainDateFromDayNumber: (dayNumber: number) => PlainDate; export declare const addDays: (date: PlainDate, days: number) => PlainDate; /** * Month arithmetic clamps the day rather than overflowing, which is * the answer every calendar app gives and the one `Date` does not: * `new Date(2026, 0, 31)` plus a month is **3 March**, because 31 * February rolls forward. One month after 31 January is 28 * February. */ export declare const addMonths: (date: PlainDate, months: number) => PlainDate; export declare const comparePlainDates: (first: PlainDate, second: PlainDate) => number; export declare const getIsSameDay: (first: PlainDate, second: PlainDate) => boolean; /** * Whole days from `from` to `to`. Docket's staleness threshold is * counted in days, and this is the subtraction that counts it — * with no hour-of-day and no DST to make "14 days" occasionally mean * 13.958. */ export declare const getDaysBetween: (from: PlainDate, to: PlainDate) => number; export declare const clampPlainDate: (date: PlainDate, minimum?: PlainDate, maximum?: PlainDate) => PlainDate; export declare const getIsWithinRange: (date: PlainDate, minimum?: PlainDate, maximum?: PlainDate) => boolean; /** * 0 is Sunday, 6 is Saturday — the numbering `Intl`'s `weekday` * option and `Date.prototype.getDay` both use, so the two never * need reconciling at a call site. * * Day number 0 (1970-01-01) was a Thursday, which is the `+ 4`. */ export declare const getWeekday: (date: PlainDate) => number; export declare const toIsoDate: (date: PlainDate) => string; /** * `null` rather than a throw, and `null` rather than a "best * effort" date. A string that is not a calendar date has no nearest * calendar date, and inventing one is the silent-guess failure this * component is built to avoid. */ export declare const parseIsoDate: (isoDate: null | string | undefined) => null | PlainDate; export type PlainDateFormatOptions = Intl.DateTimeFormatOptions & { locale?: string; }; /** * `timeZone: "UTC"` is forced, and it is not configurable. * * The instant above is UTC midnight, so formatting it in any other * zone renders the previous or the next day — which is the same * one-day error as storing an instant, arriving through the display * layer instead. There is no legitimate reason to format a calendar * date in a zone: the date *is* the answer. */ export declare const formatPlainDate: (date: PlainDate, { locale, ...options }?: PlainDateFormatOptions) => string; /** * The one instant-to-calendar-date conversion in the library, and it * makes you name the zone. * * Defaulting `timeZone` to the device's own resolved zone is right * for "what is today on this screen"; passing one explicitly is * right for "what is today for this household". Either way the * question is asked out loud, which is the difference between this * and `new Date().toISOString().slice(0, 10)` — a line that appears * in three of the fleet's repos and is wrong for everybody west of * Greenwich after 17:00. */ export declare const getLocalPlainDate: (now: Date, timeZone?: string) => PlainDate; export declare const getFirstDayOfWeek: (locale?: string) => number; /** * The seven weekday names, rotated so index 0 is the locale's own * first day. A fixed sample week is formatted rather than a * hand-written table, so every locale the browser knows is covered * and nothing here needs translating. */ export declare const getWeekdayNames: ({ firstDayOfWeek, locale, weekday, }: { firstDayOfWeek: number; locale?: string; weekday: "long" | "narrow" | "short"; }) => string[]; /** January-first month names, for the parser and the header. */ export declare const getMonthNames: ({ locale, month, }: { locale?: string; month: "long" | "short"; }) => string[]; //# sourceMappingURL=plainDate.d.ts.map