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 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..=0 => format!("{sign}{}", val.round()),
158                    1 => format!("{}{sign}{}", self.pad_with, val.round() as i64),
159                    rest => format!("{sign}{val:.*}", rest as usize - 1),
160                };
161
162                let display_prefix =
163                    !self.prefix_hidden && prefix != Prefix::One && prefix != Prefix::OneButBinary;
164                let display_unit = !self.unit_hidden && unit != Unit::None;
165
166                if display_prefix {
167                    if self.prefix_has_space {
168                        retval.push(' ');
169                    }
170                    retval.push_str(&prefix.to_string());
171                }
172                if display_unit {
173                    if self.unit_has_space || (self.prefix_has_space && !display_prefix) {
174                        retval.push(' ');
175                    }
176                    retval.push_str(&unit.to_string());
177                }
178
179                Ok(retval)
180            }
181            other => Err(FormatError::IncompatibleFormatter {
182                ty: other.type_name(),
183                fmt: "eng",
184            }),
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn eng_rounding_and_negatives() {
195        let fmt = new_fmt!(eng, w: 3).unwrap();
196        let config = SharedConfig::default();
197
198        let result = fmt
199            .format(
200                &Value::Number {
201                    val: -1.0,
202                    unit: Unit::None,
203                },
204                &config,
205            )
206            .unwrap();
207        assert_eq!(result, " -1");
208
209        let result = fmt
210            .format(
211                &Value::Number {
212                    val: 9.9999,
213                    unit: Unit::None,
214                },
215                &config,
216            )
217            .unwrap();
218        assert_eq!(result, " 10");
219
220        let result = fmt
221            .format(
222                &Value::Number {
223                    val: 999.9,
224                    unit: Unit::Bytes,
225                },
226                &config,
227            )
228            .unwrap();
229        assert_eq!(result, "1.0KB");
230
231        let result = fmt
232            .format(
233                &Value::Number {
234                    val: -9.99,
235                    unit: Unit::None,
236                },
237                &config,
238            )
239            .unwrap();
240        assert_eq!(result, "-10");
241
242        let result = fmt
243            .format(
244                &Value::Number {
245                    val: 9.94,
246                    unit: Unit::None,
247                },
248                &config,
249            )
250            .unwrap();
251        assert_eq!(result, "9.9");
252
253        let result = fmt
254            .format(
255                &Value::Number {
256                    val: 9.95,
257                    unit: Unit::None,
258                },
259                &config,
260            )
261            .unwrap();
262        assert_eq!(result, " 10");
263
264        let fmt = new_fmt!(eng, w: 5, p: 1).unwrap();
265        let result = fmt
266            .format(
267                &Value::Number {
268                    val: 321_600_000_000.,
269                    unit: Unit::Bytes,
270                },
271                &config,
272            )
273            .unwrap();
274        assert_eq!(result, "321.6GB");
275    }
276
277    #[test]
278    fn eng_prefixes() {
279        let config = SharedConfig::default();
280        // 14.96 GiB
281        let val = Value::Number {
282            val: 14.96 * 1024. * 1024. * 1024.,
283            unit: Unit::Bytes,
284        };
285
286        let fmt = new_fmt!(eng, w: 5, p: Mi).unwrap();
287        let result = fmt.format(&val, &config).unwrap();
288        assert_eq!(result, "14.96GiB");
289
290        let fmt = new_fmt!(eng, w: 4, p: Mi).unwrap();
291        let result = fmt.format(&val, &config).unwrap();
292        assert_eq!(result, "15.0GiB");
293
294        let fmt = new_fmt!(eng, w: 3, p: Mi).unwrap();
295        let result = fmt.format(&val, &config).unwrap();
296        assert_eq!(result, " 15GiB");
297
298        let fmt = new_fmt!(eng, w: 2, p: Mi).unwrap();
299        let result = fmt.format(&val, &config).unwrap();
300        assert_eq!(result, "15GiB");
301    }
302}