aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar
diff options
context:
space:
mode:
Diffstat (limited to 'internal/calendar')
-rw-r--r--internal/calendar/calendar.go76
-rw-r--r--internal/calendar/precedence_ef_repro_test.go22
2 files changed, 86 insertions, 12 deletions
diff --git a/internal/calendar/calendar.go b/internal/calendar/calendar.go
index 33706d8..f364321 100644
--- a/internal/calendar/calendar.go
+++ b/internal/calendar/calendar.go
@@ -35,14 +35,43 @@ func TemporalSlug(date time.Time, sel Selection) string {
func computeEF(date time.Time, sel Selection, layers []Layer) LiturgicalDay {
td := temporalEF(date)
merged := mergeLayers(layers)
- cands := []candidate{{Cel: td.Cel, Temporal: true, Season: td.Season}}
+ year := date.Year()
+ // occupiedByRank reports whether some OTHER fixed-date sanctoral
+ // celebration whose rank passes `allowed` resolves onto d this year.
+ // transferIfImpededEF uses this at two different thresholds: class 1
+ // only, to decide whether a candidate is impeded in the first place (a
+ // class-2 occupant never impedes a class-1 feast -- class 1 always beats
+ // class 2 outright, no tie exists); and class 1 OR 2, for RG 96's "next
+ // day that is not I or II class" once a transfer is already under way
+ // (e.g. the Visitation, 2 July, blocking the Precious Blood's transfer
+ // off 1 July in 2011).
+ occupiedByRank := func(d time.Time, exceptSlug string, allowed func(Rank) bool) bool {
+ for slug2, rc2 := range merged {
+ if slug2 == exceptSlug {
+ continue
+ }
+ cel2 := buildCelebration(slug2, rc2)
+ if !allowed(cel2.Rank) {
+ continue
+ }
+ if when2, ok := celebrationDate(cel2, year, sel); ok && sameDay(when2, d) {
+ return true
+ }
+ }
+ return false
+ }
+ isClass1 := func(r Rank) bool { return r == RankClass1 }
+ isClass1Or2 := func(r Rank) bool { return r == RankClass1 || r == RankClass2 }
+ cands := []candidate{{Cel: td.Cel, Temporal: true, Season: td.Season, Sunday: td.Sunday}}
for slug, rc := range merged {
cel := buildCelebration(slug, rc)
- when, ok := celebrationDate(cel, date.Year(), sel)
+ when, ok := celebrationDate(cel, year, sel)
if !ok {
continue
}
- effective := transferIfImpededEF(cel, when)
+ effective := transferIfImpededEF(cel, when,
+ func(d time.Time) bool { return occupiedByRank(d, cel.Slug, isClass1) },
+ func(d time.Time) bool { return occupiedByRank(d, cel.Slug, isClass1Or2) })
if sameDay(effective, date) {
cands = append(cands, candidate{Cel: cel, Temporal: false, Season: td.Season})
}
@@ -199,12 +228,29 @@ func transferIfImpeded(cel Celebration, when time.Time, sel Selection) time.Time
// transferIfImpededEF moves an EF sanctoral feast off a date it cannot be kept
// on. Two rules cover the 1962 cases: All Souls, when Nov 2 is a Sunday, is
-// transferred to the Monday (Nov 3); and a I class feast impeded by a I class
-// temporal day (Holy Week, the Easter octave) is pushed forward to the next free
-// day — for the Annunciation in Holy Week this lands on the Monday after Low
-// Sunday, past the whole privileged octave. Lower-rank feasts are only
-// commemorated, never transferred.
-func transferIfImpededEF(cel Celebration, when time.Time) time.Time {
+// transferred to the Monday (Nov 3); and a I class feast genuinely impeded is
+// pushed forward to the next day that is itself neither I nor II class (RG
+// 96: "the next following day that is not I or II class"). Lower-rank feasts
+// are only commemorated, never transferred (RG 95).
+//
+// occupiedByClass1 and occupiedByClass1Or2 are deliberately different
+// thresholds, not the same check reused twice:
+// - a candidate is impeded IN THE FIRST PLACE only by a genuine class-1
+// collision -- the temporal office of `when` is itself I class (a Sunday
+// of Advent/Lent/Passiontide, Holy Week, a named I-class feast), or
+// another FIXED I-class sanctoral feast already sits on `when` (RG
+// 97/98). A II-class occupant, temporal or sanctoral, never impedes a
+// I-class feast at all -- I class always outranks II class outright, no
+// tie-break is even reached -- so using the wider I-OR-II threshold here
+// would (and, before this fix, did: e.g. All Saints, 1 Nov, was wrongly
+// bumped to 3 Nov merely for landing on an ordinary II-class Sunday it
+// would have won outright) send an unimpeded I-class feast on an
+// unnecessary walk.
+// - once a transfer is under way, the DESTINATION must avoid landing on
+// ANY I- or II-class day (RG 96's own wider "not I or II class" text) --
+// e.g. the Visitation, 2 July, blocks the Precious Blood's transfer off 1
+// July in 2011, and 3 July's ordinary Sunday blocks it a second time.
+func transferIfImpededEF(cel Celebration, when time.Time, occupiedByClass1, occupiedByClass1Or2 func(time.Time) bool) time.Time {
if when.Month() == time.November && when.Day() == 2 && when.Weekday() == time.Sunday &&
strings.Contains(cel.Slug, "all-souls") {
return when.AddDate(0, 0, 1)
@@ -212,11 +258,17 @@ func transferIfImpededEF(cel Celebration, when time.Time) time.Time {
if cel.Rank != RankClass1 {
return when
}
- day := when
+ startTemporal := temporalEF(when)
+ startImpeded := startTemporal.Cel.Rank == RankClass1 || occupiedByClass1(when)
+ if !startImpeded {
+ return when
+ }
+ day := when.AddDate(0, 0, 1)
for i := 0; i < 30; i++ {
b := temporalEF(day)
- if precedenceEF(candidate{Cel: b.Cel, Temporal: true, Season: b.Season}) <= 2 {
- day = day.AddDate(0, 0, 1) // impeded by a I class temporal day; push forward
+ band := precedenceEF(candidate{Cel: b.Cel, Temporal: true, Season: b.Season, Sunday: b.Sunday})
+ if band <= 3 || occupiedByClass1Or2(day) {
+ day = day.AddDate(0, 0, 1) // still I- or II-class; keep walking
continue
}
return day
diff --git a/internal/calendar/precedence_ef_repro_test.go b/internal/calendar/precedence_ef_repro_test.go
index 6c333d0..1f38996 100644
--- a/internal/calendar/precedence_ef_repro_test.go
+++ b/internal/calendar/precedence_ef_repro_test.go
@@ -43,3 +43,25 @@ func TestJosephYieldsToSundayOfLent(t *testing.T) {
}
}
}
+
+// TestTransferSkipsBothIAndIIClass: RG 96 -- an impeded I-class feast
+// transfers to the next day that is NOT ITSELF I OR II CLASS (not merely "not
+// I class"). 2011: the Sacred Heart (Friday after the Corpus Christi octave)
+// falls on 1 July and impedes the Precious Blood (also 1 July, fixed). 2
+// July is the Visitation (II class, fixed); 3 July is an ordinary II-class
+// Sunday. Both must be skipped; the Precious Blood lands on 4 July, and the
+// Visitation is observed, undisplaced, on its own day.
+func TestTransferSkipsBothIAndIIClass(t *testing.T) {
+ if got := efCompute("2011-07-01").Observed.Slug; got != "ef-sacred-heart" {
+ t.Fatalf("2011-07-01 observed = %q want ef-sacred-heart", got)
+ }
+ if got := efCompute("2011-07-02").Observed.Slug; got != "visitation-of-the-blessed-virgin-mary" {
+ t.Errorf("2011-07-02 observed = %q want visitation-of-the-blessed-virgin-mary (RG 96: the Precious Blood must skip past it, not displace it)", got)
+ }
+ if got := efCompute("2011-07-03").Observed.Slug; got != "ef-time-after-pentecost-sunday-3" {
+ t.Errorf("2011-07-03 observed = %q want ef-time-after-pentecost-sunday-3 (an ordinary II-class Sunday also blocks a I-class transfer)", got)
+ }
+ if got := efCompute("2011-07-04").Observed.Slug; got != "precious-blood-of-our-lord-jesus-christ" {
+ t.Errorf("2011-07-04 observed = %q want precious-blood-of-our-lord-jesus-christ (RG 96: first day that is neither I nor II class)", got)
+ }
+}