1use std::cmp::Ordering;
6use std::str::FromStr;
7
8#[derive(thiserror::Error, Debug)]
9pub enum Error {
10 #[error("Failed to parse scale in '{0}'")]
11 ScaleParseError(String),
12
13 #[error("Failed to read Qty (num) from '{input}'")]
14 QtyNumberParseError {
15 input: String,
16 #[source] source: std::num::ParseFloatError,
18 },
19}
20
21#[derive(Debug, Clone, Eq, PartialEq, Default)]
22pub struct Scale {
23 label: &'static str,
24 base: u32,
25 pow: i32,
26}
27
28#[rustfmt::skip]
30static SCALES: [Scale;15] = [
31 Scale{ label:"Pi", base: 2, pow: 50},
32 Scale{ label:"Ti", base: 2, pow: 40},
33 Scale{ label:"Gi", base: 2, pow: 30},
34 Scale{ label:"Mi", base: 2, pow: 20},
35 Scale{ label:"Ki", base: 2, pow: 10},
36 Scale{ label:"P", base: 10, pow: 15},
37 Scale{ label:"T", base: 10, pow: 12},
38 Scale{ label:"G", base: 10, pow: 9},
39 Scale{ label:"M", base: 10, pow: 6},
40 Scale{ label:"k", base: 10, pow: 3},
41 Scale{ label:"", base: 10, pow: 0},
42 Scale{ label:"m", base: 10, pow: -3},
43 Scale{ label:"u", base: 10, pow: -6},
44 Scale{ label:"μ", base: 10, pow: -6},
45 Scale{ label:"n", base: 10, pow: -9},
46];
47
48impl FromStr for Scale {
49 type Err = Error;
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 SCALES
52 .iter()
53 .find(|v| v.label == s)
54 .cloned()
55 .ok_or_else(|| Error::ScaleParseError(s.to_owned()))
56 }
57}
58
59impl From<&Scale> for f64 {
60 fn from(v: &Scale) -> f64 {
61 if v.pow == 0 || v.base == 0 {
62 1.0
63 } else {
64 f64::from(v.base).powf(f64::from(v.pow))
65 }
66 }
67}
68
69impl PartialOrd for Scale {
70 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
72 let v1 = f64::from(self);
73 let v2 = f64::from(other);
74 if v1 > v2 {
75 Some(Ordering::Greater)
76 } else if v1 < v2 {
77 Some(Ordering::Less)
78 } else if (v1 - v2).abs() < f64::EPSILON {
79 Some(Ordering::Equal)
80 } else {
81 None
82 }
83 }
84}
85
86impl Scale {
87 pub fn min(&self, other: &Scale) -> Scale {
88 if self < other {
89 self.clone()
90 } else {
91 other.clone()
92 }
93 }
94}
95
96#[derive(Debug, Clone, Eq, PartialEq, Default)]
97pub struct Qty {
98 pub value: i64,
99 pub scale: Scale,
100}
101
102impl From<&Qty> for f64 {
103 fn from(v: &Qty) -> f64 {
104 (v.value as f64) * 0.001
105 }
106}
107
108impl Qty {
109 pub fn zero() -> Self {
110 Self {
111 value: 0,
112 scale: Scale::from_str("").unwrap(),
113 }
114 }
115
116 pub fn lowest_positive() -> Self {
117 Self {
118 value: 1,
119 scale: Scale::from_str("m").unwrap(),
120 }
121 }
122
123 pub fn is_zero(&self) -> bool {
124 self.value == 0
125 }
126
127 pub fn calc_percentage(&self, base100: &Self) -> f64 {
128 if base100.value != 0 {
129 f64::from(self) * 100f64 / f64::from(base100)
130 } else {
131 f64::NAN
132 }
133 }
134
135 pub fn adjust_scale(&self) -> Self {
136 let valuef64 = f64::from(self);
137 let scale = SCALES
138 .iter()
139 .filter(|s| s.base == self.scale.base || self.scale.base == 0)
140 .find(|s| f64::from(*s) <= valuef64);
141 match scale {
142 Some(scale) => Self {
143 value: self.value,
144 scale: scale.clone(),
145 },
146 None => self.clone(),
147 }
148 }
149}
150
151impl FromStr for Qty {
152 type Err = Error;
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 let (num_str, scale_str): (&str, &str) = match s.find(|c: char| {
155 !c.is_ascii_digit() && c != 'E' && c != 'e' && c != '+' && c != '-' && c != '.'
156 }) {
157 Some(pos) => (&s[..pos], &s[pos..]),
158 None => (s, ""),
159 };
160 let scale = Scale::from_str(scale_str.trim())?;
161 let num = f64::from_str(num_str).map_err(|source| Error::QtyNumberParseError {
162 input: num_str.to_owned(),
163 source,
164 })?;
165 let value = (num * f64::from(&scale) * 1000f64) as i64;
166 Ok(Qty { value, scale })
167 }
168}
169
170impl std::fmt::Display for Qty {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(
173 f,
174 "{:.1}{}",
175 (self.value as f64 / (f64::from(&self.scale) * 1000f64)),
176 self.scale.label
177 )
178 }
179}
180
181impl PartialOrd for Qty {
182 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
184 Some(self.cmp(other))
185 }
186}
187
188impl Ord for Qty {
189 fn cmp(&self, other: &Self) -> Ordering {
191 let v1 = self.value; let v2 = other.value; v1.partial_cmp(&v2).unwrap() }
195}
196
197pub fn select_scale_for_add(v1: &Qty, v2: &Qty) -> Scale {
198 if v2.value == 0 {
199 v1.scale.clone()
200 } else if v1.value == 0 {
201 v2.scale.clone()
202 } else {
203 v1.scale.min(&v2.scale)
204 }
205}
206
207impl std::ops::Add for Qty {
208 type Output = Qty;
209 fn add(self, other: Self) -> Qty {
210 &self + &other
211 }
212}
213
214impl std::ops::Add for &Qty {
215 type Output = Qty;
216 fn add(self, other: Self) -> Qty {
217 Qty {
218 value: self.value + other.value,
219 scale: select_scale_for_add(self, other),
220 }
221 }
222}
223
224impl<'b> std::ops::AddAssign<&'b Qty> for Qty {
225 fn add_assign(&mut self, other: &'b Self) {
226 *self = Qty {
227 value: self.value + other.value,
228 scale: select_scale_for_add(self, other),
229 }
230 }
231}
232
233impl std::ops::Sub for Qty {
234 type Output = Qty;
235 fn sub(self, other: Self) -> Qty {
236 &self - &other
237 }
238}
239
240impl std::ops::Sub for &Qty {
241 type Output = Qty;
242 fn sub(self, other: Self) -> Qty {
243 Qty {
244 value: self.value - other.value,
245 scale: select_scale_for_add(self, other),
246 }
247 }
248}
249
250impl<'b> std::ops::SubAssign<&'b Qty> for Qty {
251 fn sub_assign(&mut self, other: &'b Self) {
252 *self = Qty {
253 value: self.value - other.value,
254 scale: select_scale_for_add(self, other),
255 };
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use pretty_assertions::assert_eq;
263
264 macro_rules! assert_is_close {
265 ($x:expr, $y:expr, $range:expr) => {
266 assert!($x >= ($y - $range));
267 assert!($x <= ($y + $range));
268 };
269 }
270
271 #[test]
272 fn test_to_base() -> Result<(), Box<dyn std::error::Error>> {
273 assert_is_close!(
274 f64::from(&Qty::from_str("1k")?),
275 f64::from(&Qty::from_str("1000000m")?),
276 0.01
277 );
278 assert_eq!(
279 Qty::from_str("1Ki")?,
280 Qty {
281 value: 1024000,
282 scale: Scale {
283 label: "Ki",
284 base: 2,
285 pow: 10,
286 },
287 }
288 );
289 Ok(())
290 }
291
292 #[test]
293 fn expectation_ok_for_adjust_scale() -> Result<(), Box<dyn std::error::Error>> {
294 let cases = vec![
295 ("1k", "1.0k"),
296 ("10k", "10.0k"),
297 ("100k", "100.0k"),
298 ("999k", "999.0k"),
299 ("1000k", "1.0M"),
300 ("1999k", "2.0M"), ("1Ki", "1.0Ki"),
302 ("10Ki", "10.0Ki"),
303 ("100Ki", "100.0Ki"),
304 ("1000Ki", "1000.0Ki"),
305 ("1024Ki", "1.0Mi"),
306 ("25641877504", "25.6G"),
307 ("1770653738944", "1.8T"),
308 ("1000m", "1.0"),
309 ("100m", "100.0m"),
310 ("1m", "1.0m"),
311 ];
312 for (input, expected) in cases {
313 assert_eq!(
314 format!("{}", &Qty::from_str(input)?.adjust_scale()),
315 expected.to_string()
316 );
317 }
318 Ok(())
319 }
320
321 #[test]
322 fn test_display() -> Result<(), Box<dyn std::error::Error>> {
323 let cases = vec![
324 ("1k", "1.0k"),
325 ("10k", "10.0k"),
326 ("100k", "100.0k"),
327 ("999k", "999.0k"),
328 ("1000k", "1000.0k"),
329 ("1999k", "1999.0k"),
330 ("1Ki", "1.0Ki"),
331 ("10Ki", "10.0Ki"),
332 ("100Ki", "100.0Ki"),
333 ("1000Ki", "1000.0Ki"),
334 ("1024Ki", "1024.0Ki"),
335 ("25641877504", "25641877504.0"),
336 ("1000m", "1000.0m"),
337 ("100m", "100.0m"),
338 ("1m", "1.0m"),
339 ("1000000n", "1000000.0n"),
340 ("1u", "0.0u"),
342 ("1μ", "0.0μ"),
343 ("1n", "0.0n"),
344 ("999999n", "0.0n"),
345 ];
346 for input in cases {
347 assert_eq!(format!("{}", &Qty::from_str(input.0)?), input.1.to_string());
348 assert_eq!(format!("{}", &Qty::from_str(input.1)?), input.1.to_string());
349 }
350 Ok(())
351 }
352
353 #[test]
354 fn test_f64_from_scale() -> Result<(), Box<dyn std::error::Error>> {
355 assert_is_close!(f64::from(&Scale::from_str("m")?), 0.001, 0.00001);
356 Ok(())
357 }
358
359 #[test]
360 fn test_f64_from_qty() -> Result<(), Box<dyn std::error::Error>> {
361 assert_is_close!(f64::from(&Qty::from_str("20m")?), 0.020, 0.00001);
362 assert_is_close!(f64::from(&Qty::from_str("300m")?), 0.300, 0.00001);
363 assert_is_close!(f64::from(&Qty::from_str("1000m")?), 1.000, 0.00001);
364 assert_is_close!(f64::from(&Qty::from_str("+1000m")?), 1.000, 0.00001);
365 assert_is_close!(f64::from(&Qty::from_str("-1000m")?), -1.000, 0.00001);
366 assert_is_close!(
367 f64::from(&Qty::from_str("3145728e3")?),
368 3145728000.000,
369 0.00001
370 );
371 Ok(())
372 }
373
374 #[test]
375 fn test_add() -> Result<(), Box<dyn std::error::Error>> {
376 assert_eq!(
377 (Qty::from_str("1")?
378 + Qty::from_str("300m")?
379 + Qty::from_str("300m")?
380 + Qty::from_str("300m")?
381 + Qty::from_str("300m")?),
382 Qty::from_str("2200m")?
383 );
384 assert_eq!(
385 Qty::default() + Qty::from_str("300m")?,
386 Qty::from_str("300m")?
387 );
388 assert_eq!(
389 Qty::default() + Qty::from_str("16Gi")?,
390 Qty::from_str("16Gi")?
391 );
392 assert_eq!(
393 Qty::from_str("20m")? + Qty::from_str("300m")?,
394 Qty::from_str("320m")?
395 );
396 assert_eq!(
397 &(Qty::from_str("1k")? + Qty::from_str("300m")?),
398 &Qty::from_str("1000300m")?
399 );
400 assert_eq!(
401 &(Qty::from_str("1Ki")? + Qty::from_str("1Ki")?),
402 &Qty::from_str("2Ki")?
403 );
404 assert_eq!(
405 &(Qty::from_str("1Ki")? + Qty::from_str("1k")?),
406 &Qty {
407 value: 2024000,
408 scale: Scale {
409 label: "k",
410 base: 10,
411 pow: 3,
412 },
413 }
414 );
415 Ok(())
416 }
417}