use crate::Year;
const EPOCH_YEAR: u16 = 1901;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EasterMethod {
Western,
Orthodox,
}
#[must_use]
pub const fn easter_monday(year: Year, method: EasterMethod) -> u16 {
let idx = (year.get() - EPOCH_YEAR) as usize;
let raw = match method {
EasterMethod::Western => WESTERN_EASTER_MONDAY[idx],
EasterMethod::Orthodox => ORTHODOX_EASTER_MONDAY[idx],
};
raw as u16
}
#[must_use]
pub const fn easter_sunday(year: Year, method: EasterMethod) -> u16 {
easter_monday(year, method) - 1
}
#[rustfmt::skip]
static WESTERN_EASTER_MONDAY: [u8; 299] = [
98, 90, 103, 95, 114, 106, 91, 111, 102, 87, 107, 99, 83, 103, 95, 115, 99, 91, 111, 96, 87, 107, 92, 112, 103, 95, 108, 100, 91, 111, 96, 88, 107, 92, 112, 104, 88, 108, 100, 85, 104, 96, 116, 101, 92, 112, 97, 89, 108, 100, 85, 105, 96, 109, 101, 93, 112, 97, 89, 109, 93, 113, 105, 90, 109, 101, 86, 106, 97, 89, 102, 94, 113, 105, 90, 110, 101, 86, 106, 98, 110, 102, 94, 114, 98, 90, 110, 95, 86, 106, 91, 111, 102, 94, 107, 99, 90, 103, 95, 115, 106, 91, 111, 103, 87, 107, 99, 84, 103, 95, 115, 100, 91, 111, 96, 88, 107, 92, 112, 104, 95, 108, 100, 92, 111, 96, 88, 108, 92, 112, 104, 89, 108, 100, 85, 105, 96, 116, 101, 93, 112, 97, 89, 109, 100, 85, 105, 97, 109, 101, 93, 113, 97, 89, 109, 94, 113, 105, 90, 110, 101, 86, 106, 98, 89, 102, 94, 114, 105, 90, 110, 102, 86, 106, 98, 111, 102, 94, 114, 99, 90, 110, 95, 87, 106, 91, 111, 103, 94, 107, 99, 91, 103, 95, 115, 107, 91, 111, 103, 88, 108, 100, 85, 105, 96, 109, 101, 93, 112, 97, 89, 109, 93, 113, 105, 90, 109, 101, 86, 106, 97, 89, 102, 94, 113, 105, 90, 110, 101, 86, 106, 98, 110, 102, 94, 114, 98, 90, 110, 95, 86, 106, 91, 111, 102, 94, 107, 99, 90, 103, 95, 115, 106, 91, 111, 103, 87, 107, 99, 84, 103, 95, 115, 100, 91, 111, 96, 88, 107, 92, 112, 104, 95, 108, 100, 92, 111, 96, 88, 108, 92, 112, 104, 89, 108, 100, 85, 105, 96, 116, 101, 93, 112, 97, 89, 109, 100, 85, 105, ];
#[rustfmt::skip]
static ORTHODOX_EASTER_MONDAY: [u8; 299] = [
105, 118, 110, 102, 121, 106, 126, 118, 102, 122, 114, 99, 118, 110, 95, 115, 106, 126, 111, 103, 122, 107, 99, 119, 110, 123, 115, 107, 126, 111, 103, 123, 107, 99, 119, 104, 123, 115, 100, 120, 111, 96, 116, 108, 127, 112, 104, 124, 115, 100, 120, 112, 96, 116, 108, 128, 112, 104, 124, 109, 100, 120, 105, 125, 116, 101, 121, 113, 104, 117, 109, 101, 120, 105, 125, 117, 101, 121, 113, 98, 117, 109, 129, 114, 105, 125, 110, 102, 121, 106, 98, 118, 109, 122, 114, 106, 118, 110, 102, 122, 106, 126, 118, 103, 122, 114, 99, 119, 110, 95, 115, 107, 126, 111, 103, 123, 107, 99, 119, 111, 123, 115, 107, 127, 111, 103, 123, 108, 99, 119, 104, 124, 115, 100, 120, 112, 96, 116, 108, 128, 112, 104, 124, 116, 100, 120, 112, 97, 116, 108, 128, 113, 104, 124, 109, 101, 120, 105, 125, 117, 101, 121, 113, 105, 117, 109, 101, 121, 105, 125, 110, 102, 121, 113, 98, 118, 109, 129, 114, 106, 125, 110, 102, 122, 106, 98, 118, 110, 122, 114, 99, 119, 110, 102, 115, 107, 126, 118, 103, 123, 115, 100, 120, 112, 96, 116, 108, 128, 112, 104, 124, 109, 100, 120, 105, 125, 116, 108, 121, 113, 104, 124, 109, 101, 120, 105, 125, 117, 101, 121, 113, 98, 117, 109, 129, 114, 105, 125, 110, 102, 121, 113, 98, 118, 109, 129, 114, 106, 125, 110, 102, 122, 106, 126, 118, 103, 122, 114, 99, 119, 110, 102, 115, 107, 126, 111, 103, 123, 114, 99, 119, 111, 130, 115, 107, 127, 111, 103, 123, 108, 99, 119, 104, 124, 115, 100, 120, 112, 103, 116, 108, 128, 119, 104, 124, 116, 100, 120, 112, ];
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::{Date, Month, Weekday};
const WESTERN_ANCHORS: &[(u16, Month, u8)] = &[
(1901, Month::Apr, 8), (1913, Month::Mar, 24), (1943, Month::Apr, 26), (1950, Month::Apr, 10), (1976, Month::Apr, 19), (2000, Month::Apr, 24), (2008, Month::Mar, 24), (2020, Month::Apr, 13), (2024, Month::Apr, 1), (2025, Month::Apr, 21), (2038, Month::Apr, 26), (2100, Month::Mar, 29), (2199, Month::Apr, 15), ];
const ORTHODOX_ANCHORS: &[(u16, Month, u8)] = &[
(1901, Month::Apr, 15), (2000, Month::May, 1), (2024, Month::May, 6), (2025, Month::Apr, 21), ];
fn check_anchors(method: EasterMethod, anchors: &[(u16, Month, u8)]) {
for &(year, month, day) in anchors {
let expected = Date::from_ymd(year, month, day).unwrap();
let actual_doy = easter_monday(Year::new(year).unwrap(), method);
let actual = Date::from_ymd(year, Month::Jan, 1)
.unwrap()
.add_days(i32::from(actual_doy) - 1)
.unwrap();
assert_eq!(
actual, expected,
"{method:?} Easter Monday {year}: expected {expected}, got {actual}",
);
}
}
#[test]
fn western_table_matches_published_anchor_dates() {
check_anchors(EasterMethod::Western, WESTERN_ANCHORS);
}
#[test]
fn orthodox_table_matches_published_anchor_dates() {
check_anchors(EasterMethod::Orthodox, ORTHODOX_ANCHORS);
}
#[test]
fn every_western_entry_lands_on_a_monday() {
for year in 1901u16..=2199 {
let doy = easter_monday(Year::new(year).unwrap(), EasterMethod::Western);
let date = Date::from_ymd(year, Month::Jan, 1)
.unwrap()
.add_days(i32::from(doy) - 1)
.unwrap();
assert_eq!(
date.weekday(),
Weekday::Mon,
"Western Easter Monday {year} (doy {doy}) landed on {:?}",
date.weekday(),
);
}
}
#[test]
fn every_orthodox_entry_lands_on_a_monday() {
for year in 1901u16..=2199 {
let doy = easter_monday(Year::new(year).unwrap(), EasterMethod::Orthodox);
let date = Date::from_ymd(year, Month::Jan, 1)
.unwrap()
.add_days(i32::from(doy) - 1)
.unwrap();
assert_eq!(
date.weekday(),
Weekday::Mon,
"Orthodox Easter Monday {year} (doy {doy}) landed on {:?}",
date.weekday(),
);
}
}
#[test]
fn every_western_entry_falls_in_mar_or_apr() {
for year in 1901u16..=2199 {
let doy = easter_monday(Year::new(year).unwrap(), EasterMethod::Western);
let date = Date::from_ymd(year, Month::Jan, 1)
.unwrap()
.add_days(i32::from(doy) - 1)
.unwrap();
assert!(
matches!(date.month(), Month::Mar | Month::Apr),
"Western Easter Monday {year} in unexpected month {:?}",
date.month(),
);
}
}
#[test]
fn every_orthodox_entry_falls_in_apr_or_may() {
for year in 1901u16..=2199 {
let doy = easter_monday(Year::new(year).unwrap(), EasterMethod::Orthodox);
let date = Date::from_ymd(year, Month::Jan, 1)
.unwrap()
.add_days(i32::from(doy) - 1)
.unwrap();
assert!(
matches!(date.month(), Month::Apr | Month::May),
"Orthodox Easter Monday {year} in unexpected month {:?}",
date.month(),
);
}
}
#[allow(clippy::many_single_char_names)]
fn gregorian_computus_easter_sunday(year: u16) -> (u8, u8) {
let y = i32::from(year);
let a = y % 19;
let b = y / 100;
let c = y % 100;
let d = b / 4;
let e = b % 4;
let f = (b + 8) / 25;
let g = (b - f + 1) / 3;
let h = (19 * a + b - d - g + 15) % 30;
let i = c / 4;
let k = c % 4;
let l = (32 + 2 * e + 2 * i - h - k) % 7;
let m = (a + 11 * h + 22 * l) / 451;
let month = (h + l - 7 * m + 114) / 31;
let day = ((h + l - 7 * m + 114) % 31) + 1;
(u8::try_from(month).unwrap(), u8::try_from(day).unwrap())
}
#[allow(clippy::many_single_char_names)]
fn julian_computus_easter_sunday(year: u16) -> (u8, u8) {
let y = i32::from(year);
let a = y % 4;
let b = y % 7;
let c = y % 19;
let d = (19 * c + 15) % 30;
let e = (2 * a + 4 * b - d + 34) % 7;
let month = (d + e + 114) / 31;
let day = ((d + e + 114) % 31) + 1;
(u8::try_from(month).unwrap(), u8::try_from(day).unwrap())
}
#[test]
fn western_table_matches_gregorian_computus() {
for year in 1901u16..=2199 {
let (month, day) = gregorian_computus_easter_sunday(year);
let sunday = Date::from_ymd(year, Month::try_from_u8(month).unwrap(), day).unwrap();
let monday = sunday.add_days(1).unwrap();
assert_eq!(
easter_monday(Year::new(year).unwrap(), EasterMethod::Western),
monday.day_of_year(),
"Western table disagrees with Gregorian computus for {year}",
);
}
}
#[test]
fn orthodox_table_matches_julian_computus() {
for year in 1901u16..=2199 {
let (month, day) = julian_computus_easter_sunday(year);
let offset = if year >= 2100 { 14 } else { 13 };
let julian_as_serial =
Date::from_ymd(year, Month::try_from_u8(month).unwrap(), day).unwrap();
let sunday = julian_as_serial.add_days(offset).unwrap();
let monday = sunday.add_days(1).unwrap();
assert_eq!(
easter_monday(Year::new(year).unwrap(), EasterMethod::Orthodox),
monday.day_of_year(),
"Orthodox table disagrees with Julian computus for {year}",
);
}
}
#[test]
fn easter_sunday_is_one_day_before_monday() {
for y in 1901u16..=2199 {
let year = Year::new(y).unwrap();
for m in [EasterMethod::Western, EasterMethod::Orthodox] {
assert_eq!(easter_sunday(year, m) + 1, easter_monday(year, m));
}
}
}
}