nautilus_core/
formatting.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Number formatting utilities.
17
18fn separate_with(s: &str, sep: char) -> String {
19    let (neg, digits) = if let Some(rest) = s.strip_prefix('-') {
20        (true, rest)
21    } else {
22        (false, s)
23    };
24
25    let (int_part, dec_part) = match digits.find('.') {
26        Some(pos) => (&digits[..pos], Some(&digits[pos..])),
27        None => (digits, None),
28    };
29
30    let mut result = String::with_capacity(s.len() + int_part.len() / 3);
31
32    if neg {
33        result.push('-');
34    }
35
36    let chars: Vec<char> = int_part.chars().collect();
37    for (i, c) in chars.iter().enumerate() {
38        if i > 0 && (chars.len() - i).is_multiple_of(3) {
39            result.push(sep);
40        }
41        result.push(*c);
42    }
43
44    if let Some(dec) = dec_part {
45        result.push_str(dec);
46    }
47
48    result
49}
50
51/// Extension trait for formatting numbers with separators.
52///
53/// Drop-in replacement for the `thousands::Separable` trait.
54pub trait Separable {
55    /// Formats the number with commas as thousand separators.
56    fn separate_with_commas(&self) -> String;
57
58    /// Formats the number with underscores as thousand separators.
59    fn separate_with_underscores(&self) -> String;
60}
61
62macro_rules! impl_separable {
63    ($($t:ty),*) => {
64        $(
65            impl Separable for $t {
66                fn separate_with_commas(&self) -> String {
67                    separate_with(&self.to_string(), ',')
68                }
69
70                fn separate_with_underscores(&self) -> String {
71                    separate_with(&self.to_string(), '_')
72                }
73            }
74        )*
75    };
76}
77
78impl_separable!(
79    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
80);
81
82impl Separable for String {
83    fn separate_with_commas(&self) -> String {
84        separate_with(self, ',')
85    }
86
87    fn separate_with_underscores(&self) -> String {
88        separate_with(self, '_')
89    }
90}
91
92impl Separable for &str {
93    fn separate_with_commas(&self) -> String {
94        separate_with(self, ',')
95    }
96
97    fn separate_with_underscores(&self) -> String {
98        separate_with(self, '_')
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use rstest::rstest;
105
106    use super::*;
107
108    #[rstest]
109    #[case(0, "0")]
110    #[case(1, "1")]
111    #[case(12, "12")]
112    #[case(123, "123")]
113    #[case(1234, "1,234")]
114    #[case(12345, "12,345")]
115    #[case(123456, "123,456")]
116    #[case(1234567, "1,234,567")]
117    #[case(-1234, "-1,234")]
118    #[case(-1234567, "-1,234,567")]
119    fn test_separate_with_commas(#[case] input: i64, #[case] expected: &str) {
120        assert_eq!(input.separate_with_commas(), expected);
121    }
122
123    #[rstest]
124    #[case(1234, "1_234")]
125    #[case(1234567, "1_234_567")]
126    fn test_separate_with_underscores(#[case] input: i64, #[case] expected: &str) {
127        assert_eq!(input.separate_with_underscores(), expected);
128    }
129
130    #[rstest]
131    fn test_float_with_decimal() {
132        assert_eq!(1234.56_f64.separate_with_commas(), "1,234.56");
133        assert_eq!(1234567.89_f64.separate_with_underscores(), "1_234_567.89");
134    }
135
136    #[rstest]
137    fn test_string() {
138        assert_eq!("1234567".separate_with_commas(), "1,234,567");
139        assert_eq!("1234.5678".separate_with_underscores(), "1_234.5678");
140    }
141}