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
7#[doc(no_inline)]
8pub use calendrical_calculations::rata_die::RataDie;
9use core::fmt;
10use tinystr::TinyAsciiStr;
11use tinystr::{TinyStr16, TinyStr4};
12use zerovec::maps::ZeroMapKV;
13use zerovec::ule::AsULE;
14
15/// The type of year: Calendars like Chinese don't have an era and instead format with cyclic years.
16#[derive(Copy, Clone, Debug, PartialEq)]
17#[non_exhaustive]
18pub enum YearInfo {
19    /// An era and a year in that era
20    Era(EraYear),
21    /// A cyclic year, and the related ISO year
22    ///
23    /// Knowing the cyclic year is typically not enough to pinpoint a date, however cyclic calendars
24    /// don't typically use eras, so disambiguation can be done by saying things like "Year 甲辰 (2024)"
25    Cyclic(CyclicYear),
26}
27
28impl From<EraYear> for YearInfo {
29    fn from(value: EraYear) -> Self {
30        Self::Era(value)
31    }
32}
33
34impl From<CyclicYear> for YearInfo {
35    fn from(value: CyclicYear) -> Self {
36        Self::Cyclic(value)
37    }
38}
39
40impl YearInfo {
41    /// Get *some* year number that can be displayed
42    ///
43    /// Gets the era year for era calendars, and the related ISO year for cyclic calendars.
44    pub fn era_year_or_related_iso(self) -> i32 {
45        match self {
46            YearInfo::Era(e) => e.year,
47            YearInfo::Cyclic(c) => c.related_iso,
48        }
49    }
50
51    /// Get the era year information, if available
52    pub fn era(self) -> Option<EraYear> {
53        match self {
54            Self::Era(e) => Some(e),
55            Self::Cyclic(_) => None,
56        }
57    }
58
59    /// Get the cyclic year informat, if available
60    pub fn cyclic(self) -> Option<CyclicYear> {
61        match self {
62            Self::Era(_) => None,
63            Self::Cyclic(c) => Some(c),
64        }
65    }
66}
67
68/// Defines whether the era or century is required to interpret the year.
69///
70/// For example 2024 AD can be formatted as `2024`, or even `24`, but 1931 AD
71/// should not be formatted as `31`, and 2024 BC should not be formatted as `2024`.
72#[derive(Copy, Clone, Debug, PartialEq)]
73#[allow(clippy::exhaustive_enums)] // logically complete
74pub enum YearAmbiguity {
75    /// The year is unambiguous without a century or era.
76    Unambiguous,
77    /// The century is required, the era may be included.
78    CenturyRequired,
79    /// The era is required, the century may be included.
80    EraRequired,
81    /// The century and era are required.
82    EraAndCenturyRequired,
83}
84
85/// Year information for a year that is specified with an era
86#[derive(Copy, Clone, Debug, PartialEq)]
87#[non_exhaustive]
88pub struct EraYear {
89    /// The numeric year in that era
90    pub year: i32,
91    /// The era code as defined by CLDR, expect for cases where CLDR does not define a code.
92    pub era: TinyStr16,
93    /// An era index, for calendars with a small set of eras.
94    ///
95    /// The only guarantee we make is that these values are stable. These do *not*
96    /// match the indices produced by ICU4C or CLDR.
97    ///
98    /// These are used by ICU4X datetime formatting for efficiently storing data.
99    pub era_index: Option<u8>,
100    /// The ambiguity of the era/year combination
101    pub ambiguity: YearAmbiguity,
102}
103
104/// Year information for a year that is specified as a cyclic year
105#[derive(Copy, Clone, Debug, PartialEq)]
106#[non_exhaustive]
107pub struct CyclicYear {
108    /// The year in the cycle, 1-based
109    pub year: u8,
110    /// The ISO year corresponding to this year
111    pub related_iso: i32,
112}
113
114/// Representation of a month in a year
115///
116/// Month codes typically look like `M01`, `M02`, etc, but can handle leap months
117/// (`M03L`) in lunar calendars. Solar calendars will have codes between `M01` and `M12`
118/// potentially with an `M13` for epagomenal months. Check the docs for a particular calendar
119/// for details on what its month codes are.
120///
121/// Month codes are shared with Temporal, [see Temporal proposal][era-proposal].
122///
123/// [era-proposal]: https://tc39.es/proposal-intl-era-monthcode/
124#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[allow(clippy::exhaustive_structs)] // this is a newtype
126#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
127#[cfg_attr(feature = "datagen", databake(path = icu_calendar::types))]
128#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
129pub struct MonthCode(pub TinyStr4);
130
131impl MonthCode {
132    /// Returns an option which is `Some` containing the non-month version of a leap month
133    /// if the [`MonthCode`] this method is called upon is a leap month, and `None` otherwise.
134    /// This method assumes the [`MonthCode`] is valid.
135    pub fn get_normal_if_leap(self) -> Option<MonthCode> {
136        let bytes = self.0.all_bytes();
137        if bytes[3] == b'L' {
138            Some(MonthCode(TinyAsciiStr::try_from_utf8(&bytes[0..3]).ok()?))
139        } else {
140            None
141        }
142    }
143    /// Get the month number and whether or not it is leap from the month code
144    pub fn parsed(self) -> Option<(u8, bool)> {
145        // Match statements on tinystrs are annoying so instead
146        // we calculate it from the bytes directly
147
148        let bytes = self.0.all_bytes();
149        let is_leap = bytes[3] == b'L';
150        if bytes[0] != b'M' {
151            return None;
152        }
153        if bytes[1] == b'0' {
154            if bytes[2] >= b'1' && bytes[2] <= b'9' {
155                return Some((bytes[2] - b'0', is_leap));
156            }
157        } else if bytes[1] == b'1' && bytes[2] >= b'0' && bytes[2] <= b'3' {
158            return Some((10 + bytes[2] - b'0', is_leap));
159        }
160        None
161    }
162
163    /// Construct a "normal" month code given a number ("Mxx").
164    ///
165    /// Returns an error for months greater than 99
166    pub fn new_normal(number: u8) -> Option<Self> {
167        let tens = number / 10;
168        let ones = number % 10;
169        if tens > 9 {
170            return None;
171        }
172
173        let bytes = [b'M', b'0' + tens, b'0' + ones, 0];
174        Some(MonthCode(TinyAsciiStr::try_from_raw(bytes).ok()?))
175    }
176}
177
178#[test]
179fn test_get_normal_month_code_if_leap() {
180    let mc1 = MonthCode(tinystr::tinystr!(4, "M01L"));
181    let result1 = mc1.get_normal_if_leap();
182    assert_eq!(result1, Some(MonthCode(tinystr::tinystr!(4, "M01"))));
183
184    let mc2 = MonthCode(tinystr::tinystr!(4, "M11L"));
185    let result2 = mc2.get_normal_if_leap();
186    assert_eq!(result2, Some(MonthCode(tinystr::tinystr!(4, "M11"))));
187
188    let mc_invalid = MonthCode(tinystr::tinystr!(4, "M10"));
189    let result_invalid = mc_invalid.get_normal_if_leap();
190    assert_eq!(result_invalid, None);
191}
192
193impl AsULE for MonthCode {
194    type ULE = TinyStr4;
195    fn to_unaligned(self) -> TinyStr4 {
196        self.0
197    }
198    fn from_unaligned(u: TinyStr4) -> Self {
199        Self(u)
200    }
201}
202
203impl<'a> ZeroMapKV<'a> for MonthCode {
204    type Container = zerovec::ZeroVec<'a, MonthCode>;
205    type Slice = zerovec::ZeroSlice<MonthCode>;
206    type GetType = <MonthCode as AsULE>::ULE;
207    type OwnedType = MonthCode;
208}
209
210impl fmt::Display for MonthCode {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(f, "{}", self.0)
213    }
214}
215
216/// Representation of a formattable month.
217#[derive(Copy, Clone, Debug, PartialEq)]
218#[non_exhaustive]
219pub struct MonthInfo {
220    /// The month number in this given year. For calendars with leap months, all months after
221    /// the leap month will end up with an incremented number.
222    ///
223    /// In general, prefer using the month code in generic code.
224    pub ordinal: u8,
225
226    /// The month code, used to distinguish months during leap years.
227    ///
228    /// This follows [Temporal's specification](https://tc39.es/proposal-intl-era-monthcode/#table-additional-month-codes).
229    /// Months considered the "same" have the same code: This means that the Hebrew months "Adar" and "Adar II" ("Adar, but during a leap year")
230    /// are considered the same month and have the code M05
231    pub standard_code: MonthCode,
232    /// A month code, useable for formatting
233    ///
234    /// This may not necessarily be the canonical month code for a month in cases where a month has different
235    /// formatting in a leap year, for example Adar/Adar II in the Hebrew calendar in a leap year has
236    /// the standard code M06, but for formatting specifically the Hebrew calendar will return M06L since it is formatted
237    /// differently.
238    pub formatting_code: MonthCode,
239}
240
241impl MonthInfo {
242    /// Gets the month number. A month number N is not necessarily the Nth month in the year
243    /// if there are leap months in the year, rather it is associated with the Nth month of a "regular"
244    /// year. There may be multiple month Ns in a year
245    pub fn month_number(self) -> u8 {
246        self.standard_code
247            .parsed()
248            .map(|(i, _)| i)
249            .unwrap_or(self.ordinal)
250    }
251
252    /// Get whether the month is a leap month
253    pub fn is_leap(self) -> bool {
254        self.standard_code.parsed().map(|(_, l)| l).unwrap_or(false)
255    }
256}
257
258/// The current day of the year, 1-based.
259#[derive(Copy, Clone, Debug, PartialEq)]
260#[allow(clippy::exhaustive_structs)] // this is a newtype
261pub struct DayOfYear(pub u16);
262
263/// A 1-based day number in a month.
264#[allow(clippy::exhaustive_structs)] // this is a newtype
265#[derive(Clone, Copy, Debug, PartialEq)]
266pub struct DayOfMonth(pub u8);
267
268/// A week number in a year
269#[derive(Clone, Copy, Debug, PartialEq)]
270#[allow(clippy::exhaustive_structs)] // this is a newtype
271pub struct IsoWeekOfYear {
272    /// The 1-based ISO week number
273    pub week_number: u8,
274    /// The ISO year
275    pub iso_year: i32,
276}
277
278/// A day of week in month. 1-based.
279#[derive(Clone, Copy, Debug, PartialEq)]
280#[allow(clippy::exhaustive_structs)] // this is a newtype
281pub struct DayOfWeekInMonth(pub u8);
282
283impl From<DayOfMonth> for DayOfWeekInMonth {
284    fn from(day_of_month: DayOfMonth) -> Self {
285        DayOfWeekInMonth(1 + ((day_of_month.0 - 1) / 7))
286    }
287}
288
289#[test]
290fn test_day_of_week_in_month() {
291    assert_eq!(DayOfWeekInMonth::from(DayOfMonth(1)).0, 1);
292    assert_eq!(DayOfWeekInMonth::from(DayOfMonth(7)).0, 1);
293    assert_eq!(DayOfWeekInMonth::from(DayOfMonth(8)).0, 2);
294}
295
296/// A weekday in a 7-day week, according to ISO-8601.
297///
298/// The discriminant values correspond to ISO-8601 weekday numbers (Monday = 1, Sunday = 7).
299///
300/// # Examples
301///
302/// ```
303/// use icu::calendar::types::Weekday;
304///
305/// assert_eq!(1, Weekday::Monday as usize);
306/// assert_eq!(7, Weekday::Sunday as usize);
307/// ```
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309#[allow(missing_docs)] // The weekday variants should be self-obvious.
310#[repr(i8)]
311#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
312#[cfg_attr(feature = "datagen", databake(path = icu_calendar::types))]
313#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
314#[allow(clippy::exhaustive_enums)] // This is stable
315pub enum Weekday {
316    Monday = 1,
317    Tuesday,
318    Wednesday,
319    Thursday,
320    Friday,
321    Saturday,
322    Sunday,
323}
324
325// RD 0 is a Sunday
326const SUNDAY: RataDie = RataDie::new(0);
327
328impl From<RataDie> for Weekday {
329    fn from(value: RataDie) -> Self {
330        use Weekday::*;
331        match (value - SUNDAY).rem_euclid(7) {
332            0 => Sunday,
333            1 => Monday,
334            2 => Tuesday,
335            3 => Wednesday,
336            4 => Thursday,
337            5 => Friday,
338            6 => Saturday,
339            _ => unreachable!(),
340        }
341    }
342}
343
344impl Weekday {
345    /// Convert from an ISO-8601 weekday number to an [`Weekday`] enum. 0 is automatically converted
346    /// to 7 (Sunday). If the number is out of range, it is interpreted modulo 7.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use icu::calendar::types::Weekday;
352    ///
353    /// assert_eq!(Weekday::Sunday, Weekday::from_days_since_sunday(0));
354    /// assert_eq!(Weekday::Monday, Weekday::from_days_since_sunday(1));
355    /// assert_eq!(Weekday::Sunday, Weekday::from_days_since_sunday(7));
356    /// assert_eq!(Weekday::Monday, Weekday::from_days_since_sunday(8));
357    /// ```
358    pub fn from_days_since_sunday(input: isize) -> Self {
359        (SUNDAY + input as i64).into()
360    }
361
362    /// Returns the day after the current day.
363    pub(crate) fn next_day(self) -> Weekday {
364        use Weekday::*;
365        match self {
366            Monday => Tuesday,
367            Tuesday => Wednesday,
368            Wednesday => Thursday,
369            Thursday => Friday,
370            Friday => Saturday,
371            Saturday => Sunday,
372            Sunday => Monday,
373        }
374    }
375}