aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/caldata/caldata.go37
-rw-r--r--internal/caldata/stack_test.go16
-rw-r--r--internal/caldata/tridentine-calendar.ini112
-rw-r--r--internal/cli/liturgy.go30
-rw-r--r--internal/cli/liturgy_test.go15
5 files changed, 183 insertions, 27 deletions
diff --git a/internal/caldata/caldata.go b/internal/caldata/caldata.go
index 9272fbb..14b3985 100644
--- a/internal/caldata/caldata.go
+++ b/internal/caldata/caldata.go
@@ -16,8 +16,11 @@ import (
//go:embed roman-calendar.ini
var romanCalendar []byte
-// Universal parses the embedded universal calendar. It panics on a malformed
-// embedded file (a build-time bug caught by tests), never at the request layer.
+//go:embed tridentine-calendar.ini
+var tridentineCalendar []byte
+
+// Universal parses the embedded Ordinary Form universal calendar. It panics on a
+// malformed embedded file (a build-time bug caught by tests).
func Universal() calendar.Layer {
l, err := ParseLayer(romanCalendar)
if err != nil {
@@ -26,6 +29,24 @@ func Universal() calendar.Layer {
return l
}
+// Tridentine parses the embedded Extraordinary Form (1962) universal calendar.
+func Tridentine() calendar.Layer {
+ l, err := ParseLayer(tridentineCalendar)
+ if err != nil {
+ panic(fmt.Sprintf("caldata: embedded tridentine-calendar.ini invalid: %v", err))
+ }
+ return l
+}
+
+// Base returns the universal base layer for a form: the 1962 calendar for
+// "old" (EF), the General Roman Calendar otherwise (OF).
+func Base(form string) calendar.Layer {
+ if form == "old" {
+ return Tridentine()
+ }
+ return Universal()
+}
+
// LoadLayer reads and parses a user calendar layer file. id (the filename stem)
// is used as the layer's ID when the file's [layer] header omits one.
func LoadLayer(path, id string) (calendar.Layer, error) {
@@ -43,12 +64,12 @@ func LoadLayer(path, id string) (calendar.Layer, error) {
return l, nil
}
-// Stack returns the ordered layer stack for the engine: the universal base
-// first, then each user layer named in use (matched to "<dir>/<id>.ini"), in
-// order. A layer that fails to load is skipped and reported in the error slice
-// so a single bad file never breaks calendar computation.
-func Stack(dir string, use []string) ([]calendar.Layer, []error) {
- layers := []calendar.Layer{Universal()}
+// Stack returns the ordered layer stack for the engine: the form's universal
+// base first, then each user layer named in use (matched to "<dir>/<id>.ini"),
+// in order. A layer that fails to load is skipped and reported in the error
+// slice so a single bad file never breaks calendar computation.
+func Stack(form, dir string, use []string) ([]calendar.Layer, []error) {
+ layers := []calendar.Layer{Base(form)}
var errs []error
for _, id := range use {
l, err := LoadLayer(filepath.Join(dir, id+".ini"), id)
diff --git a/internal/caldata/stack_test.go b/internal/caldata/stack_test.go
index b9d5bfb..3c1cf3f 100644
--- a/internal/caldata/stack_test.go
+++ b/internal/caldata/stack_test.go
@@ -13,7 +13,7 @@ func TestStack(t *testing.T) {
os.WriteFile(filepath.Join(dir, "krakow.ini"),
[]byte("[layer]\nid = krakow\ntype = diocesan\n\n[st-stanislaus]\ndate = 05-08\nrank = solemnity\nclass = saint\ncolour = red\nname.pl = Św. Stanisława\n"), 0o644)
- layers, errs := Stack(dir, []string{"poland", "krakow"})
+ layers, errs := Stack("new", dir, []string{"poland", "krakow"})
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}
@@ -32,7 +32,7 @@ func TestStackMissingLayerSkips(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "good.ini"),
[]byte("[layer]\nid = good\n\n[x]\ndate = 01-02\nrank = memorial\nname.en = X\n"), 0o644)
- layers, errs := Stack(dir, []string{"good", "missing"})
+ layers, errs := Stack("new", dir, []string{"good", "missing"})
if len(errs) != 1 {
t.Fatalf("want 1 error for the missing layer, got %v", errs)
}
@@ -54,3 +54,15 @@ func TestLoadLayerDefaultsIDToStem(t *testing.T) {
t.Errorf("ID = %q, want myparish (from stem)", l.ID)
}
}
+
+func TestStackFormBase(t *testing.T) {
+ dir := t.TempDir()
+ efLayers, _ := Stack("old", dir, nil)
+ if efLayers[0].ID != "tridentine" {
+ t.Errorf("EF base = %q want tridentine", efLayers[0].ID)
+ }
+ ofLayers, _ := Stack("new", dir, nil)
+ if ofLayers[0].ID != "universal" {
+ t.Errorf("OF base = %q want universal", ofLayers[0].ID)
+ }
+}
diff --git a/internal/caldata/tridentine-calendar.ini b/internal/caldata/tridentine-calendar.ini
new file mode 100644
index 0000000..f3e9111
--- /dev/null
+++ b/internal/caldata/tridentine-calendar.ini
@@ -0,0 +1,112 @@
+; General Roman Calendar of 1962 (Extraordinary Form) — universal sanctoral.
+; The temporal cycle (seasons, Sundays, Easter/Christmas/Epiphany, Ascension,
+; Pentecost, Trinity, Corpus Christi, Sacred Heart, Christ the King) is computed
+; by internal/calendar (temporalEF) and is NOT listed here.
+; Ranks use the 1960 Code of Rubrics: class-1..class-4 + commemoration.
+; INITIAL coverage; ranks/membership refined against the missalemeum oracle.
+; Bootstrapped with reference to Divinum Officium / Missal1962 — see NOTICE.
+
+[layer]
+id = tridentine
+name = General Roman Calendar of 1962
+type = universal
+
+; --- I class ---
+
+[immaculate-conception]
+date = 12-08
+rank = class-1
+colour = white
+name.en = The Immaculate Conception of the Blessed Virgin Mary
+name.la = In Conceptione Immaculata Beatae Mariae Virginis
+
+[st-joseph]
+date = 03-19
+rank = class-1
+colour = white
+name.en = Saint Joseph, Spouse of the Blessed Virgin Mary
+name.la = Sancti Joseph Sponsi Beatae Mariae Virginis
+
+[annunciation]
+date = 03-25
+rank = class-1
+colour = white
+name.en = The Annunciation of the Blessed Virgin Mary
+name.la = In Annuntiatione Beatae Mariae Virginis
+
+[nativity-john-baptist]
+date = 06-24
+rank = class-1
+colour = white
+name.en = The Nativity of Saint John the Baptist
+
+[peter-and-paul]
+date = 06-29
+rank = class-1
+colour = red
+name.en = Saints Peter and Paul, Apostles
+
+[assumption]
+date = 08-15
+rank = class-1
+colour = white
+name.en = The Assumption of the Blessed Virgin Mary
+name.la = In Assumptione Beatae Mariae Virginis
+
+[st-michael]
+date = 09-29
+rank = class-1
+colour = white
+name.en = The Dedication of Saint Michael the Archangel
+
+[all-saints]
+date = 11-01
+rank = class-1
+colour = white
+name.en = All Saints
+
+; --- II class ---
+
+[purification]
+date = 02-02
+rank = class-2
+colour = white
+name.en = The Purification of the Blessed Virgin Mary
+
+[st-lawrence]
+date = 08-10
+rank = class-2
+colour = red
+name.en = Saint Lawrence, Martyr
+
+[nativity-bvm]
+date = 09-08
+rank = class-2
+colour = white
+name.en = The Nativity of the Blessed Virgin Mary
+
+[exaltation-of-the-cross]
+date = 09-14
+rank = class-2
+colour = red
+name.en = The Exaltation of the Holy Cross
+
+[st-andrew]
+date = 11-30
+rank = class-2
+colour = red
+name.en = Saint Andrew, Apostle
+
+; --- III class ---
+
+[conversion-of-paul]
+date = 01-25
+rank = class-3
+colour = white
+name.en = The Conversion of Saint Paul the Apostle
+
+[st-lucy]
+date = 12-13
+rank = class-3
+colour = red
+name.en = Saint Lucy, Virgin and Martyr
diff --git a/internal/cli/liturgy.go b/internal/cli/liturgy.go
index 24052ab..b1a8874 100644
--- a/internal/cli/liturgy.go
+++ b/internal/cli/liturgy.go
@@ -23,16 +23,8 @@ func runLiturgy(cfg config.Config, date string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "lectio: bad date %q (want YYYY-MM-DD)\n", date)
return 2
}
- // The computed engine currently does the Ordinary Form only. Rather than
- // silently return OF for a traditional (EF) config, say so.
- if cfg.Selection().Form == "old" {
- fmt.Fprintln(stderr, "lectio: the offline calendar engine currently computes only the Ordinary Form.")
- fmt.Fprintln(stderr, " Traditional (1962 EF) computation is not built yet; your traditional daily")
- fmt.Fprintln(stderr, " readings still work via 'lectio [DATE]'. To compute the OF: add --lectionary new.")
- return 1
- }
dir, _ := config.CalendarsDir()
- layers, errs := caldata.Stack(dir, cfg.Use)
+ layers, errs := caldata.Stack(cfg.Selection().Form, dir, cfg.Use)
for _, e := range errs {
fmt.Fprintln(stderr, "lectio: warning:", e)
}
@@ -43,8 +35,12 @@ func runLiturgy(cfg config.Config, date string, stdout, stderr io.Writer) int {
func printLiturgicalDay(out io.Writer, cfg config.Config, day calendar.LiturgicalDay) {
fmt.Fprintf(out, "%s %s\n", day.Date.Format("2006-01-02"), day.Weekday.String())
- fmt.Fprintf(out, "season: %s (week %d · Sunday cycle %s · weekday cycle %s)\n",
- day.Season, day.Week, day.SundayCycle, day.WeekdayCycle)
+ if day.SundayCycle != "" { // OF carries the reading cycles; EF does not
+ fmt.Fprintf(out, "season: %s (week %d · Sunday cycle %s · weekday cycle %s)\n",
+ day.Season, day.Week, day.SundayCycle, day.WeekdayCycle)
+ } else {
+ fmt.Fprintf(out, "season: %s (week %d)\n", day.Season, day.Week)
+ }
fmt.Fprintf(out, "observed: %s [%s · %s]\n", celebrationName(cfg, day.Observed), rankLabel(day.Observed.Rank), day.Colour)
for _, o := range day.Others {
if n := celebrationName(cfg, o); n != "" {
@@ -78,7 +74,7 @@ func celebrationName(cfg config.Config, c calendar.Celebration) string {
if c.Slug == "" {
return "(feria)"
}
- return strings.ReplaceAll(c.Slug, "-", " ")
+ return strings.ReplaceAll(strings.TrimPrefix(c.Slug, "ef-"), "-", " ")
}
func rankLabel(r calendar.Rank) string {
@@ -91,6 +87,16 @@ func rankLabel(r calendar.Rank) string {
return "memorial"
case calendar.RankOptional:
return "optional memorial"
+ case calendar.RankClass1:
+ return "I class"
+ case calendar.RankClass2:
+ return "II class"
+ case calendar.RankClass3:
+ return "III class"
+ case calendar.RankClass4:
+ return "IV class"
+ case calendar.RankCommemoration:
+ return "commemoration"
default:
return "feria"
}
diff --git a/internal/cli/liturgy_test.go b/internal/cli/liturgy_test.go
index 02f02c4..c9d66a5 100644
--- a/internal/cli/liturgy_test.go
+++ b/internal/cli/liturgy_test.go
@@ -33,7 +33,7 @@ func TestRunLiturgy(t *testing.T) {
}
}
-func TestRunLiturgyTraditionalGuarded(t *testing.T) {
+func TestRunLiturgyTraditionalComputesEF(t *testing.T) {
dir := t.TempDir()
t.Setenv("LECTIO_CONFIG", filepath.Join(dir, "config.ini"))
os.WriteFile(filepath.Join(dir, "config.ini"), []byte("lectionary = traditional\n"), 0o644)
@@ -42,11 +42,16 @@ func TestRunLiturgyTraditionalGuarded(t *testing.T) {
t.Fatal(err)
}
var buf bytes.Buffer
- if code := runLiturgy(cfg, "2025-08-15", &buf, &buf); code == 0 {
- t.Fatalf("traditional config should be guarded, not computed as OF:\n%s", buf.String())
+ if code := runLiturgy(cfg, "2025-08-15", &buf, &buf); code != 0 {
+ t.Fatalf("EF compute failed (%d):\n%s", code, buf.String())
+ }
+ out := buf.String()
+ // EF: Assumption is I class, and 2025-08-15 is in Time after Pentecost (not OF "ordinary").
+ if !strings.Contains(out, "Assumption") || !strings.Contains(out, "I class") {
+ t.Errorf("expected EF Assumption (I class):\n%s", out)
}
- if !strings.Contains(strings.ToLower(buf.String()), "ordinary form") {
- t.Errorf("expected an EF-not-implemented notice:\n%s", buf.String())
+ if !strings.Contains(out, "time-after-pentecost") {
+ t.Errorf("expected EF season time-after-pentecost:\n%s", out)
}
}