tonus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/BIBLIOGRAPHY.md +99 -0
  2. package/LICENSE +29 -0
  3. package/README.md +108 -0
  4. package/dist/data/cal.d.ts +10 -0
  5. package/dist/data/cal.js +3862 -0
  6. package/dist/data/commune.d.ts +17 -0
  7. package/dist/data/commune.js +1333 -0
  8. package/dist/data/gr.d.ts +5 -0
  9. package/dist/data/gr.js +13449 -0
  10. package/dist/data/kyriale.d.ts +11 -0
  11. package/dist/data/kyriale.js +971 -0
  12. package/dist/data/la.d.ts +5 -0
  13. package/dist/data/la.js +14229 -0
  14. package/dist/data/lh.d.ts +5 -0
  15. package/dist/data/lh.js +3619 -0
  16. package/dist/data/lu.d.ts +5 -0
  17. package/dist/data/lu.js +23779 -0
  18. package/dist/data/masses.d.ts +18 -0
  19. package/dist/data/masses.js +297 -0
  20. package/dist/data/office-roman.d.ts +19 -0
  21. package/dist/data/office-roman.js +13792 -0
  22. package/dist/data/office.d.ts +12 -0
  23. package/dist/data/office.js +13052 -0
  24. package/dist/data/propers.d.ts +13 -0
  25. package/dist/data/propers.js +7584 -0
  26. package/dist/data/psalms.d.ts +4 -0
  27. package/dist/data/psalms.js +10 -0
  28. package/dist/data/psalms.json +22918 -0
  29. package/dist/data/tones.d.ts +20 -0
  30. package/dist/data/tones.js +153 -0
  31. package/dist/data/types.d.ts +3 -0
  32. package/dist/data/types.js +2 -0
  33. package/dist/engines/cal/calendar.d.ts +21 -0
  34. package/dist/engines/cal/calendar.js +265 -0
  35. package/dist/engines/cal/date.d.ts +31 -0
  36. package/dist/engines/cal/date.js +141 -0
  37. package/dist/engines/cal/types.d.ts +66 -0
  38. package/dist/engines/cal/types.js +189 -0
  39. package/dist/engines/chant/chant.d.ts +10 -0
  40. package/dist/engines/chant/chant.js +135 -0
  41. package/dist/engines/chant/hour.d.ts +8 -0
  42. package/dist/engines/chant/hour.js +135 -0
  43. package/dist/engines/chant/intone.d.ts +8 -0
  44. package/dist/engines/chant/intone.js +84 -0
  45. package/dist/engines/chant/ordinary.d.ts +7 -0
  46. package/dist/engines/chant/ordinary.js +232 -0
  47. package/dist/engines/chant/propers.d.ts +8 -0
  48. package/dist/engines/chant/propers.js +107 -0
  49. package/dist/engines/chant/psalm.d.ts +7 -0
  50. package/dist/engines/chant/psalm.js +60 -0
  51. package/dist/engines/chant/syllabify.d.ts +20 -0
  52. package/dist/engines/chant/syllabify.js +192 -0
  53. package/dist/engines/chant/types.d.ts +76 -0
  54. package/dist/engines/chant/types.js +34 -0
  55. package/dist/engines/epoch.d.ts +2 -0
  56. package/dist/engines/epoch.js +14 -0
  57. package/dist/engines/harmonia/api.d.ts +35 -0
  58. package/dist/engines/harmonia/api.js +90 -0
  59. package/dist/engines/harmonia/aspects.d.ts +8 -0
  60. package/dist/engines/harmonia/aspects.js +15 -0
  61. package/dist/engines/harmonia/data/doctrines.d.ts +16 -0
  62. package/dist/engines/harmonia/data/doctrines.js +154 -0
  63. package/dist/engines/harmonia/data/vowels.d.ts +10 -0
  64. package/dist/engines/harmonia/data/vowels.js +21 -0
  65. package/dist/engines/harmonia/presence.d.ts +13 -0
  66. package/dist/engines/harmonia/presence.js +48 -0
  67. package/dist/engines/harmonia/tabula.d.ts +28 -0
  68. package/dist/engines/harmonia/tabula.js +32 -0
  69. package/dist/engines/harmonia/voice.d.ts +19 -0
  70. package/dist/engines/harmonia/voice.js +51 -0
  71. package/dist/engines/imprint.d.ts +30 -0
  72. package/dist/engines/imprint.js +152 -0
  73. package/dist/engines/planet/appearance.d.ts +40 -0
  74. package/dist/engines/planet/appearance.js +84 -0
  75. package/dist/engines/planet/aspects.d.ts +5 -0
  76. package/dist/engines/planet/aspects.js +41 -0
  77. package/dist/engines/planet/math.d.ts +13 -0
  78. package/dist/engines/planet/math.js +56 -0
  79. package/dist/engines/planet/orbital.d.ts +25 -0
  80. package/dist/engines/planet/orbital.js +223 -0
  81. package/dist/engines/planet/planet.d.ts +13 -0
  82. package/dist/engines/planet/planet.js +198 -0
  83. package/dist/engines/planet/position.d.ts +62 -0
  84. package/dist/engines/planet/position.js +156 -0
  85. package/dist/engines/planet/types.d.ts +61 -0
  86. package/dist/engines/planet/types.js +14 -0
  87. package/dist/engines/score/api.d.ts +54 -0
  88. package/dist/engines/score/api.js +87 -0
  89. package/dist/engines/score/articulation.d.ts +6 -0
  90. package/dist/engines/score/articulation.js +112 -0
  91. package/dist/engines/score/emitters/midi.d.ts +65 -0
  92. package/dist/engines/score/emitters/midi.js +158 -0
  93. package/dist/engines/score/emitters/musicxml.d.ts +18 -0
  94. package/dist/engines/score/emitters/musicxml.js +166 -0
  95. package/dist/engines/score/infer.d.ts +4 -0
  96. package/dist/engines/score/infer.js +77 -0
  97. package/dist/engines/score/ir.d.ts +4 -0
  98. package/dist/engines/score/ir.js +177 -0
  99. package/dist/engines/score/meta.d.ts +19 -0
  100. package/dist/engines/score/meta.js +34 -0
  101. package/dist/engines/score/neume.d.ts +3 -0
  102. package/dist/engines/score/neume.js +26 -0
  103. package/dist/engines/score/parse.d.ts +3 -0
  104. package/dist/engines/score/parse.js +359 -0
  105. package/dist/engines/score/phrasing.d.ts +24 -0
  106. package/dist/engines/score/phrasing.js +257 -0
  107. package/dist/engines/score/prosody.d.ts +35 -0
  108. package/dist/engines/score/prosody.js +109 -0
  109. package/dist/engines/score/tabula.d.ts +70 -0
  110. package/dist/engines/score/tabula.js +109 -0
  111. package/dist/engines/score/types.d.ts +159 -0
  112. package/dist/engines/score/types.js +2 -0
  113. package/dist/engines/temper/api.d.ts +60 -0
  114. package/dist/engines/temper/api.js +130 -0
  115. package/dist/engines/temper/data/constants.d.ts +27 -0
  116. package/dist/engines/temper/data/constants.js +150 -0
  117. package/dist/engines/temper/data/guido.d.ts +14 -0
  118. package/dist/engines/temper/data/guido.js +29 -0
  119. package/dist/engines/temper/data/modes.d.ts +38 -0
  120. package/dist/engines/temper/data/modes.js +158 -0
  121. package/dist/engines/temper/gabc.d.ts +5 -0
  122. package/dist/engines/temper/gabc.js +53 -0
  123. package/dist/engines/temper/gamut.d.ts +9 -0
  124. package/dist/engines/temper/gamut.js +24 -0
  125. package/dist/engines/temper/guido.d.ts +16 -0
  126. package/dist/engines/temper/guido.js +48 -0
  127. package/dist/engines/temper/interval.d.ts +15 -0
  128. package/dist/engines/temper/interval.js +31 -0
  129. package/dist/engines/temper/modes.d.ts +6 -0
  130. package/dist/engines/temper/modes.js +13 -0
  131. package/dist/engines/temper/neume.d.ts +14 -0
  132. package/dist/engines/temper/neume.js +59 -0
  133. package/dist/engines/temper/pitch.d.ts +40 -0
  134. package/dist/engines/temper/pitch.js +129 -0
  135. package/dist/engines/temper/scale.d.ts +37 -0
  136. package/dist/engines/temper/scale.js +217 -0
  137. package/dist/engines/temper/step.d.ts +23 -0
  138. package/dist/engines/temper/step.js +53 -0
  139. package/dist/index.d.ts +40 -0
  140. package/dist/index.js +27 -0
  141. package/package.json +60 -0
