reliakit_primitives/
numeric.rs1use crate::{PrimitiveError, PrimitiveResult};
2use core::fmt;
3
4#[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 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 pub const fn get(self) -> u8 {
26 self.0
27 }
28
29 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#[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 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 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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct ByteSize(u64);
104
105impl ByteSize {
106 pub const fn from_bytes(bytes: u64) -> Self {
108 Self(bytes)
109 }
110
111 pub const fn from_kb(kb: u64) -> Self {
115 Self(kb.saturating_mul(1024))
116 }
117
118 pub const fn from_mb(mb: u64) -> Self {
122 Self(mb.saturating_mul(1024 * 1024))
123 }
124
125 pub const fn from_gb(gb: u64) -> Self {
129 Self(gb.saturating_mul(1024 * 1024 * 1024))
130 }
131
132 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}