Skip to main content

rusty_money/
locale.rs

1/// Enumerates regions which have unique formatting standards for Currencies.
2///
3/// Each Locale maps 1:1 to a LocalFormat, which contains the characteristics for formatting.
4#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub enum Locale {
7    EnUs,
8    EnIn,
9    EnEu,
10    EnBy,
11}
12
13/// Stores currency formatting metadata for a specific region (e.g. EN-US).
14#[derive(Debug, PartialEq, Eq)]
15pub struct LocalFormat {
16    pub name: &'static str,
17    pub digit_separator: char,
18    pub digit_separator_pattern: &'static [usize],
19    pub exponent_separator: char,
20}
21
22// Pre-defined patterns to avoid allocation
23const PATTERN_3_3_3: &[usize] = &[3, 3, 3];
24const PATTERN_3_2_2: &[usize] = &[3, 2, 2];
25
26impl LocalFormat {
27    /// Returns the associated LocalFormat given a Locale.
28    pub fn from_locale(locale: Locale) -> LocalFormat {
29        use Locale::*;
30
31        match locale {
32            EnUs => LocalFormat {
33                name: "en-us",
34                digit_separator: ',',
35                digit_separator_pattern: PATTERN_3_3_3,
36                exponent_separator: '.',
37            },
38            EnIn => LocalFormat {
39                name: "en-in",
40                digit_separator: ',',
41                digit_separator_pattern: PATTERN_3_2_2,
42                exponent_separator: '.',
43            },
44            EnEu => LocalFormat {
45                name: "en-eu",
46                digit_separator: '.',
47                digit_separator_pattern: PATTERN_3_3_3,
48                exponent_separator: ',',
49            },
50            EnBy => LocalFormat {
51                name: "en-by",
52                digit_separator: ' ',
53                digit_separator_pattern: PATTERN_3_3_3,
54                exponent_separator: ',',
55            },
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn en_us_locale_format() {
66        let format = LocalFormat::from_locale(Locale::EnUs);
67        assert_eq!(format.name, "en-us");
68        assert_eq!(format.digit_separator, ',');
69        assert_eq!(format.exponent_separator, '.');
70        assert_eq!(format.digit_separator_pattern, &[3, 3, 3]);
71    }
72
73    #[test]
74    fn en_in_locale_format() {
75        // Indian numbering: lakhs and crores (2,2,3 pattern from right)
76        let format = LocalFormat::from_locale(Locale::EnIn);
77        assert_eq!(format.name, "en-in");
78        assert_eq!(format.digit_separator, ',');
79        assert_eq!(format.exponent_separator, '.');
80        assert_eq!(format.digit_separator_pattern, &[3, 2, 2]);
81    }
82
83    #[test]
84    fn en_eu_locale_format() {
85        // European: swap comma and period
86        let format = LocalFormat::from_locale(Locale::EnEu);
87        assert_eq!(format.name, "en-eu");
88        assert_eq!(format.digit_separator, '.');
89        assert_eq!(format.exponent_separator, ',');
90        assert_eq!(format.digit_separator_pattern, &[3, 3, 3]);
91    }
92
93    #[test]
94    fn en_by_locale_format() {
95        // Belarusian: space as digit separator
96        let format = LocalFormat::from_locale(Locale::EnBy);
97        assert_eq!(format.name, "en-by");
98        assert_eq!(format.digit_separator, ' ');
99        assert_eq!(format.exponent_separator, ',');
100        assert_eq!(format.digit_separator_pattern, &[3, 3, 3]);
101    }
102
103    #[test]
104    fn all_locales_have_distinct_names() {
105        let locales = [Locale::EnUs, Locale::EnIn, Locale::EnEu, Locale::EnBy];
106        let names: Vec<_> = locales
107            .iter()
108            .map(|l| LocalFormat::from_locale(*l).name)
109            .collect();
110
111        // Verify all names are unique
112        for (i, name) in names.iter().enumerate() {
113            for (j, other) in names.iter().enumerate() {
114                if i != j {
115                    assert_ne!(name, other, "Locale names must be unique");
116                }
117            }
118        }
119    }
120
121    #[test]
122    fn digit_and_exponent_separators_differ() {
123        // Critical invariant: digit and exponent separators must differ
124        // to avoid ambiguous parsing (e.g., "1,000" vs "1,00")
125        let locales = [Locale::EnUs, Locale::EnIn, Locale::EnEu, Locale::EnBy];
126        for locale in locales {
127            let format = LocalFormat::from_locale(locale);
128            assert_ne!(
129                format.digit_separator, format.exponent_separator,
130                "Locale {:?} has same digit and exponent separator",
131                locale
132            );
133        }
134    }
135}