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 {
35 if v < 0.0 {
36 format!("-${}", label_comma(-v))
37 } else {
38 format!("${}", label_comma(v))
39 }
40}
41
42pub fn label_scientific(v: f64) -> String {
44 if v == 0.0 {
45 return "0".to_string();
46 }
47 let exp = v.abs().log10().floor() as i32;
48 let mantissa = v / 10f64.powi(exp);
49 if (mantissa - mantissa.round()).abs() < 1e-10 {
50 format!("{}e{}", mantissa.round() as i64, exp)
51 } else {
52 let s = format!("{:.2}e{}", mantissa, exp);
53 if let Some((m, e)) = s.split_once('e') {
55 let m = m.trim_end_matches('0').trim_end_matches('.');
56 format!("{m}e{e}")
57 } else {
58 s
59 }
60 }
61}
62
63fn add_commas(s: &str) -> String {
64 let negative = s.starts_with('-');
65 let digits = if negative { &s[1..] } else { s };
66 let mut result = String::new();
67 for (i, ch) in digits.chars().rev().enumerate() {
68 if i > 0 && i % 3 == 0 {
69 result.push(',');
70 }
71 result.push(ch);
72 }
73 let formatted: String = result.chars().rev().collect();
74 if negative {
75 format!("-{formatted}")
76 } else {
77 formatted
78 }
79}
80
81pub type LabelFormatter = Arc<dyn Fn(f64) -> String + Send + Sync>;
85
86fn format_accuracy(v: f64, accuracy: Option<f64>) -> String {
88 match accuracy {
89 Some(acc) if acc > 0.0 => {
90 let rounded = (v / acc).round() * acc;
91 let decimals = (-acc.log10().floor()).max(0.0) as usize;
92 add_commas(&format!("{rounded:.decimals$}"))
93 }
94 _ => label_comma(v),
95 }
96}
97
98pub fn label_number(
102 accuracy: Option<f64>,
103 prefix: &str,
104 suffix: &str,
105 scale: f64,
106) -> impl Fn(f64) -> String + Send + Sync {
107 let prefix = prefix.to_string();
108 let suffix = suffix.to_string();
109 move |v: f64| format!("{prefix}{}{suffix}", format_accuracy(v * scale, accuracy))
110}
111
112pub fn label_si() -> impl Fn(f64) -> String + Send + Sync {
114 |v: f64| {
115 if v == 0.0 {
116 return "0".to_string();
117 }
118 let a = v.abs();
119 let (div, suffix) = if a >= 1e12 {
120 (1e12, "T")
121 } else if a >= 1e9 {
122 (1e9, "G")
123 } else if a >= 1e6 {
124 (1e6, "M")
125 } else if a >= 1e3 {
126 (1e3, "k")
127 } else if a >= 1.0 {
128 (1.0, "")
129 } else if a >= 1e-3 {
130 (1e-3, "m")
131 } else if a >= 1e-6 {
132 (1e-6, "µ")
133 } else {
134 (1e-9, "n")
135 };
136 let scaled = v / div;
137 let s = format!("{scaled:.1}");
138 let s = s.trim_end_matches('0').trim_end_matches('.');
139 format!("{s}{suffix}")
140 }
141}
142
143pub fn label_ordinal() -> impl Fn(f64) -> String + Send + Sync {
145 |v: f64| {
146 let n = v.round() as i64;
147 let suffix = match (n.rem_euclid(10), n.rem_euclid(100)) {
148 (1, r) if r != 11 => "st",
149 (2, r) if r != 12 => "nd",
150 (3, r) if r != 13 => "rd",
151 _ => "th",
152 };
153 format!("{n}{suffix}")
154 }
155}
156
157pub fn label_bytes(binary: bool) -> impl Fn(f64) -> String + Send + Sync {
160 let (base, units): (f64, &[&str]) = if binary {
161 (1024.0, &["B", "KiB", "MiB", "GiB", "TiB"])
162 } else {
163 (1000.0, &["B", "kB", "MB", "GB", "TB"])
164 };
165 move |v: f64| {
166 let a = v.abs();
167 if a < base {
168 return format!("{} {}", v.round() as i64, units[0]);
169 }
170 let mut val = a;
171 let mut i = 0;
172 while val >= base && i < units.len() - 1 {
173 val /= base;
174 i += 1;
175 }
176 let s = format!("{val:.1}");
177 let s = s.trim_end_matches('0').trim_end_matches('.');
178 let sign = if v < 0.0 { "-" } else { "" };
179 format!("{sign}{s} {}", units[i])
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_label_comma() {
189 assert_eq!(label_comma(1000.0), "1,000");
190 assert_eq!(label_comma(1234567.0), "1,234,567");
191 assert_eq!(label_comma(42.0), "42");
192 assert_eq!(label_comma(-5000.0), "-5,000");
193 }
194
195 #[test]
196 fn test_label_percent() {
197 assert_eq!(label_percent(0.5), "50%");
198 assert_eq!(label_percent(0.0), "0%");
199 assert_eq!(label_percent(1.0), "100%");
200 assert_eq!(label_percent(0.123), "12.3%");
201 }
202
203 #[test]
204 fn test_label_dollar() {
205 assert_eq!(label_dollar(1000.0), "$1,000");
206 assert_eq!(label_dollar(0.0), "$0");
207 assert_eq!(label_dollar(-500.0), "-$500");
208 }
209
210 #[test]
211 fn test_label_scientific() {
212 assert_eq!(label_scientific(12345.0), "1.23e4");
213 assert_eq!(label_scientific(0.0), "0");
214 assert_eq!(label_scientific(100.0), "1e2");
215 }
216
217 #[test]
218 fn test_label_si() {
219 let f = label_si();
220 assert_eq!(f(1500.0), "1.5k");
221 assert_eq!(f(2_300_000.0), "2.3M");
222 assert_eq!(f(5e9), "5G");
223 assert_eq!(f(0.0), "0");
224 assert_eq!(f(0.0005), "500µ");
225 assert_eq!(f(-4000.0), "-4k");
226 }
227
228 #[test]
229 fn test_label_number() {
230 let f = label_number(Some(0.1), "", " kg", 1.0);
231 assert_eq!(f(4.16), "4.2 kg");
232 let pct = label_number(Some(1.0), "", "%", 100.0);
233 assert_eq!(pct(0.25), "25%");
234 let money = label_number(None, "€", "", 1.0);
235 assert_eq!(money(1500.0), "€1,500");
236 }
237
238 #[test]
239 fn test_label_ordinal() {
240 let f = label_ordinal();
241 assert_eq!(f(1.0), "1st");
242 assert_eq!(f(2.0), "2nd");
243 assert_eq!(f(3.0), "3rd");
244 assert_eq!(f(4.0), "4th");
245 assert_eq!(f(11.0), "11th");
246 assert_eq!(f(22.0), "22nd");
247 }
248
249 #[test]
250 fn test_label_bytes() {
251 let f = label_bytes(false);
252 assert_eq!(f(500.0), "500 B");
253 assert_eq!(f(1500.0), "1.5 kB");
254 assert_eq!(f(2_000_000.0), "2 MB");
255 let b = label_bytes(true);
256 assert_eq!(b(1024.0), "1 KiB");
257 }
258}