sac13 0.1.1

The reference implementation for the SAC13 calendar system.
Documentation
mod parsed_comp;
mod separator;

pub(crate) use parsed_comp::ParsedComponent;

pub use separator::DateComponentSeparator;

use crate::{Date, GregorianDate, SacOrGreg};
use core::{fmt::Display, num::NonZeroU8};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Describes the order of date components.
///
///   - `Y` = year
///   - `M` = month
///   - `D` = day
pub enum ComponentOrder {
    /// ## Big endian
    /// Order: year, month, day
    YMD,

    /// ## Little endian
    /// Order: day, month, year
    DMY,

    /// ## Middle endian (USA)
    /// Order: month, day, year
    ///
    /// This is a special US edge case and only allowed for
    /// dates that use slash (`/`) as component separator
    /// and only for Gregorian Calendar dates.
    ///
    /// In SAC13 there is no middle endian ([ComponentOrder::MDY])!
    MDY,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// Parsing result of [SacOrGreg::parse_str].
pub struct ParsedSacOrGreg {
    pub(crate) date: SacOrGreg,
    pub(crate) format: ParsedFormat,
}

impl ParsedSacOrGreg {
    /// Converts the parsed result to a SAC13 [ParsedDate].
    /// It will return [None] if it wasn't a Gregorian Date.
    ///
    /// If you are not sure you should pattern match [ParsedSacOrGreg::date()] instead.
    pub fn to_sac13(self) -> Option<ParsedDate> {
        match self.date {
            SacOrGreg::Sac13(date) => Some(ParsedDate {
                date,
                format: self.format,
            }),
            _ => None,
        }
    }

    /// Converts the parsed result to a [ParsedGregorianDate].
    /// It will return [None] if it wasn't a Gregorian Date.
    ///
    /// If you are not sure you should pattern match [ParsedSacOrGreg::date()] instead.
    pub fn to_greg(self) -> Option<ParsedGregorianDate> {
        match self.date {
            SacOrGreg::Gregorian(date) => Some(ParsedGregorianDate {
                date,
                format: self.format,
            }),
            _ => None,
        }
    }

    /// Returns parsed date that is either a SAC13 or a Gregorian Calendar date.
    pub fn date(&self) -> &SacOrGreg {
        &self.date
    }

    /// Returns the format information from the parsed input string.
    pub fn format(&self) -> &ParsedFormat {
        &self.format
    }
}

/// The parsing result of [Date::parse_str], which is basically
/// a SAC13 [Date] with attached [parsing information](ParsedFormat).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParsedDate {
    /// The parsed SAC13 date.
    pub date: Date,

    /// Parsing information about the parsed input string.
    pub format: ParsedFormat,
}

/// The parsing result of [GregorianDate::parse_str], which is basically
/// a [GregorianDate] with attached [parsing information](ParsedFormat).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParsedGregorianDate {
    /// The parsed Gregorian Calendar date.
    pub date: GregorianDate,

    /// Parsing information about the parsed input string.
    pub format: ParsedFormat,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// Details about the format of the parsed input.
pub struct ParsedFormat {
    pub(crate) comp_ord: ComponentOrder,
    pub(crate) len_day: NonZeroU8,
    pub(crate) len_month: NonZeroU8,
    pub(crate) len_year: NonZeroU8,
    pub(crate) separators: [DateComponentSeparator; 2],
}

impl ParsedFormat {
    /// Number of digits used to descibe the year component.
    pub fn len_year(&self) -> NonZeroU8 {
        self.len_year
    }

    /// Number of digits used to descibe the month component.
    pub fn len_month(&self) -> NonZeroU8 {
        self.len_month
    }

    /// Number of digits used to descibe the day component.
    pub fn len_day(&self) -> NonZeroU8 {
        self.len_day
    }

    /// Order of the year, month, day components.
    pub fn comp_ord(&self) -> ComponentOrder {
        self.comp_ord
    }

    /// Two separator between the three date components.
    pub fn separators(&self) -> &[DateComponentSeparator; 2] {
        &self.separators
    }
}

impl Display for ParsedFormat {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        fn write_component(
            f: &mut core::fmt::Formatter<'_>,
            x: (char, NonZeroU8),
        ) -> core::fmt::Result {
            for _ in 0..x.1.get() as usize {
                write!(f, "{}", x.0)?;
            }

            Ok(())
        }

