when-am-i / files / src / utils / julianDay.ts
julianDay.ts
a71096e6 2026-06-18 Pavel Safronov | 1 | // Julian Day Number (JDN) math. |
| 2 | // | |
| 3 | // The engine compares dates by converting them to a single integer ordering | |
| 4 | // key — the Julian Day Number — so proleptic-Gregorian, Julian, BCE and | |
| 5 | // pre-1582 dates all compare correctly (see plan.md, Q2). All dates here use | |
| 6 | // astronomical year numbering: year 0 = 1 BCE, year -1 = 2 BCE, etc. | |
| 7 | ||
| 8 | import type { HistoricalDate } from '../types'; | |
| 9 | ||
| 10 | /** Proleptic-Gregorian date → Julian Day Number (Fliegel & Van Flandern). */ | |
| 11 | export function gregorianToJulianDay({ year, month, day }: HistoricalDate): number { | |
| 12 | const a = Math.floor((14 - month) / 12); | |
| 13 | const y = year + 4800 - a; | |
| 14 | const m = month + 12 * a - 3; | |
| 15 | return ( | |
| 16 | day + | |
| 17 | Math.floor((153 * m + 2) / 5) + | |
| 18 | 365 * y + | |
| 19 | Math.floor(y / 4) - | |
| 20 | Math.floor(y / 100) + | |
| 21 | Math.floor(y / 400) - | |
| 22 | 32045 | |
| 23 | ); | |
| 24 | } | |
| 25 | ||
| 26 | // Richards' algorithm (Explanatory Supplement to the Astronomical Almanac). | |
| 27 | // The Gregorian and Julian variants differ only in how `f` is derived. | |
| 28 | function fromJulianDay(jdn: number, calendar: 'gregorian' | 'julian'): HistoricalDate { | |
| 29 | const Y = 4716; | |
| 30 | const J = 1401; | |
| 31 | const M0 = 2; | |
| 32 | const N = 12; | |
| 33 | const R = 4; | |
| 34 | const P = 1461; | |
| 35 | const V = 3; | |
| 36 | const U = 5; | |
| 37 | const S = 153; | |
| 38 | const W = 2; | |
| 39 | const B = 274277; | |
| 40 | const C = -38; | |
| 41 | ||
| 42 | let f = jdn + J; | |
| 43 | if (calendar === 'gregorian') { | |
| 44 | f += Math.floor((Math.floor((4 * jdn + B) / 146097) * 3) / 4) + C; | |
| 45 | } | |
| 46 | const e = R * f + V; | |
| 47 | const g = Math.floor((e % P) / R); | |
| 48 | const h = U * g + W; | |
| 49 | const day = Math.floor((h % S) / U) + 1; | |
| 50 | const month = ((Math.floor(h / S) + M0) % N) + 1; | |
| 51 | const year = Math.floor(e / P) - Y + Math.floor((N + M0 - month) / N); | |
| 52 | return { year, month, day }; | |
| 53 | } | |
| 54 | ||
| 55 | /** Julian Day Number → proleptic-Gregorian date. */ | |
| 56 | export function julianDayToGregorian(jdn: number): HistoricalDate { | |
| 57 | return fromJulianDay(jdn, 'gregorian'); | |
| 58 | } | |
| 59 | ||
| 60 | /** Julian Day Number → Julian-calendar date. */ | |
| 61 | export function julianDayToJulian(jdn: number): HistoricalDate { | |
| 62 | return fromJulianDay(jdn, 'julian'); | |
| 63 | } | |
| 64 | ||
| 65 | const ISO_DATE = /^(-?\d+)-(\d{2})-(\d{2})$/; | |
| 66 | ||
| 67 | /** Parse a proleptic-Gregorian "YYYY-MM-DD" (optionally a negative year). */ | |
| 68 | export function parseGregorianDate(value: string): HistoricalDate { | |
| 69 | const match = ISO_DATE.exec(value); | |
| 70 | if (!match) throw new Error(`Invalid date "${value}" (expected YYYY-MM-DD)`); | |
| 71 | return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) }; | |
| 72 | } |