ako 0.0.3

Ako is a Rust crate that offers a practical and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps.
Documentation
use crate::{Calendar, Date, Month, Year, YearMonth};

// years in a normalized era
const YEARS_IN_CYCLE: i32 = 400;

// days in a normalized era
const DAYS_IN_CYCLE: i32 = 146_097;

// number of days in the first 100 years in an era
const DAYS_IN_FIRST_100_YEARS_IN_CYCLE: i32 = 36_524;

/// The [proleptic Gregorian calendar] with ISO-8601 calendar week rules.
///
/// [proleptic Gregorian calendar]: https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Iso;

// Constants: Years
impl Iso {
    /// The maximum supported ISO year, `+5000000`.
    #[allow(unsafe_code)]
    pub const MAX_YEAR: Year<Self> = unsafe { Year::unchecked_of(Self, 5_000_000) };

    /// The minimum supported ISO year, `-5000000`.
    #[allow(unsafe_code)]
    pub const MIN_YEAR: Year<Self> = unsafe { Year::unchecked_of(Self, -5_000_000) };
}

// Constants: Months
impl Iso {
    /// The common month of January with 31 days.
    #[allow(unsafe_code)]
    pub const JANUARY: Month<Self> = unsafe { Month::unchecked_of(Self, 1) };

    /// The common or leap month of February with 28 or 29 days.
    #[allow(unsafe_code)]
    pub const FEBRUARY: Month<Self> = unsafe { Month::unchecked_of(Self, 2) };

    /// The common month of March with 31 days.
    #[allow(unsafe_code)]
    pub const MARCH: Month<Self> = unsafe { Month::unchecked_of(Self, 3) };

    /// The common month of April with 30 days.
    #[allow(unsafe_code)]
    pub const APRIL: Month<Self> = unsafe { Month::unchecked_of(Self, 4) };

    /// The common month of May with 31 days.
    #[allow(unsafe_code)]
    pub const MAY: Month<Self> = unsafe { Month::unchecked_of(Self, 5) };

    /// The common month of June with 30 days.
    #[allow(unsafe_code)]
    pub const JUNE: Month<Self> = unsafe { Month::unchecked_of(Self, 6) };

    /// The common month of July with 31 days.
    #[allow(unsafe_code)]
    pub const JULY: Month<Self> = unsafe { Month::unchecked_of(Self, 7) };

    /// The common month of August with 31 days.
    #[allow(unsafe_code)]
    pub const AUGUST: Month<Self> = unsafe { Month::unchecked_of(Self, 8) };

    /// The common month of September with 31 days.
    #[allow(unsafe_code)]
    pub const SEPTEMBER: Month<Self> = unsafe { Month::unchecked_of(Self, 9) };

    /// The common month of October with 31 days.
    #[allow(unsafe_code)]
    pub const OCTOBER: Month<Self> = unsafe { Month::unchecked_of(Self, 10) };

    /// The common month of November with 30 days.
    #[allow(unsafe_code)]
    pub const NOVEMBER: Month<Self> = unsafe { Month::unchecked_of(Self, 11) };

    /// The common month of December with 31 days.
    #[allow(unsafe_code)]
    pub const DECEMBER: Month<Self> = unsafe { Month::unchecked_of(Self, 12) };
}

const MAX_DATE_DAYS: i32 = 1_826_212_805;
const MIN_DATE_DAYS: i32 = -1_826_212_560;

// Constants: Dates
impl Iso {
    /// The maximum supported ISO date, `+5000000-12-31`.
    #[allow(unsafe_code)]
    pub const MAX_DATE: Date<Self> = unsafe { Date::unchecked_of(Self, MAX_DATE_DAYS) };

    /// The minimum supported ISO date, `-5000000-01-01`.
    #[allow(unsafe_code)]
    pub const MIN_DATE: Date<Self> = unsafe { Date::unchecked_of(Self, MIN_DATE_DAYS) };
}

