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}