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// ── PercentFloat ─────────────────────────────────────────────────────────────
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 PercentFloat(f64);
264
265impl PercentFloat {
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 `PercentFloat`. 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 PercentFloat {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        write!(f, "{}%", self.0)
296    }
297}
298
299impl TryFrom<f64> for PercentFloat {
300    type Error = PrimitiveError;
301
302    fn try_from(value: f64) -> Result<Self, Self::Error> {
303        Self::new(value)
304    }
305}
306
307// ── Probability ───────────────────────────────────────────────────────────────
308
309/// Probability as `f64` in the range `0.0..=1.0`.
310///
311/// Use this for rates, weights, and sampling. For a `0..=100` percentage use
312/// [`PercentFloat`]; for an integer percentage use [`Percent`].
313#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
314pub struct Probability(f64);
315
316impl Probability {
317    /// Minimum allowed probability.
318    pub const MIN: f64 = 0.0;
319    /// Maximum allowed probability.
320    pub const MAX: f64 = 1.0;
321
322    /// Creates a `Probability`. Returns `Invalid` if `value` is not finite or is
323    /// outside `0.0..=1.0`.
324    pub fn new(value: f64) -> PrimitiveResult<Self> {
325        if !value.is_finite() || !(Self::MIN..=Self::MAX).contains(&value) {
326            return Err(PrimitiveError::Invalid {
327                message: "probability must be a finite number between 0.0 and 1.0 inclusive",
328            });
329        }
330        Ok(Self(value))
331    }
332
333    /// Returns the probability value.
334    pub fn get(self) -> f64 {
335        self.0
336    }
337
338    /// Returns the complementary probability, `1.0 - p`. Always valid because
339    /// the complement of a value in `0.0..=1.0` is itself in `0.0..=1.0`.
340    pub fn complement(self) -> Self {
341        Self(Self::MAX - self.0)
342    }
343}
344
345impl fmt::Display for Probability {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        write!(f, "{}", self.0)
348    }
349}
350
351impl TryFrom<f64> for Probability {
352    type Error = PrimitiveError;
353
354    fn try_from(value: f64) -> Result<Self, Self::Error> {
355        Self::new(value)
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::{ByteSize, Percent, PercentFloat, Port, PositiveFloat, PositiveInt, Probability};
362    use crate::PrimitiveError;
363    use alloc::string::ToString;
364
365    #[test]
366    fn percent_accepts_boundary_values() {
367        assert_eq!(Percent::new(0).unwrap().get(), 0);
368        assert_eq!(Percent::new(50).unwrap().get(), 50);
369        assert_eq!(Percent::new(100).unwrap().get(), 100);
370    }
371
372    #[test]
373    fn percent_rejects_out_of_range() {
374        assert_eq!(
375            Percent::new(101).unwrap_err(),
376            PrimitiveError::OutOfRange {
377                min: 0,
378                max: 100,
379                actual: 101
380            }
381        );
382    }
383
384    #[test]
385    fn percent_display_prints_percent_sign() {
386        assert_eq!(Percent::new(42).unwrap().to_string(), "42%");
387    }
388
389    #[test]
390    fn percent_fraction() {
391        assert_eq!(Percent::new(25).unwrap().as_fraction(), 0.25);
392    }
393
394    #[test]
395    fn port_accepts_boundaries() {
396        assert_eq!(Port::new(1).unwrap().get(), 1);
397        assert_eq!(Port::new(65535).unwrap().get(), 65535);
398    }
399
400    #[test]
401    fn port_rejects_zero() {
402        assert_eq!(
403            Port::new(0).unwrap_err(),
404            PrimitiveError::OutOfRange {
405                min: 1,
406                max: 65535,
407                actual: 0
408            }
409        );
410    }
411
412    #[test]
413    fn byte_size_constructors_work() {
414        assert_eq!(ByteSize::from_bytes(512).as_bytes(), 512);
415        assert_eq!(ByteSize::from_kb(1).as_bytes(), 1024);
416        assert_eq!(ByteSize::from_mb(1).as_bytes(), 1024 * 1024);
417        assert_eq!(ByteSize::from_gb(1).as_bytes(), 1024 * 1024 * 1024);
418    }
419
420    #[test]
421    fn byte_size_display_works() {
422        assert_eq!(ByteSize::from_bytes(512).to_string(), "512 B");
423        assert_eq!(ByteSize::from_kb(1).to_string(), "1.00 KB");
424        assert_eq!(ByteSize::from_kb(1536 / 1024).to_string(), "1.00 KB");
425        assert_eq!(ByteSize::from_bytes(1536).to_string(), "1.50 KB");
426        assert_eq!(
427            ByteSize::from_bytes(1024 * 1024 + 512 * 1024).to_string(),
428            "1.50 MB"
429        );
430        assert_eq!(
431            ByteSize::from_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024).to_string(),
432            "1.50 GB"
433        );
434    }
435
436    #[test]
437    fn percent_try_from_u8() {
438        assert_eq!(Percent::try_from(50u8).unwrap().get(), 50);
439        assert!(Percent::try_from(101u8).is_err());
440    }
441
442    #[test]
443    fn percent_from_into_u8() {
444        let p = Percent::new(75).unwrap();
445        let v: u8 = p.into();
446        assert_eq!(v, 75);
447    }
448
449    #[test]
450    fn port_try_from_u16() {
451        assert_eq!(Port::try_from(8080u16).unwrap().get(), 8080);
452        assert!(Port::try_from(0u16).is_err());
453    }
454
455    #[test]
456    fn port_from_into_u16() {
457        let p = Port::new(443).unwrap();
458        let v: u16 = p.into();
459        assert_eq!(v, 443);
460    }
461
462    #[test]
463    fn port_display() {
464        assert_eq!(Port::new(8080).unwrap().to_string(), "8080");
465    }
466
467    #[test]
468    fn byte_size_from_u64() {
469        let s = ByteSize::from(2048u64);
470        assert_eq!(s.as_bytes(), 2048);
471    }
472
473    #[test]
474    fn byte_size_into_u64() {
475        let s = ByteSize::from_bytes(4096);
476        let v: u64 = s.into();
477        assert_eq!(v, 4096);
478    }
479
480    #[test]
481    fn positive_int_accepts_nonzero() {
482        assert_eq!(PositiveInt::new(1).unwrap().get(), 1);
483        assert_eq!(PositiveInt::new(u64::MAX).unwrap().get(), u64::MAX);
484    }
485
486    #[test]
487    fn positive_int_rejects_zero() {
488        assert!(PositiveInt::new(0).is_err());
489    }
490
491    #[test]
492    fn positive_int_display() {
493        assert_eq!(PositiveInt::new(42).unwrap().to_string(), "42");
494    }
495
496    #[test]
497    fn positive_int_try_from_and_into() {
498        let p = PositiveInt::try_from(10u64).unwrap();
499        let v: u64 = p.into();
500        assert_eq!(v, 10);
501    }
502
503    #[test]
504    fn positive_float_accepts_positive() {
505        assert_eq!(PositiveFloat::new(0.001).unwrap().get(), 0.001);
506        assert_eq!(PositiveFloat::new(f64::MAX).unwrap().get(), f64::MAX);
507    }
508
509    #[test]
510    fn positive_float_rejects_zero() {
511        assert!(PositiveFloat::new(0.0).is_err());
512    }
513
514    #[test]
515    fn positive_float_rejects_negative() {
516        assert!(PositiveFloat::new(-1.0).is_err());
517    }
518
519    #[test]
520    fn positive_float_rejects_nan() {
521        assert!(PositiveFloat::new(f64::NAN).is_err());
522    }
523
524    #[test]
525    fn positive_float_rejects_infinity() {
526        assert!(PositiveFloat::new(f64::INFINITY).is_err());
527    }
528
529    #[test]
530    fn positive_float_try_from() {
531        assert!(PositiveFloat::try_from(1.5f64).is_ok());
532        assert!(PositiveFloat::try_from(0.0f64).is_err());
533    }
534
535    #[test]
536    fn percentage_f64_accepts_boundaries() {
537        assert_eq!(PercentFloat::new(0.0).unwrap().get(), 0.0);
538        assert_eq!(PercentFloat::new(50.5).unwrap().get(), 50.5);
539        assert_eq!(PercentFloat::new(100.0).unwrap().get(), 100.0);
540    }
541
542    #[test]
543    fn percentage_f64_rejects_out_of_range() {
544        assert!(PercentFloat::new(-0.1).is_err());
545        assert!(PercentFloat::new(100.1).is_err());
546    }
547
548    #[test]
549    fn percentage_f64_rejects_nan() {
550        assert!(PercentFloat::new(f64::NAN).is_err());
551    }
552
553    #[test]
554    fn percentage_f64_as_fraction() {
555        assert_eq!(PercentFloat::new(25.0).unwrap().as_fraction(), 0.25);
556    }
557
558    #[test]
559    fn percentage_f64_display() {
560        assert_eq!(PercentFloat::new(42.5).unwrap().to_string(), "42.5%");
561    }
562
563    #[test]
564    fn percentage_f64_try_from() {
565        assert!(PercentFloat::try_from(50.0f64).is_ok());
566        assert!(PercentFloat::try_from(101.0f64).is_err());
567    }
568
569    #[test]
570    fn probability_accepts_boundaries() {
571        assert_eq!(Probability::new(0.0).unwrap().get(), 0.0);
572        assert_eq!(Probability::new(0.5).unwrap().get(), 0.5);
573        assert_eq!(Probability::new(1.0).unwrap().get(), 1.0);
574    }
575
576    #[test]
577    fn probability_rejects_out_of_range() {
578        assert!(Probability::new(-0.000001).is_err());
579        assert!(Probability::new(1.000001).is_err());
580    }
581
582    #[test]
583    fn probability_rejects_nan_and_infinity() {
584        assert!(Probability::new(f64::NAN).is_err());
585        assert!(Probability::new(f64::INFINITY).is_err());
586        assert!(Probability::new(f64::NEG_INFINITY).is_err());
587    }
588
589    #[test]
590    fn probability_complement() {
591        assert_eq!(Probability::new(0.25).unwrap().complement().get(), 0.75);
592        assert_eq!(Probability::new(0.0).unwrap().complement().get(), 1.0);
593        assert_eq!(Probability::new(1.0).unwrap().complement().get(), 0.0);
594    }
595
596    #[test]
597    fn probability_display() {
598        assert_eq!(Probability::new(0.5).unwrap().to_string(), "0.5");
599    }
600
601    #[test]
602    fn probability_try_from() {
603        assert!(Probability::try_from(0.5f64).is_ok());
604        assert!(Probability::try_from(2.0f64).is_err());
605    }
606}