1use crate::{error::RangeError, provider::*, types::Weekday};
8use icu_locale_core::preferences::{define_preferences, extensions::unicode::keywords::FirstDay};
9use icu_provider::prelude::*;
10
11const MIN_UNIT_DAYS: u16 = 14;
13
14define_preferences!(
15    [Copy]
17    WeekPreferences,
18    {
19        first_weekday: FirstDay
21    }
22);
23
24#[derive(Clone, Copy, Debug)]
26#[non_exhaustive]
27pub struct WeekInformation {
28    pub first_weekday: Weekday,
30    pub weekend: WeekdaySet,
32}
33
34impl WeekInformation {
35    icu_provider::gen_buffer_data_constructors!(
36        (prefs: WeekPreferences) -> error: DataError,
37        );
39
40    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
41    pub fn try_new_unstable<P>(provider: &P, prefs: WeekPreferences) -> Result<Self, DataError>
42    where
43        P: DataProvider<crate::provider::CalendarWeekV1> + ?Sized,
44    {
45        let locale = CalendarWeekV1::make_locale(prefs.locale_preferences);
46        provider
47            .load(DataRequest {
48                id: DataIdentifierBorrowed::for_locale(&locale),
49                ..Default::default()
50            })
51            .map(|response| WeekInformation {
52                first_weekday: match prefs.first_weekday {
53                    Some(FirstDay::Mon) => Weekday::Monday,
54                    Some(FirstDay::Tue) => Weekday::Tuesday,
55                    Some(FirstDay::Wed) => Weekday::Wednesday,
56                    Some(FirstDay::Thu) => Weekday::Thursday,
57                    Some(FirstDay::Fri) => Weekday::Friday,
58                    Some(FirstDay::Sat) => Weekday::Saturday,
59                    Some(FirstDay::Sun) => Weekday::Sunday,
60                    _ => response.payload.get().first_weekday,
61                },
62                weekend: response.payload.get().weekend,
63            })
64    }
65
66    pub fn weekend(self) -> WeekdaySetIterator {
69        WeekdaySetIterator::new(self.first_weekday, self.weekend)
70    }
71}
72
73#[derive(Clone, Copy, Debug)]
74pub(crate) struct WeekCalculator {
75    first_weekday: Weekday,
76    min_week_days: u8,
77}
78
79impl WeekCalculator {
80    pub(crate) const ISO: Self = Self {
81        first_weekday: Weekday::Monday,
82        min_week_days: 4,
83    };
84
85    fn weekday_index(self, weekday: Weekday) -> i8 {
87        (7 + (weekday as i8) - (self.first_weekday as i8)) % 7
88    }
89
90    pub(crate) fn week_of(
102        self,
103        num_days_in_previous_unit: u16,
104        num_days_in_unit: u16,
105        day: u16,
106        week_day: Weekday,
107    ) -> Result<WeekOf, RangeError> {
108        let current = UnitInfo::new(
109            add_to_weekday(week_day, 1 - i32::from(day)),
111            num_days_in_unit,
112        )?;
113
114        match current.relative_week(self, day) {
115            RelativeWeek::LastWeekOfPreviousUnit => {
116                let previous = UnitInfo::new(
117                    add_to_weekday(current.first_day, -i32::from(num_days_in_previous_unit)),
118                    num_days_in_previous_unit,
119                )?;
120
121                Ok(WeekOf {
122                    week: previous.num_weeks(self),
123                    unit: RelativeUnit::Previous,
124                })
125            }
126            RelativeWeek::WeekOfCurrentUnit(w) => Ok(WeekOf {
127                week: w,
128                unit: RelativeUnit::Current,
129            }),
130            RelativeWeek::FirstWeekOfNextUnit => Ok(WeekOf {
131                week: 1,
132                unit: RelativeUnit::Next,
133            }),
134        }
135    }
136}
137
138fn add_to_weekday(weekday: Weekday, num_days: i32) -> Weekday {
140    let new_weekday = (7 + (weekday as i32) + (num_days % 7)) % 7;
141    Weekday::from_days_since_sunday(new_weekday as isize)
142}
143
144#[derive(Clone, Copy, Debug, PartialEq)]
147#[allow(clippy::enum_variant_names)]
148enum RelativeWeek {
149    LastWeekOfPreviousUnit,
151    WeekOfCurrentUnit(u8),
153    FirstWeekOfNextUnit,
155}
156
157#[derive(Clone, Copy)]
159struct UnitInfo {
160    first_day: Weekday,
162    duration_days: u16,
164}
165
166impl UnitInfo {
167    fn new(first_day: Weekday, duration_days: u16) -> Result<UnitInfo, RangeError> {
169        if duration_days < MIN_UNIT_DAYS {
170            return Err(RangeError {
171                field: "num_days_in_unit",
172                value: duration_days as i32,
173                min: MIN_UNIT_DAYS as i32,
174                max: i32::MAX,
175            });
176        }
177        Ok(UnitInfo {
178            first_day,
179            duration_days,
180        })
181    }
182
183    fn first_week_offset(self, calendar: WeekCalculator) -> i8 {
188        let first_day_index = calendar.weekday_index(self.first_day);
189        if 7 - first_day_index >= calendar.min_week_days as i8 {
190            -first_day_index
191        } else {
192            7 - first_day_index
193        }
194    }
195
196    fn num_weeks(self, calendar: WeekCalculator) -> u8 {
198        let first_week_offset = self.first_week_offset(calendar);
199        let num_days_including_first_week =
200            (self.duration_days as i32) - (first_week_offset as i32);
201        debug_assert!(
202            num_days_including_first_week >= 0,
203            "Unit is shorter than a week."
204        );
205        ((num_days_including_first_week + 7 - (calendar.min_week_days as i32)) / 7) as u8
206    }
207
208    fn relative_week(self, calendar: WeekCalculator, day: u16) -> RelativeWeek {
210        let days_since_first_week =
211            i32::from(day) - i32::from(self.first_week_offset(calendar)) - 1;
212        if days_since_first_week < 0 {
213            return RelativeWeek::LastWeekOfPreviousUnit;
214        }
215
216        let week_number = (1 + days_since_first_week / 7) as u8;
217        if week_number > self.num_weeks(calendar) {
218            return RelativeWeek::FirstWeekOfNextUnit;
219        }
220        RelativeWeek::WeekOfCurrentUnit(week_number)
221    }
222}
223
224#[derive(Debug, PartialEq)]
226#[allow(clippy::exhaustive_enums)] pub(crate) enum RelativeUnit {
228    Previous,
230    Current,
232    Next,
234}
235
236#[derive(Debug, PartialEq)]
238#[allow(clippy::exhaustive_structs)] pub(crate) struct WeekOf {
240    pub week: u8,
242    pub unit: RelativeUnit,
244}
245
246#[derive(Clone, Copy, Debug, PartialEq)]
248pub struct WeekdaySetIterator {
249    first_weekday: Weekday,
251    current_day: Weekday,
253    weekend: WeekdaySet,
255}
256
257impl WeekdaySetIterator {
258    pub(crate) fn new(first_weekday: Weekday, weekend: WeekdaySet) -> Self {
260        WeekdaySetIterator {
261            first_weekday,
262            current_day: first_weekday,
263            weekend,
264        }
265    }
266}
267
268impl Iterator for WeekdaySetIterator {
269    type Item = Weekday;
270
271    fn next(&mut self) -> Option<Self::Item> {
272        while self.current_day.next_day() != self.first_weekday {
274            if self.weekend.contains(self.current_day) {
275                let result = self.current_day;
276                self.current_day = self.current_day.next_day();
277                return Some(result);
278            } else {
279                self.current_day = self.current_day.next_day();
280            }
281        }
282
283        if self.weekend.contains(self.current_day) {
284            self.weekend = WeekdaySet::new(&[]);
287            return Some(self.current_day);
288        }
289
290        Option::None
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::{types::Weekday, Date, DateDuration, RangeError};
298
299    static ISO_CALENDAR: WeekCalculator = WeekCalculator {
300        first_weekday: Weekday::Monday,
301        min_week_days: 4,
302    };
303
304    static AE_CALENDAR: WeekCalculator = WeekCalculator {
305        first_weekday: Weekday::Saturday,
306        min_week_days: 4,
307    };
308
309    static US_CALENDAR: WeekCalculator = WeekCalculator {
310        first_weekday: Weekday::Sunday,
311        min_week_days: 1,
312    };
313
314    #[test]
315    fn test_weekday_index() {
316        assert_eq!(ISO_CALENDAR.weekday_index(Weekday::Monday), 0);
317        assert_eq!(ISO_CALENDAR.weekday_index(Weekday::Sunday), 6);
318
319        assert_eq!(AE_CALENDAR.weekday_index(Weekday::Saturday), 0);
320        assert_eq!(AE_CALENDAR.weekday_index(Weekday::Friday), 6);
321    }
322
323    #[test]
324    fn test_first_week_offset() {
325        let first_week_offset =
326            |calendar, day| UnitInfo::new(day, 30).unwrap().first_week_offset(calendar);
327        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Monday), 0);
328        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Tuesday), -1);
329        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Wednesday), -2);
330        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Thursday), -3);
331        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Friday), 3);
332        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Saturday), 2);
333        assert_eq!(first_week_offset(ISO_CALENDAR, Weekday::Sunday), 1);
334
335        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Saturday), 0);
336        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Sunday), -1);
337        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Monday), -2);
338        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Tuesday), -3);
339        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Wednesday), 3);
340        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Thursday), 2);
341        assert_eq!(first_week_offset(AE_CALENDAR, Weekday::Friday), 1);
342
343        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Sunday), 0);
344        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Monday), -1);
345        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Tuesday), -2);
346        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Wednesday), -3);
347        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Thursday), -4);
348        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Friday), -5);
349        assert_eq!(first_week_offset(US_CALENDAR, Weekday::Saturday), -6);
350    }
351
352    #[test]
353    fn test_num_weeks() {
354        assert_eq!(
356            UnitInfo::new(Weekday::Thursday, 4 + 2 * 7 + 4)
357                .unwrap()
358                .num_weeks(ISO_CALENDAR),
359            4
360        );
361        assert_eq!(
363            UnitInfo::new(Weekday::Friday, 3 + 2 * 7 + 4)
364                .unwrap()
365                .num_weeks(ISO_CALENDAR),
366            3
367        );
368        assert_eq!(
370            UnitInfo::new(Weekday::Friday, 3 + 2 * 7 + 3)
371                .unwrap()
372                .num_weeks(ISO_CALENDAR),
373            2
374        );
375
376        assert_eq!(
378            UnitInfo::new(Weekday::Saturday, 1 + 2 * 7 + 1)
379                .unwrap()
380                .num_weeks(US_CALENDAR),
381            4
382        );
383    }
384
385    fn classify_days_of_unit(calendar: WeekCalculator, unit: &UnitInfo) -> Vec<RelativeWeek> {
391        let mut weeks: Vec<Vec<Weekday>> = Vec::new();
392        for day_index in 0..unit.duration_days {
393            let day = super::add_to_weekday(unit.first_day, i32::from(day_index));
394            if day == calendar.first_weekday || weeks.is_empty() {
395                weeks.push(Vec::new());
396            }
397            weeks.last_mut().unwrap().push(day);
398        }
399
400        let mut day_week_of_units = Vec::new();
401        let mut weeks_in_unit = 0;
402        for (index, week) in weeks.iter().enumerate() {
403            let week_of_unit = if week.len() < usize::from(calendar.min_week_days) {
404                match index {
405                    0 => RelativeWeek::LastWeekOfPreviousUnit,
406                    x if x == weeks.len() - 1 => RelativeWeek::FirstWeekOfNextUnit,
407                    _ => panic!(),
408                }
409            } else {
410                weeks_in_unit += 1;
411                RelativeWeek::WeekOfCurrentUnit(weeks_in_unit)
412            };
413
414            day_week_of_units.append(&mut [week_of_unit].repeat(week.len()));
415        }
416        day_week_of_units
417    }
418
419    #[test]
420    fn test_relative_week_of_month() {
421        for min_week_days in 1..7 {
422            for start_of_week in 1..7 {
423                let calendar = WeekCalculator {
424                    first_weekday: Weekday::from_days_since_sunday(start_of_week),
425                    min_week_days,
426                };
427                for unit_duration in super::MIN_UNIT_DAYS..400 {
428                    for start_of_unit in 1..7 {
429                        let unit = UnitInfo::new(
430                            Weekday::from_days_since_sunday(start_of_unit),
431                            unit_duration,
432                        )
433                        .unwrap();
434                        let expected = classify_days_of_unit(calendar, &unit);
435                        for (index, expected_week_of) in expected.iter().enumerate() {
436                            let day = index + 1;
437                            assert_eq!(
438                                unit.relative_week(calendar, day as u16),
439                                *expected_week_of,
440                                "For the {day}/{unit_duration} starting on Weekday \
441                        {start_of_unit} using start_of_week {start_of_week} \
442                        & min_week_days {min_week_days}"
443                            );
444                        }
445                    }
446                }
447            }
448        }
449    }
450
451    fn week_of_month_from_iso_date(
452        calendar: WeekCalculator,
453        yyyymmdd: u32,
454    ) -> Result<WeekOf, RangeError> {
455        let year = (yyyymmdd / 10000) as i32;
456        let month = ((yyyymmdd / 100) % 100) as u8;
457        let day = (yyyymmdd % 100) as u8;
458
459        let date = Date::try_new_iso(year, month, day)?;
460        let previous_month = date.added(DateDuration::new(0, -1, 0, 0));
461
462        calendar.week_of(
463            u16::from(previous_month.days_in_month()),
464            u16::from(date.days_in_month()),
465            u16::from(day),
466            date.day_of_week(),
467        )
468    }
469
470    #[test]
471    fn test_week_of_month_using_dates() {
472        assert_eq!(
473            week_of_month_from_iso_date(ISO_CALENDAR, 20210418).unwrap(),
474            WeekOf {
475                week: 3,
476                unit: RelativeUnit::Current,
477            }
478        );
479        assert_eq!(
480            week_of_month_from_iso_date(ISO_CALENDAR, 20210419).unwrap(),
481            WeekOf {
482                week: 4,
483                unit: RelativeUnit::Current,
484            }
485        );
486
487        assert_eq!(
489            week_of_month_from_iso_date(ISO_CALENDAR, 20180101).unwrap(),
490            WeekOf {
491                week: 1,
492                unit: RelativeUnit::Current,
493            }
494        );
495        assert_eq!(
497            week_of_month_from_iso_date(ISO_CALENDAR, 20210101).unwrap(),
498            WeekOf {
499                week: 5,
500                unit: RelativeUnit::Previous,
501            }
502        );
503
504        assert_eq!(
506            week_of_month_from_iso_date(ISO_CALENDAR, 20200930).unwrap(),
507            WeekOf {
508                week: 1,
509                unit: RelativeUnit::Next,
510            }
511        );
512        assert_eq!(
514            week_of_month_from_iso_date(ISO_CALENDAR, 20201231).unwrap(),
515            WeekOf {
516                week: 5,
517                unit: RelativeUnit::Current,
518            }
519        );
520
521        assert_eq!(
523            week_of_month_from_iso_date(US_CALENDAR, 20201231).unwrap(),
524            WeekOf {
525                week: 5,
526                unit: RelativeUnit::Current,
527            }
528        );
529        assert_eq!(
530            week_of_month_from_iso_date(US_CALENDAR, 20210101).unwrap(),
531            WeekOf {
532                week: 1,
533                unit: RelativeUnit::Current,
534            }
535        );
536    }
537}
538
539#[test]
540fn test_first_day() {
541    use icu_locale_core::locale;
542
543    assert_eq!(
544        WeekInformation::try_new(locale!("und-US").into())
545            .unwrap()
546            .first_weekday,
547        Weekday::Sunday,
548    );
549
550    assert_eq!(
551        WeekInformation::try_new(locale!("und-FR").into())
552            .unwrap()
553            .first_weekday,
554        Weekday::Monday,
555    );
556
557    assert_eq!(
558        WeekInformation::try_new(locale!("und-FR-u-fw-tue").into())
559            .unwrap()
560            .first_weekday,
561        Weekday::Tuesday,
562    );
563}
564
565#[test]
566fn test_weekend() {
567    use icu_locale_core::locale;
568
569    assert_eq!(
570        WeekInformation::try_new(locale!("und").into())
571            .unwrap()
572            .weekend()
573            .collect::<Vec<_>>(),
574        vec![Weekday::Saturday, Weekday::Sunday],
575    );
576
577    assert_eq!(
578        WeekInformation::try_new(locale!("und-FR").into())
579            .unwrap()
580            .weekend()
581            .collect::<Vec<_>>(),
582        vec![Weekday::Saturday, Weekday::Sunday],
583    );
584
585    assert_eq!(
586        WeekInformation::try_new(locale!("und-IQ").into())
587            .unwrap()
588            .weekend()
589            .collect::<Vec<_>>(),
590        vec![Weekday::Saturday, Weekday::Friday],
591    );
592
593    assert_eq!(
594        WeekInformation::try_new(locale!("und-IR").into())
595            .unwrap()
596            .weekend()
597            .collect::<Vec<_>>(),
598        vec![Weekday::Friday],
599    );
600}
601
602#[test]
603fn test_weekdays_iter() {
604    use Weekday::*;
605
606    let default_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Saturday, Sunday]));
608    assert_eq!(vec![Saturday, Sunday], default_weekend.collect::<Vec<_>>());
609
610    let fri_sun_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Friday, Sunday]));
612    assert_eq!(vec![Friday, Sunday], fri_sun_weekend.collect::<Vec<_>>());
613
614    let multiple_contiguous_days = WeekdaySetIterator::new(
615        Monday,
616        WeekdaySet::new(&[
617            Weekday::Tuesday,
618            Weekday::Wednesday,
619            Weekday::Thursday,
620            Weekday::Friday,
621        ]),
622    );
623    assert_eq!(
624        vec![Tuesday, Wednesday, Thursday, Friday],
625        multiple_contiguous_days.collect::<Vec<_>>()
626    );
627
628    let multiple_non_contiguous_days = WeekdaySetIterator::new(
630        Wednesday,
631        WeekdaySet::new(&[
632            Weekday::Tuesday,
633            Weekday::Thursday,
634            Weekday::Friday,
635            Weekday::Sunday,
636        ]),
637    );
638    assert_eq!(
639        vec![Thursday, Friday, Sunday, Tuesday],
640        multiple_non_contiguous_days.collect::<Vec<_>>()
641    );
642}
643
644#[test]
645fn test_iso_weeks() {
646    use crate::types::IsoWeekOfYear;
647    use crate::Date;
648
649    #[allow(clippy::zero_prefixed_literal)]
650    for ((y, m, d), (iso_year, week_number)) in [
651        ((2009, 12, 30), (2009, 53)),
653        ((2009, 12, 31), (2009, 53)),
654        ((2010, 01, 01), (2009, 53)),
655        ((2010, 01, 02), (2009, 53)),
656        ((2010, 01, 03), (2009, 53)),
657        ((2010, 01, 04), (2010, 1)),
658        ((2010, 01, 05), (2010, 1)),
659        ((2029, 12, 29), (2029, 52)),
661        ((2029, 12, 30), (2029, 52)),
662        ((2029, 12, 31), (2030, 1)),
663        ((2030, 01, 01), (2030, 1)),
664        ((2030, 01, 02), (2030, 1)),
665        ((2030, 01, 03), (2030, 1)),
666        ((2030, 01, 04), (2030, 1)),
667    ] {
668        assert_eq!(
669            Date::try_new_iso(y, m, d).unwrap().week_of_year(),
670            IsoWeekOfYear {
671                iso_year,
672                week_number
673            }
674        );
675    }
676}