use alloc::collections::BTreeMap;
use alloc::vec;
use alloc::vec::Vec;
use core::cmp::Ordering;
use icu_plurals::PluralElements;
use crate::{
options::SubsecondDigits,
provider::{
fields::{self, components, Field, FieldLength, FieldSymbol},
pattern::{naively_apply_hour_cycle, runtime, PatternItem, TimeGranularity},
skeleton::{reference, FullLongMediumShort, GenericLengthPatterns},
},
};
const MAX_SKELETON_FIELDS: u32 = 10;
const NO_DISTANCE: u32 = 0;
const WIDTH_MISMATCH_DISTANCE: u32 = 1;
const GLUE_DISTANCE: u32 = 10;
const TEXT_VS_NUMERIC_DISTANCE: u32 = 100;
const SUBSTANTIAL_DIFFERENCES_DISTANCE: u32 = 1000;
const SKELETON_EXTRA_SYMBOL: u32 = 10000;
const REQUESTED_SYMBOL_MISSING: u32 = 100000;
#[derive(Debug, PartialEq, Clone)]
#[allow(missing_docs)]
pub enum BestSkeleton<T> {
AllFieldsMatch(T, SkeletonQuality),
MissingOrExtraFields(T, SkeletonQuality),
NoMatch,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SkeletonQuality(u32);
impl SkeletonQuality {
pub fn worst() -> SkeletonQuality {
SkeletonQuality(u32::MAX)
}
pub fn best() -> SkeletonQuality {
SkeletonQuality(0)
}
pub fn is_excellent_match(self) -> bool {
self.0 < GLUE_DISTANCE
}
}
fn naively_apply_time_zone_name(
pattern: &mut runtime::Pattern,
time_zone_name: Option<components::TimeZoneName>,
) {
if let Some(time_zone_name) = time_zone_name {
runtime::helpers::maybe_replace_first(pattern, |item| {
if let PatternItem::Field(Field {
symbol: FieldSymbol::TimeZone(_),
length: _,
}) = item
{
Some(PatternItem::Field((time_zone_name).into()))
} else {
None
}
});
}
}
pub fn create_best_pattern_for_fields<'data>(
skeletons: &BTreeMap<reference::Skeleton, PluralElements<runtime::Pattern<'data>>>,
length_patterns: &GenericLengthPatterns<'data>,
fields: &[Field],
components: &components::Bag,
prefer_matched_pattern: bool,
) -> BestSkeleton<PluralElements<runtime::Pattern<'data>>> {
let first_pattern_match =
get_best_available_format_pattern(skeletons, fields, prefer_matched_pattern);
if let BestSkeleton::AllFieldsMatch(mut pattern, d) = first_pattern_match {
pattern.for_each_mut(|pattern| {
naively_apply_hour_cycle(pattern, components.hour_cycle);
naively_apply_time_zone_name(pattern, components.time_zone_name);
apply_subseconds(pattern, components.subsecond);
});
return BestSkeleton::AllFieldsMatch(pattern, d);
}
let FieldsByType { date, time } = group_fields_by_type(fields);
if date.is_empty() || time.is_empty() {
return match first_pattern_match {
BestSkeleton::AllFieldsMatch(_, _) => {
unreachable!("Logic error in implementation. AllFieldsMatch handled above.")
}
BestSkeleton::MissingOrExtraFields(mut pattern, d) => {
if date.is_empty() {
pattern.for_each_mut(|pattern| {
naively_apply_hour_cycle(pattern, components.hour_cycle);
naively_apply_time_zone_name(pattern, components.time_zone_name);
apply_subseconds(pattern, components.subsecond);
});
}
BestSkeleton::MissingOrExtraFields(pattern, d)
}
BestSkeleton::NoMatch => BestSkeleton::NoMatch,
};
}
let (date_patterns, date_missing_or_extra, date_distance) =
match get_best_available_format_pattern(skeletons, &date, prefer_matched_pattern) {
BestSkeleton::MissingOrExtraFields(fields, d) => (Some(fields), true, d),
BestSkeleton::AllFieldsMatch(fields, d) => (Some(fields), false, d),
BestSkeleton::NoMatch => (None, true, SkeletonQuality(REQUESTED_SYMBOL_MISSING)),
};
let (time_patterns, time_missing_or_extra, time_distance) =
match get_best_available_format_pattern(skeletons, &time, prefer_matched_pattern) {
BestSkeleton::MissingOrExtraFields(fields, d) => (Some(fields), true, d),
BestSkeleton::AllFieldsMatch(fields, d) => (Some(fields), false, d),
BestSkeleton::NoMatch => (None, true, SkeletonQuality(REQUESTED_SYMBOL_MISSING)),
};
let time_pattern: Option<runtime::Pattern<'data>> = time_patterns.map(|pattern| {
#[allow(clippy::unwrap_used)] let mut pattern = pattern.try_into_other().unwrap();
naively_apply_hour_cycle(&mut pattern, components.hour_cycle);
naively_apply_time_zone_name(&mut pattern, components.time_zone_name);
apply_subseconds(&mut pattern, components.subsecond);
pattern
});
let patterns = match (date_patterns, time_pattern) {
(Some(mut date_patterns), Some(time_pattern)) => {
let month_field = fields
.iter()
.find(|f| matches!(f.symbol, FieldSymbol::Month(_)));
let length = match month_field {
Some(field) => match field.length {
FieldLength::Four => {
let weekday = fields
.iter()
.find(|f| matches!(f.symbol, FieldSymbol::Weekday(_)));
if weekday.is_some() {
FullLongMediumShort::Full
} else {
FullLongMediumShort::Long
}
}
FieldLength::Three => FullLongMediumShort::Medium,
_ => FullLongMediumShort::Short,
},
None => FullLongMediumShort::Short,
};
use crate::provider::pattern::runtime::GenericPattern;
let dt_pattern: &GenericPattern<'data> = match length {
FullLongMediumShort::Full => &length_patterns.full,
FullLongMediumShort::Long => &length_patterns.long,
FullLongMediumShort::Medium => &length_patterns.medium,
FullLongMediumShort::Short => &length_patterns.short,
};
date_patterns.for_each_mut(|pattern| {
let date = pattern.clone();
let time = time_pattern.clone();
#[expect(clippy::expect_used)] let dt = dt_pattern
.clone()
.combined(date, time)
.expect("Failed to combine date and time");
*pattern = dt;
});
Some(date_patterns)
}
(None, Some(pattern)) => Some(PluralElements::new(pattern)),
(Some(patterns), None) => Some(patterns),
(None, None) => None,
};
let distance = SkeletonQuality(
date_distance
.0
.saturating_add(time_distance.0)
.saturating_add(GLUE_DISTANCE),
);
match patterns {
Some(patterns) => {
if date_missing_or_extra || time_missing_or_extra {
BestSkeleton::MissingOrExtraFields(patterns, distance)
} else {
BestSkeleton::AllFieldsMatch(patterns, distance)
}
}
None => BestSkeleton::NoMatch,
}
}
struct FieldsByType {
pub date: Vec<Field>,
pub time: Vec<Field>,
}
fn group_fields_by_type(fields: &[Field]) -> FieldsByType {
let mut date = Vec::new();
let mut time = Vec::new();
for field in fields {
match field.symbol {
FieldSymbol::Era
| FieldSymbol::Year(_)
| FieldSymbol::Month(_)
| FieldSymbol::Week(_)
| FieldSymbol::Day(_)
| FieldSymbol::Weekday(_) => date.push(*field),
FieldSymbol::DayPeriod(_)
| FieldSymbol::Hour(_)
| FieldSymbol::Minute
| FieldSymbol::Second(_)
| FieldSymbol::TimeZone(_)
| FieldSymbol::DecimalSecond(_) => time.push(*field),
};
}
FieldsByType { date, time }
}
fn adjust_pattern_field_lengths(fields: &[Field], pattern: &mut runtime::Pattern) {
runtime::helpers::maybe_replace(pattern, |item| {
if let PatternItem::Field(pattern_field) = item {
if let Some(requested_field) = fields
.iter()
.find(|field| field.symbol.skeleton_cmp(pattern_field.symbol).is_eq())
{
if requested_field.length != pattern_field.length
&& requested_field.get_length_type() == pattern_field.get_length_type()
{
let length = requested_field.length;
let length = if requested_field.symbol.is_at_least_abbreviated() {
length.numeric_to_abbr()
} else {
length
};
return Some(PatternItem::Field(Field {
length,
..*pattern_field
}));
}
}
}
None
})
}
fn apply_subseconds(pattern: &mut runtime::Pattern, subseconds: Option<SubsecondDigits>) {
if let Some(subseconds) = subseconds {
let mut items = pattern.items.to_vec();
for item in items.iter_mut() {
if let PatternItem::Field(
ref mut field @ Field {
symbol:
FieldSymbol::Second(fields::Second::Second) | FieldSymbol::DecimalSecond(_),
..
},
) = item
{
field.symbol = FieldSymbol::from_subsecond_digits(subseconds);
};
}
*pattern = runtime::Pattern::from(items);
pattern
.metadata
.set_time_granularity(TimeGranularity::Nanoseconds);
}
}
pub fn get_best_available_format_pattern<'data>(
skeletons: &BTreeMap<reference::Skeleton, PluralElements<runtime::Pattern<'data>>>,
fields: &[Field],
prefer_matched_pattern: bool,
) -> BestSkeleton<PluralElements<runtime::Pattern<'data>>> {
let mut closest_format_pattern = None;
let mut closest_distance: u32 = u32::MAX;
let mut closest_missing_fields = 0;
for (skeleton, pattern) in skeletons.iter() {
debug_assert!(
skeleton.fields_len() <= MAX_SKELETON_FIELDS as usize,
"The distance mechanism assumes skeletons are less than MAX_SKELETON_FIELDS in length."
);
let mut missing_fields = 0;
let mut distance: u32 = 0;
let mut requested_fields = fields.iter().peekable();
let mut skeleton_fields = skeleton.fields_iter().peekable();
loop {
let next = (requested_fields.peek(), skeleton_fields.peek());
match next {
(Some(requested_field), Some(skeleton_field)) => {
debug_assert!(
skeleton_field.symbol != FieldSymbol::Month(fields::Month::StandAlone)
);
match skeleton_field.symbol.skeleton_cmp(requested_field.symbol) {
Ordering::Less => {
skeleton_fields.next();
distance += SKELETON_EXTRA_SYMBOL;
continue;
}
Ordering::Greater => {
distance += REQUESTED_SYMBOL_MISSING;
missing_fields += 1;
requested_fields.next();
continue;
}
_ => (),
}
distance += if requested_field == skeleton_field {
NO_DISTANCE
} else if requested_field.symbol != skeleton_field.symbol {
SUBSTANTIAL_DIFFERENCES_DISTANCE
} else if requested_field.get_length_type() != skeleton_field.get_length_type()
{
TEXT_VS_NUMERIC_DISTANCE
} else {
WIDTH_MISMATCH_DISTANCE
};
requested_fields.next();
skeleton_fields.next();
}
(None, Some(_)) => {
distance += SKELETON_EXTRA_SYMBOL;
skeleton_fields.next();
}
(Some(_), None) => {
distance += REQUESTED_SYMBOL_MISSING;
requested_fields.next();
missing_fields += 1;
}
(None, None) => {
break;
}
}
}
if distance < closest_distance {
closest_format_pattern = Some(pattern);
closest_distance = distance;
closest_missing_fields = missing_fields;
}
}
if !prefer_matched_pattern && closest_distance >= TEXT_VS_NUMERIC_DISTANCE {
if let [field] = fields {
return BestSkeleton::AllFieldsMatch(
PluralElements::new(runtime::Pattern::from(vec![PatternItem::Field(*field)])),
SkeletonQuality(closest_distance),
);
}
}
let mut closest_format_pattern = if let Some(pattern) = closest_format_pattern {
pattern.clone()
} else {
return BestSkeleton::NoMatch;
};
if closest_missing_fields == fields.len() {
return BestSkeleton::NoMatch;
}
if closest_distance == NO_DISTANCE {
return BestSkeleton::AllFieldsMatch(
closest_format_pattern,
SkeletonQuality(closest_distance),
);
}
#[allow(clippy::panic)] if prefer_matched_pattern {
#[cfg(not(feature = "datagen"))]
panic!("This code branch should only be run when transforming provider code.");
} else {
closest_format_pattern.for_each_mut(|pattern| {
adjust_pattern_field_lengths(fields, pattern);
});
}
if closest_distance >= SKELETON_EXTRA_SYMBOL {
return BestSkeleton::MissingOrExtraFields(
closest_format_pattern,
SkeletonQuality(closest_distance),
);
}
BestSkeleton::AllFieldsMatch(closest_format_pattern, SkeletonQuality(closest_distance))
}