1use std::sync::Arc;
6
7pub fn label_comma(v: f64) -> String {
9 if v == v.round() && v.abs() < 1e15 {
10 let s = format!("{}", v as i64);
11 add_commas(&s)
12 } else {
13 let s = format!("{:.2}", v);
14 let s = s.trim_end_matches('0').trim_end_matches('.');
15 if let Some((int_part, dec_part)) = s.split_once('.') {
16 format!("{}.{}", add_commas(int_part), dec_part)
17 } else {
18 add_commas(s)
19 }
20 }
21}
22
23pub fn label_percent(v: f64) -> String {
25 let pct = v * 100.0;
26 if (pct - pct.round()).abs() < 1e-10 {
27 format!("{}%", pct.round() as i64)
28 } else {
29 format!("{:.1}%", pct)
30 }
31}
32
33pub fn label_dollar(v: f64) -> String {
36 currency(v, "$", "", ",", None)
37}
38
39pub fn label_currency(
53 prefix: &str,
54 suffix: &str,
55 big_mark: &str,
56 accuracy: Option<f64>,
57) -> impl Fn(f64) -> String + Send + Sync {
58 let prefix = prefix.to_string();
59 let suffix = suffix.to_string();
60 let big_mark = big_mark.to_string();
61 move |v: f64| currency(v, &prefix, &suffix, &big_mark, accuracy)
62}
63
64fn currency(v: f64, prefix: &str, suffix: &str, big_mark: &str, accuracy: Option<f64>) -> String {
66 let (_, body) = split_sign(format_accuracy(v.abs(), accuracy));
69 let sign = if v < 0.0 && !is_zero_number(&body) {
70 "-"
71 } else {
72 ""
73 };
74 let body = if big_mark == "," {
75 body
76 } else {
77 body.replace(',', big_mark)
78 };
79 format!("{sign}{prefix}{body}{suffix}")
80}
81
82fn split_sign(s: String) -> (&'static str, String) {
84 match s.strip_prefix('-') {
85 Some(rest) => ("-", rest.to_string()),
86 None => ("", s),
87 }
88}
89
90fn is_zero_number(s: &str) -> bool {
92 s.chars().all(|c| matches!(c, '0' | '.' | ','))
93}
94
95pub fn label_scientific(v: f64) -> String {
97 if v == 0.0 {
98 return "0".to_string();
99 }
100 let exp = v.abs().log10().floor() as i32;
101 let mantissa = v / 10f64.powi(exp);
102 if (mantissa - mantissa.round()).abs() < 1e-10 {
103 format!("{}e{}", mantissa.round() as i64, exp)
104 } else {
105 let s = format!("{:.2}e{}", mantissa, exp);
106 if let Some((m, e)) = s.split_once('e') {
108 let m = m.trim_end_matches('0').trim_end_matches('.');
109 format!("{m}e{e}")
110 } else {
111 s
112 }
113 }
114}
115
116fn add_commas(s: &str) -> String {
117 let negative = s.starts_with('-');
118 let digits = if negative { &s[1..] } else { s };
119 let mut result = String::new();
120 for (i, ch) in digits.chars().rev().enumerate() {
121 if i > 0 && i % 3 == 0 {
122 result.push(',');
123 }
124 result.push(ch);
125 }
126 let formatted: String = result.chars().rev().collect();
127 if negative {
128 format!("-{formatted}")
129 } else {
130 formatted
131 }
132}
133
134pub type LabelFormatter = Arc<dyn Fn(f64) -> String + Send + Sync>;
138
139fn format_accuracy(v: f64, accuracy: Option<f64>) -> String {
141 match accuracy {
142 Some(acc) if acc > 0.0 => {
143 let rounded = (v / acc).round() * acc;
144 let decimals = (-acc.log10().floor()).max(0.0) as usize;
145 let s = format!("{rounded:.decimals$}");
146 match s.split_once('.') {
148 Some((int_part, dec_part)) => format!("{}.{dec_part}", add_commas(int_part)),
149 None => add_commas(&s),
150 }
151 }
152 _ => label_comma(v),
153 }
154}
155
156pub fn label_number(
161 accuracy: Option<f64>,
162 prefix: &str,
163 suffix: &str,
164 scale: f64,
165) -> impl Fn(f64) -> String + Send + Sync {
166 let prefix = prefix.to_string();
167 let suffix = suffix.to_string();
168 move |v: f64| {
169 let (sign, body) = split_sign(format_accuracy(v * scale, accuracy));
172 let sign = if is_zero_number(&body) { "" } else { sign };
173 format!("{sign}{prefix}{body}{suffix}")
174 }
175}
176
177pub fn label_si() -> impl Fn(f64) -> String + Send + Sync {
179 |v: f64| {
180 if v == 0.0 {
181 return "0".to_string();
182 }
183 let a = v.abs();
184 let (div, suffix) = if a >= 1e12 {
185 (1e12, "T")
186 } else if a >= 1e9 {
187 (1e9, "G")
188 } else if a >= 1e6 {
189 (1e6, "M")
190 } else if a >= 1e3 {
191 (1e3, "k")
192 } else if a >= 1.0 {
193 (1.0, "")
194 } else if a >= 1e-3 {
195 (1e-3, "m")
196 } else if a >= 1e-6 {
197 (1e-6, "µ")
198 } else {
199 (1e-9, "n")
200 };
201 let scaled = v / div;
202 let s = format!("{scaled:.1}");
203 let s = s.trim_end_matches('0').trim_end_matches('.');
204 format!("{s}{suffix}")
205 }
206}
207
208pub fn label_ordinal() -> impl Fn(f64) -> String + Send + Sync {
210 |v: f64| {
211 let n = v.round() as i64;
212 let suffix = match (n.rem_euclid(10), n.rem_euclid(100)) {
213 (1, r) if r != 11 => "st",
214 (2, r) if r != 12 => "nd",
215 (3, r) if r != 13 => "rd",
216 _ => "th",
217 };
218 format!("{n}{suffix}")
219 }
220}
221
222pub fn label_bytes(binary: bool) -> impl Fn(f64) -> String + Send + Sync {
225 let (base, units): (f64, &[&str]) = if binary {
226 (1024.0, &["B", "KiB", "MiB", "GiB", "TiB"])
227 } else {
228 (1000.0, &["B", "kB", "MB", "GB", "TB"])
229 };
230 move |v: f64| {
231 let a = v.abs();
232 if a < base {
233 return format!("{} {}", v.round() as i64, units[0]);
234 }
235 let mut val = a;
236 let mut i = 0;
237 while val >= base && i < units.len() - 1 {
238 val /= base;
239 i += 1;
240 }
241 let s = format!("{val:.1}");
242 let s = s.trim_end_matches('0').trim_end_matches('.');
243 let sign = if v < 0.0 { "-" } else { "" };
244 format!("{sign}{s} {}", units[i])
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_label_comma() {
254 assert_eq!(label_comma(1000.0), "1,000");
255 assert_eq!(label_comma(1234567.0), "1,234,567");
256 assert_eq!(label_comma(42.0), "42");
257 assert_eq!(label_comma(-5000.0), "-5,000");
258 }
259
260 #[test]
261 fn test_label_percent() {
262 assert_eq!(label_percent(0.5), "50%");
263 assert_eq!(label_percent(0.0), "0%");
264 assert_eq!(label_percent(1.0), "100%");
265 assert_eq!(label_percent(0.123), "12.3%");
266 }
267
268 #[test]
269 fn test_label_dollar() {
270 assert_eq!(label_dollar(1000.0), "$1,000");
271 assert_eq!(label_dollar(0.0), "$0");
272 assert_eq!(label_dollar(-500.0), "-$500");
273 }
274
275 #[test]
276 fn test_label_scientific() {
277 assert_eq!(label_scientific(12345.0), "1.23e4");
278 assert_eq!(label_scientific(0.0), "0");
279 assert_eq!(label_scientific(100.0), "1e2");
280 }
281
282 #[test]
283 fn test_label_si() {
284 let f = label_si();
285 assert_eq!(f(1500.0), "1.5k");
286 assert_eq!(f(2_300_000.0), "2.3M");
287 assert_eq!(f(5e9), "5G");
288 assert_eq!(f(0.0), "0");
289 assert_eq!(f(0.0005), "500µ");
290 assert_eq!(f(-4000.0), "-4k");
291 }
292
293 #[test]
294 fn test_label_number() {
295 let f = label_number(Some(0.1), "", " kg", 1.0);
296 assert_eq!(f(4.16), "4.2 kg");
297 let pct = label_number(Some(1.0), "", "%", 100.0);
298 assert_eq!(pct(0.25), "25%");
299 let money = label_number(None, "€", "", 1.0);
300 assert_eq!(money(1500.0), "€1,500");
301 }
302
303 #[test]
304 fn label_number_is_sign_aware() {
305 let eur = label_number(None, "€", "", 1.0);
306 assert_eq!(eur(-5.0), "-€5");
307 assert_eq!(eur(-1234.5), "-€1,234.5");
308 let f = label_number(Some(1.0), "€", "", 1.0);
309 assert_eq!(f(-0.2), "€0", "rounds to zero: no sign");
310 assert_eq!(f(1_234_567.0), "€1,234,567");
311 let g = label_number(Some(0.01), "", "", 1.0);
313 assert_eq!(g(1234.5), "1,234.50");
314 assert_eq!(g(-98765.4321), "-98,765.43");
315 }
316
317 #[test]
318 fn label_currency_marks_and_signs() {
319 let eur = label_currency("€", "", ".", Some(1.0));
320 assert_eq!(eur(-1_234_567.0), "-€1.234.567");
321 assert_eq!(eur(0.0), "€0");
322 let chf = label_currency("", " CHF", "'", Some(0.01));
323 assert_eq!(chf(9876.5), "9'876.50 CHF");
324 assert_eq!(chf(-0.001), "0.00 CHF");
325 let plain = label_currency("$", "", "", None);
326 assert_eq!(plain(1234.5), "$1234.5");
327 let usd = label_currency("$", "", ",", None);
328 for v in [0.0, 1.0, -500.0, 1234.5, -1_000_000.0, 0.125] {
329 assert_eq!(usd(v), label_dollar(v), "label_dollar parity at {v}");
330 }
331 assert_eq!(label_dollar(-1234.0), "-$1,234");
332 assert_eq!(usd(f64::NAN), "$NaN");
333 assert_eq!(usd(f64::NEG_INFINITY), "-$inf");
334 }
335
336 #[test]
337 fn test_label_ordinal() {
338 let f = label_ordinal();
339 assert_eq!(f(1.0), "1st");
340 assert_eq!(f(2.0), "2nd");
341 assert_eq!(f(3.0), "3rd");
342 assert_eq!(f(4.0), "4th");
343 assert_eq!(f(11.0), "11th");
344 assert_eq!(f(22.0), "22nd");
345 }
346
347 #[test]
348 fn test_label_bytes() {
349 let f = label_bytes(false);
350 assert_eq!(f(500.0), "500 B");
351 assert_eq!(f(1500.0), "1.5 kB");
352 assert_eq!(f(2_000_000.0), "2 MB");
353 let b = label_bytes(true);
354 assert_eq!(b(1024.0), "1 KiB");
355 }
356}