From f4db817d135bac2a0bd39f51aae5b108dbf0c2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 2 Aug 2026 03:14:26 +0300 Subject: [PATCH] fix: ecclesiastical moon day with alternating 29/30-day months --- src/core/astronomy.ts | 44 +++++++++++++++++++++++++++++++++++++++++- src/core/paschalion.ts | 4 ++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/core/astronomy.ts b/src/core/astronomy.ts index bb9d868..0ce8a8c 100644 --- a/src/core/astronomy.ts +++ b/src/core/astronomy.ts @@ -8,6 +8,7 @@ import { JDate } from './jdate'; import { Paschalion } from './paschalion'; +import { FOUNDATION } from './paschalion'; /** Result of sunrise/sunset calculation for a given day. */ export interface SolarTimes { @@ -120,11 +121,52 @@ export function calcSolarTimes(jdate: JDate, lat: number, lon: number, tz: numbe }; } +/** + * Calculate ecclesiastical moon day using the foundation (age on March 1) + * and alternating 29/30-day months. + * Returns 1-based day of the current lunar cycle (1-30). + */ +function calcMoonDay(jdate: JDate): number { + const year = jdate.getYear(); + const cycle = Paschalion.getLunarCycle(year); + const foundation = FOUNDATION[cycle - 1]; + + // Days from March 1 to the target date + const march1 = new JDate(3, 1, year); + let diff = jdate.difference(march1); + if (diff < 0) { + // Date is before March 1 — use previous year's foundation + const prevCycle = cycle === 1 ? 19 : cycle - 1; + const prevFoundation = FOUNDATION[prevCycle - 1]; + const prevMarch1 = new JDate(3, 1, year - 1); + const diffFromPrevMarch = jdate.difference(prevMarch1); + const adjusted = 29.53 * 12 + diffFromPrevMarch; // ~354 days for 12 lunar months + let total = prevFoundation + adjusted; + // Subtract full lunar cycles (alternating 29/30, starting with 29) + const ml = (idx: number) => (idx % 2 === 0) ? 29 : 30; + let monthIdx = 0; + while (total >= ml(monthIdx)) { total -= ml(monthIdx); monthIdx++; } + return Math.floor(total) + 1; + } + + let total = foundation + diff; + + // Subtract alternating 29/30-day months (starting with 29) + const monthLen = (idx: number) => (idx % 2 === 0) ? 29 : 30; + let monthIdx = 0; + while (total >= monthLen(monthIdx)) { + total -= monthLen(monthIdx); + monthIdx++; + } + + return Math.floor(total) + 1; +} + /** Moon phase info for a given Julian date. */ export function getMoonInfo(jdate: JDate): LunarInfo { const phase = Paschalion.getLunarPhase(jdate); const trend = phase < 0.5 ? 'waxing' : 'waning'; - const moonDay = Math.floor(phase * 29.53) + 1; + const moonDay = calcMoonDay(jdate); // Map phase (0-1) to emoji (0=new, 0.5=full) let emoji: string; diff --git a/src/core/paschalion.ts b/src/core/paschalion.ts index 656ebf0..54eacd5 100644 --- a/src/core/paschalion.ts +++ b/src/core/paschalion.ts @@ -11,8 +11,8 @@ import { JDate } from './jdate'; const LUNAR_MONTH = 29.52916667; -/** Age of the moon on March 1 for each year in the 19-year Metonic cycle (hours since midnight). */ -const FOUNDATION = [ +/** Age of the moon on March 1 for each year in the 19-year Metonic cycle (days). */ +export const FOUNDATION = [ 14.042016807, 25.462184874, 6.084033613, 17.966386555, 28.336134454, 9.210084034, 20.504201681, 1.420168067, 12.294117647, 23.168067227, 4.546218487, 15.042016807, 26.294117647, 7.630252101, 18.546218487,