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 specified unit
84    ///
85    /// # Arguments
86    /// * `unit_seconds` - The unit to align to in seconds (e.g., 300 for 5 minutes, 5 for 5 seconds)
87    ///
88    /// # Returns
89    /// * `Ok(Time)` - Aligned time
90    /// * `Err` - If unit is invalid (must be positive and less than 24 hours)
91    fn align_to(&self, unit_seconds: u64) -> 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    fn to_hour_seconds(&self) -> i64;
110
111    /// Convert time to seconds, ignoring seconds
112    ///
113    /// # Returns
114    /// Total seconds of hours and minutes (hours * 3600 + minutes * 60)
115    fn to_minute_seconds(&self) -> i64;
116}
117
118impl ExtTime for Time {
119    fn to_shorten(&self) -> String {
120        format!("{}:{:02}", self.hour(), self.minute())
121    }
122
123    fn from_str(time_str: &str) -> Result<Time, TimeError> {
124        let parts: Vec<&str> = time_str.split(':').collect();
125        if parts.len() == 2 {
126            if let (Ok(hour), Ok(minute)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>()) {
127                if hour < 24 && minute < 60 {
128                    return Time::from_hms(hour, minute, 0)
129                        .map_err(|_| TimeError::InvalidComponents(hour, minute));
130                }
131            }
132        }
133
134        Err(TimeError::InvalidFormat(time_str.to_string()))
135    }
136
137    fn sub_ext(&self, right: Time) -> Duration {
138        let diff = self.clone().sub(right);
139        if diff.is_negative() {
140            24.hours() + diff
141        } else {
142            diff
143        }
144    }
145
146    fn reset_minute(&self) -> Result<Time, TimeError> {
147        Time::from_hms(self.hour(), self.minute(), 0)
148            .map_err(|_| TimeError::ResetSecondsError(*self))
149    }
150
151    fn is_same_minute(&self, other: &Time) -> bool {
152        self.minute() == other.minute() && self.hour() == other.hour()
153    }
154
155    fn is_between(&self, start: Time, end: Time) -> bool {
156        if start <= end {
157            *self >= start && *self <= end
158        } else {
159            // Handle cross-day range (e.g., 23:00 to 01:00)
160            *self >= start || *self <= end
161        }
162    }
163
164    fn add_minutes(&self, minutes: i64) -> Time {
165        let total_minutes = self.hour() as i64 * 60 + self.minute() as i64 + minutes;
166        let normalized_minutes = total_minutes.rem_euclid(24 * 60);
167        let hours = (normalized_minutes / 60) as u8;
168        let minutes = (normalized_minutes % 60) as u8;
169        Time::from_hms(hours, minutes, self.second()).unwrap()
170    }
171
172    fn from_seconds(seconds: i64) -> Result<Time, TimeError> {
173        if seconds < 0 || seconds >= 24 * 3600 {
174            return Err(TimeError::InvalidSeconds(seconds));
175        }
176
177        let hours = (seconds / 3600) as u8;
178        let minutes = ((seconds % 3600) / 60) as u8;
179        let secs = (seconds % 60) as u8;
180
181        Time::from_hms(hours, minutes, secs)
182            .map_err(|_| TimeError::InvalidComponents(hours, minutes))
183    }
184
185    fn to_seconds(&self) -> i64 {
186        self.hour() as i64 * 3600 + self.minute() as i64 * 60 + self.second() as i64
187    }
188
189    fn align_to(&self, unit_seconds: u64) -> Result<Time, TimeError> {
190        if unit_seconds == 0 || unit_seconds >= 24 * 3600 {
191            return Err(TimeError::InvalidAlignmentUnit(unit_seconds));
192        }
193
194        let total_seconds = self.to_seconds();
195        let aligned_seconds = (total_seconds / unit_seconds as i64) * unit_seconds as i64;
196
197        Time::from_seconds(aligned_seconds).map_err(|_| TimeError::InvalidSeconds(aligned_seconds))
198    }
199
200    fn next_day(&self) -> Time {
201        // Since Time doesn't have day concept, we just return the same time
202        *self
203    }
204
205    fn next_hour(&self) -> Time {
206        let next_hour = (self.hour() + 1) % 24;
207        Time::from_hms(next_hour, self.minute(), self.second()).unwrap()
208    }
209
210    fn next_minute(&self) -> Time {
211        if self.minute() == 59 {
212            let next_hour = (self.hour() + 1) % 24;
213            Time::from_hms(next_hour, 0, self.second()).unwrap()
214        } else {
215            Time::from_hms(self.hour(), self.minute() + 1, self.second()).unwrap()
216        }
217    }
218
219    fn next_second(&self) -> Time {
220        if self.second() == 59 {
221            if self.minute() == 59 {
222                let next_hour = (self.hour() + 1) % 24;
223                Time::from_hms(next_hour, 0, 0).unwrap()
224            } else {
225                Time::from_hms(self.hour(), self.minute() + 1, 0).unwrap()
226            }
227        } else {
228            Time::from_hms(self.hour(), self.minute(), self.second() + 1).unwrap()
229        }
230    }
231
232    fn to_hour_seconds(&self) -> i64 {
233        self.hour() as i64 * 3600
234    }
235
236    fn to_minute_seconds(&self) -> i64 {
237        self.hour() as i64 * 3600 + self.minute() as i64 * 60
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use time::macros::time;
245
246    #[test]
247    fn test_shorten() {
248        let t = time!(22:01);
249        assert_eq!(t.to_shorten(), "22:01");
250
251        let t = time!(9:05);
252        assert_eq!(t.to_shorten(), "9:05");
253    }
254
255    #[test]
256    fn test_from_str() {
257        let t = <Time as ExtTime>::from_str("9:30").unwrap();
258        assert_eq!(t.hour(), 9);
259        assert_eq!(t.minute(), 30);
260
261        assert!(<Time as ExtTime>::from_str("25:00").is_err());
262        assert!(<Time as ExtTime>::from_str("invalid").is_err());
263    }
264
265    #[test]
266    fn test_sub_ext() {
267        let t1 = time!(23:00);
268        let t2 = time!(1:00);
269        assert_eq!(t1.sub_ext(t2), Duration::hours(22));
270        assert_eq!(t2.sub_ext(t1), Duration::hours(2));
271    }
272
273    #[test]
274    fn test_is_between() {
275        let t = time!(23:30);
276        assert!(t.is_between(time!(23:00), time!(0:00)));
277
278        let t = time!(0:30);
279        assert!(t.is_between(time!(23:00), time!(1:00)));
280
281        let t = time!(12:00);
282        assert!(!t.is_between(time!(23:00), time!(1:00)));
283    }
284
285    #[test]
286    fn test_add_minutes() {
287        let t = time!(23:30);
288        assert_eq!(t.add_minutes(40).to_shorten(), "0:10");
289
290        let t = time!(12:00);
291        assert_eq!(t.add_minutes(-30).to_shorten(), "11:30");
292    }
293
294    #[test]
295    fn test_from_seconds() {
296        let t = <Time as ExtTime>::from_seconds(37230).unwrap(); // 10:20:30
297        assert_eq!(t.hour(), 10);
298        assert_eq!(t.minute(), 20);
299        assert_eq!(t.second(), 30);
300
301        let t = <Time as ExtTime>::from_seconds(3660).unwrap(); // 1:01:00
302        assert_eq!(t.hour(), 1);
303        assert_eq!(t.minute(), 1);
304        assert_eq!(t.second(), 0);
305
306        assert!(<Time as ExtTime>::from_seconds(-1).is_err());
307        assert!(<Time as ExtTime>::from_seconds(24 * 3600).is_err());
308    }
309
310    #[test]
311    fn test_to_seconds() {
312        let t = time!(10:20:30);
313        assert_eq!(t.to_seconds(), 37230);
314
315        let t = time!(1:01:00);
316        assert_eq!(t.to_seconds(), 3660);
317
318        let t = time!(0:00:00);
319        assert_eq!(t.to_seconds(), 0);
320
321        let t = time!(23:59:59);
322        assert_eq!(t.to_seconds(), 86399);
323    }
324
325    #[test]
326    fn test_align_to() {
327        // Test alignment to 5 minutes
328        let t = time!(10:34:00);
329        let aligned = t.align_to(300).unwrap(); // 5 minutes = 300 seconds
330        assert_eq!(aligned.hour(), 10);
331        assert_eq!(aligned.minute(), 30);
332        assert_eq!(aligned.second(), 0);
333
334        // Test alignment to 5 seconds
335        let t = time!(00:00:03);
336        let aligned = t.align_to(5).unwrap();
337        assert_eq!(aligned.hour(), 0);
338        assert_eq!(aligned.minute(), 0);
339        assert_eq!(aligned.second(), 0);
340
341        // Test alignment to 1 hour
342        let t = time!(14:30:45);
343        let aligned = t.align_to(3600).unwrap();
344        assert_eq!(aligned.hour(), 14);
345        assert_eq!(aligned.minute(), 0);
346        assert_eq!(aligned.second(), 0);
347
348        // Test invalid unit
349        let t = time!(10:00:00);
350        assert!(t.align_to(0).is_err());
351        assert!(t.align_to(24 * 3600).is_err());
352    }
353
354    #[test]
355    fn test_next_hour() {
356        let t = time!(10:30:45);
357        let next = t.next_hour();
358        assert_eq!(next.hour(), 11);
359        assert_eq!(next.minute(), 30);
360        assert_eq!(next.second(), 45);
361
362        let t = time!(23:30:45);
363        let next = t.next_hour();
364        assert_eq!(next.hour(), 0);
365        assert_eq!(next.minute(), 30);
366        assert_eq!(next.second(), 45);
367    }
368
369    #[test]
370    fn test_next_minute() {
371        let t = time!(10:30:45);
372        let next = t.next_minute();
373        assert_eq!(next.hour(), 10);
374        assert_eq!(next.minute(), 31);
375        assert_eq!(next.second(), 45);
376
377        let t = time!(10:59:45);
378        let next = t.next_minute();
379        assert_eq!(next.hour(), 11);
380        assert_eq!(next.minute(), 0);
381        assert_eq!(next.second(), 45);
382
383        let t = time!(23:59:45);
384        let next = t.next_minute();
385        assert_eq!(next.hour(), 0);
386        assert_eq!(next.minute(), 0);
387        assert_eq!(next.second(), 45);
388    }
389
390    #[test]
391    fn test_next_second() {
392        let t = time!(10:30:45);
393        let next = t.next_second();
394        assert_eq!(next.hour(), 10);
395        assert_eq!(next.minute(), 30);
396        assert_eq!(next.second(), 46);
397
398        let t = time!(10:30:59);
399        let next = t.next_second();
400        assert_eq!(next.hour(), 10);
401        assert_eq!(next.minute(), 31);
402        assert_eq!(next.second(), 0);
403
404        let t = time!(10:59:59);
405        let next = t.next_second();
406        assert_eq!(next.hour(), 11);
407        assert_eq!(next.minute(), 0);
408        assert_eq!(next.second(), 0);
409
410        let t = time!(23:59:59);
411        let next = t.next_second();
412        assert_eq!(next.hour(), 0);
413        assert_eq!(next.minute(), 0);
414        assert_eq!(next.second(), 0);
415    }
416
417    #[test]
418    fn test_to_hour_seconds() {
419        let t = time!(10:20:30);
420        assert_eq!(t.to_hour_seconds(), 36000); // 10 * 3600
421
422        let t = time!(0:30:45);
423        assert_eq!(t.to_hour_seconds(), 0);
424
425        let t = time!(23:59:59);
426        assert_eq!(t.to_hour_seconds(), 82800); // 23 * 3600
427    }
428
429    #[test]
430    fn test_to_minute_seconds() {
431        let t = time!(10:20:30);
432        assert_eq!(t.to_minute_seconds(), 37200); // 10 * 3600 + 20 * 60
433
434        let t = time!(0:30:45);
435        assert_eq!(t.to_minute_seconds(), 1800); // 30 * 60
436
437        let t = time!(23:59:59);
438        assert_eq!(t.to_minute_seconds(), 86340); // 23 * 3600 + 59 * 60
439    }
440}