        let y = ('Y', self.len_year);
        let m = ('M', self.len_month);
        let d = ('D', self.len_day);

        let components = match self.comp_ord {
            ComponentOrder::YMD => [y, m, d],
            ComponentOrder::DMY => [d, m, y],
            ComponentOrder::MDY => [m, d, y],
        };

        write_component(f, components[0])?;
        write!(f, "{}", self.separators[0].as_str())?;
        write_component(f, components[1])?;
        write!(f, "{}", self.separators[1].as_str())?;
        write_component(f, components[2])?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    macro_rules! assert_matches {
        ($left:expr, $right:pat) => {
            assert!(matches!($left, $right));
        };
    }

    macro_rules! assert_parse_error {
        ($inp:literal) => {
            assert_matches!(SacOrGreg::parse_str($inp), None);
        };
    }

    macro_rules! parse_expect_greg {
        ($inp:expr) => {{
            const ERROR: &str = concat!(
                "Expected ",
                stringify!($inp),
                " to parse as Gregorian date."
            );

            GregorianDate::parse_str($inp).expect(ERROR)
        }};
    }

    macro_rules! parse_expect_sac13 {
        ($inp:expr) => {{
            const ERROR: &str = concat!(
                "Expected ",
                stringify!($inp),
                " to parse as Gregorian date."
            );

            Date::parse_str($inp).expect(ERROR)
        }};
    }

    macro_rules! assert_sac13 {
        ($inp:expr, $y:ident - $m:literal - $d:literal) => {{
            let parsed = parse_expect_sac13!($inp);
            assert_eq!(parsed.date, date!($y - $m - $d));
        }};

        ($inp:expr, $pat:expr, $y:ident - $m:literal - $d:literal) => {{
            let parsed = parse_expect_sac13!($inp);
            assert_eq!(parsed.date, date!($y - $m - $d));
            let parsed_pattern = format!("{}", parsed.format);
            assert_eq!(parsed_pattern, $pat);
        }};
    }

    macro_rules! assert_greg {
        ($inp:expr, $y:literal - $m:literal - $d:literal) => {{
            let parsed = parse_expect_greg!($inp);
            assert_eq!(parsed.date, date_greg!($y - $m - $d));
        }};

        ($inp:expr, $pat:expr, $y:literal - $m:literal - $d:literal) => {{
            let parsed = parse_expect_greg!($inp);
            assert_eq!(parsed.date, date_greg!($y - $m - $d));
            let parsed_pattern = format!("{}", parsed.format);
            assert_eq!(parsed_pattern, $pat);
        }};
    }

    #[test]
    fn parsing_gregorian() {
        // DD-MM-YYYY
        assert_greg!("11-12-2000", "DD-MM-YYYY", 2000 - 12 - 11);
        assert_greg!("11.12.2000", "DD.MM.YYYY", 2000 - 12 - 11);

        // YYYY-MM-DD
        assert_greg!("2000-12-11", "YYYY-MM-DD", 2000 - 12 - 11);
        assert_greg!("2000.12.11", "YYYY.MM.DD", 2000 - 12 - 11);
        assert_greg!("2000/12/11", "YYYY/MM/DD", 2000 - 12 - 11);

        // Gregorian US Format:
        assert_greg!("12/11/2000", "MM/DD/YYYY", 2000 - 12 - 11);
    }

    #[test]
    fn negative_year_greg() {
        assert_greg!("-2000-12-11", "YYYY-MM-DD", -2000 - 12 - 11);
        assert_greg!("11-12--2000", "DD-MM-YYYY", -2000 - 12 - 11);
        assert_greg!("-2000.12.11", "YYYY.MM.DD", -2000 - 12 - 11);
        assert_greg!("11.12.-2000", "DD.MM.YYYY", -2000 - 12 - 11);
    }

