1pub mod number_formatting {
2 const DEFAULT_DECIMAL_VALUE: usize = 3;
3
4 fn group_integer(n: u128, k: usize) -> String {
5 let mut s = n.to_string();
6 if s == "0" {
7 return "0".repeat(k);
8 }
9 let pad = (k - (s.len() % k)) % k;
10 if pad > 0 {
11 s = "0".repeat(pad) + &s;
12 }
13 s.as_bytes()
14 .chunks(k)
15 .map(|c| std::str::from_utf8(c).unwrap())
16 .collect::<Vec<_>>()
17 .join("_")
18 }
19
20 fn group_fractional(frac: &str, k: usize) -> String {
21 let mut f = if frac.is_empty() { "0".to_string() } else { frac.to_string() };
22 let pad = (k - (f.len() % k)) % k;
23 if pad > 0 {
24 f.push_str(&"0".repeat(pad));
25 }
26 f.as_bytes()
27 .chunks(k)
28 .map(|c| std::str::from_utf8(c).unwrap())
29 .collect::<Vec<_>>()
30 .join("_")
31 }
32
33 fn format_number_internal(x: &str, k: usize) -> String {
34 let mut s = x.trim().to_string();
35
36 let mut negative = false;
37 if s.starts_with('+') || s.starts_with('-') {
38 negative = s.starts_with('-');
39 s = s[1..].to_string();
40 }
41
42 if s.contains('.') {
43 let parts: Vec<&str> = s.splitn(2, '.').collect();
44 let int_part = if parts[0].is_empty() { "0" } else { parts[0] };
45 let frac_part = if parts.len() > 1 { parts[1] } else { "0" };
46
47 let int_val: u128 = int_part.parse().unwrap();
48 let int_fmt = group_integer(int_val, k);
49 let frac_fmt = group_fractional(frac_part, k);
50
51 let mut out = format!("{}_decimal_point_{}", int_fmt, frac_fmt);
52 if negative {
53 out = format!("negative_{}", out);
54 }
55 return out;
56 }
57
58 if s.is_empty() {
59 s = "0".to_string();
60 }
61 let int_val: u128 = s.parse().unwrap();
62 let mut out = group_integer(int_val, k);
63 if negative {
64 out = format!("negative_{}", out);
65 }
66 out
67 }
68
69 pub trait IntoGroupSize {
71 fn into_group_size(self) -> usize;
72 }
73
74 impl IntoGroupSize for &str {
75 fn into_group_size(self) -> usize {
76 if self.is_empty() {
77 return DEFAULT_DECIMAL_VALUE;
78 }
79 let k: usize = self.parse().expect("group_size must be a positive integer");
80 if k == 0 {
81 panic!("group_size must be positive");
82 }
83 k
84 }
85 }
86
87 impl IntoGroupSize for usize {
88 fn into_group_size(self) -> usize {
89 if self == 0 {
90 panic!("group_size must be positive");
91 }
92 self
93 }
94 }
95
96 pub fn format_number<T: IntoGroupSize>(x: &str, group_size: T) -> String {
98 let k = group_size.into_group_size();
99 format_number_internal(x, k)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::number_formatting::format_number;
106
107 #[test]
108 fn test_format_number_with_str() {
109 assert_eq!(format_number("12345", ""), "012_345"); assert_eq!(format_number("12345", "4"), "0001_2345");
111 assert_eq!(format_number("1.23456", "4"), "0001_decimal_point_2345_6000");
112 assert_eq!(format_number("-1234", ""), "negative_001_234");
113 }
114
115 #[test]
116 fn test_format_number_with_usize() {
117 assert_eq!(format_number("12345", 3), "012_345"); assert_eq!(format_number("12345", 4), "0001_2345");
119 assert_eq!(format_number("1.23456", 4), "0001_decimal_point_2345_6000");
120 assert_eq!(format_number("-1234", 3), "negative_001_234");
121 }
122
123 #[test]
124 #[should_panic(expected = "group_size must be positive")]
125 fn test_format_number_with_zero_usize_panics() {
126 format_number("12345", 0);
127 }
128
129 #[test]
130 #[should_panic(expected = "group_size must be positive")]
131 fn test_format_number_with_zero_str_panics() {
132 format_number("12345", "0");
133 }
134}