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