// Constant versions of trait methods
impl Iso {
    #[allow(unsafe_code)]
    pub(crate) const fn date(self, mut year: i32, month: u8, day: u8) -> crate::Result<Date<Self>> {
        // validate year number
        ensure_range!(Self::MIN_YEAR.number(), Self::MAX_YEAR.number(), year);

        // validate month number
        ensure_range!(1, 12, month);

        // validate day number
        ensure_range!(1, Self.year_month_days(year, month), day);

        // shift the year back one if the month is before March
        // the Ako epoch starts from 0000-03-01 because this allows normalizing leap days to
        // always occur at the end of the year
        if month <= 2 {
            year -= 1;
        }

        // the gregorian calendar repeats itself exactly every 400 years (when shifted to move
        // the leap day)
        let cycle = year.div_euclid(YEARS_IN_CYCLE);
        let year = year.rem_euclid(YEARS_IN_CYCLE);

        // compute the day of the year, followed by the day of the cycle
        let day_of_year = ((153 * (((month as i32) + 9) % 12) + 2) / 5) + (day as i32) - 1;
        let day_of_cycle = year * 365 + year / 4 - year / 100 + day_of_year;

        // extend to the number of days since the Ako epoch
        let days = cycle * DAYS_IN_CYCLE + day_of_cycle;

        // validate number of days
        ensure_range!(MIN_DATE_DAYS, MAX_DATE_DAYS, days);

        // SAFETY: days is pre-validated
        Ok(unsafe { Date::unchecked_of(self, days) })
    }

    #[allow(unsafe_code)]
    pub(crate) const fn date_from_ordinal(self, year: i32, day: u16) -> crate::Result<Date<Self>> {
        let cycle = year.div_euclid(YEARS_IN_CYCLE);
        let year = year.rem_euclid(YEARS_IN_CYCLE);

        // compute the day of the cycle
        let day_of_cycle = year * 365 + year / 4 - year / 100 + (day as i32);

        // extend to the number of days since the Ako epoch
        let mut days = cycle * DAYS_IN_CYCLE + day_of_cycle;

        // and shift to the Ako epoch
        days -= 61;

        // validate number of days
        ensure_range!(MIN_DATE_DAYS, MAX_DATE_DAYS, days);

        // SAFETY: days is pre-validated
        Ok(unsafe { Date::unchecked_of(self, days) })
    }

    // Returns cycle, year-of-cycle, and day-of-year
    pub(crate) const fn date_to_cycle(self, days: i32) -> (i32, i32, i32) {
        let cycle = days.div_euclid(DAYS_IN_CYCLE);
        let day = days.rem_euclid(DAYS_IN_CYCLE);

        // year number from the beginning of the cycle
        let year = (day - day / 1_460 + day / DAYS_IN_FIRST_100_YEARS_IN_CYCLE
            - day / (DAYS_IN_CYCLE - 1))
            / 365;

        // day number from the beginning of the year
        let day = day - CYCLE_DAYS_FROM_YEAR[year as usize];

        (cycle, year, day)
    }

    #[allow(unsafe_code)]
    pub(crate) const fn date_components(self, days: i32) -> (Year<Self>, Month<Self>, u8) {
        let (cycle, year, day) = self.date_to_cycle(days);

        // month number from the day number
        let month = MONTH_FROM_ORDINAL[day as usize] as i32;

        // year number in the Gregorian calendar
        let mut year = year + cycle * YEARS_IN_CYCLE;

        if month <= 2 {
            year += 1;
        }

        // day number of the month
        let day = day + YEAR_DAYS_FROM_AKO_MONTH[(month - 1) as usize] as i32;

        (
            // SAFETY: year number is coming from a pre-validated number of `days`
            unsafe { Year::unchecked_of(Self, year) },
            // SAFETY: month number is already constrained to be in-range
            unsafe { Month::unchecked_of(self, month as u8) },
            day as u8,
        )
    }

