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