aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar/precedence_ef.go
blob: 9f5016f81ca8cb5fb631dd8801d2828c9a730eaa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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:]
}