Algorithms
Calendar days
Dates as counts of days and back, by arithmetic alone: no Date, no time zone, nothing allocated.
Behaviour
Day 0 is 1 January 1970, and the first and last days an ISO date can write are where they are said to be
expect(dayFromCivil(1970, 1, 1)).toBe(0);
expect(dayFromCivil(1969, 12, 31)).toBe(-1);
expect(dayFromCivil(1, 1, 1)).toBe(FIRST_DAY);
expect(dayFromCivil(9999, 12, 31)).toBe(LAST_DAY);
expect(civil(FIRST_DAY)).toEqual([1, 1, 1]);
expect(civil(LAST_DAY)).toEqual([9999, 12, 31]);A day and its date agree with the browser's own calendar, every year an ISO date can write
fc.assert(
fc.property(fc.integer({ min: FIRST_DAY, max: LAST_DAY }), (days) => {
const date = new Date(0);
date.setUTCDate(date.getUTCDate() + days);
expect(civil(days)).toEqual([date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()]);
expect(dayFromCivil(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate())).toBe(days);
}),
{ numRuns: 2000 },
);A leap day is every fourth year's, but not a hundredth's, unless it is a four-hundredth's
expect([1900, 2000, 2024, 2026].map(isLeapYear)).toEqual([false, true, true, false]);
expect([daysInMonth(1900, 2), daysInMonth(2000, 2), daysInMonth(2026, 4), daysInMonth(2026, 12)]).toEqual(
[28, 29, 30, 31],
);
expect(dayFromCivil(2000, 3, 1) - dayFromCivil(2000, 2, 28)).toBe(2);React
Install
npx shadcn@latest add https://ui.publictheta.com/r/civil-days.jsonSource
src/algorithms/civil-days.ts
/**
* Dates of the Gregorian calendar as counts of days, and back, by arithmetic
* alone — no `Date`, no time zone, nothing allocated: the civil-day algorithms
* of H. Hinnant. Day 0 is 1 January 1970; the calendar runs back before its
* adoption as if it always had (proleptic), which is what an ISO date means.
*
* Years begin in March in the arithmetic, so that a leap day is the last day of
* one and needs no special case; a 400-year era has exactly 146,097 days.
*/
/** The first and last days an ISO date can write: 0001-01-01 and 9999-12-31. */
export const FIRST_DAY = -719_162;
export const LAST_DAY = 2_932_896;
/** The day of a year, month (1–12) and day of the month. */
export function dayFromCivil(year: number, month: number, day: number): number {
const y = month <= 2 ? year - 1 : year;
const era = Math.floor(y / 400);
const yearOfEra = y - era * 400;
const dayOfYear = Math.floor((153 * (month > 2 ? month - 3 : month + 9) + 2) / 5) + day - 1;
const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear;
return era * 146_097 + dayOfEra - 719_468;
}
/** The year, month and day of a day, written into `out` as `[year, month, day]`. */
export function civilFromDay(days: number, out: { 0: number; 1: number; 2: number }): void {
const z = days + 719_468;
const era = Math.floor(z / 146_097);
const dayOfEra = z - era * 146_097;
const yearOfEra = Math.floor(
(dayOfEra -
Math.floor(dayOfEra / 1460) +
Math.floor(dayOfEra / 36_524) -
Math.floor(dayOfEra / 146_096)) /
365,
);
const dayOfYear = dayOfEra - (365 * yearOfEra + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100));
const shifted = Math.floor((5 * dayOfYear + 2) / 153);
const month = shifted < 10 ? shifted + 3 : shifted - 9;
out[0] = yearOfEra + era * 400 + (month <= 2 ? 1 : 0);
out[1] = month;
out[2] = dayOfYear - Math.floor((153 * shifted + 2) / 5) + 1;
}
export function isLeapYear(year: number): boolean {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
export function daysInMonth(year: number, month: number): number {
if (month === 2) return isLeapYear(year) ? 29 : 28;
return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
}