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