i3status_rs/formatting/formatter/
eng.rs1use 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 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 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 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}