kubectl_view_allocations/
qty.rs

1// see [Definitions of the SI units: The binary prefixes](https://physics.nist.gov/cuu/Units/binary.html)
2// see [Managing Compute Resources for Containers - Kubernetes](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
3//TODO rewrite to support exponent, ... see [apimachinery/quantity.go at master · kubernetes/apimachinery](https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go)
4
5use 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] // optional if field name is `source`
17        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// should be sorted in DESC
29#[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    //TODO optimize accuracy with big number
71    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    //TODO optimize accuracy with big number
183    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
184        Some(self.cmp(other))
185    }
186}
187
188impl Ord for Qty {
189    //TODO optimize accuracy with big number
190    fn cmp(&self, other: &Self) -> Ordering {
191        let v1 = self.value; // f64::from(self);
192        let v2 = other.value; // f64::from(other);
193        v1.partial_cmp(&v2).unwrap() // i64 should always be comparable (no NaNs or anything crazy like that)
194    }
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"), //TODO 1.9M should be better ?
301            ("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            // lowest precision is m, under 1m value is trunked
341            ("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}