use crate::cldr_serde::units::data::Patterns;
use crate::cldr_serde::units::info::Constant;
use crate::cldr_serde::units::preferences::{CategorizedUnitsList, Supplemental, UnitType};
use core::str::FromStr;
use icu::experimental::measure::measureunit::MeasureUnit;
use icu::experimental::measure::provider::single_unit::UnitID;
use icu::experimental::units::provider::{ConversionInfo, Exactness, Sign};
use icu::experimental::units::ratio::IcuRatio;
use icu::plurals::provider::PluralElementsPackedCow;
use icu::plurals::PluralElements;
use icu_pattern::SinglePlaceholderPattern;
use icu_provider::{DataError, DataErrorKind};
use num_traits::One;
use num_traits::Signed;
use std::collections::{BTreeMap, HashSet, VecDeque};
use zerovec::ZeroVec;
pub(crate) struct ScientificNumber {
pub(crate) clean_num: Vec<String>,
pub(crate) clean_den: Vec<String>,
pub(crate) exactness: Exactness,
}
#[derive(Debug)]
struct GeneralNonScientificNumber {
clean_num: Vec<String>,
clean_den: Vec<String>,
non_scientific_num: VecDeque<String>,
non_scientific_den: VecDeque<String>,
exactness: Exactness,
}
impl GeneralNonScientificNumber {
fn new(num: &[String], den: &[String], exactness: Exactness) -> Self {
let mut constant = GeneralNonScientificNumber {
clean_num: Vec::new(),
clean_den: Vec::new(),
non_scientific_num: VecDeque::new(),
non_scientific_den: VecDeque::new(),
exactness,
};
for n in num {
if is_scientific_number(n) {
constant.clean_num.push(n.clone());
} else {
constant.non_scientific_num.push_back(n.clone());
}
}
for d in den {
if is_scientific_number(d) {
constant.clean_den.push(d.clone());
} else {
constant.non_scientific_den.push_back(d.clone());
}
}
constant
}
fn is_free_of_non_scientific(&self) -> bool {
self.non_scientific_num.is_empty() && self.non_scientific_den.is_empty()
}
}
pub(crate) fn process_factor_part(
factor_part: &str,
cons_map: &BTreeMap<&str, ScientificNumber>,
) -> Result<ScientificNumber, DataError> {
if factor_part.contains('/') {
return Err(DataError::custom("the factor part is fractional number"));
}
let mut result = ScientificNumber {
clean_num: Vec::new(),
clean_den: Vec::new(),
exactness: Exactness::Exact,
};
let factor_parts = factor_part.split('*');
for factor in factor_parts {
if let Some(cons) = cons_map.get(factor.trim()) {
result.clean_num.extend(cons.clean_num.clone());
result.clean_den.extend(cons.clean_den.clone());
if cons.exactness == Exactness::Approximate {
result.exactness = Exactness::Approximate;
}
} else {
result.clean_num.push(factor.trim().to_string());
}
}
Ok(result)
}
pub(crate) fn process_factor(
factor: &str,
cons_map: &BTreeMap<&str, ScientificNumber>,
) -> Result<ScientificNumber, DataError> {
let mut factor_parts = factor.split('/');
let factor_num_str = factor_parts.next().unwrap_or("0").trim();
let factor_den_str = factor_parts.next().unwrap_or("1").trim();
if factor_parts.next().is_some() {
return Err(DataError::custom(
"the factor is not a valid scientific notation number",
));
}
let mut result = process_factor_part(factor_num_str, cons_map)?;
let factor_den_scientific = process_factor_part(factor_den_str, cons_map)?;
result.clean_num.extend(factor_den_scientific.clean_den);
result.clean_den.extend(factor_den_scientific.clean_num);
if factor_den_scientific.exactness == Exactness::Approximate {
result.exactness = Exactness::Approximate;
}
Ok(result)
}
pub(crate) fn extract_conversion_info<'data>(
unit_id: UnitID,
base_unit: &str,
factor: &ScientificNumber,
offset: &ScientificNumber,
) -> Result<ConversionInfo<'data>, DataError> {
let factor_fraction = convert_slices_to_fraction(&factor.clean_num, &factor.clean_den)?;
let offset_fraction = convert_slices_to_fraction(&offset.clean_num, &offset.clean_den)?;
let (factor_num, factor_den, factor_sign) = flatten_fraction(factor_fraction);
let (offset_num, offset_den, offset_sign) = flatten_fraction(offset_fraction);
let exactness = if factor.exactness == Exactness::Exact && offset.exactness == Exactness::Exact
{
Exactness::Exact
} else {
Exactness::Approximate
};
let base_unit = MeasureUnit::try_from_str(base_unit)
.map_err(|_| DataError::custom("the base unit is not valid"))?;
Ok(ConversionInfo {
unit_id,
basic_units: ZeroVec::from_iter(base_unit.single_units().iter().copied()),
factor_num: factor_num.into(),
factor_den: factor_den.into(),
factor_sign,
offset_num: offset_num.into(),
offset_den: offset_den.into(),
offset_sign,
exactness,
})
}
pub(crate) fn process_constants<'a>(
constants: &'a BTreeMap<String, Constant>,
) -> Result<BTreeMap<&'a str, ScientificNumber>, DataError> {
let mut constants_with_non_scientific =
VecDeque::<(&'a str, GeneralNonScientificNumber)>::new();
let mut clean_constants_map = BTreeMap::<&str, GeneralNonScientificNumber>::new();
for (cons_name, cons_value) in constants {
let (num, den) = split_unit_term(&cons_value.value)?;
let exactness = match cons_value.status.as_deref() {
Some("approximate") => Exactness::Approximate,
_ => Exactness::Exact,
};
let constant = GeneralNonScientificNumber::new(&num, &den, exactness);
if constant.is_free_of_non_scientific() {
clean_constants_map.insert(cons_name, constant);
} else {
constants_with_non_scientific.push_back((cons_name, constant));
}
}
let mut no_update_count = 0;
while !constants_with_non_scientific.is_empty() {
let mut updated = false;
let (constant_key, mut non_scientific_constant) = constants_with_non_scientific
.pop_front()
.ok_or(DataError::custom(
"non scientific queue error: an element must exist",
))?;
for _ in 0..non_scientific_constant.non_scientific_num.len() {
if let Some(num) = non_scientific_constant.non_scientific_num.pop_front() {
if let Some(clean_constant) = clean_constants_map.get(num.as_str()) {
non_scientific_constant
.clean_num
.extend(clean_constant.clean_num.clone());
non_scientific_constant
.clean_den
.extend(clean_constant.clean_den.clone());
updated = true;
} else {
non_scientific_constant.non_scientific_num.push_back(num);
}
}
}
for _ in 0..non_scientific_constant.non_scientific_den.len() {
if let Some(den) = non_scientific_constant.non_scientific_den.pop_front() {
if let Some(clean_constant) = clean_constants_map.get(den.as_str()) {
non_scientific_constant
.clean_num
.extend(clean_constant.clean_den.clone());
non_scientific_constant
.clean_den
.extend(clean_constant.clean_num.clone());
updated = true;
} else {
non_scientific_constant.non_scientific_den.push_back(den);
}
}
}
if non_scientific_constant.is_free_of_non_scientific() {
clean_constants_map.insert(constant_key, non_scientific_constant);
} else {
constants_with_non_scientific.push_back((constant_key, non_scientific_constant));
}
no_update_count = if !updated { no_update_count + 1 } else { 0 };
if no_update_count > constants_with_non_scientific.len() {
return Err(DataError::custom(
"A loop was detected in the CLDR constants data!",
));
}
}
Ok(clean_constants_map
.into_iter()
.map(|(k, v)| {
(k, {
ScientificNumber {
clean_num: v.clean_num,
clean_den: v.clean_den,
exactness: v.exactness,
}
})
})
.collect())
}
pub(crate) fn contains_alphabetic_chars(s: &str) -> bool {
s.chars().any(char::is_alphabetic)
}
#[test]
fn test_contains_alphabetic_chars() {
let input = "1";
let expected = false;
let actual = contains_alphabetic_chars(input);
assert_eq!(expected, actual);
let input = "ft_to_m";
let expected = true;
let actual = contains_alphabetic_chars(input);
assert_eq!(expected, actual);
let input = "1E2";
let expected = true;
let actual = contains_alphabetic_chars(input);
assert_eq!(expected, actual);
let input = "1.5E-2";
let expected = true;
let actual = contains_alphabetic_chars(input);
assert_eq!(expected, actual);
}
pub(crate) fn is_scientific_number(s: &str) -> bool {
let mut parts = s.split('E');
let base = parts.next().unwrap_or("0");
let exponent = parts.next().unwrap_or("0");
if parts.next().is_some() {
return false;
}
!contains_alphabetic_chars(base) && !contains_alphabetic_chars(exponent)
}
pub(crate) fn flatten_fraction(fraction: IcuRatio) -> (Vec<u8>, Vec<u8>, Sign) {
let fraction = fraction.get_ratio();
let numer_bytes = fraction.numer().to_bytes_le().1;
let denom_bytes = fraction.denom().to_bytes_le().1;
let sign = match fraction.is_negative() {
true => Sign::Negative,
false => Sign::Positive,
};
(numer_bytes, denom_bytes, sign)
}
pub(crate) fn convert_slices_to_fraction(
numerator_strings: &[String],
denominator_strings: &[String],
) -> Result<IcuRatio, DataError> {
numerator_strings
.iter()
.try_fold(IcuRatio::one(), |result, num| {
IcuRatio::from_str(num.as_str())
.map_err(|_| {
DataError::custom("The numerator is not a valid scientific notation number")
})
.map(|num_fraction| result * num_fraction)
})
.and_then(|num_product| {
denominator_strings
.iter()
.try_fold(num_product, |result, den| {
IcuRatio::from_str(den.as_str())
.map_err(|_| {
DataError::custom(
"The denominator is not a valid scientific notation number",
)
})
.map(|den_fraction| result / den_fraction)
})
})
}
#[test]
fn test_convert_array_of_strings_to_fraction() {
use num_bigint::BigInt;
let numerator = vec!["1".to_string()];
let denominator = vec!["2".to_string()];
let expected = IcuRatio::from_big_ints(BigInt::from(1i32), BigInt::from(2i32));
let actual = convert_slices_to_fraction(&numerator, &denominator).unwrap();
assert_eq!(expected, actual);
let numerator = vec!["1".to_string(), "2".to_string()];
let denominator = vec!["3".to_string(), "1E2".to_string()];
let expected = IcuRatio::from_big_ints(BigInt::from(2i32), BigInt::from(300i32));
let actual = convert_slices_to_fraction(&numerator, &denominator).unwrap();
assert_eq!(expected, actual);
let numerator = vec!["1".to_string(), "2".to_string()];
let denominator = vec!["3".to_string(), "1E-2".to_string()];
let expected = IcuRatio::from_big_ints(BigInt::from(200i32), BigInt::from(3i32));
let actual = convert_slices_to_fraction(&numerator, &denominator).unwrap();
assert_eq!(expected, actual);
let numerator = vec!["1".to_string(), "2".to_string()];
let denominator = vec!["3".to_string(), "1E-2.5".to_string()];
let actual = convert_slices_to_fraction(&numerator, &denominator);
assert!(actual.is_err());
let numerator = vec!["1E2".to_string()];
let denominator = vec!["2".to_string()];
let expected = IcuRatio::from_big_ints(BigInt::from(50i32), BigInt::from(1i32));
let actual = convert_slices_to_fraction(&numerator, &denominator).unwrap();
assert_eq!(expected, actual);
let numerator = vec!["1E2".to_string(), "2".to_string()];
let denominator = vec!["3".to_string(), "1E2".to_string()];
let expected = IcuRatio::from_big_ints(BigInt::from(2i32), BigInt::from(3i32));
let actual = convert_slices_to_fraction(&numerator, &denominator).unwrap();
assert_eq!(expected, actual);
}
pub(crate) fn split_unit_term(
constant_string: &str,
) -> Result<(Vec<String>, Vec<String>), DataError> {
let split: Vec<&str> = constant_string.split('/').collect();
if split.len() > 2 {
return Err(DataError::custom("Invalid constant string"));
}
let process_string = |s: &str| -> Vec<String> {
if s.is_empty() {
vec!["1".to_string()]
} else {
s.split('*').map(|s| s.trim().to_string()).collect()
}
};
let numerator_values = process_string(split.first().unwrap_or(&"1"));
let denominator_values = process_string(split.get(1).unwrap_or(&"1"));
if numerator_values
.iter()
.any(|s| s.chars().any(char::is_whitespace))
|| denominator_values
.iter()
.any(|s| s.chars().any(char::is_whitespace))
{
return Err(DataError::custom(
"The constant string contains internal white spaces",
));
}
Ok((numerator_values, denominator_values))
}
#[test]
fn test_convert_constant_to_num_denom_strings() {
let input = "1/2";
let expected = (vec!["1".to_string()], vec!["2".to_string()]);
let actual = split_unit_term(input).unwrap();
assert_eq!(expected, actual);
let input = "1 * 2 / 3 * ft_to_m";
let expected = (
vec!["1".to_string(), "2".to_string()],
vec!["3".to_string(), "ft_to_m".to_string()],
);
let actual = split_unit_term(input).unwrap();
assert_eq!(expected, actual);
let input = "/2";
let expected = (vec!["1".to_string()], vec!["2".to_string()]);
let actual = split_unit_term(input).unwrap();
assert_eq!(expected, actual);
let input = "2";
let expected = (vec!["2".to_string()], vec!["1".to_string()]);
let actual = split_unit_term(input).unwrap();
assert_eq!(expected, actual);
let input = "2/";
let expected = (vec!["2".to_string()], vec!["1".to_string()]);
let actual = split_unit_term(input).unwrap();
assert_eq!(expected, actual);
let input = "1E2";
let expected = (vec!["1E2".to_string()], vec!["1".to_string()]);
let actual = split_unit_term(input).unwrap();
assert_eq!(expected, actual);
let input = "1 2 * 3";
let actual = split_unit_term(input);
assert!(actual.is_err());
}
impl Patterns {
pub(crate) fn try_into_plural_elements_packed_cow(
&self,
) -> Result<PluralElementsPackedCow<'static, SinglePlaceholderPattern>, DataError> {
let other_pattern = self.other.as_deref().ok_or_else(|| {
DataErrorKind::IdentifierNotFound
.into_error()
.with_debug_context(self)
})?;
Ok(PluralElements::new(other_pattern)
.with_zero_value(self.zero.as_deref())
.with_one_value(self.one.as_deref())
.with_two_value(self.two.as_deref())
.with_few_value(self.few.as_deref())
.with_many_value(self.many.as_deref())
.with_explicit_one_value(self.explicit_one.as_deref())
.with_explicit_zero_value(self.explicit_zero.as_deref())
.into())
}
}
impl Supplemental {
fn unit_matches_any(target_unit: &str, units: &HashSet<String>) -> bool {
units.iter().any(|u| u.contains(target_unit))
}
pub(crate) fn categorized_units_list(&self) -> CategorizedUnitsList {
let mut result = BTreeMap::new();
for (category, usage_prefs) in &self.unit_preference_data {
let mut region_units_map = BTreeMap::new();
for region_prefs in usage_prefs.values() {
for (region_str, unit_prefs) in region_prefs {
let units_set = region_units_map
.entry(region_str.clone())
.or_insert_with(HashSet::new);
for unit_pref in unit_prefs {
units_set.insert(unit_pref.unit.clone());
}
}
}
result.insert(category.clone(), region_units_map);
}
result
}
pub(crate) fn unit_type(
category: &str,
unit: &str,
region: &str,
categorized_units_list: &CategorizedUnitsList,
) -> UnitType {
let region_units = match categorized_units_list.get(category) {
Some(units) => units,
None => return UnitType::Outlier,
};
let found_in_region = region_units
.get(region)
.or_else(|| region_units.get("001"))
.is_some_and(|units| Self::unit_matches_any(unit, units));
let found_anywhere = region_units
.values()
.any(|units| Self::unit_matches_any(unit, units));
if found_in_region {
UnitType::Core
} else if found_anywhere {
UnitType::Extended
} else {
UnitType::Outlier
}
}
}