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}