From bec7c034aca15537900d74a7063d85c0f4deacae Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 27 Jul 2026 21:30:56 +0200 Subject: feat(calfeed): shared day builder + reusable reading resolver --- internal/caldata/readings.go | 31 +++++++++++++++++++ internal/calfeed/build.go | 68 ++++++++++++++++++++++++++++++++++++++++++ internal/calfeed/build_test.go | 46 ++++++++++++++++++++++++++++ internal/cli/liturgy.go | 24 +++------------ 4 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 internal/caldata/readings.go create mode 100644 internal/calfeed/build.go create mode 100644 internal/calfeed/build_test.go diff --git a/internal/caldata/readings.go b/internal/caldata/readings.go new file mode 100644 index 0000000..a11235d --- /dev/null +++ b/internal/caldata/readings.go @@ -0,0 +1,31 @@ +package caldata + +import ( + "time" + + "github.com/lukaszkasprzak/lectio/internal/calendar" +) + +// Readings resolves a day's Mass readings: the observed celebration's inline +// propers, else the temporal lectionary for its slug, else -- for an EF +// weekday with no proper Mass -- the preceding Sunday's Mass (the 1962 rule +// that green-season ferias repeat the Sunday). Shared by the CLI (cli.dayReadings +// delegates here) and calfeed.Build (injected as its readings func). +func Readings(sel calendar.Selection, layers []calendar.Layer, date time.Time, day calendar.LiturgicalDay) []calendar.Reading { + var rs []calendar.Reading + for _, m := range day.Observed.Masses { + rs = append(rs, m.Readings...) + } + if len(rs) > 0 { + return rs + } + if r := TemporalReadings(sel.Form, day.Observed.Slug); len(r) > 0 { + return r + } + if sel.Form == "old" && day.Observed.Layer == "temporal" && date.Weekday() != time.Sunday { + sun := date.AddDate(0, 0, -int(date.Weekday())) // preceding Sunday (Sunday = 0) + sd := calendar.Compute(sun, sel, layers) + return TemporalReadings(sel.Form, sd.Observed.Slug) + } + return nil +} diff --git a/internal/calfeed/build.go b/internal/calfeed/build.go new file mode 100644 index 0000000..d3962aa --- /dev/null +++ b/internal/calfeed/build.go @@ -0,0 +1,68 @@ +package calfeed + +import ( + "strings" + "time" + + "github.com/lukaszkasprzak/lectio/internal/calendar" +) + +// Build computes the day list from..to inclusive (calendar.Compute per date) +// and maps it to the wire model. readings is injected -- the caller passes +// caldata.Readings -- so calfeed never imports internal/caldata. +func Build(from, to time.Time, uiLang string, sel calendar.Selection, layers []calendar.Layer, readings func(date time.Time, day calendar.LiturgicalDay) []calendar.Reading) []DayView { + var days []DayView + for d := from; !d.After(to); d = d.AddDate(0, 0, 1) { + day := calendar.Compute(d, sel, layers) + + others := make([]CelView, 0, len(day.Others)) + for _, o := range day.Others { + others = append(others, celView(uiLang, o)) + } + + var rs []ReadingView + for _, r := range readings(d, day) { + rs = append(rs, ReadingView{Part: r.Part, Citation: r.Citation}) + } + + days = append(days, DayView{ + Date: d.Format("2006-01-02"), + Season: string(day.Season), + Week: day.Week, + Weekday: day.Weekday.String(), + Colour: string(day.Colour), + Observed: celView(uiLang, day.Observed), + Others: others, + Cycles: Cycles{Sunday: day.SundayCycle, Weekday: day.WeekdayCycle}, + Readings: rs, + }) + } + return days +} + +// celView maps a calendar.Celebration to its wire view. +func celView(uiLang string, c calendar.Celebration) CelView { + return CelView{ + Slug: c.Slug, + Name: celebrationName(uiLang, c), + Rank: string(c.Rank), + Class: int(c.Class), + } +} + +// celebrationName resolves c's name in uiLang, falling back to English, then +// a humanized slug (with the "ef-" prefix stripped and dashes turned to +// spaces), then "(feria)" for an unnamed temporal day. Mirrors +// cli.celebrationName. +func celebrationName(uiLang string, c calendar.Celebration) string { + if n := c.Name[uiLang]; n != "" { + return n + } + if n := c.Name["en"]; n != "" { + return n + } + if c.Slug == "" { + return "(feria)" + } + return strings.ReplaceAll(strings.TrimPrefix(c.Slug, "ef-"), "-", " ") +} diff --git a/internal/calfeed/build_test.go b/internal/calfeed/build_test.go new file mode 100644 index 0000000..333a51b --- /dev/null +++ b/internal/calfeed/build_test.go @@ -0,0 +1,46 @@ +package calfeed + +import ( + "testing" + "time" + + "github.com/lukaszkasprzak/lectio/internal/caldata" + "github.com/lukaszkasprzak/lectio/internal/calendar" +) + +func TestBuildThreeDayRange(t *testing.T) { + sel := calendar.DefaultSelection() + sel.Form = "old" + layers := []calendar.Layer{caldata.Tridentine()} + + from := time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC) + to := time.Date(2026, 1, 7, 0, 0, 0, 0, time.UTC) + days := Build(from, to, "en", sel, layers, func(date time.Time, day calendar.LiturgicalDay) []calendar.Reading { + return caldata.Readings(sel, layers, date, day) + }) + + if len(days) != 3 { + t.Fatalf("len(days) = %d, want 3", len(days)) + } + + wantDates := []string{"2026-01-05", "2026-01-06", "2026-01-07"} + seenWeekday := false + feastFound := false + for i, d := range days { + if d.Date != wantDates[i] { + t.Errorf("day %d: date = %q, want %q", i, d.Date, wantDates[i]) + } + if d.Weekday != "" { + seenWeekday = true + } + if d.Date == "2026-01-06" && d.Observed.Name != "" { + feastFound = true + } + } + if !seenWeekday { + t.Errorf("expected weekday strings to be set: %+v", days) + } + if !feastFound { + t.Errorf("expected Epiphany (2026-01-06) to have a non-empty Observed.Name: %+v", days) + } +} diff --git a/internal/cli/liturgy.go b/internal/cli/liturgy.go index ced23ae..5b6036e 100644 --- a/internal/cli/liturgy.go +++ b/internal/cli/liturgy.go @@ -38,27 +38,11 @@ func runLiturgy(cfg config.Config, date string, stdout, stderr io.Writer) int { return 0 } -// dayReadings resolves the day's Mass readings: the observed celebration's -// inline propers, else the temporal lectionary for its slug, else -- for an EF -// weekday with no proper Mass -- the preceding Sunday's Mass (the 1962 rule -// that green-season ferias repeat the Sunday). +// dayReadings resolves the day's Mass readings; the logic lives in +// caldata.Readings (shared with calfeed.Build) so it stays reusable outside +// the CLI. func dayReadings(sel calendar.Selection, layers []calendar.Layer, date time.Time, day calendar.LiturgicalDay) []calendar.Reading { - var rs []calendar.Reading - for _, m := range day.Observed.Masses { - rs = append(rs, m.Readings...) - } - if len(rs) > 0 { - return rs - } - if r := caldata.TemporalReadings(sel.Form, day.Observed.Slug); len(r) > 0 { - return r - } - if sel.Form == "old" && day.Observed.Layer == "temporal" && date.Weekday() != time.Sunday { - sun := date.AddDate(0, 0, -int(date.Weekday())) // preceding Sunday (Sunday = 0) - sd := calendar.Compute(sun, sel, layers) - return caldata.TemporalReadings(sel.Form, sd.Observed.Slug) - } - return nil + return caldata.Readings(sel, layers, date, day) } // vernacularVersion is the corpus that renders reading text: the config's -- cgit v1.3