use core::fmt::Display;
use crate::input::Decimal;
use crate::Cow;
use crate::{
error::CompactExponentError,
options::CompactDecimalFormatterOptions,
preferences::{CompactDecimalFormatterPreferences, DecimalFormatterPreferences},
provider::*,
DecimalFormatter,
};
#[cfg(feature = "alloc")]
use alloc::string::String;
use fixed_decimal::UnsignedDecimal;
use icu_pattern::{Pattern, PatternBackend, SinglePlaceholder};
use icu_plurals::PluralRules;
use icu_provider::DataError;
use icu_provider::{marker::ErasedMarker, prelude::*};
use writeable::Writeable;
#[derive(Debug)]
pub struct CompactDecimalFormatter {
plural_rules: PluralRules,
decimal_formatter: DecimalFormatter,
compact_data:
DataPayload<ErasedMarker<<DecimalCompactLongV1 as DynamicDataMarker>::DataStruct>>,
}
impl CompactDecimalFormatter {
#[cfg(feature = "compiled_data")]
pub fn try_new_short(
prefs: CompactDecimalFormatterPreferences,
options: CompactDecimalFormatterOptions,
) -> Result<Self, DataError> {
let locale = DecimalCompactShortV1::make_locale(prefs.locale_preferences);
Ok(Self {
decimal_formatter: DecimalFormatter::try_new((&prefs).into(), options.into())?,
plural_rules: PluralRules::try_new_cardinal((&prefs).into())?,
compact_data: load_with_fallback::<DecimalCompactShortV1>(
&Baked,
DecimalFormatterPreferences::from(&prefs)
.nu_id(&locale)
.into_iter()
.chain([DataIdentifierBorrowed::for_locale(&locale)]),
)?
.payload
.cast(),
})
}
icu_provider::gen_buffer_data_constructors!(
(prefs: CompactDecimalFormatterPreferences, options: CompactDecimalFormatterOptions) -> error: DataError,
functions: [
try_new_short: skip,
try_new_short_with_buffer_provider,
try_new_short_unstable,
Self,
]
);
#[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new_short)]
pub fn try_new_short_unstable<D>(
provider: &D,
prefs: CompactDecimalFormatterPreferences,
options: CompactDecimalFormatterOptions,
) -> Result<Self, DataError>
where
D: DataProvider<DecimalCompactShortV1>
+ DataProvider<DecimalSymbolsV1>
+ DataProvider<DecimalDigitsV1>
+ DataProvider<icu_plurals::provider::PluralsCardinalV1>
+ ?Sized,
{
let locale = DecimalCompactShortV1::make_locale(prefs.locale_preferences);
Ok(Self {
decimal_formatter: DecimalFormatter::try_new_unstable(
provider,
(&prefs).into(),
options.into(),
)?,
plural_rules: PluralRules::try_new_cardinal_unstable(provider, (&prefs).into())?,
compact_data: load_with_fallback::<DecimalCompactShortV1>(
provider,
DecimalFormatterPreferences::from(&prefs)
.nu_id(&locale)
.into_iter()
.chain([DataIdentifierBorrowed::for_locale(&locale)]),
)?
.payload
.cast(),
})
}
#[cfg(feature = "compiled_data")]
pub fn try_new_long(
prefs: CompactDecimalFormatterPreferences,
options: CompactDecimalFormatterOptions,
) -> Result<Self, DataError> {
let locale = DecimalCompactLongV1::make_locale(prefs.locale_preferences);
Ok(Self {
decimal_formatter: DecimalFormatter::try_new((&prefs).into(), options.into())?,
plural_rules: PluralRules::try_new_cardinal((&prefs).into())?,
compact_data: load_with_fallback::<DecimalCompactLongV1>(
&Baked,
DecimalFormatterPreferences::from(&prefs)
.nu_id(&locale)
.into_iter()
.chain([DataIdentifierBorrowed::for_locale(&locale)]),
)?
.payload
.cast(),
})
}
icu_provider::gen_buffer_data_constructors!(
(prefs: CompactDecimalFormatterPreferences, options: CompactDecimalFormatterOptions) -> error: DataError,
functions: [
try_new_long: skip,
try_new_long_with_buffer_provider,
try_new_long_unstable,
Self,
]
);
#[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new_long)]
pub fn try_new_long_unstable<D>(
provider: &D,
prefs: CompactDecimalFormatterPreferences,
options: CompactDecimalFormatterOptions,
) -> Result<Self, DataError>
where
D: DataProvider<DecimalCompactLongV1>
+ DataProvider<DecimalSymbolsV1>
+ DataProvider<DecimalDigitsV1>
+ DataProvider<icu_plurals::provider::PluralsCardinalV1>
+ ?Sized,
{
let locale = DecimalCompactLongV1::make_locale(prefs.locale_preferences);
Ok(Self {
decimal_formatter: DecimalFormatter::try_new_unstable(
provider,
(&prefs).into(),
options.into(),
)?,
plural_rules: PluralRules::try_new_cardinal_unstable(provider, (&prefs).into())?,
compact_data: load_with_fallback::<DecimalCompactLongV1>(
provider,
DecimalFormatterPreferences::from(&prefs)
.nu_id(&locale)
.into_iter()
.chain([DataIdentifierBorrowed::for_locale(&locale)]),
)?
.payload
.cast(),
})
}
pub fn format(&self, value: &Decimal) -> impl Writeable + Display + '_ {
let (compact_pattern, significand) = self
.compact_data
.get()
.get_pattern_and_significand(&value.absolute, &self.plural_rules);
self.decimal_formatter.format_sign(
value.sign,
compact_pattern
.unwrap_or(Pattern::<SinglePlaceholder>::PASS_THROUGH)
.interpolate([self
.decimal_formatter
.format_unsigned(Cow::Owned(significand))]),
)
}
#[cfg(feature = "alloc")]
pub fn format_to_string(&self, value: &Decimal) -> String {
use writeable::Writeable;
self.format(value).write_to_string().into_owned()
}
pub fn format_with_exponent<'l>(
&'l self,
significand: &'l Decimal,
exponent: u8,
) -> Result<impl Writeable + Display + 'l, CompactExponentError> {
let log10_type = significand.absolute.nonzero_magnitude_start() + i16::from(exponent);
let (pattern, expected_exponent) = self
.compact_data
.get()
.0
.iter()
.filter(|&t| log10_type >= i16::from(t.sized))
.last()
.map(|t| {
(
t.variable
.get((&significand.absolute).into(), &self.plural_rules)
.1,
t.sized - t.variable.get_default().0.get(),
)
})
.unwrap_or((Pattern::<SinglePlaceholder>::PASS_THROUGH, 0));
if exponent != expected_exponent {
return Err(CompactExponentError {
actual: exponent,
expected: expected_exponent,
magnitude: log10_type,
});
}
Ok(self.decimal_formatter.format_sign(
significand.sign,
pattern.interpolate([self
.decimal_formatter
.format_unsigned(Cow::Borrowed(&significand.absolute))]),
))
}
pub fn compact_exponent_for_magnitude(&self, magnitude: i16) -> u8 {
self.compact_data
.get()
.0
.iter()
.filter(|t| magnitude >= i16::from(t.sized))
.last()
.map(|t| t.sized - t.variable.get_default().0.get())
.unwrap_or_default()
}
}
impl<'a, P: PatternBackend> CompactPatterns<'a, P> {
pub fn get_pattern_and_significand(
&'a self,
value: &UnsignedDecimal,
rules: &PluralRules,
) -> (Option<&'a Pattern<P>>, UnsignedDecimal) {
let log10_type = value.nonzero_magnitude_start();
let entry = self
.0
.iter()
.enumerate()
.filter(|&(_, t)| i16::from(t.sized) <= log10_type)
.last();
let exponent = entry
.map(|(_, t)| t.sized - t.variable.get_default().0.get())
.unwrap_or_default();
let rounding_magnitude = if log10_type > i16::from(exponent) {
i16::from(exponent)
} else {
log10_type - 1
};
if let Some(t) = self
.0
.get(entry.map(|(idx, _)| idx + 1).unwrap_or_default())
{
let next_exponent = t.sized - t.variable.get_default().0.get();
let rounds_to_next_exponent = log10_type + 1 == i16::from(next_exponent)
&& value.digit_at(rounding_magnitude - 1) >= 5
&& (rounding_magnitude..=log10_type).all(|m| value.digit_at(m) == 9);
if rounds_to_next_exponent {
return (
Some(t.variable.get(1.into(), rules).1),
UnsignedDecimal::ONE,
);
}
}
let significand = value
.clone()
.rounded(rounding_magnitude)
.multiplied_pow10(-i16::from(exponent))
.trimmed_end();
(
entry.map(|(_, t)| t.variable.get((&significand).into(), rules).1),
significand,
)
}
}
#[cfg(feature = "serde")]
#[cfg(test)]
mod tests {
use super::*;
use crate::options::GroupingStrategy;
use icu_locale_core::locale;
use writeable::assert_writeable_eq;
#[allow(non_snake_case)]
#[test]
fn test_grouping() {
#[derive(Debug)]
struct TestCase<'a> {
short: bool,
options: CompactDecimalFormatterOptions,
expected1T: &'a str,
expected10T: &'a str,
}
let cases = [
TestCase {
short: true,
options: Default::default(),
expected1T: "1000T",
expected10T: "10,000T",
},
TestCase {
short: true,
options: GroupingStrategy::Always.into(),
expected1T: "1,000T",
expected10T: "10,000T",
},
TestCase {
short: true,
options: GroupingStrategy::Never.into(),
expected1T: "1000T",
expected10T: "10000T",
},
TestCase {
short: false,
options: Default::default(),
expected1T: "1000 trillion",
expected10T: "10,000 trillion",
},
TestCase {
short: false,
options: GroupingStrategy::Always.into(),
expected1T: "1,000 trillion",
expected10T: "10,000 trillion",
},
TestCase {
short: false,
options: GroupingStrategy::Never.into(),
expected1T: "1000 trillion",
expected10T: "10000 trillion",
},
];
for case in cases {
let formatter = if case.short {
CompactDecimalFormatter::try_new_short(locale!("en").into(), case.options.clone())
} else {
CompactDecimalFormatter::try_new_long(locale!("en").into(), case.options.clone())
}
.unwrap();
let result1T = formatter.format(&1_000_000_000_000_000i64.into());
assert_writeable_eq!(result1T, case.expected1T, "{:?}", case);
let result10T = formatter.format(&10_000_000_000_000_000i64.into());
assert_writeable_eq!(result10T, case.expected10T, "{:?}", case);
}
}
#[test]
fn regression_7387() {
let formatter =
CompactDecimalFormatter::try_new_short(locale!("ar").into(), Default::default())
.unwrap();
assert_writeable_eq!(formatter.format(&3_000_000i64.into()), "3\u{a0}مليون");
}
#[test]
fn numbering_system() {
let default =
CompactDecimalFormatter::try_new_short(locale!("lo").into(), Default::default())
.unwrap();
let lao = CompactDecimalFormatter::try_new_short(
locale!("lo-u-nu-laoo").into(),
Default::default(),
)
.unwrap();
assert_writeable_eq!(default.format(&12345.into()), "12 ພັນ");
assert_writeable_eq!(lao.format(&12345.into()), "໑໒ພັນ");
}
}