icu_calendar/
types.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
5//! This module contains various types used by `icu_calendar` and `icu::datetime`
6
7use crate::error::CalendarError;
8use core::convert::TryFrom;
9use core::convert::TryInto;
10use core::fmt;
11use core::num::NonZeroU8;
12use core::str::FromStr;
13use tinystr::TinyAsciiStr;
14use tinystr::{TinyStr16, TinyStr4};
15use zerovec::maps::ZeroMapKV;
16use zerovec::ule::AsULE;
17
18/// The era of a particular date
19///
20/// Different calendars use different era codes, see their documentation
21/// for details.
22///
23/// Era codes are shared with Temporal, [see Temporal proposal][era-proposal].
24///
25/// [era-proposal]: https://tc39.es/proposal-intl-era-monthcode/
26#[derive(Copy, Clone, Debug, PartialEq)]
27#[allow(clippy::exhaustive_structs)] // this is a newtype
28pub struct Era(pub TinyStr16);
29
30impl From<TinyStr16> for Era {
31    fn from(x: TinyStr16) -> Self {
32        Self(x)
33    }
34}
35
36impl FromStr for Era {
37    type Err = <TinyStr16 as FromStr>::Err;
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        s.parse().map(Self)
40    }
41}
42
43/// Representation of a formattable year.
44///
45/// More fields may be added in the future for things like extended year
46#[derive(Copy, Clone, Debug, PartialEq)]
47#[non_exhaustive]
48pub struct FormattableYear {
49    /// The era containing the year.
50    ///
51    /// This may not always be the canonical era for the calendar and could be an alias,
52    /// for example all `islamic` calendars return `islamic` as the formattable era code
53    /// which allows them to share data.
54    pub era: Era,
55
56    /// The year number in the current era (usually 1-based).
57    pub number: i32,
58
59    /// The year in the current cycle for cyclic calendars (1-indexed)
60    /// can be set to `None` for non-cyclic calendars
61    ///
62    /// For chinese and dangi it will be
63    /// a number between 1 and 60, for hypothetical other calendars it may be something else.
64    pub cyclic: Option<NonZeroU8>,
65
66    /// The related ISO year. This is normally the ISO (proleptic Gregorian) year having the greatest
67    /// overlap with the calendar year. It is used in certain date formatting patterns.
68    ///
69    /// Can be `None` if the calendar does not typically use `related_iso` (and CLDR does not contain patterns
70    /// using it)
71    pub related_iso: Option<i32>,
72}
73
74impl FormattableYear {
75    /// Construct a new Year given an era and number
76    ///
77    /// Other fields can be set mutably after construction
78    /// as needed
79    pub fn new(era: Era, number: i32, cyclic: Option<NonZeroU8>) -> Self {
80        Self {
81            era,
82            number,
83            cyclic,
84            related_iso: None,
85        }
86    }
87}
88
89/// Representation of a month in a year
90///
91/// Month codes typically look like `M01`, `M02`, etc, but can handle leap months
92/// (`M03L`) in lunar calendars. Solar calendars will have codes between `M01` and `M12`
93/// potentially with an `M13` for epagomenal months. Check the docs for a particular calendar
94/// for details on what its month codes are.
95///
96/// Month codes are shared with Temporal, [see Temporal proposal][era-proposal].
97///
98/// [era-proposal]: https://tc39.es/proposal-intl-era-monthcode/
99#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
100#[allow(clippy::exhaustive_structs)] // this is a newtype
101#[cfg_attr(
102    feature = "datagen",
103    derive(serde::Serialize, databake::Bake),
104    databake(path = icu_calendar::types),
105)]
106#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
107pub struct MonthCode(pub TinyStr4);
108
109impl MonthCode {
110    /// Returns an option which is `Some` containing the non-month version of a leap month
111    /// if the [`MonthCode`] this method is called upon is a leap month, and `None` otherwise.
112    /// This method assumes the [`MonthCode`] is valid.
113    pub fn get_normal_if_leap(self) -> Option<MonthCode> {
114        let bytes = self.0.all_bytes();
115        if bytes[3] == b'L' {
116            Some(MonthCode(TinyAsciiStr::from_bytes(&bytes[0..3]).ok()?))
117        } else {
118            None
119        }
120    }
121    /// Get the month number and whether or not it is leap from the month code
122    pub fn parsed(self) -> Option<(u8, bool)> {
123        // Match statements on tinystrs are annoying so instead
124        // we calculate it from the bytes directly
125
126        let bytes = self.0.all_bytes();
127        let is_leap = bytes[3] == b'L';
128        if bytes[0] != b'M' {
129            return None;
130        }
131        if bytes[1] == b'0' {
132            if bytes[2] >= b'1' && bytes[2] <= b'9' {
133                return Some((bytes[2] - b'0', is_leap));
134            }
135        } else if bytes[1] == b'1' && bytes[2] >= b'0' && bytes[2] <= b'3' {
136            return Some((10 + bytes[2] - b'0', is_leap));
137        }
138        None
139    }
140
141    /// Construct a "normal" month code given a number ("Mxx").
142    ///
143    /// Returns an error for months greater than 99
144    #[cfg(test)] // Only used in tests for now. Could be made public if people need it.
145    pub(crate) fn new_normal(number: u8) -> Option<Self> {
146        let tens = number / 10;
147        let ones = number % 10;
148        if tens > 9 {
149            return None;
150        }
151
152        let bytes = [b'M', b'0' + tens, b'0' + ones, 0];
153        Some(MonthCode(TinyAsciiStr::try_from_raw(bytes).ok()?))
154    }
155}
156
157#[test]
158fn test_get_normal_month_code_if_leap() {
159    let mc1 = MonthCode(tinystr::tinystr!(4, "M01L"));
160    let result1 = mc1.get_normal_if_leap();
161    assert_eq!(result1, Some(MonthCode(tinystr::tinystr!(4, "M01"))));
162
163    let mc2 = MonthCode(tinystr::tinystr!(4, "M11L"));
164    let result2 = mc2.get_normal_if_leap();
165    assert_eq!(result2, Some(MonthCode(tinystr::tinystr!(4, "M11"))));
166
167    let mc_invalid = MonthCode(tinystr::tinystr!(4, "M10"));
168    let result_invalid = mc_invalid.get_normal_if_leap();
169    assert_eq!(result_invalid, None);
170}
171
172impl AsULE for MonthCode {
173    type ULE = TinyStr4;
174    fn to_unaligned(self) -> TinyStr4 {
175        self.0
176    }
177    fn from_unaligned(u: TinyStr4) -> Self {
178        Self(u)
179    }
180}
181
182impl<'a> ZeroMapKV<'a> for MonthCode {
183    type Container = zerovec::ZeroVec<'a, MonthCode>;
184    type Slice = zerovec::ZeroSlice<MonthCode>;
185    type GetType = <MonthCode as AsULE>::ULE;
186    type OwnedType = MonthCode;
187}
188
189impl fmt::Display for MonthCode {
190    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
191        write!(f, "{}", self.0)
192    }
193}
194
195impl From<TinyStr4> for MonthCode {
196    fn from(x: TinyStr4) -> Self {
197        Self(x)
198    }
199}
200impl FromStr for MonthCode {
201    type Err = <TinyStr4 as FromStr>::Err;
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        s.parse().map(Self)
204    }
205}
206
207/// Representation of a formattable month.
208#[derive(Copy, Clone, Debug, PartialEq)]
209#[allow(clippy::exhaustive_structs)] // this type is stable
210pub struct FormattableMonth {
211    /// The month number in this given year. For calendars with leap months, all months after
212    /// the leap month will end up with an incremented number.
213    ///
214    /// In general, prefer using the month code in generic code.
215    pub ordinal: u32,
216
217    /// The month code, used to distinguish months during leap years.
218    ///
219    /// This may not necessarily be the canonical month code for a month in cases where a month has different
220    /// formatting in a leap year, for example Adar/Adar II in the Hebrew calendar in a leap year has
221    /// the code M06, but for formatting specifically the Hebrew calendar will return M06L since it is formatted
222    /// differently.
223    pub code: MonthCode,
224}
225
226/// A struct containing various details about the position of the day within a year. It is returned
227// by the [`day_of_year_info()`](trait.DateInput.html#tymethod.day_of_year_info) method of the
228// [`DateInput`] trait.
229#[derive(Copy, Clone, Debug, PartialEq)]
230#[allow(clippy::exhaustive_structs)] // this type is stable
231pub struct DayOfYearInfo {
232    /// The current day of the year, 1-based.
233    pub day_of_year: u16,
234    /// The number of days in a year.
235    pub days_in_year: u16,
236    /// The previous year.
237    pub prev_year: FormattableYear,
238    /// The number of days in the previous year.
239    pub days_in_prev_year: u16,
240    /// The next year.
241    pub next_year: FormattableYear,
242}
243
244/// A day number in a month. Usually 1-based.
245#[allow(clippy::exhaustive_structs)] // this is a newtype
246#[derive(Clone, Copy, Debug, PartialEq)]
247pub struct DayOfMonth(pub u32);
248
249/// A week number in a month. Usually 1-based.
250#[derive(Clone, Copy, Debug, PartialEq)]
251#[allow(clippy::exhaustive_structs)] // this is a newtype
252pub struct WeekOfMonth(pub u32);
253
254/// A week number in a year. Usually 1-based.
255#[derive(Clone, Copy, Debug, PartialEq)]
256#[allow(clippy::exhaustive_structs)] // this is a newtype
257pub struct WeekOfYear(pub u32);
258
259/// A day of week in month. 1-based.
260#[derive(Clone, Copy, Debug, PartialEq)]
261#[allow(clippy::exhaustive_structs)] // this is a newtype
262pub struct DayOfWeekInMonth(pub u32);
263
264impl From<DayOfMonth> for DayOfWeekInMonth {
265    fn from(day_of_month: DayOfMonth) -> Self {
266        DayOfWeekInMonth(1 + ((day_of_month.0 - 1) / 7))
267    }
268}
269
270#[test]
271fn test_day_of_week_in_month() {
272    assert_eq!(DayOfWeekInMonth::from(DayOfMonth(1)).0, 1);
273    assert_eq!(DayOfWeekInMonth::from(DayOfMonth(7)).0, 1);
274    assert_eq!(DayOfWeekInMonth::from(DayOfMonth(8)).0, 2);
275}
276
277/// This macro defines a struct for 0-based date fields: hours, minutes, seconds
278/// and fractional seconds. Each unit is bounded by a range. The traits implemented
279/// here will return a Result on whether or not the unit is in range from the given
280/// input.
281macro_rules! dt_unit {
282    ($name:ident, $storage:ident, $value:expr, $docs:expr) => {
283        #[doc=$docs]
284        #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
285        pub struct $name($storage);
286
287        impl $name {
288            /// Gets the numeric value for this component.
289            pub const fn number(self) -> $storage {
290                self.0
291            }
292
293            /// Creates a new value at 0.
294            pub const fn zero() -> $name {
295                Self(0)
296            }
297        }
298
299        impl FromStr for $name {
300            type Err = CalendarError;
301
302            fn from_str(input: &str) -> Result<Self, Self::Err> {
303                let val: $storage = input.parse()?;
304                if val > $value {
305                    Err(CalendarError::Overflow {
306                        field: "$name",
307                        max: $value,
308                    })
309                } else {
310                    Ok(Self(val))
311                }
312            }
313        }
314
315        impl TryFrom<$storage> for $name {
316            type Error = CalendarError;
317
318            fn try_from(input: $storage) -> Result<Self, Self::Error> {
319                if input > $value {
320                    Err(CalendarError::Overflow {
321                        field: "$name",
322                        max: $value,
323                    })
324                } else {
325                    Ok(Self(input))
326                }
327            }
328        }
329
330        impl TryFrom<usize> for $name {
331            type Error = CalendarError;
332
333            fn try_from(input: usize) -> Result<Self, Self::Error> {
334                if input > $value {
335                    Err(CalendarError::Overflow {
336                        field: "$name",
337                        max: $value,
338                    })
339                } else {
340                    Ok(Self(input as $storage))
341                }
342            }
343        }
344
345        impl From<$name> for $storage {
346            fn from(input: $name) -> Self {
347                input.0
348            }
349        }
350
351        impl From<$name> for usize {
352            fn from(input: $name) -> Self {
353                input.0 as Self
354            }
355        }
356
357        impl $name {
358            /// Attempts to add two values.
359            /// Returns `Some` if the sum is within bounds.
360            /// Returns `None` if the sum is out of bounds.
361            pub fn try_add(self, other: $storage) -> Option<Self> {
362                let sum = self.0.saturating_add(other);
363                if sum > $value {
364                    None
365                } else {
366                    Some(Self(sum))
367                }
368            }
369
370            /// Attempts to subtract two values.
371            /// Returns `Some` if the difference is within bounds.
372            /// Returns `None` if the difference is out of bounds.
373            pub fn try_sub(self, other: $storage) -> Option<Self> {
374                self.0.checked_sub(other).map(Self)
375            }
376        }
377    };
378}
379
380dt_unit!(
381    IsoHour,
382    u8,
383    24,
384    "An ISO-8601 hour component, for use with ISO calendars.
385
386Must be within inclusive bounds `[0, 24]`. The value could be equal to 24 to
387denote the end of a day, with the writing 24:00:00. It corresponds to the same
388time as the next day at 00:00:00."
389);
390
391dt_unit!(
392    IsoMinute,
393    u8,
394    60,
395    "An ISO-8601 minute component, for use with ISO calendars.
396
397Must be within inclusive bounds `[0, 60]`. The value could be equal to 60 to
398denote the end of an hour, with the writing 12:60:00. This example corresponds
399to the same time as 13:00:00. This is an extension to ISO 8601."
400);
401
402dt_unit!(
403    IsoSecond,
404    u8,
405    61,
406    "An ISO-8601 second component, for use with ISO calendars.
407
408Must be within inclusive bounds `[0, 61]`. `60` accommodates for leap seconds.
409
410The value could also be equal to 60 or 61, to indicate the end of a leap second,
411with the writing `23:59:61.000000000Z` or `23:59:60.000000000Z`. These examples,
412if used with this goal, would correspond to the same time as the next day, at
413time `00:00:00.000000000Z`. This is an extension to ISO 8601."
414);
415
416dt_unit!(
417    NanoSecond,
418    u32,
419    999_999_999,
420    "A fractional second component, stored as nanoseconds.
421
422Must be within inclusive bounds `[0, 999_999_999]`."
423);
424
425#[test]
426fn test_iso_hour_arithmetic() {
427    const HOUR_MAX: u8 = 24;
428    const HOUR_VALUE: u8 = 5;
429    let hour = IsoHour(HOUR_VALUE);
430
431    // middle of bounds
432    assert_eq!(
433        hour.try_add(HOUR_VALUE - 1),
434        Some(IsoHour(HOUR_VALUE + (HOUR_VALUE - 1)))
435    );
436    assert_eq!(
437        hour.try_sub(HOUR_VALUE - 1),
438        Some(IsoHour(HOUR_VALUE - (HOUR_VALUE - 1)))
439    );
440
441    // edge of bounds
442    assert_eq!(hour.try_add(HOUR_MAX - HOUR_VALUE), Some(IsoHour(HOUR_MAX)));
443    assert_eq!(hour.try_sub(HOUR_VALUE), Some(IsoHour(0)));
444
445    // out of bounds
446    assert_eq!(hour.try_add(1 + HOUR_MAX - HOUR_VALUE), None);
447    assert_eq!(hour.try_sub(1 + HOUR_VALUE), None);
448}
449
450#[test]
451fn test_iso_minute_arithmetic() {
452    const MINUTE_MAX: u8 = 60;
453    const MINUTE_VALUE: u8 = 5;
454    let minute = IsoMinute(MINUTE_VALUE);
455
456    // middle of bounds
457    assert_eq!(
458        minute.try_add(MINUTE_VALUE - 1),
459        Some(IsoMinute(MINUTE_VALUE + (MINUTE_VALUE - 1)))
460    );
461    assert_eq!(
462        minute.try_sub(MINUTE_VALUE - 1),
463        Some(IsoMinute(MINUTE_VALUE - (MINUTE_VALUE - 1)))
464    );
465
466    // edge of bounds
467    assert_eq!(
468        minute.try_add(MINUTE_MAX - MINUTE_VALUE),
469        Some(IsoMinute(MINUTE_MAX))
470    );
471    assert_eq!(minute.try_sub(MINUTE_VALUE), Some(IsoMinute(0)));
472
473    // out of bounds
474    assert_eq!(minute.try_add(1 + MINUTE_MAX - MINUTE_VALUE), None);
475    assert_eq!(minute.try_sub(1 + MINUTE_VALUE), None);
476}
477
478#[test]
479fn test_iso_second_arithmetic() {
480    const SECOND_MAX: u8 = 61;
481    const SECOND_VALUE: u8 = 5;
482    let second = IsoSecond(SECOND_VALUE);
483
484    // middle of bounds
485    assert_eq!(
486        second.try_add(SECOND_VALUE - 1),
487        Some(IsoSecond(SECOND_VALUE + (SECOND_VALUE - 1)))
488    );
489    assert_eq!(
490        second.try_sub(SECOND_VALUE - 1),
491        Some(IsoSecond(SECOND_VALUE - (SECOND_VALUE - 1)))
492    );
493
494    // edge of bounds
495    assert_eq!(
496        second.try_add(SECOND_MAX - SECOND_VALUE),
497        Some(IsoSecond(SECOND_MAX))
498    );
499    assert_eq!(second.try_sub(SECOND_VALUE), Some(IsoSecond(0)));
500
501    // out of bounds
502    assert_eq!(second.try_add(1 + SECOND_MAX - SECOND_VALUE), None);
503    assert_eq!(second.try_sub(1 + SECOND_VALUE), None);
504}
505
506#[test]
507fn test_iso_nano_second_arithmetic() {
508    const NANO_SECOND_MAX: u32 = 999_999_999;
509    const NANO_SECOND_VALUE: u32 = 5;
510    let nano_second = NanoSecond(NANO_SECOND_VALUE);
511
512    // middle of bounds
513    assert_eq!(
514        nano_second.try_add(NANO_SECOND_VALUE - 1),
515        Some(NanoSecond(NANO_SECOND_VALUE + (NANO_SECOND_VALUE - 1)))
516    );
517    assert_eq!(
518        nano_second.try_sub(NANO_SECOND_VALUE - 1),
519        Some(NanoSecond(NANO_SECOND_VALUE - (NANO_SECOND_VALUE - 1)))
520    );
521
522    // edge of bounds
523    assert_eq!(
524        nano_second.try_add(NANO_SECOND_MAX - NANO_SECOND_VALUE),
525        Some(NanoSecond(NANO_SECOND_MAX))
526    );
527    assert_eq!(nano_second.try_sub(NANO_SECOND_VALUE), Some(NanoSecond(0)));
528
529    // out of bounds
530    assert_eq!(
531        nano_second.try_add(1 + NANO_SECOND_MAX - NANO_SECOND_VALUE),
532        None
533    );
534    assert_eq!(nano_second.try_sub(1 + NANO_SECOND_VALUE), None);
535}
536
537/// A representation of a time in hours, minutes, seconds, and nanoseconds
538#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
539#[allow(clippy::exhaustive_structs)] // this type is stable
540pub struct Time {
541    /// 0-based hour.
542    pub hour: IsoHour,
543
544    /// 0-based minute.
545    pub minute: IsoMinute,
546
547    /// 0-based second.
548    pub second: IsoSecond,
549
550    /// Fractional second
551    pub nanosecond: NanoSecond,
552}
553
554impl Time {
555    /// Construct a new [`Time`], without validating that all components are in range
556    pub const fn new(
557        hour: IsoHour,
558        minute: IsoMinute,
559        second: IsoSecond,
560        nanosecond: NanoSecond,
561    ) -> Self {
562        Self {
563            hour,
564            minute,
565            second,
566            nanosecond,
567        }
568    }
569
570    /// Construct a new [`Time`] representing midnight (00:00.000)
571    pub const fn midnight() -> Self {
572        Self {
573            hour: IsoHour::zero(),
574            minute: IsoMinute::zero(),
575            second: IsoSecond::zero(),
576            nanosecond: NanoSecond::zero(),
577        }
578    }
579
580    /// Construct a new [`Time`], whilst validating that all components are in range
581    pub fn try_new(
582        hour: u8,
583        minute: u8,
584        second: u8,
585        nanosecond: u32,
586    ) -> Result<Self, CalendarError> {
587        Ok(Self {
588            hour: hour.try_into()?,
589            minute: minute.try_into()?,
590            second: second.try_into()?,
591            nanosecond: nanosecond.try_into()?,
592        })
593    }
594
595    /// Takes a number of minutes, which could be positive or negative, and returns the Time
596    /// and the day number, which could be positive or negative.
597    pub(crate) fn from_minute_with_remainder_days(minute: i32) -> (Time, i32) {
598        let (extra_days, minute_in_day) = (minute.div_euclid(1440), minute.rem_euclid(1440));
599        let (hours, minutes) = (minute_in_day / 60, minute_in_day % 60);
600        #[allow(clippy::unwrap_used)] // values are moduloed to be in range
601        (
602            Self {
603                hour: (hours as u8).try_into().unwrap(),
604                minute: (minutes as u8).try_into().unwrap(),
605                second: IsoSecond::zero(),
606                nanosecond: NanoSecond::zero(),
607            },
608            extra_days,
609        )
610    }
611}
612
613#[test]
614fn test_from_minute_with_remainder_days() {
615    #[derive(Debug)]
616    struct TestCase {
617        minute: i32,
618        expected_time: Time,
619        expected_remainder: i32,
620    }
621    let zero_time = Time::new(
622        IsoHour::zero(),
623        IsoMinute::zero(),
624        IsoSecond::zero(),
625        NanoSecond::zero(),
626    );
627    let first_minute_in_day = Time::new(
628        IsoHour::zero(),
629        IsoMinute::try_from(1u8).unwrap(),
630        IsoSecond::zero(),
631        NanoSecond::zero(),
632    );
633    let last_minute_in_day = Time::new(
634        IsoHour::try_from(23u8).unwrap(),
635        IsoMinute::try_from(59u8).unwrap(),
636        IsoSecond::zero(),
637        NanoSecond::zero(),
638    );
639    let cases = [
640        TestCase {
641            minute: 0,
642            expected_time: zero_time,
643            expected_remainder: 0,
644        },
645        TestCase {
646            minute: 30,
647            expected_time: Time::new(
648                IsoHour::zero(),
649                IsoMinute::try_from(30u8).unwrap(),
650                IsoSecond::zero(),
651                NanoSecond::zero(),
652            ),
653            expected_remainder: 0,
654        },
655        TestCase {
656            minute: 60,
657            expected_time: Time::new(
658                IsoHour::try_from(1u8).unwrap(),
659                IsoMinute::zero(),
660                IsoSecond::zero(),
661                NanoSecond::zero(),
662            ),
663            expected_remainder: 0,
664        },
665        TestCase {
666            minute: 90,
667            expected_time: Time::new(
668                IsoHour::try_from(1u8).unwrap(),
669                IsoMinute::try_from(30u8).unwrap(),
670                IsoSecond::zero(),
671                NanoSecond::zero(),
672            ),
673            expected_remainder: 0,
674        },
675        TestCase {
676            minute: 1439,
677            expected_time: last_minute_in_day,
678            expected_remainder: 0,
679        },
680        TestCase {
681            minute: 1440,
682            expected_time: Time::new(
683                IsoHour::zero(),
684                IsoMinute::zero(),
685                IsoSecond::zero(),
686                NanoSecond::zero(),
687            ),
688            expected_remainder: 1,
689        },
690        TestCase {
691            minute: 1441,
692            expected_time: first_minute_in_day,
693            expected_remainder: 1,
694        },
695        TestCase {
696            minute: i32::MAX,
697            expected_time: Time::new(
698                IsoHour::try_from(2u8).unwrap(),
699                IsoMinute::try_from(7u8).unwrap(),
700                IsoSecond::zero(),
701                NanoSecond::zero(),
702            ),
703            expected_remainder: 1491308,
704        },
705        TestCase {
706            minute: -1,
707            expected_time: last_minute_in_day,
708            expected_remainder: -1,
709        },
710        TestCase {
711            minute: -1439,
712            expected_time: first_minute_in_day,
713            expected_remainder: -1,
714        },
715        TestCase {
716            minute: -1440,
717            expected_time: zero_time,
718            expected_remainder: -1,
719        },
720        TestCase {
721            minute: -1441,
722            expected_time: last_minute_in_day,
723            expected_remainder: -2,
724        },
725        TestCase {
726            minute: i32::MIN,
727            expected_time: Time::new(
728                IsoHour::try_from(21u8).unwrap(),
729                IsoMinute::try_from(52u8).unwrap(),
730                IsoSecond::zero(),
731                NanoSecond::zero(),
732            ),
733            expected_remainder: -1491309,
734        },
735    ];
736    for cas in cases {
737        let (actual_time, actual_remainder) = Time::from_minute_with_remainder_days(cas.minute);
738        assert_eq!(actual_time, cas.expected_time, "{cas:?}");
739        assert_eq!(actual_remainder, cas.expected_remainder, "{cas:?}");
740    }
741}
742
743/// A weekday in a 7-day week, according to ISO-8601.
744///
745/// The discriminant values correspond to ISO-8601 weekday numbers (Monday = 1, Sunday = 7).
746///
747/// # Examples
748///
749/// ```
750/// use icu::calendar::types::IsoWeekday;
751///
752/// assert_eq!(1, IsoWeekday::Monday as usize);
753/// assert_eq!(7, IsoWeekday::Sunday as usize);
754/// ```
755#[derive(Clone, Copy, Debug, PartialEq, Eq)]
756#[allow(missing_docs)] // The weekday variants should be self-obvious.
757#[repr(i8)]
758#[cfg_attr(
759    feature = "datagen",
760    derive(serde::Serialize, databake::Bake),
761    databake(path = icu_calendar::types),
762)]
763#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
764#[allow(clippy::exhaustive_enums)] // This is stable
765pub enum IsoWeekday {
766    Monday = 1,
767    Tuesday,
768    Wednesday,
769    Thursday,
770    Friday,
771    Saturday,
772    Sunday,
773}
774
775impl From<usize> for IsoWeekday {
776    /// Convert from an ISO-8601 weekday number to an [`IsoWeekday`] enum. 0 is automatically converted
777    /// to 7 (Sunday). If the number is out of range, it is interpreted modulo 7.
778    ///
779    /// # Examples
780    ///
781    /// ```
782    /// use icu::calendar::types::IsoWeekday;
783    ///
784    /// assert_eq!(IsoWeekday::Sunday, IsoWeekday::from(0));
785    /// assert_eq!(IsoWeekday::Monday, IsoWeekday::from(1));
786    /// assert_eq!(IsoWeekday::Sunday, IsoWeekday::from(7));
787    /// assert_eq!(IsoWeekday::Monday, IsoWeekday::from(8));
788    /// ```
789    fn from(input: usize) -> Self {
790        use IsoWeekday::*;
791        match input % 7 {
792            0 => Sunday,
793            1 => Monday,
794            2 => Tuesday,
795            3 => Wednesday,
796            4 => Thursday,
797            5 => Friday,
798            6 => Saturday,
799            _ => unreachable!(),
800        }
801    }
802}
803
804impl IsoWeekday {
805    /// Returns the day after the current day.
806    pub(crate) fn next_day(self) -> IsoWeekday {
807        use IsoWeekday::*;
808        match self {
809            Monday => Tuesday,
810            Tuesday => Wednesday,
811            Wednesday => Thursday,
812            Thursday => Friday,
813            Friday => Saturday,
814            Saturday => Sunday,
815            Sunday => Monday,
816        }
817    }
818}