1use std::cmp::Ordering;
2use std::fmt;
3use std::str::FromStr;
4
5const MILLIS_PER_SECOND: i64 = 1_000;
6const MILLIS_PER_MINUTE: i64 = 60 * MILLIS_PER_SECOND;
7const MILLIS_PER_HOUR: i64 = 60 * MILLIS_PER_MINUTE;
8const MILLIS_PER_DAY: i64 = 24 * MILLIS_PER_HOUR;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct CivilDate {
16 year: i32,
17 month: u8,
18 day: u8,
19}
20
21impl CivilDate {
22 pub const fn new(year: i32, month: u32, day: u32) -> Option<Self> {
23 if month < 1 || month > 12 || day < 1 || day > days_in_month(year, month) {
24 return None;
25 }
26 Some(Self {
27 year,
28 month: month as u8,
29 day: day as u8,
30 })
31 }
32
33 pub const fn year(self) -> i32 {
34 self.year
35 }
36
37 pub const fn month(self) -> u32 {
38 self.month as u32
39 }
40
41 pub const fn month0(self) -> u32 {
42 self.month() - 1
43 }
44
45 pub const fn day(self) -> u32 {
46 self.day as u32
47 }
48
49 pub const fn day0(self) -> u32 {
50 self.day() - 1
51 }
52
53 pub fn weekday(self) -> Weekday {
54 Weekday::from_monday_index((self.days_since_unix_epoch() + 3).rem_euclid(7) as u8)
55 }
56
57 pub fn ordinal(self) -> u32 {
58 let january_first = Self::new(self.year, 1, 1).expect("January 1 is always valid");
59 u32::try_from(self.days_since(january_first) + 1)
60 .expect("a Gregorian year contains at most 366 days")
61 }
62
63 pub fn iso_week(self) -> IsoWeek {
64 let year = i64::from(self.year);
65 let weekday = i64::from(self.weekday().number_from_monday());
66 let mut week = (i64::from(self.ordinal()) - weekday + 10).div_euclid(7);
67 let mut week_year = year;
68 if week < 1 {
69 week_year -= 1;
70 week = i64::from(iso_weeks_in_year(week_year));
71 } else if week > i64::from(iso_weeks_in_year(year)) {
72 week_year += 1;
73 week = 1;
74 }
75 IsoWeek {
76 year: week_year,
77 week: week as u32,
78 }
79 }
80
81 pub fn at_midnight(self) -> CivilDateTime {
82 CivilDateTime {
83 date: self,
84 hour: 0,
85 minute: 0,
86 second: 0,
87 millisecond: 0,
88 }
89 }
90
91 pub const fn at_hms(self, hour: u32, minute: u32, second: u32) -> Option<CivilDateTime> {
92 CivilDateTime::new(self, hour, minute, second, 0)
93 }
94
95 pub const fn at_hms_milli(
96 self,
97 hour: u32,
98 minute: u32,
99 second: u32,
100 millisecond: u32,
101 ) -> Option<CivilDateTime> {
102 CivilDateTime::new(self, hour, minute, second, millisecond)
103 }
104
105 pub fn checked_add_days(self, days: i64) -> Option<Self> {
106 civil_from_days(self.days_since_unix_epoch().checked_add(days)?)
107 }
108
109 pub fn checked_sub_days(self, days: i64) -> Option<Self> {
110 self.checked_add_days(days.checked_neg()?)
111 }
112
113 pub fn days_since(self, earlier: Self) -> i64 {
114 self.days_since_unix_epoch() - earlier.days_since_unix_epoch()
115 }
116
117 pub(crate) fn days_since_unix_epoch(self) -> i64 {
118 days_from_civil(self.year, self.month(), self.day())
119 }
120}
121
122impl fmt::Display for CivilDate {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self.year {
125 0..=9999 => write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day),
126 10000.. => write!(f, "+{}-{:02}-{:02}", self.year, self.month, self.day),
127 _ => write!(
128 f,
129 "-{:04}-{:02}-{:02}",
130 self.year.unsigned_abs(),
131 self.month,
132 self.day
133 ),
134 }
135 }
136}
137
138impl FromStr for CivilDate {
139 type Err = ParseCivilDateError;
140
141 fn from_str(value: &str) -> Result<Self, Self::Err> {
142 let (year_month, day) = value.rsplit_once('-').ok_or(ParseCivilDateError)?;
143 let (year, month) = year_month.rsplit_once('-').ok_or(ParseCivilDateError)?;
144 if month.len() != 2
145 || day.len() != 2
146 || !month.bytes().all(|byte| byte.is_ascii_digit())
147 || !day.bytes().all(|byte| byte.is_ascii_digit())
148 {
149 return Err(ParseCivilDateError);
150 }
151
152 let year_digits = year.strip_prefix(['+', '-']).unwrap_or(year);
153 let signed = year.starts_with(['+', '-']);
154 if year_digits.len() < 4
155 || (!signed && year_digits.len() != 4)
156 || !year_digits.bytes().all(|byte| byte.is_ascii_digit())
157 {
158 return Err(ParseCivilDateError);
159 }
160
161 let year = year.parse().map_err(|_| ParseCivilDateError)?;
162 let month = month.parse().map_err(|_| ParseCivilDateError)?;
163 let day = day.parse().map_err(|_| ParseCivilDateError)?;
164 let date = Self::new(year, month, day).ok_or(ParseCivilDateError)?;
165 if date.to_string() == value {
166 Ok(date)
167 } else {
168 Err(ParseCivilDateError)
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub struct ParseCivilDateError;
175
176impl fmt::Display for ParseCivilDateError {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str("expected a canonical civil date")
179 }
180}
181
182impl std::error::Error for ParseCivilDateError {}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
185pub struct CivilDateTime {
186 date: CivilDate,
187 hour: u8,
188 minute: u8,
189 second: u8,
190 millisecond: u16,
191}
192
193impl CivilDateTime {
194 pub const fn new(
195 date: CivilDate,
196 hour: u32,
197 minute: u32,
198 second: u32,
199 millisecond: u32,
200 ) -> Option<Self> {
201 if hour > 23 || minute > 59 || second > 59 || millisecond > 999 {
202 return None;
203 }
204 Some(Self {
205 date,
206 hour: hour as u8,
207 minute: minute as u8,
208 second: second as u8,
209 millisecond: millisecond as u16,
210 })
211 }
212
213 pub const fn date(self) -> CivilDate {
214 self.date
215 }
216
217 pub const fn year(self) -> i32 {
218 self.date.year()
219 }
220
221 pub const fn month(self) -> u32 {
222 self.date.month()
223 }
224
225 pub const fn month0(self) -> u32 {
226 self.date.month0()
227 }
228
229 pub const fn day(self) -> u32 {
230 self.date.day()
231 }
232
233 pub const fn day0(self) -> u32 {
234 self.date.day0()
235 }
236
237 pub fn weekday(self) -> Weekday {
238 self.date.weekday()
239 }
240
241 pub const fn hour(self) -> u32 {
242 self.hour as u32
243 }
244
245 pub const fn minute(self) -> u32 {
246 self.minute as u32
247 }
248
249 pub const fn second(self) -> u32 {
250 self.second as u32
251 }
252
253 pub const fn millisecond(self) -> u32 {
254 self.millisecond as u32
255 }
256
257 pub fn checked_add_millis(self, milliseconds: i64) -> Option<Self> {
258 Self::from_wide_naive_unix_millis(
259 self.wide_naive_unix_millis()
260 .checked_add(i128::from(milliseconds))?,
261 )
262 }
263
264 pub fn checked_add_days(self, days: i64) -> Option<Self> {
265 Some(Self {
266 date: self.date.checked_add_days(days)?,
267 ..self
268 })
269 }
270
271 pub fn checked_add_months(self, months: i64) -> Option<Self> {
272 let month_index = i64::from(self.year())
273 .checked_mul(12)?
274 .checked_add(i64::from(self.month0()))?
275 .checked_add(months)?;
276 let year = i32::try_from(month_index.div_euclid(12)).ok()?;
277 let month = u32::try_from(month_index.rem_euclid(12)).ok()? + 1;
278 let day = self.day().min(days_in_month(year, month));
279 Some(Self {
280 date: CivilDate::new(year, month, day)?,
281 ..self
282 })
283 }
284
285 pub fn checked_add_years(self, years: i64) -> Option<Self> {
286 let years = i32::try_from(years).ok()?;
287 let year = self.year().checked_add(years)?;
288 let day = self.day().min(days_in_month(year, self.month()));
289 Some(Self {
290 date: CivilDate::new(year, self.month(), day)?,
291 ..self
292 })
293 }
294
295 pub fn signed_duration_millis_since(self, earlier: Self) -> Option<i64> {
296 self.wide_naive_unix_millis()
297 .checked_sub(earlier.wide_naive_unix_millis())?
298 .try_into()
299 .ok()
300 }
301
302 #[cfg(feature = "system-timezone")]
303 pub(crate) fn naive_unix_millis(self) -> Option<i64> {
304 self.wide_naive_unix_millis().try_into().ok()
305 }
306
307 fn wide_naive_unix_millis(self) -> i128 {
308 i128::from(self.date.days_since_unix_epoch()) * i128::from(MILLIS_PER_DAY)
309 + i128::from(self.hour) * i128::from(MILLIS_PER_HOUR)
310 + i128::from(self.minute) * i128::from(MILLIS_PER_MINUTE)
311 + i128::from(self.second) * i128::from(MILLIS_PER_SECOND)
312 + i128::from(self.millisecond)
313 }
314
315 pub(crate) fn from_naive_unix_millis(milliseconds: i64) -> Option<Self> {
316 Self::from_wide_naive_unix_millis(i128::from(milliseconds))
317 }
318
319 fn from_wide_naive_unix_millis(milliseconds: i128) -> Option<Self> {
320 let day = milliseconds.div_euclid(i128::from(MILLIS_PER_DAY));
321 let within_day = milliseconds.rem_euclid(i128::from(MILLIS_PER_DAY));
322 let date = civil_from_days(day.try_into().ok()?)?;
323 let hour = within_day / i128::from(MILLIS_PER_HOUR);
324 let within_hour = within_day % i128::from(MILLIS_PER_HOUR);
325 let minute = within_hour / i128::from(MILLIS_PER_MINUTE);
326 let within_minute = within_hour % i128::from(MILLIS_PER_MINUTE);
327 let second = within_minute / i128::from(MILLIS_PER_SECOND);
328 let millisecond = within_minute % i128::from(MILLIS_PER_SECOND);
329 Self::new(
330 date,
331 hour.try_into().ok()?,
332 minute.try_into().ok()?,
333 second.try_into().ok()?,
334 millisecond.try_into().ok()?,
335 )
336 }
337}
338
339impl fmt::Display for CivilDateTime {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 write!(
342 f,
343 "{}T{:02}:{:02}:{:02}.{:03}",
344 self.date, self.hour, self.minute, self.second, self.millisecond
345 )
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
350pub struct UtcOffset {
351 seconds: i32,
352}
353
354impl UtcOffset {
355 pub const UTC: Self = Self { seconds: 0 };
356
357 pub const fn from_minutes(minutes: i32) -> Option<Self> {
358 if minutes < -1439 || minutes > 1439 {
359 return None;
360 }
361 Some(Self {
362 seconds: minutes * 60,
363 })
364 }
365
366 pub const fn from_seconds(seconds: i32) -> Option<Self> {
367 if seconds <= -86_400 || seconds >= 86_400 {
368 return None;
369 }
370 Some(Self { seconds })
371 }
372
373 pub const fn seconds(self) -> i32 {
374 self.seconds
375 }
376
377 pub const fn minutes(self) -> i32 {
378 self.seconds / 60
379 }
380}
381
382impl fmt::Display for UtcOffset {
383 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384 let sign = if self.seconds < 0 { '-' } else { '+' };
385 let absolute = self.seconds.unsigned_abs();
386 write!(
387 f,
388 "{sign}{:02}:{:02}",
389 absolute / 3600,
390 (absolute % 3600) / 60
391 )
392 }
393}
394
395#[derive(Debug, Clone, Copy)]
396pub struct OffsetDateTime {
397 unix_millis: i64,
398 offset: UtcOffset,
399}
400
401impl OffsetDateTime {
402 pub const fn from_unix_millis(unix_millis: i64, offset: UtcOffset) -> Self {
403 Self {
404 unix_millis,
405 offset,
406 }
407 }
408
409 pub fn from_local(local: CivilDateTime, offset: UtcOffset) -> Option<Self> {
410 let unix_millis = local
411 .wide_naive_unix_millis()
412 .checked_sub(i128::from(offset.seconds) * i128::from(MILLIS_PER_SECOND))?
413 .try_into()
414 .ok()?;
415 Some(Self {
416 unix_millis,
417 offset,
418 })
419 }
420
421 pub const fn timestamp_millis(self) -> i64 {
422 self.unix_millis
423 }
424
425 pub const fn timestamp_seconds(self) -> i64 {
426 self.unix_millis.div_euclid(MILLIS_PER_SECOND)
427 }
428
429 pub const fn offset(self) -> UtcOffset {
430 self.offset
431 }
432
433 pub fn date(self) -> CivilDate {
434 self.local_datetime().date()
435 }
436
437 pub fn year(self) -> i32 {
438 self.local_datetime().year()
439 }
440
441 pub fn month(self) -> u32 {
442 self.local_datetime().month()
443 }
444
445 pub fn month0(self) -> u32 {
446 self.local_datetime().month0()
447 }
448
449 pub fn day(self) -> u32 {
450 self.local_datetime().day()
451 }
452
453 pub fn day0(self) -> u32 {
454 self.local_datetime().day0()
455 }
456
457 pub fn ordinal(self) -> u32 {
458 self.date().ordinal()
459 }
460
461 pub fn iso_week(self) -> IsoWeek {
462 self.date().iso_week()
463 }
464
465 pub fn weekday(self) -> Weekday {
466 self.date().weekday()
467 }
468
469 pub fn hour(self) -> u32 {
470 self.local_datetime().hour()
471 }
472
473 pub fn minute(self) -> u32 {
474 self.local_datetime().minute()
475 }
476
477 pub fn second(self) -> u32 {
478 self.local_datetime().second()
479 }
480
481 pub fn millisecond(self) -> u32 {
482 self.local_datetime().millisecond()
483 }
484
485 pub fn local_datetime(self) -> CivilDateTime {
486 let local_millis = i128::from(self.unix_millis)
487 + i128::from(self.offset.seconds) * i128::from(MILLIS_PER_SECOND);
488 CivilDateTime::from_wide_naive_unix_millis(local_millis)
489 .expect("an i64 millisecond instant plus a UTC offset fits an i32 civil year")
490 }
491
492 pub fn utc_datetime(self) -> CivilDateTime {
493 CivilDateTime::from_naive_unix_millis(self.unix_millis)
494 .expect("an i64 millisecond instant fits an i32 civil year")
495 }
496
497 pub const fn to_offset(self, offset: UtcOffset) -> Self {
498 Self { offset, ..self }
499 }
500
501 pub fn checked_add_millis(self, milliseconds: i64) -> Option<Self> {
502 Some(Self {
503 unix_millis: self.unix_millis.checked_add(milliseconds)?,
504 ..self
505 })
506 }
507}
508
509impl PartialEq for OffsetDateTime {
510 fn eq(&self, other: &Self) -> bool {
511 self.unix_millis == other.unix_millis
512 }
513}
514
515impl Eq for OffsetDateTime {}
516
517impl PartialOrd for OffsetDateTime {
518 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
519 Some(self.cmp(other))
520 }
521}
522
523impl Ord for OffsetDateTime {
524 fn cmp(&self, other: &Self) -> Ordering {
525 self.unix_millis.cmp(&other.unix_millis)
526 }
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
530pub enum Weekday {
531 Monday,
532 Tuesday,
533 Wednesday,
534 Thursday,
535 Friday,
536 Saturday,
537 Sunday,
538}
539
540impl Weekday {
541 const fn from_monday_index(index: u8) -> Self {
542 match index {
543 0 => Self::Monday,
544 1 => Self::Tuesday,
545 2 => Self::Wednesday,
546 3 => Self::Thursday,
547 4 => Self::Friday,
548 5 => Self::Saturday,
549 _ => Self::Sunday,
550 }
551 }
552
553 pub const fn number_from_monday(self) -> u32 {
554 match self {
555 Self::Monday => 1,
556 Self::Tuesday => 2,
557 Self::Wednesday => 3,
558 Self::Thursday => 4,
559 Self::Friday => 5,
560 Self::Saturday => 6,
561 Self::Sunday => 7,
562 }
563 }
564
565 pub const fn number_from_sunday(self) -> u32 {
566 match self {
567 Self::Sunday => 1,
568 Self::Monday => 2,
569 Self::Tuesday => 3,
570 Self::Wednesday => 4,
571 Self::Thursday => 5,
572 Self::Friday => 6,
573 Self::Saturday => 7,
574 }
575 }
576
577 pub const fn full_name(self) -> &'static str {
578 match self {
579 Self::Monday => "Monday",
580 Self::Tuesday => "Tuesday",
581 Self::Wednesday => "Wednesday",
582 Self::Thursday => "Thursday",
583 Self::Friday => "Friday",
584 Self::Saturday => "Saturday",
585 Self::Sunday => "Sunday",
586 }
587 }
588
589 pub const fn short_name(self) -> &'static str {
590 match self {
591 Self::Monday => "Mon",
592 Self::Tuesday => "Tue",
593 Self::Wednesday => "Wed",
594 Self::Thursday => "Thu",
595 Self::Friday => "Fri",
596 Self::Saturday => "Sat",
597 Self::Sunday => "Sun",
598 }
599 }
600}
601
602#[derive(Debug, Clone, Copy, PartialEq, Eq)]
603pub struct IsoWeek {
604 year: i64,
605 week: u32,
606}
607
608impl IsoWeek {
609 pub const fn year(self) -> i64 {
610 self.year
611 }
612
613 pub const fn week(self) -> u32 {
614 self.week
615 }
616}
617
618const fn is_leap_year_i64(year: i64) -> bool {
619 year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
620}
621
622pub const fn is_leap_year(year: i32) -> bool {
623 is_leap_year_i64(year as i64)
624}
625
626pub const fn days_in_month(year: i32, month: u32) -> u32 {
627 match month {
628 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
629 4 | 6 | 9 | 11 => 30,
630 2 if is_leap_year(year) => 29,
631 2 => 28,
632 _ => 0,
633 }
634}
635
636fn iso_weeks_in_year(year: i64) -> u32 {
637 let january_first = weekday_for_wide_year(year, 1, 1);
638 if january_first == Weekday::Thursday
639 || (january_first == Weekday::Wednesday && is_leap_year_i64(year))
640 {
641 53
642 } else {
643 52
644 }
645}
646
647fn weekday_for_wide_year(year: i64, month: u32, day: u32) -> Weekday {
648 Weekday::from_monday_index((days_from_civil_wide(year, month, day) + 3).rem_euclid(7) as u8)
649}
650
651fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
652 days_from_civil_wide(i64::from(year), month, day)
653}
654
655fn days_from_civil_wide(mut year: i64, month: u32, day: u32) -> i64 {
656 if month <= 2 {
657 year -= 1;
658 }
659 let era = year.div_euclid(400);
660 let year_of_era = year - era * 400;
661 let month_prime = i64::from(month) + if month > 2 { -3 } else { 9 };
662 let day_of_year = (153 * month_prime + 2) / 5 + i64::from(day) - 1;
663 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
664 era * 146_097 + day_of_era - 719_468
665}
666
667fn civil_from_days(days: i64) -> Option<CivilDate> {
668 let days = days.checked_add(719_468)?;
669 let era = days.div_euclid(146_097);
670 let day_of_era = days - era * 146_097;
671 let year_of_era =
672 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
673 let mut year = year_of_era + era * 400;
674 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
675 let month_prime = (5 * day_of_year + 2) / 153;
676 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
677 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
678 if month <= 2 {
679 year += 1;
680 }
681 CivilDate::new(
682 year.try_into().ok()?,
683 month.try_into().ok()?,
684 day.try_into().ok()?,
685 )
686}