1use crate::currency::FormattableCurrency;
2use crate::{Money, Round};
3use std::cmp::Ordering;
4
5pub struct Formatter;
7
8impl Formatter {
9 pub fn money<'a, T: FormattableCurrency>(money: &Money<'a, T>, params: Params<'_>) -> String {
11 let mut decimal = *money.amount();
12
13 if let Some(x) = params.rounding {
15 decimal = *money.round(x, Round::HalfEven).amount();
16 decimal.rescale(x);
17 }
18
19 let amount = Formatter::amount(&format!("{}", decimal), ¶ms);
21
22 let mut result = String::new();
24 for position in params.positions.iter() {
25 match position {
26 Position::Space => result.push(' '),
27 Position::Amount => result.push_str(&amount),
28 Position::Code => result.push_str(params.code.unwrap_or("")),
29 Position::Symbol => result.push_str(params.symbol.unwrap_or("")),
30 Position::Sign => result.push_str(if money.is_negative() { "-" } else { "" }),
31 }
32 }
33 result
34 }
35
36 fn amount(raw_amount: &str, params: &Params<'_>) -> String {
38 let amount_split: Vec<&str> = raw_amount.split('.').collect();
40 let mut amount_digits = amount_split[0].to_string();
41
42 amount_digits.retain(|c| c != '-');
44 amount_digits = Formatter::digits(
45 &amount_digits,
46 params.digit_separator,
47 params.separator_pattern,
48 );
49 let mut result = amount_digits;
50
51 match amount_split.len().cmp(&2) {
53 Ordering::Equal => {
54 result.push(params.exponent_separator);
56 result += amount_split[1];
57 }
58 Ordering::Less => {
59 }
61 Ordering::Greater => {
62 unreachable!(
63 "Decimal formatted string should never contain more than 1 exponent separator"
64 )
65 }
66 }
67
68 result
69 }
70
71 fn digits(raw_digits: &str, separator: char, pattern: &[usize]) -> String {
73 let mut digits = raw_digits.to_string();
74
75 let mut current_position: usize = 0;
76 for &position in pattern.iter() {
77 current_position += position;
78 if digits.len() > current_position {
79 digits.insert(digits.len() - current_position, separator);
80 current_position += 1;
81 }
82 }
83 digits
84 }
85}
86
87#[derive(Debug, Clone)]
89pub enum Position {
90 Space,
91 Amount,
92 Code,
93 Symbol,
94 Sign,
95}
96
97#[derive(Debug, Clone)]
99pub struct Params<'a> {
100 pub digit_separator: char,
102 pub exponent_separator: char,
104 pub separator_pattern: &'a [usize],
106 pub positions: &'a [Position],
108 pub rounding: Option<u32>,
110 pub symbol: Option<&'static str>,
112 pub code: Option<&'static str>,
114}
115
116const DEFAULT_SEPARATOR_PATTERN: &[usize] = &[3, 3, 3];
118const DEFAULT_POSITIONS: &[Position] = &[Position::Sign, Position::Symbol, Position::Amount];
119
120impl Default for Params<'_> {
121 fn default() -> Self {
123 Params {
124 digit_separator: ',',
125 exponent_separator: '.',
126 separator_pattern: DEFAULT_SEPARATOR_PATTERN,
127 positions: DEFAULT_POSITIONS,
128 rounding: None,
129 symbol: None,
130 code: None,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::define_currency_set;
139
140 define_currency_set!(
141 test {
142 USD: {
143 code: "USD",
144 exponent: 2,
145 locale: EnUs,
146 minor_units: 100,
147 name: "USD",
148 symbol: "$",
149 symbol_first: true,
150 }
151 }
152 );
153
154 #[test]
155 fn format_position() {
156 let _usd = test::find("USD"); let money = Money::from_major(-1000, test::USD);
159
160 let params = Params {
162 symbol: Some("$"),
163 code: Some("USD"),
164 positions: &[
165 Position::Sign,
166 Position::Space,
167 Position::Symbol,
168 Position::Amount,
169 Position::Space,
170 Position::Code,
171 ],
172 ..Default::default()
173 };
174 assert_eq!("- $1,000 USD", Formatter::money(&money, params));
175
176 let params = Params {
177 symbol: Some("$"),
178 code: Some("USD"),
179 positions: &[
180 Position::Code,
181 Position::Space,
182 Position::Amount,
183 Position::Symbol,
184 Position::Space,
185 Position::Sign,
186 ],
187 ..Default::default()
188 };
189 assert_eq!("USD 1,000$ -", Formatter::money(&money, params));
190
191 let params = Params {
193 positions: &[Position::Amount],
194 ..Default::default()
195 };
196 assert_eq!("1,000", Formatter::money(&money, params));
197
198 let params = Params {
199 symbol: Some("$"),
200 positions: &[Position::Symbol],
201 ..Default::default()
202 };
203 assert_eq!("$", Formatter::money(&money, params));
204
205 let params = Params {
207 positions: &[Position::Amount, Position::Symbol],
208 ..Default::default()
209 };
210 assert_eq!("1,000", Formatter::money(&money, params));
211
212 let params = Params {
214 symbol: Some("$"),
215 positions: &[Position::Symbol, Position::Sign, Position::Amount],
216 ..Default::default()
217 };
218 assert_eq!("$-1,000", Formatter::money(&money, params));
219 }
220
221 #[test]
222 fn format_digit_separators_with_custom_separators() {
223 let params = Params {
224 digit_separator: '/',
225 ..Default::default()
226 };
227
228 let money = Money::from_major(1_000_000, test::USD);
230 assert_eq!("1/000/000", Formatter::money(&money, params.clone()));
231
232 let money = Money::from_major(1_000, test::USD);
234 assert_eq!("1/000", Formatter::money(&money, params.clone()));
235
236 let money = Money::from_major(0, test::USD);
238 assert_eq!("0", Formatter::money(&money, params));
239
240 let params = Params {
242 rounding: Some(2),
243 exponent_separator: ',',
244 digit_separator: '.',
245 ..Default::default()
246 };
247 let money = Money::from_minor(123456, test::USD);
248 assert_eq!("1.234,56", Formatter::money(&money, params));
249 }
250
251 #[test]
252 fn format_digit_separators_with_custom_sequences() {
253 let params = Params {
255 separator_pattern: &[3, 2, 2],
256 ..Default::default()
257 };
258
259 let money = Money::from_major(10_000_000, test::USD);
260 assert_eq!("1,00,00,000", Formatter::money(&money, params.clone()));
261
262 let money = Money::from_major(100_000, test::USD);
263 assert_eq!("1,00,000", Formatter::money(&money, params.clone()));
264
265 let money = Money::from_major(1_000, test::USD);
266 assert_eq!("1,000", Formatter::money(&money, params));
267 }
268
269 #[test]
270 fn format_zero_amount() {
271 let params = Params {
272 symbol: Some("$"),
273 positions: &[Position::Sign, Position::Symbol, Position::Amount],
274 ..Default::default()
275 };
276
277 let money = Money::from_major(0, test::USD);
278 assert_eq!("$0", Formatter::money(&money, params));
280 }
281
282 #[test]
283 fn format_rounding() {
284 let money = Money::from_minor(1000, test::USD).div(3).unwrap();
285
286 let params = Params {
288 rounding: Some(0),
289 ..Default::default()
290 };
291 assert_eq!("3", Formatter::money(&money, params));
292
293 let params = Params {
295 rounding: Some(2),
296 ..Default::default()
297 };
298 assert_eq!("3.33", Formatter::money(&money, params));
299
300 let params = Params {
302 ..Default::default()
303 };
304 assert_eq!(
305 "3.3333333333333333333333333333",
306 Formatter::money(&money, params)
307 );
308 }
309}
310
311#[cfg(all(test, feature = "iso"))]
315mod golden_tests {
316 use crate::Money;
317 use crate::iso;
318
319 #[test]
320 fn usd_format_golden() {
321 assert_eq!(format!("{}", Money::from_minor(0, iso::USD)), "$0.00");
323 assert_eq!(format!("{}", Money::from_minor(1, iso::USD)), "$0.01");
324 assert_eq!(format!("{}", Money::from_minor(100, iso::USD)), "$1.00");
325 assert_eq!(
326 format!("{}", Money::from_minor(123456, iso::USD)),
327 "$1,234.56"
328 );
329 assert_eq!(
330 format!("{}", Money::from_minor(123456789, iso::USD)),
331 "$1,234,567.89"
332 );
333 assert_eq!(format!("{}", Money::from_minor(-100, iso::USD)), "-$1.00");
335 assert_eq!(
336 format!("{}", Money::from_minor(-123456, iso::USD)),
337 "-$1,234.56"
338 );
339 }
340
341 #[test]
342 fn eur_format_golden() {
343 assert_eq!(format!("{}", Money::from_minor(0, iso::EUR)), "€0,00");
345 assert_eq!(
346 format!("{}", Money::from_minor(123456, iso::EUR)),
347 "€1.234,56"
348 );
349 assert_eq!(
350 format!("{}", Money::from_minor(-123456, iso::EUR)),
351 "-€1.234,56"
352 );
353 }
354
355 #[test]
356 fn gbp_format_golden() {
357 assert_eq!(format!("{}", Money::from_minor(0, iso::GBP)), "£0.00");
359 assert_eq!(
360 format!("{}", Money::from_minor(123456, iso::GBP)),
361 "£1,234.56"
362 );
363 }
364
365 #[test]
366 fn jpy_format_golden() {
367 assert_eq!(format!("{}", Money::from_minor(0, iso::JPY)), "¥0");
369 assert_eq!(format!("{}", Money::from_minor(1, iso::JPY)), "¥1");
370 assert_eq!(format!("{}", Money::from_minor(1234, iso::JPY)), "¥1,234");
371 assert_eq!(
372 format!("{}", Money::from_minor(1234567, iso::JPY)),
373 "¥1,234,567"
374 );
375 }
376
377 #[test]
378 fn inr_format_golden() {
379 assert_eq!(format!("{}", Money::from_minor(0, iso::INR)), "₹0.00");
381 assert_eq!(format!("{}", Money::from_minor(100, iso::INR)), "₹1.00");
382 assert_eq!(
384 format!("{}", Money::from_minor(10000000, iso::INR)),
385 "₹1,00,000.00"
386 );
387 assert_eq!(
389 format!("{}", Money::from_minor(1000000000, iso::INR)),
390 "₹1,00,00,000.00"
391 );
392 }
393
394 #[test]
395 fn bhd_format_golden() {
396 assert_eq!(format!("{}", Money::from_minor(0, iso::BHD)), "د.ب0.000");
398 assert_eq!(format!("{}", Money::from_minor(1, iso::BHD)), "د.ب0.001");
399 assert_eq!(format!("{}", Money::from_minor(1000, iso::BHD)), "د.ب1.000");
400 assert_eq!(
401 format!("{}", Money::from_minor(1234567, iso::BHD)),
402 "د.ب1,234.567"
403 );
404 }
405
406 #[test]
407 fn byn_format_golden() {
408 assert_eq!(format!("{}", Money::from_minor(0, iso::BYN)), "0,00Br");
410 assert_eq!(
411 format!("{}", Money::from_minor(123456, iso::BYN)),
412 "1 234,56Br"
413 );
414 assert_eq!(
415 format!("{}", Money::from_minor(123456789, iso::BYN)),
416 "1 234 567,89Br"
417 );
418 }
419}