Skip to main content

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