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_to(&self, interval: i64) -> 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;
117
118 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(); 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(); 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); 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 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 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 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 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 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 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 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 let aligned = dt.align_to(300).unwrap(); assert_eq!(aligned.hour(), 14);
489 assert_eq!(aligned.minute(), 30);
490 assert_eq!(aligned.second(), 0);
491
492 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 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 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); 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); 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); }
596
597 #[test]
598 fn test_to_minute_seconds() {
599 let dt = create_test_datetime();
600 assert_eq!(dt.to_minute_seconds(), 52200); let dt = dt.replace_time(Time::from_hms(0, 30, 45).unwrap());
603 assert_eq!(dt.to_minute_seconds(), 1800); let dt = dt.replace_time(Time::from_hms(23, 59, 59).unwrap());
606 assert_eq!(dt.to_minute_seconds(), 86340); }
608}