fuzzyworld

when-am-i / files / src / utils / julianDay.ts

julianDay.ts

Blame: the commit that last changed each line · file · history

a71096e6 2026-06-18 Pavel Safronov1// 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
8import type { HistoricalDate } from '../types';
9
10/** Proleptic-Gregorian date → Julian Day Number (Fliegel & Van Flandern). */
11export 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.
28function 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. */
56export function julianDayToGregorian(jdn: number): HistoricalDate {
57 return fromJulianDay(jdn, 'gregorian');
58}
59
60/** Julian Day Number → Julian-calendar date. */
61export function julianDayToJulian(jdn: number): HistoricalDate {
62 return fromJulianDay(jdn, 'julian');
63}
64
65const ISO_DATE = /^(-?\d+)-(\d{2})-(\d{2})$/;
66
67/** Parse a proleptic-Gregorian "YYYY-MM-DD" (optionally a negative year). */
68export 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}