sac13 0.1.1

The reference implementation for the SAC13 calendar system.
Documentation
use crate::{
    ComponentOrder, Date, DateComponentSeparator, GregorianDate, ParsedFormat, ParsedSacOrGreg,
    parse::ParsedComponent,
};
use core::{fmt::Display, num::NonZero};

/// EitherDate is either a [GregorianDate] or SAC13 [Date].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SacOrGreg {
    /// Variant wrapping a [GregorianDate].
    Gregorian(GregorianDate),
    /// Variant wrapping a SAC13 [Date].
    Sac13(Date),
}

impl SacOrGreg {
    /// If the inner variant is [SacOrGreg::Gregorian] it returns that, otherwise [None].
    pub fn greg(&self) -> Option<GregorianDate> {
        match self {
            Self::Gregorian(x) => Some(*x),
            _ => None,
        }
    }

    /// If the inner variant is [SacOrGreg::Sac13] it returns that, otherwise [None].
    pub fn sac13(&self) -> Option<Date> {
        match self {
            Self::Sac13(x) => Some(*x),
            _ => None,
        }
    }

    /// Parses various SAC13 and Gregorian Calendar formats.
    ///
    /// ## Example
    /// ```
    /// use sac13::prelude::*;
    ///
    /// let parsed = SacOrGreg::parse_str("2009-07-03").unwrap();
    /// ```
    ///
    /// ## Supported Formats
    ///
    /// ### Nomenclature
    /// ```text
    ///     ┌── leading whitespace
    ///    ///     │   Separators   trailing whitespace
    ///  ┌──┴─┐  ┌┤  │     ┌───┴───┐
    /// "      23, 12 -2007         "
    ///        └┤  └┤ └─┬─┘   
    ///         Components
    /// ```
    ///
    /// ### Exactly three numeric components
    /// This function only allows dates that are represented by three numeric components,
    /// including SAC13 years with millenium indicator (technically it's a base-26 digit).
    /// In fact SAC13 years must be written with a millenium indicator because that is used
    /// to disambiguate between SAC13 and Gregorian Calendar dates. Components must not
    /// have thousands separators (or similar group separators).
    ///
    /// Textual representations/components like Gregorian weekdays
    /// or full month names are not supported!
    ///
    /// ### Year first or last
    /// The year component has to either be the first, or the third (and last) component.
    /// It must not be the second (middle) component.
    /// The allowed component orders are defined in the [ComponentOrder] enum.
    ///
    /// If it's a SAC13 date the year must always be exactly four digits long `A000` - `Z999`.
    /// For Gregorian Dates if any component is negative or zero, it is automatically assumed to be the year component.
    /// Positive years must be at least three digits long so the component is clearly distinct from
    /// the month and day component. If the year is positive but less than 100 it must be written with leading zeros.
    ///
    /// Even though the actual supported formats allow for three digit Gregorian Calendar years,
    /// or even one digit if the year is negative, the best practice is to pad years to at least
    /// four digits.
    ///
    /// The position of the year component implicitly defines the order of month and day,
    /// because all components are either in acending or decending order.
    /// There is one special case though: If it's Gregorian Calendar date and the separators between all
    /// components are slashes, we assume the components to be a US formatted date with the order M/D/Y
    ///
    /// ### Separators
    /// All ASCII characters that are not letters or digits, are allowed as separators;
    /// they are one or more (at most six) characters long and can be pretty arbitrary.
    /// Again, even though you can use almost anything as separators doesn't mean you should.
    /// You should especially avoid multi-character separators that end with dashes,
    /// because they will be interpreted as a negative sign for the next component.
    /// `YYYY-MM-DD` and `YYYY - MM - DD` are, of course, fine but using ` -` as a separator,
    /// like in `YYYY -MM -DD`, will (obvously?) lead to said problem,
    /// because it will be interpreted as three components separated by spaces, with at least two
    /// of them being negative. Note that `+` is never interpreted as a sign.
    /// So, something like `DD.- +MM+$%&/\YYYY` can be parsed, but please don't do that!
    ///
    /// ### ASCII only
    /// The entire input must be valid ASCII. If you have some weird format,
    /// for example with emdash separators, you must replace them,
    /// for example with regular hyphens, before parsing.
    ///
    /// ### Whitespace
    /// Only spaces (0x20) are considered "whitespace" by this method. Leading and trailing whitespace
    /// are always trimmed, no matter how long. Whitespace in separators are considered part of the
    /// separator and recorded as is in the [DateComponentSeparator].
    #[must_use]
    pub fn parse_str(input: &str) -> Option<ParsedSacOrGreg> {
        if !input.is_ascii() {
            return None;
        }

        let input_bytes = input.as_bytes();

        let mut stream = crate::iterhelp::ByteSliceIter {
            position: 0,
            slice: input_bytes,
        };

        // trim leading spaces
        stream.skip_bytes(b' ');

        let c1 = ParsedComponent::parse(&mut stream)?;
        let s1 = DateComponentSeparator::parse(&mut stream)?;
        let c2 = ParsedComponent::parse(&mut stream)?;
        let s2 = DateComponentSeparator::parse(&mut stream)?;
        let c3 = ParsedComponent::parse(&mut stream)?;

        // trim trailing spaces
        stream.skip_bytes(b' ');

        // assert end of stream
        if stream.peek().is_some() {
            // if it wasn't he end, something was wrong with the input
            return None;
        }

        if c2.is_year_comp() {
            // The middle part can never be the year
            return None;
        }

        let year_first = c1.is_year_comp();
        let year_last = c3.is_year_comp();

        if year_first == year_last {
            // either both ends or neither seem to be a year which is not allowed
            return None;
        }

        // determine sort order
        let (year, month, day, order) = if year_first {
            (c1, c2, c3, ComponentOrder::YMD)
        } else if c3.is_gregorian_year() && s1.is_single_slash() && s2.is_single_slash() {
            // edge case to support typical Gregorian Calendar US format DD/MM/YYYY
            (c3, c1, c2, ComponentOrder::MDY)
        } else {
            (c3, c2, c1, ComponentOrder::DMY)
        };

        if day.letter || month.letter {
            return None;
        }

        if !(1..=31).contains(&day.value) || !(1..=13).contains(&month.value) {
            return None;
        }

        let format = ParsedFormat {
            separators: [s1, s2],
            comp_ord: order,
            len_day: NonZero::new(day.char_cnt)?,
            len_month: NonZero::new(month.char_cnt)?,
            len_year: NonZero::new(year.char_cnt)?,
        };

        let day = day.value as u8;
        let month = month.value as u8;

        let date = if year.is_sac13_year() {
            SacOrGreg::Sac13(Date::from_ymd_untyped(year.value as u16, month, day)?)
        } else {
            SacOrGreg::Gregorian(GregorianDate::from_ymd(year.value, month, day)?)
        };

        Some(ParsedSacOrGreg { date, format })
    }
}

impl Display for SacOrGreg {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            SacOrGreg::Gregorian(x) => write!(f, "{x}"),
            SacOrGreg::Sac13(x) => write!(f, "{x}"),
        }
    }
}