package calendar import ( "strconv" "strings" "time" ) // Extraordinary Form (1962) season constants. Advent/Christmas/Lent/Easter are // shared with the OF; these are EF-specific. const ( Septuagesima Season = "septuagesima" Passiontide Season = "passiontide" TimeAfterEpiphany Season = "time-after-epiphany" TimeAfterPentecost Season = "time-after-pentecost" ) // efChristTheKing is the last Sunday of October (the 1962 date — unlike the OF's // last Sunday before Advent). func efChristTheKing(year int) time.Time { oct31 := time.Date(year, 10, 31, 0, 0, 0, 0, time.UTC) return oct31.AddDate(0, 0, -int(oct31.Weekday())) // Sunday on/before Oct 31 } // efColour is the default (Sunday/ferial) colour of an EF season. func efColour(s Season) Colour { switch s { case Advent, Septuagesima, Lent, Passiontide: return Violet case Christmas, Easter_: return White default: // TimeAfterEpiphany, TimeAfterPentecost return Green } } // efSeason returns the 1962 season for date, by chronological date boundaries. func efSeason(date time.Time, y int, easter time.Time) Season { adventThis := adventStart(y) christmasThis := time.Date(y, 12, 25, 0, 0, 0, 0, time.UTC) jan6 := time.Date(y, 1, 6, 0, 0, 0, 0, time.UTC) septStart := easter.AddDate(0, 0, -63) // Septuagesima Sunday ashWed := easter.AddDate(0, 0, -46) passionSun := easter.AddDate(0, 0, -14) paschalEnd := easter.AddDate(0, 0, 55) // Saturday after Pentecost; Trinity = +56 switch { case !date.Before(adventThis) && date.Before(christmasThis): return Advent case !date.Before(christmasThis): // Dec 25..31 return Christmas case date.Before(jan6): // Jan 1..5 return Christmas case date.Before(septStart): // Epiphany .. before Septuagesima return TimeAfterEpiphany case date.Before(ashWed): return Septuagesima case date.Before(passionSun): return Lent case date.Before(easter): return Passiontide case !date.After(paschalEnd): return Easter_ default: return TimeAfterPentecost } } func efCel(season Season, slug string, col Colour, rank Rank, week int) temporalDay { return temporalDay{ Season: season, Colour: col, Week: week, Rank: rank, Sunday: false, Cel: Celebration{Slug: slug, Rank: rank, Colour: col, Layer: "temporal"}, } } // temporalEF computes the 1962 temporal identity of date. Season is exact // (oracle-validated); ranks/weeks are best-effort for display. func temporalEF(date time.Time) temporalDay { date = date.UTC().Truncate(24 * time.Hour) y := date.Year() easter := Easter(y) season := efSeason(date, y, easter) col := efColour(season) sun := date.Weekday() == time.Sunday // Major feasts of the Lord (I class): nice titles + colour. switch { case date.Month() == time.December && date.Day() == 25: return efCel(Christmas, "ef-nativity", White, RankClass1, 0) case date.Month() == time.January && date.Day() == 1: return efCel(Christmas, "ef-circumcision", White, RankClass1, 0) case date.Month() == time.January && date.Day() == 6: return efCel(TimeAfterEpiphany, "ef-epiphany", White, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, -46)): return efCel(Lent, "ef-ash-wednesday", Violet, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, -14)): return efCel(Passiontide, "ef-passion-sunday", Violet, RankClass1, 1) case sameDay(date, easter.AddDate(0, 0, -7)): return efCel(Passiontide, "ef-palm-sunday", Violet, RankClass1, 2) case sameDay(date, easter): return efCel(Easter_, "ef-easter-sunday", White, RankClass1, 1) case sameDay(date, easter.AddDate(0, 0, 7)): return efCel(Easter_, "ef-low-sunday", White, RankClass1, 2) case sameDay(date, easter.AddDate(0, 0, 39)): return efCel(Easter_, "ef-ascension", White, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, 49)): return efCel(Easter_, "ef-pentecost", Red, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, 56)): return efCel(TimeAfterPentecost, "ef-trinity", White, RankClass1, 1) case sameDay(date, easter.AddDate(0, 0, 60)): return efCel(TimeAfterPentecost, "ef-corpus-christi", White, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, 68)): return efCel(TimeAfterPentecost, "ef-sacred-heart", White, RankClass1, 0) case sameDay(date, efChristTheKing(y)): return efCel(TimeAfterPentecost, "ef-christ-the-king", White, RankClass1, 0) } // Sundays vs ferias of the current season. if sun { week := efWeek(date, season, y, easter) rank := RankClass2 if season == Advent && sameDay(date, adventStart(y)) { rank = RankClass1 // 1st Sunday of Advent } return efCel(season, "ef-"+string(season)+"-sunday-"+strconv.Itoa(week), col, rank, week) } // The days between Ash Wednesday and the 1st Sunday of Lent have their own // proper Masses (not part of a numbered Lenten week). if season == Lent && date.Before(easter.AddDate(0, 0, -42)) { return efCel(Lent, "ef-lent-after-ashes-"+weekdayLower(date), Violet, RankClass3, 0) } week := efWeek(date, season, y, easter) rank := RankClass4 // per-annum feria if season == Advent || season == Lent || season == Passiontide || season == Septuagesima { rank = RankClass3 // penitential ferias rank higher } // Unique ferial slug (season-week-weekday) so proper ferias (Lent, Advent, // Holy Week, Ember days) can key their own readings; green-season ferias // simply have no lectionary entry and fall back to the Sunday. return efCel(season, "ef-"+string(season)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week) } func weekdayLower(d time.Time) string { return strings.ToLower(d.Weekday().String()) } // efWeek is a best-effort week-within-season number for display (not // oracle-checked). Time after Pentecost counts Sundays from Pentecost. func efWeek(date time.Time, season Season, y int, easter time.Time) int { switch season { case Advent: return daysBetween(adventStart(y), date)/7 + 1 case TimeAfterPentecost: return daysBetween(easter.AddDate(0, 0, 49), date) / 7 // Sundays after Pentecost case Lent: return daysBetween(easter.AddDate(0, 0, -42), date)/7 + 1 // from 1st Sunday of Lent case TimeAfterEpiphany: jan6 := time.Date(y, 1, 6, 0, 0, 0, 0, time.UTC) return daysBetween(jan6, date)/7 + 1 case Septuagesima: return daysBetween(easter.AddDate(0, 0, -63), date)/7 + 1 case Easter_: return daysBetween(easter, date)/7 + 1 default: return 0 } }