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 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 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 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}