package calendar import ( "sort" "strings" "time" ) // Compute returns the Ordinary Form liturgical day for date under the given // Selection, with the ordered layer stack supplying celebrations. It is pure // and total: any valid Gregorian date yields a LiturgicalDay. func Compute(date time.Time, sel Selection, layers []Layer) LiturgicalDay { date = date.UTC().Truncate(24 * time.Hour) td := temporal(date, sel) merged := mergeLayers(layers) cands := []candidate{{ Cel: td.Cel, Temporal: true, Privileged: td.Privileged, PrivFeria: td.PrivFeria, Sunday: td.Sunday, Season: td.Season, }} for slug, rc := range merged { cel := buildCelebration(slug, rc) when, ok := celebrationDate(cel, date.Year(), sel) if !ok { continue } // On band-2 privileged days only solemnities survive (and they transfer); // on Sundays (band 6) only feasts and solemnities compete. if td.Privileged && ofRankOrder(cel.Rank) < ofRankOrder(RankSolemnity) { continue } if td.Sunday && ofRankOrder(cel.Rank) < ofRankOrder(RankFeast) { continue } effective := transferIfImpeded(cel, when, sel) if sameDay(effective, date) { cands = append(cands, candidate{Cel: cel, Temporal: false, Season: td.Season}) } } obs, others := pick(cands) day := LiturgicalDay{ Date: date, Season: td.Season, Week: td.Week, Weekday: date.Weekday(), Observed: obs.Cel, Colour: colourOf(obs, td), ObservedBand: precedence(obs), SundayCycle: sundayCycle(liturgicalYearStart(date)), WeekdayCycle: weekdayCycle(liturgicalYearStart(date).Year() + 1), } for _, o := range others { if o.Temporal { continue // a superseded ferial/Sunday is not a commemoration } day.Others = append(day.Others, o.Cel) } return day } func colourOf(obs candidate, td temporalDay) Colour { if obs.Cel.Colour != "" { return obs.Cel.Colour } return td.Colour } // celebrationDate resolves a sanctoral celebration's date for the year. func celebrationDate(cel Celebration, year int, sel Selection) (time.Time, bool) { return resolveDate(cel.Date, year, Easter(year)) } // transferIfImpeded moves a solemnity off a band-1/2 temporal day to the next // free day. Non-solemnities are never transferred (they yield instead). func transferIfImpeded(cel Celebration, when time.Time, sel Selection) time.Time { if cel.Rank != RankSolemnity { return when } day := when for i := 0; i < 30; i++ { b := temporal(day, sel) band := precedence(candidate{ Cel: b.Cel, Temporal: true, Privileged: b.Privileged, PrivFeria: b.PrivFeria, Sunday: b.Sunday, Season: b.Season, }) if band <= 2 { // impeded by Triduum / a privileged day: push forward day = day.AddDate(0, 0, 1) continue } return day } return day } // buildCelebration types a RawCelebration. func buildCelebration(slug string, rc RawCelebration) Celebration { c := Celebration{Slug: strings.TrimSpace(slug), Name: map[string]string{}, Layer: rc.Fields["layer"]} for k, v := range rc.Fields { switch { case k == "rank": c.Rank = ParseRank(v) case k == "class": c.Class = ParseClass(v) case k == "colour": c.Colour = ParseColour(v) case k == "date": c.Date = DateSpec(v) case strings.HasPrefix(k, "name."): c.Name[strings.TrimPrefix(k, "name.")] = v } } if c.Rank == "" { c.Rank = RankFerial } if m := massFrom(rc.Fields); len(m.Readings) > 0 { c.Masses = append(c.Masses, m) } var variants []string for v := range rc.Variants { variants = append(variants, v) } sort.Strings(variants) for _, v := range variants { mm := massFrom(rc.Variants[v]) mm.Variant = v c.Masses = append(c.Masses, mm) } return c } // BuildCelebrationForTest exposes buildCelebration for cross-package data tests. func BuildCelebrationForTest(slug string, rc RawCelebration) Celebration { return buildCelebration(slug, rc) } var readingParts = []string{"first", "psalm", "second", "acclamation", "gospel"} func massFrom(fields map[string]string) Mass { var m Mass for _, part := range readingParts { if cit := fields["reading."+part]; cit != "" { m.Readings = append(m.Readings, Reading{Part: part, Citation: cit}) } } return m } // liturgicalYearStart returns the First Sunday of Advent that opened the // liturgical year containing date. func liturgicalYearStart(date time.Time) time.Time { a := adventStart(date.Year()) if date.Before(a) { return adventStart(date.Year() - 1) } return a } // sundayCycle: the liturgical year's civil year (Advent year + 1) mod 3 → // C(0), A(1), B(2). func sundayCycle(start time.Time) string { switch (start.Year() + 1) % 3 { case 0: return "C" case 1: return "A" default: return "B" } } // weekdayCycle: odd liturgical-year civil year → I, even → II. func weekdayCycle(year int) string { if year%2 == 1 { return "I" } return "II" }