ext_time/
extend_offset_time.rs

1use thiserror::Error;
2use time::{
3    Date, Duration, Month, 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
127impl ExtOffsetDateTime for OffsetDateTime {
128    fn is_same_minute(&self, b: &OffsetDateTime) -> bool {
129        self.hour() == b.hour() && self.minute() == b.minute()
130    }
131
132    fn reset_minute(&self) -> OffsetDateTime {
133        let time = Time::from_hms(self.hour(), self.minute(), 0).expect("Invalid time components");
134        self.replace_time(time)
135    }
136
137    fn milli_timestamp(&self) -> i64 {
138        (self.unix_timestamp() as i64) * 1000 + self.millisecond() as i64
139    }
140
141    fn to_display_string(&self, offset_hours: i8) -> String {
142        let offset = UtcOffset::from_hms(offset_hours, 0, 0).expect("Invalid offset hours");
143        self.to_offset(offset)
144            .format(
145                &format_description::parse(
146                    "[year]-[month]-[day] [hour repr:24]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]"
147                )
148                .unwrap(),
149            )
150            .expect("Failed to format datetime")
151    }
152
153    fn to_chinese_string(&self) -> String {
154        let offset = UtcOffset::from_hms(8, 0, 0).expect("Invalid offset hours");
155        let format = format_description::parse(
156            "[year]年[month]月[day]日 [hour]时[minute]分[second]秒 [offset_hour sign:mandatory]:[offset_minute]",
157        )
158        .expect("parse");
159        self.to_offset(offset)
160            .format(&format)
161            .expect("Failed to format datetime")
162    }
163
164    fn from_milliseconds(
165        timestamp: u64,
166        offset_hours: i8,
167    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
168        let seconds = timestamp / 1000;
169        let millis = timestamp % 1000;
170        let offset = UtcOffset::from_hms(offset_hours, 0, 0)
171            .map_err(|_| OffsetDateTimeError::InvalidOffsetHours(offset_hours))?;
172
173        let dt = OffsetDateTime::from_unix_timestamp(seconds as i64)
174            .map_err(|_| OffsetDateTimeError::InvalidTimestamp(seconds as i64))?;
175
176        let dt = dt
177            .replace_millisecond(millis as u16)
178            .map_err(|_| OffsetDateTimeError::InvalidMilliseconds(millis as u16))?;
179
180        Ok(dt.to_offset(offset))
181    }
182
183    fn from_seconds(
184        timestamp: u64,
185        offset_hours: i8,
186    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
187        let offset = UtcOffset::from_hms(offset_hours, 0, 0)
188            .map_err(|_| OffsetDateTimeError::InvalidOffsetHours(offset_hours))?;
189
190        let dt = OffsetDateTime::from_unix_timestamp(timestamp as i64)
191            .map_err(|_| OffsetDateTimeError::InvalidTimestamp(timestamp as i64))?;
192
193        Ok(dt.to_offset(offset))
194    }
195
196    fn from_date_time(
197        date_str: &str,
198        time_str: &str,
199        milli: u64,
200        offset_hours: i8,
201    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
202        let format = fd!(
203            "[year][month][day] [hour]:[minute]:[second].[subsecond digits:3] [offset_hour \
204             sign:mandatory]:[offset_minute]:[offset_second]"
205        );
206        let dt = format!(
207            "{} {}.{:03} {:+03}:00:00",
208            date_str, time_str, milli, offset_hours
209        );
210        OffsetDateTime::parse(&dt, &format)
211            .map_err(|e| OffsetDateTimeError::ParseError(e.to_string()))
212    }
213
214    fn from_simple(dt: &str, offset_hours: i8) -> Result<OffsetDateTime, OffsetDateTimeError> {
215        let format = fd!("[year][month][day]_[hour][minute] [offset_hour sign:mandatory]");
216        let dt = format!("{} {:+03}", dt, offset_hours);
217        OffsetDateTime::parse(&dt, &format)
218            .map_err(|e| OffsetDateTimeError::ParseError(e.to_string()))
219    }
220
221    fn convert_to_dot_date(input: &str) -> Result<String, OffsetDateTimeError> {
222        let parse_format = fd!("[year][month][day]");
223        let date = time::Date::parse(input, &parse_format)
224            .map_err(|e| OffsetDateTimeError::ParseError(e.to_string()))?;
225
226        let output_format = fd!("[year].[month].[day]");
227        date.format(&output_format)
228            .map_err(|e| OffsetDateTimeError::FormatError(e.to_string()))
229    }
230
231    fn replace_time_with_seconds(
232        &self,
233        seconds: i64,
234    ) -> Result<OffsetDateTime, OffsetDateTimeError> {
235        if seconds < 0 || seconds >= 24 * 3600 {
236            return Err(OffsetDateTimeError::InvalidSeconds(seconds));
237        }
238
239        let hours = (seconds / 3600) as u8;
240        let minutes = ((seconds % 3600) / 60) as u8;
241        let secs = (seconds % 60) as u8;
242
243        let time = Time::from_hms(hours, minutes, secs)
244            .map_err(|_| OffsetDateTimeError::InvalidSeconds(seconds))?;
245
246        Ok(self.replace_time(time))
247    }
248
249    fn align_to(&self, interval: i64) -> Result<OffsetDateTime, OffsetDateTimeError> {
250        if interval == 0 {
251            return Err(OffsetDateTimeError::InvalidAlignmentUnit(
252                interval.abs() as u64
253            ));
254        }
255
256        let total_seconds =
257            self.hour() as i64 * 3600 + self.minute() as i64 * 60 + self.second() as i64;
258        let aligned_seconds = (total_seconds / interval) * interval;
259
260        let hours = (aligned_seconds / 3600) as u8;
261        let minutes = ((aligned_seconds % 3600) / 60) as u8;
262        let secs = (aligned_seconds % 60) as u8;
263
264        let time = Time::from_hms(hours, minutes, secs)
265            .map_err(|_| OffsetDateTimeError::InvalidSeconds(aligned_seconds))?;
266
267        Ok(self.replace_time(time))
268    }
269
270    fn next_day(&self) -> OffsetDateTime {
271        self.clone() + Duration::days(1)
272    }
273
274    fn next_hour(&self) -> OffsetDateTime {
275        self.clone() + Duration::hours(1)
276    }
277
278    fn next_minute(&self) -> OffsetDateTime {
279        self.clone() + Duration::minutes(1)
280    }
281
282    fn next_second(&self) -> OffsetDateTime {
283        self.clone() + Duration::seconds(1)
284    }
285
286    fn to_hour_seconds(&self) -> i64 {
287        self.hour() as i64 * 3600
288    }
289
290    fn to_minute_seconds(&self) -> i64 {
291        self.hour() as i64 * 3600 + self.minute() as i64 * 60
292    }
293}
294
295#[allow(dead_code)]
296pub trait ExtendOffsetTime {
297    fn start_of_day(&self) -> Self;
298    fn end_of_day(&self) -> Self;
299    fn start_of_week(&self) -> Self;
300    fn end_of_week(&self) -> Self;
301    fn start_of_month(&self) -> Self;
302    fn end_of_month(&self) -> Self;
303}
304
305impl ExtendOffsetTime for OffsetDateTime {
306    fn start_of_day(&self) -> Self {
307        self.replace_time(Time::from_hms(0, 0, 0).unwrap())
308    }
309
310    fn end_of_day(&self) -> Self {
311        self.replace_time(Time::from_hms(23, 59, 59).unwrap())
312    }
313
314    fn start_of_week(&self) -> Self {
315        let days_from_monday = self.weekday().number_days_from_monday();
316        self.start_of_day() - Duration::days(days_from_monday as i64)
317    }
318
319    fn end_of_week(&self) -> Self {
320        let days_to_sunday = 6 - self.weekday().number_days_from_monday();
321        self.end_of_day() + Duration::days(days_to_sunday as i64)
322    }
323
324    fn start_of_month(&self) -> Self {
325        let date = Date::from_calendar_date(self.year(), self.month(), 1).unwrap();
326        date.with_time(Time::from_hms(0, 0, 0).unwrap())
327            .assume_offset(self.offset())
328    }
329
330    fn end_of_month(&self) -> Self {
331        let days_in_month = match self.month() {
332            Month::January => 31,
333            Month::February => {
334                if self.year() % 4 == 0 {
335                    29
336                } else {
337                    28
338                }
339            }
340            Month::March => 31,
341            Month::April => 30,
342            Month::May => 31,
343            Month::June => 30,
344            Month::July => 31,
345            Month::August => 31,
346            Month::September => 30,
347            Month::October => 31,
348            Month::November => 30,
349            Month::December => 31,
350        };
351
352        let date = Date::from_calendar_date(self.year(), self.month(), days_in_month).unwrap();
353        date.with_time(Time::from_hms(23, 59, 59).unwrap())
354            .assume_offset(self.offset())
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use time::{PrimitiveDateTime, Weekday};
362
363    fn create_test_datetime() -> OffsetDateTime {
364        let offset = UtcOffset::from_hms(8, 0, 0).unwrap();
365        OffsetDateTime::now_utc()
366            .to_offset(offset)
367            .replace_date_time(PrimitiveDateTime::new(
368                Date::from_calendar_date(2024, time::Month::March, 15).unwrap(),
369                Time::from_hms(14, 30, 45).unwrap(),
370            ))
371    }
372
373    #[test]
374    fn test_start_of_day() {
375        let dt = create_test_datetime();
376        let start = dt.start_of_day();
377        assert_eq!(start.hour(), 0);
378        assert_eq!(start.minute(), 0);
379        assert_eq!(start.second(), 0);
380    }
381
382    #[test]
383    fn test_end_of_day() {
384        let dt = create_test_datetime();
385        let end = dt.end_of_day();
386        assert_eq!(end.hour(), 23);
387        assert_eq!(end.minute(), 59);
388        assert_eq!(end.second(), 59);
389    }
390
391    #[test]
392    fn test_start_of_week() {
393        let dt = create_test_datetime(); // Friday
394        let start = dt.start_of_week();
395        assert_eq!(start.weekday(), Weekday::Monday);
396    }
397
398    #[test]
399    fn test_end_of_week() {
400        let dt = create_test_datetime(); // Friday
401        let end = dt.end_of_week();
402        assert_eq!(end.weekday(), Weekday::Sunday);
403    }
404
405    #[test]
406    fn test_start_of_month() {
407        let dt = create_test_datetime();
408        let start = dt.start_of_month();
409        assert_eq!(start.day(), 1);
410        assert_eq!(start.hour(), 0);
411        assert_eq!(start.minute(), 0);
412        assert_eq!(start.second(), 0);
413    }
414
415    #[test]
416    fn test_end_of_month() {
417        let dt = create_test_datetime();
418        let end = dt.end_of_month();
419        assert_eq!(end.day(), 31); // March has 31 days
420        assert_eq!(end.hour(), 23);
421        assert_eq!(end.minute(), 59);
422        assert_eq!(end.second(), 59);
423    }
424
425    #[test]
426    fn test_to_display_string_with_offset() {
427        // Create a fixed time with UTC+8 offset
428        let time_with_offset = OffsetDateTime::now_utc()
429            .to_offset(UtcOffset::from_hms(8, 0, 0).unwrap())
430            .replace_date_time(PrimitiveDateTime::new(
431                Date::from_calendar_date(2024, time::Month::March, 15).unwrap(),
432                Time::from_hms(12, 0, 0).unwrap(),
433            ));
434
435        // Test UTC+8
436        let str_utc8 = time_with_offset.to_display_string(8);
437        assert_eq!(str_utc8, "2024-03-15 12:00:00+08:00");
438
439        // Test UTC+0
440        let str_utc = time_with_offset.to_display_string(0);
441        assert_eq!(str_utc, "2024-03-15 04:00:00+00:00");
442
443        // Test UTC-8
444        let str_utc_minus8 = time_with_offset.to_display_string(-8);
445        assert_eq!(str_utc_minus8, "2024-03-14 20:00:00-08:00");
446    }
447
448    #[test]
449    fn test_to_chinese_string() {
450        let time_with_offset = OffsetDateTime::now_utc()
451            .to_offset(UtcOffset::from_hms(8, 0, 0).unwrap())
452            .replace_date_time(PrimitiveDateTime::new(
453                Date::from_calendar_date(2024, time::Month::March, 15).unwrap(),
454                Time::from_hms(12, 0, 0).unwrap(),
455            ));
456
457        let chinese_str = time_with_offset.to_chinese_string();
458        assert_eq!(chinese_str, "2024年03月15日 12时00分00秒 +08:00");
459    }
460
461    #[test]
462    fn test_replace_time_with_seconds() {
463        let dt = create_test_datetime();
464
465        // Test replacing with 10:20:30
466        let new_dt = dt.replace_time_with_seconds(37230).unwrap();
467        assert_eq!(new_dt.hour(), 10);
468        assert_eq!(new_dt.minute(), 20);
469        assert_eq!(new_dt.second(), 30);
470
471        // Test replacing with 1:01:00
472        let new_dt = dt.replace_time_with_seconds(3660).unwrap();
473        assert_eq!(new_dt.hour(), 1);
474        assert_eq!(new_dt.minute(), 1);
475        assert_eq!(new_dt.second(), 0);
476
477        // Test invalid seconds
478        assert!(dt.replace_time_with_seconds(-1).is_err());
479        assert!(dt.replace_time_with_seconds(24 * 3600).is_err());
480    }
481
482    #[test]
483    fn test_align_to() {
484        let dt = create_test_datetime();
485
486        // Test alignment to 5 minutes
487        let aligned = dt.align_to(300).unwrap(); // 5 minutes = 300 seconds
488        assert_eq!(aligned.hour(), 14);
489        assert_eq!(aligned.minute(), 30);
490        assert_eq!(aligned.second(), 0);
491
492        // Test alignment to 5 seconds
493        let dt = dt.replace_time(Time::from_hms(14, 30, 3).unwrap());
494        let aligned = dt.align_to(5).unwrap();
495        assert_eq!(aligned.hour(), 14);
496        assert_eq!(aligned.minute(), 30);
497        assert_eq!(aligned.second(), 0);
498
499        // Test alignment to 1 hour
500        let aligned = dt.align_to(3600).unwrap();
501        assert_eq!(aligned.hour(), 14);
502        assert_eq!(aligned.minute(), 0);
503        assert_eq!(aligned.second(), 0);
504
505        // Test invalid unit
506        assert!(dt.align_to(0).is_err());
507        assert!(dt.align_to(24 * 3600).is_err());
508    }
509
510    #[test]
511    fn test_next_day() {
512        let dt = create_test_datetime();
513        let next = dt.next_day();
514        assert_eq!(next.day(), 16); // March 16
515        assert_eq!(next.hour(), 14);
516        assert_eq!(next.minute(), 30);
517        assert_eq!(next.second(), 45);
518    }
519
520    #[test]
521    fn test_next_hour() {
522        let dt = create_test_datetime();
523        let next = dt.next_hour();
524        assert_eq!(next.day(), 15);
525        assert_eq!(next.hour(), 15);
526        assert_eq!(next.minute(), 30);
527        assert_eq!(next.second(), 45);
528    }
529
530    #[test]
531    fn test_next_minute() {
532        let dt = create_test_datetime();
533        let next = dt.next_minute();
534        assert_eq!(next.day(), 15);
535        assert_eq!(next.hour(), 14);
536        assert_eq!(next.minute(), 31);
537        assert_eq!(next.second(), 45);
538    }
539
540    #[test]
541    fn test_next_second() {
542        let dt = create_test_datetime();
543        let next = dt.next_second();
544        assert_eq!(next.day(), 15);
545        assert_eq!(next.hour(), 14);
546        assert_eq!(next.minute(), 30);
547        assert_eq!(next.second(), 46);
548    }
549
550    #[test]
551    fn test_next_day_month_boundary() {
552        let dt = OffsetDateTime::now_utc()
553            .to_offset(UtcOffset::from_hms(8, 0, 0).unwrap())
554            .replace_date_time(PrimitiveDateTime::new(
555                Date::from_calendar_date(2024, time::Month::March, 31).unwrap(),
556                Time::from_hms(14, 30, 45).unwrap(),
557            ));
558
559        let next = dt.next_day();
560        assert_eq!(next.month(), time::Month::April);
561        assert_eq!(next.day(), 1);
562        assert_eq!(next.hour(), 14);
563        assert_eq!(next.minute(), 30);
564        assert_eq!(next.second(), 45);
565    }
566
567    #[test]
568    fn test_next_day_year_boundary() {
569        let dt = OffsetDateTime::now_utc()
570            .to_offset(UtcOffset::from_hms(8, 0, 0).unwrap())
571            .replace_date_time(PrimitiveDateTime::new(
572                Date::from_calendar_date(2024, time::Month::December, 31).unwrap(),
573                Time::from_hms(14, 30, 45).unwrap(),
574            ));
575
576        let next = dt.next_day();
577        assert_eq!(next.year(), 2025);
578        assert_eq!(next.month(), time::Month::January);
579        assert_eq!(next.day(), 1);
580        assert_eq!(next.hour(), 14);
581        assert_eq!(next.minute(), 30);
582        assert_eq!(next.second(), 45);
583    }
584
585    #[test]
586    fn test_to_hour_seconds() {
587        let dt = create_test_datetime();
588        assert_eq!(dt.to_hour_seconds(), 50400); // 14 * 3600
589
590        let dt = dt.replace_time(Time::from_hms(0, 30, 45).unwrap());
591        assert_eq!(dt.to_hour_seconds(), 0);
592
593        let dt = dt.replace_time(Time::from_hms(23, 59, 59).unwrap());
594        assert_eq!(dt.to_hour_seconds(), 82800); // 23 * 3600
595    }
596
597    #[test]
598    fn test_to_minute_seconds() {
599        let dt = create_test_datetime();
600        assert_eq!(dt.to_minute_seconds(), 52200); // 14 * 3600 + 30 * 60
601
602        let dt = dt.replace_time(Time::from_hms(0, 30, 45).unwrap());
603        assert_eq!(dt.to_minute_seconds(), 1800); // 30 * 60
604
605        let dt = dt.replace_time(Time::from_hms(23, 59, 59).unwrap());
606        assert_eq!(dt.to_minute_seconds(), 86340); // 23 * 3600 + 59 * 60
607    }
608}