    #[allow(unsafe_code)]
    pub(crate) const fn date_to_ordinal(self, days: i32) -> (Year<Self>, u16) {
        let (cycle, year, mut day) = self.date_to_cycle(days);

        // month number from the day number
        let month = MONTH_FROM_ORDINAL[day as usize] as i32;

        // year number in the Gregorian calendar
        let mut year = year + cycle * YEARS_IN_CYCLE;

        if month <= 2 {
            year += 1;
            day -= 305;
        } else {
            day += 60;
        }

        (
            // SAFETY: year number is coming from a pre-validated number of `days`
            unsafe { Year::unchecked_of(Self, year) },
            day as u16,
        )
    }

    #[allow(unsafe_code)]
    pub(crate) const fn year(self, year: i32) -> crate::Result<Year<Self>> {
        ensure_range!(Self::MIN_YEAR.number(), Self::MAX_YEAR.number(), year);

        // SAFETY: year number is pre-validated
        Ok(unsafe { Year::unchecked_of(Self, year) })
    }

    pub(crate) const fn year_days(self, year: i32) -> u16 {
        if Self.year_is_leap(year) { 366 } else { 365 }
    }

    const fn year_is_leap(self, year: i32) -> bool {
        // if the year is not exactly divisible by 4, it is not a leap year
        // asking that up front removes ~75% of years from the remaining math
        year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
    }

    pub(crate) const fn year_month(self, year: i32, month: u8) -> crate::Result<YearMonth<Self>> {
        ensure_range!(Self::MIN_YEAR.number(), Self::MAX_YEAR.number(), year);
        ensure_range!(1, 12, month);

        Ok(YearMonth {
            calendar: self,
            year,
            month,
        })
    }

    const fn year_month_days(self, year: i32, month: u8) -> u8 {
        if month != 2 || !Self.year_is_leap(year) {
            MONTH_LAST_DAY[(month as usize) - 1]
        } else {
            29
        }
    }
}

impl Calendar for Iso {
    fn date(self, year: i32, month: u8, day: u8) -> crate::Result<Date<Self>> {
        Self.date(year, month, day)
    }

    fn date_from_ordinal(self, year: i32, day: u16) -> crate::Result<Date<Self>> {
        Self.date_from_ordinal(year, day)
    }

    fn date_components(self, days: i32) -> (Year<Self>, Month<Self>, u8) {
        Self.date_components(days)
    }

    fn date_to_ordinal(self, days: i32) -> (Year<Self>, u16) {
        Self.date_to_ordinal(days)
    }

    fn year(self, year: i32) -> crate::Result<Year<Self>> {
        Self.year(year)
    }

    fn year_is_leap(self, year: i32) -> bool {
        Self.year_is_leap(year)
    }

    fn year_days(self, year: i32) -> u16 {
        Self.year_days(year)
    }

    fn year_months(self, _year: i32) -> u8 {
        // there are always 12 months in an ISO year
        12
    }

    fn year_month(self, year: i32, month: u8) -> crate::Result<YearMonth<Self>> {
        Self.year_month(year, month)
    }

    fn year_month_days(self, year: i32, month: u8) -> u8 {
        Self.year_month_days(year, month)
    }
}

