use crate::options::SubsecondDigits;
#[cfg(feature = "datagen")]
use crate::provider::fields::FieldLength;
use crate::DateTimeFormatterPreferences;
use core::{cmp::Ordering, convert::TryFrom};
use displaydoc::Display;
use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle;
use icu_locale_core::subtags::region;
use icu_locale_core::subtags::Region;
use icu_provider::prelude::*;
use zerovec::ule::{AsULE, UleError, ULE};
#[derive(Display, Debug, PartialEq, Copy, Clone)]
#[non_exhaustive]
pub enum SymbolError {
#[displaydoc("Invalid field symbol index: {0}")]
InvalidIndex(u8),
#[displaydoc("Unknown field symbol: {0}")]
Unknown(char),
#[displaydoc("Invalid character for a field symbol: {0}")]
Invalid(u8),
}
impl core::error::Error for SymbolError {}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
#[cfg_attr(feature = "datagen", databake(path = icu_datetime::fields))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[allow(clippy::exhaustive_enums)] pub enum FieldSymbol {
Era,
Year(Year),
Month(Month),
Week(Week),
Day(Day),
Weekday(Weekday),
DayPeriod(DayPeriod),
Hour(Hour),
Minute,
Second(Second),
TimeZone(TimeZone),
DecimalSecond(DecimalSecond),
}
impl FieldSymbol {
#[inline]
pub(crate) fn idx(self) -> u8 {
let (high, low) = match self {
FieldSymbol::Era => (0, 0),
FieldSymbol::Year(year) => (1, year.idx()),
FieldSymbol::Month(month) => (2, month.idx()),
FieldSymbol::Week(w) => (3, w.idx()),
FieldSymbol::Day(day) => (4, day.idx()),
FieldSymbol::Weekday(wd) => (5, wd.idx()),
FieldSymbol::DayPeriod(dp) => (6, dp.idx()),
FieldSymbol::Hour(hour) => (7, hour.idx()),
FieldSymbol::Minute => (8, 0),
FieldSymbol::Second(second) => (9, second.idx()),
FieldSymbol::TimeZone(tz) => (10, tz.idx()),
FieldSymbol::DecimalSecond(second) => (11, second.idx()),
};
let result = high << 4;
result | low
}
#[inline]
pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
let low = idx & 0b0000_1111;
let high = idx >> 4;
Ok(match high {
0 if low == 0 => Self::Era,
1 => Self::Year(Year::from_idx(low)?),
2 => Self::Month(Month::from_idx(low)?),
3 => Self::Week(Week::from_idx(low)?),
4 => Self::Day(Day::from_idx(low)?),
5 => Self::Weekday(Weekday::from_idx(low)?),
6 => Self::DayPeriod(DayPeriod::from_idx(low)?),
7 => Self::Hour(Hour::from_idx(low)?),
8 if low == 0 => Self::Minute,
9 => Self::Second(Second::from_idx(low)?),
10 => Self::TimeZone(TimeZone::from_idx(low)?),
11 => Self::DecimalSecond(DecimalSecond::from_idx(low)?),
_ => return Err(SymbolError::InvalidIndex(idx)),
})
}
#[cfg(feature = "datagen")]
fn idx_for_skeleton(self) -> u8 {
match self {
FieldSymbol::Era => 0,
FieldSymbol::Year(_) => 1,
FieldSymbol::Month(_) => 2,
FieldSymbol::Week(_) => 3,
FieldSymbol::Day(_) => 4,
FieldSymbol::Weekday(_) => 5,
FieldSymbol::DayPeriod(_) => 6,
FieldSymbol::Hour(_) => 7,
FieldSymbol::Minute => 8,
FieldSymbol::Second(_) | FieldSymbol::DecimalSecond(_) => 9,
FieldSymbol::TimeZone(_) => 10,
}
}
#[cfg(feature = "datagen")]
pub(crate) fn skeleton_cmp(self, other: Self) -> Ordering {
self.idx_for_skeleton().cmp(&other.idx_for_skeleton())
}
pub(crate) fn from_subsecond_digits(subsecond_digits: SubsecondDigits) -> Self {
use SubsecondDigits::*;
match subsecond_digits {
S1 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond1),
S2 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond2),
S3 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond3),
S4 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond4),
S5 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond5),
S6 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond6),
S7 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond7),
S8 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond8),
S9 => FieldSymbol::DecimalSecond(DecimalSecond::Subsecond9),
}
}
#[cfg(feature = "datagen")]
pub(crate) fn is_at_least_abbreviated(self) -> bool {
matches!(
self,
FieldSymbol::Era
| FieldSymbol::Year(Year::Cyclic)
| FieldSymbol::Weekday(Weekday::Format)
| FieldSymbol::DayPeriod(_)
| FieldSymbol::TimeZone(TimeZone::SpecificNonLocation)
)
}
}
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct FieldSymbolULE(u8);
impl AsULE for FieldSymbol {
type ULE = FieldSymbolULE;
fn to_unaligned(self) -> Self::ULE {
FieldSymbolULE(self.idx())
}
fn from_unaligned(unaligned: Self::ULE) -> Self {
#[expect(clippy::unwrap_used)] Self::from_idx(unaligned.0).unwrap()
}
}
impl FieldSymbolULE {
#[inline]
pub(crate) fn validate_byte(byte: u8) -> Result<(), UleError> {
FieldSymbol::from_idx(byte)
.map(|_| ())
.map_err(|_| UleError::parse::<FieldSymbol>())
}
}
unsafe impl ULE for FieldSymbolULE {
fn validate_bytes(bytes: &[u8]) -> Result<(), UleError> {
for byte in bytes {
Self::validate_byte(*byte)?;
}
Ok(())
}
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
#[allow(clippy::exhaustive_enums)] #[cfg(feature = "datagen")]
pub(crate) enum TextOrNumeric {
Text,
Numeric,
}
#[cfg(feature = "datagen")]
pub(crate) trait LengthType {
fn get_length_type(self, length: FieldLength) -> TextOrNumeric;
}
impl FieldSymbol {
fn get_canonical_order(self) -> u8 {
match self {
Self::Era => 0,
Self::Year(Year::Calendar) => 1,
Self::Year(Year::Extended) => 2,
Self::Year(Year::Cyclic) => 3,
Self::Year(Year::RelatedIso) => 4,
Self::Month(Month::Format) => 5,
Self::Month(Month::StandAlone) => 6,
Self::Week(_) => unreachable!(), Self::Day(Day::DayOfMonth) => 9,
Self::Day(Day::DayOfYear) => 10,
Self::Day(Day::DayOfWeekInMonth) => 11,
Self::Day(Day::ModifiedJulianDay) => 12,
Self::Weekday(Weekday::Format) => 13,
Self::Weekday(Weekday::Local) => 14,
Self::Weekday(Weekday::StandAlone) => 15,
Self::DayPeriod(DayPeriod::AmPm) => 16,
Self::DayPeriod(DayPeriod::NoonMidnight) => 17,
Self::Hour(Hour::H11) => 18,
Self::Hour(Hour::H12) => 19,
Self::Hour(Hour::H23) => 20,
Self::Minute => 22,
Self::Second(Second::Second) => 23,
Self::Second(Second::MillisInDay) => 24,
Self::DecimalSecond(DecimalSecond::Subsecond1) => 31,
Self::DecimalSecond(DecimalSecond::Subsecond2) => 32,
Self::DecimalSecond(DecimalSecond::Subsecond3) => 33,
Self::DecimalSecond(DecimalSecond::Subsecond4) => 34,
Self::DecimalSecond(DecimalSecond::Subsecond5) => 35,
Self::DecimalSecond(DecimalSecond::Subsecond6) => 36,
Self::DecimalSecond(DecimalSecond::Subsecond7) => 37,
Self::DecimalSecond(DecimalSecond::Subsecond8) => 38,
Self::DecimalSecond(DecimalSecond::Subsecond9) => 39,
Self::TimeZone(TimeZone::SpecificNonLocation) => 100,
Self::TimeZone(TimeZone::LocalizedOffset) => 102,
Self::TimeZone(TimeZone::GenericNonLocation) => 103,
Self::TimeZone(TimeZone::Location) => 104,
Self::TimeZone(TimeZone::Iso) => 105,
Self::TimeZone(TimeZone::IsoWithZ) => 106,
}
}
}
impl TryFrom<char> for FieldSymbol {
type Error = SymbolError;
fn try_from(ch: char) -> Result<Self, Self::Error> {
if !ch.is_ascii_alphanumeric() {
return Err(SymbolError::Invalid(ch as u8));
}
(if ch == 'G' {
Ok(Self::Era)
} else {
Err(SymbolError::Unknown(ch))
})
.or_else(|_| Year::try_from(ch).map(Self::Year))
.or_else(|_| Month::try_from(ch).map(Self::Month))
.or_else(|_| Week::try_from(ch).map(Self::Week))
.or_else(|_| Day::try_from(ch).map(Self::Day))
.or_else(|_| Weekday::try_from(ch).map(Self::Weekday))
.or_else(|_| DayPeriod::try_from(ch).map(Self::DayPeriod))
.or_else(|_| Hour::try_from(ch).map(Self::Hour))
.or({
if ch == 'm' {
Ok(Self::Minute)
} else {
Err(SymbolError::Unknown(ch))
}
})
.or_else(|_| Second::try_from(ch).map(Self::Second))
.or_else(|_| TimeZone::try_from(ch).map(Self::TimeZone))
}
}
impl From<FieldSymbol> for char {
fn from(symbol: FieldSymbol) -> Self {
match symbol {
FieldSymbol::Era => 'G',
FieldSymbol::Year(year) => year.into(),
FieldSymbol::Month(month) => month.into(),
FieldSymbol::Week(week) => week.into(),
FieldSymbol::Day(day) => day.into(),
FieldSymbol::Weekday(weekday) => weekday.into(),
FieldSymbol::DayPeriod(dayperiod) => dayperiod.into(),
FieldSymbol::Hour(hour) => hour.into(),
FieldSymbol::Minute => 'm',
FieldSymbol::Second(second) => second.into(),
FieldSymbol::TimeZone(time_zone) => time_zone.into(),
FieldSymbol::DecimalSecond(_) => 's',
}
}
}
impl PartialOrd for FieldSymbol {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FieldSymbol {
fn cmp(&self, other: &Self) -> Ordering {
self.get_canonical_order().cmp(&other.get_canonical_order())
}
}
macro_rules! field_type {
($(#[$enum_attr:meta])* $i:ident; { $( $(#[$variant_attr:meta])* $key:literal => $val:ident = $idx:expr,)* }; $length_type:ident; $($ule_name:ident)?) => (
field_type!($(#[$enum_attr])* $i; {$( $(#[$variant_attr])* $key => $val = $idx,)*}; $($ule_name)?);
#[cfg(feature = "datagen")]
impl LengthType for $i {
fn get_length_type(self, _length: FieldLength) -> TextOrNumeric {
TextOrNumeric::$length_type
}
}
);
($(#[$enum_attr:meta])* $i:ident; { $( $(#[$variant_attr:meta])* $key:literal => $val:ident = $idx:expr,)* }; $($ule_name:ident)?) => (
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, yoke::Yokeable, zerofrom::ZeroFrom)]
#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
#[cfg_attr(feature = "datagen", databake(path = icu_datetime::fields))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[allow(clippy::enum_variant_names)]
$(
#[repr(u8)]
#[zerovec::make_ule($ule_name)]
#[zerovec::derive(Debug)]
)?
#[allow(clippy::exhaustive_enums)] $(#[$enum_attr])*
pub enum $i {
$(
$(#[$variant_attr])*
#[doc = core::concat!("\n\nThis field symbol is represented by the character `", $key, "` in a date formatting pattern string.")]
#[doc = "\n\nFor more details, see documentation on [date field symbols](https://unicode.org/reports/tr35/tr35-dates.html#table-date-field-symbol-table)."]
$val = $idx,
)*
}
$(
#[allow(path_statements)] const _: () = { $ule_name; };
impl $i {
#[inline]
pub(crate) fn idx(self) -> u8 {
self as u8
}
#[inline]
pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
Self::new_from_u8(idx)
.ok_or(SymbolError::InvalidIndex(idx))
}
}
)?
impl TryFrom<char> for $i {
type Error = SymbolError;
fn try_from(ch: char) -> Result<Self, Self::Error> {
match ch {
$(
$key => Ok(Self::$val),
)*
_ => Err(SymbolError::Unknown(ch)),
}
}
}
impl From<$i> for FieldSymbol {
fn from(input: $i) -> Self {
Self::$i(input)
}
}
impl From<$i> for char {
fn from(input: $i) -> char {
match input {
$(
$i::$val => $key,
)*
}
}
}
);
}
field_type! (
Year; {
'y' => Calendar = 0,
'U' => Cyclic = 1,
'r' => RelatedIso = 2,
'u' => Extended = 3,
};
YearULE
);
#[cfg(feature = "datagen")]
impl LengthType for Year {
fn get_length_type(self, _length: FieldLength) -> TextOrNumeric {
match self {
Year::Cyclic => TextOrNumeric::Text,
_ => TextOrNumeric::Numeric,
}
}
}
field_type!(
Month; {
'M' => Format = 0,
'L' => StandAlone = 1,
}; MonthULE);
#[cfg(feature = "datagen")]
impl LengthType for Month {
fn get_length_type(self, length: FieldLength) -> TextOrNumeric {
match length {
FieldLength::One => TextOrNumeric::Numeric,
FieldLength::NumericOverride(_) => TextOrNumeric::Numeric,
FieldLength::Two => TextOrNumeric::Numeric,
FieldLength::Three => TextOrNumeric::Text,
FieldLength::Four => TextOrNumeric::Text,
FieldLength::Five => TextOrNumeric::Text,
FieldLength::Six => TextOrNumeric::Text,
}
}
}
field_type!(
Day; {
'd' => DayOfMonth = 0,
'D' => DayOfYear = 1,
'F' => DayOfWeekInMonth = 2,
'g' => ModifiedJulianDay = 3,
};
Numeric;
DayULE
);
field_type!(
Hour; {
'K' => H11 = 0,
'h' => H12 = 1,
'H' => H23 = 2,
};
Numeric;
HourULE
);
impl Hour {
pub(crate) fn from_prefs(prefs: DateTimeFormatterPreferences) -> Option<Self> {
let field = match prefs.hour_cycle? {
HourCycle::H11 => Self::H11,
HourCycle::H12 => Self::H12,
HourCycle::H23 => Self::H23,
HourCycle::Clock12 => {
let region = prefs
.locale_preferences
.to_data_locale_region_priority()
.region;
const JP: Region = region!("JP");
match region {
Some(JP) => Self::H11,
_ => Self::H12,
}
}
HourCycle::Clock24 => Self::H23,
_ => unreachable!(),
};
Some(field)
}
}
#[test]
fn test_hour_cycle_selection() {
struct TestCase {
locale: &'static str,
expected: Option<Hour>,
}
let cases = [
TestCase {
locale: "en-US",
expected: None,
},
TestCase {
locale: "en-US-u-hc-h11",
expected: Some(Hour::H11),
},
TestCase {
locale: "en-US-u-hc-h12",
expected: Some(Hour::H12),
},
TestCase {
locale: "en-US-u-hc-h23",
expected: Some(Hour::H23),
},
TestCase {
locale: "en-US-u-hc-c12",
expected: Some(Hour::H12),
},
TestCase {
locale: "en-US-u-hc-c24",
expected: Some(Hour::H23),
},
TestCase {
locale: "fr-FR",
expected: None,
},
TestCase {
locale: "fr-FR-u-hc-h11",
expected: Some(Hour::H11),
},
TestCase {
locale: "fr-FR-u-hc-h12",
expected: Some(Hour::H12),
},
TestCase {
locale: "fr-FR-u-hc-h23",
expected: Some(Hour::H23),
},
TestCase {
locale: "fr-FR-u-hc-c12",
expected: Some(Hour::H12),
},
TestCase {
locale: "fr-FR-u-hc-c24",
expected: Some(Hour::H23),
},
TestCase {
locale: "ja-JP",
expected: None,
},
TestCase {
locale: "ja-JP-u-hc-h11",
expected: Some(Hour::H11),
},
TestCase {
locale: "ja-JP-u-hc-h12",
expected: Some(Hour::H12),
},
TestCase {
locale: "ja-JP-u-hc-h23",
expected: Some(Hour::H23),
},
TestCase {
locale: "ja-JP-u-hc-c12",
expected: Some(Hour::H11),
},
TestCase {
locale: "ja-JP-u-hc-c24",
expected: Some(Hour::H23),
},
];
for TestCase { locale, expected } in cases {
let locale = icu_locale_core::Locale::try_from_str(locale).unwrap();
let prefs = DateTimeFormatterPreferences::from(&locale);
let actual = Hour::from_prefs(prefs);
assert_eq!(expected, actual, "{}", locale);
}
}
field_type!(
Second; {
's' => Second = 0,
'A' => MillisInDay = 1,
};
Numeric;
SecondULE
);
field_type!(
Week; {
};
Numeric;
);
impl Week {
#[inline]
pub(crate) fn idx(self) -> u8 {
0
}
#[inline]
pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
Err(SymbolError::InvalidIndex(idx))
}
}
field_type!(
Weekday; {
'E' => Format = 0,
'e' => Local = 1,
'c' => StandAlone = 2,
};
WeekdayULE
);
#[cfg(feature = "datagen")]
impl LengthType for Weekday {
fn get_length_type(self, length: FieldLength) -> TextOrNumeric {
match self {
Self::Format => TextOrNumeric::Text,
Self::Local | Self::StandAlone => match length {
FieldLength::One | FieldLength::Two => TextOrNumeric::Numeric,
_ => TextOrNumeric::Text,
},
}
}
}
impl Weekday {
pub(crate) fn to_format_symbol(self) -> Self {
match self {
Weekday::Local => Weekday::Format,
other => other,
}
}
}
field_type!(
DayPeriod; {
'a' => AmPm = 0,
'b' => NoonMidnight = 1,
};
Text;
DayPeriodULE
);
field_type!(
TimeZone; {
'z' => SpecificNonLocation = 0,
'O' => LocalizedOffset = 1,
'v' => GenericNonLocation = 2,
'V' => Location = 3,
'x' => Iso = 4,
'X' => IsoWithZ = 5,
};
TimeZoneULE
);
#[cfg(feature = "datagen")]
impl LengthType for TimeZone {
fn get_length_type(self, _: FieldLength) -> TextOrNumeric {
use TextOrNumeric::*;
match self {
Self::Iso | Self::IsoWithZ => Numeric,
Self::LocalizedOffset
| Self::SpecificNonLocation
| Self::GenericNonLocation
| Self::Location => Text,
}
}
}
#[derive(
Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, yoke::Yokeable, zerofrom::ZeroFrom,
)]
#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
#[cfg_attr(feature = "datagen", databake(path = icu_datetime::fields))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[repr(u8)]
#[zerovec::make_ule(DecimalSecondULE)]
#[zerovec::derive(Debug)]
#[allow(clippy::exhaustive_enums)] pub enum DecimalSecond {
Subsecond1 = 1,
Subsecond2 = 2,
Subsecond3 = 3,
Subsecond4 = 4,
Subsecond5 = 5,
Subsecond6 = 6,
Subsecond7 = 7,
Subsecond8 = 8,
Subsecond9 = 9,
}
impl DecimalSecond {
#[inline]
pub(crate) fn idx(self) -> u8 {
self as u8
}
#[inline]
pub(crate) fn from_idx(idx: u8) -> Result<Self, SymbolError> {
Self::new_from_u8(idx).ok_or(SymbolError::InvalidIndex(idx))
}
}
impl From<DecimalSecond> for FieldSymbol {
fn from(input: DecimalSecond) -> Self {
Self::DecimalSecond(input)
}
}