ext_time/
extend_offset_time.rs

1use thiserror::Error;
2use time::{
3    Duration, OffsetDateTime, Time, UtcOffset,
4    format_description::{self},
5    macros::format_description as fd,
6};
7
8#[derive(Error, Debug)]
9pub enum OffsetDateTimeError {
10    #[error("Invalid offset hours: {0}")]
11    InvalidOffsetHours(i8),
12    #[error("Invalid timestamp: {0}")]
13    InvalidTimestamp(i64),
14    #[error("Invalid milliseconds: {0}")]
15    InvalidMilliseconds(u16),
16    #[error("Failed to parse datetime: {0}")]
17    ParseError(String),
18    #[error("Failed to format datetime: {0}")]
19    FormatError(String),
20    #[error("Invalid seconds value: {0}")]
21    InvalidSeconds(i64),
22    #[error("Invalid alignment unit: {0}")]
23    InvalidAlignmentUnit(u64),
24    #[error("Failed to add time: {0:?}")]
25    AddTimeError(OffsetDateTime),
26}
27
28pub trait ExtOffsetDateTime {
29    /// Check if two timestamps are in the same minute
30    fn is_same_minute(&self, b: &OffsetDateTime) -> bool;
31
32    /// Reset seconds and subseconds to zero
33    fn reset_minute(&self) -> OffsetDateTime;
34
35    /// Get timestamp in milliseconds
36    fn milli_timestamp(&self) -> i64;
37
38    /// Format datetime to display string with timezone
39    fn to_display_string(&self, offset_hours: i8) -> String;
40
41    /// Format datetime to Chinese style string with timezone
42    fn to_chinese_string(&self) -> String;
43
44    /// Parse timestamp in milliseconds with timezone offset (hours from UTC)
45    fn from_milliseconds(
46        timestamp: u64,
47        offset_hours: i8,
48    ) -> Result<OffsetDateTime, OffsetDateTimeError>;
49
50    /// Parse timestamp in seconds with timezone offset (hours from UTC)
51    fn from_seconds(
52        timestamp: u64,
53        offset_hours: i8,
54    ) -> Result<OffsetDateTime, OffsetDateTimeError>;
55
56    /// Parse datetime from date string, time string and milliseconds with timezone
57    fn from_date_time(
58        date: &str,
59        time: &str,
60        milli: u64,
61        offset_hours: i8,
62    ) -> Result<OffsetDateTime, OffsetDateTimeError>;
63
64    /// Parse datetime from simple format string (YYYYMMDD_HHMM) with timezone
65    fn from_simple(dt: &str, offset_hours: i8) -> Result<OffsetDateTime, OffsetDateTimeError>;
66
67    /// Convert date format from YYYYMMDD to YYYY.MM.DD
68    fn convert_to_dot_date(input: &str) -> Result<String, OffsetDateTimeError>;
69
70    /// Get current time with specified timezone offset (hours from UTC)
71    fn now_with_offset(offset_hours: i8) -> OffsetDateTime {
72        OffsetDateTime::now_utc().to_offset(UtcOffset::from_hms(offset_hours, 0, 0).unwrap())
73    }
74
75    /// Replace time part with seconds (hours + minutes + seconds)
76    ///
77    /// # Arguments
78    /// * `seconds` - Total seconds (hours * 3600 + minutes * 60 + seconds)
79    ///
80    /// # Returns
81    /// * `Ok(OffsetDateTime)` - DateTime with new time part
82    /// * `Err` - If seconds value is invalid
83    fn replace_time_with_seconds(
84        &self,
85        seconds: i64,
86    ) -> Result<OffsetDateTime, OffsetDateTimeError>;
87
88    /// Align time to the nearest interval
89    ///
90    /// # Arguments
91    /// * `interval` - Interval in seconds (can be negative for backward alignment)
92    ///
93    /// # Returns
94    /// * `Ok(OffsetDateTime)` - Aligned time
95    /// * `Err(Error)` - If interval is 0
96    fn align_to(&self, interval: i64) -> Result<OffsetDateTime, OffsetDateTimeError>;
97
98    /// Get next day at the same time
99    fn next_day(&self) -> OffsetDateTime;
100
101    /// Get next hour at the same minute and second
102    fn next_hour(&self) -> OffsetDateTime;
103
104    /// Get next minute at the same second
105    fn next_minute(&self) -> OffsetDateTime;
106
107    /// Get next second
108    fn next_second(&self) -> OffsetDateTime;
109
110    /// Convert time part to seconds, ignoring minutes and seconds
111    ///
112    /// # Returns
113    /// Total seconds of hours (hours * 3600)
114    ///
115    /// Note: Returns i64 to support time differences and negative values
116    fn to_hour_seconds(&self) -> i64;
117
118    /// Convert time part to seconds, ignoring seconds
119    ///
120    /// # Returns
121    /// Total seconds of hours and minutes (hours * 3600 + minutes * 60)
122    ///
123    /// Note: Returns i64 to support time differences and negative values
124    fn to_minute_seconds(&self) -> i64;
125
126    /// Calculate duration from current time to specified target time
127    ///
128    /// # Arguments
129    /// * `target_hour` - Target hour (0-23)
130    /// * `target_minute` - Target minute (0-59)
131    /// * `target_second` - Target second (0-59)
132    ///
133    /// # Returns
134    /// Duration from current time to target time, handling cross-day scenarios
135    ///
136    /// # Example
137    /// ```
138    /// use ext_time::ExtOffsetDateTime;
139    /// use time::OffsetDateTime;
140    ///
141    /// let now = OffsetDateTime::now_utc();
142    /// let duration = now.duration_to_time(20, 0, 0); // Duration to 20:00:00
143    /// ```
144    fn duration_to_time(&self, target_hour: u8, target_minute: u8, target_second: u8) -> Duration;
145}
146
147impl ExtOffsetDateTime for OffsetDateTime {
148    fn is_same_minute(&self, b: &OffsetDateTime) -> bool {
149        self.hour() == b.hour() && self.minute() == b.minute()
150    }
151
152    fn reset_minute(&self) -> OffsetDateTime {
153        let time = Time::from_hms(self.hour(), self.minute(), 0).expect("Invalid time components");
154        self.replace_time(time)
155    }
156
157    fn milli_timestamp(&self) -> i64 {
158        (self.unix_timestamp() as i64) * 1000 + self.millisecond() as i64
159    }
160
161    fn to_display_string(&self, offset_hours: i8) -> String {
162        let offset = UtcOffset::from_hms(offset_hours, 0, 0).expect("Invalid offset hours");
163        self.to_offset(offset)
164            .format(
165                &format_description::parse(
166                    "[year]-[month]-[day] [hour repr:24]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]"
167                )
168                .unwrap(),
169            )
170            .expect("Failed to format datetime")
171    }
172
173    fn to_chinese_string(&self) -> String {
174        let offset = UtcOffset::from_hms(8, 0, 0).expect("Invalid offset hours");
175        let format = format_description::parse(
176            "[year]年[month]月[day]日 [hour]时[minute]分[second]秒 [offset_hour sign:mandatory]:[offset_minute]",
177        )
178        .expect("parse");
179        self.to_offset(offset)
180            .format(&format)
181            .expect("Failed to format datetime")
182    }
183
184    fn from_milliseconds(
185        timestamp: u64,
186        offset_hours: i8,
187    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
188        let seconds = timestamp / 1000;
189        let millis = timestamp % 1000;
190        let offset = UtcOffset::from_hms(offset_hours, 0, 0)
191            .map_err(|_| OffsetDateTimeError::InvalidOffsetHours(offset_hours))?;
192
193        let dt = OffsetDateTime::from_unix_timestamp(seconds as i64)
194            .map_err(|_| OffsetDateTimeError::InvalidTimestamp(seconds as i64))?;
195
196        let dt = dt
197            .replace_millisecond(millis as u16)
198            .map_err(|_| OffsetDateTimeError::InvalidMilliseconds(millis as u16))?;
199
200        Ok(dt.to_offset(offset))
201    }
202
203    fn from_seconds(
204        timestamp: u64,
205        offset_hours: i8,
206    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
207        let offset = UtcOffset::from_hms(offset_hours, 0, 0)
208            .map_err(|_| OffsetDateTimeError::InvalidOffsetHours(offset_hours))?;
209
210        let dt = OffsetDateTime::from_unix_timestamp(timestamp as i64)
211            .map_err(|_| OffsetDateTimeError::InvalidTimestamp(timestamp as i64))?;
212
213        Ok(dt.to_offset(offset))
214    }
215
216    fn from_date_time(
217        date_str: &str,
218        time_str: &str,
219        milli: u64,
220        offset_hours: i8,
221    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
222        let format = fd!(
223            "[year][month][day] [hour]:[minute]:[second].[subsecond digits:3] [offset_hour \
224             sign:mandatory]:[offset_minute]:[offset_second]"
225        );
226        let dt = format!(
227            "{} {}.{:03} {:+03}:00:00",
228            date_str, time_str, milli, offset_hours
229        );
230        OffsetDateTime::parse(&dt, &format)
231            .map_err(|e| OffsetDateTimeError::ParseError(e.to_string()))
232    }
233
234    fn from_simple(dt: &str, offset_hours: i8) -> Result<OffsetDateTime, OffsetDateTimeError> {
235        let format = fd!("[year][month][day]_[hour][minute] [offset_hour sign:mandatory]");
236        let dt = format!("{} {:+03}", dt, offset_hours);
237        OffsetDateTime::parse(&dt, &format)
238            .map_err(|e| OffsetDateTimeError::ParseError(e.to_string()))
239    }
240
241    fn convert_to_dot_date(input: &str) -> Result<String, OffsetDateTimeError> {
242        let parse_format = fd!("[year][month][day]");
243        let date = time::Date::parse(input, &parse_format)
244            .map_err(|e| OffsetDateTimeError::ParseError(e.to_string()))?;
245
246        let output_format = fd!("[year].[month].[day]");
247        date.format(&output_format)
248            .map_err(|e| OffsetDateTimeError::FormatError(e.to_string()))
249    }
250
251    fn replace_time_with_seconds(
252        &self,
253        seconds: i64,
254    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
255        if seconds < 0 || seconds >= 24 * 3600 {
256            return Err(OffsetDateTimeError::InvalidSeconds(seconds));
257        }
258
259        let hours = (seconds / 3600) as u8;
260        let minutes = ((seconds % 3600) / 60) as u8;
261        let secs = (seconds % 60) as u8;
262
263        let time = Time::from_hms(hours, minutes, secs)
264            .map_err(|_| OffsetDateTimeError::InvalidSeconds(seconds))?;
265
266        Ok(self.replace_time(time))
267    }
268
269    fn align_to(&self, interval: i64) -> Result<OffsetDateTime, OffsetDateTimeError> {
270        if interval == 0 {
271            return Err(OffsetDateTimeError::InvalidAlignmentUnit(
272                interval.abs() as u64
273            ));
274        }
275
276        let total_seconds =
277            self.hour() as i64 * 3600 + self.minute() as i64 * 60 + self.second() as i64;
278        let aligned_seconds = (total_seconds / interval) * interval;
279
280        let hours = (aligned_seconds / 3600) as u8;
281        let minutes = ((aligned_seconds % 3600) / 60) as u8;
282        let secs = (aligned_seconds % 60) as u8;
283
284        let time = Time::from_hms(hours, minutes, secs)
285            .map_err(|_| OffsetDateTimeError::InvalidSeconds(aligned_seconds))?;
286
287        Ok(self.replace_time(time))
288    }
289
290    fn next_day(&self) -> OffsetDateTime {
291        self.clone() + Duration::days(1)
292    }
293
294    fn next_hour(&self) -> OffsetDateTime {
295        self.clone() + Duration::hours(1)
296    }
297
298    fn next_minute(&self) -> OffsetDateTime {
299        self.clone() + Duration::minutes(1)
300    }
301
302    fn next_second(&self) -> OffsetDateTime {
303        self.clone() + Duration::seconds(1)
304    }
305
306    fn to_hour_seconds(&self) -> i64 {
307        self.hour() as i64 * 3600
308    }
309
310    fn to_minute_seconds(&self) -> i64 {
311        self.hour() as i64 * 3600 + self.minute() as i64 * 60
312    }
313
314    fn duration_to_time(&self, target_hour: u8, target_minute: u8, target_second: u8) -> Duration {
315        // Create target time in the same date and timezone as current time
316        let target_time = Time::from_hms(target_hour, target_minute, target_second)
317            .expect("Invalid target time components");
318
319        // Create target datetime for today
320        let target_today = self.replace_time(target_time);
321
322        // Calculate duration to target time today
323        let duration_to_today = target_today - *self;
324
325        if duration_to_today.is_positive() || duration_to_today.is_zero() {
326            // Target time is later today
327            duration_to_today
328        } else {
329            // Target time is tomorrow (cross-day scenario)
330            let target_tomorrow = target_today + Duration::days(1);
331            target_tomorrow - *self
332        }
333    }
334}