fish_printf/
locale.rs

1/// The numeric locale. Note this is a pure value type.
2#[derive(Debug, Clone, Copy)]
3pub struct Locale {
4    /// The decimal point. Only single-char decimal points are supported.
5    pub decimal_point: char,
6
7    /// The thousands separator, or None if none.
8    /// Note some obscure locales like it_IT.ISO8859-15 seem to have a multi-char thousands separator!
9    /// We do not support that.
10    pub thousands_sep: Option<char>,
11
12    /// The grouping of digits.
13    /// This is to be read from left to right.
14    /// For example, the number 88888888888888 with a grouping of [2, 3, 4, 4]
15    /// would produce the string "8,8888,8888,888,88".
16    /// If 0, no grouping at all.
17    pub grouping: [u8; 4],
18
19    /// If true, the group is repeated.
20    /// If false, there are no groups after the last.
21    pub group_repeat: bool,
22}
23
24impl Locale {
25    /// Given a string containing only ASCII digits, return a new string with thousands separators applied.
26    /// This panics if the locale has no thousands separator; callers should only call this if there is a
27    /// thousands separator.
28    pub fn apply_grouping(&self, mut input: &str) -> String {
29        debug_assert!(input.bytes().all(|b| b.is_ascii_digit()));
30        let sep = self.thousands_sep.expect("no thousands separator");
31        let mut result = String::with_capacity(input.len() + self.separator_count(input.len()));
32        while !input.is_empty() {
33            let group_size = self.next_group_size(input.len());
34            let (group, rest) = input.split_at(group_size);
35            result.push_str(group);
36            if !rest.is_empty() {
37                result.push(sep);
38            }
39            input = rest;
40        }
41        result
42    }
43
44    // Given a count of remaining digits, return the number of characters in the next group, from the left (most significant).
45    fn next_group_size(&self, digits_left: usize) -> usize {
46        let mut accum: usize = 0;
47        for group in self.grouping {
48            if digits_left <= accum + group as usize {
49                return digits_left - accum;
50            }
51            accum += group as usize;
52        }
53        // accum now contains the sum of all groups.
54        // Maybe repeat.
55        debug_assert!(digits_left >= accum);
56        let repeat_group = if self.group_repeat {
57            *self.grouping.last().unwrap()
58        } else {
59            0
60        };
61
62        if repeat_group == 0 {
63            // No further grouping.
64            digits_left - accum
65        } else {
66            // Divide remaining digits by repeat_group.
67            // Apply any remainder to the first group.
68            let res = (digits_left - accum) % (repeat_group as usize);
69            if res > 0 {
70                res
71            } else {
72                repeat_group as usize
73            }
74        }
75    }
76
77    // Given a count of remaining digits, return the total number of separators.
78    pub fn separator_count(&self, digits_count: usize) -> usize {
79        if self.thousands_sep.is_none() {
80            return 0;
81        }
82        let mut sep_count = 0;
83        let mut accum = 0;
84        for group in self.grouping {
85            if digits_count <= accum + group as usize {
86                return sep_count;
87            }
88            if group > 0 {
89                sep_count += 1;
90            }
91            accum += group as usize;
92        }
93        debug_assert!(digits_count >= accum);
94        let repeat_group = if self.group_repeat {
95            *self.grouping.last().unwrap()
96        } else {
97            0
98        };
99        // Divide remaining digits by repeat_group.
100        // -1 because it's "100,000" and not ",100,100".
101        if repeat_group > 0 && digits_count > accum {
102            sep_count += (digits_count - accum - 1) / repeat_group as usize;
103        }
104        sep_count
105    }
106}
107
108/// The "C" numeric locale.
109pub const C_LOCALE: Locale = Locale {
110    decimal_point: '.',
111    thousands_sep: None,
112    grouping: [0; 4],
113    group_repeat: false,
114};
115
116// en_us numeric locale, for testing.
117#[allow(dead_code)]
118pub const EN_US_LOCALE: Locale = Locale {
119    decimal_point: '.',
120    thousands_sep: Some(','),
121    grouping: [3, 3, 3, 3],
122    group_repeat: true,
123};
124
125#[test]
126fn test_apply_grouping() {
127    let input = "123456789";
128    let mut result: String;
129
130    // en_US has commas.
131    assert_eq!(EN_US_LOCALE.thousands_sep, Some(','));
132    result = EN_US_LOCALE.apply_grouping(input);
133    assert_eq!(result, "123,456,789");
134
135    // Test weird locales.
136    let input: &str = "1234567890123456";
137    let mut locale: Locale = C_LOCALE;
138    locale.thousands_sep = Some('!');
139
140    locale.grouping = [5, 3, 1, 0];
141    locale.group_repeat = false;
142    result = locale.apply_grouping(input);
143    assert_eq!(result, "1234567!8!901!23456");
144
145    // group_repeat doesn't matter because trailing group is 0
146    locale.grouping = [5, 3, 1, 0];
147    locale.group_repeat = true;
148    result = locale.apply_grouping(input);
149    assert_eq!(result, "1234567!8!901!23456");
150
151    locale.grouping = [5, 3, 1, 2];
152    locale.group_repeat = false;
153    result = locale.apply_grouping(input);
154    assert_eq!(result, "12345!67!8!901!23456");
155
156    locale.grouping = [5, 3, 1, 2];
157    locale.group_repeat = true;
158    result = locale.apply_grouping(input);
159    assert_eq!(result, "1!23!45!67!8!901!23456");
160}
161
162#[test]
163#[should_panic]
164fn test_thousands_grouping_length_panics_if_no_sep() {
165    // We should panic if we try to group with no thousands separator.
166    assert_eq!(C_LOCALE.thousands_sep, None);
167    C_LOCALE.apply_grouping("123");
168}
169
170#[test]
171fn test_thousands_grouping_length() {
172    fn validate_grouping_length_hint(locale: Locale, mut input: &str) {
173        loop {
174            let expected = locale.separator_count(input.len()) + input.len();
175            let actual = locale.apply_grouping(input).len();
176            assert_eq!(expected, actual);
177            if input.is_empty() {
178                break;
179            }
180            input = &input[1..];
181        }
182    }
183
184    validate_grouping_length_hint(EN_US_LOCALE, "123456789");
185
186    // Test weird locales.
187    let input = "1234567890123456";
188    let mut locale: Locale = C_LOCALE;
189    locale.thousands_sep = Some('!');
190
191    locale.grouping = [5, 3, 1, 0];
192    locale.group_repeat = false;
193    validate_grouping_length_hint(locale, input);
194
195    // group_repeat doesn't matter because trailing group is 0
196    locale.grouping = [5, 3, 1, 0];
197    locale.group_repeat = true;
198    validate_grouping_length_hint(locale, input);
199
200    locale.grouping = [5, 3, 1, 2];
201    locale.group_repeat = false;
202    validate_grouping_length_hint(locale, input);
203
204    locale.grouping = [5, 3, 1, 2];
205    locale.group_repeat = true;
206    validate_grouping_length_hint(locale, input);
207}