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 of Advent is // the Sunday on or before Christmas Eve (Dec 24) — anchoring on Dec 24, not // Dec 25, keeps Christmas Day itself from being counted as an Advent Sunday // (which loses a week in years when Dec 25 is a Sunday). Advent I is 3 Sundays // earlier. func adventStart(year int) time.Time { eve := time.Date(year, 12, 24, 0, 0, 0, 0, time.UTC) sun := eve.AddDate(0, 0, -int(eve.Weekday())) // 4th Sunday of Advent return sun.AddDate(0, 0, -21) // 1st Sunday of Advent } 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"}} } // privFerialDay builds a band-2 privileged weekday (Ash Wednesday, Holy Week // Monday-Wednesday): a ferial no sanctoral feast or memorial can displace (only // a solemnity may, and solemnities transfer off it -- Table of Liturgical Days // #2). Distinct from ferialDay's PrivFeria (band 9) Lenten/Advent weekdays. func privFerialDay(season Season, slug string, col Colour, week int) temporalDay { return temporalDay{Season: season, Colour: col, Week: week, Rank: RankFerial, Privileged: true, 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, christmasThis): return solemn(Christmas, "christmas", White, true) 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) } // Holy Week Mon-Wed are band-2 privileged: no feast/memorial is kept. return privFerialDay(Lent, "holy-week-"+weekdayAbbr(wd), Violet, 6) case !date.Before(ashWed) && date.Before(palmSunday): // Lent if sameDay(date, ashWed) { // Ash Wednesday is band-2 privileged (suppresses any coinciding feast). return privFerialDay(Lent, "lent-after-ashes-wed", Violet, 0) } if date.Before(ashWed.AddDate(0, 0, 4)) { // Thursday-Saturday after Ash Wednesday (before Lent I): band-9 privileged. 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) } // From Dec 17 the weekday readings are proper to the DATE (the "O Antiphons" // days), not the Advent week, so key them by date rather than by week-day. if privFeria && date.Day() <= 24 { return ferialDay(Advent, "advent-dec-"+itoa(date.Day()), Violet, week, true) } return ferialDay(Advent, "advent-"+itoa(week)+"-"+weekdayAbbr(wd), Violet, week, privFeria) case !date.Before(christmasThis): // Dec 25..Dec 31 — Christmas octave // The octave-day ferials (Dec 29-31) have readings proper to the date; // Dec 25-28 are the Nativity and its feasts (Stephen/John/Innocents). if date.Day() >= 29 { return ferialDay(Christmas, "christmas-dec-"+itoa(date.Day()), White, 0, false) } 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) } // Before Epiphany the weekdays are proper to the date (Jan 2-7); after // Epiphany they are keyed by weekday (the days before the Baptism). if date.Before(epiphany(y, sel)) { return ferialDay(Christmas, "christmas-jan-"+itoa(date.Day()), White, 0, false) } return ferialDay(Christmas, "christmas-after-epiphany-"+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 { // Span 2 resumes after Pentecost; Sundays are numbered backward from // Christ the King (the 34th and last Sunday). A weekday takes the number // of its preceding Sunday, so count from that Sunday, not from the // weekday itself (which would round toward Christ the King and land a // week too high — the OT ferial off-by-one). precedingSunday := date.AddDate(0, 0, -int(date.Weekday())) week = 34 - daysBetween(precedingSunday, 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) }