i3status_rs/formatting/formatter/
eng.rs

1use crate::formatting::prefix::{Prefix, ValuePrefix};
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
88                            .parse::<ValuePrefix>()
89                            .error("invalid range start")?
90                            .result()..=*result.range.end();
91                    }
92                    if !end.is_empty() {
93                        result.range = *result.range.start()
94                            ..=end
95                                .parse::<ValuePrefix>()
96                                .error("invalid range end")?
97                                .result();
98                    }
99                }
100                "show" => {
101                    result.show = arg.parse_value()?;
102                }
103                other => {
104                    return Err(Error::new(format!("Unknown argument for 'eng': '{other}'")));
105                }
106            }
107        }
108
109        Ok(result)
110    }
111}
112
113impl Formatter for EngFormatter {
114    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
115        match val {
116            &Value::Number { mut val, mut unit } => {
117                if !self.range.contains(&val) {
118                    return Err(FormatError::NumberOutOfRange(val));
119                }
120
121                if !self.show {
122                    return Ok(String::new());
123                }
124
125                let is_negative = val.is_sign_negative();
126                if is_negative {
127                    val = -val;
128                }
129
130                if let Some(new_unit) = self.unit {
131                    val = unit.convert(val, new_unit)?;
132                    unit = new_unit;
133                }
134
135                let (min_prefix, max_prefix) = match (self.prefix, self.prefix_forced) {
136                    (Some(prefix), true) => (prefix, prefix),
137                    (Some(prefix), false) => (prefix, Prefix::max_available()),
138                    (None, _) => (Prefix::min_available(), Prefix::max_available()),
139                };
140
141                let mut prefix = unit
142                    .clamp_prefix(if min_prefix.is_binary() {
143                        Prefix::eng_binary(val)
144                    } else {
145                        Prefix::eng(val)
146                    })
147                    .clamp(min_prefix, max_prefix);
148                val = prefix.apply(val);
149
150                let mut digits = (val.max(1.).log10().floor() + 1.0) as i32 + is_negative as i32;
151
152                // handle rounding
153                if self.width as i32 - digits >= 1 {
154                    let round_up_to = self.width as i32 - digits - 1;
155                    let m = 10f64.powi(round_up_to);
156                    val = (val * m).round() / m;
157                    digits = (val.max(1.).log10().floor() + 1.0) as i32 + is_negative as i32;
158                }
159
160                let sign = if is_negative { "-" } else { "" };
161                let mut retval = match self.width as i32 - digits {
162                    i32::MIN..=1 => {
163                        // Remove prefix after rounding value to be displayed
164                        val = prefix.unapply(val.round());
165
166                        prefix = unit
167                            .clamp_prefix(if min_prefix.is_binary() {
168                                Prefix::eng_binary(val)
169                            } else {
170                                Prefix::eng(val)
171                            })
172                            .clamp(min_prefix, max_prefix);
173                        val = prefix.apply(val);
174
175                        digits = (val.max(1.).log10().floor() + 1.0) as i32 + is_negative as i32;
176
177                        match self.width as i32 - digits {
178                            i32::MIN..=0 => format!("{sign}{}", val),
179                            1 => format!("{}{sign}{}", self.pad_with, val as i64),
180                            rest => format!("{sign}{val:.*}", rest as usize - 1),
181                        }
182                    }
183                    rest => format!("{sign}{val:.*}", rest as usize - 1),
184                };
185
186                let display_prefix =
187                    !self.prefix_hidden && prefix != Prefix::One && prefix != Prefix::OneButBinary;
188                let display_unit = !self.unit_hidden && unit != Unit::None;
189
190                if display_prefix {
191                    if self.prefix_has_space {
192                        retval.push(' ');
193                    }
194                    retval.push_str(&prefix.to_string());
195                }
196                if display_unit {
197                    if self.unit_has_space || (self.prefix_has_space && !display_prefix) {
198                        retval.push(' ');
199                    }
200                    retval.push_str(&unit.to_string());
201                }
202
203                Ok(retval)
204            }
205            other => Err(FormatError::IncompatibleFormatter {
206                ty: other.type_name(),
207                fmt: "eng",
208            }),
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn eng_rounding_and_negatives() {
219        let fmt = new_fmt!(eng, w: 3).unwrap();
220        let config = SharedConfig::default();
221
222        let result = fmt
223            .format(
224                &Value::Number {
225                    val: -1.0,
226                    unit: Unit::None,
227                },
228                &config,
229            )
230            .unwrap();
231        assert_eq!(result, " -1");
232
233        let result = fmt
234            .format(
235                &Value::Number {
236                    val: 9.9999,
237                    unit: Unit::None,
238                },
239                &config,
240            )
241            .unwrap();
242        assert_eq!(result, " 10");
243
244        let result = fmt
245            .format(
246                &Value::Number {
247                    val: 999.9,
248                    unit: Unit::Bytes,
249                },
250                &config,
251            )
252            .unwrap();
253        assert_eq!(result, "1.0KB");
254
255        let result = fmt
256            .format(
257                &Value::Number {
258                    val: -9.99,
259                    unit: Unit::None,
260                },
261                &config,
262            )
263            .unwrap();
264        assert_eq!(result, "-10");
265
266        let result = fmt
267            .format(
268                &Value::Number {
269                    val: 9.94,
270                    unit: Unit::None,
271                },
272                &config,
273            )
274            .unwrap();
275        assert_eq!(result, "9.9");
276
277        let result = fmt
278            .format(
279                &Value::Number {
280                    val: 9.95,
281                    unit: Unit::None,
282                },
283                &config,
284            )
285            .unwrap();
286        assert_eq!(result, " 10");
287
288        let fmt = new_fmt!(eng, w: 5, p: 1).unwrap();
289        let result = fmt
290            .format(
291                &Value::Number {
292                    val: 321_600_000_000.,
293                    unit: Unit::Bytes,
294                },
295                &config,
296            )
297            .unwrap();
298        assert_eq!(result, "321.6GB");
299
300        let fmt = new_fmt!(eng, w: 3, p: K).unwrap();
301        let result = fmt
302            .format(
303                &Value::Number {
304                    val: 998_888.,
305                    unit: Unit::Bytes,
306                },
307                &config,
308            )
309            .unwrap();
310        assert_eq!(result, "999KB");
311
312        let result = fmt
313            .format(
314                &Value::Number {
315                    val: 999_888.,
316                    unit: Unit::Bytes,
317                },
318                &config,
319            )
320            .unwrap();
321        assert_eq!(result, "1.0MB");
322
323        let result = fmt
324            .format(
325                &Value::Number {
326                    val: 1_000_000.,
327                    unit: Unit::Bytes,
328                },
329                &config,
330            )
331            .unwrap();
332        assert_eq!(result, "1.0MB");
333    }
334
335    #[test]
336    fn eng_prefixes() {
337        let config = SharedConfig::default();
338        // 14.96 GiB
339        let val = Value::Number {
340            val: 14.96 * 1024. * 1024. * 1024.,
341            unit: Unit::Bytes,
342        };
343
344        let fmt = new_fmt!(eng, w: 5, p: Mi).unwrap();
345        let result = fmt.format(&val, &config).unwrap();
346        assert_eq!(result, "14.96GiB");
347
348        let fmt = new_fmt!(eng, w: 4, p: Mi).unwrap();
349        let result = fmt.format(&val, &config).unwrap();
350        assert_eq!(result, "15.0GiB");
351
352        let fmt = new_fmt!(eng, w: 3, p: Mi).unwrap();
353        let result = fmt.format(&val, &config).unwrap();
354        assert_eq!(result, " 15GiB");
355
356        let fmt = new_fmt!(eng, w: 2, p: Mi).unwrap();
357        let result = fmt.format(&val, &config).unwrap();
358        assert_eq!(result, "15GiB");
359    }
360
361    #[test]
362    fn eng_range() {
363        let config = SharedConfig::default();
364        let fmt = new_formatter(
365            "eng",
366            &[Arg {
367                key: "range",
368                val: Some("..10Gi"),
369            }],
370        )
371        .unwrap();
372
373        let val = Value::Number {
374            val: 100. * 1000. * 1000.,
375            unit: Unit::Bytes,
376        };
377        let result = fmt.format(&val, &config).unwrap();
378        assert_eq!(result, "100MB");
379
380        let val = Value::Number {
381            val: 100. * 1000. * 1000. * 1000.,
382            unit: Unit::Bytes,
383        };
384        let result = fmt.format(&val, &config);
385        assert!(result.is_err());
386    }
387}