rabbit-time 0.1.2

Rust implementations of Neralie and Arvelie time formats.
Documentation
use std::fmt::{Debug, Display};

use chrono::{Datelike, NaiveDate};

/// Represents a date on the [Arvelie](https://wiki.xxiivv.com/site/arvelie.html) calendar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ArvelieDate {
    /// The Gregorian calendar-year at which you began counting.
    pub start_year: i32,
    /// The year, as of whenever you started counting.
    pub year: i32,
    /// The day of the year.
    pub sub_year: SubYear,
}
impl ArvelieDate {
    /// Returns the 1-indexed day of the year.
    pub fn day_of_year(&self) -> u16 {
        match self.sub_year {
            SubYear::LeapDay => 366,
            SubYear::YearDay => 365,
            SubYear::NormalSubYear(sub_year) => {
                sub_year.day as u16 + sub_year.month as u16 * 14 + 1
            }
        }
    }
    /// Returns the year, on the Gregorian calendar.
    pub fn gregorian_year(&self) -> i32 {
        self.start_year + self.year
    }
}
impl Display for ArvelieDate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:0>2}", self.year)?;
        write!(f, "{}", self.sub_year)
    }
}

/// The day and month of an [ArvelieDate].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SubYear {
    /// A normal day/month pair.
    NormalSubYear(NormalSubYear),
    /// Year day. The last day of a normal year.
    YearDay,
    /// Leap day. The last day of a leap year.
    LeapDay,
}
impl SubYear {
    /// Returns the month as a number from 0 to 26.
    pub fn month(&self) -> u8 {
        match self {
            SubYear::NormalSubYear(normal_sub_year) => normal_sub_year.month,
            SubYear::YearDay | SubYear::LeapDay => 26,
        }
    }
    /// Returns the month as a letter of the alphabet.
    pub fn format_month(&self) -> String {
        match self {
            SubYear::NormalSubYear(normal_sub_year) => normal_sub_year.format_month(),
            SubYear::YearDay | SubYear::LeapDay => "+".to_string(),
        }
    }
    /// Returns the day of the month as a number from 0 to 13.
    pub fn day(&self) -> u8 {
        match self {
            SubYear::NormalSubYear(normal_sub_year) => normal_sub_year.day,
            SubYear::YearDay => 0,
            SubYear::LeapDay => 1,
        }
    }
    /// Returns the day of the month, formatted as a string.
    pub fn format_day(&self) -> String {
        format!("{:0>2}", self.day())
    }
}
impl Default for SubYear {
    fn default() -> Self {
        SubYear::NormalSubYear(NormalSubYear::default())
    }
}
impl Display for SubYear {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SubYear::NormalSubYear(normal_sub_year) => write!(f, "{normal_sub_year}"),
            SubYear::YearDay => f.write_str("+00"),
            SubYear::LeapDay => f.write_str("+01"),
        }
    }
}

/// A normal day/month pair for an [ArvelieDate].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct NormalSubYear {
    /// The month of the year. There are 26 months in a year.
    pub month: u8,
    /// The day of the month. There are 14 days in a month.
    pub day: u8,
}
impl NormalSubYear {
    /// Returns the month as a letter of the alphabet.
    pub fn format_month(&self) -> String {
        let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect::<Vec<char>>();
        let letter = chars.get(self.month as usize).unwrap();
        letter.to_string()
    }
}
impl Display for NormalSubYear {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}{:0>2}", self.format_month(), self.day)
    }
}

/// The inputs for converting a [NaiveDate] to an [ArvelieDate].
pub struct NaiveDateAndStartYear {
    /// The date to be converted.
    pub date: NaiveDate,
    /// The year at which the converted date will have started counting.
    pub start_year: i32,
}

impl From<NaiveDateAndStartYear> for ArvelieDate {
    fn from(value: NaiveDateAndStartYear) -> Self {
        let NaiveDateAndStartYear { date, start_year } = value;
        let year = date.year() - start_year;
        let day_of_year = date.ordinal0() as i32;
        let sub_year = match day_of_year {
            365 => SubYear::LeapDay,
            364 => SubYear::YearDay,
            _ => {
                let month = day_of_year / 14;
                let day = day_of_year - month * 14;
                SubYear::NormalSubYear(NormalSubYear {
                    month: month as u8,
                    day: day as u8,
                })
            }
        };

        ArvelieDate {
            start_year,
            year,
            sub_year,
        }
    }
}

impl From<ArvelieDate> for NaiveDate {
    fn from(value: ArvelieDate) -> Self {
        NaiveDate::from_yo_opt(value.gregorian_year(), value.day_of_year() as u32).unwrap()
    }
}