zenith_core/data/
format.rs1#[derive(Debug, Clone, PartialEq)]
9pub enum DataFormat {
10 Currency {
14 locale: Option<String>,
15 precision: Option<u8>,
16 },
17 Percent { precision: Option<u8> },
19 Number { precision: Option<u8> },
21}
22
23pub fn format_data_value(raw: &str, fmt: &DataFormat) -> String {
29 let value: f64 = match raw.parse() {
30 Ok(v) => v,
31 Err(_) => return raw.to_owned(),
32 };
33
34 match fmt {
35 DataFormat::Currency { precision, .. } => {
36 let prec = precision.unwrap_or(2) as usize;
37 let negative = value < 0.0;
38 let abs_val = value.abs();
39 let formatted = format_number_parts(abs_val, prec);
40 if negative {
41 format!("-${formatted}")
42 } else {
43 format!("${formatted}")
44 }
45 }
46 DataFormat::Percent { precision } => {
47 let prec = precision.unwrap_or(1) as usize;
48 let pct = value * 100.0;
49 let formatted = format_fixed(pct, prec);
50 format!("{formatted}%")
51 }
52 DataFormat::Number { precision } => {
53 let prec = precision.unwrap_or(0) as usize;
54 let negative = value < 0.0;
55 let abs_val = value.abs();
56 let formatted = format_number_parts(abs_val, prec);
57 if negative {
58 format!("-{formatted}")
59 } else {
60 formatted
61 }
62 }
63 }
64}
65
66fn format_number_parts(value: f64, prec: usize) -> String {
71 let fixed = format_fixed(value, prec);
72 let (integer_part, decimal_part) = if let Some(dot) = fixed.find('.') {
74 (&fixed[..dot], Some(&fixed[dot..]))
75 } else {
76 (fixed.as_str(), None)
77 };
78
79 let with_thousands = insert_thousands(integer_part);
80 match decimal_part {
81 Some(dec) => format!("{with_thousands}{dec}"),
82 None => with_thousands,
83 }
84}
85
86fn format_fixed(value: f64, prec: usize) -> String {
92 format!("{value:.prec$}")
93}
94
95fn insert_thousands(integer_str: &str) -> String {
101 if integer_str.len() <= 3 {
102 return integer_str.to_owned();
103 }
104 let chars: Vec<char> = integer_str.chars().collect();
105 let len = chars.len();
106 let comma_count = (len - 1) / 3;
108 let mut result = String::with_capacity(len + comma_count);
109 for (i, ch) in chars.iter().enumerate() {
110 if i > 0 && (len - i) % 3 == 0 {
111 result.push(',');
112 }
113 result.push(*ch);
114 }
115 result
116}
117
118#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
127 fn currency_default_precision() {
128 assert_eq!(
129 format_data_value(
130 "1234.5",
131 &DataFormat::Currency {
132 locale: None,
133 precision: None
134 }
135 ),
136 "$1,234.50"
137 );
138 }
139
140 #[test]
141 fn currency_zero_precision() {
142 assert_eq!(
143 format_data_value(
144 "9999.99",
145 &DataFormat::Currency {
146 locale: None,
147 precision: Some(0)
148 }
149 ),
150 "$10,000"
151 );
152 }
153
154 #[test]
155 fn currency_negative() {
156 assert_eq!(
157 format_data_value(
158 "-42.5",
159 &DataFormat::Currency {
160 locale: None,
161 precision: None
162 }
163 ),
164 "-$42.50"
165 );
166 }
167
168 #[test]
169 fn currency_thousands() {
170 assert_eq!(
171 format_data_value(
172 "1000000.0",
173 &DataFormat::Currency {
174 locale: None,
175 precision: Some(2)
176 }
177 ),
178 "$1,000,000.00"
179 );
180 }
181
182 #[test]
183 fn currency_small() {
184 assert_eq!(
185 format_data_value(
186 "5.0",
187 &DataFormat::Currency {
188 locale: None,
189 precision: Some(2)
190 }
191 ),
192 "$5.00"
193 );
194 }
195
196 #[test]
199 fn percent_default_precision() {
200 assert_eq!(
201 format_data_value("0.1234", &DataFormat::Percent { precision: None }),
202 "12.3%"
203 );
204 }
205
206 #[test]
207 fn percent_zero_precision() {
208 assert_eq!(
209 format_data_value("0.5", &DataFormat::Percent { precision: Some(0) }),
210 "50%"
211 );
212 }
213
214 #[test]
215 fn percent_negative() {
216 assert_eq!(
217 format_data_value("-0.05", &DataFormat::Percent { precision: Some(1) }),
218 "-5.0%"
219 );
220 }
221
222 #[test]
223 fn percent_high_precision() {
224 assert_eq!(
225 format_data_value("0.12345", &DataFormat::Percent { precision: Some(3) }),
226 "12.345%"
227 );
228 }
229
230 #[test]
233 fn number_default_precision() {
234 assert_eq!(
235 format_data_value("1234567.8", &DataFormat::Number { precision: None }),
236 "1,234,568"
237 );
238 }
239
240 #[test]
241 fn number_with_precision() {
242 assert_eq!(
243 format_data_value("1234.5", &DataFormat::Number { precision: Some(2) }),
244 "1,234.50"
245 );
246 }
247
248 #[test]
249 fn number_negative() {
250 assert_eq!(
251 format_data_value("-9876.0", &DataFormat::Number { precision: Some(0) }),
252 "-9,876"
253 );
254 }
255
256 #[test]
257 fn number_small_no_thousands() {
258 assert_eq!(
259 format_data_value("42.0", &DataFormat::Number { precision: Some(0) }),
260 "42"
261 );
262 }
263
264 #[test]
267 fn passthrough_non_numeric() {
268 assert_eq!(
269 format_data_value(
270 "N/A",
271 &DataFormat::Currency {
272 locale: None,
273 precision: None
274 }
275 ),
276 "N/A"
277 );
278 }
279
280 #[test]
281 fn passthrough_empty() {
282 assert_eq!(
283 format_data_value("", &DataFormat::Number { precision: None }),
284 ""
285 );
286 }
287
288 #[test]
289 fn passthrough_string_with_letters() {
290 assert_eq!(
291 format_data_value("twelve", &DataFormat::Percent { precision: None }),
292 "twelve"
293 );
294 }
295}