From e829b48b08c683d9dcef5e4b9502c1c42bc895f1 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 28 Jul 2026 00:47:16 +0200 Subject: feat(of): full sanctoral calendar + weekday/memorial readings Populate the OF General Roman Calendar sanctoral and extend the OF readings to weekdays and sanctoral days. Sanctoral calendar (roman-calendar.ini: 29 -> 202 celebrations): - scripts/gen-sanctoral.go generates it from calapi.inadiutorium.cz (authoritative General Roman Calendar) -- English name/rank/colour from general-en, Latin from general-la, Polish from niedziela DayInfo (obligatory celebrations). Fixed dates the temporal engine already computes are excluded. VALIDATED: my engine's observed rank agrees with calapi on all 86 obligatory sanctoral days of 2026 (0 mismatches). Precedence fix (precedence.go): an optional memorial no longer displaces the weekday as the DEFAULT observed celebration (it sorts below the ferial, shown as an option) -- matching the General Roman Calendar / calapi. Readings (of-lectionary.ini: 254 -> 1018 entries) via genlect-of.go: - Weekday (ferial) 2-year cycle I/II, keyed -, harvested from niedziela over 2020-2025 (multiple years per cycle; retry-on-failure so a glitch year never poisons a key; only TRUE ferials + optional-memorial days, where niedziela shows the ferial). - Obligatory memorials keyed by their OWN slug (niedziela shows their proper, e.g. Barnabas -> Acts 11, or the ferial for memorials without a proper) -- never miskeyed to a ferial position. - caldata.Readings: OF resolves slug+SundayCycle | slug+WeekdayCycle, else a memorial falls back to the day's ferial readings. - Source-glitch normalisation (1J->1 J, PnP->Pnp). books.ini: canonical names "Song of Solomon" and "The Acts" added as their own [en] forms (ToEnglishRef emits the canonical; it must resolve). Fixes ~32 cross-chapter Song-of-Songs/Acts citations. Coverage: 359/365 days of 2026 render (98.4%); 6 residual ferial positions never a true-ferial in 2020-2025. EF sanctoral and OF psalm renumbering still pending. --- scripts/genlect-of.go | 105 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 30 deletions(-) (limited to 'scripts/genlect-of.go') diff --git a/scripts/genlect-of.go b/scripts/genlect-of.go index 6126dea..00d0992 100644 --- a/scripts/genlect-of.go +++ b/scripts/genlect-of.go @@ -1,9 +1,11 @@ //go:build ignore -// genlect-of generates the OF (Ordinary Form) Sunday/solemnity temporal -// lectionary from niedziela.pl (via internal/readings), keyed by lectio's -// computed -. One-time; requires network. Run -// from the repo root: +// genlect-of generates the OF (Ordinary Form) temporal lectionary from +// niedziela.pl (via internal/readings): Sundays & solemnities keyed by +// - (A/B/C), ferial weekdays by - +// (I/II). Only TRUE ferials are harvested for the weekday table; +// memorial days are skipped (their readings come from the ferial fallback at +// runtime). One-time; requires network. Run from the repo root: // // go run scripts/genlect-of.go // @@ -47,6 +49,27 @@ func cleanCite(s string) string { return s } +// numBookSpaceRe restores the space niedziela sometimes drops in a numbered +// book sigil ("1J 3,11" -> "1 J 3,11"; "2Kor" -> "2 Kor") so ToEnglishRef's +// Polish table (keyed "1 J", "2 Kor", …) matches. Applied to the raw Polish +// citation BEFORE ToEnglishRef. +var numBookSpaceRe = regexp.MustCompile(`^(\d)([^\d\s])`) + +// sourceGlitches are exact niedziela sigla-casing glitches that don't match the +// ToEnglishRef Polish table (which is Title-case). E.g. "PnP" for Song of Songs. +var sourceGlitches = map[string]string{"PnP": "Pnp"} + +// fixSourceCite normalises a raw niedziela citation before ToEnglishRef. +func fixSourceCite(s string) string { + s = numBookSpaceRe.ReplaceAllString(strings.TrimSpace(s), "$1 $2") + for bad, good := range sourceGlitches { + if strings.HasPrefix(s, bad+" ") { + s = good + s[len(bad):] + } + } + return s +} + // partIDToPart maps a niedziela.pl (liturgy.Section) PartID to our lectionary // field name. Anything absent (e.g. "aklamacja") is not a reading and is // ignored. @@ -67,35 +90,49 @@ func main() { lect := map[string]map[string]string{} // key -> part -> citation seen := map[string]bool{} - var missing []string // date+key kept but missing first/gospel - - // Harvest the PAST cycle-equivalent years: 2023=A, 2024=B, 2025=C. niedziela - // only publishes ~6 weeks ahead, so future years (2026-2027) are unavailable; - // the 3-year cycle repeats, so a liturgical position's cycle-A readings are the - // same whether they fall in 2023 or 2026. All three years are past and archived. - start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + missing := map[string]string{} // key -> last failure reason; cleared on success + + // niedziela only publishes ~6 weeks ahead, so future years are unavailable; + // the cycles repeat, so a liturgical position's readings are identical + // whichever civil year they fall in. Harvest 2020-2025 -- TWO+ years per + // cycle (Sunday A:2020,2023 B:2021,2024 C:2022,2025; weekday I:2021,2023,2025 + // II:2020,2022,2024) so a position displaced by a saint/feast in one year is + // still captured in another. A key is stored from the FIRST year it resolves; + // a year where it fails does not block a later retry (seen is set on success). + start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC) for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { - day := calendar.Compute(d, sel, layers) - keep := d.Weekday() == time.Sunday || - day.Observed.Rank == calendar.RankSolemnity || - day.Observed.Rank == calendar.RankFeast - if !keep { - continue + full := calendar.Compute(d, sel, layers) + sunOrSol := d.Weekday() == time.Sunday || + full.Observed.Rank == calendar.RankSolemnity || + full.Observed.Rank == calendar.RankFeast + + var key string + if sunOrSol { + key = full.Observed.Slug + "-" + full.SundayCycle + } else if full.Observed.Rank == calendar.RankFerial && full.Observed.Layer == "temporal" { + // A true ferial (incl. optional-memorial days, where the ferial is the + // observed default): niedziela shows the ferial readings. + key = full.Observed.Slug + "-" + full.WeekdayCycle + } else if full.Observed.Rank == calendar.RankMemorial { + // An obligatory memorial: niedziela shows its PROPER readings (e.g. St + // Barnabas, Acts 11) or, for memorials without proper readings, the + // ferial. Either way it is keyed by the memorial's OWN slug, so it is + // correctly attributed and never miskeyed to a ferial position. + key = full.Observed.Slug + "-" + full.WeekdayCycle + } else { + continue // (unreachable: optional memorials are ferial-observed) } - - key := day.Observed.Slug + "-" + day.SundayCycle if seen[key] { - continue + continue // already stored from an earlier year } - seen[key] = true dateStr := d.Format("2006-01-02") secs, _, err := readings.Load(cfg, readings.Options{Date: dateStr, All: true}) if err != nil { fmt.Fprintf(os.Stderr, "%s %-32s ERROR readings.Load: %v\n", dateStr, key, err) - missing = append(missing, fmt.Sprintf("%s %s (readings.Load error: %v)", dateStr, key, err)) + missing[key] = fmt.Sprintf("%s (readings.Load error: %v)", dateStr, err) continue } @@ -109,7 +146,7 @@ func main() { if part == "psalm" { system = "drb" } - eng, err := bible.ToEnglishRef(s.Citation, system) + eng, err := bible.ToEnglishRef(fixSourceCite(s.Citation), system) if err != nil { fmt.Fprintf(os.Stderr, "%s %-32s part=%-6s citation=%q: ToEnglishRef error: %v\n", dateStr, key, part, s.Citation, err) @@ -119,11 +156,13 @@ func main() { } if parts["first"] == "" || parts["gospel"] == "" { - missing = append(missing, fmt.Sprintf("%s %s (first=%q gospel=%q)", dateStr, key, parts["first"], parts["gospel"])) + missing[key] = fmt.Sprintf("%s (first=%q gospel=%q)", dateStr, parts["first"], parts["gospel"]) continue // require at least first+gospel to store the entry } lect[key] = parts + seen[key] = true // store from the first year it resolves; stop retrying + delete(missing, key) // a later year succeeded; not actually missing fmt.Fprintf(os.Stderr, "%s %-32s first=%-22s psalm=%-22s second=%-22s gospel=%s\n", dateStr, key, parts["first"], parts["psalm"], parts["second"], parts["gospel"]) } @@ -135,9 +174,10 @@ func main() { sort.Strings(keys) var b strings.Builder - b.WriteString("; OF (Ordinary Form) Sunday & solemnity lectionary, keyed by\n") - b.WriteString("; -. Citations English-canonical.\n") - b.WriteString("; Generated from niedziela.pl by scripts/genlect-of.go (2025-2027).\n") + b.WriteString("; OF (Ordinary Form) temporal lectionary, keyed by -:\n") + b.WriteString("; Sundays & solemnities use the Sunday cycle (A/B/C), ferial weekdays the weekday\n") + b.WriteString("; cycle (I/II). Citations English-canonical.\n") + b.WriteString("; Generated from niedziela.pl by scripts/genlect-of.go (harvest 2020-2025).\n") for _, k := range keys { v := lect[k] fmt.Fprintf(&b, "\n[%s]\n", k) @@ -154,9 +194,14 @@ func main() { fmt.Fprintf(os.Stderr, "\nwrote %d OF temporal entries\n", len(lect)) if len(missing) > 0 { - fmt.Fprintf(os.Stderr, "%d kept days missing first/gospel (not stored):\n", len(missing)) - for _, m := range missing { - fmt.Fprintln(os.Stderr, m) + mk := make([]string, 0, len(missing)) + for k := range missing { + mk = append(mk, k) + } + sort.Strings(mk) + fmt.Fprintf(os.Stderr, "%d keys NEVER resolved in any harvest year:\n", len(missing)) + for _, k := range mk { + fmt.Fprintf(os.Stderr, " %-34s %s\n", k, missing[k]) } } } -- cgit v1.3