package calendar import ( "sort" "strings" ) // efRankOrder orders EF (1960) ranks: class-1 highest, then class-2..4, // commemoration, ferial lowest. func efRankOrder(r Rank) int { switch r { case RankClass1: return 5 case RankClass2: return 4 case RankClass3: return 3 case RankClass4: return 2 case RankCommemoration: return 1 default: // ferial / unset return 0 } } // precedenceEF ranks an EF candidate for occurrence (lower = higher precedence): // by class first, then a tie-break at equal class (RG 91's Table of // Precedence, read entry-by-entry within each class). // // The tie-break is asymmetric by season AND, at class 2, by whether the // temporal day is a Sunday or a privileged FERIA: // - III/IV class: an ORDINARY feria (per annum, Advent to 16 Dec, // Septuagesima) yields to an equal-class feast (entries 24/25 above the // feria) -- the feast is celebrated, the feria commemorated (e.g. St // Francis Xavier, Dec 3). The ferias of Lent and Passiontide are // privileged (entry 22, ABOVE entry 24): they outrank an equal-class // feast, which is only commemorated. // - II class: an ordinary SUNDAY wins its tie (entry 15, above entry 16's // feasts) -- but a II-class privileged FERIA (the Ember days, the // late-Advent 17-23 Dec ferias, entry 18) sits BELOW entry 16 and yields // to an equal-class feast, the opposite of a Sunday's own tie. This is // the same table shape as III class's Advent/per-annum ferias, just // inverted for which II-class temporal days count as "privileged". // - I class: Sundays (entry 6) and I-class ferias (entry 7, Ash // Wednesday/Holy Week) both sit above entry 11's feasts, so they always // win -- there is no I-class "ordinary feria yields" case at all. // // The *2 class spacing means the ±1 tie-break never crosses a class boundary. func precedenceEF(c candidate) int { p := (6 - efRankOrder(c.Cel.Rank)) * 2 // class-1 -> 2, ferial -> 12 if c.Temporal { yieldsToFeast := false switch c.Cel.Rank { case RankClass3, RankClass4: yieldsToFeast = c.Season != Lent && c.Season != Passiontide case RankClass2: // A II-class Sunday (entry 15) wins; a II-class privileged feria -- // Ember days, the late-Advent 17-23 Dec ferias (entry 18) -- yields // to an equal-class feast (entry 16), e.g. St Matthew (21 Sep) // beating the September Ember Wednesday, or St Thomas (21 Dec) // beating an Advent late feria. yieldsToFeast = !c.Sunday } if yieldsToFeast { p++ } else { p-- // Sundays, I-class ferias, and penitential (Lent/Passiontide) ferias win their ties } } else if c.Cel.Class == ClassLord && c.Cel.Rank == RankClass2 { // A II class feast of the Lord takes the place of a II class Sunday it // falls on (unlike a saint's feast, which is only commemorated). Give it // the edge over the Sunday's tie-break bonus. p -= 2 } else if c.Cel.Rank == RankClass1 && beatsClass1Sunday(c.Cel.Slug) { // RG 91 entries 4 (Immaculate Conception, Assumption BVM) and 5 // (Vigil & Octave day of the Nativity) both sit ABOVE entry 6 // (Sundays of Advent/Lent/Passiontide, Low Sunday) -- unlike an // ORDINARY I-class feast (entry 11, e.g. St Joseph, the Precious // Blood), which yields to a I-class Sunday, these win the tie. // Found while verifying defect 4 (Sunday ranks): before that fix, // Advent/Lent Sundays were wrongly II class, so these feasts beat // them outright on base class alone, accidentally right; once // Sundays became I class the tie-break mattered, and without this // branch the Sunday would wrongly win. Two live cases in the fixed // calendar: the Immaculate Conception (8 December) on an Advent // Sunday, and the Vigil of Christmas (24 December) on Advent IV -- // the latter is not merely a wrong winner but a WORSE bug without // this branch: transferIfImpededEF has no way to re-place a // transfer that crosses the Dec31/Jan1 boundary (celebrationDate // re-resolves a fixed date using the YEAR OF THE DAY BEING QUERIED, // so a walk landing in January of the following year can never // match the query that produced it), so the Vigil simply vanished // for the year instead of landing on the wrong day. The Assumption // (15 August) never falls in Advent or Lent, so its own share of // this branch is citation-complete but not live. p -= 2 } return p } // beatsClass1Sunday reports whether slug is one of RG 91's entries 4 or 5 -- // the Immaculate Conception, the Assumption, or the Vigil of Christmas -- // all of which sit above entry 6's Sundays in the Table of Precedence. func beatsClass1Sunday(slug string) bool { return strings.Contains(slug, "immaculate-conception") || strings.Contains(slug, "assumption-of-the-blessed-virgin-mary") || slug == "vigil-of-christmas" } // pickEF returns the observed EF celebration and the commemorations, ordered // deterministically by precedence then slug. func pickEF(cands []candidate) (candidate, []candidate) { sorted := make([]candidate, len(cands)) copy(sorted, cands) sort.SliceStable(sorted, func(i, j int) bool { pi, pj := precedenceEF(sorted[i]), precedenceEF(sorted[j]) if pi != pj { return pi < pj } return sorted[i].Cel.Slug < sorted[j].Cel.Slug }) return sorted[0], sorted[1:] }