diff options
Diffstat (limited to 'internal/calendar')
| -rw-r--r-- | internal/calendar/computus.go | 23 | ||||
| -rw-r--r-- | internal/calendar/computus_test.go | 20 | ||||
| -rw-r--r-- | internal/calendar/types.go | 146 | ||||
| -rw-r--r-- | internal/calendar/types_test.go | 15 |
4 files changed, 204 insertions, 0 deletions
diff --git a/internal/calendar/computus.go b/internal/calendar/computus.go new file mode 100644 index 0000000..13410cb --- /dev/null +++ b/internal/calendar/computus.go @@ -0,0 +1,23 @@ +package calendar + +import "time" + +// Easter returns Gregorian Easter Sunday (UTC midnight) via the Anonymous +// Gregorian algorithm (Meeus/Jones/Butcher). +func Easter(year int) time.Time { + a := year % 19 + b := year / 100 + c := year % 100 + d := b / 4 + e := b % 4 + f := (b + 8) / 25 + g := (b - f + 1) / 3 + h := (19*a + b - d - g + 15) % 30 + i := c / 4 + k := c % 4 + l := (32 + 2*e + 2*i - h - k) % 7 + m := (a + 11*h + 22*l) / 451 + month := (h + l - 7*m + 114) / 31 + day := ((h + l - 7*m + 114) % 31) + 1 + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) +} diff --git a/internal/calendar/computus_test.go b/internal/calendar/computus_test.go new file mode 100644 index 0000000..c524093 --- /dev/null +++ b/internal/calendar/computus_test.go @@ -0,0 +1,20 @@ +package calendar + +import ( + "testing" +) + +func TestEasterKnownDates(t *testing.T) { + // Published Gregorian Easter Sundays. + cases := map[int]string{ + 2020: "2020-04-12", 2021: "2021-04-04", 2022: "2022-04-17", + 2024: "2024-03-31", 2025: "2025-04-20", 2027: "2027-03-28", + 2038: "2038-04-25", 2000: "2000-04-23", + } + for y, want := range cases { + got := Easter(y).Format("2006-01-02") + if got != want { + t.Errorf("Easter(%d) = %s, want %s", y, got, want) + } + } +} diff --git a/internal/calendar/types.go b/internal/calendar/types.go new file mode 100644 index 0000000..f481441 --- /dev/null +++ b/internal/calendar/types.go @@ -0,0 +1,146 @@ +// Package calendar computes the Ordinary Form liturgical day for any Gregorian +// date, offline and from first principles. It imports only the standard library. +package calendar + +import "time" + +type Rank int + +const ( + RankFerial Rank = iota // ordinary weekday + RankOptional // optional memorial + RankMemorial // obligatory memorial + RankFeast + RankSolemnity +) + +type Class int + +const ( + ClassNone Class = iota // temporal / ferial + ClassSaint // a saint + ClassBVM // the Blessed Virgin Mary + ClassLord // the Lord +) + +type Colour string + +const ( + White Colour = "white" + Red Colour = "red" + Green Colour = "green" + Violet Colour = "violet" + Rose Colour = "rose" + Black Colour = "black" +) + +type Season string + +const ( + Advent Season = "advent" + Christmas Season = "christmas" + Lent Season = "lent" + Triduum Season = "triduum" + Easter_ Season = "easter" // trailing underscore: Easter is the func name + Ordinary Season = "ordinary" +) + +type Reading struct{ Part, Citation string } + +type Mass struct { + Variant string // "" = the day Mass; e.g. "vigil" + Readings []Reading +} + +// DateSpec is a celebration's raw date expression, resolved by resolveDate: +// "MM-DD" (fixed) | "easter±N" | "christmas±N" | "sunday-after MM-DD". +type DateSpec string + +// RawCelebration is a celebration's fields exactly as parsed from a layer, +// before typing/merging. Fields are raw INI values keyed by INI key +// (e.g. "rank", "name.pl"); Variants holds "[slug/variant]" sub-sections. +type RawCelebration struct { + Fields map[string]string + Variants map[string]map[string]string +} + +// Celebration is the typed, built form used in results. +type Celebration struct { + Slug string + Name map[string]string // lang -> name + Rank Rank + Class Class + Colour Colour + Date DateSpec + Masses []Mass + Layer string // provenance +} + +// Layer is one calendar layer (the embedded universal base, or later an +// override file). Cels is keyed by slug; insertion order is not significant. +type Layer struct { + ID, Name, Type string + Cels map[string]RawCelebration +} + +// Selection carries the config choices that steer computation. +type Selection struct { + Form string // "new" (OF). "old" reserved. + Epiphany string // "fixed" | "sunday" + Ascension string // "thursday" | "sunday" + CorpusChristi string // "thursday" | "sunday" +} + +// DefaultSelection is the Universal Roman Calendar default. +func DefaultSelection() Selection { + return Selection{Form: "new", Epiphany: "fixed", Ascension: "thursday", CorpusChristi: "thursday"} +} + +type LiturgicalDay struct { + Date time.Time + Season Season + Week int + Weekday time.Weekday + Observed Celebration + Others []Celebration + Colour Colour + SundayCycle string // "A" | "B" | "C" + WeekdayCycle string // "I" | "II" +} + +func ParseRank(s string) Rank { + switch s { + case "solemnity": + return RankSolemnity + case "feast": + return RankFeast + case "memorial": + return RankMemorial + case "optional": + return RankOptional + default: + return RankFerial + } +} + +func ParseClass(s string) Class { + switch s { + case "lord": + return ClassLord + case "bvm": + return ClassBVM + case "saint": + return ClassSaint + default: + return ClassNone + } +} + +func ParseColour(s string) Colour { + switch Colour(s) { + case White, Red, Green, Violet, Rose, Black: + return Colour(s) + default: + return "" + } +} diff --git a/internal/calendar/types_test.go b/internal/calendar/types_test.go new file mode 100644 index 0000000..c09b53e --- /dev/null +++ b/internal/calendar/types_test.go @@ -0,0 +1,15 @@ +package calendar + +import "testing" + +func TestParseEnums(t *testing.T) { + if ParseRank("feast") != RankFeast || ParseRank("nonsense") != RankFerial { + t.Error("ParseRank") + } + if ParseClass("bvm") != ClassBVM || ParseClass("") != ClassNone { + t.Error("ParseClass") + } + if ParseColour("violet") != Violet || ParseColour("chartreuse") != "" { + t.Error("ParseColour") + } +} |
