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 fn is_same_minute(&self, b: &OffsetDateTime) -> bool;
31
32 fn reset_minute(&self) -> OffsetDateTime;
34
35 fn milli_timestamp(&self) -> i64;
37
38 fn to_display_string(&self, offset_hours: i8) -> String;
40
41 fn to_chinese_string(&self) -> String;
43
44 fn from_milliseconds(
46 timestamp: u64,
47 offset_hours: i8,
48 ) -> Result<OffsetDateTime, OffsetDateTimeError>;
49
50 fn from_seconds(
52 timestamp: u64,
53 offset_hours: i8,
54 ) -> Result<OffsetDateTime, OffsetDateTimeError>;
55
56 fn from_date_time(
58 date: &str,
59 time: &str,
60 milli: u64,
61 offset_hours: i8,
62 ) -> Result<OffsetDateTime, OffsetDateTimeError>;
63
64 fn from_simple(dt: &str, offset_hours: i8) -> Result<OffsetDateTime, OffsetDateTimeError>;
66
67 fn convert_to_dot_date(input: &str) -> Result<String, OffsetDateTimeError>;
69
70 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 fn replace_time_with_seconds(
84 &self,
85 seconds: i64,
86 ) -> Result<OffsetDateTime, OffsetDateTimeError>;
87
88 fn align_time_to(&self, unit_seconds: u64) -> Result<OffsetDateTime, OffsetDateTimeError>;
97
98 fn next_day(&self) -> OffsetDateTime;
100
101 fn next_hour(&self) -> OffsetDateTime;
103
104 fn next_minute(&self) -> OffsetDateTime;
106
107 fn next_second(&self) -> OffsetDateTime;
109
110 fn to_hour_seconds(&self) -> i64;
115
116 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(); 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(); 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); 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 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 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 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 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 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 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 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 let aligned = dt.align_time_to(300).unwrap(); assert_eq!(aligned.hour(), 14);
483 assert_eq!(aligned.minute(), 30);
484 assert_eq!(aligned.second(), 0);
485
486 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 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 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); 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); 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); }
590
591 #[test]
592 fn test_to_minute_seconds() {
593 let dt = create_test_datetime();
594 assert_eq!(dt.to_minute_seconds(), 52200); let dt = dt.replace_time(Time::from_hms(0, 30, 45).unwrap());
597 assert_eq!(dt.to_minute_seconds(), 1800); let dt = dt.replace_time(Time::from_hms(23, 59, 59).unwrap());
600 assert_eq!(dt.to_minute_seconds(), 86340); }
602}