@@ -0,0 +1,20 @@
1
+ export interface Differentia {
2
+ code: string;
3
+ termination: number[];
4
+ }
5
+ export interface PsalmTone {
6
+ mode: number;
7
+ name: string;
8
+ tenor: number;
9
+ intonation: number[];
10
+ flex: number[];
11
+ mediant: number[];
12
+ defaultDiff: string;
13
+ differentiae: Differentia[];
14
+ }
15
+ export declare const TONES: PsalmTone[];
16
+ /** Look up a PsalmTone by mode number (1–8, or 0 for Peregrinus) */
17
+ export declare function getTone(mode: number): PsalmTone;
18
+ /** Look up a differentia by code, falling back to the tone's defaultDiff */
19
+ export declare function getDifferentia(tone: PsalmTone, code?: string): Differentia;
20
+ //# sourceMappingURL=tones.d.ts.map
@@ -0,0 +1,153 @@
1
+ // humana/data/tones — psalm tones (Graduale Romanum appendix)
2
+ // Pitches as MIDI numbers; tone codes follow DO convention: "1g", "6F", "4e".
3
+ // c4 clef reference: f=53 g=55 h=57 i=59 j=60 k=62 l=64 (F3–F4)
4
+ const f = 53, g = 55, h = 57, i = 59, j = 60, k = 62, l = 64;
5
+ const d3 = 50, e3 = 52; // below c4 clef range, needed for Tone II
6
+ export const TONES = [
7
+ {
8
+ mode: 1,
9
+ name: "Tonus I",
10
+ tenor: h, // A3
11
+ intonation: [f, h, j], // F-A-C
12
+ flex: [h, g], // A-G
13
+ mediant: [j, h, g, h], // C-A-G-A
14
+ defaultDiff: "1g",
15
+ differentiae: [
16
+ { code: "1g", termination: [j, h, g, h, g] }, // standard
17
+ { code: "1g2", termination: [j, h, j, h, g] }, // alt
18
+ { code: "1f", termination: [j, i, h, f] },
19
+ { code: "1D", termination: [h, g, f, f] }, // ending D
20
+ { code: "1a", termination: [j, k, j, h] }, // high ending
21
+ ],
22
+ },
23
+ {
24
+ mode: 2,
25
+ name: "Tonus II",
26
+ tenor: f, // F3
27
+ intonation: [d3, f], // D3-F3
28
+ flex: [f, e3], // F-E
29
+ mediant: [g, f, e3, f], // G-F-E-F
30
+ defaultDiff: "2D",
31
+ differentiae: [
32
+ { code: "2D", termination: [g, f, e3, d3] }, // ending D
33
+ { code: "2d", termination: [g, f, e3, d3] }, // same
34
+ { code: "2e", termination: [g, f, g, e3] },
35
+ { code: "2f", termination: [g, f, f] },
36
+ ],
37
+ },
38
+ {
39
+ mode: 3,
40
+ name: "Tonus III",
41
+ tenor: i, // B3
42
+ intonation: [g, i, j], // G-B-C
43
+ flex: [i, h], // B-A
44
+ mediant: [k, i, j, i], // D-B-C-B
45
+ defaultDiff: "3b",
46
+ differentiae: [
47
+ { code: "3b", termination: [j, i, h, i] },
48
+ { code: "3a", termination: [k, j, i, h, i] },
49
+ { code: "3g", termination: [j, i, h, g] },
50
+ ],
51
+ },
52
+ {
53
+ mode: 4,
54
+ name: "Tonus IV",
55
+ tenor: h, // A3
56
+ intonation: [f, g, h], // F-G-A
57
+ flex: [h, f], // A-F
58
+ mediant: [j, i, h], // C-B-A
59
+ defaultDiff: "4e",
60
+ differentiae: [
61
+ { code: "4e", termination: [h, g, h, f, e3] }, // ending E (standard)
62
+ { code: "4g", termination: [h, g, f, g] },
63
+ { code: "4A", termination: [h, i, h, f] },
64
+ { code: "4a", termination: [i, h, g, h] },
65
+ ],
66
+ },
67
+ {
68
+ mode: 5,
69
+ name: "Tonus V",
70
+ tenor: j, // C4
71
+ intonation: [h, j], // A-C
72
+ flex: [j, i], // C-B
73
+ mediant: [l, k, j], // E-D-C
74
+ defaultDiff: "5a",
75
+ differentiae: [
76
+ { code: "5a", termination: [l, k, j, j] }, // ending F
77
+ { code: "5f", termination: [k, j, i, j] },
78
+ { code: "5F", termination: [k, j, h, j] },
79
+ ],
80
+ },
81
+ {
82
+ mode: 6,
83
+ name: "Tonus VI",
84
+ tenor: f, // F3
85
+ intonation: [d3, e3, f], // D-E-F
86
+ flex: [f, e3], // F-E
87
+ mediant: [g, f, e3, f], // G-F-E-F
88
+ defaultDiff: "6F",
89
+ differentiae: [
90
+ { code: "6F", termination: [g, h, g, f] }, // standard ending F
91
+ { code: "6f", termination: [g, f, e3, f] },
92
+ { code: "6g", termination: [g, f, g] },
93
+ ],
94
+ },
95
+ {
96
+ mode: 7,
97
+ name: "Tonus VII",
98
+ tenor: j, // C4
99
+ intonation: [h, i, j], // A-B-C
100
+ flex: [j, i], // C-B
101
+ mediant: [k, j, i, j], // D-C-B-C
102
+ defaultDiff: "7a",
103
+ differentiae: [
104
+ { code: "7a", termination: [k, j, i, j] }, // ending G-area
105
+ { code: "7c", termination: [l, k, j, k, j] },
106
+ { code: "7b", termination: [l, k, i, j] },
107
+ { code: "7d", termination: [k, j, h, j] },
108
+ ],
109
+ },
110
+ {
111
+ mode: 8,
112
+ name: "Tonus VIII",
113
+ tenor: j, // C4
114
+ intonation: [h, j], // A-C
115
+ flex: [j, h], // C-A
116
+ mediant: [k, j], // D-C
117
+ defaultDiff: "8G",
118
+ differentiae: [
119
+ { code: "8G", termination: [k, j, h, j] }, // standard ending G
120
+ { code: "8g", termination: [k, j, i, j] },
121
+ { code: "8c", termination: [l, k, j, i, j] },
122
+ { code: "8G2", termination: [k, j, h, g, j] },
123
+ ],
124
+ },
125
+ // ── Tonus Peregrinus ── irregular: two different tenors
126
+ {
127
+ mode: 0,
128
+ name: "Tonus Peregrinus",
129
+ tenor: j, // C4 (first half tenor)
130
+ intonation: [h, i, j], // A-B-C
131
+ flex: [j, i],
132
+ mediant: [k, i, h, j], // D-B-A-C (descends to A then rises)
133
+ defaultDiff: "per",
134
+ differentiae: [
135
+ { code: "per", termination: [i, h, g, h] }, // second half on A
136
+ ],
137
+ },
138
+ ];
139
+ /** Look up a PsalmTone by mode number (1–8, or 0 for Peregrinus) */
140
+ export function getTone(mode) {
141
+ return TONES.find(t => t.mode === mode) ?? TONES.find(t => t.mode === 8);
142
+ }
143
+ /** Look up a differentia by code, falling back to the tone's defaultDiff */
144
+ export function getDifferentia(tone, code) {
145
+ if (code) {
146
+ const d = tone.differentiae.find(d => d.code === code);
147
+ if (d)
148
+ return d;
149
+ }
150
+ return tone.differentiae.find(d => d.code === tone.defaultDiff)
151
+ ?? tone.differentiae[0];
152
+ }
153
+ //# sourceMappingURL=tones.js.map
@@ -0,0 +1,3 @@
1
+ export type { Chant } from "../engines/chant/types.js";
2
+ export type { PsalmVerse } from "../engines/chant/types.js";
3
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,21 @@
1
+ import { type CalEntry } from "../../data/cal.js";
2
+ import { type RuleAnchors } from "./date.js";
3
+ import { type Feast, type FeastQuery, type Pascha } from "./types.js";
4
+ export declare function getAnchors(year: number): RuleAnchors;
5
+ /**
6
+ * The movable anchors of a liturgical year (`tonus.pascha`). Easter is
7
+ * computed by the Gregorian (Gauss/Butcher) computus from 1583 and by the
8
+ * Julian computus with day-number conversion before that; everything else
9
+ * anchors to it, except Advent, which anchors to November 27.
10
+ */
11
+ export declare function getPascha(year: number): Pascha;
12
+ export declare function buildCalendar(year: number): Map<string, CalEntry[]>;
13
+ /**
14
+ * Calendar lookup (`tonus.festum`). Returns matching feasts sorted
15
+ * `day asc, rank desc` — for a date, the primary feast plus concurrent
16
+ * feasts; for a `from`/`to` range, every day flattened; with no query,
17
+ * the current liturgical year. Dates are UTC-canonical: build them from
18
+ * ISO strings or `Date.UTC`.
19
+ */
20
+ export declare function getFeast(query?: FeastQuery): Feast[];
21
+ //# sourceMappingURL=calendar.d.ts.map
@@ -0,0 +1,265 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/cal/calendar — liturgical feast lookup
3
+ // ---------------------------------------------------------------------------
4
+ import { CAL } from "../../data/cal.js";
5
+ import { MASSES } from "../../data/masses.js";
6
+ import { isoDate, startOfDay, addDays, subDays, firstSundayOnOrAfter, nextSunday, pascha, resolveEntryId, DEFAULT_EPOCH, } from "./date.js";
7
+ import { TEMPUS_NAMES, entryGrade, gradeOrder, BVM_FEAST_IDS, APOSTOLIC_FEAST_IDS, } from "./types.js";
8
+ const _calCache = new Map();
9
+ const _anchorCache = new Map();
10
+ // Preferred mass order; ad libitum (mass 0) is handled separately in ordinary.ts.
11
+ const DEFAULT_MASSES = [
12
+ 8, 9, 11, 1, 2, 3, 4, 5, 6, 7, 10, 12, 13, 14, 15, 16, 17, 18,
13
+ ];
14
+ export function getAnchors(year) {
15
+ if (_anchorCache.has(year))
16
+ return _anchorCache.get(year);
17
+ const easter = startOfDay(pascha(year));
18
+ const advent1 = firstSundayOnOrAfter(new Date(Date.UTC(year, 10, 27)));
19
+ const anchors = {
20
+ year,
21
+ easter,
22
+ ashWednesday: subDays(easter, 46),
23
+ firstLentSunday: addDays(subDays(easter, 46), 4),
24
+ septuagesima: subDays(easter, 63),
25
+ pentecost: addDays(easter, 49),
26
+ ascension: addDays(easter, 39),
27
+ adventFirstSunday: advent1,
28
+ gaudete: addDays(advent1, 14),
29
+ christmas: new Date(Date.UTC(year, 11, 25)),
30
+ epiphany: new Date(Date.UTC(year, 0, 6)),
31
+ baptism: nextSunday(new Date(Date.UTC(year, 0, 7))),
32
+ };
33
+ _anchorCache.set(year, anchors);
34
+ return anchors;
35
+ }
36
+ /**
37
+ * The movable anchors of a liturgical year (`tonus.pascha`). Easter is
38
+ * computed by the Gregorian (Gauss/Butcher) computus from 1583 and by the
39
+ * Julian computus with day-number conversion before that; everything else
40
+ * anchors to it, except Advent, which anchors to November 27.
41
+ */
42
+ export function getPascha(year) {
43
+ if (!Number.isFinite(year)) {
44
+ throw new RangeError(`pascha requires a finite year, got ${year}`);
45
+ }
46
+ const a = getAnchors(Math.trunc(year));
47
+ const d = (x) => new Date(x.getTime());
48
+ return {
49
+ year: Math.trunc(year),
50
+ septuagesima: d(a.septuagesima),
51
+ ashWednesday: d(a.ashWednesday),
52
+ firstLentSunday: d(a.firstLentSunday),
53
+ palmSunday: subDays(a.easter, 7),
54
+ goodFriday: subDays(a.easter, 2),
55
+ easter: d(a.easter),
56
+ ascension: d(a.ascension),
57
+ pentecost: d(a.pentecost),
58
+ trinitySunday: addDays(a.pentecost, 7),
59
+ corpusChristi: addDays(a.pentecost, 11),
60
+ adventFirstSunday: d(a.adventFirstSunday),
61
+ gaudete: d(a.gaudete),
62
+ christmas: d(a.christmas),
63
+ epiphany: d(a.epiphany),
64
+ baptism: d(a.baptism),
65
+ };
66
+ }
67
+ export function buildCalendar(year) {
68
+ if (_calCache.has(year))
69
+ return _calCache.get(year);
70
+ const anchors = getAnchors(year);
71
+ const map = new Map();
72
+ for (const entry of CAL) {
73
+ for (const { key } of resolveEntryId(entry.id, year, anchors)) {
74
+ if (!map.has(key))
75
+ map.set(key, []);
76
+ map.get(key).push(entry);
77
+ }
78
+ }
79
+ // Lower rank number means higher priority.
80
+ for (const list of map.values())
81
+ list.sort((a, b) => entryGradeOrder(a) - entryGradeOrder(b));
82
+ _calCache.set(year, map);
83
+ return map;
84
+ }
85
+ // Season boundaries follow the Divinum Officium Tempora stems exactly, so a
86
+ // date's season always matches the stem of any Tempora feast that falls on it
87
+ // (asserted by the stem↔season test):
88
+ // adv Advent I Sunday → Christmas (Dec 25)
89
+ // nat Christmas → epiphanySunday (1st Sun after Epiphany)
90
+ // epi epiphanySunday → Septuagesima (Easter − 63)
91
+ // quadp Septuagesima Sunday → Ash Wednesday (Easter − 46)
92
+ // quad Ash Wednesday → Easter
93
+ // pasc Easter → trinitySunday (Pentecost + 7); the
94
+ // Pentecost octave stays paschal
95
+ // pent trinitySunday → next Advent I Sunday
96
+ // Epi and Nat both anchor on firstSundayOnOrAfter(feast+1) in
97
+ // resolveTemporaStem; epiphanySunday is that boundary between them.
98
+ function epiphanySunday(a) {
99
+ return firstSundayOnOrAfter(addDays(a.epiphany, 1));
100
+ }
101
+ function trinitySunday(a) {
102
+ return addDays(a.pentecost, 7);
103
+ }
104
+ function findSeason(date) {
105
+ const year = date.getUTCFullYear();
106
+ const a = getAnchors(year);
107
+ const prev = getAnchors(year - 1);
108
+ const next = getAnchors(year + 1);
109
+ const s = (code, start, end) => ({
110
+ code,
111
+ start: startOfDay(start),
112
+ end: startOfDay(end),
113
+ });
114
+ if (date >= next.adventFirstSunday && date < next.christmas)
115
+ return s("adv", next.adventFirstSunday, next.christmas);
116
+ if (date >= a.adventFirstSunday && date < a.christmas)
117
+ return s("adv", a.adventFirstSunday, a.christmas);
118
+ if (date >= prev.christmas && date < epiphanySunday(a))
119
+ return s("nat", prev.christmas, epiphanySunday(a));
120
+ if (date >= a.christmas && date < epiphanySunday(next))
121
+ return s("nat", a.christmas, epiphanySunday(next));
122
+ if (date >= epiphanySunday(a) && date < a.septuagesima)
123
+ return s("epi", epiphanySunday(a), a.septuagesima);
124
+ if (date >= a.septuagesima && date < a.ashWednesday)
125
+ return s("quadp", a.septuagesima, a.ashWednesday);
126
+ if (date >= a.ashWednesday && date < a.easter)
127
+ return s("quad", a.ashWednesday, a.easter);
128
+ if (date >= a.easter && date < trinitySunday(a))
129
+ return s("pasc", a.easter, trinitySunday(a));
130
+ if (date >= trinitySunday(a) && date < next.adventFirstSunday)
131
+ return s("pent", trinitySunday(a), next.adventFirstSunday);
132
+ return s("epi", epiphanySunday(a), a.septuagesima);
133
+ }
134
+ function selectMasses(id, grade, season, date) {
135
+ const dowCode = date.getUTCDay() === 0 ? "dominica" : "feria";
136
+ const requireBvm = BVM_FEAST_IDS.has(id);
137
+ const matches = [];
138
+ for (const num of DEFAULT_MASSES) {
139
+ const mass = MASSES.get(num);
140
+ if (!mass)
141
+ continue;
142
+ if (requireBvm !== mass.bvm)
143
+ continue;
144
+ if (!mass.seasons.includes(season))
145
+ continue;
146
+ if (!mass.grades.includes(grade))
147
+ continue;
148
+ if (!mass.days.includes(dowCode))
149
+ continue;
150
+ matches.push(num);
151
+ }
152
+ return matches;
153
+ }
154
+ function calEntryToFeast(entry, season, d) {
155
+ const id = entry.id ?? "";
156
+ // All 642 entries carry a ritus; "Feria" is a defensive floor only.
157
+ const ritus = entry.ritus ?? "Feria";
158
+ const grade = entryGrade(id, ritus);
159
+ return {
160
+ id,
161
+ nomen: entry.name,
162
+ ritus,
163
+ grade,
164
+ season: season.code,
165
+ tempus: TEMPUS_NAMES[season.code],
166
+ seasonStart: season.start,
167
+ seasonEnd: season.end,
168
+ date: d,
169
+ weekday: d.getUTCDay(),
170
+ masses: selectMasses(id, grade, season.code, d),
171
+ marian: BVM_FEAST_IDS.has(id),
172
+ apostolic: APOSTOLIC_FEAST_IDS.has(id),
173
+ };
174
+ }
175
+ // Precedence order of a raw CalEntry (for same-day sorting before conversion).
176
+ function entryGradeOrder(entry) {
177
+ return gradeOrder(entryGrade(entry.id, entry.ritus ?? "Feria"));
178
+ }
179
+ function feastsForDate(date) {
180
+ const d = startOfDay(date);
181
+ const key = isoDate(d);
182
+ const year = d.getUTCFullYear();
183
+ // Tempora anchored in the previous year can spill into January (e.g.
184
+ // Nat2-0, the Sunday of the Holy Name), so the prior year's calendar is
185
+ // consulted too.
186
+ const entries = [
187
+ ...(buildCalendar(year).get(key) ?? []),
188
+ ...(buildCalendar(year - 1).get(key) ?? []),
189
+ ];
190
+ if (!entries.length)
191
+ return [];
192
+ entries.sort((a, b) => entryGradeOrder(a) - entryGradeOrder(b));
193
+ const season = findSeason(d);
194
+ return entries.map((e) => calEntryToFeast(e, season, d));
195
+ }
196
+ /**
197
+ * Calendar lookup (`tonus.festum`). Returns matching feasts sorted
198
+ * `day asc, rank desc` — for a date, the primary feast plus concurrent
199
+ * feasts; for a `from`/`to` range, every day flattened; with no query,
200
+ * the current liturgical year. Dates are UTC-canonical: build them from
201
+ * ISO strings or `Date.UTC`.
202
+ */
203
+ export function getFeast(query) {
204
+ if (!query || Object.keys(query).length === 0) {
205
+ return feastsForDate(DEFAULT_EPOCH);
206
+ }
207
+ let results;
208
+ if (query.date) {
209
+ results = feastsForDate(query.date);
210
+ }
211
+ else if (query.from != null || query.to != null) {
212
+ if (query.from == null || query.to == null) {
213
+ throw new RangeError("festum range requires both from and to");
214
+ }
215
+ if (query.to.getTime() < query.from.getTime()) {
216
+ throw new RangeError("festum range: to must be >= from");
217
+ }
218
+ results = [];
219
+ let d = startOfDay(query.from);
220
+ const end = startOfDay(query.to);
221
+ while (d <= end) {
222
+ results.push(...feastsForDate(d));
223
+ d = addDays(d, 1);
224
+ }
225
+ }
226
+ else {
227
+ // Full calendar scan for the default liturgical year range (the year
228
+ // containing DEFAULT_EPOCH — Guido d'Arezzo's era). The liturgical year
229
+ // begins at Advent, so before Advent the range anchors to the previous
230
+ // civil year's first Advent Sunday.
231
+ const today = startOfDay(DEFAULT_EPOCH);
232
+ let year = today.getUTCFullYear();
233
+ if (today < getAnchors(year).adventFirstSunday)
234
+ year -= 1;
235
+ const startDate = getAnchors(year).adventFirstSunday;
236
+ const endDate = getAnchors(year + 1).adventFirstSunday;
237
+ results = [];
238
+ let d = startDate;
239
+ while (d < endDate) {
240
+ results.push(...feastsForDate(d));
241
+ d = addDays(d, 1);
242
+ }
243
+ }
244
+ if (query.nomen) {
245
+ const n = query.nomen.toLowerCase();
246
+ results = results.filter((f) => f.nomen.toLowerCase().includes(n));
247
+ }
248
+ if (query.season) {
249
+ results = results.filter((f) => f.season === query.season);
250
+ }
251
+ if (query.grade !== undefined) {
252
+ results = results.filter((f) => f.grade === query.grade);
253
+ }
254
+ if (query.marian !== undefined) {
255
+ results = results.filter((f) => f.marian === query.marian);
256
+ }
257
+ if (query.apostolic !== undefined) {
258
+ results = results.filter((f) => f.apostolic === query.apostolic);
259
+ }
260
+ // Sort: day asc, rank desc (lower rank number = higher priority, so asc)
261
+ results.sort((a, b) => a.date.getTime() - b.date.getTime() ||
262
+ gradeOrder(a.grade) - gradeOrder(b.grade));
263
+ return results;
264
+ }
265
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1,31 @@
1
+ export interface RuleAnchors {
2
+ year: number;
3
+ easter: Date;
4
+ ashWednesday: Date;
5
+ firstLentSunday: Date;
6
+ septuagesima: Date;
7
+ pentecost: Date;
8
+ ascension: Date;
9
+ adventFirstSunday: Date;
10
+ gaudete: Date;
11
+ christmas: Date;
12
+ epiphany: Date;
13
+ baptism: Date;
14
+ }
15
+ export { DEFAULT_EPOCH } from "../epoch.js";
16
+ export declare function isoDate(date: Date): string;
17
+ export declare function startOfDay(date: Date): Date;
18
+ export declare function addDays(date: Date, days: number): Date;
19
+ export declare function subDays(date: Date, days: number): Date;
20
+ export declare function firstSundayOnOrAfter(date: Date): Date;
21
+ export declare function nextSunday(date: Date): Date;
22
+ export declare function parseMonthDay(year: number, mmdd: string): Date;
23
+ export declare function pascha(year: number): Date;
24
+ export interface Placement {
25
+ date: Date;
26
+ key: string;
27
+ }
28
+ export declare function resolveEntryId(id: string, year: number, anchors: RuleAnchors): Placement[];
29
+ /** For a tempora weekday ID like "Adv1-3", return the Sunday "Adv1-0". Null for non-tempora or already a Sunday. */
30
+ export declare function temporaSundayId(id: string): string | null;
31
+ //# sourceMappingURL=date.d.ts.map
@@ -0,0 +1,141 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/cal/date — date math, computus, and liturgical calendar rule resolution
3
+ // ---------------------------------------------------------------------------
4
+ // Re-exported for cal consumers; the canonical definition and rationale live
5
+ // in engines/epoch (it is shared with the planet engine).
6
+ export { DEFAULT_EPOCH } from "../epoch.js";
7
+ // ── Date math ──
8
+ export function isoDate(date) {
9
+ return date.toISOString().slice(0, 10);
10
+ }
11
+ export function startOfDay(date) {
12
+ // Canonical form is UTC midnight, so dates from ISO strings like
13
+ // new Date("2025-12-25") behave identically in every timezone.
14
+ const y = date.getUTCFullYear(), m = date.getUTCMonth(), d = date.getUTCDate();
15
+ return new Date(Date.UTC(y, m, d));
16
+ }
17
+ export function addDays(date, days) {
18
+ const d = new Date(date);
19
+ d.setUTCDate(d.getUTCDate() + days);
20
+ return d;
21
+ }
22
+ export function subDays(date, days) {
23
+ return addDays(date, -days);
24
+ }
25
+ export function firstSundayOnOrAfter(date) {
26
+ const d = startOfDay(date);
27
+ return addDays(d, (7 - d.getUTCDay()) % 7);
28
+ }
29
+ export function nextSunday(date) {
30
+ const d = startOfDay(date);
31
+ return addDays(d, (7 - d.getUTCDay()) % 7 || 7);
32
+ }
33
+ export function parseMonthDay(year, mmdd) {
34
+ const [m, d] = mmdd.split("-").map(Number);
35
+ if (!m || !d)
36
+ throw new Error(`Invalid month-day: ${mmdd}`);
37
+ return new Date(Date.UTC(year, m - 1, d));
38
+ }
39
+ // ── Easter ──
40
+ export function pascha(year) {
41
+ if (year < 1583)
42
+ return paschaJulian(year);
43
+ const t = Math.trunc;
44
+ const G = year % 19;
45
+ const C = t(year / 100);
46
+ const H = (C - t(C / 4) - t((8 * C + 13) / 25) + 19 * G + 15) % 30;
47
+ const I = H - t(H / 28) * (1 - t(29 / (H + 1)) * t((21 - G) / 11));
48
+ const J = (year + t(year / 4) + I + 2 - C + t(C / 4)) % 7;
49
+ const L = I - J;
50
+ const month = 3 + t((L + 40) / 44);
51
+ const day = L + 28 - 31 * t(month / 4);
52
+ return new Date(Date.UTC(year, month - 1, day));
53
+ }
54
+ function paschaJulian(year) {
55
+ const a = year % 4, b = year % 7, c = year % 19;
56
+ const d = (19 * c + 15) % 30;
57
+ const e = (2 * a + 4 * b - d + 34) % 7;
58
+ const month = Math.floor((d + e + 114) / 31);
59
+ const day = ((d + e + 114) % 31) + 1;
60
+ return julianToGregorian(year, month, day);
61
+ }
62
+ function julianToJdn(year, month, day) {
63
+ const a = Math.floor((14 - month) / 12);
64
+ const y = year + 4800 - a;
65
+ const m = month + 12 * a - 3;
66
+ return (day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - 32083);
67
+ }
68
+ function julianToGregorian(y, m, d) {
69
+ const jdn = julianToJdn(y, m, d);
70
+ let l = jdn + 68569;
71
+ const n = Math.floor((4 * l) / 146097);
72
+ l -= Math.floor((146097 * n + 3) / 4);
73
+ const i = Math.floor((4000 * (l + 1)) / 1461001);
74
+ l = l - Math.floor((1461 * i) / 4) + 31;
75
+ const j = Math.floor((80 * l) / 2447);
76
+ const day = l - Math.floor((2447 * j) / 80);
77
+ l = Math.floor(j / 11);
78
+ const month = j + 2 - 12 * l;
79
+ const year = 100 * (n - 49) + i + l;
80
+ return new Date(Date.UTC(year, month - 1, day));
81
+ }
82
+ function place(date) {
83
+ return { date, key: isoDate(date) };
84
+ }
85
+ export function resolveEntryId(id, year, anchors) {
86
+ if (/^\d{2}-\d{2}$/.test(id)) {
87
+ return [place(startOfDay(parseMonthDay(year, id)))];
88
+ }
89
+ return [place(resolveTemporaStem(id, anchors))];
90
+ }
91
+ function resolveTemporaStem(stem, anchors) {
92
+ // Stem format: PREFIX + week + "-" + weekday (0=Sun … 6=Sat)
93
+ // e.g. Adv1-0, Pasc3-4, Pent12-2, Quad2-5, Quadp1-0, Nat1-0, Epi1-0
94
+ const m = stem.match(/^([A-Za-z]+)(\d+)-(\d+)$/);
95
+ if (!m)
96
+ throw new Error(`Unrecognized tempora stem: ${stem}`);
97
+ const prefix = m[1];
98
+ const week = parseInt(m[2], 10);
99
+ const weekday = parseInt(m[3], 10);
100
+ let base;
101
+ switch (prefix) {
102
+ case "Adv":
103
+ base = addDays(anchors.adventFirstSunday, (week - 1) * 7);
104
+ break;
105
+ case "Nat":
106
+ base = firstSundayOnOrAfter(addDays(anchors.christmas, 1));
107
+ if (week > 1)
108
+ base = addDays(base, (week - 1) * 7);
109
+ break;
110
+ case "Epi":
111
+ base = firstSundayOnOrAfter(addDays(anchors.epiphany, 1));
112
+ if (week > 1)
113
+ base = addDays(base, (week - 1) * 7);
114
+ break;
115
+ case "Quadp":
116
+ // Quadp1-0 = Septuagesima Sunday
117
+ base = addDays(anchors.septuagesima, (week - 1) * 7);
118
+ break;
119
+ case "Quad":
120
+ // Quad1-0 = first Sunday of Lent (4 days after Ash Wednesday)
121
+ base = addDays(anchors.ashWednesday, 4 + (week - 1) * 7);
122
+ break;
123
+ case "Pasc":
124
+ // Pasc0-0 = Easter Sunday; Pasc1-0 = +7, etc.
125
+ base = addDays(anchors.easter, week * 7);
126
+ break;
127
+ case "Pent":
128
+ // Pent01-0 = Trinity Sunday (Pentecost + 7 days)
129
+ base = addDays(anchors.pentecost, week * 7);
130
+ break;
131
+ default:
132
+ throw new Error(`Unknown tempora prefix: ${prefix}`);
133
+ }
134
+ return addDays(base, weekday);
135
+ }
136
+ /** For a tempora weekday ID like "Adv1-3", return the Sunday "Adv1-0". Null for non-tempora or already a Sunday. */
137
+ export function temporaSundayId(id) {
138
+ const m = id.match(/^([A-Za-z]+\d+)-([1-6])$/);
139
+ return m ? `${m[1]}-0` : null;
140
+ }
141
+ //# sourceMappingURL=date.js.map