Skip to main content

ggplot_rs/
format.rs

1//! Public value and label formatting helpers.
2//!
3//! [`format_value`] renders a single data [`Value`] the way ggplot-rs shows it
4//! in hover tooltips (rounded numbers, calendar dates for datetimes). The
5//! `label_*` axis/legend formatters from [`crate::scale::format`] are
6//! re-exported here so hosts can format numbers consistently with the plot.
7
8use crate::data::Value;
9
10pub use crate::locale::{CurrencyPosition, Locale};
11pub use crate::scale::format::{
12    label_bytes, label_comma, label_comma_locale, label_comma_marks, label_currency,
13    label_currency_locale, label_currency_marks, label_dollar, label_number, label_number_locale,
14    label_number_marks, label_ordinal, label_percent, label_percent_locale, label_percent_marks,
15    label_scientific, label_si, LabelFormatter,
16};
17
18/// Format a timestamp (seconds since the Unix epoch, UTC) with the same
19/// strftime subset the datetime scales use for their labels —
20/// `%Y %y %m %-m %b %B %d %-d %e %a %A %H %M %S %%` — and the month / weekday
21/// names of `locale` (English without). For hosts that format a date the way
22/// an axis would, e.g. in their own tooltips.
23///
24/// ```
25/// use ggplot_rs::format::format_timestamp;
26/// assert_eq!(format_timestamp(86_400.0, "%d %b %Y", None), "02 Jan 1970");
27/// let de = ggplot_rs::format::Locale::from_tag("de-DE");
28/// assert_eq!(format_timestamp(86_400.0 * 40.0, "%-d. %B", Some(&de)), "10. Februar");
29/// ```
30pub fn format_timestamp(secs: f64, fmt: &str, locale: Option<&Locale>) -> String {
31    crate::scale::datetime::strftime_locale(secs, fmt, locale)
32}
33
34/// Format one data value for display (tooltips, data labels):
35///
36/// - strings as-is, booleans as `true`/`false`, `NA` as the empty string;
37/// - numbers rounded to three decimals with trailing zeros dropped
38///   (`3.14159` → `"3.142"`, `2.0` → `"2"`);
39/// - datetimes (epoch seconds) as `YYYY-MM-DD[ HH:MM:SS]` UTC.
40///
41/// ```
42/// use ggplot_rs::data::Value;
43/// use ggplot_rs::format::format_value;
44/// assert_eq!(format_value(&Value::Float(3.14159)), "3.142");
45/// assert_eq!(format_value(&Value::DateTime(86_400)), "1970-01-02");
46/// assert_eq!(format_value(&Value::Na), "");
47/// ```
48pub fn format_value(v: &Value) -> String {
49    match v {
50        Value::Str(s) => s.clone(),
51        Value::Bool(b) => b.to_string(),
52        Value::Na => String::new(),
53        // A datetime axis reads as a calendar date, not raw epoch seconds.
54        Value::DateTime(secs) => crate::data::format_epoch_secs(*secs),
55        _ => v
56            .as_f64()
57            .map(|f| format!("{}", (f * 1000.0).round() / 1000.0))
58            .unwrap_or_default(),
59    }
60}
61
62/// [`format_value`] in the conventions of `locale`: numbers get its decimal
63/// and thousands marks, datetimes its date format and month names.
64///
65/// ```
66/// use ggplot_rs::data::Value;
67/// use ggplot_rs::format::{format_value_locale, Locale};
68/// let de = Locale::de_de();
69/// assert_eq!(format_value_locale(&Value::Float(1234.56789), &de), "1.234,568");
70/// assert_eq!(format_value_locale(&Value::DateTime(1_792_022_400), &de), "15. Okt. 2026");
71/// ```
72pub fn format_value_locale(v: &Value, locale: &Locale) -> String {
73    match v {
74        Value::DateTime(secs) => locale.format_datetime(*secs),
75        Value::Str(_) | Value::Bool(_) | Value::Na => format_value(v),
76        _ => locale.localize_number(&format_value(v)),
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn formats_each_variant() {
86        assert_eq!(format_value(&Value::Str("a<b".into())), "a<b");
87        assert_eq!(format_value(&Value::Bool(true)), "true");
88        assert_eq!(format_value(&Value::Integer(-42)), "-42");
89        assert_eq!(format_value(&Value::Float(2.0)), "2");
90        assert_eq!(format_value(&Value::Float(0.12345)), "0.123");
91        assert_eq!(format_value(&Value::DateTime(3_661)), "1970-01-01 01:01:01");
92        assert_eq!(format_value(&Value::Na), "");
93    }
94
95    #[test]
96    fn formats_each_variant_with_locale() {
97        let de = Locale::de_de();
98        assert_eq!(format_value_locale(&Value::Str("1.5".into()), &de), "1.5");
99        assert_eq!(format_value_locale(&Value::Integer(-4200), &de), "-4.200");
100        assert_eq!(format_value_locale(&Value::Float(0.5), &de), "0,5");
101        assert_eq!(format_value_locale(&Value::Na, &de), "");
102        assert_eq!(
103            format_value_locale(&Value::DateTime(1_792_022_400), &Locale::en_us()),
104            "Oct 15, 2026"
105        );
106        assert_eq!(
107            format_value_locale(&Value::Float(1234.5), &Locale::en_us()),
108            "1,234.5"
109        );
110    }
111}
112
113#[cfg(test)]
114mod format_timestamp_tests {
115    use super::*;
116
117    #[test]
118    fn formats_like_the_datetime_scales() {
119        assert_eq!(
120            format_timestamp(0.0, "%Y-%m-%d %H:%M", None),
121            "1970-01-01 00:00"
122        );
123        assert_eq!(
124            format_timestamp(86_400.0 * 59.0, "%a %-d %b %y", None),
125            "Sun 1 Mar 70"
126        );
127        let de = Locale::from_tag("de-DE");
128        assert_eq!(
129            format_timestamp(86_400.0 * 59.0, "%A, %-d. %B %Y", Some(&de)),
130            "Sonntag, 1. März 1970"
131        );
132    }
133}