use crate::calendar::gregorian::Gregorian;
use crate::calendar::prelude::CommonDate;
use crate::calendar::prelude::HasLeapYears;
use crate::calendar::prelude::Perennial;
use crate::calendar::prelude::Quarter;
use crate::calendar::prelude::ToFromCommonDate;
use crate::calendar::AllowYearZero;
use crate::calendar::CalendarMoment;
use crate::calendar::HasEpagemonae;
use crate::calendar::OrdinalDate;
use crate::calendar::ToFromOrdinalDate;
use crate::common::error::CalendarError;
use crate::common::math::TermNum;
use crate::day_count::BoundedDayCount;
use crate::day_count::CalculatedBounds;
use crate::day_count::Epoch;
use crate::day_count::Fixed;
use crate::day_count::FromFixed;
use crate::day_count::ToFixed;
#[allow(unused_imports)] use num_traits::FromPrimitive;
use std::num::NonZero;
const FRENCH_EPOCH_GREGORIAN: CommonDate = CommonDate {
year: 1792,
month: 9,
day: 22,
};
const NON_MONTH: u8 = 13;
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
pub enum FrenchRevMonth {
Vendemiaire = 1,
Brumaire,
Frimaire,
Nivose,
Pluviose,
Ventose,
Germinal,
Floreal,
Prairial,
Messidor,
Thermidor,
Fructidor,
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
pub enum FrenchRevWeekday {
Primidi = 1,
Duodi,
Tridi,
Quartidi,
Quintidi,
Sextidi,
Septidi,
Octidi,
Nonidi,
Decadi,
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
pub enum Sansculottide {
Vertu = 1,
Genie,
Travail,
Opinion,
Recompense,
Revolution,
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
pub struct FrenchRevArith<const L: bool>(CommonDate);
impl<const L: bool> AllowYearZero for FrenchRevArith<L> {}
impl<const L: bool> ToFromOrdinalDate for FrenchRevArith<L> {
fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
let correction = if Self::is_leap(ord.year) { 1 } else { 0 };
if ord.day_of_year > 0 && ord.day_of_year <= (365 + correction) {
Ok(())
} else {
Err(CalendarError::InvalidDayOfYear)
}
}
fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
let date = fixed_date.get_day_i();
let epoch = Self::epoch().get_day_i();
let approx = ((4000 * (date - epoch + 2)).div_euclid(1460969) + 1) as i32;
let approx_start = Self(CommonDate::new(approx, 1, 1)).to_fixed().get_day_i();
let year = if date < approx_start {
approx - 1
} else {
approx
};
let year_start = Self(CommonDate::new(year, 1, 1)).to_fixed().get_day_i();
let doy = (date - year_start + 1) as u16;
OrdinalDate {
year: year,
day_of_year: doy,
}
}
fn to_ordinal(self) -> OrdinalDate {
let offset_m = 30 * ((self.0.month as u16) - 1);
let offset_d = self.0.day as u16;
OrdinalDate {
year: self.0.year,
day_of_year: offset_m + offset_d,
}
}
fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
let month = (1 + (ord.day_of_year - 1).div_euclid(30)) as u8;
let month_start = Self(CommonDate::new(ord.year, month, 1)).to_ordinal();
let day = (1 + ord.day_of_year - month_start.day_of_year) as u8;
FrenchRevArith(CommonDate::new(ord.year, month, day))
}
}
impl<const L: bool> FrenchRevArith<L> {
pub fn is_adjusted(self) -> bool {
L
}
}
impl<const L: bool> HasEpagemonae<Sansculottide> for FrenchRevArith<L> {
fn epagomenae(self) -> Option<Sansculottide> {
if self.0.month == NON_MONTH {
Sansculottide::from_u8(self.0.day)
} else {
None
}
}
fn epagomenae_count(f_year: i32) -> u8 {
if FrenchRevArith::<L>::is_leap(f_year) {
6
} else {
5
}
}
}
impl<const L: bool> Perennial<FrenchRevMonth, FrenchRevWeekday> for FrenchRevArith<L> {
fn weekday(self) -> Option<FrenchRevWeekday> {
if self.0.month == NON_MONTH {
None
} else {
FrenchRevWeekday::from_i64((self.0.day as i64).adjusted_remainder(10))
}
}
fn days_per_week() -> u8 {
10
}
fn weeks_per_month() -> u8 {
3
}
}
impl<const L: bool> HasLeapYears for FrenchRevArith<L> {
fn is_leap(year: i32) -> bool {
let f_year = if L { year + 1 } else { year };
let m4 = f_year.modulus(4);
let m400 = f_year.modulus(400);
let m4000 = f_year.modulus(4000);
m4 == 0 && (m400 != 100 && m400 != 200 && m400 != 300) && m4000 != 0
}
}
impl<const L: bool> CalculatedBounds for FrenchRevArith<L> {}
impl<const L: bool> Epoch for FrenchRevArith<L> {
fn epoch() -> Fixed {
Gregorian::try_from_common_date(FRENCH_EPOCH_GREGORIAN)
.expect("Epoch known to be valid")
.to_fixed()
}
}
impl<const L: bool> FromFixed for FrenchRevArith<L> {
fn from_fixed(fixed_date: Fixed) -> FrenchRevArith<L> {
let ord = Self::ordinal_from_fixed(fixed_date);
Self::from_ordinal_unchecked(ord)
}
}
impl<const L: bool> ToFixed for FrenchRevArith<L> {
fn to_fixed(self) -> Fixed {
let year = self.0.year as i64;
let y_adj = if L { 1 } else { 0 };
let offset_e = Self::epoch().get_day_i() - 1;
let offset_y = 365 * (year - 1);
let offset_leap = (year + y_adj - 1).div_euclid(4) - (year + y_adj - 1).div_euclid(100)
+ (year + y_adj - 1).div_euclid(400)
- (year + y_adj - 1).div_euclid(4000);
let ord = self.to_ordinal().day_of_year as i64;
Fixed::cast_new(offset_e + offset_y + offset_leap + ord)
}
}
impl<const L: bool> ToFromCommonDate<FrenchRevMonth> for FrenchRevArith<L> {
fn to_common_date(self) -> CommonDate {
self.0
}
fn from_common_date_unchecked(date: CommonDate) -> Self {
debug_assert!(Self::valid_ymd(date).is_ok());
Self(date)
}
fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
if date.month < 1 || date.month > NON_MONTH {
Err(CalendarError::InvalidMonth)
} else if date.day < 1 {
Err(CalendarError::InvalidDay)
} else if date.month < NON_MONTH && date.day > 30 {
Err(CalendarError::InvalidDay)
} else if date.month == NON_MONTH
&& date.day > FrenchRevArith::<L>::epagomenae_count(date.year)
{
Err(CalendarError::InvalidDay)
} else {
Ok(())
}
}
fn year_end_date(year: i32) -> CommonDate {
CommonDate::new(year, NON_MONTH, FrenchRevArith::<L>::epagomenae_count(year))
}
fn month_length(_year: i32, _month: FrenchRevMonth) -> u8 {
30
}
}
impl<const L: bool> Quarter for FrenchRevArith<L> {
fn quarter(self) -> NonZero<u8> {
let m = self.to_common_date().month;
if m == NON_MONTH {
NonZero::new(4 as u8).expect("4 != 0")
} else {
NonZero::new(((m - 1) / 3) + 1).expect("(m-1)/3 > -1")
}
}
}
pub type FrenchRevArithMoment<const L: bool> = CalendarMoment<FrenchRevArith<L>>;
#[cfg(test)]
mod tests {
use super::*;
use proptest::proptest;
#[test]
fn leaps() {
assert!(FrenchRevArith::<true>::is_leap(3));
assert!(FrenchRevArith::<true>::is_leap(7));
assert!(FrenchRevArith::<true>::is_leap(11));
assert!(FrenchRevArith::<false>::is_leap(4));
assert!(FrenchRevArith::<false>::is_leap(8));
assert!(FrenchRevArith::<false>::is_leap(12));
}
#[test]
fn revolutionary_events() {
let event_list = [
(
CommonDate::new(2, FrenchRevMonth::Prairial as u8, 22),
CommonDate::new(2, FrenchRevMonth::Prairial as u8, 22),
CommonDate::new(1794, 6, 10),
),
(
CommonDate::new(2, FrenchRevMonth::Thermidor as u8, 9),
CommonDate::new(2, FrenchRevMonth::Thermidor as u8, 9),
CommonDate::new(1794, 7, 27),
),
(
CommonDate::new(4, FrenchRevMonth::Vendemiaire as u8, 13),
CommonDate::new(4, FrenchRevMonth::Vendemiaire as u8, 13 + 1), CommonDate::new(1795, 10, 5),
),
(
CommonDate::new(5, FrenchRevMonth::Fructidor as u8, 18),
CommonDate::new(5, FrenchRevMonth::Fructidor as u8, 18),
CommonDate::new(1797, 9, 4),
),
(
CommonDate::new(6, FrenchRevMonth::Floreal as u8, 22),
CommonDate::new(6, FrenchRevMonth::Floreal as u8, 22),
CommonDate::new(1798, 5, 11),
),
(
CommonDate::new(7, FrenchRevMonth::Prairial as u8, 30),
CommonDate::new(7, FrenchRevMonth::Prairial as u8, 30),
CommonDate::new(1799, 6, 18),
),
(
CommonDate::new(8, FrenchRevMonth::Brumaire as u8, 18),
CommonDate::new(8, FrenchRevMonth::Brumaire as u8, 18 + 1), CommonDate::new(1799, 11, 9),
),
(
CommonDate::new(79, FrenchRevMonth::Floreal as u8, 16),
CommonDate::new(79, FrenchRevMonth::Floreal as u8, 16),
CommonDate::new(1871, 5, 6),
),
];
for pair in event_list {
let df0 = FrenchRevArith::<true>::try_from_common_date(pair.0)
.unwrap()
.to_fixed();
let df1 = FrenchRevArith::<false>::try_from_common_date(pair.1)
.unwrap()
.to_fixed();
let dg = Gregorian::try_from_common_date(pair.2).unwrap().to_fixed();
assert_eq!(df0, dg);
assert_eq!(df1, dg);
}
}
proptest! {
#[test]
fn align_to_gregorian(year in 0..100) {
let d_list = [
( CommonDate{ year, month: 1, day: 1 }, 9, 22, 24),
( CommonDate{ year, month: 2, day: 1 }, 10, 22, 24),
( CommonDate{ year, month: 3, day: 1 }, 11, 21, 23),
( CommonDate{ year, month: 4, day: 1 }, 12, 21, 23),
( CommonDate{ year, month: 5, day: 1 }, 1, 20, 22),
( CommonDate{ year, month: 6, day: 1 }, 2, 19, 21),
( CommonDate{ year, month: 7, day: 1 }, 3, 21, 22),
( CommonDate{ year, month: 8, day: 1 }, 4, 20, 21),
( CommonDate{ year, month: 9, day: 1 }, 5, 20, 21),
( CommonDate{ year, month: 10, day: 1 }, 6, 19, 20),
( CommonDate{ year, month: 11, day: 1 }, 7, 19, 20),
( CommonDate{ year, month: 12, day: 1 }, 8, 18, 19),
];
for item in d_list {
let r0 = FrenchRevArith::<true>::try_from_common_date(item.0).unwrap();
let f0 = r0.to_fixed();
let r1 = FrenchRevArith::<false>::try_from_common_date(item.0).unwrap();
let f1 = r1.to_fixed();
let g = Gregorian::from_fixed(f0);
let gc = g.to_common_date();
assert_eq!(gc.month, item.1);
assert!(item.2 <= gc.day && item.3 >= gc.day);
assert!((f1.get_day_i() - f0.get_day_i()).abs() < 2);
}
}
}
}