Skip to main content

ferroui_base/utilities/
number_format_info.rs

1//! A counterpart of .NET's `System.Globalization.NumberFormatInfo`: the
2//! symbols, separators, digit counts and patterns used to format and parse
3//! numbers.
4//!
5//! A new instance carries the values of the invariant culture, the only
6//! culture whose data is built in. The conventions of every other culture
7//! come from the [`ICultureDataProvider`](super::ICultureDataProvider)
8//! registered with the locator; a
9//! culture the provider has nothing for (or any culture, without a
10//! provider) has the conventions of its nearest parent culture that has
11//! some, in the end those of the invariant culture.
12
13use super::i_culture_data_provider::find_culture_data;
14use super::CultureInfo;
15use std::cell::Cell;
16use std::rc::Rc;
17
18crate::ferro_thread_local! {
19    static INVARIANT: Rc<NumberFormatInfo> = Rc::new(NumberFormatInfo::new());
20}
21
22/// Culture-specific information for formatting and parsing numbers.
23///
24/// Used as `Rc<NumberFormatInfo>` where the managed runtime passes the
25/// object around; like every reference type without value equality it
26/// compares by identity.
27#[derive(Clone, Debug)]
28pub struct NumberFormatInfo {
29    currency_decimal_digits: Cell<i32>,
30    currency_decimal_separator: String,
31    currency_group_separator: String,
32    currency_group_sizes: Vec<i32>,
33    currency_negative_pattern: Cell<i32>,
34    currency_positive_pattern: Cell<i32>,
35    currency_symbol: String,
36    nan_symbol: String,
37    negative_infinity_symbol: String,
38    negative_sign: String,
39    number_decimal_digits: Cell<i32>,
40    number_decimal_separator: String,
41    number_group_separator: String,
42    number_group_sizes: Vec<i32>,
43    number_negative_pattern: Cell<i32>,
44    percent_decimal_digits: Cell<i32>,
45    percent_decimal_separator: String,
46    percent_group_separator: String,
47    percent_group_sizes: Vec<i32>,
48    percent_negative_pattern: Cell<i32>,
49    percent_positive_pattern: Cell<i32>,
50    percent_symbol: String,
51    per_mille_symbol: String,
52    positive_infinity_symbol: String,
53    positive_sign: String,
54}
55
56impl PartialEq for NumberFormatInfo {
57    #[inline]
58    fn eq(&self, other: &Self) -> bool {
59        std::ptr::eq(self, other)
60    }
61}
62
63impl Default for NumberFormatInfo {
64    #[inline]
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70macro_rules! text_properties {
71    ($($(#[$doc:meta])* $name:ident, $set:ident, $with:ident;)*) => {
72        impl NumberFormatInfo {
73            $(
74                $(#[$doc])*
75                #[inline]
76                pub fn $name(&self) -> &str {
77                    &self.$name
78                }
79
80                $(#[$doc])*
81                #[inline]
82                pub fn $set(&mut self, value: impl Into<String>) {
83                    self.$name = value.into();
84                }
85
86                $(#[$doc])*
87                #[inline]
88                pub fn $with(mut self, value: impl Into<String>) -> Self {
89                    self.$name = value.into();
90                    self
91                }
92            )*
93        }
94    };
95}
96
97macro_rules! number_properties {
98    ($($(#[$doc:meta])* $name:ident, $set:ident, $with:ident, $max:literal, $label:literal;)*) => {
99        impl NumberFormatInfo {
100            $(
101                $(#[$doc])*
102                #[inline]
103                pub fn $name(&self) -> i32 {
104                    self.$name.get()
105                }
106
107                $(#[$doc])*
108                ///
109                /// Panics when the value is out of range. Takes a shared
110                /// reference, as the instance is shared through `Rc` where the
111                /// original passes the object around (markup sets the property
112                /// on the instance it created).
113                pub fn $set(&self, value: i32) {
114                    assert!(
115                        (0..=$max).contains(&value),
116                        concat!($label, " must be between 0 and ", stringify!($max), ", inclusive.")
117                    );
118                    self.$name.set(value);
119                }
120
121                $(#[$doc])*
122                ///
123                /// Panics when the value is out of range.
124                #[inline]
125                pub fn $with(self, value: i32) -> Self {
126                    self.$set(value);
127                    self
128                }
129            )*
130        }
131    };
132}
133
134macro_rules! group_properties {
135    ($($(#[$doc:meta])* $name:ident, $set:ident, $with:ident;)*) => {
136        impl NumberFormatInfo {
137            $(
138                $(#[$doc])*
139                #[inline]
140                pub fn $name(&self) -> &[i32] {
141                    &self.$name
142                }
143
144                $(#[$doc])*
145                ///
146                /// Panics when a size is not between 1 and 9, except the
147                /// last, which may be 0 (the remaining digits are not
148                /// grouped).
149                pub fn $set(&mut self, value: &[i32]) {
150                    Self::check_group_sizes(value);
151                    self.$name = value.to_vec();
152                }
153
154                $(#[$doc])*
155                ///
156                /// Panics when a size is not valid.
157                #[inline]
158                pub fn $with(mut self, value: &[i32]) -> Self {
159                    self.$set(value);
160                    self
161                }
162            )*
163        }
164    };
165}
166
167text_properties! {
168    /// The decimal separator of currency values.
169    currency_decimal_separator, set_currency_decimal_separator, with_currency_decimal_separator;
170    /// The separator of the digit groups of currency values.
171    currency_group_separator, set_currency_group_separator, with_currency_group_separator;
172    /// The currency symbol.
173    currency_symbol, set_currency_symbol, with_currency_symbol;
174    /// The text of not-a-number.
175    nan_symbol, set_nan_symbol, with_nan_symbol;
176    /// The text of negative infinity.
177    negative_infinity_symbol, set_negative_infinity_symbol, with_negative_infinity_symbol;
178    /// The sign of negative numbers.
179    negative_sign, set_negative_sign, with_negative_sign;
180    /// The decimal separator of numbers.
181    number_decimal_separator, set_number_decimal_separator, with_number_decimal_separator;
182    /// The separator of the digit groups of numbers.
183    number_group_separator, set_number_group_separator, with_number_group_separator;
184    /// The decimal separator of percent values.
185    percent_decimal_separator, set_percent_decimal_separator, with_percent_decimal_separator;
186    /// The separator of the digit groups of percent values.
187    percent_group_separator, set_percent_group_separator, with_percent_group_separator;
188    /// The percent symbol.
189    percent_symbol, set_percent_symbol, with_percent_symbol;
190    /// The per mille symbol.
191    per_mille_symbol, set_per_mille_symbol, with_per_mille_symbol;
192    /// The text of positive infinity.
193    positive_infinity_symbol, set_positive_infinity_symbol, with_positive_infinity_symbol;
194    /// The sign of positive numbers.
195    positive_sign, set_positive_sign, with_positive_sign;
196}
197
198number_properties! {
199    /// The number of decimal places of currency values.
200    currency_decimal_digits, set_currency_decimal_digits, with_currency_decimal_digits, 99, "CurrencyDecimalDigits";
201    /// The pattern of negative currency values (0 is `($n)`, 1 is `-$n`).
202    currency_negative_pattern, set_currency_negative_pattern, with_currency_negative_pattern, 16,
203        "CurrencyNegativePattern";
204    /// The pattern of positive currency values (0 is `$n`, 1 is `n$`, 2 is
205    /// `$ n`, 3 is `n $`).
206    currency_positive_pattern, set_currency_positive_pattern, with_currency_positive_pattern, 3,
207        "CurrencyPositivePattern";
208    /// The number of decimal places of numbers.
209    number_decimal_digits, set_number_decimal_digits, with_number_decimal_digits, 99, "NumberDecimalDigits";
210    /// The pattern of negative numbers (0 is `(n)`, 1 is `-n`, 2 is `- n`,
211    /// 3 is `n-`, 4 is `n -`).
212    number_negative_pattern, set_number_negative_pattern, with_number_negative_pattern, 4, "NumberNegativePattern";
213    /// The number of decimal places of percent values.
214    percent_decimal_digits, set_percent_decimal_digits, with_percent_decimal_digits, 99, "PercentDecimalDigits";
215    /// The pattern of negative percent values (0 is `-n %`, 1 is `-n%`).
216    percent_negative_pattern, set_percent_negative_pattern, with_percent_negative_pattern, 11,
217        "PercentNegativePattern";
218    /// The pattern of positive percent values (0 is `n %`, 1 is `n%`, 2 is
219    /// `%n`, 3 is `% n`).
220    percent_positive_pattern, set_percent_positive_pattern, with_percent_positive_pattern, 3,
221        "PercentPositivePattern";
222}
223
224group_properties! {
225    /// The number of digits in each group of currency values, from the
226    /// decimal separator leftwards; the last size repeats.
227    currency_group_sizes, set_currency_group_sizes, with_currency_group_sizes;
228    /// The number of digits in each group of numbers, from the decimal
229    /// separator leftwards; the last size repeats.
230    number_group_sizes, set_number_group_sizes, with_number_group_sizes;
231    /// The number of digits in each group of percent values, from the
232    /// decimal separator leftwards; the last size repeats.
233    percent_group_sizes, set_percent_group_sizes, with_percent_group_sizes;
234}
235
236impl NumberFormatInfo {
237    /// Format information with the values of the invariant culture (C#
238    /// `new NumberFormatInfo()`).
239    pub fn new() -> NumberFormatInfo {
240        NumberFormatInfo {
241            currency_decimal_digits: Cell::new(2),
242            currency_decimal_separator: ".".to_string(),
243            currency_group_separator: ",".to_string(),
244            currency_group_sizes: vec![3],
245            currency_negative_pattern: Cell::new(0),
246            currency_positive_pattern: Cell::new(0),
247            currency_symbol: "\u{00A4}".to_string(),
248            nan_symbol: "NaN".to_string(),
249            negative_infinity_symbol: "-Infinity".to_string(),
250            negative_sign: "-".to_string(),
251            number_decimal_digits: Cell::new(2),
252            number_decimal_separator: ".".to_string(),
253            number_group_separator: ",".to_string(),
254            number_group_sizes: vec![3],
255            number_negative_pattern: Cell::new(1),
256            percent_decimal_digits: Cell::new(2),
257            percent_decimal_separator: ".".to_string(),
258            percent_group_separator: ",".to_string(),
259            percent_group_sizes: vec![3],
260            percent_negative_pattern: Cell::new(0),
261            percent_positive_pattern: Cell::new(0),
262            percent_symbol: "%".to_string(),
263            per_mille_symbol: "\u{2030}".to_string(),
264            positive_infinity_symbol: "Infinity".to_string(),
265            positive_sign: "+".to_string(),
266        }
267    }
268
269    /// The shared format information of a culture: that of the registered
270    /// provider for the culture or for its nearest parent culture the
271    /// provider has data for, else the invariant one.
272    pub(crate) fn for_culture(culture: &CultureInfo) -> Rc<NumberFormatInfo> {
273        find_culture_data(culture, |provider, name| provider.get_number_format(name))
274            .unwrap_or_else(Self::invariant_info)
275    }
276
277    /// The format information of the invariant culture.
278    pub fn invariant_info() -> Rc<NumberFormatInfo> {
279        INVARIANT.with(Clone::clone)
280    }
281
282    /// The format information of the current culture.
283    pub fn current_info() -> Rc<NumberFormatInfo> {
284        Self::for_culture(&CultureInfo::current_culture())
285    }
286
287    /// The given format information, or that of the current culture when
288    /// there is none (C# `GetInstance(IFormatProvider)`).
289    pub fn get_instance(provider: Option<&Rc<NumberFormatInfo>>) -> Rc<NumberFormatInfo> {
290        match provider {
291            Some(info) => info.clone(),
292            None => Self::current_info(),
293        }
294    }
295
296    fn check_group_sizes(sizes: &[i32]) {
297        for (index, size) in sizes.iter().enumerate() {
298            let valid = (1..=9).contains(size) || (*size == 0 && index + 1 == sizes.len());
299            assert!(
300                valid,
301                "Every element in the value array should be between one and nine, except for the last element, which can be zero."
302            );
303        }
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::utilities::TestCultureDataProvider;
311    use crate::FerroLocator;
312
313    #[test]
314    fn a_new_instance_has_the_invariant_values() {
315        let info = NumberFormatInfo::new();
316        assert_eq!(info.currency_decimal_digits(), 2);
317        assert_eq!(info.currency_decimal_separator(), ".");
318        assert_eq!(info.currency_group_separator(), ",");
319        assert_eq!(info.currency_group_sizes(), [3]);
320        assert_eq!(info.currency_negative_pattern(), 0);
321        assert_eq!(info.currency_positive_pattern(), 0);
322        assert_eq!(info.currency_symbol(), "\u{00A4}");
323        assert_eq!(info.nan_symbol(), "NaN");
324        assert_eq!(info.negative_infinity_symbol(), "-Infinity");
325        assert_eq!(info.negative_sign(), "-");
326        assert_eq!(info.number_decimal_digits(), 2);
327        assert_eq!(info.number_decimal_separator(), ".");
328        assert_eq!(info.number_group_separator(), ",");
329        assert_eq!(info.number_group_sizes(), [3]);
330        assert_eq!(info.number_negative_pattern(), 1);
331        assert_eq!(info.percent_decimal_digits(), 2);
332        assert_eq!(info.percent_decimal_separator(), ".");
333        assert_eq!(info.percent_group_separator(), ",");
334        assert_eq!(info.percent_group_sizes(), [3]);
335        assert_eq!(info.percent_negative_pattern(), 0);
336        assert_eq!(info.percent_positive_pattern(), 0);
337        assert_eq!(info.percent_symbol(), "%");
338        assert_eq!(info.per_mille_symbol(), "\u{2030}");
339        assert_eq!(info.positive_infinity_symbol(), "Infinity");
340        assert_eq!(info.positive_sign(), "+");
341    }
342
343    #[test]
344    fn properties_can_be_changed() {
345        let mut info = NumberFormatInfo::new().with_number_decimal_separator(";").with_number_group_sizes(&[3, 2, 0]);
346        info.set_currency_symbol("kr");
347        info.set_number_negative_pattern(0);
348        assert_eq!(info.number_decimal_separator(), ";");
349        assert_eq!(info.number_group_sizes(), [3, 2, 0]);
350        assert_eq!(info.currency_symbol(), "kr");
351        assert_eq!(info.number_negative_pattern(), 0);
352    }
353
354    #[test]
355    #[should_panic(expected = "NumberNegativePattern must be between 0 and 4, inclusive.")]
356    fn a_pattern_out_of_range_panics() {
357        NumberFormatInfo::new().set_number_negative_pattern(5);
358    }
359
360    #[test]
361    #[should_panic(expected = "between one and nine")]
362    fn a_group_size_out_of_range_panics() {
363        NumberFormatInfo::new().set_number_group_sizes(&[0, 3]);
364    }
365
366    #[test]
367    fn instances_compare_by_identity() {
368        let a = Rc::new(NumberFormatInfo::new());
369        let b = Rc::new(NumberFormatInfo::new());
370        assert!(a == a.clone());
371        assert!(a != b);
372        assert!(NumberFormatInfo::invariant_info() == NumberFormatInfo::invariant_info());
373    }
374
375    #[test]
376    fn cultures() {
377        let _scope = FerroLocator::enter_scope();
378
379        let invariant = CultureInfo::invariant_culture().number_format();
380        assert!(invariant == NumberFormatInfo::invariant_info());
381        assert_eq!(invariant.currency_symbol(), "\u{00A4}");
382
383        // Without a provider every culture has the invariant conventions.
384        assert!(CultureInfo::get_culture_info("en-US").number_format() == invariant);
385
386        TestCultureDataProvider::register();
387
388        let english = CultureInfo::get_culture_info("en-US").number_format();
389        assert_eq!(english.currency_symbol(), "$");
390        assert_eq!(english.currency_negative_pattern(), 1);
391        assert_eq!(english.percent_positive_pattern(), 1);
392        assert_eq!(english.percent_negative_pattern(), 1);
393        // Shared per culture.
394        assert!(english == CultureInfo::get_culture_info("EN-us").number_format());
395
396        // A culture the provider has no data for has the data of its parent.
397        assert!(CultureInfo::get_culture_info("en-AU").number_format() == CultureInfo::get_culture_info("en").number_format());
398        assert!(CultureInfo::get_culture_info("ja-JP").number_format() == invariant);
399        assert!(CultureInfo::invariant_culture().number_format() == invariant);
400
401        // The current culture is the invariant culture until it is set.
402        assert_eq!(NumberFormatInfo::current_info().currency_symbol(), "\u{00A4}");
403        let given = Rc::new(NumberFormatInfo::new());
404        assert!(NumberFormatInfo::get_instance(Some(&given)) == given);
405        assert!(NumberFormatInfo::get_instance(None) == NumberFormatInfo::current_info());
406    }
407}