icu_decimal/
lib.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! Formatting basic decimal numbers.
6//!
7//! This module is published as its own crate ([`icu_decimal`](https://docs.rs/icu_decimal/latest/icu_decimal/))
8//! and as part of the [`icu`](https://docs.rs/icu/latest/icu/) crate. See the latter for more details on the ICU4X project.
9//!
10//! Support for currencies, measurement units, and compact notation is planned. To track progress,
11//! follow [icu4x#275](https://github.com/unicode-org/icu4x/issues/275).
12//!
13//! # Examples
14//!
15//! ## Format a number with Bangla digits
16//!
17//! ```
18//! use icu::decimal::input::Decimal;
19//! use icu::decimal::DecimalFormatter;
20//! use icu::locale::locale;
21//! use writeable::assert_writeable_eq;
22//!
23//! let formatter =
24//!     DecimalFormatter::try_new(locale!("bn").into(), Default::default())
25//!         .expect("locale should be present");
26//!
27//! let decimal = Decimal::from(1000007);
28//!
29//! assert_writeable_eq!(formatter.format(&decimal), "১০,০০,০০৭");
30//! ```
31//!
32//! ## Format a number with digits after the decimal separator
33//!
34//! ```
35//! use icu::decimal::input::Decimal;
36//! use icu::decimal::DecimalFormatter;
37//! use icu::locale::Locale;
38//! use writeable::assert_writeable_eq;
39//!
40//! let formatter =
41//!     DecimalFormatter::try_new(Default::default(), Default::default())
42//!         .expect("locale should be present");
43//!
44//! let decimal = {
45//!     let mut decimal = Decimal::from(200050);
46//!     decimal.multiply_pow10(-2);
47//!     decimal
48//! };
49//!
50//! assert_writeable_eq!(formatter.format(&decimal), "2,000.50");
51//! ```
52//!
53//! ## Format a number using an alternative numbering system
54//!
55//! Numbering systems specified in the `-u-nu` subtag will be followed.
56//!
57//! ```
58//! use icu::decimal::input::Decimal;
59//! use icu::decimal::DecimalFormatter;
60//! use icu::locale::locale;
61//! use writeable::assert_writeable_eq;
62//!
63//! let formatter = DecimalFormatter::try_new(
64//!     locale!("th-u-nu-thai").into(),
65//!     Default::default(),
66//! )
67//! .expect("locale should be present");
68//!
69//! let decimal = Decimal::from(1000007);
70//!
71//! assert_writeable_eq!(formatter.format(&decimal), "๑,๐๐๐,๐๐๗");
72//! ```
73//!
74//! [`DecimalFormatter`]: DecimalFormatter
75
76// https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations
77#![cfg_attr(not(any(test, doc)), no_std)]
78#![cfg_attr(
79    not(test),
80    deny(
81        clippy::indexing_slicing,
82        clippy::unwrap_used,
83        clippy::expect_used,
84        clippy::panic,
85        clippy::exhaustive_structs,
86        clippy::exhaustive_enums,
87        clippy::trivially_copy_pass_by_ref,
88        missing_debug_implementations,
89    )
90)]
91#![warn(missing_docs)]
92
93#[cfg(feature = "alloc")]
94extern crate alloc;
95
96mod format;
97mod grouper;
98pub mod options;
99pub mod parts;
100pub mod provider;
101pub(crate) mod size_test_macro;
102
103pub use format::FormattedDecimal;
104
105use fixed_decimal::Decimal;
106use icu_locale_core::locale;
107use icu_locale_core::preferences::define_preferences;
108use icu_provider::prelude::*;
109use size_test_macro::size_test;
110
111size_test!(DecimalFormatter, decimal_formatter_size, 96);
112
113define_preferences!(
114    /// The preferences for fixed decimal formatting.
115    [Copy]
116    DecimalFormatterPreferences,
117    {
118        /// The user's preferred numbering system.
119        ///
120        /// Corresponds to the `-u-nu` in Unicode Locale Identifier.
121        ///
122        /// To get the resolved numbering system, you can inspect the data provider.
123        /// See the [`provider`] module for an example.
124        numbering_system: preferences::NumberingSystem
125    }
126);
127
128/// Locale preferences used by this crate
129pub mod preferences {
130    /// **This is a reexport of a type in [`icu::locale`](icu_locale_core::preferences::extensions::unicode::keywords)**.
131    #[doc = "\n"] // prevent autoformatting
132    pub use icu_locale_core::preferences::extensions::unicode::keywords::NumberingSystem;
133}
134
135/// Types that can be fed to [`DecimalFormatter`] and their utilities
136///
137/// This module contains re-exports from the [`fixed_decimal`] crate.
138pub mod input {
139    pub use fixed_decimal::Decimal;
140    #[cfg(feature = "ryu")]
141    pub use fixed_decimal::FloatPrecision;
142    pub use fixed_decimal::SignDisplay;
143}
144
145/// A formatter for [`Decimal`], rendering decimal digits in an i18n-friendly way.
146///
147/// [`DecimalFormatter`] supports:
148///
149/// 1. Rendering in the local numbering system
150/// 2. Locale-sensitive grouping separator positions
151/// 3. Locale-sensitive plus and minus signs
152///
153/// To get the resolved numbering system, see [`provider`].
154///
155/// See the crate-level documentation for examples.
156#[doc = decimal_formatter_size!()]
157#[derive(Debug, Clone)]
158pub struct DecimalFormatter {
159    options: options::DecimalFormatterOptions,
160    symbols: DataPayload<provider::DecimalSymbolsV1>,
161    digits: DataPayload<provider::DecimalDigitsV1>,
162}
163
164impl AsRef<DecimalFormatter> for DecimalFormatter {
165    fn as_ref(&self) -> &DecimalFormatter {
166        self
167    }
168}
169
170impl DecimalFormatter {
171    icu_provider::gen_buffer_data_constructors!(
172        (prefs: DecimalFormatterPreferences, options: options::DecimalFormatterOptions) -> error: DataError,
173        /// Creates a new [`DecimalFormatter`] from compiled data and an options bag.
174    );
175
176    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
177    pub fn try_new_unstable<
178        D: DataProvider<provider::DecimalSymbolsV1> + DataProvider<provider::DecimalDigitsV1> + ?Sized,
179    >(
180        provider: &D,
181        prefs: DecimalFormatterPreferences,
182        options: options::DecimalFormatterOptions,
183    ) -> Result<Self, DataError> {
184        let locale = provider::DecimalSymbolsV1::make_locale(prefs.locale_preferences);
185        let provided_nu = prefs.numbering_system.as_ref().map(|s| s.as_str());
186
187        // In case the user explicitly specified a numbering system, use digits from that numbering system. In case of explicitly specified numbering systems,
188        // the resolved one may end up being different due to a lack of data or fallback, e.g. attempting to resolve en-u-nu-thai will likely produce en-u-nu-Latn data.
189        //
190        // This correctly handles the following cases:
191        // - Explicitly specified numbering system that is the same as the resolved numbering system: This code effects no change
192        // - Explicitly specified numbering system that is different from the resolved one: This code overrides it, but the symbols are still correctly loaded for the locale
193        // - No explicitly specified numbering system: The default numbering system for the locale is used.
194        // - Explicitly specified numbering system without data for it: this falls back to the resolved numbering system
195        //
196        // Assuming the provider has symbols for en-u-nu-latn, th-u-nu-thai (default for th), and th-u-nu-latin, this produces the following behavior:
197        //
198        // | Input Locale | Symbols | Digits | Return value of `numbering_system()` |
199        // |--------------|---------|--------|--------------------------------------|
200        // | en           | latn    | latn   | latn                                 |
201        // | en-u-nu-thai | latn    | thai   | thai                                 |
202        // | th           | thai    | thai   | thai                                 |
203        // | th-u-nu-latn | latn    | latn   | latn                                 |
204        // | en-u-nu-wxyz | latn    | latn   | latn                                 |
205        // | th-u-nu-wxyz | thai    | thai   | thai                                 |
206
207        if let Some(provided_nu) = provided_nu {
208            // Load symbols for the locale/numsys pair provided
209            let symbols: DataPayload<provider::DecimalSymbolsV1> = provider
210                .load(DataRequest {
211                    id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
212                        DataMarkerAttributes::from_str_or_panic(provided_nu),
213                        &locale,
214                    ),
215                    ..Default::default()
216                })
217                // If it doesn't exist, fall back to the locale
218                .or_else(|_err| {
219                    provider.load(DataRequest {
220                        id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
221                            DataMarkerAttributes::empty(),
222                            &locale,
223                        ),
224                        ..Default::default()
225                    })
226                })?
227                .payload;
228
229            let resolved_nu = symbols.get().numsys();
230
231            // Attempt to load the provided numbering system first
232            let digits = provider
233                .load(DataRequest {
234                    id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
235                        DataMarkerAttributes::from_str_or_panic(provided_nu),
236                        &locale!("und").into(),
237                    ),
238                    ..Default::default()
239                })
240                .or_else(|_err| {
241                    provider.load(DataRequest {
242                        id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
243                            DataMarkerAttributes::from_str_or_panic(resolved_nu),
244                            &locale!("und").into(),
245                        ),
246                        ..Default::default()
247                    })
248                })?
249                .payload;
250            Ok(Self {
251                options,
252                symbols,
253                digits,
254            })
255        } else {
256            let symbols: DataPayload<provider::DecimalSymbolsV1> = provider
257                .load(DataRequest {
258                    id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
259                        DataMarkerAttributes::empty(),
260                        &locale,
261                    ),
262                    ..Default::default()
263                })?
264                .payload;
265
266            let resolved_nu = symbols.get().numsys();
267
268            let digits = provider
269                .load(DataRequest {
270                    id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
271                        DataMarkerAttributes::from_str_or_panic(resolved_nu),
272                        &locale!("und").into(),
273                    ),
274                    ..Default::default()
275                })?
276                .payload;
277            Ok(Self {
278                options,
279                symbols,
280                digits,
281            })
282        }
283    }
284
285    /// Formats a [`Decimal`], returning a [`FormattedDecimal`].
286    pub fn format<'l>(&'l self, value: &'l Decimal) -> FormattedDecimal<'l> {
287        FormattedDecimal {
288            value,
289            options: &self.options,
290            symbols: self.symbols.get(),
291            digits: self.digits.get(),
292        }
293    }
294
295    /// Formats a [`Decimal`], returning a [`String`].
296    #[cfg(feature = "alloc")]
297    pub fn format_to_string(&self, value: &Decimal) -> alloc::string::String {
298        use writeable::Writeable;
299        self.format(value).write_to_string().into_owned()
300    }
301}
302
303#[test]
304fn test_numbering_resolution_fallback() {
305    fn test_locale(locale: icu_locale_core::Locale, expected_format: &str) {
306        let formatter =
307            DecimalFormatter::try_new((&locale).into(), Default::default()).expect("Must load");
308        let fd = 1234.into();
309        writeable::assert_writeable_eq!(
310            formatter.format(&fd),
311            expected_format,
312            "Correct format for {locale}"
313        );
314    }
315
316    // Loading en with default latn numsys
317    test_locale(locale!("en"), "1,234");
318    // Loading en with arab numsys not present in symbols data will mix en symbols with arab digits
319    test_locale(locale!("en-u-nu-arab"), "١,٢٣٤");
320    // Loading ar-EG with default (arab) numsys
321    test_locale(locale!("ar-EG"), "١٬٢٣٤");
322    // Loading ar-EG with overridden latn numsys, present in symbols data, uses ar-EG-u-nu-latn symbols data
323    test_locale(locale!("ar-EG-u-nu-latn"), "1,234");
324    // Loading ar-EG with overridden thai numsys, not present in symbols data, uses ar-EG symbols data + thai digits
325    test_locale(locale!("ar-EG-u-nu-thai"), "๑٬๒๓๔");
326    // Loading with nonexistant numbering systems falls back to default
327    test_locale(locale!("en-u-nu-wxyz"), "1,234");
328    test_locale(locale!("ar-EG-u-nu-wxyz"), "١٬٢٣٤");
329}