// lookup table from year-of-cycle to days elapsed before the cycle
const CYCLE_DAYS_FROM_YEAR: [i32; 400] = [
    0, 365, 730, 1095, 1461, 1826, 2191, 2556, 2922, 3287, 3652, 4017, 4383, 4748, 5113, 5478,
    5844, 6209, 6574, 6939, 7305, 7670, 8035, 8400, 8766, 9131, 9496, 9861, 10227, 10592, 10957,
    11322, 11688, 12053, 12418, 12783, 13149, 13514, 13879, 14244, 14610, 14975, 15340, 15705,
    16071, 16436, 16801, 17166, 17532, 17897, 18262, 18627, 18993, 19358, 19723, 20088, 20454,
    20819, 21184, 21549, 21915, 22280, 22645, 23010, 23376, 23741, 24106, 24471, 24837, 25202,
    25567, 25932, 26298, 26663, 27028, 27393, 27759, 28124, 28489, 28854, 29220, 29585, 29950,
    30315, 30681, 31046, 31411, 31776, 32142, 32507, 32872, 33237, 33603, 33968, 34333, 34698,
    35064, 35429, 35794, 36159, 36524, 36889, 37254, 37619, 37985, 38350, 38715, 39080, 39446,
    39811, 40176, 40541, 40907, 41272, 41637, 42002, 42368, 42733, 43098, 43463, 43829, 44194,
    44559, 44924, 45290, 45655, 46020, 46385, 46751, 47116, 47481, 47846, 48212, 48577, 48942,
    49307, 49673, 50038, 50403, 50768, 51134, 51499, 51864, 52229, 52595, 52960, 53325, 53690,
    54056, 54421, 54786, 55151, 55517, 55882, 56247, 56612, 56978, 57343, 57708, 58073, 58439,
    58804, 59169, 59534, 59900, 60265, 60630, 60995, 61361, 61726, 62091, 62456, 62822, 63187,
    63552, 63917, 64283, 64648, 65013, 65378, 65744, 66109, 66474, 66839, 67205, 67570, 67935,
    68300, 68666, 69031, 69396, 69761, 70127, 70492, 70857, 71222, 71588, 71953, 72318, 72683,
    73048, 73413, 73778, 74143, 74509, 74874, 75239, 75604, 75970, 76335, 76700, 77065, 77431,
    77796, 78161, 78526, 78892, 79257, 79622, 79987, 80353, 80718, 81083, 81448, 81814, 82179,
    82544, 82909, 83275, 83640, 84005, 84370, 84736, 85101, 85466, 85831, 86197, 86562, 86927,
    87292, 87658, 88023, 88388, 88753, 89119, 89484, 89849, 90214, 90580, 90945, 91310, 91675,
    92041, 92406, 92771, 93136, 93502, 93867, 94232, 94597, 94963, 95328, 95693, 96058, 96424,
    96789, 97154, 97519, 97885, 98250, 98615, 98980, 99346, 99711, 100076, 100441, 100807, 101172,
    101537, 101902, 102268, 102633, 102998, 103363, 103729, 104094, 104459, 104824, 105190, 105555,
    105920, 106285, 106651, 107016, 107381, 107746, 108112, 108477, 108842, 109207, 109572, 109937,
    110302, 110667, 111033, 111398, 111763, 112128, 112494, 112859, 113224, 113589, 113955, 114320,
    114685, 115050, 115416, 115781, 116146, 116511, 116877, 117242, 117607, 117972, 118338, 118703,
    119068, 119433, 119799, 120164, 120529, 120894, 121260, 121625, 121990, 122355, 122721, 123086,
    123451, 123816, 124182, 124547, 124912, 125277, 125643, 126008, 126373, 126738, 127104, 127469,
    127834, 128199, 128565, 128930, 129295, 129660, 130026, 130391, 130756, 131121, 131487, 131852,
    132217, 132582, 132948, 133313, 133678, 134043, 134409, 134774, 135139, 135504, 135870, 136235,
    136600, 136965, 137331, 137696, 138061, 138426, 138792, 139157, 139522, 139887, 140253, 140618,
    140983, 141348, 141714, 142079, 142444, 142809, 143175, 143540, 143905, 144270, 144636, 145001,
    145366, 145731,
];

// lookup table from days-of-year, in Ako epoch where day 0 is March 1st
// returns the month, pre-shifted from Ako epoch
const MONTH_FROM_ORDINAL: [u8; 366] = [
    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
    4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5,
    5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6,
    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8,
    8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10,
    10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
    11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
    11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
];

// lookup table from month-of-year to negative days-elapsed-in-year (in Ako epoch)
const YEAR_DAYS_FROM_AKO_MONTH: [i16; 12] = [
    -305, -336, 1, -30, -60, -91, -121, -152, -183, -213, -244, -274,
];

// lookup table from month-of-year to last-day-of-month in a common year
const MONTH_LAST_DAY: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];