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#[expect(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::DateDuration, types::Weekday, Date, 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
461 .try_added_with_options(DateDuration::for_months(-1), Default::default())
462 .unwrap();
463
464 calendar.week_of(
465 u16::from(previous_month.days_in_month()),
466 u16::from(date.days_in_month()),
467 u16::from(day),
468 date.day_of_week(),
469 )
470 }
471
472 #[test]
473 fn test_week_of_month_using_dates() {
474 assert_eq!(
475 week_of_month_from_iso_date(ISO_CALENDAR, 20210418).unwrap(),
476 WeekOf {
477 week: 3,
478 unit: RelativeUnit::Current,
479 }
480 );
481 assert_eq!(
482 week_of_month_from_iso_date(ISO_CALENDAR, 20210419).unwrap(),
483 WeekOf {
484 week: 4,
485 unit: RelativeUnit::Current,
486 }
487 );
488
489 assert_eq!(
491 week_of_month_from_iso_date(ISO_CALENDAR, 20180101).unwrap(),
492 WeekOf {
493 week: 1,
494 unit: RelativeUnit::Current,
495 }
496 );
497 assert_eq!(
499 week_of_month_from_iso_date(ISO_CALENDAR, 20210101).unwrap(),
500 WeekOf {
501 week: 5,
502 unit: RelativeUnit::Previous,
503 }
504 );
505
506 assert_eq!(
508 week_of_month_from_iso_date(ISO_CALENDAR, 20200930).unwrap(),
509 WeekOf {
510 week: 1,
511 unit: RelativeUnit::Next,
512 }
513 );
514 assert_eq!(
516 week_of_month_from_iso_date(ISO_CALENDAR, 20201231).unwrap(),
517 WeekOf {
518 week: 5,
519 unit: RelativeUnit::Current,
520 }
521 );
522
523 assert_eq!(
525 week_of_month_from_iso_date(US_CALENDAR, 20201231).unwrap(),
526 WeekOf {
527 week: 5,
528 unit: RelativeUnit::Current,
529 }
530 );
531 assert_eq!(
532 week_of_month_from_iso_date(US_CALENDAR, 20210101).unwrap(),
533 WeekOf {
534 week: 1,
535 unit: RelativeUnit::Current,
536 }
537 );
538 }
539}
540
541#[test]
542fn test_first_day() {
543 use icu_locale_core::locale;
544
545 assert_eq!(
546 WeekInformation::try_new(locale!("und-US").into())
547 .unwrap()
548 .first_weekday,
549 Weekday::Sunday,
550 );
551
552 assert_eq!(
553 WeekInformation::try_new(locale!("und-FR").into())
554 .unwrap()
555 .first_weekday,
556 Weekday::Monday,
557 );
558
559 assert_eq!(
560 WeekInformation::try_new(locale!("und-FR-u-fw-tue").into())
561 .unwrap()
562 .first_weekday,
563 Weekday::Tuesday,
564 );
565}
566
567#[test]
568fn test_weekend() {
569 use icu_locale_core::locale;
570
571 assert_eq!(
572 WeekInformation::try_new(locale!("und").into())
573 .unwrap()
574 .weekend()
575 .collect::<Vec<_>>(),
576 vec![Weekday::Saturday, Weekday::Sunday],
577 );
578
579 assert_eq!(
580 WeekInformation::try_new(locale!("und-FR").into())
581 .unwrap()
582 .weekend()
583 .collect::<Vec<_>>(),
584 vec![Weekday::Saturday, Weekday::Sunday],
585 );
586
587 assert_eq!(
588 WeekInformation::try_new(locale!("und-IQ").into())
589 .unwrap()
590 .weekend()
591 .collect::<Vec<_>>(),
592 vec![Weekday::Saturday, Weekday::Friday],
593 );
594
595 assert_eq!(
596 WeekInformation::try_new(locale!("und-IR").into())
597 .unwrap()
598 .weekend()
599 .collect::<Vec<_>>(),
600 vec![Weekday::Friday],
601 );
602}
603
604#[test]
605fn test_weekdays_iter() {
606 use Weekday::*;
607
608 let default_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Saturday, Sunday]));
610 assert_eq!(vec![Saturday, Sunday], default_weekend.collect::<Vec<_>>());
611
612 let fri_sun_weekend = WeekdaySetIterator::new(Monday, WeekdaySet::new(&[Friday, Sunday]));
614 assert_eq!(vec![Friday, Sunday], fri_sun_weekend.collect::<Vec<_>>());
615
616 let multiple_contiguous_days = WeekdaySetIterator::new(
617 Monday,
618 WeekdaySet::new(&[
619 Weekday::Tuesday,
620 Weekday::Wednesday,
621 Weekday::Thursday,
622 Weekday::Friday,
623 ]),
624 );
625 assert_eq!(
626 vec![Tuesday, Wednesday, Thursday, Friday],
627 multiple_contiguous_days.collect::<Vec<_>>()
628 );
629
630 let multiple_non_contiguous_days = WeekdaySetIterator::new(
632 Wednesday,
633 WeekdaySet::new(&[
634 Weekday::Tuesday,
635 Weekday::Thursday,
636 Weekday::Friday,
637 Weekday::Sunday,
638 ]),
639 );
640 assert_eq!(
641 vec![Thursday, Friday, Sunday, Tuesday],
642 multiple_non_contiguous_days.collect::<Vec<_>>()
643 );
644}
645
646#[test]
647fn test_iso_weeks() {
648 use crate::types::IsoWeekOfYear;
649 use crate::Date;
650
651 #[expect(clippy::zero_prefixed_literal)]
652 for ((y, m, d), (iso_year, week_number)) in [
653 ((2009, 12, 30), (2009, 53)),
655 ((2009, 12, 31), (2009, 53)),
656 ((2010, 01, 01), (2009, 53)),
657 ((2010, 01, 02), (2009, 53)),
658 ((2010, 01, 03), (2009, 53)),
659 ((2010, 01, 04), (2010, 1)),
660 ((2010, 01, 05), (2010, 1)),
661 ((2029, 12, 29), (2029, 52)),
663 ((2029, 12, 30), (2029, 52)),
664 ((2029, 12, 31), (2030, 1)),
665 ((2030, 01, 01), (2030, 1)),
666 ((2030, 01, 02), (2030, 1)),
667 ((2030, 01, 03), (2030, 1)),
668 ((2030, 01, 04), (2030, 1)),
669 ] {
670 assert_eq!(
671 Date::try_new_iso(y, m, d).unwrap().week_of_year(),
672 IsoWeekOfYear {
673 iso_year,
674 week_number
675 }
676 );
677 }
678}