Skip to main content

icu_datetime/provider/fields/
symbols.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
5use crate::DateTimeFormatterPreferences;
6use crate::options::SubsecondDigits;
7#[cfg(feature = "datagen")]
8use crate::provider::fields::FieldLength;
9use core::{cmp::Ordering, convert::TryFrom};
10use displaydoc::Display;
11use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle;
12use icu_locale_core::subtags::Region;
13use icu_locale_core::subtags::region;
14use icu_provider::prelude::*;
15use zerovec::ule::{AsULE, ULE, UleError};
16
17/// An error relating to the field symbol for a date pattern field.
18#[derive(Display, Debug, PartialEq, Copy, Clone)]
19#[non_exhaustive]
20pub enum SymbolError {
21    /// Invalid field symbol index.
22    #[displaydoc("Invalid field symbol index: {0}")]
23    InvalidIndex(u8),
24    /// Unknown field symbol.
25    #[displaydoc("Unknown field symbol: {0}")]
26    Unknown(char),
27    /// Invalid character for a field symbol.
28    #[displaydoc("Invalid character for a field symbol: {0}")]
29    Invalid(u8),
30}
31
32impl core::error::Error for SymbolError {}
33
34/// A field symbol for a date formatting pattern.
35///
36/// Field symbols are a more granular distinction
37/// for a pattern field within the category of a field type. Examples of field types are:
38/// `Year`, `Month`, `Hour`.  Within the [`Hour`] field type, examples of field symbols are: [`Hour::H12`],
39/// [`Hour::H23`]. Each field symbol is represented within the date formatting pattern string
40/// by a distinct character from the set of `A..Z` and `a..z`.
41#[derive(Debug, Eq, PartialEq, Clone, Copy)]
42#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
43#[cfg_attr(feature = "datagen", databake(path = icu_datetime::fields))]
44#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
45#[allow(clippy::exhaustive_enums)] // part of data struct
46pub enum FieldSymbol {
47    /// Era name.
48    Era,
49    /// Year number or year name.
50    Year(Year),
51    /// Month number or month name.
52    Month(Month),
53    /// Week number or week name.
54    Week(Week),
55    /// Day number relative to a time period longer than a week (ex: month, year).
56    Day(Day),
57    /// Day number or day name relative to a week.
58    Weekday(Weekday),
59    /// Name of a period within a day.
60    DayPeriod(DayPeriod),
61    /// Hour number within a day, possibly with day period.
62    Hour(Hour),
63    /// Minute number within an hour.
64    Minute,
65    /// Seconds integer within a minute or milliseconds within a day.
66    Second(Second),
67    /// Time zone as a name, a zone ID, or a ISO 8601 numerical offset.
68    TimeZone(TimeZone),
69    /// Seconds with fractional digits. If seconds are an integer,
70    /// [`FieldSymbol::Second`] is used.
71    DecimalSecond(DecimalSecond),
72}
73
74impl FieldSymbol {
75    /// Symbols are necessary components of `Pattern` struct which
76    /// uses efficient byte serialization and deserialization via `zerovec`.
77    ///
78    /// The `FieldSymbol` impl provides non-public methods that can be used to efficiently
79    /// convert between `u8` and the symbol variant.
80    ///
81    /// The serialization model packages the variant in one byte.
82    ///
83    /// 1) The top four bits are used to determine the type of the field
84    ///    using that type's `idx()/from_idx()` for the mapping.
85    ///    (Examples: `Year`, `Month`, `Hour`)
86    ///
87    /// 2) The bottom four bits are used to determine the symbol of the type.
88    ///    (Examples: `Year::Calendar`, `Hour::H11`)
89    ///
90    /// # Diagram
91    ///
92    /// ```text
93    /// ┌─┬─┬─┬─┬─┬─┬─┬─┐
94    /// ├─┴─┴─┴─┼─┴─┴─┴─┤
95    /// │ Type  │Symbol │
96    /// └───────┴───────┘
97    /// ```
98    ///
99    /// # Optimization
100    ///
101    /// This model is optimized to package data efficiently when `FieldSymbol`
102    /// is used as a variant of `PatternItem`. See the documentation of `PatternItemULE`
103    /// for details on how it is composed.
104    ///
105    /// # Constraints
106    ///
107    /// This model limits the available number of possible types and symbols to 16 each.
108    #[inline]
109    pub(crate) fn idx(self) -> u8 {
110        let (high, low) = match self {
111            FieldSymbol::Era => (0, 0),
112            FieldSymbol::Year(year) => (1, year.idx()),
113            FieldSymbol::Month(month) => (2, month.idx()),
114            FieldSymbol::Week(w) => (3, w.idx()),
115            FieldSymbol::Day(day) => (4, day.idx()),
116            FieldSymbol::Weekday(wd) => (5, wd.idx()),
117            FieldSymbol::DayPeriod(dp) => (6, dp.idx()),
118            FieldSymbol::Hour(hour) => (7, hour.idx()),
119            FieldSymbol::Minute => (8, 0),
120            FieldSymbol::Second(second) => (9, second.idx()),
121            FieldSymbol::TimeZone(tz) => (10, tz.idx()),
122            FieldSymbol::DecimalSecond(second) => (11, second.idx()),
123        };
124        let result = high << 4;
125        result | low
126    }
127
128    #[inline]
129    pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
130        // extract the top four bits to determine the symbol.
131        let low = idx & 0b0000_1111;
132        // use the bottom four bits out of `u8` to disriminate the field type.
133        let high = idx >> 4;
134
135        Ok(match high {
136            0 if low == 0 => Self::Era,
137            1 => Self::Year(Year::from_idx(low)?),
138            2 => Self::Month(Month::from_idx(low)?),
139            3 => Self::Week(Week::from_idx(low)?),
140            4 => Self::Day(Day::from_idx(low)?),
141            5 => Self::Weekday(Weekday::from_idx(low)?),
142            6 => Self::DayPeriod(DayPeriod::from_idx(low)?),
143            7 => Self::Hour(Hour::from_idx(low)?),
144            8 if low == 0 => Self::Minute,
145            9 => Self::Second(Second::from_idx(low)?),
146            10 => Self::TimeZone(TimeZone::from_idx(low)?),
147            11 => Self::DecimalSecond(DecimalSecond::from_idx(low)?),
148            _ => return Err(SymbolError::InvalidIndex(idx)),
149        })
150    }
151
152    /// Returns the high 4 bits of `idx()`, representing the field type category
153    /// (e.g., Year, Month, Day, Hour).
154    #[inline]
155    pub(crate) fn type_idx(self) -> u8 {
156        self.idx() >> 4
157    }
158
159    /// Returns the index associated with this [`FieldSymbol`].
160    #[cfg(feature = "datagen")]
161    fn idx_for_skeleton(self) -> u8 {
162        match self {
163            FieldSymbol::Era => 0,
164            FieldSymbol::Year(_) => 1,
165            FieldSymbol::Month(_) => 2,
166            FieldSymbol::Week(_) => 3,
167            FieldSymbol::Day(_) => 4,
168            FieldSymbol::Weekday(_) => 5,
169            FieldSymbol::DayPeriod(_) => 6,
170            FieldSymbol::Hour(_) => 7,
171            FieldSymbol::Minute => 8,
172            FieldSymbol::Second(_) | FieldSymbol::DecimalSecond(_) => 9,
173            FieldSymbol::TimeZone(_) => 10,
174        }
175    }
176
177    /// Compares this enum with other solely based on the enum variant,
178    /// ignoring the enum's data.
179    ///
180    /// [`Second`] and [`DecimalSecond`] are considered equal.
181    #[cfg(feature = "datagen")]
182    pub(crate) fn skeleton_cmp(self, other: Self) -> Ordering {
183        self.idx_for_skeleton().cmp(&other.idx_for_skeleton())
184    }
185
186    pub(crate) fn from_subsecond_digits(subsecond_digits: SubsecondDigits) -> Self {
187        use SubsecondDigits::*;
188        match subsecond_digits {
189            S1 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond1),
190            S2 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond2),
191            S3 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond3),
192            S4 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond4),
193            S5 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond5),
194            S6 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond6),
195            S7 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond7),
196            S8 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond8),
197            S9 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond9),
198        }
199    }
200
201    /// UTS 35 defines several 1 and 2 symbols to be the same as 3 symbols (abbreviated).
202    /// For example, 'a' represents an abbreviated day period, the same as 'aaa'.
203    ///
204    /// This function maps field lengths 1 and 2 to field length 3.
205    #[cfg(feature = "datagen")]
206    pub(crate) fn is_at_least_abbreviated(self) -> bool {
207        matches!(
208            self,
209            FieldSymbol::Era
210                | FieldSymbol::Year(Year::Cyclic)
211                | FieldSymbol::Weekday(Weekday::Format)
212                | FieldSymbol::DayPeriod(_)
213                | FieldSymbol::TimeZone(TimeZone::SpecificNonLocation)
214        )
215    }
216}
217
218/// [`ULE`](zerovec::ule::ULE) type for [`FieldSymbol`]
219#[repr(transparent)]
220#[derive(Debug, Copy, Clone, PartialEq, Eq)]
221pub struct FieldSymbolULE(u8);
222
223impl AsULE for FieldSymbol {
224    type ULE = FieldSymbolULE;
225    fn to_unaligned(self) -> Self::ULE {
226        FieldSymbolULE(self.idx())
227    }
228    fn from_unaligned(unaligned: Self::ULE) -> Self {
229        #[expect(clippy::unwrap_used)] // OK because the ULE is pre-validated
230        Self::from_idx(unaligned.0).unwrap()
231    }
232}
233
234impl FieldSymbolULE {
235    #[inline]
236    pub(crate) fn validate_byte(byte: u8) -> Result<(), UleError> {
237        FieldSymbol::from_idx(byte)
238            .map(|_| ())
239            .map_err(|_| UleError::parse::<FieldSymbol>())
240    }
241}
242
243// Safety checklist for ULE:
244//
245// 1. Must not include any uninitialized or padding bytes (true since transparent over a ULE).
246// 2. Must have an alignment of 1 byte (true since transparent over a ULE).
247// 3. ULE::validate_bytes() checks that the given byte slice represents a valid slice.
248// 4. ULE::validate_bytes() checks that the given byte slice has a valid length
249//    (true since transparent over a type of size 1).
250// 5. All other methods must be left with their default impl.
251// 6. Byte equality is semantic equality.
252unsafe impl ULE for FieldSymbolULE {
253    fn validate_bytes(bytes: &[u8]) -> Result<(), UleError> {
254        for byte in bytes {
255            Self::validate_byte(*byte)?;
256        }
257        Ok(())
258    }
259}
260
261#[derive(Debug, Eq, PartialEq, Clone, Copy)]
262#[allow(clippy::exhaustive_enums)] // used in data struct
263/// Categorization of field symbols as text or numeric.
264#[cfg(feature = "datagen")]
265pub enum TextOrNumeric {
266    /// The field is represented by text.
267    Text,
268    /// The field is represented by numeric digits.
269    Numeric,
270}
271
272/// [`FieldSymbols`](FieldSymbol) can be either text or numeric. This categorization is important
273/// when matching skeletons with a components [`Bag`](crate::options::components::Bag).
274#[cfg(feature = "datagen")]
275pub(crate) trait LengthType {
276    fn get_length_type(self, length: FieldLength) -> TextOrNumeric;
277}
278
279impl FieldSymbol {
280    /// Skeletons are a Vec<Field>, and represent the Fields that can be used to match to a
281    /// specific pattern. The order of the Vec does not affect the Pattern that is output.
282    /// However, it's more performant when matching these fields, and it's more deterministic
283    /// when serializing them to present them in a consistent order.
284    ///
285    /// This ordering is taken by the order of the fields listed in the [UTS 35 Date Field Symbol Table]
286    /// (<https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table>), and are generally
287    /// ordered most significant to least significant.
288    fn get_canonical_order(self) -> u8 {
289        match self {
290            Self::Era => 0,
291            Self::Year(Year::Calendar) => 1,
292            // Self::Year(Year::WeekOf) => 2,
293            Self::Year(Year::Extended) => 2,
294            Self::Year(Year::Cyclic) => 3,
295            Self::Year(Year::RelatedIso) => 4,
296            Self::Month(Month::Format) => 5,
297            Self::Month(Month::StandAlone) => 6,
298            // TODO(#5643): Add week fields back
299            // Self::Week(Week::WeekOfYear) => 7,
300            // Self::Week(Week::WeekOfMonth) => 8,
301            Self::Week(_) => unreachable!(), // ZST references aren't uninhabited
302            Self::Day(Day::DayOfMonth) => 9,
303            Self::Day(Day::DayOfYear) => 10,
304            Self::Day(Day::DayOfWeekInMonth) => 11,
305            Self::Day(Day::ModifiedJulianDay) => 12,
306            Self::Weekday(Weekday::Format) => 13,
307            Self::Weekday(Weekday::Local) => 14,
308            Self::Weekday(Weekday::StandAlone) => 15,
309            Self::DayPeriod(DayPeriod::AmPm) => 16,
310            Self::DayPeriod(DayPeriod::NoonMidnight) => 17,
311            Self::DayPeriod(DayPeriod::Flexible) => 18,
312            Self::Hour(Hour::H11) => 20,
313            Self::Hour(Hour::H12) => 21,
314            Self::Hour(Hour::H23) => 22,
315            Self::Minute => 25,
316            Self::Second(Second::Second) => 26,
317            Self::Second(Second::MillisInDay) => 27,
318            Self::DecimalSecond(DecimalSecond::Subsecond1) => 31,
319            Self::DecimalSecond(DecimalSecond::Subsecond2) => 32,
320            Self::DecimalSecond(DecimalSecond::Subsecond3) => 33,
321            Self::DecimalSecond(DecimalSecond::Subsecond4) => 34,
322            Self::DecimalSecond(DecimalSecond::Subsecond5) => 35,
323            Self::DecimalSecond(DecimalSecond::Subsecond6) => 36,
324            Self::DecimalSecond(DecimalSecond::Subsecond7) => 37,
325            Self::DecimalSecond(DecimalSecond::Subsecond8) => 38,
326            Self::DecimalSecond(DecimalSecond::Subsecond9) => 39,
327            Self::TimeZone(TimeZone::SpecificNonLocation) => 100,
328            Self::TimeZone(TimeZone::LocalizedOffset) => 102,
329            Self::TimeZone(TimeZone::GenericNonLocation) => 103,
330            Self::TimeZone(TimeZone::Location) => 104,
331            Self::TimeZone(TimeZone::Iso) => 105,
332            Self::TimeZone(TimeZone::IsoWithZ) => 106,
333        }
334    }
335}
336
337impl TryFrom<char> for FieldSymbol {
338    type Error = SymbolError;
339    fn try_from(ch: char) -> Result<Self, Self::Error> {
340        if !ch.is_ascii_alphanumeric() {
341            return Err(SymbolError::Invalid(ch as u8));
342        }
343
344        (if ch == 'G' {
345            Ok(Self::Era)
346        } else {
347            Err(SymbolError::Unknown(ch))
348        })
349        .or_else(|_| Year::try_from(ch).map(Self::Year))
350        .or_else(|_| Month::try_from(ch).map(Self::Month))
351        .or_else(|_| Week::try_from(ch).map(Self::Week))
352        .or_else(|_| Day::try_from(ch).map(Self::Day))
353        .or_else(|_| Weekday::try_from(ch).map(Self::Weekday))
354        .or_else(|_| DayPeriod::try_from(ch).map(Self::DayPeriod))
355        .or_else(|_| Hour::try_from(ch).map(Self::Hour))
356        .or({
357            if ch == 'm' {
358                Ok(Self::Minute)
359            } else {
360                Err(SymbolError::Unknown(ch))
361            }
362        })
363        .or_else(|_| Second::try_from(ch).map(Self::Second))
364        .or_else(|_| TimeZone::try_from(ch).map(Self::TimeZone))
365        // Note: char-to-enum conversion for DecimalSecond is handled directly in the parser
366    }
367}
368
369impl From<FieldSymbol> for char {
370    fn from(symbol: FieldSymbol) -> Self {
371        match symbol {
372            FieldSymbol::Era => 'G',
373            FieldSymbol::Year(year) => year.into(),
374            FieldSymbol::Month(month) => month.into(),
375            FieldSymbol::Week(week) => week.into(),
376            FieldSymbol::Day(day) => day.into(),
377            FieldSymbol::Weekday(weekday) => weekday.into(),
378            FieldSymbol::DayPeriod(dayperiod) => dayperiod.into(),
379            FieldSymbol::Hour(hour) => hour.into(),
380            FieldSymbol::Minute => 'm',
381            FieldSymbol::Second(second) => second.into(),
382            FieldSymbol::TimeZone(time_zone) => time_zone.into(),
383            // Note: This is only used for representing the integer portion
384            FieldSymbol::DecimalSecond(_) => 's',
385        }
386    }
387}
388
389impl PartialOrd for FieldSymbol {
390    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
391        Some(self.cmp(other))
392    }
393}
394
395impl Ord for FieldSymbol {
396    fn cmp(&self, other: &Self) -> Ordering {
397        self.get_canonical_order().cmp(&other.get_canonical_order())
398    }
399}
400
401macro_rules! field_type {
402    ($(#[$enum_attr:meta])* $i:ident; { $( $(#[$variant_attr:meta])* $key:literal => $val:ident = $idx:expr,)* }; $length_type:ident; $($ule_name:ident)?) => (
403        field_type!($(#[$enum_attr])* $i; {$( $(#[$variant_attr])* $key => $val = $idx,)*}; $($ule_name)?);
404
405        #[cfg(feature = "datagen")]
406        impl LengthType for $i {
407            fn get_length_type(self, _length: FieldLength) -> TextOrNumeric {
408                TextOrNumeric::$length_type
409            }
410        }
411    );
412    ($(#[$enum_attr:meta])* $i:ident; { $( $(#[$variant_attr:meta])* $key:literal => $val:ident = $idx:expr,)* }; $($ule_name:ident)?) => (
413        #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, yoke::Yokeable, zerofrom::ZeroFrom)]
414        // TODO(#1044): This should be replaced with a custom derive.
415        #[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
416        #[cfg_attr(feature = "datagen", databake(path = icu_datetime::fields))]
417        #[cfg_attr(feature = "serde", derive(serde::Deserialize))]
418        #[allow(clippy::enum_variant_names)]
419        $(
420            #[repr(u8)]
421            #[zerovec::make_ule($ule_name)]
422            #[zerovec::derive(Debug)]
423        )?
424        #[allow(clippy::exhaustive_enums)] // used in data struct
425        $(#[$enum_attr])*
426        pub enum $i {
427            $(
428                $(#[$variant_attr])*
429                #[doc = core::concat!("\n\nThis field symbol is represented by the character `", $key, "` in a date formatting pattern string.")]
430                #[doc = "\n\nFor more details, see documentation on [date field symbols](https://unicode.org/reports/tr35/tr35-dates.html#table-date-field-symbol-table)."]
431                $val = $idx,
432            )*
433        }
434
435        $(
436            #[allow(path_statements)] // #5643 impl conditional on $ule_name
437            const _: () = { $ule_name; };
438
439        impl $i {
440            /// Retrieves an index of the field variant.
441            ///
442            /// # Examples
443            ///
444            /// ```ignore
445            /// use icu::datetime::fields::Month;
446            ///
447            /// assert_eq!(Month::StandAlone::idx(), 1);
448            /// ```
449            ///
450            /// # Stability
451            ///
452            /// This is mostly useful for serialization,
453            /// and does not guarantee index stability between ICU4X
454            /// versions.
455            #[inline]
456            pub(crate) fn idx(self) -> u8 {
457                self as u8
458            }
459
460            /// Retrieves a field variant from an index.
461            ///
462            /// # Examples
463            ///
464            /// ```ignore
465            /// use icu::datetime::fields::Month;
466            ///
467            /// assert_eq!(Month::from_idx(0), Month::Format);
468            /// ```
469            ///
470            /// # Stability
471            ///
472            /// This is mostly useful for serialization,
473            /// and does not guarantee index stability between ICU4X
474            /// versions.
475            #[inline]
476            pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
477                Self::new_from_u8(idx)
478                    .ok_or(SymbolError::InvalidIndex(idx))
479            }
480        }
481        )?
482
483        impl TryFrom<char> for $i {
484            type Error = SymbolError;
485
486            fn try_from(ch: char) -> Result<Self, Self::Error> {
487                match ch {
488                    $(
489                        $key => Ok(Self::$val),
490                    )*
491                    _ => Err(SymbolError::Unknown(ch)),
492                }
493            }
494        }
495
496        impl From<$i> for FieldSymbol {
497            fn from(input: $i) -> Self {
498                Self::$i(input)
499            }
500        }
501
502        impl From<$i> for char {
503            fn from(input: $i) -> char {
504                match input {
505                    $(
506                        $i::$val => $key,
507                    )*
508                }
509            }
510        }
511    );
512}
513
514field_type! (
515    /// An enum for the possible symbols of a year field in a date pattern.
516    Year; {
517        /// Field symbol for calendar year (numeric).
518        ///
519        /// In most cases the length of this field specifies the minimum number of digits to display, zero-padded as necessary. For most use cases, [`Year::Calendar`] or `Year::WeekOf` should be adequate.
520        'y' => Calendar = 0,
521        /// Field symbol for cyclic year; used in calendars where years are tracked in cycles, such as the Chinese or Dangi calendars.
522        'U' => Cyclic = 1,
523        /// Field symbol for related ISO; some calendars which use different year numbering than ISO, or no year numbering, may express years in an ISO year corresponding to a calendar year.
524        'r' => RelatedIso = 2,
525        /// Field symbol for extended year
526        'u' => Extended = 3,
527        // /// Field symbol for year in "week of year".
528        // ///
529        // /// This works for “week of year” based calendars in which the year transition occurs on a week boundary; may differ from calendar year [`Year::Calendar`] near a year transition. This numeric year designation is used in conjunction with [`Week::WeekOfYear`], but can be used in non-Gregorian based calendar systems where week date processing is desired. The field length is interpreted in the same way as for [`Year::Calendar`].
530        // 'Y' => WeekOf = 4,
531    };
532    YearULE
533);
534
535#[cfg(feature = "datagen")]
536impl LengthType for Year {
537    fn get_length_type(self, _length: FieldLength) -> TextOrNumeric {
538        // https://unicode.org/reports/tr35/tr35-dates.html#dfst-year
539        match self {
540            Year::Cyclic => TextOrNumeric::Text,
541            _ => TextOrNumeric::Numeric,
542        }
543    }
544}
545
546field_type!(
547    /// An enum for the possible symbols of a month field in a date pattern.
548    Month; {
549        /// Field symbol for month number or name in a pattern that contains multiple fields.
550        'M' => Format = 0,
551        /// Field symbol for a "stand-alone" month number or name.
552        /// 
553        /// The stand-alone month name is used when the month is displayed by itself. This may differ from the standard form based on the language and context.
554        'L' => StandAlone = 1,
555}; MonthULE);
556
557#[cfg(feature = "datagen")]
558impl LengthType for Month {
559    fn get_length_type(self, length: FieldLength) -> TextOrNumeric {
560        match length {
561            FieldLength::One => TextOrNumeric::Numeric,
562            FieldLength::NumericOverride(_) => TextOrNumeric::Numeric,
563            FieldLength::Two => TextOrNumeric::Numeric,
564            FieldLength::Three => TextOrNumeric::Text,
565            FieldLength::Four => TextOrNumeric::Text,
566            FieldLength::Five => TextOrNumeric::Text,
567            FieldLength::Six => TextOrNumeric::Text,
568        }
569    }
570}
571
572field_type!(
573    /// An enum for the possible symbols of a day field in a date pattern.
574    Day; {
575        /// Field symbol for day of month (numeric).
576        'd' => DayOfMonth = 0,
577        /// Field symbol for day of year (numeric).
578        'D' => DayOfYear = 1,
579        /// Field symbol for the day of week occurrence relative to the month (numeric).
580        ///
581        /// For the example `"2nd Wed in July"`, this field would provide `"2"`.  Should likely be paired with the [`Weekday`] field.
582        'F' => DayOfWeekInMonth = 2,
583        /// Field symbol for the modified Julian day (numeric).
584        ///
585        /// The value of this field differs from the conventional Julian day number in a couple of ways, which are based on measuring relative to the local time zone.
586        'g' => ModifiedJulianDay = 3,
587    };
588    Numeric;
589    DayULE
590);
591
592field_type!(
593    /// An enum for the possible symbols of an hour field in a date pattern.
594    Hour; {
595        /// Field symbol for numeric hour [0-11].
596        'K' => H11 = 0,
597        /// Field symbol for numeric hour [1-12].
598        'h' => H12 = 1,
599        /// Field symbol for numeric hour [0-23].
600        'H' => H23 = 2,
601    };
602    Numeric;
603    HourULE
604);
605
606impl Hour {
607    /// Picks the best [`Hour`] given the preferences.
608    ///
609    /// If `-u-hc-h**` is specified, that field is used. Otherwise, the field is selected
610    /// based on the locale, using `-u-hc-c**`, if present, as a hint.
611    pub(crate) fn from_prefs(prefs: DateTimeFormatterPreferences) -> Option<Self> {
612        let field = match prefs.hour_cycle? {
613            HourCycle::H11 => Self::H11,
614            HourCycle::H12 => Self::H12,
615            HourCycle::H23 => Self::H23,
616            HourCycle::Clock12 => {
617                let region = prefs
618                    .locale_preferences
619                    .to_data_locale_region_priority()
620                    .region;
621                // TODO(#7415): Test that this fallback stays consistent with CLDR.
622                // This is derived from timeData. Currently the only region using
623                // something other than h12/h23 is JP.
624                const JP: Region = region!("JP");
625                match region {
626                    Some(JP) => Self::H11,
627                    _ => Self::H12,
628                }
629            }
630            // Note: ICU4X does not support H24.
631            HourCycle::Clock24 => Self::H23,
632            _ => unreachable!(),
633        };
634        Some(field)
635    }
636}
637
638#[test]
639fn test_hour_cycle_selection() {
640    struct TestCase {
641        locale: &'static str,
642        expected: Option<Hour>,
643    }
644    let cases = [
645        TestCase {
646            locale: "en-US",
647            expected: None,
648        },
649        TestCase {
650            locale: "en-US-u-hc-h11",
651            expected: Some(Hour::H11),
652        },
653        TestCase {
654            locale: "en-US-u-hc-h12",
655            expected: Some(Hour::H12),
656        },
657        TestCase {
658            locale: "en-US-u-hc-h23",
659            expected: Some(Hour::H23),
660        },
661        TestCase {
662            locale: "en-US-u-hc-c12",
663            expected: Some(Hour::H12),
664        },
665        TestCase {
666            locale: "en-US-u-hc-c24",
667            expected: Some(Hour::H23),
668        },
669        TestCase {
670            locale: "fr-FR",
671            expected: None,
672        },
673        TestCase {
674            locale: "fr-FR-u-hc-h11",
675            expected: Some(Hour::H11),
676        },
677        TestCase {
678            locale: "fr-FR-u-hc-h12",
679            expected: Some(Hour::H12),
680        },
681        TestCase {
682            locale: "fr-FR-u-hc-h23",
683            expected: Some(Hour::H23),
684        },
685        TestCase {
686            locale: "fr-FR-u-hc-c12",
687            expected: Some(Hour::H12),
688        },
689        TestCase {
690            locale: "fr-FR-u-hc-c24",
691            expected: Some(Hour::H23),
692        },
693        TestCase {
694            locale: "ja-JP",
695            expected: None,
696        },
697        TestCase {
698            locale: "ja-JP-u-hc-h11",
699            expected: Some(Hour::H11),
700        },
701        TestCase {
702            locale: "ja-JP-u-hc-h12",
703            expected: Some(Hour::H12),
704        },
705        TestCase {
706            locale: "ja-JP-u-hc-h23",
707            expected: Some(Hour::H23),
708        },
709        TestCase {
710            locale: "ja-JP-u-hc-c12",
711            expected: Some(Hour::H11),
712        },
713        TestCase {
714            locale: "ja-JP-u-hc-c24",
715            expected: Some(Hour::H23),
716        },
717    ];
718    for TestCase { locale, expected } in cases {
719        let locale = icu_locale_core::Locale::try_from_str(locale).unwrap();
720        let prefs = DateTimeFormatterPreferences::from(&locale);
721        let actual = Hour::from_prefs(prefs);
722        assert_eq!(expected, actual, "{}", locale);
723    }
724}
725
726// NOTE: 'S' Subsecond is represented via DecimalSecond,
727// so it is not included in the Second enum.
728
729field_type!(
730    /// An enum for the possible symbols of a second field in a date pattern.
731    Second; {
732        /// Field symbol for second (numeric).
733        's' => Second = 0,
734        /// Field symbol for milliseconds in day (numeric).
735        ///
736        /// This field behaves exactly like a composite of all time-related fields, not including the zone fields.
737        'A' => MillisInDay = 1,
738    };
739    Numeric;
740    SecondULE
741);
742
743field_type!(
744    /// An enum for the possible symbols of a week field in a date pattern.
745    Week; {
746        // /// Field symbol for week of year (numeric).
747        // ///
748        // /// When used in a pattern with year, use [`Year::WeekOf`] for the year field instead of [`Year::Calendar`].
749        // 'w' => WeekOfYear = 0,
750        // /// Field symbol for week of month (numeric).
751        // 'W' => WeekOfMonth = 1,
752    };
753    Numeric;
754    // TODO(#5643): Recover ULE once the type is inhabited
755    // WeekULE
756);
757
758impl Week {
759    /// Retrieves an index of the field variant.
760    ///
761    /// # Examples
762    ///
763    /// ```ignore
764    /// use icu::datetime::fields::Month;
765    ///
766    /// assert_eq!(Month::StandAlone::idx(), 1);
767    /// ```
768    ///
769    /// # Stability
770    ///
771    /// This is mostly useful for serialization,
772    /// and does not guarantee index stability between ICU4X
773    /// versions.
774    #[inline]
775    pub(crate) fn idx(self) -> u8 {
776        0
777    }
778
779    /// Retrieves a field variant from an index.
780    ///
781    /// # Examples
782    ///
783    /// ```ignore
784    /// use icu::datetime::fields::Month;
785    ///
786    /// assert_eq!(Month::from_idx(0), Month::Format);
787    /// ```
788    ///
789    /// # Stability
790    ///
791    /// This is mostly useful for serialization,
792    /// and does not guarantee index stability between ICU4X
793    /// versions.
794    #[inline]
795    pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
796        Err(SymbolError::InvalidIndex(idx))
797    }
798}
799
800field_type!(
801    /// An enum for the possible symbols of a weekday field in a date pattern.
802    Weekday;  {
803        /// Field symbol for the weekday (text format only).
804        'E' => Format = 0,
805        /// Field symbol for the weekday; numeric formats produce a locale-dependent ordinal weekday number.
806        ///
807        /// For example, in de-DE, Monday is the 1st day of the week.
808        'e' => Local = 1,
809        /// Field symbol for stand-alone local weekday number/name.
810        ///
811        /// The stand-alone weekday name is used when the weekday is displayed by itself. This may differ from the standard form based on the language and context.
812        'c' => StandAlone = 2,
813    };
814    WeekdayULE
815);
816
817#[cfg(feature = "datagen")]
818impl LengthType for Weekday {
819    fn get_length_type(self, length: FieldLength) -> TextOrNumeric {
820        match self {
821            Self::Format => TextOrNumeric::Text,
822            Self::Local | Self::StandAlone => match length {
823                FieldLength::One | FieldLength::Two => TextOrNumeric::Numeric,
824                _ => TextOrNumeric::Text,
825            },
826        }
827    }
828}
829
830impl Weekday {
831    /// UTS 35 says that "e" (local weekday) and "E" (format weekday) have the same non-numeric names.
832    ///
833    /// This function normalizes "e" to "E".
834    pub(crate) fn to_format_symbol(self) -> Self {
835        match self {
836            Weekday::Local => Weekday::Format,
837            other => other,
838        }
839    }
840}
841
842field_type!(
843    /// An enum for the possible symbols of a day period field in a date pattern.
844    DayPeriod; {
845        /// Field symbol for the AM, PM day period.  (Does not include noon, midnight.)
846        'a' => AmPm = 0,
847        /// Field symbol for the am, pm, noon, midnight day period.
848        'b' => NoonMidnight = 1,
849        /// Field symbol for the flexible day period.
850        'B' => Flexible = 2,
851    };
852    Text;
853    DayPeriodULE
854);
855
856field_type!(
857    /// An enum for the possible symbols of a time zone field in a date pattern.
858    TimeZone; {
859        /// Field symbol for the specific non-location format of a time zone.
860        ///
861        /// For example: "Pacific Standard Time"
862        'z' => SpecificNonLocation = 0,
863        /// Field symbol for the localized offset format of a time zone.
864        ///
865        /// For example: "GMT-07:00"
866        'O' => LocalizedOffset = 1,
867        /// Field symbol for the generic non-location format of a time zone.
868        ///
869        /// For example: "Pacific Time"
870        'v' => GenericNonLocation = 2,
871        /// Field symbol for any of: the time zone id, time zone exemplar city, or generic location format.
872        'V' => Location = 3,
873        /// Field symbol for either the ISO-8601 basic format or ISO-8601 extended format. This does not use an
874        /// optional ISO-8601 UTC indicator `Z`, whereas [`TimeZone::IsoWithZ`] produces `Z`.
875        'x' => Iso = 4,
876        /// Field symbol for either the ISO-8601 basic format or ISO-8601 extended format, with the ISO-8601 UTC indicator `Z`.
877        'X' => IsoWithZ = 5,
878    };
879    TimeZoneULE
880);
881
882#[cfg(feature = "datagen")]
883impl LengthType for TimeZone {
884    fn get_length_type(self, _: FieldLength) -> TextOrNumeric {
885        use TextOrNumeric::*;
886        match self {
887            Self::Iso | Self::IsoWithZ => Numeric,
888            Self::LocalizedOffset
889            | Self::SpecificNonLocation
890            | Self::GenericNonLocation
891            | Self::Location => Text,
892        }
893    }
894}
895
896/// A second field with fractional digits.
897#[derive(
898    Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, yoke::Yokeable, zerofrom::ZeroFrom,
899)]
900#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
901#[cfg_attr(feature = "datagen", databake(path = icu_datetime::fields))]
902#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
903#[repr(u8)]
904#[zerovec::make_ule(DecimalSecondULE)]
905#[zerovec::derive(Debug)]
906#[allow(clippy::exhaustive_enums)] // used in data struct
907pub enum DecimalSecond {
908    /// A second with 1 fractional digit: "1.0"
909    Subsecond1 = 1,
910    /// A second with 2 fractional digits: "1.00"
911    Subsecond2 = 2,
912    /// A second with 3 fractional digits: "1.000"
913    Subsecond3 = 3,
914    /// A second with 4 fractional digits: "1.0000"
915    Subsecond4 = 4,
916    /// A second with 5 fractional digits: "1.00000"
917    Subsecond5 = 5,
918    /// A second with 6 fractional digits: "1.000000"
919    Subsecond6 = 6,
920    /// A second with 7 fractional digits: "1.0000000"
921    Subsecond7 = 7,
922    /// A second with 8 fractional digits: "1.00000000"
923    Subsecond8 = 8,
924    /// A second with 9 fractional digits: "1.000000000"
925    Subsecond9 = 9,
926}
927
928impl DecimalSecond {
929    #[inline]
930    pub(crate) fn idx(self) -> u8 {
931        self as u8
932    }
933    #[inline]
934    pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
935        Self::new_from_u8(idx).ok_or(SymbolError::InvalidIndex(idx))
936    }
937}
938impl From<DecimalSecond> for FieldSymbol {
939    fn from(input: DecimalSecond) -> Self {
940        Self::DecimalSecond(input)
941    }
942}