tokenburn_core/utils/
fmt.rs1pub fn thousands(n: u64) -> String {
5 let s = n.to_string();
6 let mut out = String::with_capacity(s.len() + s.len() / 3);
7 for (i, ch) in s.chars().enumerate() {
8 if i > 0 && (s.len() - i).is_multiple_of(3) {
9 out.push(',');
10 }
11 out.push(ch);
12 }
13 out
14}
15
16pub fn compact(n: u64) -> String {
18 let f = n as f64;
19 if n >= 1_000_000_000 {
20 format!("{:.2}B", f / 1e9)
21 } else if n >= 1_000_000 {
22 format!("{:.2}M", f / 1e6)
23 } else if n >= 10_000 {
24 format!("{:.1}k", f / 1e3)
25 } else {
26 n.to_string()
27 }
28}
29
30pub fn usd(v: f64) -> String {
33 format!("${v:.2}")
34}
35
36pub fn percent(fraction: f64) -> String {
38 format!("{:.0}%", fraction * 100.0)
39}
40
41pub fn axis_tokens(v: f64) -> String {
43 let (scaled, unit) = match v {
44 v if v >= 1e9 => (v / 1e9, "B"),
45 v if v >= 1e6 => (v / 1e6, "M"),
46 v if v >= 1e3 => (v / 1e3, "k"),
47 v => (v, ""),
48 };
49 let s = format!("{scaled:.1}");
50 format!("{}{unit}", s.strip_suffix(".0").unwrap_or(&s))
51}
52
53pub fn short_label(label: &str) -> String {
55 match label.len() {
56 16 => label[11..].to_string(),
57 10 => label[5..].to_string(),
58 _ => label.to_string(),
59 }
60}
61
62pub fn short_name(full: &str) -> String {
64 match full.split_once(':') {
65 Some((tool, rest)) => {
66 let last = rest
67 .rsplit(['-', '/', '\\'])
68 .find(|s| !s.is_empty())
69 .unwrap_or(rest);
70 format!("{tool}:{last}")
71 }
72 None => full.to_string(),
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn thousands_groups() {
82 assert_eq!(thousands(0), "0");
83 assert_eq!(thousands(999), "999");
84 assert_eq!(thousands(1000), "1,000");
85 assert_eq!(thousands(1_234_567), "1,234,567");
86 }
87
88 #[test]
89 fn compact_scales() {
90 assert_eq!(compact(999), "999");
91 assert_eq!(compact(12_345), "12.3k");
92 assert_eq!(compact(1_500_000), "1.50M");
93 assert_eq!(compact(2_000_000_000), "2.00B");
94 }
95
96 #[test]
97 fn usd_four_decimals() {
98 assert_eq!(usd(1.5), "$1.50");
99 assert_eq!(usd(0.0), "$0.00");
100 assert_eq!(usd(0.04), "$0.04", "small costs stay readable");
101 assert_eq!(usd(12.34), "$12.34");
102 assert_eq!(usd(165.9051), "$165.91");
103 assert_eq!(usd(1234.56), "$1234.56");
104 }
105
106 #[test]
107 fn percentages_are_whole_numbers() {
108 assert_eq!(percent(0.486), "49%");
109 assert_eq!(percent(1.0), "100%");
110 assert_eq!(percent(0.0), "0%");
111 assert_eq!(percent(0.004), "0%");
112 }
113
114 #[test]
115 fn axis_tokens_have_no_trailing_zeros() {
116 assert_eq!(axis_tokens(20_000_000.0), "20M");
117 assert_eq!(axis_tokens(2_500_000.0), "2.5M");
118 assert_eq!(axis_tokens(15_000.0), "15k");
119 assert_eq!(axis_tokens(800.0), "800");
120 assert_eq!(axis_tokens(0.0), "0");
121 assert_eq!(axis_tokens(3_000_000_000.0), "3B");
122 }
123
124 #[test]
125 fn x_labels_drop_the_year() {
126 assert_eq!(short_label("2026-05-10"), "05-10");
127 assert_eq!(short_label("2026-05-10 13:00"), "13:00");
128 assert_eq!(short_label("2026-05"), "2026-05");
129 }
130
131 #[test]
132 fn short_names_keep_the_tool_and_the_last_path_segment() {
133 assert_eq!(short_name("pi:Users-me-Projects-tokenburn"), "pi:tokenburn");
134 assert_eq!(short_name("zed:claude-sonnet"), "zed:sonnet");
135 assert_eq!(short_name("plain"), "plain");
136 }
137}