calendar: real occurrence/precedence resolution (feast ranks, sanctoral content)
Replaces the FeastRank placeholder with a real ordered FeastClass (the old pre-1955 six-level scale) and a new TemporalCategory (privileged/ordinary Sunday/feria). calendar/commemorations.ts's decideOccurrence encodes the actual precedence rules as explicit, commented branches per category rather than the reference engine's own opaque numeric weights — this app targets one ruleset, so the readability trade-off is worth it. Category membership (data/calendar/temporal-categories.yml) is a best-effort reconstruction of the pre-1955 tradition, not sourced from a primary text; expect corrections. calendar/feasts.ts resolves real sanctoral candidates, and resolveDay() wires it all together — occurring/temporalCategory are no longer stubs. isDoubleOrHigher (hours/antiphon.ts) is now a real comparison instead of a hardcoded false. Seeded 7 real saints/feasts, each verified directly against Divinum Officium's own rank data (not reconstructed from memory) as a small, growable start: St. Lawrence, the Assumption, St. Augustine, the Nativity of the BVM, St. Michael, All Saints, the Immaculate Conception.
This commit is contained in:
@@ -11,17 +11,24 @@
|
||||
// a season is the temporal-cycle backdrop a day sits on; which saint (if
|
||||
// any) is being kept that day, and whether it outranks the season, is a
|
||||
// separate, larger project.
|
||||
import type { Season } from './types';
|
||||
import type { Season, TemporalCategory, Weekday } from './types';
|
||||
import { easterSunday } from './easter';
|
||||
import { addDays, daysBetween, toIsoDate } from './date-math';
|
||||
import fixedDateData from '../data/calendar/fixed-date-calendar.yml';
|
||||
import easterOffsetsData from '../data/calendar/easter-offsets.yml';
|
||||
import temporalCategoriesData from '../data/calendar/temporal-categories.yml';
|
||||
|
||||
const fixedDates = fixedDateData as { dates: Record<string, string> };
|
||||
const easterOffsets = easterOffsetsData as {
|
||||
ranges: { season: string; fromOffset: number }[];
|
||||
days?: Record<string, string>;
|
||||
};
|
||||
const temporalCategories = temporalCategoriesData as {
|
||||
bySeason: Record<string, { sunday: TemporalCategory; feria: TemporalCategory }>;
|
||||
offsets?: Record<string, TemporalCategory>;
|
||||
offsetRanges?: { fromOffset: number; toOffset: number; category: TemporalCategory }[];
|
||||
fixedDates?: Record<string, TemporalCategory>;
|
||||
};
|
||||
|
||||
function fixedDateFor(id: string): { month: number; day: number } {
|
||||
const entry = Object.entries(fixedDates.dates).find(([, value]) => value === id);
|
||||
@@ -36,7 +43,7 @@ const CHRISTMAS = fixedDateFor('christmas-day');
|
||||
const EPIPHANY = fixedDateFor('epiphany');
|
||||
|
||||
/** The Sunday nearest Nov 30 (St. Andrew's Day) — Advent's real start rule. */
|
||||
function adventStart(year: number): string {
|
||||
export function adventStart(year: number): string {
|
||||
const nov30 = `${year}-11-30`;
|
||||
const dow = new Date(`${nov30}T00:00:00Z`).getUTCDay(); // 0 = Sunday
|
||||
const delta = dow <= 3 ? -dow : 7 - dow;
|
||||
@@ -94,3 +101,50 @@ export function resolveSeason(isoDate: string): Season {
|
||||
|
||||
return seasonFromEasterOffset(daysBetween(easterIso, isoDate));
|
||||
}
|
||||
|
||||
/** Days from Easter Sunday (negative = before, 0 = Easter, positive = after) for the given date's own year. */
|
||||
export function easterOffsetOf(isoDate: string): number {
|
||||
const year = Number(isoDate.slice(0, 4));
|
||||
return daysBetween(toIsoDate(easterSunday(year)), isoDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* A day's precedence category under the temporal cycle alone — see
|
||||
* calendar/types.ts's TemporalCategory doc comment and
|
||||
* data/calendar/temporal-categories.yml for the reconstruction caveat.
|
||||
*
|
||||
* Sundays never fall on any of the offset/fixed-date overrides below (Ash
|
||||
* Wednesday, Holy Week, the Easter/Pentecost octaves' weekdays, and the
|
||||
* Christmas vigil are all, by construction, not Sundays) except possibly
|
||||
* the Christmas vigil (Dec 24 can land on a Sunday) — and a Sunday's own
|
||||
* privileged/ordinary status should win in that case regardless, so
|
||||
* Sundays are resolved straight from `bySeason` without consulting the
|
||||
* overrides at all.
|
||||
*/
|
||||
export function resolveTemporalCategory(isoDate: string, season: Season, weekday: Weekday): TemporalCategory {
|
||||
const bySeasonEntry = temporalCategories.bySeason[season];
|
||||
const base = bySeasonEntry ? (weekday === 'sunday' ? bySeasonEntry.sunday : bySeasonEntry.feria) : 'ordinary-feria';
|
||||
if (weekday === 'sunday') {
|
||||
return base;
|
||||
}
|
||||
|
||||
const fixedOverride = temporalCategories.fixedDates?.[isoDate.slice(5)];
|
||||
if (fixedOverride) {
|
||||
return fixedOverride;
|
||||
}
|
||||
|
||||
const offset = easterOffsetOf(isoDate);
|
||||
|
||||
const singleOverride = temporalCategories.offsets?.[String(offset)];
|
||||
if (singleOverride) {
|
||||
return singleOverride;
|
||||
}
|
||||
|
||||
for (const range of temporalCategories.offsetRanges ?? []) {
|
||||
if (offset >= range.fromOffset && offset <= range.toOffset) {
|
||||
return range.category;
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user