    #[test]
    fn parsing_sac13() {
        assert_sac13!("M003-02-01", "YYYY-MM-DD", M003 - 02 - 01);
        assert_sac13!("M003.02.01", "YYYY.MM.DD", M003 - 02 - 01);
        assert_sac13!("M003/02/01", "YYYY/MM/DD", M003 - 02 - 01);

        assert_sac13!("01-02-M003", "DD-MM-YYYY", M003 - 02 - 01);
        assert_sac13!("01.02.M003", "DD.MM.YYYY", M003 - 02 - 01);
        assert_sac13!("01/02/M003", "DD/MM/YYYY", M003 - 02 - 01);

        // Note: SAC13 is always YMD or DMY and never the US format MDY
    }

    #[test]
    fn no_letter_allowed_as_month() {
        assert_parse_error!("2001-L-03");
    }

    #[test]
    fn no_letter_allowed_as_day() {
        assert_parse_error!("2001-02-L");
    }

    #[test]
    fn ambiguous_year_end_fails_to_parse() {
        assert_parse_error!("2020-12-2020");
    }

    #[test]
    fn no_year_end_fails_to_parse() {
        assert_parse_error!("01-01-01");
    }

    #[test]
    fn three_digit_years_can_be_parsed() {
        assert_greg!("001-01-01", "YYY-MM-DD", 1 - 1 - 1);
        assert_greg!("01-01-001", "DD-MM-YYY", 1 - 1 - 1);
    }

    #[test]
    fn zero_component_is_assumed_to_be_the_year() {
        assert_greg!("2-1-0", "D-M-Y", 0 - 1 - 2);
        assert_greg!("0-1-2", "Y-M-D", 0 - 1 - 2);
        assert_greg!("1/2/0", "M/D/Y", 0 - 1 - 2);
    }

    #[test]
    fn negative_component_is_assumed_to_be_the_year() {
        assert_greg!("2-1--1", "D-M-Y", -1 - 1 - 2);
        assert_greg!("-1-1-2", "Y-M-D", -1 - 1 - 2);
        assert_greg!("1/2/-1", "M/D/Y", -1 - 1 - 2);
    }

    #[test]
    fn plus_is_not_a_sign_but_a_separator() {
        assert_greg!("2-1-+2000", "D-M-+YYYY", 2000 - 1 - 2);

        // fails, because + is not a sign and there are
        // no separators allowed before the first component
        assert_parse_error!("+2000-1-2");

        // Note: no longer US order because second separator
        // is no longer a sinlge slash!
        assert_greg!("1/2/+0", "D/M/+Y", 0 - 2 - 1);
    }

    #[test]
    fn us_special_case_only_single_slash() {
        assert_greg!("2/1/2000", "M/D/YYYY", 2000 - 2 - 1);
        assert_greg!("2 /1 /2000", "M /D /YYYY", 2000 - 2 - 1);
        assert_greg!("2/ 1/ 2000", "M/ D/ YYYY", 2000 - 2 - 1);
        assert_greg!("2 / 1 / 2000", "M / D / YYYY", 2000 - 2 - 1);

        // both have to be a single slash.
        // if at least one of the separators is not a single slash
        // we no longer interpret it as a US format date
        assert_greg!("2 // 1 / 2000", "D // M / YYYY", 2000 - 1 - 2);
    }

    #[test]
    fn weird_but_valid_cases() {
        // Note: Don't invent date formats like that and try to stick
        // to typical stuff. These tests are here to detect breaking changes

        assert_greg!(" 2&%01()--1 ", "D&%MM()-Y", -1 - 1 - 2);
        assert_greg!("2 --- 1 --- 0", "D --- M --- Y", 0 - 1 - 2);
        assert_greg!("2 --- 1 --- 0", "D --- M --- Y", 0 - 1 - 2);
        assert_greg!("23, 12 2004", "DD, MM YYYY", 2004 - 12 - 23);
        assert_greg!("-11 - 11 - 11", "YY - MM - DD", -11 - 11 - 11);

        assert_greg!(
            "01######02######2005",
            "DD######MM######YYYY",
            2005 - 02 - 01
        );
    }

    #[test]
    fn seven_long_spacers_are_not_allowed() {
        assert_parse_error!("01#######02#2005");
        assert_parse_error!("01#02#######2005");
    }

    #[test]
    fn long_day_month_components_fail_to_parse() {
        assert_parse_error!("01-001-2000");
        assert_parse_error!("001-01-2000");
    }
}