summaryrefslogtreecommitdiff
path: root/internal/calendar/precedence.go
blob: 3b7c0a65383bb8b078951d1f708dcb54bfa5789a (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
package calendar

import "sort"

// candidate is one celebration competing for a given day: the temporal day, or
// a sanctoral celebration landing on it. The flags come from the temporal
// builder (Task 4) or are empty for sanctoral entries.
type candidate struct {
	Cel        Celebration
	Temporal   bool
	Privileged bool // band 1-2 temporal day
	PrivFeria  bool // band 9 privileged feria
	Sunday     bool // Ordinary/Christmas Sunday (band 6)
	Season     Season
}

// precedence maps a candidate to its band in the Table of Liturgical Days
// (Universal Norms, 1969) — lower = higher precedence. "Proper" celebrations
// (a non-universal, non-temporal layer) sit one band below their General
// Calendar counterpart.
func precedence(c candidate) int {
	proper := c.Cel.Layer != "" && c.Cel.Layer != "universal" && c.Cel.Layer != "temporal"
	if c.Temporal {
		switch {
		case c.Season == Triduum:
			return 1
		case c.Privileged:
			return 2
		case c.Sunday: // Ordinary/Christmas Sunday (checked before the solemnity-rank fallback)
			return 6
		case c.Cel.Rank == RankSolemnity: // Trinity, Corpus Christi, Sacred Heart, Christ the King
			return 3
		case c.Cel.Rank == RankFeast: // feasts of the Lord (Baptism, Holy Family)
			return 5
		case c.PrivFeria:
			return 9
		default:
			return 13
		}
	}
	switch c.Cel.Rank {
	case RankSolemnity:
		if proper {
			return 4
		}
		return 3
	case RankFeast:
		switch {
		case proper:
			return 8
		case c.Cel.Class == ClassLord: // feasts of the Lord in the General Calendar
			return 5
		default:
			return 7
		}
	case RankMemorial:
		if proper {
			return 11
		}
		return 10
	case RankOptional:
		// An optional memorial does NOT displace the weekday as the day's
		// DEFAULT observed celebration -- the ferial is celebrated unless the
		// priest chooses the option. It sorts just below the ferial so it lands
		// in Others (shown as an option), matching the General Roman Calendar's
		// default (calapi's celebrations[0] on such a day is the ferial).
		return 14
	default:
		return 13
	}
}

// pick returns the highest-precedence candidate as observed and the rest as
// others (commemorations / optional memorials). Ordering is deterministic:
// by precedence band, then by slug, so equal-band collisions resolve the same
// way on every run.
func pick(cands []candidate) (candidate, []candidate) {
	sorted := make([]candidate, len(cands))
	copy(sorted, cands)
	sort.SliceStable(sorted, func(i, j int) bool {
		pi, pj := precedence(sorted[i]), precedence(sorted[j])
		if pi != pj {
			return pi < pj
		}
		return sorted[i].Cel.Slug < sorted[j].Cel.Slug
	})
	// General Norms 60: when two obligatory memorials fall on the same day, both
	// become optional and the weekday (ferial) is celebrated instead -- UNLESS one
	// is Mary, Mother of the Church, whose 2018 institution decree gives it
	// precedence over a coinciding obligatory memorial (then it is observed).
	if len(sorted) >= 2 && !sorted[0].Temporal && !sorted[1].Temporal &&
		sorted[0].Cel.Rank == RankMemorial && sorted[1].Cel.Rank == RankMemorial {
		if i := slugIndex(sorted, "mary-mother-of-the-church"); i >= 0 {
			motc := sorted[i]
			sorted = append(sorted[:i], sorted[i+1:]...)
			sorted = append([]candidate{motc}, sorted...)
		} else {
			for i, c := range sorted {
				if c.Temporal {
					ferial := sorted[i]
					sorted = append(sorted[:i], sorted[i+1:]...)
					sorted = append([]candidate{ferial}, sorted...)
					break
				}
			}
		}
	}
	return sorted[0], sorted[1:]
}

// slugIndex returns the position of the candidate with the given slug, or -1.
func slugIndex(cands []candidate, slug string) int {
	for i, c := range cands {
		if c.Cel.Slug == slug {
			return i
		}
	}
	return -1
}