duration_extender/
lib.rs

1use std::time::Duration;
2
3/// An extension trait that adds fluent time unit methods to integer primitives,
4/// allowing for highly readable time duration creation.
5///
6/// This crate is optimized for **system timing** (timeouts, sleeps, fixed cache TTLs).
7/// It explicitly excludes methods for units longer than hours (days, weeks) 
8/// to prevent calendar errors related to Daylight Saving Time (DST) and time zones.
9///
10/// # Panics
11///
12/// - Signed integers (`i32`, `i64`) **panic** if the value is negative.
13/// - Overflow panics for `.minutes()` and `.hours()` when the resulting seconds exceed `u64::MAX`.
14///
15/// # Examples
16///
17/// ```rust
18/// use duration_extender::DurationExt;
19/// use std::time::Duration;
20///
21/// let timeout = 10.seconds();
22/// let delay = 5.minutes();
23/// 
24/// // For a fixed 2-day duration, use the hour equivalent:
25/// let fixed_long_wait = (2 * 24).hours();
26///
27/// let total_time = 2.hours() + 30.minutes() + 15.seconds();
28///
29/// // Signed integers must be non-negative
30/// let elapsed = 100.seconds(); // ✅ Works
31/// // let bad = (-100).seconds(); // ❌ Panics!
32/// ```
33pub trait DurationExt {
34    /// Creates a `Duration` representing this many seconds.
35    fn seconds(self) -> Duration;
36    /// Creates a `Duration` representing this many minutes.
37    fn minutes(self) -> Duration;
38    /// Creates a `Duration` representing this many hours.
39    fn hours(self) -> Duration;
40    /// Creates a `Duration` representing this many milliseconds.
41    fn milliseconds(self) -> Duration;
42    /// Creates a `Duration` representing this many microseconds.
43    fn microseconds(self) -> Duration;
44    /// Creates a `Duration` representing this many nanoseconds.
45    fn nanoseconds(self) -> Duration;
46}
47
48// ===== Implementation for unsigned integers =====
49impl DurationExt for u64 {
50    fn seconds(self) -> Duration {
51        Duration::from_secs(self)
52    }
53
54    fn minutes(self) -> Duration {
55        let secs = self.checked_mul(60)
56            .expect(&format!("duration value {} minutes overflows u64 seconds capacity", self));
57        Duration::from_secs(secs)
58    }
59
60    fn hours(self) -> Duration {
61        let secs = self.checked_mul(3600)
62            .expect(&format!("duration value {} hours overflows u64 seconds capacity", self));
63        Duration::from_secs(secs)
64    }
65
66    fn milliseconds(self) -> Duration {
67        Duration::from_millis(self)
68    }
69
70    fn microseconds(self) -> Duration {
71        Duration::from_micros(self)
72    }
73
74    fn nanoseconds(self) -> Duration {
75        Duration::from_nanos(self)
76    }
77}
78
79impl DurationExt for u32 {
80    fn seconds(self) -> Duration {
81        Duration::from_secs(self as u64)
82    }
83
84    // Delegates to u64's implementation which includes the correct overflow check
85    fn minutes(self) -> Duration {
86        (self as u64).minutes()
87    }
88
89    // Delegates to u64's implementation which includes the correct overflow check
90    fn hours(self) -> Duration {
91        (self as u64).hours()
92    }
93
94    fn milliseconds(self) -> Duration {
95        Duration::from_millis(self as u64)
96    }
97
98    fn microseconds(self) -> Duration {
99        Duration::from_micros(self as u64)
100    }
101
102    fn nanoseconds(self) -> Duration {
103        Duration::from_nanos(self as u64)
104    }
105}
106
107// ===== Implementation for signed integers =====
108impl DurationExt for i64 {
109    fn seconds(self) -> Duration {
110        assert!(self >= 0, "duration cannot be negative: got {} seconds", self);
111        Duration::from_secs(self as u64)
112    }
113
114    fn minutes(self) -> Duration {
115        assert!(self >= 0, "duration cannot be negative: got {} minutes", self);
116        // Delegates to u64 for the multiplication and overflow check
117        (self as u64).minutes()
118    }
119
120    fn hours(self) -> Duration {
121        assert!(self >= 0, "duration cannot be negative: got {} hours", self);
122        // Delegates to u64 for the multiplication and overflow check
123        (self as u64).hours()
124    }
125
126    fn milliseconds(self) -> Duration {
127        assert!(self >= 0, "duration cannot be negative: got {} milliseconds", self);
128        Duration::from_millis(self as u64)
129    }
130
131    fn microseconds(self) -> Duration {
132        assert!(self >= 0, "duration cannot be negative: got {} microseconds", self);
133        Duration::from_micros(self as u64)
134    }
135
136    fn nanoseconds(self) -> Duration {
137        assert!(self >= 0, "duration cannot be negative: got {} nanoseconds", self);
138        Duration::from_nanos(self as u64)
139    }
140}
141
142impl DurationExt for i32 {
143    fn seconds(self) -> Duration {
144        assert!(self >= 0, "duration cannot be negative: got {} seconds", self);
145        Duration::from_secs(self as u64)
146    }
147
148    fn minutes(self) -> Duration {
149        assert!(self >= 0, "duration cannot be negative: got {} minutes", self);
150        // Delegates to u64 for the multiplication and overflow check
151        (self as u64).minutes()
152    }
153
154    fn hours(self) -> Duration {
155        assert!(self >= 0, "duration cannot be negative: got {} hours", self);
156        // Delegates to u64 for the multiplication and overflow check
157        (self as u64).hours()
158    }
159
160    fn milliseconds(self) -> Duration {
161        assert!(self >= 0, "duration cannot be negative: got {} milliseconds", self);
162        Duration::from_millis(self as u64)
163    }
164
165    fn microseconds(self) -> Duration {
166        assert!(self >= 0, "duration cannot be negative: got {} microseconds", self);
167        Duration::from_micros(self as u64)
168    }
169
170    fn nanoseconds(self) -> Duration {
171        assert!(self >= 0, "duration cannot be negative: got {} nanoseconds", self);
172        Duration::from_nanos(self as u64)
173    }
174}
175
176impl DurationExt for f64 {
177    fn seconds(self) -> Duration {
178        Duration::from_secs_f64(self)
179    }
180
181    fn minutes(self) -> Duration {
182        Duration::from_secs_f64(self * 60.0)
183    }
184
185    fn hours(self) -> Duration {
186        Duration::from_secs_f64(self * 3600.0)
187    }
188
189    fn milliseconds(self) -> Duration {
190        Duration::from_secs_f64(self / 1000.0)
191    }
192
193    fn microseconds(self) -> Duration {
194        Duration::from_secs_f64(self / 1_000_000.0)
195    }
196
197    fn nanoseconds(self) -> Duration {
198        Duration::from_secs_f64(self / 1_000_000_000.0)
199    }
200}
201
202
203impl DurationExt for f32 {
204    fn seconds(self) -> Duration {
205        Duration::from_secs_f32(self)
206    }
207
208    fn minutes(self) -> Duration {
209        Duration::from_secs_f32(self * 60.0)
210    }
211
212    fn hours(self) -> Duration {
213        Duration::from_secs_f32(self * 3600.0)
214    }
215
216    fn milliseconds(self) -> Duration {
217        Duration::from_secs_f32(self / 1000.0)
218    }
219
220    fn microseconds(self) -> Duration {
221        Duration::from_secs_f32(self / 1_000_000.0)
222    }
223
224    fn nanoseconds(self) -> Duration {
225        Duration::from_secs_f32(self / 1_000_000_000.0)
226    }
227}
228
229// ===== Tests =====
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::time::Duration;
234
235    // Constants for calculating the exact overflow boundary of u64 in minutes/hours.
236    const MAX_FOR_MINUTES: u64 = u64::MAX / 60;
237    const MAX_FOR_HOURS: u64 = u64::MAX / 3600;
238
239    const OVERFLOW_MINUTES: u64 = MAX_FOR_MINUTES + 1;
240    const OVERFLOW_HOURS: u64 = MAX_FOR_HOURS + 1;
241
242    // --- u64 Tests ---
243    #[test]
244    fn test_u64_large_units() {
245        let five: u64 = 5;
246        assert_eq!(five.minutes(), Duration::from_secs(5 * 60));
247        assert_eq!(five.hours(), Duration::from_secs(5 * 3600));
248    }
249
250    #[test]
251    #[should_panic(expected = "overflows u64 seconds capacity")]
252    fn test_u64_minutes_panics_on_overflow() {
253        let _ = OVERFLOW_MINUTES.minutes();
254    }
255
256    #[test]
257    #[should_panic(expected = "overflows u64 seconds capacity")]
258    fn test_u64_hours_panics_on_overflow() {
259        let _ = OVERFLOW_HOURS.hours();
260    }
261
262    #[test]
263    fn test_max_u64_small_units() {
264        let max_u64 = u64::MAX;
265        assert_eq!(max_u64.milliseconds(), Duration::from_millis(u64::MAX));
266        assert_eq!(max_u64.microseconds(), Duration::from_micros(u64::MAX));
267        assert_eq!(max_u64.nanoseconds(), Duration::from_nanos(u64::MAX));
268    }
269
270    // --- i64 Tests ---
271    #[test]
272    fn test_i64_positive() {
273        let pos: i64 = 10;
274        assert_eq!(pos.seconds(), Duration::from_secs(10));
275        assert_eq!(pos.minutes(), Duration::from_secs(600));
276        assert_eq!(pos.hours(), Duration::from_secs(36000));
277    }
278
279    #[test]
280    #[should_panic(expected = "duration cannot be negative")]
281    fn test_i64_negative_panics() {
282        let neg: i64 = -10;
283        let _ = neg.minutes();
284    }
285
286    #[test]
287    #[should_panic(expected = "overflows u64 seconds capacity")]
288    fn test_i64_minutes_panics_on_overflow() {
289        let _ = (OVERFLOW_MINUTES as i64).minutes();
290    }
291
292    #[test]
293    #[should_panic(expected = "overflows u64 seconds capacity")]
294    fn test_i64_hours_panics_on_overflow() {
295        let _ = (OVERFLOW_HOURS as i64).hours();
296    }
297    
298    // --- i32/u32 Tests ---
299
300    #[test]
301    fn test_i32_positive_full() {
302        let val: i32 = 5;
303        assert_eq!(val.seconds(), Duration::from_secs(5));
304        assert_eq!(val.minutes(), Duration::from_secs(300));
305        assert_eq!(val.hours(), Duration::from_secs(18000));
306    }
307
308    #[test]
309    #[should_panic(expected = "duration cannot be negative")]
310    fn test_i32_negative_panics() { let _ = (-5).seconds(); }
311    
312    // u32 test to ensure delegation works for non-overflow cases
313    #[test]
314    fn test_u32_positive_full() {
315        let val: u32 = 5;
316        assert_eq!(val.seconds(), Duration::from_secs(5));
317        assert_eq!(val.minutes(), Duration::from_secs(300));
318        assert_eq!(val.hours(), Duration::from_secs(18000));
319    }
320
321    #[test]
322    fn test_f64_fractional() {
323        let half = 0.5.seconds();
324        assert_eq!(half, Duration::from_millis(500));
325        
326        let two_half = 2.5.minutes();
327        assert_eq!(two_half, Duration::from_secs(150));
328        
329        let one_half_hour = 1.5.hours();
330        assert_eq!(one_half_hour, Duration::from_secs(5400));
331    }
332
333    #[test]
334    #[should_panic]
335    fn test_f64_nan_panics() {
336        let _ = f64::NAN.seconds();
337    }
338
339    #[test]
340    #[should_panic]
341    fn test_f64_infinity_panics() {
342        let _ = f64::INFINITY.seconds();
343    }
344
345    #[test]
346    #[should_panic]
347    fn test_f64_negative_panics() {
348        let _ = (-1.5).seconds();
349    }
350
351    #[test]
352    fn test_f64_zero() {
353        let zero = 0.0.seconds();
354        assert_eq!(zero, Duration::ZERO);
355    }
356
357    // --- f32 Tests ---
358    #[test]
359    fn test_f32_fractional() {
360        let half: f32 = 0.5;
361        assert_eq!(half.seconds(), Duration::from_millis(500));
362    }
363
364    #[test]
365    #[should_panic]
366    fn test_f32_nan_panics() {
367        let _ = f32::NAN.seconds();
368    }
369}