//go:build ignore // gen-sanctoral generates the OF (Ordinary Form) universal sanctoral // (internal/caldata/roman-calendar.ini) from the authoritative General Roman // Calendar published by calapi.inadiutorium.cz. For each fixed date it takes // the saint/Marian/Lord celebrations (memorial/optional-memorial/feast/ // solemnity) that the temporal engine does NOT already compute, with: // - English name + rank + colour from calapi general-en, // - Latin name from calapi general-la, // - Polish name from niedziela.pl (DayInfo, obligatory celebrations). // // Multiple reference years are unioned so a saint whose date is a Sunday in one // year is still captured from another. One-time; requires network + curl-ish UA. // // go run scripts/gen-sanctoral.go // // Writes internal/caldata/roman-calendar.ini. package main import ( "encoding/json" "fmt" "net/http" "os" "regexp" "sort" "strings" "time" "github.com/lukaszkasprzak/lectio/internal/calendar" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/readings" ) const ua = "Mozilla/5.0 (lectio sanctoral generator)" type calDay struct { Date string `json:"date"` Celebrations []struct { Title string `json:"title"` Colour string `json:"colour"` Rank string `json:"rank"` RankNum float64 `json:"rank_num"` } `json:"celebrations"` } func fetchMonth(cal string, y, m int) ([]calDay, error) { url := fmt.Sprintf("http://calapi.inadiutorium.cz/api/v0/en/calendars/%s/%d/%d", cal, y, m) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", ua) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var days []calDay if err := json.NewDecoder(resp.Body).Decode(&days); err != nil { return nil, err } return days, nil } var ( rankMap = map[string]calendar.Rank{"solemnity": calendar.RankSolemnity, "feast": calendar.RankFeast, "memorial": calendar.RankMemorial, "optional memorial": calendar.RankOptional} slugStripRe = regexp.MustCompile(`[^a-z0-9]+`) plPrefixRe = regexp.MustCompile(`^(?:Uroczystość|Święto|Wspomnienie(?: obowiązkowe)?)\s+`) ) func slugify(title string) string { s := strings.ToLower(title) s = strings.NewReplacer("ł", "l", "æ", "ae", "é", "e", "è", "e", "ô", "o", "ç", "c").Replace(s) s = slugStripRe.ReplaceAllString(s, "-") s = strings.Trim(s, "-") s = strings.TrimPrefix(s, "saint-") s = strings.TrimPrefix(s, "st-") return s } // classOf infers the celebration class from its English title (precedence hint). func classOf(en string) string { l := strings.ToLower(en) switch { case strings.Contains(l, "blessed virgin mary"), strings.Contains(l, "our lady"), strings.Contains(l, "of mary"): return "bvm" case strings.Contains(l, "the lord"), strings.Contains(l, "of the cross"), strings.Contains(l, "transfiguration"), strings.Contains(l, "holy trinity"): return "lord" default: return "saint" } } type entry struct { slug, date, rank, colour, class, en, la, pl string rankNum float64 } func main() { sel := calendar.DefaultSelection() sel.Form = "new" cfg := config.Default() cfg.Lectionary = "new" entries := map[string]entry{} // slug -> entry (first year wins per slug) // Reference years: cover every weekday so a saint whose date is a Sunday in // one year is captured in another. 2025 is past -> niedziela gives pl names. for _, y := range []int{2025, 2026, 2027} { for m := 1; m <= 12; m++ { en, err := fetchMonth("general-en", y, m) if err != nil { fmt.Fprintf(os.Stderr, "en %d/%d: %v\n", y, m, err) continue } la, _ := fetchMonth("general-la", y, m) laByDate := map[string][]string{} for _, d := range la { for _, c := range d.Celebrations { laByDate[d.Date] = append(laByDate[d.Date], c.Title) } } for _, d := range en { t, _ := time.Parse("2006-01-02", d.Date) // Skip dates the temporal engine owns (its own solemnities/feasts: // Christmas, Easter cycle, etc.). Sanctoral dates are ferial (or a // plain Sunday) in the pure temporal. temp := calendar.Compute(t, sel, nil) if temp.Observed.Rank == calendar.RankSolemnity && t.Weekday() != time.Sunday { continue } if temp.Observed.Rank == calendar.RankFeast { continue } // Polish name for the observed (obligatory) celebration, 2025 only. plName := "" if y == 2025 { if _, info, e := readings.Load(cfg, readings.Options{Date: d.Date, All: false}); e == nil { plName = strings.TrimSpace(plPrefixRe.ReplaceAllString(info.Name, "")) } } for i, c := range d.Celebrations { rk, ok := rankMap[c.Rank] if !ok { continue // "ferial" or unknown -> not a sanctoral saint } slug := slugify(c.Title) if slug == "" { continue } if _, seen := entries[slug]; seen { continue } laTitle := "" if l := laByDate[d.Date]; i < len(l) { laTitle = l[i] } pl := "" if i == 0 { // the observed celebration niedziela reports pl = plName } entries[slug] = entry{ slug: slug, date: t.Format("01-02"), rank: string(rk), colour: c.Colour, class: classOf(c.Title), en: c.Title, la: laTitle, pl: pl, rankNum: c.RankNum, } } } fmt.Fprintf(os.Stderr, "%d/%02d done (%d entries so far)\n", y, m, len(entries)) } } es := make([]entry, 0, len(entries)) for _, e := range entries { es = append(es, e) } sort.Slice(es, func(i, j int) bool { if es[i].date != es[j].date { return es[i].date < es[j].date } return es[i].rankNum < es[j].rankNum // higher rank (lower num) first }) var b strings.Builder b.WriteString("; General Roman Calendar (Ordinary Form) — universal sanctoral.\n") b.WriteString("; The temporal cycle (Sundays, seasons, Easter/Christmas cycles, Christ the King,\n") b.WriteString("; Baptism, Holy Family, Trinity, Corpus Christi, Sacred Heart) is computed by\n") b.WriteString("; internal/calendar and is NOT listed here.\n") b.WriteString("; Generated by scripts/gen-sanctoral.go from calapi.inadiutorium.cz (General Roman\n") b.WriteString("; Calendar; en+la names) and niedziela.pl (pl names). See NOTICE.\n\n") b.WriteString("[layer]\nid = universal\nname = General Roman Calendar\ntype = universal\n") for _, e := range es { fmt.Fprintf(&b, "\n[%s]\ndate = %s\nrank = %s\n", e.slug, e.date, e.rank) if e.class != "" { fmt.Fprintf(&b, "class = %s\n", e.class) } fmt.Fprintf(&b, "colour = %s\n", e.colour) fmt.Fprintf(&b, "name.en = %s\n", e.en) if e.pl != "" { fmt.Fprintf(&b, "name.pl = %s\n", e.pl) } if e.la != "" { fmt.Fprintf(&b, "name.la = %s\n", e.la) } } if err := os.WriteFile("internal/caldata/roman-calendar.ini", []byte(b.String()), 0o644); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Fprintf(os.Stderr, "\nwrote %d sanctoral celebrations\n", len(es)) }