1use crate::{
6 error::CalendarError,
7 provider::*,
8 types::{DayOfMonth, DayOfYearInfo, IsoWeekday, WeekOfMonth},
9};
10use icu_provider::prelude::*;
11
12pub const MIN_UNIT_DAYS: u16 = 14;
14
15#[derive(Clone, Copy, Debug)]
17#[non_exhaustive]
18pub struct WeekCalculator {
19 pub first_weekday: IsoWeekday,
21 pub min_week_days: u8,
24 pub weekend: Option<WeekdaySet>,
26}
27
28impl From<WeekDataV1> for WeekCalculator {
29 fn from(other: WeekDataV1) -> Self {
30 Self {
31 first_weekday: other.first_weekday,
32 min_week_days: other.min_week_days,
33 weekend: None,
34 }
35 }
36}
37
38impl From<&WeekDataV1> for WeekCalculator {
39 fn from(other: &WeekDataV1) -> Self {
40 Self {
41 first_weekday: other.first_weekday,
42 min_week_days: other.min_week_days,
43 weekend: None,
44 }
45 }
46}
47
48impl WeekCalculator {
49 #[cfg(feature = "compiled_data")]
55 pub fn try_new(locale: &DataLocale) -> Result<Self, CalendarError> {
56 Self::try_new_unstable(&crate::provider::Baked, locale)
57 }
58
59 #[doc = icu_provider::gen_any_buffer_unstable_docs!(ANY, Self::try_new_unstable)]
60 pub fn try_new_with_any_provider(
61 provider: &(impl AnyProvider + ?Sized),
62 locale: &DataLocale,
63 ) -> Result<Self, CalendarError> {
64 Self::try_new_unstable(&provider.as_downcasting(), locale).or_else(|e| {
65 DataProvider::<WeekDataV1Marker>::load(
66 &provider.as_downcasting(),
67 DataRequest {
68 locale,
69 metadata: Default::default(),
70 },
71 )
72 .and_then(DataResponse::take_payload)
73 .map(|payload| payload.get().into())
74 .map_err(|_| e)
75 })
76 }
77
78 #[cfg(feature = "serde")]
79 #[doc = icu_provider::gen_any_buffer_unstable_docs!(BUFFER, Self::try_new_unstable)]
80 pub fn try_new_with_buffer_provider(
81 provider: &(impl BufferProvider + ?Sized),
82 locale: &DataLocale,
83 ) -> Result<Self, CalendarError> {
84 Self::try_new_unstable(&provider.as_deserializing(), locale).or_else(|e| {
85 DataProvider::<WeekDataV1Marker>::load(
86 &provider.as_deserializing(),
87 DataRequest {
88 locale,
89 metadata: Default::default(),
90 },
91 )
92 .and_then(DataResponse::take_payload)
93 .map(|payload| payload.get().into())
94 .map_err(|_| e)
95 })
96 }
97
98 #[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
99 pub fn try_new_unstable<P>(provider: &P, locale: &DataLocale) -> Result<Self, CalendarError>
100 where
101 P: DataProvider<crate::provider::WeekDataV2Marker> + ?Sized,
102 {
103 provider
104 .load(DataRequest {
105 locale,
106 metadata: Default::default(),
107 })
108 .and_then(DataResponse::take_payload)
109 .map(|payload| WeekCalculator {
110 first_weekday: payload.get().first_weekday,
111 min_week_days: payload.get().min_week_days,
112 weekend: Some(payload.get().weekend),
113 })
114 .map_err(Into::into)
115 }
116
117 pub fn week_of_month(&self, day_of_month: DayOfMonth, iso_weekday: IsoWeekday) -> WeekOfMonth {
143 WeekOfMonth(simple_week_of(self.first_weekday, day_of_month.0 as u16, iso_weekday) as u32)
144 }
145
146 pub fn week_of_year(
173 &self,
174 day_of_year_info: DayOfYearInfo,
175 iso_weekday: IsoWeekday,
176 ) -> Result<WeekOf, CalendarError> {
177 week_of(
178 self,
179 day_of_year_info.days_in_prev_year,
180 day_of_year_info.days_in_year,
181 day_of_year_info.day_of_year,
182 iso_weekday,
183 )
184 }
185
186 fn weekday_index(&self, weekday: IsoWeekday) -> i8 {
188 (7 + (weekday as i8) - (self.first_weekday as i8)) % 7
189 }
190
191 pub fn weekend(&self) -> impl Iterator<Item = IsoWeekday> {
194 WeekdaySetIterator::new(
195 self.first_weekday,
196 self.weekend.unwrap_or(WeekdaySet::new(&[])),
197 )
198 }
199}
200
201impl Default for WeekCalculator {
202 fn default() -> Self {
203 Self {
204 first_weekday: IsoWeekday::Monday,
205 min_week_days: 1,
206 weekend: Some(WeekdaySet::new(&[IsoWeekday::Saturday, IsoWeekday::Sunday])),
207 }
208 }
209}
210
211fn add_to_weekday(weekday: IsoWeekday, num_days: i32) -> IsoWeekday {
213 let new_weekday = (7 + (weekday as i32) + (num_days % 7)) % 7;
214 IsoWeekday::from(new_weekday as usize)
215}
216
217#[derive(Clone, Copy, Debug, PartialEq)]
220#[allow(clippy::enum_variant_names)]
221enum RelativeWeek {
222 LastWeekOfPreviousUnit,
224 WeekOfCurrentUnit(u16),
226 FirstWeekOfNextUnit,
228}
229
230struct UnitInfo {
232 first_day: IsoWeekday,
234 duration_days: u16,
236}
237
238impl UnitInfo {
239 fn new(first_day: IsoWeekday, duration_days: u16) -> Result<UnitInfo, CalendarError> {
241 if duration_days < MIN_UNIT_DAYS {
242 return Err(CalendarError::Underflow {
243 field: "Month/Year duration",
244 min: MIN_UNIT_DAYS as isize,
245 });
246 }
247 Ok(UnitInfo {
248 first_day,
249 duration_days,
250 })
251 }
252
253 fn first_week_offset(&self, calendar: &WeekCalculator) -> i8 {
258 let first_day_index = calendar.weekday_index(self.first_day);
259 if 7 - first_day_index >= calendar.min_week_days as i8 {
260 -first_day_index
261 } else {
262 7 - first_day_index
263 }
264 }
265
266 fn num_weeks(&self, calendar: &WeekCalculator) -> u16 {
268 let first_week_offset = self.first_week_offset(calendar);
269 let num_days_including_first_week =
270 (self.duration_days as i32) - (first_week_offset as i32);
271 debug_assert!(
272 num_days_including_first_week >= 0,
273 "Unit is shorter than a week."
274 );
275 ((num_days_including_first_week + 7 - (calendar.min_week_days as i32)) / 7) as u16
276 }
277
278 fn relative_week(&self, calendar: &WeekCalculator, day: u16) -> RelativeWeek {
280 let days_since_first_week =
281 i32::from(day) - i32::from(self.first_week_offset(calendar)) - 1;
282 if days_since_first_week < 0 {
283 return RelativeWeek::LastWeekOfPreviousUnit;
284 }
285
286 let week_number = (1 + days_since_first_week / 7) as u16;
287 if week_number > self.num_weeks(calendar) {
288 return RelativeWeek::FirstWeekOfNextUnit;
289 }
290 RelativeWeek::WeekOfCurrentUnit(week_number)
291 }
292}
293
294#[derive(Debug, PartialEq)]
296#[allow(clippy::exhaustive_enums)] pub enum RelativeUnit {
298 Previous,
300 Current,
302 Next,
304}
305
306#[derive(Debug, PartialEq)]
308#[allow(clippy::exhaustive_structs)] pub struct WeekOf {
310 pub week: u16,
312 pub unit: RelativeUnit,
314}
315
316pub fn week_of(
325 calendar: &WeekCalculator,
326 num_days_in_previous_unit: u16,
327 num_days_in_unit: u16,
328 day: u16,
329 week_day: IsoWeekday,
330) -> Result<WeekOf, CalendarError> {
331 let current = UnitInfo::new(
332 add_to_weekday(week_day, 1 - i32::from(day)),
334 num_days_in_unit,
335 )?;
336
337 match current.relative_week(calendar, day) {
338 RelativeWeek::LastWeekOfPreviousUnit => {
339 let previous = UnitInfo::new(
340 add_to_weekday(current.first_day, -i32::from(num_days_in_previous_unit)),
341 num_days_in_previous_unit,
342 )?;
343
344 Ok(WeekOf {
345 week: previous.num_weeks(calendar),
346 unit: RelativeUnit::Previous,
347 })
348 }
349 RelativeWeek::WeekOfCurrentUnit(w) => Ok(WeekOf {
350 week: w,
351 unit: RelativeUnit::Current,
352 }),
353 RelativeWeek::FirstWeekOfNextUnit => Ok(WeekOf {
354 week: 1,
355 unit: RelativeUnit::Next,
356 }),
357 }
358}
359
360pub fn simple_week_of(first_weekday: IsoWeekday, day: u16, week_day: IsoWeekday) -> u16 {
371 let calendar = WeekCalculator {
372 first_weekday,
373 min_week_days: 1,
374 weekend: None,
375 };
376
377 #[allow(clippy::unwrap_used)] week_of(
379 &calendar,
380 MIN_UNIT_DAYS,
383 u16::MAX,
384 day,
385 week_day,
386 )
387 .unwrap()
388 .week
389}
390
391#[derive(Clone, Copy, Debug, PartialEq)]
393pub struct WeekdaySetIterator {
394 first_weekday: IsoWeekday,
396 current_day: IsoWeekday,
398 weekend: WeekdaySet,
400}
401
402impl WeekdaySetIterator {
403 pub(crate) fn new(first_weekday: IsoWeekday, weekend: WeekdaySet) -> Self {
405 WeekdaySetIterator {
406 first_weekday,
407 current_day: first_weekday,
408 weekend,
409 }
410 }
411}
412
413impl Iterator for WeekdaySetIterator {
414 type Item = IsoWeekday;
415
416 fn next(&mut self) -> Option<Self::Item> {
417 while self.current_day.next_day() != self.first_weekday {
419 if self.weekend.contains(self.current_day) {
420 let result = self.current_day;
421 self.current_day = self.current_day.next_day();
422 return Some(result);
423 } else {
424 self.current_day = self.current_day.next_day();
425 }
426 }
427
428 if self.weekend.contains(self.current_day) {
429 self.weekend = WeekdaySet::new(&[]);
432 return Some(self.current_day);
433 }
434
435 Option::None
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::{week_of, RelativeUnit, RelativeWeek, UnitInfo, WeekCalculator, WeekOf};
442 use crate::{error::CalendarError, types::IsoWeekday, Date, DateDuration};
443
444 static ISO_CALENDAR: WeekCalculator = WeekCalculator {
445 first_weekday: IsoWeekday::Monday,
446 min_week_days: 4,
447 weekend: None,
448 };
449
450 static AE_CALENDAR: WeekCalculator = WeekCalculator {
451 first_weekday: IsoWeekday::Saturday,
452 min_week_days: 4,
453 weekend: None,
454 };
455
456 static US_CALENDAR: WeekCalculator = WeekCalculator {
457 first_weekday: IsoWeekday::Sunday,
458 min_week_days: 1,
459 weekend: None,
460 };
461
462 #[test]
463 fn test_weekday_index() {
464 assert_eq!(ISO_CALENDAR.weekday_index(IsoWeekday::Monday), 0);
465 assert_eq!(ISO_CALENDAR.weekday_index(IsoWeekday::Sunday), 6);
466
467 assert_eq!(AE_CALENDAR.weekday_index(IsoWeekday::Saturday), 0);
468 assert_eq!(AE_CALENDAR.weekday_index(IsoWeekday::Friday), 6);
469 }
470
471 #[test]
472 fn test_first_week_offset() {
473 let first_week_offset =
474 |calendar, day| UnitInfo::new(day, 30).unwrap().first_week_offset(calendar);
475 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Monday), 0);
476 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Tuesday), -1);
477 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Wednesday), -2);
478 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Thursday), -3);
479 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Friday), 3);
480 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Saturday), 2);
481 assert_eq!(first_week_offset(&ISO_CALENDAR, IsoWeekday::Sunday), 1);
482
483 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Saturday), 0);
484 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Sunday), -1);
485 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Monday), -2);
486 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Tuesday), -3);
487 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Wednesday), 3);
488 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Thursday), 2);
489 assert_eq!(first_week_offset(&AE_CALENDAR, IsoWeekday::Friday), 1);
490
491 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Sunday), 0);
492 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Monday), -1);
493 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Tuesday), -2);
494 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Wednesday), -3);
495 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Thursday), -4);
496 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Friday), -5);
497 assert_eq!(first_week_offset(&US_CALENDAR, IsoWeekday::Saturday), -6);
498 }
499
500 #[test]
501 fn test_num_weeks() -> Result<(), CalendarError> {
502 assert_eq!(
504 UnitInfo::new(IsoWeekday::Thursday, 4 + 2 * 7 + 4)?.num_weeks(&ISO_CALENDAR),
505 4
506 );
507 assert_eq!(
509 UnitInfo::new(IsoWeekday::Friday, 3 + 2 * 7 + 4)?.num_weeks(&ISO_CALENDAR),
510 3
511 );
512 assert_eq!(
514 UnitInfo::new(IsoWeekday::Friday, 3 + 2 * 7 + 3)?.num_weeks(&ISO_CALENDAR),
515 2
516 );
517
518 assert_eq!(
520 UnitInfo::new(IsoWeekday::Saturday, 1 + 2 * 7 + 1)?.num_weeks(&US_CALENDAR),
521 4
522 );
523 Ok(())
524 }
525
526 fn classify_days_of_unit(calendar: &WeekCalculator, unit: &UnitInfo) -> Vec<RelativeWeek> {
532 let mut weeks: Vec<Vec<IsoWeekday>> = Vec::new();
533 for day_index in 0..unit.duration_days {
534 let day = super::add_to_weekday(unit.first_day, i32::from(day_index));
535 if day == calendar.first_weekday || weeks.is_empty() {
536 weeks.push(Vec::new());
537 }
538 weeks.last_mut().unwrap().push(day);
539 }
540
541 let mut day_week_of_units = Vec::new();
542 let mut weeks_in_unit = 0;
543 for (index, week) in weeks.iter().enumerate() {
544 let week_of_unit = if week.len() < usize::from(calendar.min_week_days) {
545 match index {
546 0 => RelativeWeek::LastWeekOfPreviousUnit,
547 x if x == weeks.len() - 1 => RelativeWeek::FirstWeekOfNextUnit,
548 _ => panic!(),
549 }
550 } else {
551 weeks_in_unit += 1;
552 RelativeWeek::WeekOfCurrentUnit(weeks_in_unit)
553 };
554
555 day_week_of_units.append(&mut [week_of_unit].repeat(week.len()));
556 }
557 day_week_of_units
558 }
559
560 #[test]
561 fn test_relative_week_of_month() -> Result<(), CalendarError> {
562 for min_week_days in 1..7 {
563 for start_of_week in 1..7 {
564 let calendar = WeekCalculator {
565 first_weekday: IsoWeekday::from(start_of_week),
566 min_week_days,
567 weekend: None,
568 };
569 for unit_duration in super::MIN_UNIT_DAYS..400 {
570 for start_of_unit in 1..7 {
571 let unit = UnitInfo::new(IsoWeekday::from(start_of_unit), unit_duration)?;
572 let expected = classify_days_of_unit(&calendar, &unit);
573 for (index, expected_week_of) in expected.iter().enumerate() {
574 let day = index + 1;
575 assert_eq!(
576 unit.relative_week(&calendar, day as u16),
577 *expected_week_of,
578 "For the {day}/{unit_duration} starting on IsoWeekday \
579 {start_of_unit} using start_of_week {start_of_week} \
580 & min_week_days {min_week_days}"
581 );
582 }
583 }
584 }
585 }
586 }
587 Ok(())
588 }
589
590 fn week_of_month_from_iso_date(
591 calendar: &WeekCalculator,
592 yyyymmdd: u32,
593 ) -> Result<WeekOf, CalendarError> {
594 let year = (yyyymmdd / 10000) as i32;
595 let month = ((yyyymmdd / 100) % 100) as u8;
596 let day = (yyyymmdd % 100) as u8;
597
598 let date = Date::try_new_iso_date(year, month, day)?;
599 let previous_month = date.added(DateDuration::new(0, -1, 0, 0));
600
601 week_of(
602 calendar,
603 u16::from(previous_month.days_in_month()),
604 u16::from(date.days_in_month()),
605 u16::from(day),
606 date.day_of_week(),
607 )
608 }
609
610 #[test]
611 fn test_week_of_month_using_dates() -> Result<(), CalendarError> {
612 assert_eq!(
613 week_of_month_from_iso_date(&ISO_CALENDAR, 20210418)?,
614 WeekOf {
615 week: 3,
616 unit: RelativeUnit::Current,
617 }
618 );
619 assert_eq!(
620 week_of_month_from_iso_date(&ISO_CALENDAR, 20210419)?,
621 WeekOf {
622 week: 4,
623 unit: RelativeUnit::Current,
624 }
625 );
626
627 assert_eq!(
629 week_of_month_from_iso_date(&ISO_CALENDAR, 20180101)?,
630 WeekOf {
631 week: 1,
632 unit: RelativeUnit::Current,
633 }
634 );
635 assert_eq!(
637 week_of_month_from_iso_date(&ISO_CALENDAR, 20210101)?,
638 WeekOf {
639 week: 5,
640 unit: RelativeUnit::Previous,
641 }
642 );
643
644 assert_eq!(
646 week_of_month_from_iso_date(&ISO_CALENDAR, 20200930)?,
647 WeekOf {
648 week: 1,
649 unit: RelativeUnit::Next,
650 }
651 );
652 assert_eq!(
654 week_of_month_from_iso_date(&ISO_CALENDAR, 20201231)?,
655 WeekOf {
656 week: 5,
657 unit: RelativeUnit::Current,
658 }
659 );
660
661 assert_eq!(
663 week_of_month_from_iso_date(&US_CALENDAR, 20201231)?,
664 WeekOf {
665 week: 5,
666 unit: RelativeUnit::Current,
667 }
668 );
669 assert_eq!(
670 week_of_month_from_iso_date(&US_CALENDAR, 20210101)?,
671 WeekOf {
672 week: 1,
673 unit: RelativeUnit::Current,
674 }
675 );
676
677 Ok(())
678 }
679}
680
681#[test]
682fn test_simple_week_of() {
683 assert_eq!(
685 simple_week_of(IsoWeekday::Monday, 2, IsoWeekday::Tuesday),
686 1
687 );
688 assert_eq!(simple_week_of(IsoWeekday::Monday, 7, IsoWeekday::Sunday), 1);
689 assert_eq!(simple_week_of(IsoWeekday::Monday, 8, IsoWeekday::Monday), 2);
690
691 assert_eq!(
693 simple_week_of(IsoWeekday::Tuesday, 1, IsoWeekday::Wednesday),
694 1
695 );
696 assert_eq!(
697 simple_week_of(IsoWeekday::Tuesday, 6, IsoWeekday::Monday),
698 1
699 );
700 assert_eq!(
701 simple_week_of(IsoWeekday::Tuesday, 7, IsoWeekday::Tuesday),
702 2
703 );
704
705 assert_eq!(
707 simple_week_of(IsoWeekday::Sunday, 26, IsoWeekday::Friday),
708 4
709 );
710}
711
712#[test]
713fn test_weekend() {
714 use icu_locid::locale;
715
716 assert_eq!(
717 WeekCalculator::try_new(&locale!("und").into())
718 .unwrap()
719 .weekend()
720 .collect::<Vec<_>>(),
721 vec![IsoWeekday::Saturday, IsoWeekday::Sunday],
722 );
723
724 assert_eq!(
725 WeekCalculator::try_new(&locale!("und-FR").into())
726 .unwrap()
727 .weekend()
728 .collect::<Vec<_>>(),
729 vec![IsoWeekday::Saturday, IsoWeekday::Sunday],
730 );
731
732 assert_eq!(
733 WeekCalculator::try_new(&locale!("und-IQ").into())
734 .unwrap()
735 .weekend()
736 .collect::<Vec<_>>(),
737 vec![IsoWeekday::Saturday, IsoWeekday::Friday],
738 );
739
740 assert_eq!(
741 WeekCalculator::try_new(&locale!("und-IR").into())
742 .unwrap()
743 .weekend()
744 .collect::<Vec<_>>(),
745 vec![IsoWeekday::Friday],
746 );
747}
748
749#[test]
750fn test_weekdays_iter() {
751 use IsoWeekday::*;
752
753 let default_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Saturday, Sunday]));
755 assert_eq!(vec![Saturday, Sunday], default_weekend.collect::<Vec<_>>());
756
757 let fri_sun_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Friday, Sunday]));
759 assert_eq!(vec![Friday, Sunday], fri_sun_weekend.collect::<Vec<_>>());
760
761 let multiple_contiguous_days = WeekdaySetIterator::new(
762 Monday,
763 WeekdaySet::new(&[
764 IsoWeekday::Tuesday,
765 IsoWeekday::Wednesday,
766 IsoWeekday::Thursday,
767 IsoWeekday::Friday,
768 ]),
769 );
770 assert_eq!(
771 vec![Tuesday, Wednesday, Thursday, Friday],
772 multiple_contiguous_days.collect::<Vec<_>>()
773 );
774
775 let multiple_non_contiguous_days = WeekdaySetIterator::new(
777 Wednesday,
778 WeekdaySet::new(&[
779 IsoWeekday::Tuesday,
780 IsoWeekday::Thursday,
781 IsoWeekday::Friday,
782 IsoWeekday::Sunday,
783 ]),
784 );
785 assert_eq!(
786 vec![Thursday, Friday, Sunday, Tuesday],
787 multiple_non_contiguous_days.collect::<Vec<_>>()
788 );
789}