Skip to main content

rustrails_support/
numeric_ext.rs

1use crate::duration::Duration;
2
3const KILOBYTE: u64 = 1024;
4
5/// Binary byte-size conversions.
6pub trait ByteSize {
7    /// Returns the value in bytes.
8    fn bytes(self) -> u64;
9
10    /// Returns the value in kibibytes.
11    fn kilobytes(self) -> u64;
12
13    /// Returns the value in mebibytes.
14    fn megabytes(self) -> u64;
15
16    /// Returns the value in gibibytes.
17    fn gigabytes(self) -> u64;
18
19    /// Returns the value in tebibytes.
20    fn terabytes(self) -> u64;
21
22    /// Returns the value in pebibytes.
23    fn petabytes(self) -> u64;
24
25    /// Returns the value in exbibytes.
26    fn exabytes(self) -> u64;
27}
28
29/// Duration constructors on numeric types.
30pub trait NumericDuration {
31    /// Creates a duration in seconds.
32    fn seconds(self) -> Duration;
33
34    /// Creates a duration in minutes.
35    fn minutes(self) -> Duration;
36
37    /// Creates a duration in hours.
38    fn hours(self) -> Duration;
39
40    /// Creates a duration in days.
41    fn days(self) -> Duration;
42
43    /// Creates a duration in weeks.
44    fn weeks(self) -> Duration;
45
46    /// Creates a duration in months.
47    fn months(self) -> Duration;
48
49    /// Creates a duration in years.
50    fn years(self) -> Duration;
51}
52
53/// English ordinal suffix formatting.
54pub trait Ordinalize {
55    /// Returns the ordinal suffix for the numeric value.
56    fn ordinal(&self) -> &'static str;
57
58    /// Returns the numeric value followed by its ordinal suffix.
59    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}