1use super::Cow;
6use core::fmt::Write;
7#[cfg(feature = "unstable")]
8use icu_plurals::PluralOperands;
9use writeable::Part;
10
11use crate::DecimalFormatterPreferences;
12use crate::grouper;
13use crate::input::Decimal;
14use crate::options::*;
15use crate::parts;
16use crate::provider::*;
17use crate::size_test_macro::size_test;
18#[cfg(feature = "alloc")]
19use alloc::string::String;
20use fixed_decimal::{Sign, UnsignedDecimal};
21use icu_provider::prelude::*;
22use writeable::PartsWrite;
23use writeable::Writeable;
24
25size_test!(DecimalFormatter, decimal_formatter_size, 96);
26
27#[doc = decimal_formatter_size!()]
37#[derive(Debug, Clone)]
38pub struct DecimalFormatter {
39 options: DecimalFormatterOptions,
40 symbols: DataPayload<DecimalSymbolsV1>,
41 digits: DataPayload<DecimalDigitsV1>,
42}
43
44impl AsRef<DecimalFormatter> for DecimalFormatter {
45 fn as_ref(&self) -> &DecimalFormatter {
46 self
47 }
48}
49
50impl DecimalFormatter {
51 icu_provider::gen_buffer_data_constructors!(
52 (prefs: DecimalFormatterPreferences, options: DecimalFormatterOptions) -> error: DataError,
53 );
55
56 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
57 pub fn try_new_unstable<
58 D: DataProvider<DecimalSymbolsV1> + DataProvider<DecimalDigitsV1> + ?Sized,
59 >(
60 provider: &D,
61 prefs: DecimalFormatterPreferences,
62 options: DecimalFormatterOptions,
63 ) -> Result<Self, DataError> {
64 let locale = DecimalSymbolsV1::make_locale(prefs.locale_preferences);
85
86 let symbols = load_with_fallback::<DecimalSymbolsV1>(
88 provider,
89 prefs
91 .nu_id(&locale)
92 .into_iter()
93 .chain([DataIdentifierBorrowed::for_locale(&locale)]),
94 )?
95 .payload;
96
97 let resolved_nu_id = DataIdentifierBorrowed::for_marker_attributes(
98 DataMarkerAttributes::from_str_or_panic(symbols.get().numsys()),
99 );
100
101 let digits = load_with_fallback(
102 provider,
103 prefs.nu_id(&locale).into_iter().chain([resolved_nu_id]),
105 )?
106 .payload;
107
108 Ok(Self {
109 options,
110 symbols,
111 digits,
112 })
113 }
114
115 pub fn format<'l>(&'l self, value: &'l Decimal) -> FormattedDecimal<'l> {
117 FormattedDecimal(self.format_sign(
118 value.sign,
119 self.format_unsigned(Cow::Borrowed(&value.absolute)),
120 ))
121 }
122
123 pub(crate) fn format_unsigned<'l>(
124 &'l self,
125 value: Cow<'l, UnsignedDecimal>,
126 ) -> FormattedUnsignedDecimal<'l> {
127 FormattedUnsignedDecimal {
128 value,
129 options: &self.options,
130 symbols: self.symbols.get(),
131 digits: self.digits.get(),
132 }
133 }
134
135 pub(crate) fn format_sign<'l, T>(&'l self, sign: Sign, value: T) -> FormattedSign<'l, T> {
136 FormattedSign {
137 sign: match sign {
138 Sign::None => None,
139 Sign::Negative => Some((
140 parts::MINUS_SIGN,
141 self.symbols.get().strings.minus_sign_prefix(),
142 self.symbols.get().strings.minus_sign_suffix(),
143 )),
144 Sign::Positive => Some((
145 parts::PLUS_SIGN,
146 self.symbols.get().strings.plus_sign_prefix(),
147 self.symbols.get().strings.plus_sign_suffix(),
148 )),
149 },
150 value,
151 }
152 }
153
154 #[cfg(feature = "alloc")]
158 pub fn format_to_string(&self, value: &Decimal) -> String {
159 use writeable::Writeable;
160 self.format(value).write_to_string().into_owned()
161 }
162}
163
164#[derive(Debug, PartialEq, Clone)]
167pub struct FormattedDecimal<'l>(pub(crate) FormattedSign<'l, FormattedUnsignedDecimal<'l>>);
168
169#[doc(hidden)] #[derive(Debug, PartialEq, Clone)]
172pub struct FormattedUnsignedDecimal<'l> {
173 pub(crate) value: Cow<'l, UnsignedDecimal>,
174 pub(crate) options: &'l DecimalFormatterOptions,
175 pub(crate) symbols: &'l DecimalSymbols<'l>,
176 pub(crate) digits: &'l [char; 10],
177}
178
179impl FormattedUnsignedDecimal<'_> {
180 #[cfg(feature = "unstable")]
181 pub(crate) fn plural_operands(&self) -> PluralOperands {
182 PluralOperands::from_significand_and_exponent(&self.value, 0)
183 }
184}
185
186#[doc(hidden)] #[derive(Debug, PartialEq, Clone)]
189pub struct FormattedSign<'l, T> {
190 pub(crate) value: T,
191 pub(crate) sign: Option<(Part, &'l str, &'l str)>,
192}
193
194impl<T: Writeable> Writeable for FormattedSign<'_, T> {
195 fn write_to_parts<S: PartsWrite + ?Sized>(&self, sink: &mut S) -> core::fmt::Result {
196 if let Some((part, prefix, _)) = self.sign {
197 sink.with_part(part, |w| w.write_str(prefix))?;
198 }
199 self.value.write_to_parts(sink)?;
200 if let Some((part, _, suffix)) = self.sign {
201 sink.with_part(part, |w| w.write_str(suffix))?;
202 }
203 Ok(())
204 }
205}
206
207impl Writeable for FormattedUnsignedDecimal<'_> {
208 fn write_to_parts<W>(&self, w: &mut W) -> Result<(), core::fmt::Error>
209 where
210 W: PartsWrite + ?Sized,
211 {
212 let range = self.value.magnitude_range();
213 let upper_magnitude = *range.end();
214 let mut range = range.rev().peekable();
215 w.with_part(parts::INTEGER, |w| {
216 while let Some(m) = range.next_if(|&m| m >= 0) {
217 #[expect(clippy::indexing_slicing)] w.write_char(self.digits[self.value.digit_at(m) as usize])?;
219 if grouper::check(
220 upper_magnitude,
221 m,
222 self.options.grouping_strategy.unwrap_or_default(),
223 self.symbols.grouping_sizes,
224 ) {
225 w.with_part(parts::GROUP, |w| {
226 w.write_str(self.symbols.grouping_separator())
227 })?;
228 }
229 }
230 Ok(())
231 })?;
232 if range.peek().is_some() {
233 w.with_part(parts::DECIMAL, |w| {
234 w.write_str(self.symbols.decimal_separator())
235 })?;
236 w.with_part(parts::FRACTION, |w| {
237 for m in range.by_ref() {
238 #[expect(clippy::indexing_slicing)] w.write_char(self.digits[self.value.digit_at(m) as usize])?;
240 }
241 Ok(())
242 })?;
243 }
244 Ok(())
245 }
246}
247
248writeable::impl_writeable_delegate!(FormattedDecimal<'_>, |&self| &self.0, #[cfg(feature = "alloc")] fn write_to_string);
249writeable::impl_display_with_writeable!(FormattedDecimal<'_>, #[cfg(feature = "alloc")]);
250writeable::impl_display_with_writeable!(FormattedUnsignedDecimal<'_>, #[cfg(feature = "alloc")]);
251writeable::impl_display_with_writeable!(FormattedSign<'_, T>, #[cfg(feature = "alloc")], where T: Writeable);
252
253#[test]
254fn test_numbering_resolution_fallback() {
255 use icu_locale_core::locale;
256 fn test_locale(locale: icu_locale_core::Locale, expected_format: &str) {
257 let formatter =
258 DecimalFormatter::try_new((&locale).into(), Default::default()).expect("Must load");
259 let fd = 1234.into();
260 writeable::assert_writeable_eq!(
261 formatter.format(&fd),
262 expected_format,
263 "Correct format for {locale}"
264 );
265 }
266
267 test_locale(locale!("en"), "1,234");
269 test_locale(locale!("en-u-nu-arab"), "١,٢٣٤");
271 test_locale(locale!("ar-EG"), "١٬٢٣٤");
273 test_locale(locale!("ar-EG-u-nu-latn"), "1,234");
275 test_locale(locale!("ar-EG-u-nu-thai"), "๑٬๒๓๔");
277 test_locale(locale!("en-u-nu-wxyz"), "1,234");
279 test_locale(locale!("ar-EG-u-nu-wxyz"), "١٬٢٣٤");
280}
281
282#[test]
283pub fn test_es_mx() {
284 use icu_locale_core::locale;
285 use writeable::assert_writeable_eq;
286 let locale = locale!("es-MX").into();
287 let fmt = DecimalFormatter::try_new(locale, Default::default()).unwrap();
288 let fd = "12345.67".parse().unwrap();
289 assert_writeable_eq!(fmt.format(&fd), "12,345.67");
290}