chartml_core/format/mod.rs
1mod number;
2mod date;
3
4pub use number::NumberFormatter;
5pub use date::{DateFormatter, detect_date_format, reformat_date_label};
6
7/// Format a numeric value using a d3-format string, or a sensible default.
8///
9/// When `format_str` is `Some`, delegates to [`NumberFormatter`]. When `None`,
10/// uses [`default_format_value`] which applies comma separators for integers
11/// and minimal-precision formatting for floats.
12pub fn format_value(value: f64, format_str: Option<&str>) -> String {
13 match format_str {
14 Some(fmt) => NumberFormatter::new(fmt).format(value),
15 None => default_format_value(value),
16 }
17}
18
19/// Default numeric formatting: integers get comma separators (e.g. `847,293`),
20/// floats get minimal precision with trailing-zero trimming.
21pub fn default_format_value(value: f64) -> String {
22 if value == value.floor() && value.abs() < 1e15 {
23 // Use comma separator for large integers
24 let abs = value.abs() as u64;
25 let formatted = number::insert_commas(&abs.to_string());
26 if value < 0.0 {
27 format!("-{}", formatted)
28 } else {
29 formatted
30 }
31 } else {
32 // Use enough decimal places to show significant digits.
33 // For values like 0.007, we need 3 decimals; for 1.5, 1 decimal suffices.
34 let abs_val = value.abs();
35 let precision = if abs_val < 1e-15 {
36 1usize
37 } else if abs_val >= 1.0 {
38 // For values >= 1, one decimal is fine (e.g. 3.5 -> "3.5")
39 1usize
40 } else {
41 // For values < 1, compute digits needed: -floor(log10(abs)) gives the
42 // position of the first significant digit. Add 1 to show at least two
43 // significant fractional digits (e.g. 0.007 -> precision 3 -> "0.007").
44 let digits = -(abs_val.log10().floor()) as usize;
45 digits.max(1)
46 };
47 // Format and strip unnecessary trailing zeros after the decimal point,
48 // but keep at least one decimal digit.
49 let formatted = format!("{:.prec$}", value, prec = precision);
50 let trimmed = formatted.trim_end_matches('0');
51 // Ensure we don't end with just a decimal point (e.g. "3." -> "3.0")
52 if trimmed.ends_with('.') {
53 format!("{}0", trimmed)
54 } else {
55 trimmed.to_string()
56 }
57 }
58}