i3status_rs/formatting/formatter/
eng.rs

1use crate::formatting::prefix::Prefix;
2use crate::formatting::unit::Unit;
3
4use std::borrow::Cow;
5use std::ops::RangeInclusive;
6
7use super::*;
8
9const DEFAULT_NUMBER_WIDTH: usize = 2;
10
11pub const DEFAULT_NUMBER_FORMATTER: EngFormatter = EngFormatter {
12    show: true,
13    width: DEFAULT_NUMBER_WIDTH,
14    unit: None,
15    unit_has_space: false,
16    unit_hidden: false,
17    prefix: None,
18    prefix_has_space: false,
19    prefix_hidden: false,
20    prefix_forced: false,
21    pad_with: DEFAULT_NUMBER_PAD_WITH,
22    range: f64::NEG_INFINITY..=f64::INFINITY,
23};
24
25#[derive(Debug)]
26pub struct EngFormatter {
27    show: bool,
28    width: usize,
29    unit: Option<Unit>,
30    unit_has_space: bool,
31    unit_hidden: bool,
32    prefix: Option<Prefix>,
33    prefix_has_space: bool,
34    prefix_hidden: bool,
35    prefix_forced: bool,
36    pad_with: PadWith,
37    range: RangeInclusive<f64>,
38}
39
40impl EngFormatter {
41    pub(super) fn from_args(args: &[Arg]) -> Result<Self> {
42        let mut result = DEFAULT_NUMBER_FORMATTER;
43
44        for arg in args {
45            match arg.key {
46                "width" | "w" => {
47                    result.width = arg.parse_value()?;
48                }
49                "unit" | "u" => {
50                    result.unit = Some(arg.parse_value()?);
51                }
52                "hide_unit" => {
53                    result.unit_hidden = arg.parse_value()?;
54                }
55                "unit_space" => {
56                    result.unit_has_space = arg.parse_value()?;
57                }
58                "prefix" | "p" => {
59                    result.prefix = Some(arg.parse_value()?);
60                }
61                "hide_prefix" => {
62                    result.prefix_hidden = arg.parse_value()?;
63                }
64                "prefix_space" => {
65                    result.prefix_has_space = arg.parse_value()?;
66                }
67                "force_prefix" => {
68                    result.prefix_forced = arg.parse_value()?;
69                }
70                "pad_with" => {
71                    let pad_with_str = arg.val.error("pad_with must be specified")?;
72                    if pad_with_str.graphemes(true).count() < 2 {
73                        result.pad_with = Cow::Owned(pad_with_str.into());
74                    } else {
75                        return Err(Error::new(
76                            "pad_with must be an empty string or a single character",
77                        ));
78                    }
79                }
80                "range" => {
81                    let (start, end) = arg
82                        .val
83                        .error("range must be specified")?
84                        .split_once("..")
85                        .error("invalid range")?;
86                    if !start.is_empty() {
87                        result.range = start.parse::<f64>().error("invalid range start")?
88                            ..=*result.range.end();
89                    }
90                    if !end.is_empty() {
91                        result.range = *result.range.start()
92                            ..=end.parse::<f64>().error("invalid range end")?;
93                    }
94                }
95                "show" => {
96                    result.show = arg.parse_value()?;
97                }
98                other => {
99                    return Err(Error::new(format!("Unknown argument for 'eng': '{other}'")));
100                }
101            }
102        }
103
104        Ok(result)
105    }
106}
107
108impl Formatter for EngFormatter {
109    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
110        match val {
111            &Value::Number { mut val, mut unit } => {
112                if !self.range.contains(&val) {
113                    return Err(FormatError::NumberOutOfRange(val));
114                }
115
116                if !self.show {
117                    return Ok(String::new());
118                }
119
120                let is_negative = val.is_sign_negative();
121                if is_negative {
122                    val = -val;
123                }
124
125                if let Some(new_unit) = self.unit {
126                    val = unit.convert(val, new_unit)?;
127                    unit = new_unit;
128                }
129
130                let (min_prefix, max_prefix) = match (self.prefix, self.prefix_forced) {
131                    (Some(prefix), true) => (prefix, prefix),
132                    (Some(prefix), false) => (prefix, Prefix::max_available()),
133                    (None, _) => (Prefix::min_available(), Prefix::max_available()),
134                };
135
136                let mut prefix = unit
137                    .clamp_prefix(if min_prefix.is_binary() {
138                        Prefix::eng_binary(val)
139                    } else {
140                        Prefix::eng(val)
141                    })
142                    .clamp(min_prefix, max_prefix);
143                val = prefix.apply(val);
144
145                let mut digits = (val.max(1.).log10().floor() + 1.0) as i32 + is_negative as i32;
146
147                // handle rounding
148                if self.width as i32 - digits >= 1 {
149                    let round_up_to = self.width as i32 - digits - 1;
150                    let m = 10f64.powi(round_up_to);
151                    val = (val * m).round() / m;
152                    digits = (val.max(1.).log10().floor() + 1.0) as i32 + is_negative as i32;
153                }
154
155                let sign = if is_negative { "-" } else { "" };
156                let mut retval = match self.width as i32 - digits {
157                    i32::MIN..=1 => {
158                        // Remove prefix after rounding value to be displayed
159                        val = prefix.unapply(val.round());
160
161                        prefix = unit
162                            .clamp_prefix(if min_prefix.is_binary() {
163                                Prefix::eng_binary(val)
164                            } else {
165                                Prefix::eng(val)
166                            })
167                            .clamp(min_prefix, max_prefix);
168                        val = prefix.apply(val);
169
170                        digits = (val.max(1.).log10().floor() + 1.0) as i32 + is_negative as i32;
171
172                        match self.width as i32 - digits {
173                            i32::MIN..=0 => format!("{sign}{}", val),
174                            1 => format!("{}{sign}{}", self.pad_with, val as i64),
175                            rest => format!("{sign}{val:.*}", rest as usize - 1),
176                        }
177                    }
178                    rest => format!("{sign}{val:.*}", rest as usize - 1),
179                };
180
181                let display_prefix =
182                    !self.prefix_hidden && prefix != Prefix::One && prefix != Prefix::OneButBinary;
183                let display_unit = !self.unit_hidden && unit != Unit::None;
184
185                if display_prefix {
186                    if self.prefix_has_space {
187                        retval.push(' ');
188                    }
189                    retval.push_str(&prefix.to_string());
190                }
191                if display_unit {
192                    if self.unit_has_space || (self.prefix_has_space && !display_prefix) {
193                        retval.push(' ');
194                    }
195                    retval.push_str(&unit.to_string());
196                }
197
198                Ok(retval)
199            }
200            other => Err(FormatError::IncompatibleFormatter {
201                ty: other.type_name(),
202                fmt: "eng",
203            }),
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn eng_rounding_and_negatives() {
214        let fmt = new_fmt!(eng, w: 3).unwrap();
215        let config = SharedConfig::default();
216
217        let result = fmt
218            .format(
219                &Value::Number {
220                    val: -1.0,
221                    unit: Unit::None,
222                },
223                &config,
224            )
225            .unwrap();
226        assert_eq!(result, " -1");
227
228        let result = fmt
229            .format(
230                &Value::Number {
231                    val: 9.9999,
232                    unit: Unit::None,
233                },
234                &config,
235            )
236            .unwrap();
237        assert_eq!(result, " 10");
238
239        let result = fmt
240            .format(
241                &Value::Number {
242                    val: 999.9,
243                    unit: Unit::Bytes,
244                },
245                &config,
246            )
247            .unwrap();
248        assert_eq!(result, "1.0KB");
249
250        let result = fmt
251            .format(
252                &Value::Number {
253                    val: -9.99,
254                    unit: Unit::None,
255                },
256                &config,
257            )
258            .unwrap();
259        assert_eq!(result, "-10");
260
261        let result = fmt
262            .format(
263                &Value::Number {
264                    val: 9.94,
265                    unit: Unit::None,
266                },
267                &config,
268            )
269            .unwrap();
270        assert_eq!(result, "9.9");
271
272        let result = fmt
273            .format(
274                &Value::Number {
275                    val: 9.95,
276                    unit: Unit::None,
277                },
278                &config,
279            )
280            .unwrap();
281        assert_eq!(result, " 10");
282
283        let fmt = new_fmt!(eng, w: 5, p: 1).unwrap();
284        let result = fmt
285            .format(
286                &Value::Number {
287                    val: 321_600_000_000.,
288                    unit: Unit::Bytes,
289                },
290                &config,
291            )
292            .unwrap();
293        assert_eq!(result, "321.6GB");
294
295        let fmt = new_fmt!(eng, w: 3, p: K).unwrap();
296        let result = fmt
297            .format(
298                &Value::Number {
299                    val: 998_888.,
300                    unit: Unit::Bytes,
301                },
302                &config,
303            )
304            .unwrap();
305        assert_eq!(result, "999KB");
306
307        let result = fmt
308            .format(
309                &Value::Number {
310                    val: 999_888.,
311                    unit: Unit::Bytes,
312                },
313                &config,
314            )
315            .unwrap();
316        assert_eq!(result, "1.0MB");
317
318        let result = fmt
319            .format(
320                &Value::Number {
321                    val: 1_000_000.,
322                    unit: Unit::Bytes,
323                },
324                &config,
325            )
326            .unwrap();
327        assert_eq!(result, "1.0MB");
328    }
329
330    #[test]
331    fn eng_prefixes() {
332        let config = SharedConfig::default();
333        // 14.96 GiB
334        let val = Value::Number {
335            val: 14.96 * 1024. * 1024. * 1024.,
336            unit: Unit::Bytes,
337        };
338
339        let fmt = new_fmt!(eng, w: 5, p: Mi).unwrap();
340        let result = fmt.format(&val, &config).unwrap();
341        assert_eq!(result, "14.96GiB");
342
343        let fmt = new_fmt!(eng, w: 4, p: Mi).unwrap();
344        let result = fmt.format(&val, &config).unwrap();
345        assert_eq!(result, "15.0GiB");
346
347        let fmt = new_fmt!(eng, w: 3, p: Mi).unwrap();
348        let result = fmt.format(&val, &config).unwrap();
349        assert_eq!(result, " 15GiB");
350
351        let fmt = new_fmt!(eng, w: 2, p: Mi).unwrap();
352        let result = fmt.format(&val, &config).unwrap();
353        assert_eq!(result, "15GiB");
354    }
355}