package calendar_test import ( "encoding/json" "os" "sort" "strings" "testing" "time" "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/calendar" ) type efOracleDay struct { Tempora string `json:"tempora"` Title string `json:"title"` Rank int `json:"rank"` Colour string `json:"colour"` } // efSeasonFromString maps a missalemeum tempora/title phrase (for a day in the // given month) to lectio's EF season. Order matters. Two position-dependent // subtleties: the Whit-octave weekdays ("Monday after Pentecost") are still // Paschaltide, so only "Sunday after Pentecost" is Time after Pentecost; and the // resumed Sundays after Epiphany (celebrated in autumn) sit within Time after // Pentecost, so an "after Epiphany" phrase in the second half of the year maps // there, not to Time after Epiphany. func efSeasonFromString(s string, month int) calendar.Season { s = strings.ToLower(s) has := func(subs ...string) bool { for _, sub := range subs { if strings.Contains(s, sub) { return true } } return false } switch { case has("vigil of christmas"): // last day of Advent return calendar.Advent case has("advent"): return calendar.Advent case has("sunday after pentecost"), has("trinity", "christ the king", "corpus christi"): return calendar.TimeAfterPentecost case has("after epiphany", "epiphany"): if month >= 6 { // resumed Epiphany Sundays, celebrated within Time after Pentecost return calendar.TimeAfterPentecost } return calendar.TimeAfterEpiphany case has("passion", "palm", "holy week", "maundy", "good friday", "holy saturday"): return calendar.Passiontide case has("septuagesima", "sexagesima", "quinquagesima"): return calendar.Septuagesima case has("ash wednesday", "lent"): return calendar.Lent case has("pentecost", "ascension", "rogation", "after easter", "easter", "low sunday", "paschal", "whit"): return calendar.Easter_ // incl. the Whit octave (Mon-Sat after Pentecost) case has("christmas", "nativity", "circumcision", "holy name", "octave day of christ"): return calendar.Christmas default: return "" // unknown phrase -> skip this day } } // TestOracleEF diffs the EF engine against missalemeum (Divinum Officium data). // Season is asserted strictly (validates temporalEF); rank/colour are reported // informationally (partial 1962 sanctoral). func TestOracleEF(t *testing.T) { raw, err := os.ReadFile("testdata/oracle-ef.json") if err != nil { t.Skip("EF oracle snapshot missing; run scripts/build-oracle-ef.sh") } var oracle map[string]efOracleDay if err := json.Unmarshal(raw, &oracle); err != nil { t.Fatal(err) } sel := calendar.DefaultSelection() sel.Form = "old" layers := []calendar.Layer{caldata.Tridentine()} dates := make([]string, 0, len(oracle)) for d := range oracle { dates = append(dates, d) } sort.Strings(dates) var seasonMiss, skipped, total int shown := 0 for _, date := range dates { od := oracle[date] src := od.Tempora if src == "" { src = od.Title } day, _ := time.Parse("2006-01-02", date) want := efSeasonFromString(src, int(day.Month())) if want == "" { skipped++ continue } total++ got := calendar.Compute(day.UTC(), sel, layers) if got.Season != want { seasonMiss++ if shown < 25 { t.Errorf("%s: EF season got %q want %q (from %q)", date, got.Season, want, src) shown++ } } } t.Logf("EF oracle: %d checked, %d skipped(unmapped), %d season mismatches", total, skipped, seasonMiss) if seasonMiss > 0 { t.Fatalf("%d/%d EF season mismatches vs missalemeum — temporalEF is wrong", seasonMiss, total) } }