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}