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}