computus 2.0.0

Computus Easter calculation in Rust
Documentation
//! ### Overview
//!
//! Calculate the date of Easter Sunday in Gregorian/Julian calendar using the [computus](https://simple.wikipedia.org/wiki/Computus) method.
//!
//! ### Features
//!
//! Optional features:
//!
//! - [`chrono`][]: Enable directly producing a `chrono::NaiveDate`
//! - [`jiff`][]: Enable directly producing a `jiff::civil::Date`
//!
//! ### Example
//!
//! You can find when Easter is for a particular year with:
//!
//! ```rust
//! // For Gregorian calendars
//! let easter = computus::gregorian(2016).unwrap();
//! assert_eq!((easter.month, easter.day), (3, 27));
//! // For Julian calendars
//! let easter = computus::julian(2016).unwrap();
//! assert_eq!((easter.month, easter.day), (4, 18));
//! // With `chrono` feature
//! #[cfg(feature = "chrono")] {
//!     use chrono::Datelike;
//!     let easter = computus::gregorian_naive(2023).unwrap();
//!     assert_eq!((easter.month(), easter.day()), (4, 9));
//! }
//! // With `jiff` feature
//! #[cfg(feature = "jiff")] {
//!     use jiff::civil::Date;
//!     let easter = computus::gregorian_jiff_date(2023).unwrap();
//!     assert_eq!((easter.month(), easter.day()), (4, 9));
//! }
//! ```
//!
#![no_std]

#[derive(Debug, Eq, PartialEq)]
pub struct Date {
    pub year: i32,
    pub month: u32,
    pub day: u32,
}

#[derive(Debug, Eq, PartialEq)]
pub struct OutOfRangeErr(i32);

impl Date {
    pub const fn ymd(y: i32, m: u32, d: u32) -> Self {
        Date {
            year: y,
            month: m,
            day: d,
        }
    }
}

impl core::fmt::Display for OutOfRangeErr {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "Computus is only valid from 1583 to 9999, got {}",
            self.0,
        )
    }
}

impl core::error::Error for OutOfRangeErr {}

/// Easter in the Gregorian calendar
pub const fn gregorian(year: i32) -> Result<Date, OutOfRangeErr> {
    if year < 1583 || year > 9999 {
        return Err(OutOfRangeErr(year));
    }
    let aa = year % 19;
    let bb = year / 100;
    let cc = year % 100;
    let dd = bb / 4;
    let ee = bb % 4;
    let ff = (bb + 8) / 25;
    let gg = (bb - ff + 1) / 3;
    let hh = (19 * aa + bb - dd - gg + 15) % 30;
    let ii = cc / 4;
    let kk = cc % 4;
    let ll = (32 + 2 * ee + 2 * ii - hh - kk) % 7;
    let mm = (aa + 11 * hh + 22 * ll) / 451;
    let month = (hh + ll - 7 * mm + 114) / 31;
    let day = (hh + ll - 7 * mm + 114) % 31 + 1;
    Ok(Date::ymd(year, month as u32, day as u32))
}

/// Easter in the Julian calendar
pub const fn julian(year: i32) -> Result<Date, OutOfRangeErr> {
    if year < 1583 || year > 9999 {
        return Err(OutOfRangeErr(year));
    }
    let aa = year % 4;
    let bb = year % 7;
    let cc = year % 19;
    let dd = (19 * cc + 15) % 30;
    let ee = (2 * aa + 4 * bb - dd + 34) % 7;
    let ff = dd + ee + 114;
    let month = ff / 31;
    let day = ff % 31 + 1;
    Ok(Date::ymd(year, month as u32, day as u32))
}

/// Easter in the Gregorian calendar. Requires `chrono` feature to provide a `chrono::NaiveDate`.
#[cfg(feature = "chrono")]
pub fn gregorian_naive(year: i32) -> Result<chrono::NaiveDate, OutOfRangeErr> {
    let Date { year, month, day } = gregorian(year)?;
    Ok(chrono::NaiveDate::from_ymd_opt(year, month, day).unwrap())
}

