package caldata import ( "strings" "testing" "github.com/lukaszkasprzak/lectio/internal/calendar" "time" ) func TestUniversalLoads(t *testing.T) { l := Universal() if l.ID != "universal" { t.Fatalf("layer id = %q", l.ID) } // the Assumption (08-15, solemnity) must be present under some slug. assumption := false for _, rc := range l.Cels { if rc.Fields["date"] == "08-15" && rc.Fields["rank"] == "solemnity" { assumption = true } } if !assumption { t.Fatal("no 08-15 solemnity (Assumption) in the universal calendar") } // every entry must build into a typed Celebration whose rank parsed. for slug, rc := range l.Cels { c := calendar.BuildCelebrationForTest(slug, rc) if c.Rank == calendar.RankFerial && rc.Fields["rank"] != "" { t.Errorf("%s: rank did not parse (%q)", slug, rc.Fields["rank"]) } } } func TestUniversalSanctoralReadings(t *testing.T) { l := Universal() // A feast/solemnity or a PROPER-reading memorial carries its own inline Mass. // Partial-proper memorials keep ONLY the strictly-proper part (the resolver // fills the rest from the ferial): the Guardian Angels a proper gospel, St // Barnabas a proper first reading, Our Lady of Sorrows a proper gospel. want := map[string][2]string{ // slug -> {reading.first substr, reading.gospel substr; "" = absent} "assumption-of-the-blessed-virgin-mary": {"Revelation 11", "Luke 1:39-56"}, "all-saints": {"Revelation 7", "Matthew 5:1-12"}, "barnabas-the-apostle": {"Acts 11", ""}, "guardian-angels": {"", "Matthew 18:1-5"}, "our-lady-of-sorrows": {"", "John 19:25-27"}, } for slug, w := range want { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing %q", slug) continue } for i, part := range []string{"reading.first", "reading.gospel"} { got := rc.Fields[part] if w[i] == "" { if got != "" { t.Errorf("%s: %s = %q, want absent (ferial)", slug, part, got) } } else if !strings.Contains(got, w[i]) { t.Errorf("%s: %s = %q, want containing %q", slug, part, got, w[i]) } } } } func TestTridentineLoads(t *testing.T) { l := Tridentine() if l.ID != "tridentine" { t.Fatalf("layer id = %q", l.ID) } // The full 1962 sanctoral is populated (generated from missalemeum), not the // 16-entry bootstrap. if len(l.Cels) < 250 { t.Fatalf("EF sanctoral has only %d entries; expected the full calendar", len(l.Cels)) } // Landmark feasts must be present with the right rank/class: Sts Peter & Paul // (I class, 06-29), the Purification/Presentation (II class, 02-02, class // = lord -- see classOf's own doc comment in scripts/gen-sanctoral-ef.go // for why: the calendarium's own title names the BLESSED VIRGIN, but the // tag exists to drive OCCURRENCE behaviour, and missalemeum -- this // data's own oracle -- shows the Purification taking a II-class Sunday's // place outright, the "festum Domini" pattern, not the ordinary-BVM // pattern), the Immaculate Conception (I class, 12-08). want := map[string]struct{ date, rank, class string }{ "sts-peter-paul": {"06-29", "class-1", ""}, "purification-of-the-blessed-virgin-mary": {"02-02", "class-2", "lord"}, "immaculate-conception-of-the-blessed-virgin-mary": {"12-08", "class-1", ""}, } for slug, w := range want { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing landmark feast %q", slug) continue } if rc.Fields["date"] != w.date || rc.Fields["rank"] != w.rank { t.Errorf("%s: date/rank = %q/%q, want %q/%q", slug, rc.Fields["date"], rc.Fields["rank"], w.date, w.rank) } if rc.Fields["class"] != w.class { t.Errorf("%s: class = %q, want %q", slug, rc.Fields["class"], w.class) } } // every entry must build into a typed Celebration whose rank parsed. for slug, rc := range l.Cels { c := calendar.BuildCelebrationForTest(slug, rc) if c.Rank == calendar.RankFerial && rc.Fields["rank"] != "" { t.Errorf("%s: rank did not parse (%q)", slug, rc.Fields["rank"]) } } } // TestTridentineClassOfLord: scripts/gen-sanctoral-ef.go's classOf tags // feasts of the Lord (class = lord). "Most Holy Name of Mary" was wrongly // tagged lord (a title-substring false positive: "holy name" alone matches // both "Holy Name of Jesus" and "Most Holy Name of MARY", calendarium // "Sanctissimi Nominis Mariae") and is fixed here, not contested. The // Baptism commemoration was missing the tag entirely (13 January's title // ends "of THE Lord", not "of OUR Lord", the only suffix classOf used to // check -- calendarium "IN COMMEMORATIONE BAPTISMATIS D. N. I. C."), also // fixed and not contested. The Purification is DELIBERATELY absent from // this table: it stays tagged lord, on occurrence-behaviour evidence, not // the calendarium's title -- see TestTridentineLoads and classOf's own doc // comment in scripts/gen-sanctoral-ef.go for the full account of why it is // the one contested case, not a clean-cut fix. func TestTridentineClassOfLord(t *testing.T) { l := Tridentine() want := map[string]string{ "most-holy-name-of-mary": "", // BVM, not the Lord "commemoration-of-the-baptism-of-the-lord": "lord", } for slug, wantClass := range want { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing landmark feast %q", slug) continue } if got := rc.Fields["class"]; got != wantClass { t.Errorf("%s: class = %q, want %q", slug, got, wantClass) } } } // TestTridentineLentRankNotCommemoration: 15 III-class feasts, every one // falling 6 March - 5 April, were tagged rank = commemoration because all // six of the generator's reference years happen to place that fixed date // within Lent, where a privileged Lenten feria always outranks an // equal-class feast (RG 109(e)) -- the generator mistook "how the entry // presented on the sampled days" for the entry's true, intrinsic rank. A // sample of the 15 (full list in the report); the calendarium gives each one // III class outright, e.g. 5 April "S. Vincentii Ferrerii Conf., III // classis." func TestTridentineLentRankNotCommemoration(t *testing.T) { l := Tridentine() for _, slug := range []string{ "sts-felicitas-perpetua", "thomas-aquinas", "frances-rome", "gregory-the-great", "patrick", "benedict", "francis-of-paola", "isidore-of-seville", "vincent-ferrer", } { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing %q", slug) continue } if got := rc.Fields["rank"]; got != "class-3" { t.Errorf("%s: rank = %q, want class-3 (not commemoration)", slug, got) } } } // TestTridentineGenuineCommemorationStaysCommemoration: the fix above must // NOT promote every commemoration-only entry indiscriminately -- some really // are without an independent Mass in the 1960-reformed books, demoted even // on an ORDINARY, unprivileged day (not explained by Lent/Passiontide or any // higher-class temporal day). St Blaise (3 Feb, an ordinary Septuagesima- // season feria) is live-confirmed by missalemeum as a mere commemoration // even then, so RankCommemoration is the honest, correct rank -- promoting // him to class-4 would let him wrongly win an ordinary green-season feria he // has no independent Mass to celebrate. func TestTridentineGenuineCommemorationStaysCommemoration(t *testing.T) { l := Tridentine() rc, ok := l.Cels["blaise"] if !ok { t.Fatal("missing blaise") } if got := rc.Fields["rank"]; got != "commemoration" { t.Errorf("blaise: rank = %q, want commemoration (genuinely no independent Mass)", got) } } // TestTridentineNoMissingEntries: four entries present in missalemeum (and // so in the calendarium) were silently dropped by the generator's own // dedup, which keyed a single global map by SLUG alone -- two by a literal // slug collision with an unrelated feast of the same English title on a // different date (28 January's "St. Agnes", the traditional SECOND // commemoration of 21 January's own Agnes; 14 May's "St. Boniface", a // different martyr from 5 June's Boniface of Mainz), and two (Evaristus, // Theodore) by the generator returning on the first reference year that // showed an observed office, before ever reaching the later year that // revealed their commemoration. func TestTridentineNoMissingEntries(t *testing.T) { l := Tridentine() want := map[string]string{ "agnes-secundo": "01-28", "boniface-martyr": "05-14", "evaristus": "10-26", "theodore": "11-09", } for slug, date := range want { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing %q (%s)", slug, date) continue } if got := rc.Fields["date"]; got != date { t.Errorf("%s: date = %q, want %q", slug, got, date) } } } // TestTridentineRomanusAndEusebiusPresent: CORRECTED after review found the // opposite claim resting on an incomplete primary-source check. An earlier // version of this test (and of scripts/gen-sanctoral-ef.go's // knownSpuriousComm) asserted Romanus ABSENT, on the strength of ONE of the // three local Missal scans -- "1962-06-23,…LT.pdf", an ELECTRONIC // TRANSCRIPTION that silently drops vigil commemorations generally (also // missing: 7 August Donatus, 25 December Anastasia, both present elsewhere). // The other two, PHOTOGRAPHIC scans of the actual 1962 Missale Romanum, both // carry it: "missale-romanum-1962.pdf" calendarium, 9 August row: "XVI d V // 9 Vigilia, III classis, Commemoratio S. Romani Mart.", with the saint's // own proper text elsewhere in the same scan ("Et fit commemoratio S. Romani // Mar-"), and its own back-of-book index ("Romani Mart., 9 augusti ... 621"). // Where the scans and the transcription disagree, the scans win -- see // gen-sanctoral-ef.go's own primary-source note, same rule recorded there so // it is not lost a second time. // // 14 August's "St. Eusebius" is the identical shape (calendarium: "XI b XIX // 14 Vigilia, II classis, Commemoratio S. Eusebii Conf.") and a DIFFERENT // person from 16 December's "St. Eusebius, Ep. et Mart." (calendarium: "V // XVII S. Eusebii Ep. et Mart., III classis.") -- a Confessor and a Bishop // and Martyr, not the same saint moved or duplicated. missalemeum gives both // the same bare English title, so they collide by slug; slugOverride // disambiguates rather than either being dropped. func TestTridentineRomanusAndEusebiusPresent(t *testing.T) { l := Tridentine() if rc, ok := l.Cels["romanus"]; !ok { t.Error("romanus missing: the calendarium's photographic scans both carry \"Commemoratio S. Romani Mart.\" on 9 August") } else if rc.Fields["date"] != "08-09" { t.Errorf("romanus: date = %q, want 08-09", rc.Fields["date"]) } want := map[string]struct{ date, rank string }{ // St Eusebius Confessor (14 Aug) and St Romanus (9 Aug, checked // above) are both bare "Commemoratio" in the calendarium (no class // of their own) -- pinned here, not just presence/date, per // TestTridentineCommemorationRanksPinned's own reasoning below: // romanus's own rank is pinned there instead, since that test // groups every commemoration-rank entry this task's fixes touched // in one place. eusebius-confessor is listed here because it is // the OTHER half of this specific test's own slug-disambiguation // story. "eusebius-confessor": {"08-14", "commemoration"}, // "S. Eusebii Conf." -- a Confessor "eusebius": {"12-16", "class-3"}, // "S. Eusebii Ep. et Mart." -- a Bishop and Martyr, a different person, unaffected by this round } for slug, w := range want { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing %q (%s)", slug, w.date) continue } if rc.Fields["date"] != w.date { t.Errorf("%s: date = %q, want %q", slug, rc.Fields["date"], w.date) } if rc.Fields["rank"] != w.rank { t.Errorf("%s: rank = %q, want %q", slug, rc.Fields["rank"], w.rank) } } } // TestTridentineCommemorationRanksPinned: nothing in this test suite // asserted `rank` for thomas-becket, silvester, or romanus before this -- // `TestTridentineRomanusAndEusebiusPresent` above pinned presence and date // for romanus but not rank, and Thomas Becket/Silvester (the I3 fix, RG // 68(d)/(e)) had no rank assertion anywhere at all. So a future // regeneration could silently rewrite any of the three back to class-4 -- // the EXACT I3 failure mode -- with the suite green throughout. Pinned // together because they are the same failure shape (a bare "Commemoratio" // in the calendarium, no class of its own, silently promoted by // refYearExplainsAbsence's own coupling to temporal_ef.go, see that // function's doc comment) even though they were found in two different // review rounds (romanus in C2, Thomas Becket/Silvester in I3). func TestTridentineCommemorationRanksPinned(t *testing.T) { l := Tridentine() for _, slug := range []string{"romanus", "thomas-becket", "silvester"} { rc, ok := l.Cels[slug] if !ok { t.Errorf("missing %q", slug) continue } if rc.Fields["rank"] != "commemoration" { t.Errorf("%s: rank = %q, want commemoration", slug, rc.Fields["rank"]) } } } // TestTridentineNoTransferArtifacts: a movable-transfer feast displayed on // whatever civil date it actually landed on in a given reference year (St // Joseph pushed to 20 March by a Sunday of Lent; the Annunciation deferred // past Holy Week; All Souls moved to the Monday; St Matthias shown on the // 25th in a leap year) must not leak into the sanctoral as a phantom // fixed-date entry keyed to that transferred civil date. func TestTridentineNoTransferArtifacts(t *testing.T) { l := Tridentine() for _, slug := range []string{ "joseph-spouse-of-the-bl-virgin-mary-0320", "annunciation-of-the-blessed-virgin-mary-0405", "annunciation-of-the-blessed-virgin-mary-0408", "annunciation-of-the-blessed-virgin-mary-0409", "commemoration-of-all-souls-1103", "matthias-0225", } { if _, ok := l.Cels[slug]; ok { t.Errorf("phantom transfer-artifact entry %q present", slug) } } } // TestTridentineNamesPreservedAcrossRegeneration is the coverage guard a // regeneration silently destroying a whole language's names needed and did // not have: an earlier version of scripts/gen-sanctoral-ef.go's main() // preserved existing name.la values across a regeneration (missalemeum // supplies English only) but had no equivalent for name.pl -- and, because // the bootstrapped file has in fact never carried a name.la value, that // mechanism looked correct while doing nothing at all. A regeneration // deleted all 322 Polish names outright (name.pl count 322 -> 0), silently: // no test here asserted anything about a name.* field, and // `len(l.Cels) < 250` (TestTridentineLoads) does not notice a field going // missing within entries that still exist. `naming.CelebrationName`'s own // name[lang] -> name.en fallback (internal/naming/naming.go) then quietly // substituted English for Polish on every EF display in that language, // reaching mobile.Day(date, "ef", version, "pl") -- a shipped dlectio entry // point -- with no error anywhere in the chain. // // CORRECTED after review: the first version of this test counted name.pl // only (a hardcoded single language -- the exact "whitelist of two // languages" mistake C1's own fix was written to stop repeating, just // moved into the test), and its coverage floor (>= 315) had 7 entries of // slack -- the review proved dropping 6 entries' Polish names still passed // it. Fixed on both axes: the language set is DISCOVERED from the data // (every "name." key actually present, not a hardcoded list, so a // regeneration dropping name.la or introducing a future name.de is // checked the same way as name.pl), and the floor for each discovered // language is its EXACT true count, not a loose approximation -- verified // directly against the branch point (`git show 2386a45:...`) once, by // hand, and hardcoded as the answer, not derived at test time from data // that could itself be wrong. // // True counts, independently verified: name.en on every entry (327, all // regenerated fresh from missalemeum, including the 5 entries this task's // own fixes added); name.pl on exactly 322 (preserved from the branch // point; the 5 new entries -- St Agnes secundo, St Boniface Martyr, St // Evaristus, St Theodore, St Eusebius Confessor -- never had a curated // Polish name to preserve in the first place, so 322, not 327, is the // correct target, not a shortfall); no OTHER name. exists in the // branch point at all (name.la is read by the generator but the // bootstrapped file has in fact never carried one), so the discovered // language set itself must be exactly {en, pl} -- a regeneration that // silently introduced or lost an entire language key, not just some // values within one, is caught by this assertion, not only by the count. func TestTridentineNamesPreservedAcrossRegeneration(t *testing.T) { l := Tridentine() counts := map[string]int{} for _, rc := range l.Cels { for k, v := range rc.Fields { if v == "" || !strings.HasPrefix(k, "name.") { continue } lang := strings.TrimPrefix(k, "name.") counts[lang]++ } } wantLangs := map[string]bool{"en": true, "pl": true} for lang := range counts { if !wantLangs[lang] { t.Errorf("unexpected name.%s present (%d entries) -- discovered language set must be exactly {en, pl}", lang, counts[lang]) } } for lang := range wantLangs { if counts[lang] == 0 { t.Errorf("name.%s entirely absent -- discovered language set must be exactly {en, pl}", lang) } } if got := counts["en"]; got != len(l.Cels) { t.Errorf("name.en coverage = %d entries, want %d (every entry, all freshly regenerated)", got, len(l.Cels)) } if got := counts["pl"]; got != 322 { t.Errorf("name.pl coverage = %d entries, want exactly 322 (the branch point's own true count; the 5 entries this task added have no curated Polish name to preserve, so more or fewer than 322 is a bug either way)", got) } rc, ok := l.Cels["assumption-of-the-blessed-virgin-mary"] if !ok { t.Fatal("missing assumption-of-the-blessed-virgin-mary") } if got := rc.Fields["name.pl"]; got != "Wniebowzięcie N. M. P." { t.Errorf("assumption-of-the-blessed-virgin-mary: name.pl = %q, want the preserved Polish name", got) } } // TestTridentineMissalOverrides pins the four fields where the 1962 MISSALE // ROMANUM decides against missalemeum, and the Missal wins. // // This is the safety net for a specific accident. This file is generated by // scripts/gen-sanctoral-ef.go from missalemeum, which does NOT carry any of // these values; the generator re-applies them from its own missalOverrides // table on every run. If that table is ever removed, or a slug is renamed // upstream so an override silently stops matching, the generated file quietly // reverts to missalemeum's values and nothing else in this repository would // notice. This test notices. // // Each was found by the sibling project colitur, which validates this data // against the Missal and its Rubricae Generales rather than against another // calendar program: // // - ubaldus and didacus: the Missal's own universal calendarium ranks both // "III classis" outright, with no commemoration rubric, and each has a // Mass entry of its own in the Proprium taking its readings from a Common // -- which a bare commemoration never has. // - the two August vigils: RG 128 gives violet to "vigils of II and III // class outside Paschaltide". Both qualify. The rule's sole exception is // the Ascension's vigil, inside Paschaltide, which is white -- and which // is therefore checked here too, in the opposite direction, so that a // careless "make every vigil violet" fix would fail rather than pass. func TestTridentineMissalOverrides(t *testing.T) { l := Tridentine() for _, c := range []struct{ slug, field, want, why string }{ {"ubaldus", "rank", "class-3", "Missal calendarium: S. Ubaldi Ep. et Conf., III classis"}, {"didacus", "rank", "class-3", "Missal calendarium: S. Didaci Conf., III classis"}, {"vigil-of-st-lawrence", "colour", "violet", "RG 128: III-class vigil outside Paschaltide"}, {"vigil-of-the-assumption", "colour", "violet", "RG 128: II-class vigil outside Paschaltide"}, // RG 124: red is for Apostles, Evangelists and Martyrs. missalemeum has // these inverted in BOTH directions -- confessors and virgins red, two // actual martyrs white -- which is why both directions are pinned here. // A fix that turned every one of them white would pass ten of these // twelve and fail the two that matter. {"alphonsus-liguori", "colour", "white", "RG 124: not a martyr, so white"}, {"apollinaris", "colour", "red", "RG 124(e): Bishop and MARTYR, red"}, {"augustine", "colour", "white", "RG 124: not a martyr, so white"}, {"camillus-de-lellis", "colour", "white", "RG 124: not a martyr, so white"}, {"ephrem-of-syria", "colour", "white", "RG 124: not a martyr, so white"}, {"jerome-emiliani", "colour", "white", "RG 124: not a martyr, so white"}, {"john-gualbert", "colour", "white", "RG 124: not a martyr, so white"}, {"john-of-san-fecundo", "colour", "white", "RG 124: not a martyr, so white"}, {"josaphat", "colour", "red", "RG 124(e): Bishop and MARTYR, red"}, {"julia-of-falconieri", "colour", "white", "RG 124: not a martyr, so white"}, {"martha", "colour", "white", "RG 124: not a martyr, so white"}, {"rose-of-lima", "colour", "white", "RG 124: not a martyr, so white"}, } { rc, ok := l.Cels[c.slug] if !ok { t.Errorf("missing %q -- if the slug was renamed upstream, the generator's "+ "missalOverrides entry must be renamed with it, or the correction is lost", c.slug) continue } if got := rc.Fields[c.field]; got != c.want { t.Errorf("%s: %s = %q, want %q (%s). This is a MISSAL correction that "+ "missalemeum does not carry -- if it reverted, the generator's "+ "missalOverrides table has stopped being applied", c.slug, c.field, got, c.want, c.why) } } // The exception, asserted in the other direction: RG 128's vigil rule is // "outside Paschaltide", and the Ascension's vigil is inside it. if rc, ok := l.Cels["vigil-of-the-ascension"]; ok { if got := rc.Fields["colour"]; got != "" && got != "white" { t.Errorf("vigil-of-the-ascension: colour = %q, want white -- RG 128's "+ "violet rule is for vigils OUTSIDE Paschaltide and this one is inside it", got) } } } // TestEFBVMSaturdayReadings pins RG 309(a)'s seasonal selection among the five // Missae de sancta Maria in sabbato. Without it the day was kept as Our Lady's // office (RG 78) but read the FERIA's Mass -- so a Saturday in Septuagesima // announced Our Lady and then read the parable of the labourers. // // Citations verified by colitur against both photographic scans of the 1962 // Missal; see its lectionary_ef.ml for the per-formulary scan references. func TestEFBVMSaturdayReadings(t *testing.T) { sel := calendar.Selection{Form: "old"} for _, c := range []struct{ date, first string }{ {"2026-01-03", "Titus 3:4-7"}, // Christmas Time {"2026-01-10", "Titus 3:4-7"}, // still Christmas Time {"2026-02-14", "Ecclus 24:14-16"}, // after the Purification {"2026-06-20", "Ecclus 24:14-16"}, // Time after Pentecost {"2026-04-18", "Ecclus 24:14-16"}, // Paschaltide -- same first, DIFFERENT gospel } { day, _ := time.Parse("2006-01-02", c.date) layers := []calendar.Layer{Tridentine()} ld := calendar.Compute(day, sel, layers) got := Readings(sel, layers, day, ld) if len(got) == 0 || got[0].Citation != c.first { first := "(none)" if len(got) > 0 { first = got[0].Citation } t.Errorf("%s: first reading = %q, want %q (RG 309(a), the Mass of Our Lady on Saturday)", c.date, first, c.first) } } // Paschaltide shares its first reading with the rest of the year but has // its own gospel -- St John at the Cross rather than the woman in the // crowd. Asserted separately because a first-reading-only test would pass // with the wrong gospel. day, _ := time.Parse("2006-01-02", "2026-04-18") layers := []calendar.Layer{Tridentine()} got := Readings(sel, layers, day, calendar.Compute(day, sel, layers)) var gospel string for _, r := range got { if r.Part == "gospel" { gospel = r.Citation } } if gospel != "John 19:25-27" { t.Errorf("2026-04-18: gospel = %q, want %q (the Paschaltide formulary)", gospel, "John 19:25-27") } } // TestEFNativityOctaveWeekdayMass pins the Mass of a WEEKDAY within the Octave // of the Nativity, and the Sunday it must not swallow. // // The Missal gives 29-31 December a direct formulary, "Diebus infra octavam // Nativitatis Domini, II classis" -- Titus 3:4-7 / Luke 2:15-20 -- and names // them in the calendarium ("De V die infra octavam ... II classis"). Treated // as ordinary ferias they fell back to the preceding Sunday and served // "Dominica infra octavam" (Gal 4:1-7 / Luke 2:33-40), which is a different // heading for a different day. // // The Sunday case is asserted too, because it is the one the wrong citation // blurred: when 29-31 December IS a Sunday the day really is "Dominica infra // octavam" and keeps that Mass. A fix that applied the weekday formulary by // date alone would pass the first half of this test and fail the second. func TestEFNativityOctaveWeekdayMass(t *testing.T) { sel := calendar.Selection{Form: "old"} layers := []calendar.Layer{Tridentine()} get := func(date string) (string, string) { day, _ := time.Parse("2006-01-02", date) var first, gospel string for _, r := range Readings(sel, layers, day, calendar.Compute(day, sel, layers)) { switch r.Part { case "first": first = r.Citation case "gospel": gospel = r.Citation } } return first, gospel } // 2026: 29-31 December are Tuesday, Wednesday, Thursday -- weekdays. for _, date := range []string{"2026-12-29", "2026-12-30", "2026-12-31"} { if f, g := get(date); f != "Titus 3:4-7" || g != "Luke 2:15-20" { t.Errorf("%s: got %q / %q, want Titus 3:4-7 / Luke 2:15-20 "+ "(Diebus infra octavam Nativitatis Domini)", date, f, g) } } // 2030: 29 December is a SUNDAY, so it is Dominica infra octavam and keeps // the Sunday's own Mass. if f, g := get("2030-12-29"); f != "Gal 4:1-7" || g != "Luke 2:33-40" { t.Errorf("2030-12-29 (a Sunday): got %q / %q, want Gal 4:1-7 / Luke 2:33-40 "+ "-- the weekday formulary must not swallow Dominica infra octavam", f, g) } }