use crate::calendar::gregorian::Gregorian;
use crate::calendar::prelude::CommonDate;
use crate::calendar::prelude::CommonWeekOfYear;
use crate::calendar::prelude::Quarter;
use crate::calendar::prelude::ToFromCommonDate;
use crate::calendar::prelude::ToFromOrdinalDate;
use crate::calendar::AllowYearZero;
use crate::calendar::CalendarMoment;
use crate::calendar::HasLeapYears;
use crate::clock::TimeOfDay;
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;
use crate::day_cycle::Weekday;
use crate::CalendarError;
use num_traits::FromPrimitive;
use std::cmp::Ordering;
use std::num::NonZero;
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct ISO {
year: i32,
week: NonZero<u8>,
day: Weekday,
}
impl ISO {
pub fn try_new(year: i32, week: u8, day: Weekday) -> Result<Self, CalendarError> {
if week < 1 || week > 53 || (week == 53 && !Self::is_leap(year)) {
return Err(CalendarError::InvalidWeek);
}
Ok(ISO {
year: year,
week: NonZero::<u8>::new(week).expect("Checked in if"),
day: day,
})
}
pub fn year(self) -> i32 {
self.year
}
pub fn week(self) -> NonZero<u8> {
self.week
}
pub fn day(self) -> Weekday {
self.day
}
pub fn day_num(self) -> u8 {
(self.day as u8).adjusted_remainder(7)
}
pub fn new_year(year: i32) -> Self {
ISO::try_new(year, 1, Weekday::Monday).expect("Week 1 known to be valid")
}
}
impl PartialOrd for ISO {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.year != other.year {
self.year.partial_cmp(&other.year)
} else if self.week != other.week {
self.week.partial_cmp(&other.week)
} else {
let self_day = (self.day as i64).adjusted_remainder(7);
let other_day = (other.day as i64).adjusted_remainder(7);
self_day.partial_cmp(&other_day)
}
}
}
impl AllowYearZero for ISO {}
impl CalculatedBounds for ISO {}
impl Epoch for ISO {
fn epoch() -> Fixed {
Gregorian::epoch()
}
}
impl HasLeapYears for ISO {
fn is_leap(i_year: i32) -> bool {
let jan1 = Gregorian::try_year_start(i_year)
.expect("Year known to be valid")
.convert::<Weekday>();
let dec31 = Gregorian::try_year_end(i_year)
.expect("Year known to be valid")
.convert::<Weekday>();
jan1 == Weekday::Thursday || dec31 == Weekday::Thursday
}
}
impl FromFixed for ISO {
fn from_fixed(fixed_date: Fixed) -> ISO {
let date = fixed_date.get_day_i();
let approx = Gregorian::ordinal_from_fixed(Fixed::cast_new(date - 3)).year;
let next = ISO::new_year(approx + 1).to_fixed().get_day_i();
let year = if date >= next { approx + 1 } else { approx };
let current = ISO::new_year(year).to_fixed().get_day_i();
let week = (date - current).div_euclid(7) + 1;
debug_assert!(week < 55 && week > 0);
let day = Weekday::from_u8(date.modulus(7) as u8).expect("In range due to modulus.");
ISO::try_new(year, week as u8, day).expect("Week known to be valid")
}
}
impl ToFixed for ISO {
fn to_fixed(self) -> Fixed {
let g = CommonDate::new(self.year - 1, 12, 28);
let w = NonZero::<i16>::from(self.week);
let day_i = (self.day as i64).adjusted_remainder(7);
let result = Gregorian::try_from_common_date(g)
.expect("month 12, day 28 is always valid for Gregorian")
.nth_kday(w, Weekday::Sunday)
.get_day_i()
+ day_i;
Fixed::cast_new(result)
}
}
impl Quarter for ISO {
fn quarter(self) -> NonZero<u8> {
NonZero::new(((self.week().get() - 1) / 14) + 1).expect("(m - 1)/14 > -1")
}
}
pub type ISOMoment = CalendarMoment<ISO>;
impl ISOMoment {
pub fn year(self) -> i32 {
self.date().year()
}
pub fn week(self) -> NonZero<u8> {
self.date().week()
}
pub fn day(self) -> Weekday {
self.date().day()
}
pub fn day_num(self) -> u8 {
self.date().day_num()
}
pub fn new_year(year: i32) -> Self {
ISOMoment::new(ISO::new_year(year), TimeOfDay::midnight())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calendar::prelude::HasLeapYears;
use crate::calendar::prelude::ToFromCommonDate;
use crate::day_count::FIXED_MAX;
use proptest::proptest;
const MAX_YEARS: i32 = (FIXED_MAX / 365.25) as i32;
#[test]
fn week_of_impl() {
let g = Gregorian::try_from_common_date(CommonDate::new(2025, 5, 15))
.unwrap()
.to_fixed();
let i = ISO::from_fixed(g);
assert_eq!(i.week().get(), 20);
}
#[test]
fn epoch() {
let i0 = ISO::from_fixed(Fixed::cast_new(0));
let i1 = ISO::from_fixed(Fixed::cast_new(-1));
assert!(i0 > i1, "i0: {:?}, i1: {:?}", i0, i1);
}
proptest! {
#[test]
fn first_week(year in -MAX_YEARS..MAX_YEARS) {
let g = Gregorian::try_from_common_date(CommonDate {
year,
month: 1,
day: 1,
}).unwrap();
let f = g.to_fixed();
let w = Weekday::from_fixed(f);
let i = ISO::from_fixed(f);
let expected_week: u8 = match w {
Weekday::Monday => 1,
Weekday::Tuesday => 1,
Weekday::Wednesday => 1,
Weekday::Thursday => 1,
Weekday::Friday => 53,
Weekday::Saturday => if Gregorian::is_leap(year - 1) {53} else {52},
Weekday::Sunday => 52,
};
let expected_year: i32 = if expected_week == 1 { year } else { year - 1 };
assert_eq!(i.day(), w);
assert_eq!(i.week().get(), expected_week);
assert_eq!(i.year(), expected_year);
if expected_week == 53 {
assert!(ISO::is_leap(i.year()));
}
}
#[test]
fn fixed_week_numbers(y1 in -MAX_YEARS..MAX_YEARS, y2 in -MAX_YEARS..MAX_YEARS) {
let targets = [
(1, 4), (1, 11), (1, 18), (1, 25),
(2, 1), (2, 8), (2, 15), (2, 22),
];
for target in targets {
let g1 = Gregorian::try_from_common_date(CommonDate {
year: y1,
month: target.0,
day: target.1,
}).unwrap();
let g2 = Gregorian::try_from_common_date(CommonDate {
year: y2,
month: target.0,
day: target.1,
}).unwrap();
assert_eq!(g1.convert::<ISO>().week(), g2.convert::<ISO>().week());
}
}
}
}