use super::Cow;
use core::fmt::Write;
use writeable::Part;
use crate::grouper;
use crate::input::Decimal;
use crate::options::*;
use crate::parts;
use crate::provider::*;
use crate::size_test_macro::size_test;
use crate::DecimalFormatterPreferences;
#[cfg(feature = "alloc")]
use alloc::string::String;
use fixed_decimal::{Sign, UnsignedDecimal};
use icu_provider::prelude::*;
use writeable::PartsWrite;
use writeable::Writeable;
size_test!(DecimalFormatter, decimal_formatter_size, 96);
#[doc = decimal_formatter_size!()]
#[derive(Debug, Clone)]
pub struct DecimalFormatter {
options: DecimalFormatterOptions,
symbols: DataPayload<DecimalSymbolsV1>,
digits: DataPayload<DecimalDigitsV1>,
}
impl AsRef<DecimalFormatter> for DecimalFormatter {
fn as_ref(&self) -> &DecimalFormatter {
self
}
}
impl DecimalFormatter {
icu_provider::gen_buffer_data_constructors!(
(prefs: DecimalFormatterPreferences, options: DecimalFormatterOptions) -> error: DataError,
);
#[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable<
D: DataProvider<DecimalSymbolsV1> + DataProvider<DecimalDigitsV1> + ?Sized,
>(
provider: &D,
prefs: DecimalFormatterPreferences,
options: DecimalFormatterOptions,
) -> Result<Self, DataError> {
let locale = DecimalSymbolsV1::make_locale(prefs.locale_preferences);
let symbols = load_with_fallback::<DecimalSymbolsV1>(
provider,
prefs
.nu_id(&locale)
.into_iter()
.chain([DataIdentifierBorrowed::for_locale(&locale)]),
)?
.payload;
let resolved_nu_id = DataIdentifierBorrowed::for_marker_attributes(
DataMarkerAttributes::from_str_or_panic(symbols.get().numsys()),
);
let digits = load_with_fallback(
provider,
prefs.nu_id(&locale).into_iter().chain([resolved_nu_id]),
)?
.payload;
Ok(Self {
options,
symbols,
digits,
})
}
pub fn format<'l>(&'l self, value: &'l Decimal) -> FormattedDecimal<'l> {
FormattedDecimal(self.format_sign(
value.sign,
self.format_unsigned(Cow::Borrowed(&value.absolute)),
))
}
#[doc(hidden)] pub fn format_unsigned<'l>(
&'l self,
value: Cow<'l, UnsignedDecimal>,
) -> FormattedUnsignedDecimal<'l> {
FormattedUnsignedDecimal {
value,
options: &self.options,
symbols: self.symbols.get(),
digits: self.digits.get(),
}
}
#[doc(hidden)] pub fn format_sign<'l, T>(&'l self, sign: Sign, value: T) -> FormattedSign<'l, T> {
FormattedSign {
sign: match sign {
Sign::None => None,
Sign::Negative => Some((
parts::MINUS_SIGN,
self.symbols.get().strings.minus_sign_prefix(),
self.symbols.get().strings.minus_sign_suffix(),
)),
Sign::Positive => Some((
parts::PLUS_SIGN,
self.symbols.get().strings.plus_sign_prefix(),
self.symbols.get().strings.plus_sign_suffix(),
)),
},
value,
}
}
#[cfg(feature = "alloc")]
pub fn format_to_string(&self, value: &Decimal) -> String {
use writeable::Writeable;
self.format(value).write_to_string().into_owned()
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FormattedDecimal<'l>(pub(crate) FormattedSign<'l, FormattedUnsignedDecimal<'l>>);
#[doc(hidden)] #[derive(Debug, PartialEq, Clone)]
pub struct FormattedUnsignedDecimal<'l> {
pub(crate) value: Cow<'l, UnsignedDecimal>,
pub(crate) options: &'l DecimalFormatterOptions,
pub(crate) symbols: &'l DecimalSymbols<'l>,
pub(crate) digits: &'l [char; 10],
}
#[doc(hidden)] #[derive(Debug, PartialEq, Clone)]
pub struct FormattedSign<'l, T> {
pub(crate) value: T,
pub(crate) sign: Option<(Part, &'l str, &'l str)>,
}
impl<T: Writeable> Writeable for FormattedSign<'_, T> {
fn write_to_parts<S: PartsWrite + ?Sized>(&self, sink: &mut S) -> core::fmt::Result {
if let Some((part, prefix, _)) = self.sign {
sink.with_part(part, |w| w.write_str(prefix))?;
}
self.value.write_to_parts(sink)?;
if let Some((part, _, suffix)) = self.sign {
sink.with_part(part, |w| w.write_str(suffix))?;
}
Ok(())
}
}
impl Writeable for FormattedUnsignedDecimal<'_> {
fn write_to_parts<W>(&self, w: &mut W) -> Result<(), core::fmt::Error>
where
W: PartsWrite + ?Sized,
{
let range = self.value.magnitude_range();
let upper_magnitude = *range.end();
let mut range = range.rev().peekable();
w.with_part(parts::INTEGER, |w| {
while let Some(m) = range.next_if(|&m| m >= 0) {
#[expect(clippy::indexing_slicing)] w.write_char(self.digits[self.value.digit_at(m) as usize])?;
if grouper::check(
upper_magnitude,
m,
self.options.grouping_strategy.unwrap_or_default(),
self.symbols.grouping_sizes,
) {
w.with_part(parts::GROUP, |w| {
w.write_str(self.symbols.grouping_separator())
})?;
}
}
Ok(())
})?;
if range.peek().is_some() {
w.with_part(parts::DECIMAL, |w| {
w.write_str(self.symbols.decimal_separator())
})?;
w.with_part(parts::FRACTION, |w| {
for m in range.by_ref() {
#[expect(clippy::indexing_slicing)] w.write_char(self.digits[self.value.digit_at(m) as usize])?;
}
Ok(())
})?;
}
Ok(())
}
}
impl Writeable for FormattedDecimal<'_> {
fn write_to_parts<W>(&self, sink: &mut W) -> Result<(), core::fmt::Error>
where
W: PartsWrite + ?Sized,
{
self.0.write_to_parts(sink)
}
}
writeable::impl_display_with_writeable!(FormattedDecimal<'_>, #[cfg(feature = "alloc")]);
writeable::impl_display_with_writeable!(FormattedUnsignedDecimal<'_>, #[cfg(feature = "alloc")]);
impl<T: Writeable> core::fmt::Display for FormattedSign<'_, T> {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
Writeable::write_to(&self, f)
}
}
#[cfg(feature = "alloc")]
impl<T: Writeable> FormattedSign<'_, T> {
#[allow(clippy::inherent_to_string_shadow_display)]
pub fn to_string(&self) -> String {
Writeable::write_to_string(self).into_owned()
}
}
#[test]
fn test_numbering_resolution_fallback() {
use icu_locale_core::locale;
fn test_locale(locale: icu_locale_core::Locale, expected_format: &str) {
let formatter =
DecimalFormatter::try_new((&locale).into(), Default::default()).expect("Must load");
let fd = 1234.into();
writeable::assert_writeable_eq!(
formatter.format(&fd),
expected_format,
"Correct format for {locale}"
);
}
test_locale(locale!("en"), "1,234");
test_locale(locale!("en-u-nu-arab"), "١,٢٣٤");
test_locale(locale!("ar-EG"), "١٬٢٣٤");
test_locale(locale!("ar-EG-u-nu-latn"), "1,234");
test_locale(locale!("ar-EG-u-nu-thai"), "๑٬๒๓๔");
test_locale(locale!("en-u-nu-wxyz"), "1,234");
test_locale(locale!("ar-EG-u-nu-wxyz"), "١٬٢٣٤");
}
#[test]
pub fn test_es_mx() {
use icu_locale_core::locale;
use writeable::assert_writeable_eq;
let locale = locale!("es-MX").into();
let fmt = DecimalFormatter::try_new(locale, Default::default()).unwrap();
let fd = "12345.67".parse().unwrap();
assert_writeable_eq!(fmt.format(&fd), "12,345.67");
}