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