pub use crate::preferences::WeekPreferences;
use crate::preferences::FirstDay;
use crate::{error::RangeError, provider::*, types::Weekday};
use icu_provider::prelude::*;
const MIN_UNIT_DAYS: u16 = 14;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct WeekInformation {
pub first_weekday: Weekday,
pub weekend: WeekdaySet,
}
impl WeekInformation {
icu_provider::gen_buffer_data_constructors!(
(prefs: WeekPreferences) -> error: DataError,
);
#[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable<P>(provider: &P, prefs: WeekPreferences) -> Result<Self, DataError>
where
P: DataProvider<CalendarWeekV1> + ?Sized,
{
let locale = CalendarWeekV1::make_locale(prefs.locale_preferences);
provider
.load(DataRequest {
id: DataIdentifierBorrowed::for_locale(&locale),
..Default::default()
})
.map(|response| WeekInformation {
first_weekday: match prefs.first_weekday {
Some(FirstDay::Mon) => Weekday::Monday,
Some(FirstDay::Tue) => Weekday::Tuesday,
Some(FirstDay::Wed) => Weekday::Wednesday,
Some(FirstDay::Thu) => Weekday::Thursday,
Some(FirstDay::Fri) => Weekday::Friday,
Some(FirstDay::Sat) => Weekday::Saturday,
Some(FirstDay::Sun) => Weekday::Sunday,
_ => response.payload.get().first_weekday,
},
weekend: response.payload.get().weekend,
})
}
pub fn weekend(self) -> WeekdaySetIterator {
WeekdaySetIterator::new(self.first_weekday, self.weekend)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct WeekCalculator {
first_weekday: Weekday,
min_week_days: u8,
}
impl WeekCalculator {
pub(crate) const ISO: Self = Self {
first_weekday: Weekday::Monday,
min_week_days: 4,
};
fn weekday_index(self, weekday: Weekday) -> i8 {
(7 + (weekday as i8) - (self.first_weekday as i8)) % 7
}
pub(crate) fn week_of(
self,
num_days_in_previous_unit: u16,
num_days_in_unit: u16,
day: u16,
weekday: Weekday,
) -> Result<WeekOf, RangeError> {
let current = UnitInfo::new(
add_to_weekday(weekday, 1 - i32::from(day)),
num_days_in_unit,
)?;
match current.relative_week(self, day) {
RelativeWeek::LastWeekOfPreviousUnit => {
let previous = UnitInfo::new(
add_to_weekday(current.first_day, -i32::from(num_days_in_previous_unit)),
num_days_in_previous_unit,
)?;
Ok(WeekOf {
week: previous.num_weeks(self),
unit: RelativeUnit::Previous,
})
}
RelativeWeek::WeekOfCurrentUnit(w) => Ok(WeekOf {
week: w,
unit: RelativeUnit::Current,
}),
RelativeWeek::FirstWeekOfNextUnit => Ok(WeekOf {
week: 1,
unit: RelativeUnit::Next,
}),
}
}
}
fn add_to_weekday(weekday: Weekday, num_days: i32) -> Weekday {
let new_weekday = (7 + (weekday as i32) + (num_days % 7)) % 7;
Weekday::from_days_since_sunday(new_weekday as isize)
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[expect(clippy::enum_variant_names)]
enum RelativeWeek {
LastWeekOfPreviousUnit,
WeekOfCurrentUnit(u8),
FirstWeekOfNextUnit,
}
#[derive(Clone, Copy)]
struct UnitInfo {
first_day: Weekday,
duration_days: u16,
}
impl UnitInfo {
fn new(first_day: Weekday, duration_days: u16) -> Result<UnitInfo, RangeError> {
if duration_days < MIN_UNIT_DAYS {
return Err(RangeError {
field: "num_days_in_unit",
value: duration_days as i32,
min: MIN_UNIT_DAYS as i32,
max: i32::MAX,
});
}
Ok(UnitInfo {
first_day,
duration_days,
})
}
fn first_week_offset(self, calendar: WeekCalculator) -> i8 {
let first_day_index = calendar.weekday_index(self.first_day);
if 7 - first_day_index >= calendar.min_week_days as i8 {
-first_day_index
} else {
7 - first_day_index
}
}
fn num_weeks(self, calendar: WeekCalculator) -> u8 {
let first_week_offset = self.first_week_offset(calendar);
let num_days_including_first_week =
(self.duration_days as i32) - (first_week_offset as i32);
debug_assert!(
num_days_including_first_week >= 0,
"Unit is shorter than a week."
);
((num_days_including_first_week + 7 - (calendar.min_week_days as i32)) / 7) as u8
}
fn relative_week(self, calendar: WeekCalculator, day: u16) -> RelativeWeek {
let days_since_first_week =
i32::from(day) - i32::from(self.first_week_offset(calendar)) - 1;
if days_since_first_week < 0 {
return RelativeWeek::LastWeekOfPreviousUnit;
}
let week_number = (1 + days_since_first_week / 7) as u8;
if week_number > self.num_weeks(calendar) {
return RelativeWeek::FirstWeekOfNextUnit;
}
RelativeWeek::WeekOfCurrentUnit(week_number)
}
}
#[derive(Debug, PartialEq)]
#[allow(clippy::exhaustive_enums)] pub(crate) enum RelativeUnit {
Previous,
Current,
Next,
}
#[derive(Debug, PartialEq)]
#[allow(clippy::exhaustive_structs)] pub(crate) struct WeekOf {
pub week: u8,
pub unit: RelativeUnit,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WeekdaySetIterator {
first_weekday: Weekday,
current_day: Weekday,
weekend: WeekdaySet,
}
impl WeekdaySetIterator {
pub(crate) fn new(first_weekday: Weekday, weekend: WeekdaySet) -> Self {
WeekdaySetIterator {
first_weekday,
current_day: first_weekday,
weekend,
}
}
}
impl Iterator for WeekdaySetIterator {
type Item = Weekday;
fn next(&mut self) -> Option<Self::Item> {
while self.current_day.next_day() != self.first_weekday {
if self.weekend.contains(self.current_day) {
let result = self.current_day;
self.current_day = self.current_day.next_day();
return Some(result);
} else {
self.current_day = self.current_day.next_day();
}
}
if self.weekend.contains(self.current_day) {
self.weekend = WeekdaySet::new(&[]);
return Some(self.current_day);
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{types::DateDuration, types::Weekday, Date, RangeError};
static ISO_CALENDAR: WeekCalculator = WeekCalculator {
first_weekday: Weekday::Monday,
min_week_days: 4,
};
static AE_CALENDAR: WeekCalculator = WeekCalculator {
first_weekday: Weekday::Saturday,
min_week_days: 4,
};
static US_CALENDAR: WeekCalculator = WeekCalculator {
first_weekday: Weekday::Sunday,
min_week_days: 1,
};
#[test]
fn test_weekday_index() {
assert_eq!(ISO_CALENDAR.weekday_index(Weekday::Monday), 0);
assert_eq!(ISO_CALENDAR.weekday_index(Weekday::Sunday), 6);
assert_eq!(AE_CALENDAR.weekday_index(Weekday::Saturday), 0);
assert_eq!(AE_CALENDAR.weekday_index(Weekday::Friday), 6);
}
#[test]
fn test_first_week_offset() {
let first_week_offset =
|calendar, day| UnitInfo::new(day, 30).unwrap().first_week_offset(calendar);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Monday), 0);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Tuesday), -1);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Wednesday), -2);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Thursday), -3);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Friday), 3);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Saturday), 2);
assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Sunday), 1);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Saturday), 0);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Sunday), -1);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Monday), -2);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Tuesday), -3);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Wednesday), 3);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Thursday), 2);
assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Friday), 1);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Sunday), 0);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Monday), -1);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Tuesday), -2);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Wednesday), -3);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Thursday), -4);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Friday), -5);
assert_eq!(first_week_offset(US_CALENDAR, Weekday::Saturday), -6);
}
#[test]
fn test_num_weeks() {
assert_eq!(
UnitInfo::new(Weekday::Thursday, 4 + 2 * 7 + 4)
.unwrap()
.num_weeks(ISO_CALENDAR),
4
);
assert_eq!(
UnitInfo::new(Weekday::Friday, 3 + 2 * 7 + 4)
.unwrap()
.num_weeks(ISO_CALENDAR),
3
);
assert_eq!(
UnitInfo::new(Weekday::Friday, 3 + 2 * 7 + 3)
.unwrap()
.num_weeks(ISO_CALENDAR),
2
);
assert_eq!(
UnitInfo::new(Weekday::Saturday, 1 + 2 * 7 + 1)
.unwrap()
.num_weeks(US_CALENDAR),
4
);
}
fn classify_days_of_unit(calendar: WeekCalculator, unit: UnitInfo) -> Vec<RelativeWeek> {
let mut weeks: Vec<Vec<Weekday>> = Vec::new();
for day_index in 0..unit.duration_days {
let day = add_to_weekday(unit.first_day, i32::from(day_index));
if day == calendar.first_weekday || weeks.is_empty() {
weeks.push(Vec::new());
}
weeks.last_mut().unwrap().push(day);
}
let mut day_week_of_units = Vec::new();
let mut weeks_in_unit = 0;
for (index, week) in weeks.iter().enumerate() {
let week_of_unit = if week.len() < usize::from(calendar.min_week_days) {
match index {
0 => RelativeWeek::LastWeekOfPreviousUnit,
x if x == weeks.len() - 1 => RelativeWeek::FirstWeekOfNextUnit,
_ => panic!(),
}
} else {
weeks_in_unit += 1;
RelativeWeek::WeekOfCurrentUnit(weeks_in_unit)
};
day_week_of_units.append(&mut [week_of_unit].repeat(week.len()));
}
day_week_of_units
}
#[test]
fn test_relative_week_of_month() {
for min_week_days in 1..7 {
for start_of_week in 1..7 {
let calendar = WeekCalculator {
first_weekday: Weekday::from_days_since_sunday(start_of_week),
min_week_days,
};
for unit_duration in MIN_UNIT_DAYS..400 {
for start_of_unit in 1..7 {
let unit = UnitInfo::new(
Weekday::from_days_since_sunday(start_of_unit),
unit_duration,
)
.unwrap();
let expected = classify_days_of_unit(calendar, unit);
for (index, expected_week_of) in expected.iter().enumerate() {
let day = index + 1;
assert_eq!(
unit.relative_week(calendar, day as u16),
*expected_week_of,
"For the {day}/{unit_duration} starting on Weekday \
{start_of_unit} using start_of_week {start_of_week} \
& min_week_days {min_week_days}"
);
}
}
}
}
}
}
fn week_of_month_from_iso_date(
calendar: WeekCalculator,
yyyymmdd: u32,
) -> Result<WeekOf, RangeError> {
let year = (yyyymmdd / 10000) as i32;
let month = ((yyyymmdd / 100) % 100) as u8;
let day = (yyyymmdd % 100) as u8;
let date = Date::try_new_iso(year, month, day)?;
let previous_month = date
.try_added_with_options(DateDuration::for_months(-1), Default::default())
.unwrap();
calendar.week_of(
u16::from(previous_month.days_in_month()),
u16::from(date.days_in_month()),
u16::from(day),
date.weekday(),
)
}
#[test]
fn test_week_of_month_using_dates() {
assert_eq!(
week_of_month_from_iso_date(ISO_CALENDAR, 20210418).unwrap(),
WeekOf {
week: 3,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(ISO_CALENDAR, 20210419).unwrap(),
WeekOf {
week: 4,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(ISO_CALENDAR, 20180101).unwrap(),
WeekOf {
week: 1,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(ISO_CALENDAR, 20210101).unwrap(),
WeekOf {
week: 5,
unit: RelativeUnit::Previous,
}
);
assert_eq!(
week_of_month_from_iso_date(ISO_CALENDAR, 20200930).unwrap(),
WeekOf {
week: 1,
unit: RelativeUnit::Next,
}
);
assert_eq!(
week_of_month_from_iso_date(ISO_CALENDAR, 20201231).unwrap(),
WeekOf {
week: 5,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(US_CALENDAR, 20201231).unwrap(),
WeekOf {
week: 5,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(US_CALENDAR, 20210101).unwrap(),
WeekOf {
week: 1,
unit: RelativeUnit::Current,
}
);
}
}
#[test]
fn test_first_day() {
use icu_locale_core::locale;
assert_eq!(
WeekInformation::try_new(locale!("und-US").into())
.unwrap()
.first_weekday,
Weekday::Sunday,
);
assert_eq!(
WeekInformation::try_new(locale!("und-FR").into())
.unwrap()
.first_weekday,
Weekday::Monday,
);
assert_eq!(
WeekInformation::try_new(locale!("und-FR-u-fw-tue").into())
.unwrap()
.first_weekday,
Weekday::Tuesday,
);
}
#[test]
fn test_weekend() {
use icu_locale_core::locale;
assert_eq!(
WeekInformation::try_new(locale!("und").into())
.unwrap()
.weekend()
.collect::<Vec<_>>(),
vec![Weekday::Saturday, Weekday::Sunday],
);
assert_eq!(
WeekInformation::try_new(locale!("und-FR").into())
.unwrap()
.weekend()
.collect::<Vec<_>>(),
vec![Weekday::Saturday, Weekday::Sunday],
);
assert_eq!(
WeekInformation::try_new(locale!("und-IQ").into())
.unwrap()
.weekend()
.collect::<Vec<_>>(),
vec![Weekday::Saturday, Weekday::Friday],
);
assert_eq!(
WeekInformation::try_new(locale!("und-IR").into())
.unwrap()
.weekend()
.collect::<Vec<_>>(),
vec![Weekday::Friday],
);
}
#[test]
fn test_weekdays_iter() {
use Weekday::*;
let default_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Saturday, Sunday]));
assert_eq!(vec![Saturday, Sunday], default_weekend.collect::<Vec<_>>());
let fri_sun_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Friday, Sunday]));
assert_eq!(vec![Friday, Sunday], fri_sun_weekend.collect::<Vec<_>>());
let multiple_contiguous_days = WeekdaySetIterator::new(
Monday,
WeekdaySet::new(&[Tuesday, Wednesday, Thursday, Friday]),
);
assert_eq!(
vec![Tuesday, Wednesday, Thursday, Friday],
multiple_contiguous_days.collect::<Vec<_>>()
);
let multiple_non_contiguous_days = WeekdaySetIterator::new(
Wednesday,
WeekdaySet::new(&[Tuesday, Thursday, Friday, Sunday]),
);
assert_eq!(
vec![Thursday, Friday, Sunday, Tuesday],
multiple_non_contiguous_days.collect::<Vec<_>>()
);
}
#[test]
fn test_iso_weeks() {
use crate::types::IsoWeekOfYear;
use crate::Date;
#[expect(clippy::zero_prefixed_literal)]
for ((y, m, d), (iso_year, week_number)) in [
((2009, 12, 30), (2009, 53)),
((2009, 12, 31), (2009, 53)),
((2010, 01, 01), (2009, 53)),
((2010, 01, 02), (2009, 53)),
((2010, 01, 03), (2009, 53)),
((2010, 01, 04), (2010, 1)),
((2010, 01, 05), (2010, 1)),
((2029, 12, 29), (2029, 52)),
((2029, 12, 30), (2029, 52)),
((2029, 12, 31), (2030, 1)),
((2030, 01, 01), (2030, 1)),
((2030, 01, 02), (2030, 1)),
((2030, 01, 03), (2030, 1)),
((2030, 01, 04), (2030, 1)),
] {
assert_eq!(
Date::try_new_iso(y, m, d).unwrap().week_of_year(),
IsoWeekOfYear {
iso_year,
week_number
}
);
}
}