1use crate::duration::Duration;
2
3const KILOBYTE: u64 = 1024;
4
5pub trait ByteSize {
7 fn bytes(self) -> u64;
9
10 fn kilobytes(self) -> u64;
12
13 fn megabytes(self) -> u64;
15
16 fn gigabytes(self) -> u64;
18
19 fn terabytes(self) -> u64;
21
22 fn petabytes(self) -> u64;
24
25 fn exabytes(self) -> u64;
27}
28
29pub trait NumericDuration {
31 fn seconds(self) -> Duration;
33
34 fn minutes(self) -> Duration;
36
37 fn hours(self) -> Duration;
39
40 fn days(self) -> Duration;
42
43 fn weeks(self) -> Duration;
45
46 fn months(self) -> Duration;
48
49 fn years(self) -> Duration;
51}
52
53pub trait Ordinalize {
55 fn ordinal(&self) -> &'static str;
57
58 fn ordinalize(&self) -> String;
60}
61
62fn pow_1024(exponent: u32) -> u64 {
63 KILOBYTE.saturating_pow(exponent)
64}
65
66fn clamp_signed_to_u64<T>(value: T) -> u64
67where
68 T: Into<i128>,
69{
70 let value = value.into();
71 if value <= 0 {
72 0
73 } else if value >= u64::MAX as i128 {
74 u64::MAX
75 } else {
76 value as u64
77 }
78}
79
80fn clamp_float_to_u64(value: f64) -> u64 {
81 if !value.is_finite() || value <= 0.0 {
82 0
83 } else if value >= u64::MAX as f64 {
84 u64::MAX
85 } else {
86 value.round() as u64
87 }
88}
89
90fn ordinal_suffix(value: i128) -> &'static str {
91 let abs = value.abs();
92 let last_two = abs % 100;
93 if (11..=13).contains(&last_two) {
94 return "th";
95 }
96
97 match abs % 10 {
98 1 => "st",
99 2 => "nd",
100 3 => "rd",
101 _ => "th",
102 }
103}
104
105macro_rules! impl_integer_numeric_traits {
106 ($($ty:ty),* $(,)?) => {
107 $(
108 impl ByteSize for $ty {
109 fn bytes(self) -> u64 {
110 clamp_signed_to_u64(self)
111 }
112
113 fn kilobytes(self) -> u64 {
114 clamp_signed_to_u64(self).saturating_mul(pow_1024(1))
115 }
116
117 fn megabytes(self) -> u64 {
118 clamp_signed_to_u64(self).saturating_mul(pow_1024(2))
119 }
120
121 fn gigabytes(self) -> u64 {
122 clamp_signed_to_u64(self).saturating_mul(pow_1024(3))
123 }
124
125 fn terabytes(self) -> u64 {
126 clamp_signed_to_u64(self).saturating_mul(pow_1024(4))
127 }
128
129 fn petabytes(self) -> u64 {
130 clamp_signed_to_u64(self).saturating_mul(pow_1024(5))
131 }
132
133 fn exabytes(self) -> u64 {
134 clamp_signed_to_u64(self).saturating_mul(pow_1024(6))
135 }
136 }
137
138 impl NumericDuration for $ty {
139 fn seconds(self) -> Duration {
140 Duration::seconds(self as i64)
141 }
142
143 fn minutes(self) -> Duration {
144 Duration::minutes(self as i64)
145 }
146
147 fn hours(self) -> Duration {
148 Duration::hours(self as i64)
149 }
150
151 fn days(self) -> Duration {
152 Duration::days(self as i64)
153 }
154
155 fn weeks(self) -> Duration {
156 Duration::weeks(self as i64)
157 }
158
159 fn months(self) -> Duration {
160 Duration::months(self as i64)
161 }
162
163 fn years(self) -> Duration {
164 Duration::years(self as i64)
165 }
166 }
167
168 impl Ordinalize for $ty {
169 fn ordinal(&self) -> &'static str {
170 ordinal_suffix(*self as i128)
171 }
172
173 fn ordinalize(&self) -> String {
174 format!("{}{}", self, self.ordinal())
175 }
176 }
177 )*
178 };
179}
180
181impl_integer_numeric_traits!(i32, i64, u32, u64);
182
183impl ByteSize for f64 {
184 fn bytes(self) -> u64 {
185 clamp_float_to_u64(self)
186 }
187
188 fn kilobytes(self) -> u64 {
189 clamp_float_to_u64(self * pow_1024(1) as f64)
190 }
191
192 fn megabytes(self) -> u64 {
193 clamp_float_to_u64(self * pow_1024(2) as f64)
194 }
195
196 fn gigabytes(self) -> u64 {
197 clamp_float_to_u64(self * pow_1024(3) as f64)
198 }
199
200 fn terabytes(self) -> u64 {
201 clamp_float_to_u64(self * pow_1024(4) as f64)
202 }
203
204 fn petabytes(self) -> u64 {
205 clamp_float_to_u64(self * pow_1024(5) as f64)
206 }
207
208 fn exabytes(self) -> u64 {
209 clamp_float_to_u64(self * pow_1024(6) as f64)
210 }
211}
212
213impl NumericDuration for f64 {
214 fn seconds(self) -> Duration {
215 Duration::seconds(self.round() as i64)
216 }
217
218 fn minutes(self) -> Duration {
219 Duration::seconds((self * 60.0).round() as i64)
220 }
221
222 fn hours(self) -> Duration {
223 Duration::seconds((self * 3_600.0).round() as i64)
224 }
225
226 fn days(self) -> Duration {
227 Duration::seconds((self * 86_400.0).round() as i64)
228 }
229
230 fn weeks(self) -> Duration {
231 Duration::seconds((self * 604_800.0).round() as i64)
232 }
233
234 fn months(self) -> Duration {
235 Duration::seconds((self * 2_629_746.0).round() as i64)
236 }
237
238 fn years(self) -> Duration {
239 Duration::seconds((self * 31_556_952.0).round() as i64)
240 }
241}
242
243impl Ordinalize for f64 {
244 fn ordinal(&self) -> &'static str {
245 ordinal_suffix(self.trunc() as i128)
246 }
247
248 fn ordinalize(&self) -> String {
249 if self.fract() == 0.0 {
250 format!("{}{}", *self as i64, self.ordinal())
251 } else {
252 format!("{}{}", self, self.ordinal())
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn bytes_are_identity() {
263 assert_eq!(10_u64.bytes(), 10);
264 }
265
266 #[test]
267 fn kilobytes_scale_by_1024() {
268 assert_eq!(1024_u64.kilobytes(), 1_048_576);
269 }
270
271 #[test]
272 fn megabytes_scale_by_1024_squared() {
273 assert_eq!(5_u64.megabytes(), 5_242_880);
274 }
275
276 #[test]
277 fn gigabytes_scale_by_1024_cubed() {
278 assert_eq!(3_u64.gigabytes(), 3_221_225_472);
279 }
280
281 #[test]
282 fn terabytes_scale_by_1024_fourth() {
283 assert_eq!(1_u64.terabytes(), 1_099_511_627_776);
284 }
285
286 #[test]
287 fn petabytes_scale_by_1024_fifth() {
288 assert_eq!(1_u64.petabytes(), 1_125_899_906_842_624);
289 }
290
291 #[test]
292 fn exabytes_scale_by_1024_sixth() {
293 assert_eq!(1_u64.exabytes(), 1_152_921_504_606_846_976);
294 }
295
296 #[test]
297 fn floating_point_byte_sizes_are_rounded() {
298 assert_eq!(3.5_f64.megabytes(), 3_670_016);
299 assert_eq!(3.5_f64.gigabytes(), 3_758_096_384);
300 }
301
302 #[test]
303 fn negative_signed_byte_sizes_clamp_to_zero() {
304 assert_eq!((-1_i64).bytes(), 0);
305 assert_eq!((-1_i64).megabytes(), 0);
306 }
307
308 #[test]
309 fn duration_factories_match_duration_constructors() {
310 assert_eq!(2_i64.hours(), Duration::hours(2));
311 assert_eq!(5_u32.minutes(), Duration::minutes(5));
312 assert_eq!(1_i32.days(), Duration::days(1));
313 assert_eq!(3_u64.weeks(), Duration::weeks(3));
314 assert_eq!(4_i64.months(), Duration::months(4));
315 assert_eq!(2_i64.years(), Duration::years(2));
316 }
317
318 #[test]
319 fn floating_point_duration_factories_round_to_seconds() {
320 assert_eq!(1.5_f64.hours(), Duration::seconds(5_400));
321 assert_eq!(1.5_f64.days(), Duration::seconds(129_600));
322 }
323
324 #[test]
325 fn ordinal_suffixes_cover_common_cases() {
326 assert_eq!(1_i64.ordinal(), "st");
327 assert_eq!(2_i64.ordinal(), "nd");
328 assert_eq!(3_i64.ordinal(), "rd");
329 assert_eq!(4_i64.ordinal(), "th");
330 }
331
332 #[test]
333 fn ordinal_suffixes_handle_teens() {
334 assert_eq!(11_i64.ordinal(), "th");
335 assert_eq!(12_i64.ordinal(), "th");
336 assert_eq!(13_i64.ordinal(), "th");
337 }
338
339 #[test]
340 fn ordinal_suffixes_handle_large_numbers() {
341 assert_eq!(21_i64.ordinal(), "st");
342 assert_eq!(112_i64.ordinal(), "th");
343 assert_eq!(1_003_i64.ordinal(), "rd");
344 }
345
346 #[test]
347 fn ordinalize_formats_integers() {
348 assert_eq!(1_i64.ordinalize(), "1st");
349 assert_eq!(2_i64.ordinalize(), "2nd");
350 assert_eq!(112_i64.ordinalize(), "112th");
351 }
352
353 #[test]
354 fn ordinalize_formats_zero_and_negative_numbers() {
355 assert_eq!(0_i64.ordinalize(), "0th");
356 assert_eq!((-1_i64).ordinalize(), "-1st");
357 assert_eq!((-11_i64).ordinalize(), "-11th");
358 }
359
360 #[test]
361 fn ordinalize_formats_unsigned_values() {
362 assert_eq!(22_u64.ordinalize(), "22nd");
363 }
364
365 #[test]
366 fn floating_point_ordinalize_uses_truncated_value_for_suffix() {
367 assert_eq!(1.0_f64.ordinalize(), "1st");
368 assert_eq!(2.5_f64.ordinalize(), "2.5nd");
369 }
370
371 #[test]
372 fn byte_size_operations_compose_as_expected() {
373 assert_eq!(1_u64.kilobytes().pow(4), 1_u64.terabytes());
374 assert_eq!(1024_u64.kilobytes() + 2_u64.megabytes(), 3_u64.megabytes());
375 }
376
377 #[test]
378 fn byte_sizes_saturate_at_large_bounds() {
379 assert_eq!(0_i32.kilobytes(), 0);
380 assert_eq!(u64::MAX.kilobytes(), u64::MAX);
381 assert_eq!(i64::MAX.exabytes(), u64::MAX);
382 }
383
384 #[test]
385 fn floating_point_byte_sizes_clamp_non_finite_and_round_small_values() {
386 assert_eq!(f64::NAN.bytes(), 0);
387 assert_eq!(f64::INFINITY.bytes(), 0);
388 assert_eq!(f64::NEG_INFINITY.kilobytes(), 0);
389 assert_eq!(0.49_f64.bytes(), 0);
390 assert_eq!(0.5_f64.bytes(), 1);
391 assert_eq!(1e40_f64.bytes(), u64::MAX);
392 }
393
394 #[test]
395 fn integer_duration_helpers_support_zero_and_negative_values() {
396 assert_eq!(0_i32.seconds(), Duration::seconds(0));
397 assert_eq!((-2_i64).hours(), Duration::hours(-2));
398 assert_eq!((-3_i64).weeks(), Duration::weeks(-3));
399 assert_eq!((-1_i64).months(), Duration::months(-1));
400 assert_eq!((-4_i64).years(), Duration::years(-4));
401 }
402
403 #[test]
404 fn floating_point_duration_helpers_round_minutes_weeks_months_and_years() {
405 assert_eq!(1.5_f64.minutes(), Duration::seconds(90));
406 assert_eq!((-1.5_f64).seconds(), Duration::seconds(-2));
407 assert_eq!(1.25_f64.weeks(), Duration::seconds(756_000));
408 assert_eq!(1.5_f64.months(), Duration::seconds(3_944_619));
409 assert_eq!(1.25_f64.years(), Duration::seconds(39_446_190));
410 }
411
412 #[test]
413 fn ordinal_suffixes_handle_negative_and_large_teens() {
414 assert_eq!((-12_i64).ordinal(), "th");
415 assert_eq!((-23_i64).ordinal(), "rd");
416 assert_eq!(1_011_i64.ordinal(), "th");
417 assert_eq!(1_012_i64.ordinal(), "th");
418 assert_eq!(1_013_i64.ordinal(), "th");
419 }
420
421 #[test]
422 fn floating_point_ordinalize_handles_negative_whole_and_fractional_values() {
423 assert_eq!((-11.0_f64).ordinalize(), "-11th");
424 assert_eq!((-2.5_f64).ordinalize(), "-2.5nd");
425 }
426
427 #[test]
428 fn bytes_are_identity_for_signed_positive_integers() {
429 assert_eq!(2_i32.bytes(), 2);
430 assert_eq!(7_i64.bytes(), 7);
431 }
432
433 #[test]
434 fn bytes_are_identity_for_unsigned_integers() {
435 assert_eq!(3_u32.bytes(), 3);
436 assert_eq!(9_u64.bytes(), 9);
437 }
438
439 #[test]
440 fn kilobytes_scale_signed_values() {
441 assert_eq!(2_i64.kilobytes(), 2_048);
442 }
443
444 #[test]
445 fn megabytes_scale_unsigned_values() {
446 assert_eq!(2_u32.megabytes(), 2_097_152);
447 }
448
449 #[test]
450 fn gigabytes_scale_unsigned_values() {
451 assert_eq!(2_u64.gigabytes(), 2_147_483_648);
452 }
453
454 #[test]
455 fn terabytes_scale_signed_values() {
456 assert_eq!(2_i64.terabytes(), 2_199_023_255_552);
457 }
458
459 #[test]
460 fn petabytes_scale_unsigned_values() {
461 assert_eq!(2_u64.petabytes(), 2_251_799_813_685_248);
462 }
463
464 #[test]
465 fn exabytes_scale_unsigned_values() {
466 assert_eq!(2_u64.exabytes(), 2_305_843_009_213_693_952);
467 }
468
469 #[test]
470 fn negative_signed_sizes_clamp_all_units_to_zero() {
471 assert_eq!((-2_i32).kilobytes(), 0);
472 assert_eq!((-2_i32).gigabytes(), 0);
473 assert_eq!((-2_i32).terabytes(), 0);
474 }
475
476 #[test]
477 fn floating_point_kilobytes_round_to_nearest_byte() {
478 assert_eq!(0.1_f64.kilobytes(), 102);
479 assert_eq!(0.5_f64.kilobytes(), 512);
480 }
481
482 #[test]
483 fn floating_point_terabytes_and_petabytes_scale() {
484 assert_eq!(1.5_f64.terabytes(), 1_649_267_441_664);
485 assert_eq!(1.25_f64.petabytes(), 1_407_374_883_553_280);
486 }
487
488 #[test]
489 fn floating_point_size_helpers_clamp_negative_values() {
490 assert_eq!((-0.5_f64).bytes(), 0);
491 assert_eq!((-0.5_f64).petabytes(), 0);
492 }
493
494 #[test]
495 fn integer_duration_helpers_support_unsigned_values() {
496 assert_eq!(7_u64.seconds(), Duration::seconds(7));
497 assert_eq!(8_u32.minutes(), Duration::minutes(8));
498 assert_eq!(9_u64.days(), Duration::days(9));
499 }
500
501 #[test]
502 fn integer_duration_helpers_support_negative_days() {
503 assert_eq!((-2_i32).days(), Duration::days(-2));
504 }
505
506 #[test]
507 fn floating_point_duration_helpers_round_hours_and_days() {
508 assert_eq!(1.25_f64.hours(), Duration::seconds(4_500));
509 assert_eq!(0.5_f64.days(), Duration::seconds(43_200));
510 }
511
512 #[test]
513 fn floating_point_duration_helpers_handle_zero_and_negative_years() {
514 assert_eq!(0.0_f64.years(), Duration::seconds(0));
515 assert_eq!((-1.25_f64).years(), Duration::seconds(-39_446_190));
516 }
517
518 #[test]
519 fn ordinal_suffixes_handle_zero_and_hundreds() {
520 assert_eq!(0_i64.ordinal(), "th");
521 assert_eq!(100_i64.ordinal(), "th");
522 assert_eq!(101_i64.ordinal(), "st");
523 assert_eq!(102_i64.ordinal(), "nd");
524 assert_eq!(103_i64.ordinal(), "rd");
525 }
526
527 #[test]
528 fn ordinal_suffixes_handle_unsigned_teens_and_twenties() {
529 assert_eq!(11_u32.ordinal(), "th");
530 assert_eq!(12_u32.ordinal(), "th");
531 assert_eq!(13_u32.ordinal(), "th");
532 assert_eq!(21_u32.ordinal(), "st");
533 }
534
535 #[test]
536 fn ordinalize_formats_large_unsigned_and_positive_floats() {
537 assert_eq!(101_u64.ordinalize(), "101st");
538 assert_eq!(3.0_f64.ordinalize(), "3rd");
539 }
540}