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