use alloc::vec;
use alloc::vec::Vec;
use core::cmp::Ordering;
use crate::{
fields::{self, Field, FieldLength, FieldSymbol},
options::{components, length},
pattern::{
hour_cycle,
runtime::{self, PatternPlurals},
PatternItem, TimeGranularity,
},
provider::calendar::{patterns::GenericLengthPatternsV1, DateSkeletonPatternsV1},
};
const MAX_SKELETON_FIELDS: u32 = 10;
const NO_DISTANCE: u32 = 0;
const WIDTH_MISMATCH_DISTANCE: u32 = 1;
const TEXT_VS_NUMERIC_DISTANCE: u32 = 10;
const SUBSTANTIAL_DIFFERENCES_DISTANCE: u32 = 100;
const SKELETON_EXTRA_SYMBOL: u32 = 1000;
const REQUESTED_SYMBOL_MISSING: u32 = 10000;
#[derive(Debug, PartialEq, Clone)]
pub enum BestSkeleton<T> {
AllFieldsMatch(T),
MissingOrExtraFields(T),
NoMatch,
}
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(fields::Field {
symbol: fields::FieldSymbol::TimeZone(_),
length: _,
}) = item
{
Some(PatternItem::Field((*time_zone_name).into()))
} else {
None
}
});
}
}
pub fn create_best_pattern_for_fields<'data>(
skeletons: &DateSkeletonPatternsV1<'data>,
length_patterns: &GenericLengthPatternsV1<'data>,
fields: &[Field],
components: &components::Bag,
prefer_matched_pattern: bool,
) -> BestSkeleton<PatternPlurals<'data>> {
let first_pattern_match =
get_best_available_format_pattern(skeletons, fields, prefer_matched_pattern);
if let BestSkeleton::AllFieldsMatch(mut pattern_plurals) = first_pattern_match {
pattern_plurals.for_each_mut(|pattern| {
hour_cycle::naively_apply_preferences(pattern, &components.preferences);
naively_apply_time_zone_name(pattern, &components.time_zone_name);
});
return BestSkeleton::AllFieldsMatch(pattern_plurals);
}
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_plurals) => {
if date.is_empty() {
pattern_plurals.for_each_mut(|pattern| {
hour_cycle::naively_apply_preferences(pattern, &components.preferences);
naively_apply_time_zone_name(pattern, &components.time_zone_name);
append_fractional_seconds(pattern, &time);
});
}
BestSkeleton::MissingOrExtraFields(pattern_plurals)
}
BestSkeleton::NoMatch => BestSkeleton::NoMatch,
};
}
let (date_patterns, date_missing_or_extra): (Option<PatternPlurals<'data>>, bool) =
match get_best_available_format_pattern(skeletons, &date, prefer_matched_pattern) {
BestSkeleton::MissingOrExtraFields(fields) => (Some(fields), true),
BestSkeleton::AllFieldsMatch(fields) => (Some(fields), false),
BestSkeleton::NoMatch => (None, true),
};
let (time_patterns, time_missing_or_extra): (Option<PatternPlurals<'data>>, bool) =
match get_best_available_format_pattern(skeletons, &time, prefer_matched_pattern) {
BestSkeleton::MissingOrExtraFields(fields) => (Some(fields), true),
BestSkeleton::AllFieldsMatch(fields) => (Some(fields), false),
BestSkeleton::NoMatch => (None, true),
};
let time_pattern: Option<runtime::Pattern<'data>> = time_patterns.map(|pattern_plurals| {
let mut pattern =
pattern_plurals.expect_pattern("Only date patterns can contain plural variants");
hour_cycle::naively_apply_preferences(&mut pattern, &components.preferences);
naively_apply_time_zone_name(&mut pattern, &components.time_zone_name);
append_fractional_seconds(&mut pattern, &time);
pattern
});
let patterns: Option<PatternPlurals<'data>> = 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::Wide => {
let weekday = fields
.iter()
.find(|f| matches!(f.symbol, FieldSymbol::Weekday(_)));
if weekday.is_some() {
length::Date::Full
} else {
length::Date::Long
}
}
FieldLength::Abbreviated => length::Date::Medium,
_ => length::Date::Short,
},
None => length::Date::Short,
};
use crate::pattern::runtime::GenericPattern;
let dt_pattern: &GenericPattern<'data> = match length {
length::Date::Full => &length_patterns.full,
length::Date::Long => &length_patterns.long,
length::Date::Medium => &length_patterns.medium,
length::Date::Short => &length_patterns.short,
};
date_patterns.for_each_mut(|pattern| {
let date = pattern.clone();
let time = time_pattern.clone();
#[allow(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(pattern.into()),
(Some(patterns), None) => Some(patterns),
(None, None) => None,
};
match patterns {
Some(patterns) => {
if date_missing_or_extra || time_missing_or_extra {
BestSkeleton::MissingOrExtraFields(patterns)
} else {
BestSkeleton::AllFieldsMatch(patterns)
}
}
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(_) => 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.discriminant_cmp(&pattern_field.symbol).is_eq())
{
if requested_field.length != pattern_field.length
&& requested_field.get_length_type() == pattern_field.get_length_type()
{
return Some(PatternItem::Field(*requested_field));
}
}
}
None
})
}
fn append_fractional_seconds(pattern: &mut runtime::Pattern, fields: &[Field]) {
if let Some(requested_field) = fields
.iter()
.find(|field| field.symbol == FieldSymbol::Second(fields::Second::FractionalSecond))
{
let mut items = pattern.items.to_vec();
if let Some(pos) = items.iter().position(|&item| match item {
PatternItem::Field(field) => {
matches!(field.symbol, FieldSymbol::Second(fields::Second::Second))
}
_ => false,
}) {
if let FieldLength::Fixed(p) = requested_field.length {
if p > 0 {
items.insert(pos + 1, PatternItem::Field(*requested_field));
}
}
}
*pattern = runtime::Pattern::from(items);
pattern.time_granularity = TimeGranularity::Nanoseconds;
}
}
pub fn get_best_available_format_pattern<'data>(
skeletons: &DateSkeletonPatternsV1<'data>,
fields: &[Field],
prefer_matched_pattern: bool,
) -> BestSkeleton<PatternPlurals<'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.0.iter() {
debug_assert!(
skeleton.0.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.0.fields_iter().peekable();
let mut matched_seconds = false;
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
.discriminant_cmp(&requested_field.symbol)
{
Ordering::Less => {
skeleton_fields.next();
distance += SKELETON_EXTRA_SYMBOL;
continue;
}
Ordering::Greater => {
if !(matched_seconds
&& requested_field.symbol
== FieldSymbol::Second(fields::Second::FractionalSecond))
{
distance += REQUESTED_SYMBOL_MISSING;
missing_fields += 1;
requested_fields.next();
continue;
}
}
_ => (),
}
if requested_field.symbol
== FieldSymbol::Second(fields::Second::FractionalSecond)
&& skeleton_field.symbol
== FieldSymbol::Second(fields::Second::FractionalSecond)
{
matched_seconds = true;
}
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(
runtime::Pattern::from(vec![PatternItem::Field(*field)]).into(),
);
}
}
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);
}
#[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);
}
BestSkeleton::AllFieldsMatch(closest_format_pattern)
}