//go:build ignore // 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 // // Writes internal/caldata/of-lectionary.ini. package main import ( "fmt" "os" "regexp" "sort" "strings" "time" "github.com/lukaszkasprzak/lectio/internal/bible" "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/calendar" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/readings" ) // bookCommaRe strips a stray comma between the book name and the first // chapter ("4 Kings, 5:1-15" -> "4 Kings 5:1-15"), a source data glitch. // The [^:] guard means it only fires before any chapter:verse, so legitimate // multi-chapter commas ("Gen 1:1, 2:3") are left intact. // Copied verbatim from scripts/genlect.go. var bookCommaRe = regexp.MustCompile(`^([^:]*?),\s+(\d+:)`) // chapDotRe rewrites a European-style "chapter. verse" separator to a colon // ("John 20. 19-31" -> "John 20:19-31"), another source data glitch. It is // applied only when the citation carries no colon at all, so disjoint verse // groups in a normal citation ("Ps 62:2. 3-4") are never touched. // Copied verbatim from scripts/genlect.go. var chapDotRe = regexp.MustCompile(`(\d+)\.\s+(\d)`) func cleanCite(s string) string { s = bookCommaRe.ReplaceAllString(strings.TrimSpace(s), "$1 $2") if !strings.Contains(s, ":") { s = chapDotRe.ReplaceAllString(s, "$1:$2") } 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. var partIDToPart = map[string]string{ "pierwsze_czytanie": "first", "psalm": "psalm", "drugie_czytanie": "second", "ewangelia": "gospel", } func main() { sel := calendar.DefaultSelection() sel.Form = "new" layers := []calendar.Layer{caldata.Universal()} cfg := config.Default() cfg.Lectionary = "new" lect := map[string]map[string]string{} // key -> part -> citation seen := map[string]bool{} 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 2018-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(2018, 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) { 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 { // Obligatory memorial: SKIP. Most memorials read the ferial of the // DAY, which varies year to year (the fixed feast date lands in a // different Ordinary-Time week each year) -- so keying by the saint // slug would freeze one year's ferial and be wrong in others. At // runtime the resolver falls the memorial back to the day's ferial // slug, which resolves to the correct week's reading. (Proper-reading // memorials therefore show the ferial, a permitted OF option; their // propers are a later refinement.) continue } if seen[key] { continue // already stored from an earlier year } 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[key] = fmt.Sprintf("%s (readings.Load error: %v)", dateStr, err) continue } parts := map[string]string{} for _, s := range secs { part, ok := partIDToPart[s.PartID] if !ok { continue } system := "" if part == "psalm" { system = "drb" } 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) continue } parts[part] = cleanCite(eng) } if 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"]) } keys := make([]string, 0, len(lect)) for k := range lect { keys = append(keys, k) } sort.Strings(keys) var b strings.Builder 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 2018-2025).\n") for _, k := range keys { v := lect[k] fmt.Fprintf(&b, "\n[%s]\n", k) for _, part := range []string{"first", "psalm", "second", "gospel"} { if c := v[part]; c != "" { fmt.Fprintf(&b, "%s = %s\n", part, c) } } } if err := os.WriteFile("internal/caldata/of-lectionary.ini", []byte(b.String()), 0o644); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Fprintf(os.Stderr, "\nwrote %d OF temporal entries\n", len(lect)) if len(missing) > 0 { 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]) } } }