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
176// ===== Tests =====
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::time::Duration;
181
182    // Constants for calculating the exact overflow boundary of u64 in minutes/hours.
183    const MAX_FOR_MINUTES: u64 = u64::MAX / 60;
184    const MAX_FOR_HOURS: u64 = u64::MAX / 3600;
185
186    const OVERFLOW_MINUTES: u64 = MAX_FOR_MINUTES + 1;
187    const OVERFLOW_HOURS: u64 = MAX_FOR_HOURS + 1;
188
189    // --- u64 Tests ---
190    #[test]
191    fn test_u64_large_units() {
192        let five: u64 = 5;
193        assert_eq!(five.minutes(), Duration::from_secs(5 * 60));
194        assert_eq!(five.hours(), Duration::from_secs(5 * 3600));
195    }
196
197    #[test]
198    #[should_panic(expected = "overflows u64 seconds capacity")]
199    fn test_u64_minutes_panics_on_overflow() {
200        let _ = OVERFLOW_MINUTES.minutes();
201    }
202
203    #[test]
204    #[should_panic(expected = "overflows u64 seconds capacity")]
205    fn test_u64_hours_panics_on_overflow() {
206        let _ = OVERFLOW_HOURS.hours();
207    }
208
209    #[test]
210    fn test_max_u64_small_units() {
211        let max_u64 = u64::MAX;
212        assert_eq!(max_u64.milliseconds(), Duration::from_millis(u64::MAX));
213        assert_eq!(max_u64.microseconds(), Duration::from_micros(u64::MAX));
214        assert_eq!(max_u64.nanoseconds(), Duration::from_nanos(u64::MAX));
215    }
216
217    // --- i64 Tests ---
218    #[test]
219    fn test_i64_positive() {
220        let pos: i64 = 10;
221        assert_eq!(pos.seconds(), Duration::from_secs(10));
222        assert_eq!(pos.minutes(), Duration::from_secs(600));
223        assert_eq!(pos.hours(), Duration::from_secs(36000));
224    }
225
226    #[test]
227    #[should_panic(expected = "duration cannot be negative")]
228    fn test_i64_negative_panics() {
229        let neg: i64 = -10;
230        let _ = neg.minutes();
231    }
232
233    #[test]
234    #[should_panic(expected = "overflows u64 seconds capacity")]
235    fn test_i64_minutes_panics_on_overflow() {
236        let _ = (OVERFLOW_MINUTES as i64).minutes();
237    }
238
239    #[test]
240    #[should_panic(expected = "overflows u64 seconds capacity")]
241    fn test_i64_hours_panics_on_overflow() {
242        let _ = (OVERFLOW_HOURS as i64).hours();
243    }
244    
245    // --- i32/u32 Tests ---
246
247    #[test]
248    fn test_i32_positive_full() {
249        let val: i32 = 5;
250        assert_eq!(val.seconds(), Duration::from_secs(5));
251        assert_eq!(val.minutes(), Duration::from_secs(300));
252        assert_eq!(val.hours(), Duration::from_secs(18000));
253    }
254
255    #[test]
256    #[should_panic(expected = "duration cannot be negative")]
257    fn test_i32_negative_panics() { let _ = (-5).seconds(); }
258    
259    // u32 test to ensure delegation works for non-overflow cases
260    #[test]
261    fn test_u32_positive_full() {
262        let val: u32 = 5;
263        assert_eq!(val.seconds(), Duration::from_secs(5));
264        assert_eq!(val.minutes(), Duration::from_secs(300));
265        assert_eq!(val.hours(), Duration::from_secs(18000));
266    }
267}