use core::fmt::Display;
use crate::Cow;
use crate::input::Decimal;
use crate::{
DecimalFormatter,
error::CompactExponentError,
options::CompactDecimalFormatterOptions,
preferences::{CompactDecimalFormatterPreferences, DecimalFormatterPreferences},
provider::*,
};
#[cfg(feature = "alloc")]
use alloc::string::String;
use fixed_decimal::UnsignedDecimal;
use icu_pattern::{Pattern, SinglePlaceholder};
use icu_plurals::{PluralOperands, PluralRules};
use icu_provider::DataError;
use icu_provider::{marker::ErasedMarker, prelude::*};
use writeable::Writeable;
#[derive(Debug)]
pub struct CompactDecimalFormatter {
plural_rules: PluralRules,
pub(crate) 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<'a>(&'a self, value: &Decimal) -> impl Writeable + Display + 'a {
self.decimal_formatter
.format_sign(value.sign, self.format_unsigned(&value.absolute))
}
pub(crate) fn format_unsigned<'a>(
&'a self,
value: &UnsignedDecimal,
) -> FormattedUnsignedCompactDecimal<'a> {
let log10_type = value.nonzero_magnitude_start();
let entry = self
.compact_data
.get()
.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
.compact_data
.get()
.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 FormattedUnsignedCompactDecimal {
pattern: Some(t.variable.get(1.into(), &self.plural_rules).1),
significand: UnsignedDecimal::ONE,
decimal_formatter: &self.decimal_formatter,
exponent: next_exponent,
};
}
}
FormattedUnsignedCompactDecimal {
pattern: entry.map(|(_, t)| {
t.variable
.get(
(&value
.clone()
.rounded(rounding_magnitude)
.multiplied_pow10(-i16::from(exponent))
.trimmed_end())
.into(),
&self.plural_rules,
)
.1
}),
significand: value
.clone()
.rounded(rounding_magnitude)
.multiplied_pow10(-i16::from(exponent))
.trimmed_end(),
decimal_formatter: &self.decimal_formatter,
exponent,
}
}
#[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()
}
}
#[doc(hidden)] #[derive(Debug)]
pub struct FormattedUnsignedCompactDecimal<'l> {
pattern: Option<&'l Pattern<SinglePlaceholder>>,
significand: UnsignedDecimal,
decimal_formatter: &'l DecimalFormatter,
exponent: u8,
}
impl Writeable for FormattedUnsignedCompactDecimal<'_> {
fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
self.pattern
.unwrap_or(Pattern::<SinglePlaceholder>::PASS_THROUGH)
.interpolate([self
.decimal_formatter
.format_unsigned(Cow::Borrowed(&self.significand))])
.write_to(sink)
}
}
impl FormattedUnsignedCompactDecimal<'_> {
pub(crate) fn plural_operands(&self) -> PluralOperands {
PluralOperands::from_significand_and_exponent(&self.significand, self.exponent)
}
}
#[cfg(feature = "serde")]
#[cfg(test)]
mod tests {
use super::*;
use crate::options::GroupingStrategy;
use icu_locale_core::locale;
use icu_plurals::PluralElements;
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 plural() {
let decimal = DecimalFormatter::try_new(locale!("fr").into(), Default::default()).unwrap();
let decimal = decimal.format_unsigned(Cow::Owned(999_999u64.into()));
let compact =
CompactDecimalFormatter::try_new_short(locale!("fr").into(), Default::default())
.unwrap();
let compact = compact.format_unsigned(&999_999u64.into());
let dollars = PluralElements::new("dollars US")
.with_one_value(Some("dollar US"))
.with_many_value(Some("de dollars US"));
assert_eq!(
*dollars.get(
decimal.plural_operands(),
&PluralRules::try_new(locale!("fr").into(), Default::default()).unwrap()
),
"dollars US"
);
assert_eq!(
*dollars.get(
compact.plural_operands(),
&PluralRules::try_new(locale!("fr").into(), Default::default()).unwrap()
),
"de dollars US"
);
}
#[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()), "໑໒ພັນ");
}
}