use crate::error::EasterError;
use chrono::NaiveDate;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EasterMethod {
Julian = 1,
Orthodox = 2,
Western = 3,
}
impl EasterMethod {
pub fn from_i32(v: i32) -> Result<Self, EasterError> {
match v {
1 => Ok(Self::Julian),
2 => Ok(Self::Orthodox),
3 => Ok(Self::Western),
_ => Err(EasterError::InvalidMethod(v)),
}
}
}
#[inline]
pub fn easter(year: i32, method: EasterMethod) -> Result<NaiveDate, EasterError> {
if year <= 0 {
return Err(EasterError::InvalidYear(year));
}
let g = year.rem_euclid(19);
let mut e = 0;
let (i, j) = if (method as i32) < 3 {
let i = (19 * g + 15).rem_euclid(30);
let j = (year + year / 4 + i).rem_euclid(7);
if method == EasterMethod::Orthodox {
e = 10;
if year > 1600 {
e += year / 100 - 16 - (year / 100 - 16) / 4;
}
}
(i, j)
} else {
let c = year / 100;
let h = (c - c / 4 - (8 * c + 13) / 25 + 19 * g + 15).rem_euclid(30);
let i = h - (h / 28) * (1 - (h / 28) * (29 / (h + 1)) * ((21 - g) / 11));
let j = (year + year / 4 + i + 2 - c + c / 4).rem_euclid(7);
(i, j)
};
let p = i - j + e;
let d = 1 + (p + 27 + (p + 6) / 40).rem_euclid(31);
let m = 3 + (p + 26) / 30;
NaiveDate::from_ymd_opt(year, m as u32, d as u32).ok_or(EasterError::DateOutOfRange {
year,
month: m as u32,
day: d as u32,
})
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Datelike;
#[test]
fn test_invalid_method_from_i32() {
assert!(matches!(
EasterMethod::from_i32(4),
Err(EasterError::InvalidMethod(4))
));
assert!(matches!(
EasterMethod::from_i32(0),
Err(EasterError::InvalidMethod(0))
));
}
#[test]
fn test_western_range_1990_2050() {
let expected: Vec<(i32, u32, u32)> = vec![
(1990, 4, 15),
(1991, 3, 31),
(1992, 4, 19),
(1993, 4, 11),
(1994, 4, 3),
(1995, 4, 16),
(1996, 4, 7),
(1997, 3, 30),
(1998, 4, 12),
(1999, 4, 4),
(2000, 4, 23),
(2001, 4, 15),
(2002, 3, 31),
(2003, 4, 20),
(2004, 4, 11),
(2005, 3, 27),
(2006, 4, 16),
(2007, 4, 8),
(2008, 3, 23),
(2009, 4, 12),
(2010, 4, 4),
(2011, 4, 24),
(2012, 4, 8),
(2013, 3, 31),
(2014, 4, 20),
(2015, 4, 5),
(2016, 3, 27),
(2017, 4, 16),
(2018, 4, 1),
(2019, 4, 21),
(2020, 4, 12),
(2021, 4, 4),
(2022, 4, 17),
(2023, 4, 9),
(2024, 3, 31),
(2025, 4, 20),
(2026, 4, 5),
(2027, 3, 28),
(2028, 4, 16),
(2029, 4, 1),
(2030, 4, 21),
(2031, 4, 13),
(2032, 3, 28),
(2033, 4, 17),
(2034, 4, 9),
(2035, 3, 25),
(2036, 4, 13),
(2037, 4, 5),
(2038, 4, 25),
(2039, 4, 10),
(2040, 4, 1),
(2041, 4, 21),
(2042, 4, 6),
(2043, 3, 29),
(2044, 4, 17),
(2045, 4, 9),
(2046, 3, 25),
(2047, 4, 14),
(2048, 4, 5),
(2049, 4, 18),
(2050, 4, 10),
];
for (y, m, d) in expected {
assert_eq!(
easter(y, EasterMethod::Western).unwrap(),
NaiveDate::from_ymd_opt(y, m, d).unwrap(),
"Failed for year {y}"
);
}
}
#[test]
fn test_orthodox_range_1990_2050() {
let expected: Vec<(i32, u32, u32)> = vec![
(1990, 4, 15),
(1991, 4, 7),
(1992, 4, 26),
(1993, 4, 18),
(1994, 5, 1),
(1995, 4, 23),
(1996, 4, 14),
(1997, 4, 27),
(1998, 4, 19),
(1999, 4, 11),
(2000, 4, 30),
(2001, 4, 15),
(2002, 5, 5),
(2003, 4, 27),
(2004, 4, 11),
(2005, 5, 1),
(2006, 4, 23),
(2007, 4, 8),
(2008, 4, 27),
(2009, 4, 19),
(2010, 4, 4),
(2011, 4, 24),
(2012, 4, 15),
(2013, 5, 5),
(2014, 4, 20),
(2015, 4, 12),
(2016, 5, 1),
(2017, 4, 16),
(2018, 4, 8),
(2019, 4, 28),
(2020, 4, 19),
(2021, 5, 2),
(2022, 4, 24),
(2023, 4, 16),
(2024, 5, 5),
(2025, 4, 20),
(2026, 4, 12),
(2027, 5, 2),
(2028, 4, 16),
(2029, 4, 8),
(2030, 4, 28),
(2031, 4, 13),
(2032, 5, 2),
(2033, 4, 24),
(2034, 4, 9),
(2035, 4, 29),
(2036, 4, 20),
(2037, 4, 5),
(2038, 4, 25),
(2039, 4, 17),
(2040, 5, 6),
(2041, 4, 21),
(2042, 4, 13),
(2043, 5, 3),
(2044, 4, 24),
(2045, 4, 9),
(2046, 4, 29),
(2047, 4, 21),
(2048, 4, 5),
(2049, 4, 25),
(2050, 4, 17),
];
for (y, m, d) in expected {
assert_eq!(
easter(y, EasterMethod::Orthodox).unwrap(),
NaiveDate::from_ymd_opt(y, m, d).unwrap(),
"Failed for year {y}"
);
}
}
#[test]
fn test_western_year_1583() {
let d = easter(1583, EasterMethod::Western).unwrap();
assert_eq!(d, NaiveDate::from_ymd_opt(1583, 4, 10).unwrap());
}
#[test]
fn test_western_year_4099() {
let d = easter(4099, EasterMethod::Western).unwrap();
assert_eq!(d, NaiveDate::from_ymd_opt(4099, 4, 19).unwrap());
}
#[test]
fn test_year_1_all_methods() {
assert!(easter(1, EasterMethod::Julian).is_ok());
assert!(easter(1, EasterMethod::Orthodox).is_ok());
assert!(easter(1, EasterMethod::Western).is_ok());
}
#[test]
fn test_year_9999() {
assert!(easter(9999, EasterMethod::Western).is_ok());
assert!(easter(9999, EasterMethod::Orthodox).is_ok());
assert!(easter(9999, EasterMethod::Julian).is_ok());
}
#[test]
fn test_invalid_year_i32_min() {
assert!(matches!(
easter(i32::MIN, EasterMethod::Western),
Err(EasterError::InvalidYear(i32::MIN))
));
}
#[test]
fn test_orthodox_boundary_1600() {
let before = easter(1600, EasterMethod::Orthodox).unwrap();
let after = easter(1601, EasterMethod::Orthodox).unwrap();
assert!(before.month() >= 3 && before.month() <= 5);
assert!(after.month() >= 3 && after.month() <= 5);
}
#[test]
fn test_easter_always_march_or_april_western() {
for y in 1990..=2100 {
let d = easter(y, EasterMethod::Western).unwrap();
assert!(
d.month() == 3 || d.month() == 4,
"Western Easter {y} in month {}",
d.month()
);
}
}
#[test]
fn test_easter_orthodox_march_to_may() {
for y in 1990..=2100 {
let d = easter(y, EasterMethod::Orthodox).unwrap();
assert!(
(3..=5).contains(&d.month()),
"Orthodox Easter {y} in month {}",
d.month()
);
}
}
#[test]
fn test_method_from_i32_valid() {
assert_eq!(EasterMethod::from_i32(1).unwrap(), EasterMethod::Julian);
assert_eq!(EasterMethod::from_i32(2).unwrap(), EasterMethod::Orthodox);
assert_eq!(EasterMethod::from_i32(3).unwrap(), EasterMethod::Western);
}
#[test]
fn test_method_from_i32_negative() {
assert!(EasterMethod::from_i32(-1).is_err());
assert!(EasterMethod::from_i32(i32::MIN).is_err());
}
#[test]
fn test_easter_is_always_sunday() {
for y in 2000..=2050 {
let d = easter(y, EasterMethod::Western).unwrap();
assert_eq!(
d.weekday(),
chrono::Weekday::Sun,
"Western Easter {y} is not Sunday: {:?}",
d.weekday()
);
}
}
#[test]
fn test_julian_range() {
let expected: Vec<(i32, u32, u32)> = vec![
(326, 4, 3),
(375, 4, 5),
(492, 4, 5),
(552, 3, 31),
(562, 4, 9),
(569, 4, 21),
(597, 4, 14),
(621, 4, 19),
(636, 3, 31),
(655, 3, 29),
(700, 4, 11),
(725, 4, 8),
(750, 3, 29),
(782, 4, 7),
(835, 4, 18),
(849, 4, 14),
(867, 3, 30),
(890, 4, 12),
(922, 4, 21),
(934, 4, 6),
(1049, 3, 26),
(1058, 4, 19),
(1113, 4, 6),
(1119, 3, 30),
(1242, 4, 20),
(1255, 3, 28),
(1257, 4, 8),
(1258, 3, 24),
(1261, 4, 24),
(1278, 4, 17),
(1333, 4, 4),
(1351, 4, 17),
(1371, 4, 6),
(1391, 3, 26),
(1402, 3, 26),
(1412, 4, 3),
(1439, 4, 5),
(1445, 3, 28),
(1531, 4, 9),
(1555, 4, 14),
];
for (y, m, d) in expected {
assert_eq!(
easter(y, EasterMethod::Julian).unwrap(),
NaiveDate::from_ymd_opt(y, m, d).unwrap(),
"Failed for year {y}"
);
}
}
#[test]
fn test_year_0_is_invalid() {
assert!(matches!(
easter(0, EasterMethod::Western),
Err(EasterError::InvalidYear(0))
));
assert!(matches!(
easter(0, EasterMethod::Orthodox),
Err(EasterError::InvalidYear(0))
));
assert!(matches!(
easter(0, EasterMethod::Julian),
Err(EasterError::InvalidYear(0))
));
}
#[test]
fn test_negative_years_various() {
for y in [-1, -100, -1000, -999_999] {
assert!(easter(y, EasterMethod::Western).is_err());
assert!(easter(y, EasterMethod::Orthodox).is_err());
assert!(easter(y, EasterMethod::Julian).is_err());
}
}
#[test]
fn test_i32_max_year() {
let result = std::panic::catch_unwind(|| easter(i32::MAX, EasterMethod::Western));
if let Ok(res) = result {
assert!(res.is_ok() || matches!(res, Err(EasterError::DateOutOfRange { .. })));
}
}
#[test]
fn test_century_boundary_leap_years() {
for y in [1900, 2000, 2100] {
let d = easter(y, EasterMethod::Western).unwrap();
assert!(
d.month() == 3 || d.month() == 4,
"year={y}, month={}",
d.month()
);
}
}
#[test]
fn test_easter_method_from_i32_large_values() {
assert!(EasterMethod::from_i32(100).is_err());
assert!(EasterMethod::from_i32(i32::MAX).is_err());
assert!(EasterMethod::from_i32(i32::MIN).is_err());
}
#[test]
fn test_easter_error_display() {
assert_eq!(
EasterError::InvalidMethod(99).to_string(),
"invalid method: 99"
);
assert_eq!(EasterError::InvalidYear(-5).to_string(), "invalid year: -5");
assert_eq!(
EasterError::DateOutOfRange {
year: 2024,
month: 13,
day: 1
}
.to_string(),
"date out of range: 2024-13-1"
);
}
#[test]
fn test_easter_always_sunday_western_wide_range() {
for y in [1583, 1900, 2000, 2100, 3000, 4000, 5000, 9999] {
let d = easter(y, EasterMethod::Western).unwrap();
assert_eq!(
d.weekday(),
chrono::Weekday::Sun,
"Western Easter year={y} is not Sunday"
);
}
}
#[test]
fn test_easter_method_enum_values() {
assert_eq!(EasterMethod::Julian as i32, 1);
assert_eq!(EasterMethod::Orthodox as i32, 2);
assert_eq!(EasterMethod::Western as i32, 3);
}
#[test]
fn test_western_earliest_and_latest_possible() {
let d2008 = easter(2008, EasterMethod::Western).unwrap();
assert_eq!(d2008.month(), 3);
assert_eq!(d2008.day(), 23);
let d2038 = easter(2038, EasterMethod::Western).unwrap();
assert_eq!(d2038.month(), 4);
assert_eq!(d2038.day(), 25);
}
}