Skip to main content

reliakit_primitives/
numeric.rs

1use crate::{PrimitiveError, PrimitiveResult};
2use core::fmt;
3
4/// Percentage value from 0 to 100 inclusive.
5#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6pub struct Percent(u8);
7
8impl Percent {
9    pub const MIN: u8 = 0;
10    pub const MAX: u8 = 100;
11
12    /// Creates a new percentage value.
13    pub fn new(value: u8) -> PrimitiveResult<Self> {
14        if value > Self::MAX {
15            return Err(PrimitiveError::OutOfRange {
16                min: Self::MIN as u128,
17                max: Self::MAX as u128,
18                actual: value as u128,
19            });
20        }
21        Ok(Self(value))
22    }
23
24    /// Returns the integer percentage value.
25    pub const fn get(self) -> u8 {
26        self.0
27    }
28
29    /// Returns the percentage as a fraction between 0.0 and 1.0.
30    pub fn as_fraction(self) -> f64 {
31        f64::from(self.0) / 100.0
32    }
33}
34
35impl fmt::Display for Percent {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}%", self.0)
38    }
39}
40
41impl TryFrom<u8> for Percent {
42    type Error = PrimitiveError;
43
44    fn try_from(value: u8) -> Result<Self, Self::Error> {
45        Self::new(value)
46    }
47}
48
49impl From<Percent> for u8 {
50    fn from(value: Percent) -> Self {
51        value.get()
52    }
53}
54
55/// TCP/UDP port number from 1 to 65535 inclusive.
56#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct Port(u16);
58
59impl Port {
60    pub const MIN: u16 = 1;
61    pub const MAX: u16 = 65535;
62
63    /// Creates a new port.
64    pub fn new(value: u16) -> PrimitiveResult<Self> {
65        if !(Self::MIN..=Self::MAX).contains(&value) {
66            return Err(PrimitiveError::OutOfRange {
67                min: Self::MIN as u128,
68                max: Self::MAX as u128,
69                actual: value as u128,
70            });
71        }
72        Ok(Self(value))
73    }
74
75    /// Returns the port number.
76    pub const fn get(self) -> u16 {
77        self.0
78    }
79}
80
81impl fmt::Display for Port {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(f, "{}", self.0)
84    }
85}
86
87impl TryFrom<u16> for Port {
88    type Error = PrimitiveError;
89
90    fn try_from(value: u16) -> Result<Self, Self::Error> {
91        Self::new(value)
92    }
93}
94
95impl From<Port> for u16 {
96    fn from(value: Port) -> Self {
97        value.get()
98    }
99}
100
101/// Byte size value.
102#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct ByteSize(u64);
104
105impl ByteSize {
106    /// Creates a size from bytes.
107    pub const fn from_bytes(bytes: u64) -> Self {
108        Self(bytes)
109    }
110
111    /// Creates a size from kibibytes (1 KiB = 1024 bytes).
112    ///
113    /// Saturates to `u64::MAX` on overflow instead of panicking.
114    pub const fn from_kb(kb: u64) -> Self {
115        Self(kb.saturating_mul(1024))
116    }
117
118    /// Creates a size from mebibytes (1 MiB = 1024 KiB).
119    ///
120    /// Saturates to `u64::MAX` on overflow instead of panicking.
121    pub const fn from_mb(mb: u64) -> Self {
122        Self(mb.saturating_mul(1024 * 1024))
123    }
124
125    /// Creates a size from gibibytes (1 GiB = 1024 MiB).
126    ///
127    /// Saturates to `u64::MAX` on overflow instead of panicking.
128    pub const fn from_gb(gb: u64) -> Self {
129        Self(gb.saturating_mul(1024 * 1024 * 1024))
130    }
131
132    /// Returns the size in bytes.
133    pub const fn as_bytes(self) -> u64 {
134        self.0
135    }
136}
137
138impl fmt::Display for ByteSize {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        const KB: u64 = 1024;
141        const MB: u64 = KB * 1024;
142        const GB: u64 = MB * 1024;
143
144        let bytes = self.0;
145        if bytes < KB {
146            write!(f, "{bytes} B")
147        } else if bytes < MB {
148            write!(f, "{:.2} KB", bytes as f64 / KB as f64)
149        } else if bytes < GB {
150            write!(f, "{:.2} MB", bytes as f64 / MB as f64)
151        } else {
152            write!(f, "{:.2} GB", bytes as f64 / GB as f64)
153        }
154    }
155}
156
157impl From<u64> for ByteSize {
158    fn from(value: u64) -> Self {
159        Self::from_bytes(value)
160    }
161}
162
163impl From<ByteSize> for u64 {
164    fn from(value: ByteSize) -> Self {
165        value.as_bytes()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{ByteSize, Percent, Port};
172    use crate::PrimitiveError;
173    use alloc::string::ToString;
174
175    #[test]
176    fn percent_accepts_boundary_values() {
177        assert_eq!(Percent::new(0).unwrap().get(), 0);
178        assert_eq!(Percent::new(50).unwrap().get(), 50);
179        assert_eq!(Percent::new(100).unwrap().get(), 100);
180    }
181
182    #[test]
183    fn percent_rejects_out_of_range() {
184        assert_eq!(
185            Percent::new(101).unwrap_err(),
186            PrimitiveError::OutOfRange {
187                min: 0,
188                max: 100,
189                actual: 101
190            }
191        );
192    }
193
194    #[test]
195    fn percent_display_prints_percent_sign() {
196        assert_eq!(Percent::new(42).unwrap().to_string(), "42%");
197    }
198
199    #[test]
200    fn percent_fraction() {
201        assert_eq!(Percent::new(25).unwrap().as_fraction(), 0.25);
202    }
203
204    #[test]
205    fn port_accepts_boundaries() {
206        assert_eq!(Port::new(1).unwrap().get(), 1);
207        assert_eq!(Port::new(65535).unwrap().get(), 65535);
208    }
209
210    #[test]
211    fn port_rejects_zero() {
212        assert_eq!(
213            Port::new(0).unwrap_err(),
214            PrimitiveError::OutOfRange {
215                min: 1,
216                max: 65535,
217                actual: 0
218            }
219        );
220    }
221
222    #[test]
223    fn byte_size_constructors_work() {
224        assert_eq!(ByteSize::from_bytes(512).as_bytes(), 512);
225        assert_eq!(ByteSize::from_kb(1).as_bytes(), 1024);
226        assert_eq!(ByteSize::from_mb(1).as_bytes(), 1024 * 1024);
227        assert_eq!(ByteSize::from_gb(1).as_bytes(), 1024 * 1024 * 1024);
228    }
229
230    #[test]
231    fn byte_size_display_works() {
232        assert_eq!(ByteSize::from_bytes(512).to_string(), "512 B");
233        assert_eq!(ByteSize::from_kb(1).to_string(), "1.00 KB");
234        assert_eq!(ByteSize::from_kb(1536 / 1024).to_string(), "1.00 KB");
235        assert_eq!(ByteSize::from_bytes(1536).to_string(), "1.50 KB");
236        assert_eq!(
237            ByteSize::from_bytes(1024 * 1024 + 512 * 1024).to_string(),
238            "1.50 MB"
239        );
240        assert_eq!(
241            ByteSize::from_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024).to_string(),
242            "1.50 GB"
243        );
244    }
245
246    #[test]
247    fn percent_try_from_u8() {
248        assert_eq!(Percent::try_from(50u8).unwrap().get(), 50);
249        assert!(Percent::try_from(101u8).is_err());
250    }
251
252    #[test]
253    fn percent_from_into_u8() {
254        let p = Percent::new(75).unwrap();
255        let v: u8 = p.into();
256        assert_eq!(v, 75);
257    }
258
259    #[test]
260    fn port_try_from_u16() {
261        assert_eq!(Port::try_from(8080u16).unwrap().get(), 8080);
262        assert!(Port::try_from(0u16).is_err());
263    }
264
265    #[test]
266    fn port_from_into_u16() {
267        let p = Port::new(443).unwrap();
268        let v: u16 = p.into();
269        assert_eq!(v, 443);
270    }
271
272    #[test]
273    fn port_display() {
274        assert_eq!(Port::new(8080).unwrap().to_string(), "8080");
275    }
276
277    #[test]
278    fn byte_size_from_u64() {
279        let s = ByteSize::from(2048u64);
280        assert_eq!(s.as_bytes(), 2048);
281    }
282
283    #[test]
284    fn byte_size_into_u64() {
285        let s = ByteSize::from_bytes(4096);
286        let v: u64 = s.into();
287        assert_eq!(v, 4096);
288    }
289}