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