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// ── PositiveInt ───────────────────────────────────────────────────────────────
170
171/// Integer value strictly greater than zero.
172#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
173pub struct PositiveInt(u64);
174
175impl PositiveInt {
176    /// Creates a `PositiveInt`. Returns `OutOfRange` if `value` is zero.
177    pub fn new(value: u64) -> PrimitiveResult<Self> {
178        if value == 0 {
179            return Err(PrimitiveError::OutOfRange {
180                min: 1,
181                max: u64::MAX as u128,
182                actual: 0,
183            });
184        }
185        Ok(Self(value))
186    }
187
188    pub const fn get(self) -> u64 {
189        self.0
190    }
191}
192
193impl fmt::Display for PositiveInt {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{}", self.0)
196    }
197}
198
199impl TryFrom<u64> for PositiveInt {
200    type Error = PrimitiveError;
201
202    fn try_from(value: u64) -> Result<Self, Self::Error> {
203        Self::new(value)
204    }
205}
206
207impl From<PositiveInt> for u64 {
208    fn from(value: PositiveInt) -> Self {
209        value.get()
210    }
211}
212
213// ── PositiveFloat ─────────────────────────────────────────────────────────────
214
215/// Finite floating-point value strictly greater than zero.
216#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
217pub struct PositiveFloat(f64);
218
219impl PositiveFloat {
220    /// Creates a `PositiveFloat`. Returns `Invalid` if `value` is not finite
221    /// or is not greater than zero.
222    pub fn new(value: f64) -> PrimitiveResult<Self> {
223        if !value.is_finite() || value <= 0.0 {
224            return Err(PrimitiveError::Invalid {
225                message: "value must be a finite positive number greater than zero",
226            });
227        }
228        Ok(Self(value))
229    }
230
231    pub fn get(self) -> f64 {
232        self.0
233    }
234}
235
236impl fmt::Display for PositiveFloat {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        write!(f, "{}", self.0)
239    }
240}
241
242impl TryFrom<f64> for PositiveFloat {
243    type Error = PrimitiveError;
244
245    fn try_from(value: f64) -> Result<Self, Self::Error> {
246        Self::new(value)
247    }
248}
249
250// ── PercentageF64 ─────────────────────────────────────────────────────────────
251
252/// Percentage value as `f64` in the range `0.0..=100.0`.
253///
254/// Use this when decimal precision is required. For integer percentages, use
255/// [`Percent`].
256#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
257pub struct PercentageF64(f64);
258
259impl PercentageF64 {
260    pub const MIN: f64 = 0.0;
261    pub const MAX: f64 = 100.0;
262
263    /// Creates a `PercentageF64`. Returns `Invalid` if `value` is not finite
264    /// or is outside `0.0..=100.0`.
265    pub fn new(value: f64) -> PrimitiveResult<Self> {
266        if !value.is_finite() || !(Self::MIN..=Self::MAX).contains(&value) {
267            return Err(PrimitiveError::Invalid {
268                message: "percentage must be a finite number between 0.0 and 100.0 inclusive",
269            });
270        }
271        Ok(Self(value))
272    }
273
274    pub fn get(self) -> f64 {
275        self.0
276    }
277
278    /// Returns the value as a fraction between `0.0` and `1.0`.
279    pub fn as_fraction(self) -> f64 {
280        self.0 / 100.0
281    }
282}
283
284impl fmt::Display for PercentageF64 {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        write!(f, "{}%", self.0)
287    }
288}
289
290impl TryFrom<f64> for PercentageF64 {
291    type Error = PrimitiveError;
292
293    fn try_from(value: f64) -> Result<Self, Self::Error> {
294        Self::new(value)
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::{ByteSize, Percent, PercentageF64, Port, PositiveFloat, PositiveInt};
301    use crate::PrimitiveError;
302    use alloc::string::ToString;
303
304    #[test]
305    fn percent_accepts_boundary_values() {
306        assert_eq!(Percent::new(0).unwrap().get(), 0);
307        assert_eq!(Percent::new(50).unwrap().get(), 50);
308        assert_eq!(Percent::new(100).unwrap().get(), 100);
309    }
310
311    #[test]
312    fn percent_rejects_out_of_range() {
313        assert_eq!(
314            Percent::new(101).unwrap_err(),
315            PrimitiveError::OutOfRange {
316                min: 0,
317                max: 100,
318                actual: 101
319            }
320        );
321    }
322
323    #[test]
324    fn percent_display_prints_percent_sign() {
325        assert_eq!(Percent::new(42).unwrap().to_string(), "42%");
326    }
327
328    #[test]
329    fn percent_fraction() {
330        assert_eq!(Percent::new(25).unwrap().as_fraction(), 0.25);
331    }
332
333    #[test]
334    fn port_accepts_boundaries() {
335        assert_eq!(Port::new(1).unwrap().get(), 1);
336        assert_eq!(Port::new(65535).unwrap().get(), 65535);
337    }
338
339    #[test]
340    fn port_rejects_zero() {
341        assert_eq!(
342            Port::new(0).unwrap_err(),
343            PrimitiveError::OutOfRange {
344                min: 1,
345                max: 65535,
346                actual: 0
347            }
348        );
349    }
350
351    #[test]
352    fn byte_size_constructors_work() {
353        assert_eq!(ByteSize::from_bytes(512).as_bytes(), 512);
354        assert_eq!(ByteSize::from_kb(1).as_bytes(), 1024);
355        assert_eq!(ByteSize::from_mb(1).as_bytes(), 1024 * 1024);
356        assert_eq!(ByteSize::from_gb(1).as_bytes(), 1024 * 1024 * 1024);
357    }
358
359    #[test]
360    fn byte_size_display_works() {
361        assert_eq!(ByteSize::from_bytes(512).to_string(), "512 B");
362        assert_eq!(ByteSize::from_kb(1).to_string(), "1.00 KB");
363        assert_eq!(ByteSize::from_kb(1536 / 1024).to_string(), "1.00 KB");
364        assert_eq!(ByteSize::from_bytes(1536).to_string(), "1.50 KB");
365        assert_eq!(
366            ByteSize::from_bytes(1024 * 1024 + 512 * 1024).to_string(),
367            "1.50 MB"
368        );
369        assert_eq!(
370            ByteSize::from_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024).to_string(),
371            "1.50 GB"
372        );
373    }
374
375    #[test]
376    fn percent_try_from_u8() {
377        assert_eq!(Percent::try_from(50u8).unwrap().get(), 50);
378        assert!(Percent::try_from(101u8).is_err());
379    }
380
381    #[test]
382    fn percent_from_into_u8() {
383        let p = Percent::new(75).unwrap();
384        let v: u8 = p.into();
385        assert_eq!(v, 75);
386    }
387
388    #[test]
389    fn port_try_from_u16() {
390        assert_eq!(Port::try_from(8080u16).unwrap().get(), 8080);
391        assert!(Port::try_from(0u16).is_err());
392    }
393
394    #[test]
395    fn port_from_into_u16() {
396        let p = Port::new(443).unwrap();
397        let v: u16 = p.into();
398        assert_eq!(v, 443);
399    }
400
401    #[test]
402    fn port_display() {
403        assert_eq!(Port::new(8080).unwrap().to_string(), "8080");
404    }
405
406    #[test]
407    fn byte_size_from_u64() {
408        let s = ByteSize::from(2048u64);
409        assert_eq!(s.as_bytes(), 2048);
410    }
411
412    #[test]
413    fn byte_size_into_u64() {
414        let s = ByteSize::from_bytes(4096);
415        let v: u64 = s.into();
416        assert_eq!(v, 4096);
417    }
418
419    #[test]
420    fn positive_int_accepts_nonzero() {
421        assert_eq!(PositiveInt::new(1).unwrap().get(), 1);
422        assert_eq!(PositiveInt::new(u64::MAX).unwrap().get(), u64::MAX);
423    }
424
425    #[test]
426    fn positive_int_rejects_zero() {
427        assert!(PositiveInt::new(0).is_err());
428    }
429
430    #[test]
431    fn positive_int_display() {
432        assert_eq!(PositiveInt::new(42).unwrap().to_string(), "42");
433    }
434
435    #[test]
436    fn positive_int_try_from_and_into() {
437        let p = PositiveInt::try_from(10u64).unwrap();
438        let v: u64 = p.into();
439        assert_eq!(v, 10);
440    }
441
442    #[test]
443    fn positive_float_accepts_positive() {
444        assert_eq!(PositiveFloat::new(0.001).unwrap().get(), 0.001);
445        assert_eq!(PositiveFloat::new(f64::MAX).unwrap().get(), f64::MAX);
446    }
447
448    #[test]
449    fn positive_float_rejects_zero() {
450        assert!(PositiveFloat::new(0.0).is_err());
451    }
452
453    #[test]
454    fn positive_float_rejects_negative() {
455        assert!(PositiveFloat::new(-1.0).is_err());
456    }
457
458    #[test]
459    fn positive_float_rejects_nan() {
460        assert!(PositiveFloat::new(f64::NAN).is_err());
461    }
462
463    #[test]
464    fn positive_float_rejects_infinity() {
465        assert!(PositiveFloat::new(f64::INFINITY).is_err());
466    }
467
468    #[test]
469    fn positive_float_try_from() {
470        assert!(PositiveFloat::try_from(1.5f64).is_ok());
471        assert!(PositiveFloat::try_from(0.0f64).is_err());
472    }
473
474    #[test]
475    fn percentage_f64_accepts_boundaries() {
476        assert_eq!(PercentageF64::new(0.0).unwrap().get(), 0.0);
477        assert_eq!(PercentageF64::new(50.5).unwrap().get(), 50.5);
478        assert_eq!(PercentageF64::new(100.0).unwrap().get(), 100.0);
479    }
480
481    #[test]
482    fn percentage_f64_rejects_out_of_range() {
483        assert!(PercentageF64::new(-0.1).is_err());
484        assert!(PercentageF64::new(100.1).is_err());
485    }
486
487    #[test]
488    fn percentage_f64_rejects_nan() {
489        assert!(PercentageF64::new(f64::NAN).is_err());
490    }
491
492    #[test]
493    fn percentage_f64_as_fraction() {
494        assert_eq!(PercentageF64::new(25.0).unwrap().as_fraction(), 0.25);
495    }
496
497    #[test]
498    fn percentage_f64_display() {
499        assert_eq!(PercentageF64::new(42.5).unwrap().to_string(), "42.5%");
500    }
501
502    #[test]
503    fn percentage_f64_try_from() {
504        assert!(PercentageF64::try_from(50.0f64).is_ok());
505        assert!(PercentageF64::try_from(101.0f64).is_err());
506    }
507}