icu_calendar/
week_of.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::{
6    error::CalendarError,
7    provider::*,
8    types::{DayOfMonth, DayOfYearInfo, IsoWeekday, WeekOfMonth},
9};
10use icu_provider::prelude::*;
11
12/// Minimum number of days in a month unit required for using this module
13pub const MIN_UNIT_DAYS: u16 = 14;
14
15/// Calculator for week-of-month and week-of-year based on locale-specific configurations.
16#[derive(Clone, Copy, Debug)]
17#[non_exhaustive]
18pub struct WeekCalculator {
19    /// The first day of a week.
20    pub first_weekday: IsoWeekday,
21    /// For a given week, the minimum number of that week's days present in a given month or year
22    /// for the week to be considered part of that month or year.
23    pub min_week_days: u8,
24    /// The set of weekend days, if available
25    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    /// Creates a new [`WeekCalculator`] from compiled data.
50    ///
51    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
52    ///
53    /// [📚 Help choosing a constructor](icu_provider::constructors)
54    #[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    /// Returns the week of month according to a calendar with min_week_days = 1.
118    ///
119    /// This is different from what the UTS35 spec describes [1] but the latter is
120    /// missing a month of week-of-month field so following the spec would result
121    /// in inconsistencies (e.g. in the ISO calendar 2021-01-01 is the last week
122    /// of December but 'MMMMW' would have it formatted as 'week 5 of January').
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use icu::calendar::types::{DayOfMonth, IsoWeekday, WeekOfMonth};
128    /// use icu::calendar::week::WeekCalculator;
129    ///
130    /// let week_calculator =
131    ///     WeekCalculator::try_new(&icu::locid::locale!("und-GB").into())
132    ///         .expect("locale should be present");
133    ///
134    /// // Wednesday the 10th is in week 2:
135    /// assert_eq!(
136    ///     WeekOfMonth(2),
137    ///     week_calculator.week_of_month(DayOfMonth(10), IsoWeekday::Wednesday)
138    /// );
139    /// ```
140    ///
141    /// [1]: https://www.unicode.org/reports/tr35/tr35-55/tr35-dates.html#Date_Patterns_Week_Of_Year
142    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    /// Returns the week of year according to the weekday and [`DayOfYearInfo`].
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use icu::calendar::types::IsoWeekday;
152    /// use icu::calendar::week::{RelativeUnit, WeekCalculator, WeekOf};
153    /// use icu::calendar::Date;
154    ///
155    /// let week_calculator =
156    ///     WeekCalculator::try_new(&icu::locid::locale!("und-GB").into())
157    ///         .expect("locale should be present");
158    ///
159    /// let iso_date = Date::try_new_iso_date(2022, 8, 26).unwrap();
160    ///
161    /// // Friday August 26 is in week 34 of year 2022:
162    /// assert_eq!(
163    ///     WeekOf {
164    ///         unit: RelativeUnit::Current,
165    ///         week: 34
166    ///     },
167    ///     week_calculator
168    ///         .week_of_year(iso_date.day_of_year_info(), IsoWeekday::Friday)
169    ///         .unwrap()
170    /// );
171    /// ```
172    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    /// Returns the zero based index of `weekday` vs this calendar's start of week.
187    fn weekday_index(&self, weekday: IsoWeekday) -> i8 {
188        (7 + (weekday as i8) - (self.first_weekday as i8)) % 7
189    }
190
191    /// Weekdays that are part of the 'weekend', for calendar purposes.
192    /// Days may not be contiguous, and order is based off the first weekday.
193    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
211/// Returns the weekday that's `num_days` after `weekday`.
212fn 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/// Which year or month that a calendar assigns a week to relative to the year/month
218/// the week is in.
219#[derive(Clone, Copy, Debug, PartialEq)]
220#[allow(clippy::enum_variant_names)]
221enum RelativeWeek {
222    /// A week that is assigned to the last week of the previous year/month. e.g. 2021-01-01 is week 54 of 2020 per the ISO calendar.
223    LastWeekOfPreviousUnit,
224    /// A week that's assigned to the current year/month. The offset is 1-based. e.g. 2021-01-11 is week 2 of 2021 per the ISO calendar so would be WeekOfCurrentUnit(2).
225    WeekOfCurrentUnit(u16),
226    /// A week that is assigned to the first week of the next year/month. e.g. 2019-12-31 is week 1 of 2020 per the ISO calendar.
227    FirstWeekOfNextUnit,
228}
229
230/// Information about a year or month.
231struct UnitInfo {
232    /// The weekday of this year/month's first day.
233    first_day: IsoWeekday,
234    /// The number of days in this year/month.
235    duration_days: u16,
236}
237
238impl UnitInfo {
239    /// Creates a UnitInfo for a given year or month.
240    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    /// Returns the start of this unit's first week.
254    ///
255    /// The returned value can be negative if this unit's first week started during the previous
256    /// unit.
257    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    /// Returns the number of weeks in this unit according to `calendar`.
267    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    /// Returns the week number for the given day in this unit.
279    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/// The year or month that a calendar assigns a week to relative to the year/month that it is in.
295#[derive(Debug, PartialEq)]
296#[allow(clippy::exhaustive_enums)] // this type is stable
297pub enum RelativeUnit {
298    /// A week that is assigned to previous year/month. e.g. 2021-01-01 is week 54 of 2020 per the ISO calendar.
299    Previous,
300    /// A week that's assigned to the current year/month. e.g. 2021-01-11 is week 2 of 2021 per the ISO calendar.
301    Current,
302    /// A week that is assigned to the next year/month. e.g. 2019-12-31 is week 1 of 2020 per the ISO calendar.
303    Next,
304}
305
306/// The week number assigned to a given week according to a calendar.
307#[derive(Debug, PartialEq)]
308#[allow(clippy::exhaustive_structs)] // this type is stable
309pub struct WeekOf {
310    /// Week of month/year. 1 based.
311    pub week: u16,
312    /// The month/year that this week is in, relative to the month/year of the input date.
313    pub unit: RelativeUnit,
314}
315
316/// Computes & returns the week of given month/year according to `calendar`.
317///
318/// # Arguments
319///  - calendar: Calendar information used to compute the week number.
320///  - num_days_in_previous_unit: The number of days in the preceding month/year.
321///  - num_days_in_unit: The number of days in the month/year.
322///  - day: 1-based day of month/year.
323///  - week_day: The weekday of `day`..
324pub 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        // The first day of this month/year is (day - 1) days from `day`.
333        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
360/// Computes & returns the week of given month or year according to a calendar with min_week_days = 1.
361///
362/// Does not know anything about the unit size (month or year), and will just assume the date falls
363/// within whatever unit that is being considered. In other words, this function returns strictly increasing
364/// values as `day` increases, unlike [`week_of()`] which is cyclic.
365///
366/// # Arguments
367///  - first_weekday: The first day of a week.
368///  - day: 1-based day of the month or year.
369///  - week_day: The weekday of `day`.
370pub 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 should can't fail with MIN_UNIT_DAYS
378    week_of(
379        &calendar,
380        // The duration of the previous unit does not influence the result if min_week_days = 1
381        // so we only need to use a valid value.
382        MIN_UNIT_DAYS,
383        u16::MAX,
384        day,
385        week_day,
386    )
387    .unwrap()
388    .week
389}
390
391/// [Iterator] that yields weekdays that are part of the weekend.
392#[derive(Clone, Copy, Debug, PartialEq)]
393pub struct WeekdaySetIterator {
394    /// Determines the order in which we should start reading values from `weekend`.
395    first_weekday: IsoWeekday,
396    /// Day being evaluated.
397    current_day: IsoWeekday,
398    /// Bitset to read weekdays from.
399    weekend: WeekdaySet,
400}
401
402impl WeekdaySetIterator {
403    /// Creates the Iterator. Sets `current_day` to the day after `first_weekday`.
404    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        // Check each bit until we find one that is ON or until we are back to the start of the week.
418        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            // Clear weekend, we've seen all bits.
430            // Breaks the loop next time `next()` is called
431            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        // 4 days in first & last week.
503        assert_eq!(
504            UnitInfo::new(IsoWeekday::Thursday, 4 + 2 * 7 + 4)?.num_weeks(&ISO_CALENDAR),
505            4
506        );
507        // 3 days in first week, 4 in last week.
508        assert_eq!(
509            UnitInfo::new(IsoWeekday::Friday, 3 + 2 * 7 + 4)?.num_weeks(&ISO_CALENDAR),
510            3
511        );
512        // 3 days in first & last week.
513        assert_eq!(
514            UnitInfo::new(IsoWeekday::Friday, 3 + 2 * 7 + 3)?.num_weeks(&ISO_CALENDAR),
515            2
516        );
517
518        // 1 day in first & last week.
519        assert_eq!(
520            UnitInfo::new(IsoWeekday::Saturday, 1 + 2 * 7 + 1)?.num_weeks(&US_CALENDAR),
521            4
522        );
523        Ok(())
524    }
525
526    /// Uses enumeration & bucketing to assign each day of a month or year `unit` to a week.
527    ///
528    /// This alternative implementation serves as an exhaustive safety check
529    /// of relative_week() (in addition to the manual test points used
530    /// for testing week_of()).
531    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        // First day of year is a Thursday.
628        assert_eq!(
629            week_of_month_from_iso_date(&ISO_CALENDAR, 20180101)?,
630            WeekOf {
631                week: 1,
632                unit: RelativeUnit::Current,
633            }
634        );
635        // First day of year is a Friday.
636        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        // The month ends on a Wednesday.
645        assert_eq!(
646            week_of_month_from_iso_date(&ISO_CALENDAR, 20200930)?,
647            WeekOf {
648                week: 1,
649                unit: RelativeUnit::Next,
650            }
651        );
652        // The month ends on a Thursday.
653        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        // US calendar always assigns the week to the current month. 2020-12-31 is a Thursday.
662        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    // The 1st is a Monday and the week starts on Mondays.
684    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    // The 1st is a Wednesday and the week starts on Tuesdays.
692    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    // The 1st is a Monday and the week starts on Sundays.
706    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    // Weekend ends one day before week starts
754    let default_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Saturday, Sunday]));
755    assert_eq!(vec![Saturday, Sunday], default_weekend.collect::<Vec<_>>());
756
757    // Non-contiguous weekend
758    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    // Non-contiguous days and iterator yielding elements based off first_weekday
776    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}