si_scale/
format.rs

1//! The `format_value` macro.
2
3/// Formats a [`Value`][`crate::value::Value`]'s mantissa and unit prefix (but
4/// not the unit itself). Because it simply delegates to
5/// [`format_args!()`][`std::format_args`], the output should be consumed by
6/// macros such as `println!()`, `write!()`, etc.
7///
8/// It provides more control than the `Display` implementation in
9/// [`Value`][`crate::value::Value`] because you can provide the number
10/// formatting.
11///
12/// # Example
13///
14/// ```
15/// use si_scale::{value::Value, format_value};
16///
17/// let x = 3.4e-12f32;
18/// let v: Value = x.into();
19/// let unit = "F"; // just something displayable.
20///
21/// let actual = format!("result is {}{u}",
22///     format_value!(v, "{:>8.2}"), u = unit
23/// );
24/// let expected = "result is     3.40 pF";
25/// assert_eq!(actual, expected);
26///
27/// // left alignment
28///
29/// let actual = format!("result is {}{u}",
30///     format_value!(v, "{:<8.3}"), u = unit
31/// );
32/// let expected = "result is 3.400    pF";
33/// assert_eq!(actual, expected);
34/// ```
35///
36/// Additionally, you can provide a symbol for thousands' groupings.
37///
38/// # Example
39///
40/// In this example, the number `x` is converted into a value and displayed
41/// using the most appropriate SI prefix. The user chose to constrain the
42/// prefix to be anything lower than `Unit` (1) because kilo-seconds make
43/// no sense.
44///
45/// ```
46/// use si_scale::format_value;
47/// # fn main() {
48/// use si_scale::{value::Value, base::Base, prefix::Constraint};
49///
50/// let x = 1234.5678;
51/// let v = Value::new_with(x, Base::B1000, Constraint::UnitAndBelow);
52/// let unit = "s";
53///
54/// let actual = format!(
55///     "result is {}{u}",
56///     format_value!(v, "{:.5}", groupings: '_'),
57///     u = unit
58/// );
59/// let expected = "result is 1_234.567_80 s";
60/// assert_eq!(actual, expected);
61/// # }
62/// ```
63///
64#[macro_export]
65macro_rules! format_value {
66    ($name:ident, $fmt_str:literal) => {
67        format_args! {
68            concat!($fmt_str, " {}{}"),
69            $name.mantissa,
70            $name.prefix,
71            match $name.base {
72                $crate::base::Base::B1000 => "",
73                $crate::base::Base::B1024 => if $name.prefix == $crate::prefix::Prefix::Unit {""} else {"i"},
74            },
75        }
76    };
77
78    ($name:ident, $fmt_str:literal, groupings: $separator:expr) => {
79        format_args! {
80            "{} {}{}",
81            $crate::format::separated_float(&format!($fmt_str, $name.mantissa), $separator),
82            $name.prefix,
83            match $name.base {
84                $crate::base::Base::B1000 => "",
85                $crate::base::Base::B1024 => if $name.prefix == $crate::prefix::Prefix::Unit {""} else {"i"},
86            },
87        }
88    };
89
90    ($name:ident, $fmt_str:literal, groupings: $separator:expr, no_unit) => {
91        format_args! {
92            "{}{}{}{}",
93            $crate::format::separated_float(&format!($fmt_str, $name.mantissa), $separator),
94            match $name.prefix {
95                $crate::prefix::Prefix::Unit => "",
96                _=> " "
97            },
98            $name.prefix,
99            match $name.base {
100                $crate::base::Base::B1000 => "",
101                $crate::base::Base::B1024 => if $name.prefix == $crate::prefix::Prefix::Unit {""} else {"i"},
102            },
103        }
104    };
105}
106
107/// Given a input `&str` representing a digit (float or int), this function
108/// returns a `String` in which thousands separators are inserted both on the
109/// integral part and the fractional part.
110///
111pub fn separated_float(input: &str, separator: char) -> String {
112    let idx = match input.find('.') {
113        Some(i) => i,
114        None => input.len(),
115    };
116
117    let int_part = &input[..idx];
118    let frac_part = &input[idx..];
119
120    let int_part_separated = separate_thousands_backward(int_part, separator);
121    let frac_part_separated = separate_thousands_forward(frac_part, separator);
122    int_part_separated + &frac_part_separated
123}
124
125fn separate_thousands_backward(input: &str, separator: char) -> String {
126    let mut output = String::with_capacity(input.len() + input.len() / 4);
127    let mut pos = 0;
128    for ch in input.chars().rev() {
129        if ch.is_ascii_digit() {
130            // don't push a sep on first char
131            if pos > 1 && pos % 3 == 0 {
132                output.push(separator);
133            }
134            pos += 1;
135        }
136        output.push(ch);
137    }
138    output.chars().rev().collect()
139}
140
141fn separate_thousands_forward(input: &str, separator: char) -> String {
142    let mut output = String::with_capacity(input.len() + input.len() / 4);
143    let mut pos = 0;
144    for ch in input.chars() {
145        if ch.is_ascii_digit() {
146            // don't push a sep on first char
147            if pos > 1 && pos % 3 == 0 {
148                output.push(separator);
149            }
150            pos += 1;
151        }
152        output.push(ch);
153    }
154    output
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::format_value;
161    use crate::value::Value;
162
163    #[test]
164    fn format_value_without_groupings() {
165        let x = 3.4e-12f32;
166        let v: Value = x.into();
167        let unit = "F"; // just something displayable.
168
169        let actual = format!("result is {}{u}", format_value!(v, "{:>8.2}"), u = unit);
170        let expected = "result is     3.40 pF";
171        assert_eq!(actual, expected);
172
173        let actual = format!("result is {}{u}", format_value!(v, "{:<8.3}"), u = unit);
174        let expected = "result is 3.400    pF";
175        assert_eq!(actual, expected);
176    }
177
178    #[test]
179    fn format_value_with_groupings() {
180        let x = 1234.5678;
181        let v: Value = x.into();
182        let unit = "m"; // just something displayable.
183
184        let actual = format!(
185            "result is {}{u}",
186            format_value!(v, "{:.7}", groupings: '_'),
187            u = unit
188        );
189        let expected = "result is 1.234_567_8 km";
190        assert_eq!(actual, expected);
191
192        use crate::base::Base;
193        use crate::prefix::Constraint;
194
195        let v = Value::new_with(x, Base::B1000, Constraint::UnitAndBelow);
196        let unit = "s";
197
198        let actual = format!(
199            "result is {}{u}",
200            format_value!(v, "{:.5}", groupings: '_'),
201            u = unit
202        );
203        let expected = "result is 1_234.567_80 s";
204        assert_eq!(actual, expected);
205    }
206
207    #[test]
208    fn separate_float() {
209        let actual: String = separated_float("123456.123456", '_');
210        let expected = "123_456.123_456";
211        assert_eq!(actual, expected);
212
213        let actual: String = separated_float("123456789.123456789", '_');
214        let expected = "123_456_789.123_456_789";
215        assert_eq!(actual, expected);
216
217        let actual: String = separated_float("1234567.1234567", '_');
218        let expected = "1_234_567.123_456_7";
219        assert_eq!(actual, expected);
220
221        let actual: String = separated_float("--1234567.1234567++", '_');
222        let expected = "--1_234_567.123_456_7++";
223        assert_eq!(actual, expected);
224    }
225
226    #[test]
227    fn int_part_with_separate_thousands_backward() {
228        let actual = separate_thousands_backward("123456", '_');
229        let expected = "123_456";
230        assert_eq!(actual, expected);
231
232        let actual = separate_thousands_backward("  123456..", '_');
233        let expected = "  123_456..";
234        assert_eq!(actual, expected);
235    }
236
237    #[test]
238    fn frac_part_with_separate_thousands_forward() {
239        let actual = separate_thousands_forward(".123456789", '_');
240        let expected = ".123_456_789";
241        assert_eq!(actual, expected);
242
243        let actual = separate_thousands_forward(".1234567--", '_');
244        let expected = ".123_456_7--";
245        assert_eq!(actual, expected);
246    }
247
248    #[test]
249    fn format_zero_value() {
250        let x = 0.0f32;
251        let v: Value = x.into();
252        let unit = "F"; // just something displayable.
253
254        let actual = format!("result is {}{u}", format_value!(v, "{:>8.2}"), u = unit);
255        let expected = "result is     0.00 F";
256        assert_eq!(actual, expected);
257
258        let actual = format!("result is {}{u}", format_value!(v, "{:<8.3}"), u = unit);
259        let expected = "result is 0.000    F";
260        assert_eq!(actual, expected);
261    }
262}
263
264// macro_rules! format_scale {
265//     ($name:ident $fmtstr:literal groupings $groupings:expr) => {
266//         pub fn $name(value: Value) -> String {
267//             match value.prefix {
268//                 Some(prefix) => format!(concat!($fmtstr, " {}"), value.mantissa, prefix),
269//                 None => format!($fmtstr, value.mantissa),
270//             }
271//         }
272//     };
273// }
274//
275// format_scale!(scafmt1 "{:<8.3}" groupings "_");