icu_datetime/provider/fields/
components.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//! 🚧 \[Experimental\] Types for specifying fields in a classical datetime skeleton.
6//!
7//! <div class="stab unstable">
8//! 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
9//! including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
10//! of the icu meta-crate. Use with caution.
11//! <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
12//! </div>
13//!
14//! # Examples
15//!
16//! ```
17//! use icu::datetime::provider::fields::components;
18//!
19//! let mut bag = components::Bag::default();
20//! bag.year = Some(components::Year::Numeric);
21//! bag.month = Some(components::Month::Long);
22//! bag.day = Some(components::Day::NumericDayOfMonth);
23//!
24//! bag.hour = Some(components::Numeric::TwoDigit);
25//! bag.minute = Some(components::Numeric::TwoDigit);
26//! ```
27//!
28//! *Note*: The exact formatted result is a subject to change over
29//! time. Formatted result should be treated as opaque and displayed to the user as-is,
30//! and it is strongly recommended to never write tests that expect a particular formatted output.
31
32use crate::{
33    options::SubsecondDigits,
34    provider::fields::{self, Field, FieldLength, FieldSymbol},
35    provider::pattern::{reference, runtime::Pattern, PatternItem},
36};
37
38use crate::pattern::DateTimePattern;
39use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle;
40#[cfg(feature = "serde")]
41use serde::{Deserialize, Serialize};
42
43/// See the [module-level](./index.html) docs for more information.
44///
45/// <div class="stab unstable">
46/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
47/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
48/// of the icu meta-crate. Use with caution.
49/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
50/// </div>
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
52#[non_exhaustive]
53pub struct Bag {
54    /// Include the era, such as "AD" or "CE".
55    pub era: Option<Text>,
56    /// Include the year, such as "1970" or "70".
57    pub year: Option<Year>,
58    /// Include the month, such as "April" or "Apr".
59    pub month: Option<Month>,
60    /// Include the week number, such as "51st" or "51" for week 51.
61    pub week: Option<Week>,
62    /// Include the day of the month/year, such as "07" or "7".
63    pub day: Option<Day>,
64    /// Include the weekday, such as "Wednesday" or "Wed".
65    pub weekday: Option<Text>,
66
67    /// Include the hour such as "2" or "14".
68    pub hour: Option<Numeric>,
69    /// Include the minute such as "3" or "03".
70    pub minute: Option<Numeric>,
71    /// Include the second such as "3" or "03".
72    pub second: Option<Numeric>,
73    /// Specify the number of fractional second digits such as 1 (".3") or 3 (".003").
74    pub subsecond: Option<SubsecondDigits>,
75
76    /// Include the time zone, such as "GMT+05:00".
77    pub time_zone_name: Option<TimeZoneName>,
78
79    /// An override of the hour cycle.
80    pub hour_cycle: Option<HourCycle>,
81}
82
83impl Bag {
84    /// Creates an empty components bag
85    ///
86    /// Has the same behavior as the [`Default`] implementation on this type.
87    pub fn empty() -> Self {
88        Self::default()
89    }
90
91    /// Merges the fields of other into self if non-None. If both fields are set, `other` is kept.
92    pub fn merge(self, other: Self) -> Self {
93        Self {
94            era: other.era.or(self.era),
95            year: other.year.or(self.year),
96            month: other.month.or(self.month),
97            week: other.week.or(self.week),
98            day: other.day.or(self.day),
99            weekday: other.weekday.or(self.weekday),
100            hour: other.hour.or(self.hour),
101            minute: other.minute.or(self.minute),
102            second: other.second.or(self.second),
103            subsecond: other.subsecond.or(self.subsecond),
104            time_zone_name: other.time_zone_name.or(self.time_zone_name),
105            hour_cycle: other.hour_cycle.or(self.hour_cycle),
106        }
107    }
108
109    /// Converts the components::Bag into a `Vec<Field>`. The fields will be ordered in from most
110    /// significant field to least significant. This is the order the fields are listed in
111    /// the UTS 35 table - <https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table>
112    ///
113    /// Arguments:
114    ///
115    /// - `default_hour_cycle` specifies the hour cycle to use for the hour field if not in the Bag.
116    ///   `preferences::Bag::hour_cycle` takes precedence over this argument.
117    pub fn to_vec_fields(&self, default_hour_cycle: HourCycle) -> alloc::vec::Vec<Field> {
118        let mut fields = alloc::vec::Vec::new();
119        if let Some(era) = self.era {
120            fields.push(Field {
121                symbol: FieldSymbol::Era,
122                length: match era {
123                    // Era name, format length.
124                    //
125                    // G..GGG  AD           Abbreviated
126                    // GGGG    Anno Domini  Wide
127                    // GGGGG   A            Narrow
128                    Text::Short => FieldLength::Three,
129                    Text::Long => FieldLength::Four,
130                    Text::Narrow => FieldLength::Five,
131                },
132            })
133        }
134
135        if let Some(year) = self.year {
136            // Unimplemented year fields:
137            // u - Extended year
138            // U - Cyclic year name
139            // r - Related Gregorian year
140            fields.push(Field {
141                symbol: FieldSymbol::Year(match year {
142                    Year::Numeric | Year::TwoDigit => fields::Year::Calendar,
143                    Year::NumericWeekOf | Year::TwoDigitWeekOf => {
144                        unimplemented!("#5643 fields::Year::WeekOf")
145                    }
146                }),
147                length: match year {
148                    // Calendar year (numeric).
149                    // y       2, 20, 201, 2017, 20173
150                    // yy      02, 20, 01, 17, 73
151                    // yyy     002, 020, 201, 2017, 20173    (not implemented)
152                    // yyyy    0002, 0020, 0201, 2017, 20173 (not implemented)
153                    // yyyyy+  ...                           (not implemented)
154                    Year::Numeric | Year::NumericWeekOf => FieldLength::One,
155                    Year::TwoDigit | Year::TwoDigitWeekOf => FieldLength::Two,
156                },
157            });
158        }
159
160        // TODO(#501) - Unimplemented quarter fields:
161        // Q - Quarter number/name
162        // q - Stand-alone quarter
163
164        if let Some(month) = self.month {
165            fields.push(Field {
166                // Always choose Month::Format as Month::StandAlone is not used in skeletons.
167                symbol: FieldSymbol::Month(fields::Month::Format),
168                length: match month {
169                    // (intended to be used in conjunction with ‘d’ for day number).
170                    // M      9, 12      Numeric: minimum digits
171                    // MM     09, 12     Numeric: 2 digits, zero pad if needed
172                    // MMM    Sep        Abbreviated
173                    // MMMM   September  Wide
174                    // MMMMM  S          Narrow
175                    Month::Numeric => FieldLength::One,
176                    Month::TwoDigit => FieldLength::Two,
177                    Month::Long => FieldLength::Four,
178                    Month::Short => FieldLength::Three,
179                    Month::Narrow => FieldLength::Five,
180                },
181            });
182        }
183
184        if let Some(week) = self.week {
185            #[allow(
186                unreachable_code,
187                reason = "Uninhabited MultipleVariants (due to pivot_field), see #7118"
188            )]
189            fields.push(Field {
190                symbol: FieldSymbol::Week(match week {
191                    Week::WeekOfMonth => unimplemented!("#5643 fields::Week::WeekOfMonth"),
192                    Week::NumericWeekOfYear | Week::TwoDigitWeekOfYear => {
193                        unimplemented!("#5643 fields::Week::WeekOfYear")
194                    }
195                }),
196                length: match week {
197                    Week::WeekOfMonth | Week::NumericWeekOfYear => FieldLength::One,
198                    Week::TwoDigitWeekOfYear => FieldLength::Two,
199                },
200            });
201        }
202
203        if let Some(day) = self.day {
204            // TODO(#591) Unimplemented day fields:
205            // g - Modified Julian day.
206            fields.push(Field {
207                symbol: FieldSymbol::Day(match day {
208                    Day::NumericDayOfMonth | Day::TwoDigitDayOfMonth => fields::Day::DayOfMonth,
209                    Day::DayOfWeekInMonth => fields::Day::DayOfWeekInMonth,
210                    Day::DayOfYear => fields::Day::DayOfYear,
211                    Day::ModifiedJulianDay => fields::Day::ModifiedJulianDay,
212                }),
213                length: match day {
214                    // d    1 	  Numeric day of month: minimum digits
215                    // dd   01 	  Numeric day of month: 2 digits, zero pad if needed
216                    // F    1  	  Numeric day of week in month: minimum digits
217                    // D    1     Numeric day of year: minimum digits
218                    Day::NumericDayOfMonth
219                    | Day::DayOfWeekInMonth
220                    | Day::DayOfYear
221                    | Day::ModifiedJulianDay => FieldLength::One,
222                    Day::TwoDigitDayOfMonth => FieldLength::Two,
223                },
224            });
225        }
226
227        if let Some(weekday) = self.weekday {
228            // TODO(#593) Unimplemented fields
229            // e - Local day of week.
230            // c - Stand-alone local day of week.
231            fields.push(Field {
232                symbol: FieldSymbol::Weekday(fields::Weekday::Format),
233                length: match weekday {
234                    // Day of week name, format length.
235                    //
236                    // E..EEE   Tue      Abbreviated
237                    // EEEE     Tuesday  Wide
238                    // EEEEE    T 	     Narrow
239                    // EEEEEE   Tu       Short
240                    Text::Long => FieldLength::Four,
241                    Text::Short => FieldLength::One,
242                    Text::Narrow => FieldLength::Five,
243                },
244            });
245        }
246
247        // The period fields are not included in skeletons:
248        // a - AM, PM
249        // b - am, pm, noon, midnight
250        // c - flexible day periods
251
252        if let Some(hour) = self.hour {
253            // fields::Hour::H11
254            // fields::Hour::H12
255            // fields::Hour::H23
256
257            let hour_cycle = self.hour_cycle.unwrap_or(default_hour_cycle);
258
259            // When used in skeleton data or in a skeleton passed in an API for flexible date
260            // pattern generation, it should match the 12-hour-cycle format preferred by the
261            // locale (h or K); it should not match a 24-hour-cycle format (H or k).
262            fields.push(Field {
263                symbol: FieldSymbol::Hour(match hour_cycle {
264                    // Skeletons only contain the h12, not h11. The pattern that is matched
265                    // is free to use h11 or h12.
266                    HourCycle::H11 | HourCycle::H12 => {
267                        // h - symbol
268                        fields::Hour::H12
269                    }
270                    // Skeletons only contain the h23, not h24. The pattern that is matched
271                    // is free to use h23 or h24.
272                    HourCycle::H23 => {
273                        // H - symbol
274                        fields::Hour::H23
275                    }
276                    _ => unreachable!(),
277                }),
278                length: match hour {
279                    // Example for h: (note that this is the same for k, K, and H)
280                    // h     1, 12  Numeric: minimum digits
281                    // hh   01, 12  Numeric: 2 digits, zero pad if needed
282                    Numeric::Numeric => FieldLength::One,
283                    Numeric::TwoDigit => FieldLength::Two,
284                },
285            });
286        }
287
288        if let Some(minute) = self.minute {
289            // m   8, 59    Numeric: minimum digits
290            // mm  08, 59   Numeric: 2 digits, zero pad if needed
291            fields.push(Field {
292                symbol: FieldSymbol::Minute,
293                length: match minute {
294                    Numeric::Numeric => FieldLength::One,
295                    Numeric::TwoDigit => FieldLength::Two,
296                },
297            });
298        }
299
300        if let Some(second) = self.second {
301            let symbol = match self.subsecond {
302                None => FieldSymbol::Second(fields::Second::Second),
303                Some(subsecond) => FieldSymbol::from_subsecond_digits(subsecond),
304            };
305            // s    8, 12    Numeric: minimum digits
306            // ss  08, 12    Numeric: 2 digits, zero pad if needed
307            fields.push(Field {
308                symbol,
309                length: match second {
310                    Numeric::Numeric => FieldLength::One,
311                    Numeric::TwoDigit => FieldLength::Two,
312                },
313            });
314            // S - Fractional seconds. Represented as DecimalSecond.
315            // A - Milliseconds in day. Not used in skeletons.
316        }
317
318        if self.time_zone_name.is_some() {
319            // Only the lower "v" field is used in skeletons.
320            fields.push(Field {
321                symbol: FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation),
322                length: FieldLength::One,
323            });
324        }
325
326        {
327            #![allow(clippy::indexing_slicing)] // debug
328            debug_assert!(
329                fields.windows(2).all(|f| f[0] < f[1]),
330                "The fields are sorted and unique."
331            );
332        }
333
334        fields
335    }
336}
337
338/// A numeric component for the `components::`[`Bag`]. It is used for the year, day, hour, minute,
339/// and second.
340///
341/// <div class="stab unstable">
342/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
343/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
344/// of the icu meta-crate. Use with caution.
345/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
346/// </div>
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
348#[cfg_attr(
349    feature = "serde",
350    derive(Serialize, Deserialize),
351    serde(rename_all = "kebab-case")
352)]
353#[non_exhaustive]
354pub enum Numeric {
355    /// Display the numeric value. For instance in a year this would be "1970".
356    Numeric,
357    /// Display the two digit value. For instance in a year this would be "70".
358    TwoDigit,
359}
360
361/// A text component for the `components::`[`Bag`]. It is used for the era and weekday.
362///
363/// <div class="stab unstable">
364/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
365/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
366/// of the icu meta-crate. Use with caution.
367/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
368/// </div>
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
370#[cfg_attr(
371    feature = "serde",
372    derive(Serialize, Deserialize),
373    serde(rename_all = "kebab-case")
374)]
375#[non_exhaustive]
376pub enum Text {
377    /// Display the long form of the text, such as "Wednesday" for the weekday.
378    /// In UTS-35, known as "Wide" (4 letters)
379    Long,
380    /// Display the short form of the text, such as "Wed" for the weekday.
381    /// In UTS-35, known as "Abbreviated" (3 letters)
382    Short,
383    /// Display the narrow form of the text, such as "W" for the weekday.
384    /// In UTS-35, known as "Narrow" (5 letters)
385    Narrow,
386}
387
388/// Options for displaying a Year for the `components::`[`Bag`].
389///
390/// <div class="stab unstable">
391/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
392/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
393/// of the icu meta-crate. Use with caution.
394/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
395/// </div>
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
397#[cfg_attr(
398    feature = "serde",
399    derive(Serialize, Deserialize),
400    serde(rename_all = "kebab-case")
401)]
402#[non_exhaustive]
403pub enum Year {
404    /// The numeric value of the year, such as "2018" for 2018-12-31.
405    Numeric,
406    /// The two-digit value of the year, such as "18" for 2018-12-31.
407    TwoDigit,
408    /// The numeric value of the year in "week-of-year", such as "2019" in
409    /// "week 01 of 2019" for the week of 2018-12-31 according to the ISO calendar.
410    NumericWeekOf,
411    /// The numeric value of the year in "week-of-year", such as "19" in
412    /// "week 01 '19" for the week of 2018-12-31 according to the ISO calendar.
413    TwoDigitWeekOf,
414}
415
416/// Options for displaying a Month for the `components::`[`Bag`].
417///
418/// <div class="stab unstable">
419/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
420/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
421/// of the icu meta-crate. Use with caution.
422/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
423/// </div>
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
425#[cfg_attr(
426    feature = "serde",
427    derive(Serialize, Deserialize),
428    serde(rename_all = "kebab-case")
429)]
430#[non_exhaustive]
431pub enum Month {
432    /// The numeric value of the month, such as "4".
433    Numeric,
434    /// The two-digit value of the month, such as "04".
435    TwoDigit,
436    /// The long value of the month, such as "April".
437    Long,
438    /// The short value of the month, such as "Apr".
439    Short,
440    /// The narrow value of the month, such as "A".
441    Narrow,
442}
443
444// Each enum variant is documented with the UTS 35 field information from:
445// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
446
447/// Options for displaying the current week number for the `components::`[`Bag`].
448///
449/// Week numbers are relative to either a month or year, e.g. 'week 3 of January' or 'week 40 of 2000'.
450///
451/// <div class="stab unstable">
452/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
453/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
454/// of the icu meta-crate. Use with caution.
455/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
456/// </div>
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
458#[cfg_attr(
459    feature = "serde",
460    derive(Serialize, Deserialize),
461    serde(rename_all = "kebab-case")
462)]
463#[non_exhaustive]
464pub enum Week {
465    /// The week of the month, such as the "3" in "week 3 of January".
466    WeekOfMonth,
467    /// The numeric value of the week of the year, such as the "8" in "week 8 of 2000".
468    NumericWeekOfYear,
469    /// The two-digit value of the week of the year, such as the "08" in "2000-W08".
470    TwoDigitWeekOfYear,
471}
472
473/// Options for displaying the current day of the month or year.
474///
475/// <div class="stab unstable">
476/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
477/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
478/// of the icu meta-crate. Use with caution.
479/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
480/// </div>
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
482#[cfg_attr(
483    feature = "serde",
484    derive(Serialize, Deserialize),
485    serde(rename_all = "kebab-case")
486)]
487#[non_exhaustive]
488pub enum Day {
489    /// The numeric value of the day of month, such as the "2" in July 2 1984.
490    NumericDayOfMonth,
491    /// The two digit value of the day of month, such as the "02" in 1984-07-02.
492    TwoDigitDayOfMonth,
493    /// The day of week in this month, such as the "2" in 2nd Wednesday of July.
494    DayOfWeekInMonth,
495    /// The day of year (numeric).
496    DayOfYear,
497    /// The modified Julian day (numeric)
498    ModifiedJulianDay,
499}
500
501/// Options for displaying a time zone for the `components::`[`Bag`].
502///
503/// Note that the initial implementation is focusing on only supporting ECMA-402 compatible
504/// options.
505///
506/// <div class="stab unstable">
507/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways,
508/// including in SemVer minor releases. It can be enabled with the `experimental` Cargo feature
509/// of the icu meta-crate. Use with caution.
510/// <a href="https://github.com/unicode-org/icu4x/issues/1317">#1317</a>
511/// </div>
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
513#[cfg_attr(
514    feature = "serde",
515    derive(Serialize, Deserialize),
516    serde(rename_all = "kebab-case")
517)]
518#[non_exhaustive]
519pub enum TimeZoneName {
520    // UTS-35 fields: z..zzz
521    /// Short localized form, without the location. (e.g.: PST, GMT-8)
522    ShortSpecific,
523
524    // UTS-35 fields: zzzz
525    // Per UTS-35: [long form] specific non-location (falling back to long localized offset)
526    /// Long localized form, without the location (e.g., Pacific Standard Time, Nordamerikanische Westküsten-Normalzeit)
527    LongSpecific,
528
529    // UTS-35 fields: OOOO
530    // Per UTS-35: The long localized offset format. This is equivalent to the "OOOO" specifier
531    /// Long localized offset form, e.g. GMT-08:00
532    LongOffset,
533
534    // UTS-35 fields: O
535    // Per UTS-35: Short localized offset format
536    /// Short localized offset form, e.g. GMT-8
537    ShortOffset,
538
539    // UTS-35 fields: v
540    //   * falling back to generic location (See UTS 35 for more specific rules)
541    //   * falling back to short localized offset
542    /// Short generic non-location format (e.g.: PT, Los Angeles, Zeit).
543    ShortGeneric,
544
545    // UTS-35 fields: vvvv
546    //  * falling back to generic location (See UTS 35 for more specific rules)
547    //  * falling back to long localized offset
548    /// Long generic non-location format (e.g.: Pacific Time, Nordamerikanische Westküstenzeit),
549    LongGeneric,
550}
551
552impl From<TimeZoneName> for Field {
553    fn from(time_zone_name: TimeZoneName) -> Self {
554        match time_zone_name {
555            TimeZoneName::ShortSpecific => Field {
556                symbol: FieldSymbol::TimeZone(fields::TimeZone::SpecificNonLocation),
557                length: FieldLength::One,
558            },
559            TimeZoneName::LongSpecific => Field {
560                symbol: FieldSymbol::TimeZone(fields::TimeZone::SpecificNonLocation),
561                length: FieldLength::Four,
562            },
563            TimeZoneName::LongOffset => Field {
564                symbol: FieldSymbol::TimeZone(fields::TimeZone::LocalizedOffset),
565                length: FieldLength::Four,
566            },
567            TimeZoneName::ShortOffset => Field {
568                symbol: FieldSymbol::TimeZone(fields::TimeZone::LocalizedOffset),
569                length: FieldLength::One,
570            },
571            TimeZoneName::ShortGeneric => Field {
572                symbol: FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation),
573                length: FieldLength::One,
574            },
575            TimeZoneName::LongGeneric => Field {
576                symbol: FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation),
577                length: FieldLength::Four,
578            },
579        }
580    }
581}
582
583impl From<&DateTimePattern> for Bag {
584    fn from(value: &DateTimePattern) -> Self {
585        Self::from(value.as_borrowed().0)
586    }
587}
588
589impl From<&Pattern<'_>> for Bag {
590    fn from(pattern: &Pattern) -> Self {
591        Self::from_pattern_items(pattern.items.iter())
592    }
593}
594
595impl From<&reference::Pattern> for Bag {
596    fn from(pattern: &reference::Pattern) -> Self {
597        Self::from_pattern_items(pattern.items.iter().copied())
598    }
599}
600
601impl Bag {
602    fn from_pattern_items(pattern_items: impl Iterator<Item = PatternItem>) -> Self {
603        let mut bag: Bag = Default::default();
604
605        // Transform the fields into components per:
606        // https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
607        for item in pattern_items {
608            let field = match item {
609                PatternItem::Field(ref field) => field,
610                PatternItem::Literal(_) => continue,
611            };
612            match field.symbol {
613                FieldSymbol::Era => {
614                    bag.era = Some(match field.length {
615                        FieldLength::One
616                        | FieldLength::NumericOverride(_)
617                        | FieldLength::Two
618                        | FieldLength::Three => Text::Short,
619                        FieldLength::Four => Text::Long,
620                        FieldLength::Five | FieldLength::Six => Text::Narrow,
621                    });
622                }
623                FieldSymbol::Year(year) => {
624                    bag.year = Some(match year {
625                        fields::Year::Calendar => match field.length {
626                            FieldLength::Two => Year::TwoDigit,
627                            _ => Year::Numeric,
628                        },
629                        // fields::Year::WeekOf => match field.length {
630                        //     FieldLength::TwoDigit => Year::TwoDigitWeekOf,
631                        //     _ => Year::NumericWeekOf,
632                        // },
633                        // TODO(#3762): Add support for U and r
634                        _ => Year::Numeric,
635                    });
636                }
637                FieldSymbol::Month(_) => {
638                    // `Month::StandAlone` is only relevant in the pattern, so only differentiate
639                    // on the field length.
640                    bag.month = Some(match field.length {
641                        FieldLength::One => Month::Numeric,
642                        FieldLength::NumericOverride(_) => Month::Numeric,
643                        FieldLength::Two => Month::TwoDigit,
644                        FieldLength::Three => Month::Short,
645                        FieldLength::Four => Month::Long,
646                        FieldLength::Five | FieldLength::Six => Month::Narrow,
647                    });
648                }
649                FieldSymbol::Week(_week) => {
650                    // TODO(#5643): Add week fields back
651                    // bag.week = Some(match week {
652                    //     fields::Week::WeekOfYear => match field.length {
653                    //         FieldLength::TwoDigit => Week::TwoDigitWeekOfYear,
654                    //         _ => Week::NumericWeekOfYear,
655                    //     },
656                    //     fields::Week::WeekOfMonth => Week::WeekOfMonth,
657                    // });
658                }
659                FieldSymbol::Day(day) => {
660                    bag.day = Some(match day {
661                        fields::Day::DayOfMonth => match field.length {
662                            FieldLength::Two => Day::TwoDigitDayOfMonth,
663                            _ => Day::NumericDayOfMonth,
664                        },
665                        fields::Day::DayOfYear => Day::DayOfYear,
666                        fields::Day::DayOfWeekInMonth => Day::DayOfWeekInMonth,
667                        fields::Day::ModifiedJulianDay => Day::ModifiedJulianDay,
668                    });
669                }
670                FieldSymbol::Weekday(weekday) => {
671                    bag.weekday = Some(match weekday {
672                        fields::Weekday::Format => match field.length {
673                            FieldLength::One | FieldLength::Two | FieldLength::Three => Text::Short,
674                            FieldLength::Four => Text::Long,
675                            _ => Text::Narrow,
676                        },
677                        fields::Weekday::StandAlone => match field.length {
678                            FieldLength::One
679                            | FieldLength::Two
680                            | FieldLength::NumericOverride(_) => {
681                                // Stand-alone fields also support a numeric 1 digit per UTS-35, but there is
682                                // no way to request it using the current system. As of 2021-12-06
683                                // no skeletons resolve to patterns containing this symbol.
684                                //
685                                // All resolved patterns from cldr-json:
686                                // https://github.com/gregtatum/cldr-json/blob/d4779f9611a4cc1e3e6a0a4597e92ead32d9f118/stand-alone-week.js
687                                //     'ccc',
688                                //     'ccc d. MMM',
689                                //     'ccc d. MMMM',
690                                //     'cccc d. MMMM y',
691                                //     'd, ccc',
692                                //     'cccနေ့',
693                                //     'ccc, d MMM',
694                                //     "ccc, d 'de' MMMM",
695                                //     "ccc, d 'de' MMMM 'de' y",
696                                //     'ccc, h:mm B',
697                                //     'ccc, h:mm:ss B',
698                                //     'ccc, d',
699                                //     "ccc, dd.MM.y 'г'.",
700                                //     'ccc, d.MM.y',
701                                //     'ccc, MMM d. y'
702                                unimplemented!("Numeric stand-alone fields are not supported.")
703                            }
704                            FieldLength::Three => Text::Short,
705                            FieldLength::Four => Text::Long,
706                            FieldLength::Five | FieldLength::Six => Text::Narrow,
707                        },
708                        fields::Weekday::Local => unimplemented!("fields::Weekday::Local"),
709                    });
710                }
711                FieldSymbol::DayPeriod(_) => {
712                    // Day period does not affect the resolved components.
713                }
714                FieldSymbol::Hour(hour) => {
715                    bag.hour = Some(match field.length {
716                        FieldLength::Two => Numeric::TwoDigit,
717                        _ => Numeric::Numeric,
718                    });
719                    bag.hour_cycle = Some(match hour {
720                        fields::Hour::H11 => HourCycle::H11,
721                        fields::Hour::H12 => HourCycle::H12,
722                        fields::Hour::H23 => HourCycle::H23,
723                    });
724                }
725                FieldSymbol::Minute => {
726                    bag.minute = Some(match field.length {
727                        FieldLength::Two => Numeric::TwoDigit,
728                        _ => Numeric::Numeric,
729                    });
730                }
731                FieldSymbol::Second(second) => match second {
732                    fields::Second::Second => {
733                        bag.second = Some(match field.length {
734                            FieldLength::Two => Numeric::TwoDigit,
735                            _ => Numeric::Numeric,
736                        });
737                    }
738                    fields::Second::MillisInDay => unimplemented!("fields::Second::MillisInDay"),
739                },
740                FieldSymbol::DecimalSecond(decimal_second) => {
741                    use SubsecondDigits::*;
742                    bag.second = Some(match field.length {
743                        FieldLength::Two => Numeric::TwoDigit,
744                        _ => Numeric::Numeric,
745                    });
746                    bag.subsecond = Some(match decimal_second {
747                        fields::DecimalSecond::Subsecond1 => S1,
748                        fields::DecimalSecond::Subsecond2 => S2,
749                        fields::DecimalSecond::Subsecond3 => S3,
750                        fields::DecimalSecond::Subsecond4 => S4,
751                        fields::DecimalSecond::Subsecond5 => S5,
752                        fields::DecimalSecond::Subsecond6 => S6,
753                        fields::DecimalSecond::Subsecond7 => S7,
754                        fields::DecimalSecond::Subsecond8 => S8,
755                        fields::DecimalSecond::Subsecond9 => S9,
756                    });
757                }
758                FieldSymbol::TimeZone(time_zone_name) => {
759                    bag.time_zone_name = Some(match time_zone_name {
760                        fields::TimeZone::SpecificNonLocation => match field.length {
761                            FieldLength::One => TimeZoneName::ShortSpecific,
762                            _ => TimeZoneName::LongSpecific,
763                        },
764                        fields::TimeZone::GenericNonLocation => match field.length {
765                            FieldLength::One => TimeZoneName::ShortGeneric,
766                            _ => TimeZoneName::LongGeneric,
767                        },
768                        fields::TimeZone::LocalizedOffset => match field.length {
769                            FieldLength::One => TimeZoneName::ShortOffset,
770                            _ => TimeZoneName::LongOffset,
771                        },
772                        fields::TimeZone::Location => unimplemented!("fields::TimeZone::Location"),
773                        fields::TimeZone::Iso => unimplemented!("fields::TimeZone::IsoZ"),
774                        fields::TimeZone::IsoWithZ => unimplemented!("fields::TimeZone::Iso"),
775                    });
776                }
777            }
778        }
779
780        bag
781    }
782}
783
784#[cfg(test)]
785mod test {
786    use super::*;
787
788    // Shorten these for terser tests.
789    type Symbol = FieldSymbol;
790    type Length = FieldLength;
791
792    #[test]
793    fn test_component_bag_to_vec_field() {
794        let bag = Bag {
795            year: Some(Year::Numeric),
796            month: Some(Month::Long),
797            // TODO(#5643): Add week fields back
798            week: None,
799            day: Some(Day::NumericDayOfMonth),
800
801            hour: Some(Numeric::Numeric),
802            minute: Some(Numeric::Numeric),
803            second: Some(Numeric::Numeric),
804            subsecond: Some(SubsecondDigits::S3),
805
806            ..Default::default()
807        };
808        assert_eq!(
809            bag.to_vec_fields(HourCycle::H23),
810            [
811                (Symbol::Year(fields::Year::Calendar), Length::One).into(),
812                (Symbol::Month(fields::Month::Format), Length::Four).into(),
813                (Symbol::Day(fields::Day::DayOfMonth), Length::One).into(),
814                (Symbol::Hour(fields::Hour::H23), Length::One).into(),
815                (Symbol::Minute, Length::One).into(),
816                (
817                    Symbol::DecimalSecond(fields::DecimalSecond::Subsecond3),
818                    Length::One
819                )
820                    .into(),
821            ]
822        );
823    }
824
825    #[test]
826    fn test_component_bag_to_vec_field2() {
827        let bag = Bag {
828            year: Some(Year::Numeric),
829            month: Some(Month::TwoDigit),
830            day: Some(Day::NumericDayOfMonth),
831            ..Default::default()
832        };
833        assert_eq!(
834            bag.to_vec_fields(HourCycle::H23),
835            [
836                (Symbol::Year(fields::Year::Calendar), Length::One).into(),
837                (Symbol::Month(fields::Month::Format), Length::Two).into(),
838                (Symbol::Day(fields::Day::DayOfMonth), Length::One).into(),
839            ]
840        );
841    }
842}