monies/
format.rs

1use crate::currency::FormattableCurrency;
2use crate::{Money, Round};
3use std::cmp::Ordering;
4
5/// Converts Money objects into human readable strings.
6pub struct Formatter;
7
8impl<'a> Formatter {
9    /// Returns a formatted Money String given parameters and a Money object.  
10    pub fn money<T: FormattableCurrency>(money: &Money<'a, T>, params: Params) -> String {
11        let mut decimal = *money.amount();
12
13        // Round the decimal
14        if let Some(x) = params.rounding {
15            decimal = *money.round(x, Round::HalfEven).amount();
16        }
17
18        // Format the Amount String
19        let amount = Formatter::amount(&format!("{}", decimal), &params);
20
21        // Position values in the Output String
22        let mut result = String::new();
23        for position in params.positions.iter() {
24            match position {
25                Position::Space => result.push(' '),
26                Position::Amount => result.push_str(&amount),
27                Position::Code => result.push_str(params.code.unwrap_or("")),
28                Position::Symbol => result.push_str(params.symbol.unwrap_or("")),
29                Position::Sign => result.push_str(if money.is_negative() { "-" } else { "" }),
30            }
31        }
32        result
33    }
34
35    /// Returns a formatted amount String, given the raw amount and formatting parameters.
36    fn amount(raw_amount: &str, params: &Params) -> String {
37        // Split amount into digits and exponent.
38        let amount_split: Vec<&str> = raw_amount.split('.').collect();
39        let mut amount_digits = amount_split[0].to_string();
40
41        // Format the digits
42        amount_digits.retain(|c| c != '-');
43        amount_digits = Formatter::digits(
44            &amount_digits,
45            params.digit_separator,
46            &params.separator_pattern,
47        );
48        let mut result = amount_digits;
49
50        // Format the exponent, and add to digits
51        match amount_split.len().cmp(&2) {
52            Ordering::Equal => {
53                // Exponent found, concatenate to digits.
54                result.push(params.exponent_separator);
55                result += amount_split[1];
56            }
57            Ordering::Less => {
58                // No exponent, do nothing.
59            }
60            Ordering::Greater => panic!("More than 1 exponent separators when parsing Decimal"),
61        }
62
63        result
64    }
65
66    /// Returns a formatted digit component, given the digit string, separator and pattern of separation.
67    fn digits(raw_digits: &str, separator: char, pattern: &[usize]) -> String {
68        let mut digits = raw_digits.to_string();
69
70        let mut current_position: usize = 0;
71        for position in pattern.iter() {
72            current_position += position;
73            if digits.len() > current_position {
74                digits.insert(digits.len() - current_position, separator);
75                current_position += 1;
76            }
77        }
78        digits
79    }
80}
81
82/// Items which must be positioned in a Money string.
83#[derive(Debug, Clone)]
84pub enum Position {
85    Space,
86    Amount,
87    Code,
88    Symbol,
89    Sign,
90}
91
92/// Group of formatting parameters consumed by `Formatter`.
93#[derive(Debug, Clone)]
94pub struct Params {
95    /// The character that separates grouped digits (e.g. 1,000,000)
96    pub digit_separator: char,
97    /// The character that separates minor units from major units (e.g. 1,000.00)
98    pub exponent_separator: char,
99    /// The grouping pattern that is applied to digits / major units (e.g. 1,000,000 vs 1,00,000)
100    pub separator_pattern: Vec<usize>,
101    /// The relative positions of the elements in a currency string (e.g. -$1,000 vs $ -1,000)
102    pub positions: Vec<Position>,
103    /// The number of minor unit digits should remain after Round::HalfEven is applied.
104    pub rounding: Option<u32>,
105    /// The symbol of the currency (e.g. $)
106    pub symbol: Option<&'static str>,
107    /// The currency's ISO code (e.g. USD)
108    pub code: Option<&'static str>,
109}
110
111impl Default for Params {
112    /// Defines the default parameters to format a Money string.
113    fn default() -> Params {
114        Params {
115            digit_separator: ',',
116            exponent_separator: '.',
117            separator_pattern: vec![3, 3, 3],
118            positions: vec![Position::Sign, Position::Symbol, Position::Amount],
119            rounding: None,
120            symbol: None,
121            code: None,
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::define_currency_set;
130
131    define_currency_set!(
132        test {
133            USD: {
134                code: "USD",
135                exponent: 2,
136                locale: EnUs,
137                minor_units: 100,
138                name: "USD",
139                symbol: "$",
140                symbol_first: true,
141            }
142        }
143    );
144
145    #[test]
146    fn format_position() {
147        let _usd = test::find("USD"); // Prevents unused code warnings from the defined module.
148
149        let money = Money::from_major(-1000, test::USD);
150
151        // Test that you can position eSpace, Amount, Code, Symbol and Sign in different places
152        let params = Params {
153            symbol: Some("$"),
154            code: Some("USD"),
155            positions: vec![
156                Position::Sign,
157                Position::Space,
158                Position::Symbol,
159                Position::Amount,
160                Position::Space,
161                Position::Code,
162            ],
163            ..Default::default()
164        };
165        assert_eq!("- $1,000 USD", Formatter::money(&money, params));
166
167        let params = Params {
168            symbol: Some("$"),
169            code: Some("USD"),
170            positions: vec![
171                Position::Code,
172                Position::Space,
173                Position::Amount,
174                Position::Symbol,
175                Position::Space,
176                Position::Sign,
177            ],
178            ..Default::default()
179        };
180        assert_eq!("USD 1,000$ -", Formatter::money(&money, params));
181
182        // Test that you can omit some, and it works fine.
183        let params = Params {
184            positions: vec![Position::Amount],
185            ..Default::default()
186        };
187        assert_eq!("1,000", Formatter::money(&money, params));
188
189        let params = Params {
190            symbol: Some("$"),
191            positions: vec![Position::Symbol],
192            ..Default::default()
193        };
194        assert_eq!("$", Formatter::money(&money, params));
195
196        // Missing Optionals Insert Nothing
197        let params = Params {
198            positions: vec![Position::Amount, Position::Symbol],
199            ..Default::default()
200        };
201        assert_eq!("1,000", Formatter::money(&money, params));
202    }
203
204    #[test]
205    fn format_digit_separators_with_custom_separators() {
206        let params = Params {
207            digit_separator: '/',
208            ..Default::default()
209        };
210
211        // For 1_000_000
212        let money = Money::from_major(1_000_000, test::USD);
213        assert_eq!("1/000/000", Formatter::money(&money, params.clone()));
214
215        // For 1_000
216        let money = Money::from_major(1_000, test::USD);
217        assert_eq!("1/000", Formatter::money(&money, params.clone()));
218
219        // For 0 Chars
220        let money = Money::from_major(0, test::USD);
221        assert_eq!("0", Formatter::money(&money, params.clone()));
222    }
223
224    #[test]
225    fn format_digit_separators_with_custom_sequences() {
226        let params = Params {
227            separator_pattern: vec![3, 2, 2],
228            ..Default::default()
229        };
230
231        let money = Money::from_major(1_00_00_000, test::USD);
232        assert_eq!("1,00,00,000", Formatter::money(&money, params.clone()));
233
234        let money = Money::from_major(1_00_000, test::USD);
235        assert_eq!("1,00,000", Formatter::money(&money, params.clone()));
236
237        let money = Money::from_major(1_000, test::USD);
238        assert_eq!("1,000", Formatter::money(&money, params.clone()));
239
240        // With a zero sequence
241        let params = Params {
242            separator_pattern: vec![0, 2],
243            ..Default::default()
244        };
245
246        let money = Money::from_major(100, test::USD);
247        assert_eq!("1,00,", Formatter::money(&money, params.clone()));
248
249        let money = Money::from_major(0, test::USD);
250        assert_eq!("0,", Formatter::money(&money, params.clone()));
251    }
252
253    // What if pattern includes a zero or negative number?
254
255    #[test]
256    fn format_rounding() {
257        let money = Money::from_minor(1000, test::USD) / 3;
258
259        // Rounding = Some (0)
260        let params = Params {
261            rounding: Some(0),
262            ..Default::default()
263        };
264        assert_eq!("3", Formatter::money(&money, params));
265
266        // Rounding = Some(2)
267        let params = Params {
268            rounding: Some(2),
269            ..Default::default()
270        };
271        assert_eq!("3.33", Formatter::money(&money, params));
272
273        // Rounding = None
274        let params = Params {
275            ..Default::default()
276        };
277        assert_eq!(
278            "3.3333333333333333333333333333",
279            Formatter::money(&money, params)
280        );
281    }
282}