use crate::convert;
use crate::gregorian::{GregorianDate, GregorianDateError};
use crate::leap::days_in_shahanshahi_month;
use core::fmt;
pub const LEGAL_ERA_START_YEAR: i32 = 2535;
pub const LEGAL_ERA_START_MONTH: u8 = 1;
pub const LEGAL_ERA_START_DAY: u8 = 1;
pub const LEGAL_ERA_END_YEAR: i32 = 2537;
pub const LEGAL_ERA_END_MONTH: u8 = 6;
pub const LEGAL_ERA_END_DAY: u8 = 10;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ShahanshahiDate {
year: i32,
month: u8,
day: u8,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShahanshahiDateError {
MonthOutOfRange { month: u8 },
DayOutOfRange {
year: i32,
month: u8,
day: u8,
max_day: u8,
},
OutOfLegalEra { year: i32, month: u8, day: u8 },
InvalidGregorianDate(GregorianDateError),
}
impl fmt::Display for ShahanshahiDateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MonthOutOfRange { month } => {
write!(f, "month {month} is not in the range 1..=12 (Farvardin..=Esfand)")
}
Self::DayOutOfRange {
year,
month,
day,
max_day,
} => write!(
f,
"day {day} is invalid for Shahanshahi {year}-{month:02} (max day {max_day})",
),
Self::OutOfLegalEra {
year,
month,
day,
} => write!(
f,
"date {year}-{month:02}-{day:02} is outside the default legal Shahanshahi civil era \
(inclusive {sy}-{sm:02}-{sd:02} ..= {ey}-{em:02}-{ed:02}; see SPEC.md and crate docs)",
sy = LEGAL_ERA_START_YEAR,
sm = LEGAL_ERA_START_MONTH,
sd = LEGAL_ERA_START_DAY,
ey = LEGAL_ERA_END_YEAR,
em = LEGAL_ERA_END_MONTH,
ed = LEGAL_ERA_END_DAY,
),
Self::InvalidGregorianDate(err) => {
write!(f, "invalid Gregorian date from interop: {err}")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ShahanshahiDateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidGregorianDate(err) => Some(err),
_ => None,
}
}
}
impl From<GregorianDateError> for ShahanshahiDateError {
fn from(err: GregorianDateError) -> Self {
Self::InvalidGregorianDate(err)
}
}
impl ShahanshahiDate {
pub fn try_new(year: i32, month: u8, day: u8) -> Result<Self, ShahanshahiDateError> {
Self::validate_ymd(year, month, day)?;
if !is_within_legal_era(year, month, day) {
return Err(ShahanshahiDateError::OutOfLegalEra { year, month, day });
}
Ok(Self { year, month, day })
}
#[cfg(feature = "proleptic")]
pub fn try_new_proleptic(year: i32, month: u8, day: u8) -> Result<Self, ShahanshahiDateError> {
Self::validate_ymd(year, month, day)?;
Ok(Self { year, month, day })
}
fn validate_ymd(year: i32, month: u8, day: u8) -> Result<(), ShahanshahiDateError> {
if !(1..=12).contains(&month) {
return Err(ShahanshahiDateError::MonthOutOfRange { month });
}
let max_day = days_in_month(year, month);
if !(1..=max_day).contains(&day) {
return Err(ShahanshahiDateError::DayOutOfRange {
year,
month,
day,
max_day,
});
}
Ok(())
}
#[inline]
pub const fn year(self) -> i32 {
self.year
}
#[inline]
pub const fn month(self) -> u8 {
self.month
}
#[inline]
pub const fn day(self) -> u8 {
self.day
}
#[inline]
pub fn to_gregorian(self) -> GregorianDate {
convert::shahanshahi_to_gregorian(self.year, self.month, self.day)
}
pub fn try_from_gregorian(date: GregorianDate) -> Result<Self, ShahanshahiDateError> {
let (y, m, d) = convert::gregorian_to_shahanshahi_ymd(date);
Self::try_new(y, m, d)
}
#[cfg(feature = "proleptic")]
pub fn try_from_gregorian_proleptic(date: GregorianDate) -> Result<Self, ShahanshahiDateError> {
let (y, m, d) = convert::gregorian_to_shahanshahi_ymd(date);
Self::try_new_proleptic(y, m, d)
}
}
fn days_in_month(year: i32, month: u8) -> u8 {
days_in_shahanshahi_month(year, month)
}
fn is_within_legal_era(year: i32, month: u8, day: u8) -> bool {
let candidate = (year, i32::from(month), i32::from(day));
let start = (
LEGAL_ERA_START_YEAR,
i32::from(LEGAL_ERA_START_MONTH),
i32::from(LEGAL_ERA_START_DAY),
);
let end = (
LEGAL_ERA_END_YEAR,
i32::from(LEGAL_ERA_END_MONTH),
i32::from(LEGAL_ERA_END_DAY),
);
candidate >= start && candidate <= end
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn era_anchor_ok() {
let d = ShahanshahiDate::try_new(2535, 1, 1).unwrap();
assert_eq!((d.year(), d.month(), d.day()), (2535, 1, 1));
}
#[test]
fn era_last_day_ok() {
ShahanshahiDate::try_new(2537, 6, 10).unwrap();
}
#[test]
fn month_out_of_range() {
assert!(matches!(
ShahanshahiDate::try_new(2535, 0, 1),
Err(ShahanshahiDateError::MonthOutOfRange { .. })
));
assert!(matches!(
ShahanshahiDate::try_new(2535, 13, 1),
Err(ShahanshahiDateError::MonthOutOfRange { .. })
));
}
#[test]
fn day_out_of_range_mehr() {
assert!(matches!(
ShahanshahiDate::try_new(2535, 7, 31),
Err(ShahanshahiDateError::DayOutOfRange { max_day: 30, .. })
));
}
#[test]
fn esfand_29_in_common_year() {
ShahanshahiDate::try_new(2535, 12, 29).unwrap();
assert!(matches!(
ShahanshahiDate::try_new(2535, 12, 30),
Err(ShahanshahiDateError::DayOutOfRange { max_day: 29, .. })
));
}
#[cfg(feature = "proleptic")]
#[test]
fn proleptic_leap_esfand_30() {
ShahanshahiDate::try_new_proleptic(2534, 12, 30).unwrap();
}
#[test]
fn before_era_rejected() {
assert!(matches!(
ShahanshahiDate::try_new(2534, 12, 29),
Err(ShahanshahiDateError::OutOfLegalEra { .. })
));
}
#[test]
fn after_era_rejected() {
assert!(matches!(
ShahanshahiDate::try_new(2537, 6, 11),
Err(ShahanshahiDateError::OutOfLegalEra { .. })
));
assert!(matches!(
ShahanshahiDate::try_new(2537, 7, 1),
Err(ShahanshahiDateError::OutOfLegalEra { .. })
));
}
#[test]
fn ordering() {
let a = ShahanshahiDate::try_new(2535, 1, 1).unwrap();
let b = ShahanshahiDate::try_new(2535, 12, 29).unwrap();
assert!(a < b);
}
#[test]
fn to_gregorian_anchor_matches_spec() {
use crate::GregorianDate;
let sh = ShahanshahiDate::try_new(2535, 1, 1).unwrap();
let g = sh.to_gregorian();
assert_eq!(GregorianDate::try_new(1976, 3, 21).unwrap(), g);
}
#[test]
fn try_from_gregorian_round_trip_legal_era() {
use crate::GregorianDate;
let g = GregorianDate::try_new(1976, 7, 22).unwrap();
let sh = ShahanshahiDate::try_from_gregorian(g).unwrap();
assert_eq!((sh.year(), sh.month(), sh.day()), (2535, 4, 31));
assert_eq!(sh.to_gregorian(), g);
}
#[test]
fn try_from_gregorian_rejects_proleptic_only_date() {
use crate::GregorianDate;
let g = GregorianDate::try_new(1996, 3, 20).unwrap();
assert!(matches!(
ShahanshahiDate::try_from_gregorian(g),
Err(ShahanshahiDateError::OutOfLegalEra { .. })
));
}
#[cfg(feature = "proleptic")]
#[test]
fn try_from_gregorian_proleptic_accepts_outside_legal_era() {
use crate::GregorianDate;
let g = GregorianDate::try_new(1996, 3, 19).unwrap();
let sh = ShahanshahiDate::try_from_gregorian_proleptic(g).unwrap();
assert_eq!((sh.year(), sh.month(), sh.day()), (2554, 12, 29));
}
}