nmbrs_workload/
metric_format.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct FormatSpec {
24 pub decimal_places: u8,
27 pub integer_places: u8,
30}
31
32impl FormatSpec {
33 pub fn apply(&self, value: f64) -> f64 {
36 let scale = 10f64.powi(self.decimal_places as i32);
37 (value * scale).round() / scale
38 }
39}
40
41pub fn parse_format_spec(s: &str) -> Result<FormatSpec, String> {
58 if s.is_empty() {
59 return Err("empty format string".into());
60 }
61 if s.starts_with('%') {
62 return Err(format!(
63 "printf-style '{s}' not accepted; use Excel-style \
64 hash patterns like '#.##' or '0.000'"
65 ));
66 }
67
68 let mut dot_seen = false;
70 for c in s.chars() {
71 match c {
72 '#' | '0' => {}
73 '.' => {
74 if dot_seen {
75 return Err(format!("format '{s}': multiple '.' separators"));
76 }
77 dot_seen = true;
78 }
79 other => {
80 return Err(format!(
81 "format '{s}': unexpected '{other}' \
82 (only '#', '0', '.' accepted)"
83 ));
84 }
85 }
86 }
87
88 let (int_part, dec_part) = match s.split_once('.') {
89 Some((i, d)) => (i, d),
90 None => (s, ""),
91 };
92
93 let integer_places = int_part.len() as u8;
94 let decimal_places = dec_part.len() as u8;
95
96 if integer_places == 0 && decimal_places == 0 {
99 return Err(format!("format '{s}': no placeholders found"));
100 }
101
102 Ok(FormatSpec {
103 integer_places,
104 decimal_places,
105 })
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn parse_basic_decimal_patterns() {
114 assert_eq!(
115 parse_format_spec("#.##").unwrap(),
116 FormatSpec {
117 integer_places: 1,
118 decimal_places: 2
119 }
120 );
121 assert_eq!(
122 parse_format_spec("##.###").unwrap(),
123 FormatSpec {
124 integer_places: 2,
125 decimal_places: 3
126 }
127 );
128 assert_eq!(
129 parse_format_spec("0.000").unwrap(),
130 FormatSpec {
131 integer_places: 1,
132 decimal_places: 3
133 }
134 );
135 }
136
137 #[test]
138 fn parse_integer_only() {
139 assert_eq!(
140 parse_format_spec("#").unwrap(),
141 FormatSpec {
142 integer_places: 1,
143 decimal_places: 0
144 }
145 );
146 assert_eq!(
147 parse_format_spec("0").unwrap(),
148 FormatSpec {
149 integer_places: 1,
150 decimal_places: 0
151 }
152 );
153 assert_eq!(
154 parse_format_spec("###").unwrap(),
155 FormatSpec {
156 integer_places: 3,
157 decimal_places: 0
158 }
159 );
160 }
161
162 #[test]
163 fn hash_and_zero_interchangeable() {
164 assert_eq!(
169 parse_format_spec("#.##").unwrap().decimal_places,
170 parse_format_spec("0.00").unwrap().decimal_places,
171 );
172 assert_eq!(
173 parse_format_spec("###").unwrap().decimal_places,
174 parse_format_spec("000").unwrap().decimal_places,
175 );
176 }
177
178 #[test]
179 fn rejects_printf_style() {
180 let err = parse_format_spec("%3.2f").unwrap_err();
181 assert!(err.contains("printf-style"));
182 }
183
184 #[test]
185 fn rejects_unknown_chars() {
186 assert!(parse_format_spec("#,###").is_err()); assert!(parse_format_spec("0.0%").is_err()); assert!(parse_format_spec("0.0e2").is_err()); assert!(parse_format_spec("$0.00").is_err()); }
191
192 #[test]
193 fn rejects_multiple_dots() {
194 assert!(parse_format_spec("0.0.0").is_err());
195 }
196
197 #[test]
198 fn rejects_empty_and_degenerate() {
199 assert!(parse_format_spec("").is_err());
200 assert!(parse_format_spec(".").is_err());
201 }
202
203 #[test]
204 fn apply_rounds_to_decimals() {
205 let two = parse_format_spec("#.##").unwrap();
206 assert_eq!(two.apply(1.0), 1.0);
207 assert_eq!(two.apply(1.234), 1.23);
208 assert_eq!(two.apply(1.235), 1.24); assert_eq!(two.apply(1.999), 2.0);
210
211 let three = parse_format_spec("0.000").unwrap();
212 assert_eq!(three.apply(1.23456), 1.235);
213 }
214
215 #[test]
216 fn apply_rounds_to_integer() {
217 let int = parse_format_spec("#").unwrap();
218 assert_eq!(int.apply(1.0), 1.0);
219 assert_eq!(int.apply(1.49), 1.0);
220 assert_eq!(int.apply(1.51), 2.0);
221 assert_eq!(int.apply(-0.5), -1.0);
226 assert_eq!(int.apply(0.5), 1.0);
227 }
228
229 #[test]
230 fn apply_preserves_negative_values() {
231 let two = parse_format_spec("#.##").unwrap();
232 assert_eq!(two.apply(-1.234), -1.23);
233 assert_eq!(two.apply(-1.235), -1.24);
234 }
235}