use crate::{
error::CalendarError,
provider::WeekDataV1,
types::{DayOfMonth, DayOfYearInfo, IsoWeekday, WeekOfMonth},
};
use icu_provider::prelude::*;
pub const MIN_UNIT_DAYS: u16 = 14;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct WeekCalculator {
pub first_weekday: IsoWeekday,
pub min_week_days: u8,
}
impl From<WeekDataV1> for WeekCalculator {
fn from(other: WeekDataV1) -> Self {
Self {
first_weekday: other.first_weekday,
min_week_days: other.min_week_days,
}
}
}
impl From<&WeekDataV1> for WeekCalculator {
fn from(other: &WeekDataV1) -> Self {
Self {
first_weekday: other.first_weekday,
min_week_days: other.min_week_days,
}
}
}
impl WeekCalculator {
pub fn try_new_unstable<P>(provider: &P, locale: &DataLocale) -> Result<Self, CalendarError>
where
P: DataProvider<crate::provider::WeekDataV1Marker>,
{
provider
.load(DataRequest {
locale,
metadata: Default::default(),
})
.and_then(DataResponse::take_payload)
.map(|payload| payload.get().into())
.map_err(Into::into)
}
icu_provider::gen_any_buffer_constructors!(
locale: include,
options: skip,
error: CalendarError
);
pub fn week_of_month(&self, day_of_month: DayOfMonth, iso_weekday: IsoWeekday) -> WeekOfMonth {
WeekOfMonth(simple_week_of(self.first_weekday, day_of_month.0 as u16, iso_weekday) as u32)
}
pub fn week_of_year(
&self,
day_of_year_info: DayOfYearInfo,
iso_weekday: IsoWeekday,
) -> Result<WeekOf, CalendarError> {
week_of(
self,
day_of_year_info.days_in_prev_year as u16,
day_of_year_info.days_in_year as u16,
day_of_year_info.day_of_year as u16,
iso_weekday,
)
}
fn weekday_index(&self, weekday: IsoWeekday) -> i8 {
(7 + (weekday as i8) - (self.first_weekday as i8)) % 7
}
}
impl Default for WeekCalculator {
fn default() -> Self {
Self {
first_weekday: IsoWeekday::Monday,
min_week_days: 1,
}
}
}
fn add_to_weekday(weekday: IsoWeekday, num_days: i32) -> IsoWeekday {
let new_weekday = (7 + (weekday as i32) + (num_days % 7)) % 7;
IsoWeekday::from(new_weekday as usize)
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(clippy::enum_variant_names)]
enum RelativeWeek {
LastWeekOfPreviousUnit,
WeekOfCurrentUnit(u16),
FirstWeekOfNextUnit,
}
struct UnitInfo {
first_day: IsoWeekday,
duration_days: u16,
}
impl UnitInfo {
fn new(first_day: IsoWeekday, duration_days: u16) -> Result<UnitInfo, CalendarError> {
if duration_days < MIN_UNIT_DAYS {
return Err(CalendarError::Underflow {
field: "Month/Year duration",
min: MIN_UNIT_DAYS as isize,
});
}
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) -> u16 {
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 u16
}
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 u16;
if week_number > self.num_weeks(calendar) {
return RelativeWeek::FirstWeekOfNextUnit;
}
RelativeWeek::WeekOfCurrentUnit(week_number)
}
}
#[derive(Debug, PartialEq)]
#[allow(clippy::exhaustive_enums)] pub enum RelativeUnit {
Previous,
Current,
Next,
}
#[derive(Debug, PartialEq)]
#[allow(clippy::exhaustive_structs)] pub struct WeekOf {
pub week: u16,
pub unit: RelativeUnit,
}
pub fn week_of(
calendar: &WeekCalculator,
num_days_in_previous_unit: u16,
num_days_in_unit: u16,
day: u16,
week_day: IsoWeekday,
) -> Result<WeekOf, CalendarError> {
let current = UnitInfo::new(
add_to_weekday(week_day, 1 - i32::from(day)),
num_days_in_unit,
)?;
match current.relative_week(calendar, 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(calendar),
unit: RelativeUnit::Previous,
})
}
RelativeWeek::WeekOfCurrentUnit(w) => Ok(WeekOf {
week: w,
unit: RelativeUnit::Current,
}),
RelativeWeek::FirstWeekOfNextUnit => Ok(WeekOf {
week: 1,
unit: RelativeUnit::Next,
}),
}
}
pub fn simple_week_of(first_weekday: IsoWeekday, day: u16, week_day: IsoWeekday) -> u16 {
let calendar = WeekCalculator {
first_weekday,
min_week_days: 1,
};
#[allow(clippy::unwrap_used)] week_of(
&calendar,
MIN_UNIT_DAYS,
u16::MAX,
day,
week_day,
)
.unwrap()
.week
}
#[cfg(test)]
mod tests {
use super::{week_of, RelativeUnit, RelativeWeek, UnitInfo, WeekCalculator, WeekOf};
use crate::{error::CalendarError, types::IsoWeekday, Date, DateDuration};
static ISO_CALENDAR: WeekCalculator = WeekCalculator {
first_weekday: IsoWeekday::Monday,
min_week_days: 4,
};
static AE_CALENDAR: WeekCalculator = WeekCalculator {
first_weekday: IsoWeekday::Saturday,
min_week_days: 4,
};
static US_CALENDAR: WeekCalculator = WeekCalculator {
first_weekday: IsoWeekday::Sunday,
min_week_days: 1,
};
#[test]
fn test_weekday_index() {
assert_eq!(ISO_CALENDAR.weekday_index(IsoWeekday::Monday), 0);
assert_eq!(ISO_CALENDAR.weekday_index(IsoWeekday::Sunday), 6);
assert_eq!(AE_CALENDAR.weekday_index(IsoWeekday::Saturday), 0);
assert_eq!(AE_CALENDAR.weekday_index(IsoWeekday::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, IsoWeekday::Monday), 0);
assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Tuesday), -1);
assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Wednesday), -2);
assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Thursday), -3);
assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Friday), 3);
assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Saturday), 2);
assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Sunday), 1);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Saturday), 0);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Sunday), -1);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Monday), -2);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Tuesday), -3);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Wednesday), 3);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Thursday), 2);
assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Friday), 1);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Sunday), 0);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Monday), -1);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Tuesday), -2);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Wednesday), -3);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Thursday), -4);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Friday), -5);
assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Saturday), -6);
}
#[test]
fn test_num_weeks() -> Result<(), CalendarError> {
assert_eq!(
UnitInfo::new(IsoWeekday::Thursday, 4 + 2 * 7 + 4)?.num_weeks(&ISO_CALENDAR),
4
);
assert_eq!(
UnitInfo::new(IsoWeekday::Friday, 3 + 2 * 7 + 4)?.num_weeks(&ISO_CALENDAR),
3
);
assert_eq!(
UnitInfo::new(IsoWeekday::Friday, 3 + 2 * 7 + 3)?.num_weeks(&ISO_CALENDAR),
2
);
assert_eq!(
UnitInfo::new(IsoWeekday::Saturday, 1 + 2 * 7 + 1)?.num_weeks(&US_CALENDAR),
4
);
Ok(())
}
fn classify_days_of_unit(calendar: &WeekCalculator, unit: &UnitInfo) -> Vec<RelativeWeek> {
let mut weeks: Vec<Vec<IsoWeekday>> = Vec::new();
for day_index in 0..unit.duration_days {
let day = super::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() -> Result<(), CalendarError> {
for min_week_days in 1..7 {
for start_of_week in 1..7 {
let calendar = WeekCalculator {
first_weekday: IsoWeekday::from(start_of_week),
min_week_days,
};
for unit_duration in super::MIN_UNIT_DAYS..400 {
for start_of_unit in 1..7 {
let unit = UnitInfo::new(IsoWeekday::from(start_of_unit), unit_duration)?;
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 {}/{} starting on IsoWeekday {} using start_of_week {} & min_week_days {}",
day,
unit_duration,
start_of_unit,
start_of_week,
min_week_days
);
}
}
}
}
}
Ok(())
}
fn week_of_month_from_iso_date(
calendar: &WeekCalculator,
yyyymmdd: u32,
) -> Result<WeekOf, CalendarError> {
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_date(year, month, day)?;
let previous_month = date.added(DateDuration::new(0, -1, 0, 0));
week_of(
calendar,
u16::from(previous_month.days_in_month()),
u16::from(date.days_in_month()),
u16::from(day),
date.day_of_week(),
)
}
#[test]
fn test_week_of_month_using_dates() -> Result<(), CalendarError> {
assert_eq!(
week_of_month_from_iso_date(&ISO_CALENDAR, 20210418)?,
WeekOf {
week: 3,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(&ISO_CALENDAR, 20210419)?,
WeekOf {
week: 4,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(&ISO_CALENDAR, 20180101)?,
WeekOf {
week: 1,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(&ISO_CALENDAR, 20210101)?,
WeekOf {
week: 5,
unit: RelativeUnit::Previous,
}
);
assert_eq!(
week_of_month_from_iso_date(&ISO_CALENDAR, 20200930)?,
WeekOf {
week: 1,
unit: RelativeUnit::Next,
}
);
assert_eq!(
week_of_month_from_iso_date(&ISO_CALENDAR, 20201231)?,
WeekOf {
week: 5,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(&US_CALENDAR, 20201231)?,
WeekOf {
week: 5,
unit: RelativeUnit::Current,
}
);
assert_eq!(
week_of_month_from_iso_date(&US_CALENDAR, 20210101)?,
WeekOf {
week: 1,
unit: RelativeUnit::Current,
}
);
Ok(())
}
}
#[test]
fn test_simple_week_of() {
assert_eq!(
simple_week_of(IsoWeekday::Monday, 2, IsoWeekday::Tuesday),
1
);
assert_eq!(simple_week_of(IsoWeekday::Monday, 7, IsoWeekday::Sunday), 1);
assert_eq!(simple_week_of(IsoWeekday::Monday, 8, IsoWeekday::Monday), 2);
assert_eq!(
simple_week_of(IsoWeekday::Tuesday, 1, IsoWeekday::Wednesday),
1
);
assert_eq!(
simple_week_of(IsoWeekday::Tuesday, 6, IsoWeekday::Monday),
1
);
assert_eq!(
simple_week_of(IsoWeekday::Tuesday, 7, IsoWeekday::Tuesday),
2
);
assert_eq!(
simple_week_of(IsoWeekday::Sunday, 26, IsoWeekday::Friday),
4
);
}