aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar/oracle_ef_test.go
blob: ce4badd158d96573280c3a0b0545b59cc065bd29 (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
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)
	}
}