ext_time/
extend_time.rs

1use std::ops::Sub;
2use thiserror::Error;
3use time::{Duration, Time, ext::NumericalDuration};
4
5#[derive(Error, Debug)]
6pub enum TimeError {
7    #[error("Invalid time format. Expected HH:MM or H:MM, got: {0}")]
8    InvalidFormat(String),
9    #[error("Invalid time components: {0}:{1}")]
10    InvalidComponents(u8, u8),
11    #[error("Failed to reset seconds for time: {0:?}")]
12    ResetSecondsError(Time),
13    #[error("Invalid seconds value: {0}")]
14    InvalidSeconds(i64),
15    #[error("Invalid alignment unit: {0}")]
16    InvalidAlignmentUnit(u64),
17    #[error("Failed to add time: {0:?}")]
18    AddTimeError(Time),
19}
20
21/// Extension trait for Time struct providing additional utility methods
22pub trait ExtTime {
23    /// Format time as HH:MM, padding minutes with zero if needed
24    ///
25    /// # Example
26    /// ```
27    /// use time::macros::time;
28    /// use ext_time::ExtTime;
29    ///
30    /// let t = time!(9:05);
31    /// assert_eq!(t.to_shorten(), "9:05");
32    /// ```
33    fn to_shorten(&self) -> String;
34
35    /// Parse time string in HH:MM format
36    ///
37    /// # Arguments
38    /// * `time_str` - Time string in "HH:MM" format
39    ///
40    /// # Returns
41    /// * `Ok(Time)` - Parsed time
42    /// * `Err` - If parsing fails
43    fn from_str(time_str: &str) -> Result<Time, TimeError>;
44
45    /// Calculate duration between two times, handling cross-day scenarios
46    ///
47    /// # Arguments
48    /// * `right` - The time to subtract from self
49    ///
50    /// # Returns
51    /// Duration between times, always positive by adding 24 hours if needed
52    fn sub_ext(&self, right: Time) -> Duration;
53
54    /// Reset seconds to zero, keeping hours and minutes
55    fn reset_minute(&self) -> Result<Time, TimeError>;
56
57    /// Check if two times are in the same minute
58    fn is_same_minute(&self, other: &Time) -> bool;
59
60    /// Check if time is between start and end (inclusive)
61    /// Handles cross-day ranges (e.g., 23:00 to 01:00)
62    fn is_between(&self, start: Time, end: Time) -> bool;
63
64    /// Add minutes to time, wrapping around midnight if needed
65    fn add_minutes(&self, minutes: i64) -> Time;
66
67    /// Convert seconds (hours + minutes + seconds) to Time
68    ///
69    /// # Arguments
70    /// * `seconds` - Total seconds (hours * 3600 + minutes * 60 + seconds)
71    ///
72    /// # Returns
73    /// * `Ok(Time)` - Converted time
74    /// * `Err` - If seconds value is invalid
75    fn from_seconds(seconds: i64) -> Result<Time, TimeError>;
76
77    /// Convert Time to seconds (hours + minutes + seconds)
78    ///
79    /// # Returns
80    /// Total seconds (hours * 3600 + minutes * 60 + seconds)
81    fn to_seconds(&self) -> i64;
82
83    /// Align time to the nearest interval
84    ///
85    /// # Arguments
86    /// * `interval` - Interval in seconds (can be negative for backward alignment)
87    ///
88    /// # Returns
89    /// * `Ok(Time)` - Aligned time
90    /// * `Err(Error)` - If interval is 0
91    fn align_to(&self, interval: i64) -> Result<Time, TimeError>;
92
93    /// Get next day at the same time
94    fn next_day(&self) -> Time;
95
96    /// Get next hour at the same minute and second
97    fn next_hour(&self) -> Time;
98
99    /// Get next minute at the same second
100    fn next_minute(&self) -> Time;
101
102    /// Get next second
103    fn next_second(&self) -> Time;
104
105    /// Convert time to seconds, ignoring minutes and seconds
106    ///
107    /// # Returns
108    /// Total seconds of hours (hours * 3600)
109    ///
110    /// Note: Returns i64 to support time differences and negative values
111    fn to_hour_seconds(&self) -> i64;
112
113    /// Convert time to seconds, ignoring seconds
114    ///
115    /// # Returns
116    /// Total seconds of hours and minutes (hours * 3600 + minutes * 60)
117    ///
118    /// Note: Returns i64 to support time differences and negative values
119    fn to_minute_seconds(&self) -> i64;
120}
121
122impl ExtTime for Time {
123    fn to_shorten(&self) -> String {
124        format!("{}:{:02}", self.hour(), self.minute())
125    }
126
127    fn from_str(time_str: &str) -> Result<Time, TimeError> {
128        let parts: Vec<&str> = time_str.split(':').collect();
129        if parts.len() == 2 {
130            if let (Ok(hour), Ok(minute)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>()) {
131                if hour < 24 && minute < 60 {
132                    return Time::from_hms(hour, minute, 0)
133                        .map_err(|_| TimeError::InvalidComponents(hour, minute));
134                }
135            }
136        }
137
138        Err(TimeError::InvalidFormat(time_str.to_string()))
139    }
140
141    fn sub_ext(&self, right: Time) -> Duration {
142        let diff = self.clone().sub(right);
143        if diff.is_negative() {
144            24.hours() + diff
145        } else {
146            diff
147        }
148    }
149
150    fn reset_minute(&self) -> Result<Time, TimeError> {
151        Time::from_hms(self.hour(), self.minute(), 0)
152            .map_err(|_| TimeError::ResetSecondsError(*self))
153    }
154
155    fn is_same_minute(&self, other: &Time) -> bool {
156        self.minute() == other.minute() && self.hour() == other.hour()
157    }
158
159    fn is_between(&self, start: Time, end: Time) -> bool {
160        if start <= end {
161            *self >= start && *self <= end
162        } else {
163            // Handle cross-day range (e.g., 23:00 to 01:00)
164            *self >= start || *self <= end
165        }
166    }
167
168    fn add_minutes(&self, minutes: i64) -> Time {
169        let total_minutes = self.hour() as i64 * 60 + self.minute() as i64 + minutes;
170        let normalized_minutes = total_minutes.rem_euclid(24 * 60);
171        let hours = (normalized_minutes / 60) as u8;
172        let minutes = (normalized_minutes % 60) as u8;
173        Time::from_hms(hours, minutes, self.second()).unwrap()
174    }
175
176    fn from_seconds(seconds: i64) -> Result<Time, TimeError> {
177        if seconds < 0 || seconds >= 24 * 3600 {
178            return Err(TimeError::InvalidSeconds(seconds));
179        }
180
181        let hours = (seconds / 3600) as u8;
182        let minutes = ((seconds % 3600) / 60) as u8;
183        let secs = (seconds % 60) as u8;
184
185        Time::from_hms(hours, minutes, secs)
186            .map_err(|_| TimeError::InvalidComponents(hours, minutes))
187    }
188
189    fn to_seconds(&self) -> i64 {
190        self.hour() as i64 * 3600 + self.minute() as i64 * 60 + self.second() as i64
191    }
192
193    fn align_to(&self, interval: i64) -> Result<Time, TimeError> {
194        if interval == 0 {
195            return Err(TimeError::InvalidAlignmentUnit(interval.abs() as u64));
196        }
197
198        let total_seconds = self.to_seconds();
199        let aligned_seconds = (total_seconds / interval) * interval;
200
201        Time::from_seconds(aligned_seconds).map_err(|_| TimeError::InvalidSeconds(aligned_seconds))
202    }
203
204    fn next_day(&self) -> Time {
205        // Since Time doesn't have day concept, we just return the same time
206        *self
207    }
208
209    fn next_hour(&self) -> Time {
210        let next_hour = (self.hour() + 1) % 24;
211        Time::from_hms(next_hour, self.minute(), self.second()).unwrap()
212    }
213
214    fn next_minute(&self) -> Time {
215        if self.minute() == 59 {
216            let next_hour = (self.hour() + 1) % 24;
217            Time::from_hms(next_hour, 0, self.second()).unwrap()
218        } else {
219            Time::from_hms(self.hour(), self.minute() + 1, self.second()).unwrap()
220        }
221    }
222
223    fn next_second(&self) -> Time {
224        if self.second() == 59 {
225            if self.minute() == 59 {
226                let next_hour = (self.hour() + 1) % 24;
227                Time::from_hms(next_hour, 0, 0).unwrap()
228            } else {
229                Time::from_hms(self.hour(), self.minute() + 1, 0).unwrap()
230            }
231        } else {
232            Time::from_hms(self.hour(), self.minute(), self.second() + 1).unwrap()
233        }
234    }
235
236    fn to_hour_seconds(&self) -> i64 {
237        self.hour() as i64 * 3600
238    }
239
240    fn to_minute_seconds(&self) -> i64 {
241        self.hour() as i64 * 3600 + self.minute() as i64 * 60
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use time::macros::time;
249
250    #[test]
251    fn test_shorten() {
252        let t = time!(22:01);
253        assert_eq!(t.to_shorten(), "22:01");
254
255        let t = time!(9:05);
256        assert_eq!(t.to_shorten(), "9:05");
257    }
258
259    #[test]
260    fn test_from_str() {
261        let t = <Time as ExtTime>::from_str("9:30").unwrap();
262        assert_eq!(t.hour(), 9);
263        assert_eq!(t.minute(), 30);
264
265        assert!(<Time as ExtTime>::from_str("25:00").is_err());
266        assert!(<Time as ExtTime>::from_str("invalid").is_err());
267    }
268
269    #[test]
270    fn test_sub_ext() {
271        let t1 = time!(23:00);
272        let t2 = time!(1:00);
273        assert_eq!(t1.sub_ext(t2), Duration::hours(22));
274        assert_eq!(t2.sub_ext(t1), Duration::hours(2));
275    }
276
277    #[test]
278    fn test_is_between() {
279        let t = time!(23:30);
280        assert!(t.is_between(time!(23:00), time!(0:00)));
281
282        let t = time!(0:30);
283        assert!(t.is_between(time!(23:00), time!(1:00)));
284
285        let t = time!(12:00);
286        assert!(!t.is_between(time!(23:00), time!(1:00)));
287    }
288
289    #[test]
290    fn test_add_minutes() {
291        let t = time!(23:30);
292        assert_eq!(t.add_minutes(40).to_shorten(), "0:10");
293
294        let t = time!(12:00);
295        assert_eq!(t.add_minutes(-30).to_shorten(), "11:30");
296    }
297
298    #[test]
299    fn test_from_seconds() {
300        let t = <Time as ExtTime>::from_seconds(37230).unwrap(); // 10:20:30
301        assert_eq!(t.hour(), 10);
302        assert_eq!(t.minute(), 20);
303        assert_eq!(t.second(), 30);
304
305        let t = <Time as ExtTime>::from_seconds(3660).unwrap(); // 1:01:00
306        assert_eq!(t.hour(), 1);
307        assert_eq!(t.minute(), 1);
308        assert_eq!(t.second(), 0);
309
310        assert!(<Time as ExtTime>::from_seconds(-1).is_err());
311        assert!(<Time as ExtTime>::from_seconds(24 * 3600).is_err());
312    }
313
314    #[test]
315    fn test_to_seconds() {
316        let t = time!(10:20:30);
317        assert_eq!(t.to_seconds(), 37230);
318
319        let t = time!(1:01:00);
320        assert_eq!(t.to_seconds(), 3660);
321
322        let t = time!(0:00:00);
323        assert_eq!(t.to_seconds(), 0);
324
325        let t = time!(23:59:59);
326        assert_eq!(t.to_seconds(), 86399);
327    }
328
329    #[test]
330    fn test_align_to() {
331        // Test alignment to 5 minutes
332        let t = time!(10:34:00);
333        let aligned = t.align_to(300).unwrap(); // 5 minutes = 300 seconds
334        assert_eq!(aligned.hour(), 10);
335        assert_eq!(aligned.minute(), 30);
336        assert_eq!(aligned.second(), 0);
337
338        // Test alignment to 5 seconds
339        let t = time!(00:00:03);
340        let aligned = t.align_to(5).unwrap();
341        assert_eq!(aligned.hour(), 0);
342        assert_eq!(aligned.minute(), 0);
343        assert_eq!(aligned.second(), 0);
344
345        // Test alignment to 1 hour
346        let t = time!(14:30:45);
347        let aligned = t.align_to(3600).unwrap();
348        assert_eq!(aligned.hour(), 14);
349        assert_eq!(aligned.minute(), 0);
350        assert_eq!(aligned.second(), 0);
351
352        // Test invalid unit
353        let t = time!(10:00:00);
354        assert!(t.align_to(0).is_err());
355        assert!(t.align_to(24 * 3600).is_err());
356    }
357
358    #[test]
359    fn test_next_hour() {
360        let t = time!(10:30:45);
361        let next = t.next_hour();
362        assert_eq!(next.hour(), 11);
363        assert_eq!(next.minute(), 30);
364        assert_eq!(next.second(), 45);
365
366        let t = time!(23:30:45);
367        let next = t.next_hour();
368        assert_eq!(next.hour(), 0);
369        assert_eq!(next.minute(), 30);
370        assert_eq!(next.second(), 45);
371    }
372
373    #[test]
374    fn test_next_minute() {
375        let t = time!(10:30:45);
376        let next = t.next_minute();
377        assert_eq!(next.hour(), 10);
378        assert_eq!(next.minute(), 31);
379        assert_eq!(next.second(), 45);
380
381        let t = time!(10:59:45);
382        let next = t.next_minute();
383        assert_eq!(next.hour(), 11);
384        assert_eq!(next.minute(), 0);
385        assert_eq!(next.second(), 45);
386
387        let t = time!(23:59:45);
388        let next = t.next_minute();
389        assert_eq!(next.hour(), 0);
390        assert_eq!(next.minute(), 0);
391        assert_eq!(next.second(), 45);
392    }
393
394    #[test]
395    fn test_next_second() {
396        let t = time!(10:30:45);
397        let next = t.next_second();
398        assert_eq!(next.hour(), 10);
399        assert_eq!(next.minute(), 30);
400        assert_eq!(next.second(), 46);
401
402        let t = time!(10:30:59);
403        let next = t.next_second();
404        assert_eq!(next.hour(), 10);
405        assert_eq!(next.minute(), 31);
406        assert_eq!(next.second(), 0);
407
408        let t = time!(10:59:59);
409        let next = t.next_second();
410        assert_eq!(next.hour(), 11);
411        assert_eq!(next.minute(), 0);
412        assert_eq!(next.second(), 0);
413
414        let t = time!(23:59:59);
415        let next = t.next_second();
416        assert_eq!(next.hour(), 0);
417        assert_eq!(next.minute(), 0);
418        assert_eq!(next.second(), 0);
419    }
420
421    #[test]
422    fn test_to_hour_seconds() {
423        let t = time!(10:20:30);
424        assert_eq!(t.to_hour_seconds(), 36000); // 10 * 3600
425
426        let t = time!(0:30:45);
427        assert_eq!(t.to_hour_seconds(), 0);
428
429        let t = time!(23:59:59);
430        assert_eq!(t.to_hour_seconds(), 82800); // 23 * 3600
431    }
432
433    #[test]
434    fn test_to_minute_seconds() {
435        let t = time!(10:20:30);
436        assert_eq!(t.to_minute_seconds(), 37200); // 10 * 3600 + 20 * 60
437
438        let t = time!(0:30:45);
439        assert_eq!(t.to_minute_seconds(), 1800); // 30 * 60
440
441        let t = time!(23:59:59);
442        assert_eq!(t.to_minute_seconds(), 86340); // 23 * 3600 + 59 * 60
443    }
444}