Skip to main content

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}