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