diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 12:18:01 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 12:18:01 +0200 |
| commit | 3677ad490c9f18020d68349c9e3a71bcf389178f (patch) | |
| tree | 3a4b135ca139a2acca15ac0f9f5cf851d88a45dd /internal/calendar/temporal.go | |
| parent | 7e2d958f4855a92b8f349e553979e098fdb0a04c (diff) | |
| download | lectio-3677ad490c9f18020d68349c9e3a71bcf389178f.tar.gz lectio-3677ad490c9f18020d68349c9e3a71bcf389178f.zip | |
feat(calendar): temporal cycle (seasons, Sundays, movable solemnities)
Diffstat (limited to 'internal/calendar/temporal.go')
| -rw-r--r-- | internal/calendar/temporal.go | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/internal/calendar/temporal.go b/internal/calendar/temporal.go new file mode 100644 index 0000000..011e023 --- /dev/null +++ b/internal/calendar/temporal.go @@ -0,0 +1,241 @@ +package calendar + +import ( + "strconv" + "time" +) + +// temporalDay is the temporal (season/movable) identity of a date, before any +// sanctoral celebration is considered. Privileged marks band 1-2 days (Sundays +// of Advent/Lent/Easter, Holy Week, Triduum, Easter octave, the great +// solemnities); PrivFeria marks band-9 privileged ferias (Advent 17-24, +// Lenten weekdays); Sunday marks an Ordinary/Christmas Sunday (band 6). +type temporalDay struct { + Season Season + Week int + Colour Colour + Cel Celebration + Rank Rank + Class Class + Privileged bool + PrivFeria bool + Sunday bool +} + +// adventStart returns the First Sunday of Advent that opens the liturgical year +// containing Christmas of the given calendar year: the 4th Sunday before Dec 25. +func adventStart(year int) time.Time { + xmas := time.Date(year, 12, 25, 0, 0, 0, 0, time.UTC) + sun := xmas.AddDate(0, 0, -int(xmas.Weekday())) // Sunday on/before Christmas (Sunday=0) + return sun.AddDate(0, 0, -21) +} + +func sameDay(a, b time.Time) bool { + return a.Year() == b.Year() && a.YearDay() == b.YearDay() +} + +// daysBetween is the whole-day count from..to (both UTC midnight). +func daysBetween(from, to time.Time) int { + return int(to.Sub(from).Hours()/24 + 0.5) +} + +func weekdayAbbr(wd time.Weekday) string { + return [...]string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}[wd] +} + +// epiphany is fixed Jan 6, or (sel.Epiphany=="sunday") the Sunday between Jan 2 and Jan 8. +func epiphany(year int, sel Selection) time.Time { + if sel.Epiphany == "sunday" { + return nextWeekday(time.Date(year, 1, 2, 0, 0, 0, 0, time.UTC), time.Sunday) + } + return time.Date(year, 1, 6, 0, 0, 0, 0, time.UTC) +} + +// baptismOfLord is the Sunday after Epiphany; when Epiphany (transferred to a +// Sunday) is Jan 7 or 8, Baptism is the next day. +func baptismOfLord(year int, sel Selection) time.Time { + epi := epiphany(year, sel) + if sel.Epiphany == "sunday" && (epi.Day() == 7 || epi.Day() == 8) { + return epi.AddDate(0, 0, 1) + } + return nextWeekday(epi.AddDate(0, 0, 1), time.Sunday) +} + +// holyFamily is the Sunday within the Christmas octave (Dec 26-31), or Dec 30 +// when Christmas itself is a Sunday. +func holyFamily(year int) time.Time { + xmas := time.Date(year, 12, 25, 0, 0, 0, 0, time.UTC) + if xmas.Weekday() == time.Sunday { + return time.Date(year, 12, 30, 0, 0, 0, 0, time.UTC) + } + return nextWeekday(xmas.AddDate(0, 0, 1), time.Sunday) +} + +// ascension: Thursday (easter+39) or, where transferred, the 7th Sunday of Easter (easter+42). +func ascension(y int, sel Selection) time.Time { + e := Easter(y) + if sel.Ascension == "sunday" { + return e.AddDate(0, 0, 42) + } + return e.AddDate(0, 0, 39) +} + +// corpusChristi: Thursday (easter+60) or, where transferred, Sunday (easter+63). +func corpusChristi(y int, sel Selection) time.Time { + e := Easter(y) + if sel.CorpusChristi == "sunday" { + return e.AddDate(0, 0, 63) + } + return e.AddDate(0, 0, 60) +} + +// --- small constructors --- + +func solemn(season Season, slug string, col Colour, privileged bool) temporalDay { + return temporalDay{Season: season, Colour: col, Rank: RankSolemnity, Class: ClassLord, Privileged: privileged, + Cel: Celebration{Slug: slug, Rank: RankSolemnity, Class: ClassLord, Colour: col, Layer: "temporal"}} +} + +func feastOfLord(season Season, slug string, col Colour) temporalDay { + return temporalDay{Season: season, Colour: col, Rank: RankFeast, Class: ClassLord, + Cel: Celebration{Slug: slug, Rank: RankFeast, Class: ClassLord, Colour: col, Layer: "temporal"}} +} + +func sundayDay(season Season, slug string, col Colour, week int) temporalDay { + priv := season == Advent || season == Lent || season == Easter_ + td := temporalDay{Season: season, Colour: col, Week: week, Rank: RankSolemnity, Privileged: priv, Sunday: !priv, + Cel: Celebration{Slug: slug, Rank: RankSolemnity, Colour: col, Layer: "temporal"}} + return td +} + +func ferialDay(season Season, slug string, col Colour, week int, privFeria bool) temporalDay { + return temporalDay{Season: season, Colour: col, Week: week, Rank: RankFerial, PrivFeria: privFeria, + Cel: Celebration{Slug: slug, Rank: RankFerial, Colour: col, Layer: "temporal"}} +} + +// temporal computes the temporal identity of date under the given Selection. +func temporal(date time.Time, sel Selection) temporalDay { + date = date.UTC().Truncate(24 * time.Hour) + y := date.Year() + easter := Easter(y) + ashWed := easter.AddDate(0, 0, -46) + palmSunday := easter.AddDate(0, 0, -7) + holyThu := easter.AddDate(0, 0, -3) + pentecost := easter.AddDate(0, 0, 49) + adventThis := adventStart(y) + christmasThis := time.Date(y, 12, 25, 0, 0, 0, 0, time.UTC) + baptismThis := baptismOfLord(y, sel) + christTheKing := adventThis.AddDate(0, 0, -7) + wd := date.Weekday() + sun := wd == time.Sunday + + // 1. Movable / dated solemnities and feasts (highest identity first). + switch { + case sameDay(date, easter): + return solemn(Easter_, "easter-sunday", White, true) + case date.After(easter) && !date.After(easter.AddDate(0, 0, 7)): + // Octave of Easter (Mon..Sun); easter+7 = 2nd Sunday of Easter. + td := solemn(Easter_, "easter-octave-"+weekdayAbbr(wd), White, true) + td.Week = 1 + return td + case sameDay(date, ascension(y, sel)): + return solemn(Easter_, "ascension", White, true) + case sameDay(date, pentecost): + return solemn(Easter_, "pentecost", Red, true) + case sameDay(date, easter.AddDate(0, 0, 56)): + return solemn(Ordinary, "trinity-sunday", White, false) + case sameDay(date, corpusChristi(y, sel)): + return solemn(Ordinary, "corpus-christi", White, false) + case sameDay(date, easter.AddDate(0, 0, 68)): + return solemn(Ordinary, "sacred-heart", White, false) + case sameDay(date, christTheKing): + return solemn(Ordinary, "christ-the-king", White, false) + case sameDay(date, epiphany(y, sel)): + return solemn(Christmas, "epiphany", White, true) + case sameDay(date, baptismThis): + return feastOfLord(Christmas, "baptism-of-the-lord", White) + case sameDay(date, holyFamily(y)): + return feastOfLord(Christmas, "holy-family", White) + } + + // 2. Seasons by span (chronological within the calendar year). + switch { + case !date.Before(holyThu) && date.Before(easter): // Triduum: Thu, Fri, Sat + col := White + if wd == time.Friday { + col = Red + } else if wd == time.Saturday { + col = Violet + } + td := solemn(Triduum, "triduum-"+weekdayAbbr(wd), col, true) + return td + + case !date.Before(palmSunday) && date.Before(holyThu): // Palm Sunday + Holy Week Mon-Wed + if sun { + return sundayDay(Lent, "palm-sunday", Red, 6) + } + return ferialDay(Lent, "holy-week-"+weekdayAbbr(wd), Violet, 6, true) + + case !date.Before(ashWed) && date.Before(palmSunday): // Lent + if !date.Before(ashWed) && date.Before(ashWed.AddDate(0, 0, 4)) { + // Ash Wednesday through the Saturday after (before Lent I). + return ferialDay(Lent, "lent-after-ashes-"+weekdayAbbr(wd), Violet, 0, true) + } + lent1 := nextWeekday(ashWed, time.Sunday) + week := daysBetween(lent1, date)/7 + 1 + col := Violet + if week == 4 { + col = Rose // Laetare + } + if sun { + return sundayDay(Lent, "lent-sunday-"+itoa(week), col, week) + } + return ferialDay(Lent, "lent-"+itoa(week)+"-"+weekdayAbbr(wd), Violet, week, true) + + case !date.Before(easter) && !date.After(pentecost): // Easter season + week := daysBetween(easter, date)/7 + 1 + if sun { + return sundayDay(Easter_, "easter-sunday-"+itoa(week), White, week) + } + return ferialDay(Easter_, "easter-"+itoa(week)+"-"+weekdayAbbr(wd), White, week, false) + + case !date.Before(adventThis) && date.Before(christmasThis): // Advent + week := daysBetween(adventThis, date)/7 + 1 + col := Violet + if week == 3 { + col = Rose // Gaudete + } + privFeria := date.Month() == time.December && date.Day() >= 17 + if sun { + return sundayDay(Advent, "advent-sunday-"+itoa(week), col, week) + } + return ferialDay(Advent, "advent-"+itoa(week)+"-"+weekdayAbbr(wd), Violet, week, privFeria) + + case !date.Before(christmasThis): // Dec 25..Dec 31 — Christmas octave + return ferialDay(Christmas, "christmas-octave-"+weekdayAbbr(wd), White, 0, false) + + case !date.After(baptismThis): // Jan 1..Baptism — Christmas season + if sun { + return sundayDay(Christmas, "christmas-sunday-"+weekdayAbbr(wd), White, 0) + } + return ferialDay(Christmas, "christmas-"+weekdayAbbr(wd), White, 0, false) + + default: // Ordinary Time (span 1: after Baptism..before Lent; span 2: after Pentecost..before Advent) + var week int + if date.Before(ashWed) { + week = daysBetween(baptismThis, date)/7 + 1 + } else { + // count back from Christ the King = the 34th week. + week = 34 - daysBetween(date, christTheKing)/7 + } + if week < 1 { + week = 1 + } + if sun { + return sundayDay(Ordinary, "ordinary-sunday-"+itoa(week), Green, week) + } + return ferialDay(Ordinary, "ordinary-"+itoa(week)+"-"+weekdayAbbr(wd), Green, week, false) + } +} + +func itoa(n int) string { return strconv.Itoa(n) } |