#[cfg(feature = "jiff")]
pub fn gregorian_jiff_date(year: i32) -> Result<jiff::civil::Date, OutOfRangeErr> {
    let Date { year, month, day } = gregorian(year)?;
    Ok(jiff::civil::Date::new(year as i16, month as i8, day as i8).unwrap())
}

#[cfg(test)]
mod tests {
    use rstest::rstest;
    use rstest_reuse::{apply, template};

    #[template]
    #[rstest]
    #[case(1961, 4, 2)]
    #[case(1996, 4, 7)]
    #[case(1997, 3, 30)]
    #[case(1998, 4, 12)]
    #[case(2000, 4, 23)]
    #[case(2001, 4, 15)]
    #[case(2002, 3, 31)]
    #[case(2003, 4, 20)]
    #[case(2004, 4, 11)]
    #[case(2005, 3, 27)]
    #[case(2006, 4, 16)]
    #[case(2007, 4, 8)]
    #[case(2008, 3, 23)]
    #[case(2009, 4, 12)]
    #[case(2010, 4, 4)]
    #[case(2011, 4, 24)]
    #[case(2012, 4, 8)]
    #[case(2013, 3, 31)]
    #[case(2014, 4, 20)]
    #[case(2015, 4, 5)]
    #[case(2016, 3, 27)]
    #[case(2017, 4, 16)]
    #[case(2018, 4, 1)]
    #[case(2019, 4, 21)]
    #[case(2020, 4, 12)]
    #[case(2021, 4, 4)]
    #[case(2022, 4, 17)]
    #[case(3035, 4, 19)]
    #[case(4000, 4, 9)]
    #[case(9999, 3, 28)]
    fn gregorian_data(#[case] y: i32, #[case] m: u32, #[case] d: u32) {}

    #[apply(gregorian_data)]
    fn gregorian_month_day(y: i32, m: u32, d: u32) {
        use super::{gregorian, Date};
        assert_eq!(gregorian(y), Ok(Date::ymd(y, m, d)));
    }

    #[cfg(feature = "chrono")]
    #[apply(gregorian_data)]
    fn gregorian_naive(y: i32, m: u32, d: u32) {
        assert_eq!(
            super::gregorian_naive(y),
            Ok(chrono::NaiveDate::from_ymd_opt(y, m, d).unwrap())
        );
    }

    #[cfg(feature = "jiff")]
    #[apply(gregorian_data)]
    fn into_jiff(y: i32, m: i8, d: i8) {
        assert_eq!(
            super::gregorian_jiff_date(y),
            Ok(jiff::civil::Date::new(y as i16, m, d).unwrap()),
        );
    }

    #[rstest]
    #[case(1961, 3, 27)]
    #[case(1996, 4, 1)]
    #[case(1997, 4, 14)]
    #[case(1998, 4, 6)]
    #[case(1999, 3, 29)]
    #[case(2000, 4, 17)]
    #[case(2001, 4, 2)]
    #[case(2002, 4, 22)]
    #[case(2003, 4, 14)]
    #[case(2004, 3, 29)]
    #[case(2005, 4, 18)]
    #[case(2006, 4, 10)]
    #[case(2007, 3, 26)]
    #[case(2008, 4, 14)]
    #[case(2009, 4, 6)]
    #[case(2010, 3, 22)]
    #[case(2011, 4, 11)]
    #[case(2012, 4, 2)]
    #[case(2013, 4, 22)]
    #[case(2014, 4, 7)]
    #[case(2015, 3, 30)]
    #[case(2016, 4, 18)]
    #[case(2017, 4, 3)]
    #[case(2018, 3, 26)]
    #[case(2019, 4, 15)]
    #[case(2020, 4, 6)]
    fn julian_month_day(#[case] y: i32, #[case] m: u32, #[case] d: u32) {
        use super::{julian, Date};
        assert_eq!(julian(y), Ok(Date::ymd(y, m, d)));
    }
}