use crate::{
options::SubsecondDigits,
provider::fields::{self, Field, FieldLength, FieldSymbol},
provider::pattern::{reference, runtime::Pattern, PatternItem},
};
use crate::pattern::DateTimePattern;
use alloc::vec::Vec;
use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[non_exhaustive]
pub struct Bag {
pub era: Option<Text>,
pub year: Option<Year>,
pub month: Option<Month>,
pub week: Option<Week>,
pub day: Option<Day>,
pub weekday: Option<Text>,
pub hour: Option<Numeric>,
pub minute: Option<Numeric>,
pub second: Option<Numeric>,
pub subsecond: Option<SubsecondDigits>,
pub time_zone_name: Option<TimeZoneName>,
pub hour_cycle: Option<HourCycle>,
}
impl Bag {
pub fn empty() -> Self {
Self::default()
}
pub fn merge(self, other: Self) -> Self {
Self {
era: other.era.or(self.era),
year: other.year.or(self.year),
month: other.month.or(self.month),
week: other.week.or(self.week),
day: other.day.or(self.day),
weekday: other.weekday.or(self.weekday),
hour: other.hour.or(self.hour),
minute: other.minute.or(self.minute),
second: other.second.or(self.second),
subsecond: other.subsecond.or(self.subsecond),
time_zone_name: other.time_zone_name.or(self.time_zone_name),
hour_cycle: other.hour_cycle.or(self.hour_cycle),
}
}
pub fn to_vec_fields(&self, default_hour_cycle: HourCycle) -> Vec<Field> {
let mut fields = Vec::new();
if let Some(era) = self.era {
fields.push(Field {
symbol: FieldSymbol::Era,
length: match era {
Text::Short => FieldLength::Three,
Text::Long => FieldLength::Four,
Text::Narrow => FieldLength::Five,
},
})
}
if let Some(year) = self.year {
fields.push(Field {
symbol: FieldSymbol::Year(match year {
Year::Numeric | Year::TwoDigit => fields::Year::Calendar,
Year::NumericWeekOf | Year::TwoDigitWeekOf => {
unimplemented!("#5643 fields::Year::WeekOf")
}
}),
length: match year {
Year::Numeric | Year::NumericWeekOf => FieldLength::One,
Year::TwoDigit | Year::TwoDigitWeekOf => FieldLength::Two,
},
});
}
if let Some(month) = self.month {
fields.push(Field {
symbol: FieldSymbol::Month(fields::Month::Format),
length: match month {
Month::Numeric => FieldLength::One,
Month::TwoDigit => FieldLength::Two,
Month::Long => FieldLength::Four,
Month::Short => FieldLength::Three,
Month::Narrow => FieldLength::Five,
},
});
}
if let Some(week) = self.week {
#[allow(
unreachable_code,
reason = "Uninhabited MultipleVariants (due to pivot_field), see #7118"
)]
fields.push(Field {
symbol: FieldSymbol::Week(match week {
Week::WeekOfMonth => unimplemented!("#5643 fields::Week::WeekOfMonth"),
Week::NumericWeekOfYear | Week::TwoDigitWeekOfYear => {
unimplemented!("#5643 fields::Week::WeekOfYear")
}
}),
length: match week {
Week::WeekOfMonth | Week::NumericWeekOfYear => FieldLength::One,
Week::TwoDigitWeekOfYear => FieldLength::Two,
},
});
}
if let Some(day) = self.day {
fields.push(Field {
symbol: FieldSymbol::Day(match day {
Day::NumericDayOfMonth | Day::TwoDigitDayOfMonth => fields::Day::DayOfMonth,
Day::DayOfWeekInMonth => fields::Day::DayOfWeekInMonth,
Day::DayOfYear => fields::Day::DayOfYear,
Day::ModifiedJulianDay => fields::Day::ModifiedJulianDay,
}),
length: match day {
Day::NumericDayOfMonth
| Day::DayOfWeekInMonth
| Day::DayOfYear
| Day::ModifiedJulianDay => FieldLength::One,
Day::TwoDigitDayOfMonth => FieldLength::Two,
},
});
}
if let Some(weekday) = self.weekday {
fields.push(Field {
symbol: FieldSymbol::Weekday(fields::Weekday::Format),
length: match weekday {
Text::Long => FieldLength::Four,
Text::Short => FieldLength::One,
Text::Narrow => FieldLength::Five,
},
});
}
if let Some(hour) = self.hour {
let hour_cycle = self.hour_cycle.unwrap_or(default_hour_cycle);
fields.push(Field {
symbol: FieldSymbol::Hour(match hour_cycle {
HourCycle::H11 | HourCycle::H12 => {
fields::Hour::H12
}
HourCycle::H23 => {
fields::Hour::H23
}
_ => unreachable!(),
}),
length: match hour {
Numeric::Numeric => FieldLength::One,
Numeric::TwoDigit => FieldLength::Two,
},
});
}
if let Some(minute) = self.minute {
fields.push(Field {
symbol: FieldSymbol::Minute,
length: match minute {
Numeric::Numeric => FieldLength::One,
Numeric::TwoDigit => FieldLength::Two,
},
});
}
if let Some(second) = self.second {
let symbol = match self.subsecond {
None => FieldSymbol::Second(fields::Second::Second),
Some(subsecond) => FieldSymbol::from_subsecond_digits(subsecond),
};
fields.push(Field {
symbol,
length: match second {
Numeric::Numeric => FieldLength::One,
Numeric::TwoDigit => FieldLength::Two,
},
});
}
if self.time_zone_name.is_some() {
fields.push(Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation),
length: FieldLength::One,
});
}
{
#![allow(clippy::indexing_slicing)] debug_assert!(
fields.windows(2).all(|f| f[0] < f[1]),
"The fields are sorted and unique."
);
}
fields
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum Numeric {
Numeric,
TwoDigit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum Text {
Long,
Short,
Narrow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum Year {
Numeric,
TwoDigit,
NumericWeekOf,
TwoDigitWeekOf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum Month {
Numeric,
TwoDigit,
Long,
Short,
Narrow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum Week {
WeekOfMonth,
NumericWeekOfYear,
TwoDigitWeekOfYear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum Day {
NumericDayOfMonth,
TwoDigitDayOfMonth,
DayOfWeekInMonth,
DayOfYear,
ModifiedJulianDay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
#[non_exhaustive]
pub enum TimeZoneName {
ShortSpecific,
LongSpecific,
LongOffset,
ShortOffset,
ShortGeneric,
LongGeneric,
}
impl From<TimeZoneName> for Field {
fn from(time_zone_name: TimeZoneName) -> Self {
match time_zone_name {
TimeZoneName::ShortSpecific => Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::SpecificNonLocation),
length: FieldLength::One,
},
TimeZoneName::LongSpecific => Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::SpecificNonLocation),
length: FieldLength::Four,
},
TimeZoneName::LongOffset => Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::LocalizedOffset),
length: FieldLength::Four,
},
TimeZoneName::ShortOffset => Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::LocalizedOffset),
length: FieldLength::One,
},
TimeZoneName::ShortGeneric => Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation),
length: FieldLength::One,
},
TimeZoneName::LongGeneric => Field {
symbol: FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation),
length: FieldLength::Four,
},
}
}
}
impl From<&DateTimePattern> for Bag {
fn from(value: &DateTimePattern) -> Self {
Self::from(value.as_borrowed().0)
}
}
impl From<&Pattern<'_>> for Bag {
fn from(pattern: &Pattern) -> Self {
Self::from_pattern_items(pattern.items.iter())
}
}
impl From<&reference::Pattern> for Bag {
fn from(pattern: &reference::Pattern) -> Self {
Self::from_pattern_items(pattern.items.iter().copied())
}
}
impl Bag {
fn from_pattern_items(pattern_items: impl Iterator<Item = PatternItem>) -> Self {
let mut bag: Bag = Default::default();
for item in pattern_items {
let field = match item {
PatternItem::Field(ref field) => field,
PatternItem::Literal(_) => continue,
};
match field.symbol {
FieldSymbol::Era => {
bag.era = Some(match field.length {
FieldLength::One
| FieldLength::NumericOverride(_)
| FieldLength::Two
| FieldLength::Three => Text::Short,
FieldLength::Four => Text::Long,
FieldLength::Five | FieldLength::Six => Text::Narrow,
});
}
FieldSymbol::Year(year) => {
bag.year = Some(match year {
fields::Year::Calendar => match field.length {
FieldLength::Two => Year::TwoDigit,
_ => Year::Numeric,
},
_ => Year::Numeric,
});
}
FieldSymbol::Month(_) => {
bag.month = Some(match field.length {
FieldLength::One => Month::Numeric,
FieldLength::NumericOverride(_) => Month::Numeric,
FieldLength::Two => Month::TwoDigit,
FieldLength::Three => Month::Short,
FieldLength::Four => Month::Long,
FieldLength::Five | FieldLength::Six => Month::Narrow,
});
}
FieldSymbol::Week(_week) => {
}
FieldSymbol::Day(day) => {
bag.day = Some(match day {
fields::Day::DayOfMonth => match field.length {
FieldLength::Two => Day::TwoDigitDayOfMonth,
_ => Day::NumericDayOfMonth,
},
fields::Day::DayOfYear => Day::DayOfYear,
fields::Day::DayOfWeekInMonth => Day::DayOfWeekInMonth,
fields::Day::ModifiedJulianDay => Day::ModifiedJulianDay,
});
}
FieldSymbol::Weekday(weekday) => {
bag.weekday = Some(match weekday {
fields::Weekday::Format => match field.length {
FieldLength::One | FieldLength::Two | FieldLength::Three => Text::Short,
FieldLength::Four => Text::Long,
_ => Text::Narrow,
},
fields::Weekday::StandAlone => match field.length {
FieldLength::One
| FieldLength::Two
| FieldLength::NumericOverride(_) => {
unimplemented!("Numeric stand-alone fields are not supported.")
}
FieldLength::Three => Text::Short,
FieldLength::Four => Text::Long,
FieldLength::Five | FieldLength::Six => Text::Narrow,
},
fields::Weekday::Local => unimplemented!("fields::Weekday::Local"),
});
}
FieldSymbol::DayPeriod(_) => {
}
FieldSymbol::Hour(hour) => {
bag.hour = Some(match field.length {
FieldLength::Two => Numeric::TwoDigit,
_ => Numeric::Numeric,
});
bag.hour_cycle = Some(match hour {
fields::Hour::H11 => HourCycle::H11,
fields::Hour::H12 => HourCycle::H12,
fields::Hour::H23 => HourCycle::H23,
});
}
FieldSymbol::Minute => {
bag.minute = Some(match field.length {
FieldLength::Two => Numeric::TwoDigit,
_ => Numeric::Numeric,
});
}
FieldSymbol::Second(second) => match second {
fields::Second::Second => {
bag.second = Some(match field.length {
FieldLength::Two => Numeric::TwoDigit,
_ => Numeric::Numeric,
});
}
fields::Second::MillisInDay => unimplemented!("fields::Second::MillisInDay"),
},
FieldSymbol::DecimalSecond(decimal_second) => {
use SubsecondDigits::*;
bag.second = Some(match field.length {
FieldLength::Two => Numeric::TwoDigit,
_ => Numeric::Numeric,
});
bag.subsecond = Some(match decimal_second {
fields::DecimalSecond::Subsecond1 => S1,
fields::DecimalSecond::Subsecond2 => S2,
fields::DecimalSecond::Subsecond3 => S3,
fields::DecimalSecond::Subsecond4 => S4,
fields::DecimalSecond::Subsecond5 => S5,
fields::DecimalSecond::Subsecond6 => S6,
fields::DecimalSecond::Subsecond7 => S7,
fields::DecimalSecond::Subsecond8 => S8,
fields::DecimalSecond::Subsecond9 => S9,
});
}
FieldSymbol::TimeZone(time_zone_name) => {
bag.time_zone_name = Some(match time_zone_name {
fields::TimeZone::SpecificNonLocation => match field.length {
FieldLength::One => TimeZoneName::ShortSpecific,
_ => TimeZoneName::LongSpecific,
},
fields::TimeZone::GenericNonLocation => match field.length {
FieldLength::One => TimeZoneName::ShortGeneric,
_ => TimeZoneName::LongGeneric,
},
fields::TimeZone::LocalizedOffset => match field.length {
FieldLength::One => TimeZoneName::ShortOffset,
_ => TimeZoneName::LongOffset,
},
fields::TimeZone::Location => unimplemented!("fields::TimeZone::Location"),
fields::TimeZone::Iso => unimplemented!("fields::TimeZone::IsoZ"),
fields::TimeZone::IsoWithZ => unimplemented!("fields::TimeZone::Iso"),
});
}
}
}
bag
}
}
#[cfg(test)]
mod test {
use super::*;
type Symbol = FieldSymbol;
type Length = FieldLength;
#[test]
fn test_component_bag_to_vec_field() {
let bag = Bag {
year: Some(Year::Numeric),
month: Some(Month::Long),
week: None,
day: Some(Day::NumericDayOfMonth),
hour: Some(Numeric::Numeric),
minute: Some(Numeric::Numeric),
second: Some(Numeric::Numeric),
subsecond: Some(SubsecondDigits::S3),
..Default::default()
};
assert_eq!(
bag.to_vec_fields(HourCycle::H23),
[
(Symbol::Year(fields::Year::Calendar), Length::One).into(),
(Symbol::Month(fields::Month::Format), Length::Four).into(),
(Symbol::Day(fields::Day::DayOfMonth), Length::One).into(),
(Symbol::Hour(fields::Hour::H23), Length::One).into(),
(Symbol::Minute, Length::One).into(),
(
Symbol::DecimalSecond(fields::DecimalSecond::Subsecond3),
Length::One
)
.into(),
]
);
}
#[test]
fn test_component_bag_to_vec_field2() {
let bag = Bag {
year: Some(Year::Numeric),
month: Some(Month::TwoDigit),
day: Some(Day::NumericDayOfMonth),
..Default::default()
};
assert_eq!(
bag.to_vec_fields(HourCycle::H23),
[
(Symbol::Year(fields::Year::Calendar), Length::One).into(),
(Symbol::Month(fields::Month::Format), Length::Two).into(),
(Symbol::Day(fields::Day::DayOfMonth), Length::One).into(),
]
);
}
}