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    /// Minimum allowed integer percentage.
10    pub const MIN: u8 = 0;
11    /// Maximum allowed integer percentage.
12    pub const MAX: u8 = 100;
13
14    /// Creates a new percentage value.
15    pub const fn new(value: u8) -> PrimitiveResult<Self> {
16        if value > Self::MAX {
17            return Err(PrimitiveError::OutOfRange {
18                min: Self::MIN as u128,
19                max: Self::MAX as u128,
20                actual: value as u128,
21            });
22        }
23        Ok(Self(value))
24    }
25
26    /// Returns the integer percentage value.
27    pub const fn get(self) -> u8 {
28        self.0
29    }
30
31    /// Returns the percentage as a fraction between 0.0 and 1.0.
32    pub fn as_fraction(self) -> f64 {
33        f64::from(self.0) / 100.0
34    }
35}
36
37impl fmt::Display for Percent {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "{}%", self.0)
40    }
41}
42
43impl TryFrom<u8> for Percent {
44    type Error = PrimitiveError;
45
46    fn try_from(value: u8) -> Result<Self, Self::Error> {
47        Self::new(value)
48    }
49}
50
51impl From<Percent> for u8 {
52    fn from(value: Percent) -> Self {
53        value.get()
54    }
55}
56
57/// TCP/UDP port number from 1 to 65535 inclusive.
58#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct Port(u16);
60
61impl Port {
62    /// Minimum allowed TCP/UDP port.
63    pub const MIN: u16 = 1;
64    /// Maximum allowed TCP/UDP port.
65    pub const MAX: u16 = 65535;
66
67    /// Creates a new port.
68    pub const fn new(value: u16) -> PrimitiveResult<Self> {
69        if value < Self::MIN {
70            return Err(PrimitiveError::OutOfRange {
71                min: Self::MIN as u128,
72                max: Self::MAX as u128,
73                actual: value as u128,
74            });
75        }
76        Ok(Self(value))
77    }
78
79    /// Returns the port number.
80    pub const fn get(self) -> u16 {
81        self.0
82    }
83}
84
85impl fmt::Display for Port {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91impl TryFrom<u16> for Port {
92    type Error = PrimitiveError;
93
94    fn try_from(value: u16) -> Result<Self, Self::Error> {
95        Self::new(value)
96    }
97}
98
99impl From<Port> for u16 {
100    fn from(value: Port) -> Self {
101        value.get()
102    }
103}
104
105/// Byte size value.
106#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
107pub struct ByteSize(u64);
108
109impl ByteSize {
110    /// Creates a size from bytes.
111    pub const fn from_bytes(bytes: u64) -> Self {
112        Self(bytes)
113    }
114
115    /// Creates a size from kibibytes (1 KiB = 1024 bytes).
116    ///
117    /// Saturates to `u64::MAX` on overflow instead of panicking.
118    pub const fn from_kb(kb: u64) -> Self {
119        Self(kb.saturating_mul(1024))
120    }
121
122    /// Creates a size from mebibytes (1 MiB = 1024 KiB).
123    ///
124    /// Saturates to `u64::MAX` on overflow instead of panicking.
125    pub const fn from_mb(mb: u64) -> Self {
126        Self(mb.saturating_mul(1024 * 1024))
127    }
128
129    /// Creates a size from gibibytes (1 GiB = 1024 MiB).
130    ///
131    /// Saturates to `u64::MAX` on overflow instead of panicking.
132    pub const fn from_gb(gb: u64) -> Self {
133        Self(gb.saturating_mul(1024 * 1024 * 1024))
134    }
135
136    /// Returns the size in bytes.
137    pub const fn as_bytes(self) -> u64 {
138        self.0
139    }
140}
141
142impl fmt::Display for ByteSize {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        const KB: u64 = 1024;
145        const MB: u64 = KB * 1024;
146        const GB: u64 = MB * 1024;
147
148        let bytes = self.0;
149        if bytes < KB {
150            write!(f, "{bytes} B")
151        } else if bytes < MB {
152            write!(f, "{:.2} KB", bytes as f64 / KB as f64)
153        } else if bytes < GB {
154            write!(f, "{:.2} MB", bytes as f64 / MB as f64)
155        } else {
156            write!(f, "{:.2} GB", bytes as f64 / GB as f64)
157        }
158    }
159}
160
161impl From<u64> for ByteSize {
162    fn from(value: u64) -> Self {
163        Self::from_bytes(value)
164    }
165}
166
167impl From<ByteSize> for u64 {
168    fn from(value: ByteSize) -> Self {
169        value.as_bytes()
170    }
171}
172
173// ── PositiveInt ───────────────────────────────────────────────────────────────
174
175/// Integer value strictly greater than zero.
176#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
177pub struct PositiveInt(u64);
178
179impl PositiveInt {
180    /// Creates a `PositiveInt`. Returns `OutOfRange` if `value` is zero.
181    pub const fn new(value: u64) -> PrimitiveResult<Self> {
182        if value == 0 {
183            return Err(PrimitiveError::OutOfRange {
184                min: 1,
185                max: u64::MAX as u128,
186                actual: 0,
187            });
188        }
189        Ok(Self(value))
190    }
191
192    /// Returns the positive integer value.
193    pub const fn get(self) -> u64 {
194        self.0
195    }
196}
197
198impl fmt::Display for PositiveInt {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "{}", self.0)
201    }
202}
203
204impl TryFrom<u64> for PositiveInt {
205    type Error = PrimitiveError;
206
207    fn try_from(value: u64) -> Result<Self, Self::Error> {
208        Self::new(value)
209    }
210}
211
212impl From<PositiveInt> for u64 {
213    fn from(value: PositiveInt) -> Self {
214        value.get()
215    }
216}
217
218// ── PositiveFloat ─────────────────────────────────────────────────────────────
219
220/// Finite floating-point value strictly greater than zero.
221#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
222pub struct PositiveFloat(f64);
223
224impl PositiveFloat {
225    /// Creates a `PositiveFloat`. Returns `Invalid` if `value` is not finite
226    /// or is not greater than zero.
227    pub fn new(value: f64) -> PrimitiveResult<Self> {
228        if !value.is_finite() || value <= 0.0 {
229            return Err(PrimitiveError::Invalid {
230                message: "value must be a finite positive number greater than zero",
231            });
232        }
233        Ok(Self(value))
234    }
235
236    /// Returns the positive floating-point value.
237    pub fn get(self) -> f64 {
238        self.0
239    }
240}
241
242impl fmt::Display for PositiveFloat {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        write!(f, "{}", self.0)
245    }
246}
247
248impl TryFrom<f64> for PositiveFloat {
249    type Error = PrimitiveError;
250
251    fn try_from(value: f64) -> Result<Self, Self::Error> {
252        Self::new(value)
253    }
254}
255
256// ── PercentageF64 ─────────────────────────────────────────────────────────────
257
258/// Percentage value as `f64` in the range `0.0..=100.0`.
259///
260/// Use this when decimal precision is required. For integer percentages, use
261/// [`Percent`].
262#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
263pub struct PercentageF64(f64);
264
265impl PercentageF64 {
266    /// Minimum allowed floating-point percentage.
267    pub const MIN: f64 = 0.0;
268    /// Maximum allowed floating-point percentage.
269    pub const MAX: f64 = 100.0;
270
271    /// Creates a `PercentageF64`. Returns `Invalid` if `value` is not finite
272    /// or is outside `0.0..=100.0`.
273    pub fn new(value: f64) -> PrimitiveResult<Self> {
274        if !value.is_finite() || !(Self::MIN..=Self::MAX).contains(&value) {
275            return Err(PrimitiveError::Invalid {
276                message: "percentage must be a finite number between 0.0 and 100.0 inclusive",
277            });
278        }
279        Ok(Self(value))
280    }
281
282    /// Returns the floating-point percentage value.
283    pub fn get(self) -> f64 {
284        self.0
285    }
286
287    /// Returns the value as a fraction between `0.0` and `1.0`.
288    pub fn as_fraction(self) -> f64 {
289        self.0 / 100.0
290    }
291}
292
293impl fmt::Display for PercentageF64 {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        write!(f, "{}%", self.0)
296    }
297}
298
299impl TryFrom<f64> for PercentageF64 {
300    type Error = PrimitiveError;
301
302    fn try_from(value: f64) -> Result<Self, Self::Error> {
303        Self::new(value)
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::{ByteSize, Percent, PercentageF64, Port, PositiveFloat, PositiveInt};
310    use crate::PrimitiveError;
311    use alloc::string::ToString;
312
313    #[test]
314    fn percent_accepts_boundary_values() {
315        assert_eq!(Percent::new(0).unwrap().get(), 0);
316        assert_eq!(Percent::new(50).unwrap().get(), 50);
317        assert_eq!(Percent::new(100).unwrap().get(), 100);
318    }
319
320    #[test]
321    fn percent_rejects_out_of_range() {
322        assert_eq!(
323            Percent::new(101).unwrap_err(),
324            PrimitiveError::OutOfRange {
325                min: 0,
326                max: 100,
327                actual: 101
328            }
329        );
330    }
331
332    #[test]
333    fn percent_display_prints_percent_sign() {
334        assert_eq!(Percent::new(42).unwrap().to_string(), "42%");
335    }
336
337    #[test]
338    fn percent_fraction() {
339        assert_eq!(Percent::new(25).unwrap().as_fraction(), 0.25);
340    }
341
342    #[test]
343    fn port_accepts_boundaries() {
344        assert_eq!(Port::new(1).unwrap().get(), 1);
345        assert_eq!(Port::new(65535).unwrap().get(), 65535);
346    }
347
348    #[test]
349    fn port_rejects_zero() {
350        assert_eq!(
351            Port::new(0).unwrap_err(),
352            PrimitiveError::OutOfRange {
353                min: 1,
354                max: 65535,
355                actual: 0
356            }
357        );
358    }
359
360    #[test]
361    fn byte_size_constructors_work() {
362        assert_eq!(ByteSize::from_bytes(512).as_bytes(), 512);
363        assert_eq!(ByteSize::from_kb(1).as_bytes(), 1024);
364        assert_eq!(ByteSize::from_mb(1).as_bytes(), 1024 * 1024);
365        assert_eq!(ByteSize::from_gb(1).as_bytes(), 1024 * 1024 * 1024);
366    }
367
368    #[test]
369    fn byte_size_display_works() {
370        assert_eq!(ByteSize::from_bytes(512).to_string(), "512 B");
371        assert_eq!(ByteSize::from_kb(1).to_string(), "1.00 KB");
372        assert_eq!(ByteSize::from_kb(1536 / 1024).to_string(), "1.00 KB");
373        assert_eq!(ByteSize::from_bytes(1536).to_string(), "1.50 KB");
374        assert_eq!(
375            ByteSize::from_bytes(1024 * 1024 + 512 * 1024).to_string(),
376            "1.50 MB"
377        );
378        assert_eq!(
379            ByteSize::from_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024).to_string(),
380            "1.50 GB"
381        );
382    }
383
384    #[test]
385    fn percent_try_from_u8() {
386        assert_eq!(Percent::try_from(50u8).unwrap().get(), 50);
387        assert!(Percent::try_from(101u8).is_err());
388    }
389
390    #[test]
391    fn percent_from_into_u8() {
392        let p = Percent::new(75).unwrap();
393        let v: u8 = p.into();
394        assert_eq!(v, 75);
395    }
396
397    #[test]
398    fn port_try_from_u16() {
399        assert_eq!(Port::try_from(8080u16).unwrap().get(), 8080);
400        assert!(Port::try_from(0u16).is_err());
401    }
402
403    #[test]
404    fn port_from_into_u16() {
405        let p = Port::new(443).unwrap();
406        let v: u16 = p.into();
407        assert_eq!(v, 443);
408    }
409
410    #[test]
411    fn port_display() {
412        assert_eq!(Port::new(8080).unwrap().to_string(), "8080");
413    }
414
415    #[test]
416    fn byte_size_from_u64() {
417        let s = ByteSize::from(2048u64);
418        assert_eq!(s.as_bytes(), 2048);
419    }
420
421    #[test]
422    fn byte_size_into_u64() {
423        let s = ByteSize::from_bytes(4096);
424        let v: u64 = s.into();
425        assert_eq!(v, 4096);
426    }
427
428    #[test]
429    fn positive_int_accepts_nonzero() {
430        assert_eq!(PositiveInt::new(1).unwrap().get(), 1);
431        assert_eq!(PositiveInt::new(u64::MAX).unwrap().get(), u64::MAX);
432    }
433
434    #[test]
435    fn positive_int_rejects_zero() {
436        assert!(PositiveInt::new(0).is_err());
437    }
438
439    #[test]
440    fn positive_int_display() {
441        assert_eq!(PositiveInt::new(42).unwrap().to_string(), "42");
442    }
443
444    #[test]
445    fn positive_int_try_from_and_into() {
446        let p = PositiveInt::try_from(10u64).unwrap();
447        let v: u64 = p.into();
448        assert_eq!(v, 10);
449    }
450
451    #[test]
452    fn positive_float_accepts_positive() {
453        assert_eq!(PositiveFloat::new(0.001).unwrap().get(), 0.001);
454        assert_eq!(PositiveFloat::new(f64::MAX).unwrap().get(), f64::MAX);
455    }
456
457    #[test]
458    fn positive_float_rejects_zero() {
459        assert!(PositiveFloat::new(0.0).is_err());
460    }
461
462    #[test]
463    fn positive_float_rejects_negative() {
464        assert!(PositiveFloat::new(-1.0).is_err());
465    }
466
467    #[test]
468    fn positive_float_rejects_nan() {
469        assert!(PositiveFloat::new(f64::NAN).is_err());
470    }
471
472    #[test]
473    fn positive_float_rejects_infinity() {
474        assert!(PositiveFloat::new(f64::INFINITY).is_err());
475    }
476
477    #[test]
478    fn positive_float_try_from() {
479        assert!(PositiveFloat::try_from(1.5f64).is_ok());
480        assert!(PositiveFloat::try_from(0.0f64).is_err());
481    }
482
483    #[test]
484    fn percentage_f64_accepts_boundaries() {
485        assert_eq!(PercentageF64::new(0.0).unwrap().get(), 0.0);
486        assert_eq!(PercentageF64::new(50.5).unwrap().get(), 50.5);
487        assert_eq!(PercentageF64::new(100.0).unwrap().get(), 100.0);
488    }
489
490    #[test]
491    fn percentage_f64_rejects_out_of_range() {
492        assert!(PercentageF64::new(-0.1).is_err());
493        assert!(PercentageF64::new(100.1).is_err());
494    }
495
496    #[test]
497    fn percentage_f64_rejects_nan() {
498        assert!(PercentageF64::new(f64::NAN).is_err());
499    }
500
501    #[test]
502    fn percentage_f64_as_fraction() {
503        assert_eq!(PercentageF64::new(25.0).unwrap().as_fraction(), 0.25);
504    }
505
506    #[test]
507    fn percentage_f64_display() {
508        assert_eq!(PercentageF64::new(42.5).unwrap().to_string(), "42.5%");
509    }
510
511    #[test]
512    fn percentage_f64_try_from() {
513        assert!(PercentageF64::try_from(50.0f64).is_ok());
514        assert!(PercentageF64::try_from(101.0f64).is_err());
515    }
516}