monitrs_core/units/
percent.rs1use core::fmt;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[cfg_attr(feature = "serde", serde(transparent))]
17pub struct Percent(f32);
18
19impl Percent {
20 pub const ZERO: Self = Self(0.0);
22 pub const FULL: Self = Self(100.0);
24
25 #[must_use]
31 pub fn new(value: f32) -> Option<Self> {
32 if value.is_finite() && value >= 0.0 {
33 Some(Self(value))
34 } else {
35 None
36 }
37 }
38
39 #[must_use]
44 pub fn ratio(part: u64, whole: u64) -> Option<Self> {
45 if whole == 0 {
46 return None;
47 }
48 #[allow(clippy::cast_possible_truncation)]
52 let percent = (part as f64 / whole as f64 * 100.0) as f32;
53 Self::new(percent)
54 }
55
56 #[must_use]
58 pub fn clamped_to_100(self) -> Self {
59 Self(self.0.min(100.0))
60 }
61
62 #[must_use]
64 pub const fn value(self) -> f32 {
65 self.0
66 }
67
68 #[must_use]
70 pub fn fraction(self) -> f32 {
71 self.0 / 100.0
72 }
73
74 #[must_use]
76 pub fn points_from(self, other: Self) -> f32 {
77 self.0 - other.0
78 }
79}
80
81impl fmt::Display for Percent {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 if self.0 < 10.0 && self.0 != 0.0 {
88 write!(f, "{:.1}%", self.0)
89 } else {
90 write!(f, "{:.0}%", self.0)
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn rejects_non_finite_and_negative() {
101 assert!(Percent::new(f32::NAN).is_none());
102 assert!(Percent::new(f32::INFINITY).is_none());
103 assert!(Percent::new(-0.5).is_none());
104 assert!(Percent::new(0.0).is_some());
105 assert!(Percent::new(287.0).is_some(), "process CPU may exceed 100%");
106 }
107
108 #[test]
109 fn zero_whole_is_undefined_not_zero() {
110 assert!(Percent::ratio(0, 0).is_none());
111 assert!(Percent::ratio(5, 0).is_none());
112 }
113
114 #[test]
115 fn ratio_computes_expected_values() {
116 let p = Percent::ratio(1, 4).expect("4 is non-zero");
117 assert!((p.value() - 25.0).abs() < f32::EPSILON);
118 }
119
120 #[test]
121 fn ratio_survives_counters_that_overflow_f32_precision() {
122 let p = Percent::ratio(u64::MAX / 2, u64::MAX).expect("non-zero whole");
124 assert!((p.value() - 50.0).abs() < 0.01, "got {}", p.value());
125 }
126
127 #[test]
128 fn clamping_only_affects_the_upper_bound() {
129 let p = Percent::new(287.0).expect("valid");
130 assert!((p.clamped_to_100().value() - 100.0).abs() < f32::EPSILON);
131 let q = Percent::new(37.0).expect("valid");
132 assert!((q.clamped_to_100().value() - 37.0).abs() < f32::EPSILON);
133 }
134
135 #[test]
136 fn display_adds_a_decimal_only_below_ten_percent() {
137 assert_eq!(Percent::new(0.0).expect("valid").to_string(), "0%");
138 assert_eq!(Percent::new(4.2).expect("valid").to_string(), "4.2%");
139 assert_eq!(Percent::new(37.4).expect("valid").to_string(), "37%");
140 assert_eq!(Percent::new(287.0).expect("valid").to_string(), "287%");
141 }
142}