1//! The [`DateTime`] struct and its associated `impl`s.
2
3// TODO(jhpratt) Document everything before making public.
4#![allow(clippy::missing_docs_in_private_items)]
5// This is intentional, as the struct will likely be exposed at some point.
6#![allow(unreachable_pub)]
7
8use core::cmp::Ordering;
9use core::fmt;
10use core::hash::{Hash, Hasher};
11use core::mem::size_of;
12use core::ops::{Add, AddAssign, Sub, SubAssign};
13use core::time::Duration as StdDuration;
14#[cfg(feature = "formatting")]
15use std::io;
16#[cfg(feature = "std")]
17use std::time::SystemTime;
18
19use crate::convert::*;
20use crate::date::{MAX_YEAR, MIN_YEAR};
21#[cfg(feature = "formatting")]
22use crate::formatting::Formattable;
23#[cfg(feature = "parsing")]
24use crate::parsing::{Parsable, Parsed};
25use crate::{error, util, Date, Duration, Month, Time, UtcOffset, Weekday};
26
27#[allow(missing_debug_implementations, missing_copy_implementations)]
28pub(crate) mod offset_kind {
29 pub enum None {}
30 pub enum Fixed {}
31}
32
33pub(crate) use sealed::MaybeOffset;
34use sealed::*;
35mod sealed {
36 use super::*;
37
38 /// A type that is guaranteed to be either `()` or [`UtcOffset`].
39 ///
40 /// **Do not** add any additional implementations of this trait.
41 #[allow(unreachable_pub)] // intentional
42 pub trait MaybeOffsetType {}
43 impl MaybeOffsetType for () {}
44 impl MaybeOffsetType for UtcOffset {}
45
46 pub trait MaybeOffset: Sized {
47 /// The offset type as it is stored in memory.
48 #[cfg(feature = "quickcheck")]
49 type MemoryOffsetType: Copy + MaybeOffsetType + quickcheck::Arbitrary;
50 #[cfg(not(feature = "quickcheck"))]
51 type MemoryOffsetType: Copy + MaybeOffsetType;
52
53 /// The offset type as it should be thought about.
54 ///
55 /// For example, a `DateTime<Utc>` has a logical offset type of [`UtcOffset`], but does not
56 /// actually store an offset in memory.
57 type LogicalOffsetType: Copy + MaybeOffsetType;
58
59 /// Required to be `Self`. Used for bound equality.
60 type Self_;
61
62 /// True if and only if `Self::LogicalOffsetType` is `UtcOffset`.
63 const HAS_LOGICAL_OFFSET: bool =
64 size_of::<Self::LogicalOffsetType>() == size_of::<UtcOffset>();
65 /// True if and only if `Self::MemoryOffsetType` is `UtcOffset`.
66 const HAS_MEMORY_OFFSET: bool =
67 size_of::<Self::MemoryOffsetType>() == size_of::<UtcOffset>();
68
69 /// `Some` if and only if the logical UTC offset is statically known.
70 // TODO(jhpratt) When const trait impls are stable, this can be removed in favor of
71 // `.as_offset_opt()`.
72 const STATIC_OFFSET: Option<UtcOffset>;
73
74 #[cfg(feature = "parsing")]
75 fn try_from_parsed(parsed: Parsed) -> Result<Self::MemoryOffsetType, error::TryFromParsed>;
76 }
77
78 // Traits to indicate whether a `MaybeOffset` has a logical offset type of `UtcOffset` or not.
79
80 pub trait HasLogicalOffset: MaybeOffset<LogicalOffsetType = UtcOffset> {}
81 impl<T: MaybeOffset<LogicalOffsetType = UtcOffset>> HasLogicalOffset for T {}
82
83 pub trait NoLogicalOffset: MaybeOffset<LogicalOffsetType = ()> {}
84 impl<T: MaybeOffset<LogicalOffsetType = ()>> NoLogicalOffset for T {}
85
86 // Traits to indicate whether a `MaybeOffset` has a memory offset type of `UtcOffset` or not.
87
88 pub trait HasMemoryOffset: MaybeOffset<MemoryOffsetType = UtcOffset> {}
89 impl<T: MaybeOffset<MemoryOffsetType = UtcOffset>> HasMemoryOffset for T {}
90
91 pub trait NoMemoryOffset: MaybeOffset<MemoryOffsetType = ()> {}
92 impl<T: MaybeOffset<MemoryOffsetType = ()>> NoMemoryOffset for T {}
93
94 // Traits to indicate backing type being implemented.
95
96 pub trait IsOffsetKindNone:
97 MaybeOffset<Self_ = offset_kind::None, MemoryOffsetType = (), LogicalOffsetType = ()>
98 {
99 }
100 impl IsOffsetKindNone for offset_kind::None {}
101
102 pub trait IsOffsetKindFixed:
103 MaybeOffset<
104 Self_ = offset_kind::Fixed,
105 MemoryOffsetType = UtcOffset,
106 LogicalOffsetType = UtcOffset,
107 >
108 {
109 }
110 impl IsOffsetKindFixed for offset_kind::Fixed {}
111}
112
113impl MaybeOffset for offset_kind::None {
114 type MemoryOffsetType = ();
115 type LogicalOffsetType = ();
116
117 type Self_ = Self;
118
119 const STATIC_OFFSET: Option<UtcOffset> = None;
120
121 #[cfg(feature = "parsing")]
122 fn try_from_parsed(_: Parsed) -> Result<(), error::TryFromParsed> {
123 Ok(())
124 }
125}
126
127impl MaybeOffset for offset_kind::Fixed {
128 type MemoryOffsetType = UtcOffset;
129 type LogicalOffsetType = UtcOffset;
130
131 type Self_ = Self;
132
133 const STATIC_OFFSET: Option<UtcOffset> = None;
134
135 #[cfg(feature = "parsing")]
136 fn try_from_parsed(parsed: Parsed) -> Result<UtcOffset, error::TryFromParsed> {
137 parsed.try_into()
138 }
139}
140
141// region: const trait method hacks
142// TODO(jhpratt) When const trait impls are stable, these methods can be removed in favor of methods
143// in `MaybeOffset`, which would then be made `const`.
144const fn maybe_offset_as_offset_opt<O: MaybeOffset>(
145 offset: O::MemoryOffsetType,
146) -> Option<UtcOffset> {
147 if O::STATIC_OFFSET.is_some() {
148 O::STATIC_OFFSET
149 } else if O::HAS_MEMORY_OFFSET {
150 #[repr(C)] // needed to guarantee they align at the start
151 union Convert<O: MaybeOffset> {
152 input: O::MemoryOffsetType,
153 output: UtcOffset,
154 }
155
156 // Safety: `O::HAS_OFFSET` indicates that `O::Offset` is `UtcOffset`. This code effectively
157 // performs a transmute from `O::Offset` to `UtcOffset`, which we know is the same type.
158 Some(unsafe { Convert::<O> { input: offset }.output })
159 } else {
160 None
161 }
162}
163
164const fn maybe_offset_as_offset<O: MaybeOffset + HasLogicalOffset>(
165 offset: O::MemoryOffsetType,
166) -> UtcOffset {
167 match maybe_offset_as_offset_opt::<O>(offset) {
168 Some(offset: UtcOffset) => offset,
169 None => bug!("`MaybeOffset::as_offset` called on a type without an offset in memory"),
170 }
171}
172
173pub(crate) const fn maybe_offset_from_offset<O: MaybeOffset>(
174 offset: UtcOffset,
175) -> O::MemoryOffsetType {
176 #[repr(C)] // needed to guarantee the types align at the start
177 union Convert<O: MaybeOffset> {
178 input: UtcOffset,
179 output: O::MemoryOffsetType,
180 }
181
182 // Safety: It is statically known that there are only two possibilities due to the trait bound
183 // of `O::MemoryOffsetType`, which ultimately relies on `MaybeOffsetType`. The two possibilities
184 // are:
185 // 1. UtcOffset -> UtcOffset
186 // 2. UtcOffset -> ()
187 // (1) is valid because it is an identity conversion, which is always valid. (2) is valid
188 // because `()` is a 1-ZST, so converting to it is always valid.
189 unsafe { Convert::<O> { input: offset }.output }
190}
191// endregion const trait methods hacks
192
193/// The Julian day of the Unix epoch.
194const UNIX_EPOCH_JULIAN_DAY: i32 = Date::__from_ordinal_date_unchecked(year:1970, ordinal:1).to_julian_day();
195
196pub struct DateTime<O: MaybeOffset> {
197 pub(crate) date: Date,
198 pub(crate) time: Time,
199 pub(crate) offset: O::MemoryOffsetType,
200}
201
202// Manual impl to remove extraneous bounds.
203impl<O: MaybeOffset> Clone for DateTime<O> {
204 fn clone(&self) -> Self {
205 *self
206 }
207}
208
209// Manual impl to remove extraneous bounds.
210impl<O: MaybeOffset> Copy for DateTime<O> {}
211
212// region: constructors
213impl DateTime<offset_kind::None> {
214 pub const MIN: Self = Self {
215 date: Date::MIN,
216 time: Time::MIN,
217 offset: (),
218 };
219
220 pub const MAX: Self = Self {
221 date: Date::MAX,
222 time: Time::MAX,
223 offset: (),
224 };
225}
226
227impl DateTime<offset_kind::Fixed> {
228 pub const UNIX_EPOCH: Self = Self {
229 date: Date::__from_ordinal_date_unchecked(year:1970, ordinal:1),
230 time: Time::MIDNIGHT,
231 offset: UtcOffset::UTC,
232 };
233}
234
235impl<O: MaybeOffset> DateTime<O> {
236 pub const fn new(date: Date, time: Time) -> Self
237 where
238 O: IsOffsetKindNone,
239 {
240 Self {
241 date,
242 time,
243 offset: (),
244 }
245 }
246
247 pub const fn from_unix_timestamp(timestamp: i64) -> Result<Self, error::ComponentRange>
248 where
249 O: HasLogicalOffset,
250 {
251 #[allow(clippy::missing_docs_in_private_items)]
252 const MIN_TIMESTAMP: i64 = Date::MIN.midnight().assume_utc().unix_timestamp();
253 #[allow(clippy::missing_docs_in_private_items)]
254 const MAX_TIMESTAMP: i64 = Date::MAX
255 .with_time(Time::__from_hms_nanos_unchecked(23, 59, 59, 999_999_999))
256 .assume_utc()
257 .unix_timestamp();
258
259 ensure_value_in_range!(timestamp in MIN_TIMESTAMP => MAX_TIMESTAMP);
260
261 // Use the unchecked method here, as the input validity has already been verified.
262 let date = Date::from_julian_day_unchecked(
263 UNIX_EPOCH_JULIAN_DAY + div_floor!(timestamp, Second.per(Day) as i64) as i32,
264 );
265
266 let seconds_within_day = timestamp.rem_euclid(Second.per(Day) as _);
267 let time = Time::__from_hms_nanos_unchecked(
268 (seconds_within_day / Second.per(Hour) as i64) as _,
269 ((seconds_within_day % Second.per(Hour) as i64) / Minute.per(Hour) as i64) as _,
270 (seconds_within_day % Second.per(Minute) as i64) as _,
271 0,
272 );
273
274 Ok(Self {
275 date,
276 time,
277 offset: maybe_offset_from_offset::<O>(UtcOffset::UTC),
278 })
279 }
280
281 pub const fn from_unix_timestamp_nanos(timestamp: i128) -> Result<Self, error::ComponentRange>
282 where
283 O: HasLogicalOffset,
284 {
285 let datetime = const_try!(Self::from_unix_timestamp(div_floor!(
286 timestamp,
287 Nanosecond.per(Second) as i128
288 ) as i64));
289
290 Ok(Self {
291 date: datetime.date,
292 time: Time::__from_hms_nanos_unchecked(
293 datetime.hour(),
294 datetime.minute(),
295 datetime.second(),
296 timestamp.rem_euclid(Nanosecond.per(Second) as _) as u32,
297 ),
298 offset: maybe_offset_from_offset::<O>(UtcOffset::UTC),
299 })
300 }
301 // endregion constructors
302
303 // region: now
304 // The return type will likely be loosened once `ZonedDateTime` is implemented. This is not a
305 // breaking change calls are currently limited to only `OffsetDateTime`.
306 #[cfg(feature = "std")]
307 pub fn now_utc() -> DateTime<offset_kind::Fixed>
308 where
309 O: IsOffsetKindFixed,
310 {
311 #[cfg(all(
312 target_family = "wasm",
313 not(any(target_os = "emscripten", target_os = "wasi")),
314 feature = "wasm-bindgen"
315 ))]
316 {
317 js_sys::Date::new_0().into()
318 }
319
320 #[cfg(not(all(
321 target_family = "wasm",
322 not(any(target_os = "emscripten", target_os = "wasi")),
323 feature = "wasm-bindgen"
324 )))]
325 SystemTime::now().into()
326 }
327
328 // The return type will likely be loosened once `ZonedDateTime` is implemented. This is not a
329 // breaking change calls are currently limited to only `OffsetDateTime`.
330 #[cfg(feature = "local-offset")]
331 pub fn now_local() -> Result<DateTime<offset_kind::Fixed>, error::IndeterminateOffset>
332 where
333 O: IsOffsetKindFixed,
334 {
335 let t = DateTime::<offset_kind::Fixed>::now_utc();
336 Ok(t.to_offset(UtcOffset::local_offset_at(crate::OffsetDateTime(t))?))
337 }
338 // endregion now
339
340 // region: getters
341 // region: component getters
342 pub const fn date(self) -> Date {
343 self.date
344 }
345
346 pub const fn time(self) -> Time {
347 self.time
348 }
349
350 pub const fn offset(self) -> UtcOffset
351 where
352 O: HasLogicalOffset,
353 {
354 maybe_offset_as_offset::<O>(self.offset)
355 }
356 // endregion component getters
357
358 // region: date getters
359 pub const fn year(self) -> i32 {
360 self.date.year()
361 }
362
363 pub const fn month(self) -> Month {
364 self.date.month()
365 }
366
367 pub const fn day(self) -> u8 {
368 self.date.day()
369 }
370
371 pub const fn ordinal(self) -> u16 {
372 self.date.ordinal()
373 }
374
375 pub const fn iso_week(self) -> u8 {
376 self.date.iso_week()
377 }
378
379 pub const fn sunday_based_week(self) -> u8 {
380 self.date.sunday_based_week()
381 }
382
383 pub const fn monday_based_week(self) -> u8 {
384 self.date.monday_based_week()
385 }
386
387 pub const fn to_calendar_date(self) -> (i32, Month, u8) {
388 self.date.to_calendar_date()
389 }
390
391 pub const fn to_ordinal_date(self) -> (i32, u16) {
392 self.date.to_ordinal_date()
393 }
394
395 pub const fn to_iso_week_date(self) -> (i32, u8, Weekday) {
396 self.date.to_iso_week_date()
397 }
398
399 pub const fn weekday(self) -> Weekday {
400 self.date.weekday()
401 }
402
403 pub const fn to_julian_day(self) -> i32 {
404 self.date.to_julian_day()
405 }
406 // endregion date getters
407
408 // region: time getters
409 pub const fn as_hms(self) -> (u8, u8, u8) {
410 self.time.as_hms()
411 }
412
413 pub const fn as_hms_milli(self) -> (u8, u8, u8, u16) {
414 self.time.as_hms_milli()
415 }
416
417 pub const fn as_hms_micro(self) -> (u8, u8, u8, u32) {
418 self.time.as_hms_micro()
419 }
420
421 pub const fn as_hms_nano(self) -> (u8, u8, u8, u32) {
422 self.time.as_hms_nano()
423 }
424
425 pub const fn hour(self) -> u8 {
426 self.time.hour()
427 }
428
429 pub const fn minute(self) -> u8 {
430 self.time.minute()
431 }
432
433 pub const fn second(self) -> u8 {
434 self.time.second()
435 }
436
437 pub const fn millisecond(self) -> u16 {
438 self.time.millisecond()
439 }
440
441 pub const fn microsecond(self) -> u32 {
442 self.time.microsecond()
443 }
444
445 pub const fn nanosecond(self) -> u32 {
446 self.time.nanosecond()
447 }
448 // endregion time getters
449
450 // region: unix timestamp getters
451 pub const fn unix_timestamp(self) -> i64
452 where
453 O: HasLogicalOffset,
454 {
455 let offset = maybe_offset_as_offset::<O>(self.offset).whole_seconds() as i64;
456
457 let days =
458 (self.to_julian_day() as i64 - UNIX_EPOCH_JULIAN_DAY as i64) * Second.per(Day) as i64;
459 let hours = self.hour() as i64 * Second.per(Hour) as i64;
460 let minutes = self.minute() as i64 * Second.per(Minute) as i64;
461 let seconds = self.second() as i64;
462 days + hours + minutes + seconds - offset
463 }
464
465 pub const fn unix_timestamp_nanos(self) -> i128
466 where
467 O: HasLogicalOffset,
468 {
469 self.unix_timestamp() as i128 * Nanosecond.per(Second) as i128 + self.nanosecond() as i128
470 }
471 // endregion unix timestamp getters
472 // endregion: getters
473
474 // region: attach offset
475 pub const fn assume_offset(self, offset: UtcOffset) -> DateTime<offset_kind::Fixed>
476 where
477 O: NoLogicalOffset,
478 {
479 DateTime {
480 date: self.date,
481 time: self.time,
482 offset,
483 }
484 }
485
486 pub const fn assume_utc(self) -> DateTime<offset_kind::Fixed>
487 where
488 O: NoLogicalOffset,
489 {
490 self.assume_offset(UtcOffset::UTC)
491 }
492 // endregion attach offset
493
494 // region: to offset
495 pub const fn to_offset(self, offset: UtcOffset) -> DateTime<offset_kind::Fixed>
496 where
497 O: HasLogicalOffset,
498 {
499 expect_opt!(
500 self.checked_to_offset(offset),
501 "local datetime out of valid range"
502 )
503 }
504
505 pub const fn checked_to_offset(self, offset: UtcOffset) -> Option<DateTime<offset_kind::Fixed>>
506 where
507 O: HasLogicalOffset,
508 {
509 let self_offset = maybe_offset_as_offset::<O>(self.offset);
510
511 if self_offset.whole_hours() == offset.whole_hours()
512 && self_offset.minutes_past_hour() == offset.minutes_past_hour()
513 && self_offset.seconds_past_minute() == offset.seconds_past_minute()
514 {
515 return Some(DateTime {
516 date: self.date,
517 time: self.time,
518 offset,
519 });
520 }
521
522 let (year, ordinal, time) = self.to_offset_raw(offset);
523
524 if year > MAX_YEAR || year < MIN_YEAR {
525 return None;
526 }
527
528 Some(DateTime {
529 date: Date::__from_ordinal_date_unchecked(year, ordinal),
530 time,
531 offset,
532 })
533 }
534
535 /// Equivalent to `.to_offset(UtcOffset::UTC)`, but returning the year, ordinal, and time. This
536 /// avoids constructing an invalid [`Date`] if the new value is out of range.
537 pub(crate) const fn to_offset_raw(self, offset: UtcOffset) -> (i32, u16, Time) {
538 let Some(from) = maybe_offset_as_offset_opt::<O>(self.offset) else {
539 // No adjustment is needed because there is no offset.
540 return (self.year(), self.ordinal(), self.time);
541 };
542 let to = offset;
543
544 // Fast path for when no conversion is necessary.
545 if from.whole_hours() == to.whole_hours()
546 && from.minutes_past_hour() == to.minutes_past_hour()
547 && from.seconds_past_minute() == to.seconds_past_minute()
548 {
549 return (self.year(), self.ordinal(), self.time());
550 }
551
552 let mut second = self.second() as i16 - from.seconds_past_minute() as i16
553 + to.seconds_past_minute() as i16;
554 let mut minute =
555 self.minute() as i16 - from.minutes_past_hour() as i16 + to.minutes_past_hour() as i16;
556 let mut hour = self.hour() as i8 - from.whole_hours() + to.whole_hours();
557 let (mut year, ordinal) = self.to_ordinal_date();
558 let mut ordinal = ordinal as i16;
559
560 // Cascade the values twice. This is needed because the values are adjusted twice above.
561 cascade!(second in 0..Second.per(Minute) as i16 => minute);
562 cascade!(second in 0..Second.per(Minute) as i16 => minute);
563 cascade!(minute in 0..Minute.per(Hour) as i16 => hour);
564 cascade!(minute in 0..Minute.per(Hour) as i16 => hour);
565 cascade!(hour in 0..Hour.per(Day) as i8 => ordinal);
566 cascade!(hour in 0..Hour.per(Day) as i8 => ordinal);
567 cascade!(ordinal => year);
568
569 debug_assert!(ordinal > 0);
570 debug_assert!(ordinal <= crate::util::days_in_year(year) as i16);
571
572 (
573 year,
574 ordinal as _,
575 Time::__from_hms_nanos_unchecked(
576 hour as _,
577 minute as _,
578 second as _,
579 self.nanosecond(),
580 ),
581 )
582 }
583 // endregion to offset
584
585 // region: checked arithmetic
586 pub const fn checked_add(self, duration: Duration) -> Option<Self> {
587 let (date_adjustment, time) = self.time.adjusting_add(duration);
588 let date = const_try_opt!(self.date.checked_add(duration));
589
590 Some(Self {
591 date: match date_adjustment {
592 util::DateAdjustment::Previous => const_try_opt!(date.previous_day()),
593 util::DateAdjustment::Next => const_try_opt!(date.next_day()),
594 util::DateAdjustment::None => date,
595 },
596 time,
597 offset: self.offset,
598 })
599 }
600
601 pub const fn checked_sub(self, duration: Duration) -> Option<Self> {
602 let (date_adjustment, time) = self.time.adjusting_sub(duration);
603 let date = const_try_opt!(self.date.checked_sub(duration));
604
605 Some(Self {
606 date: match date_adjustment {
607 util::DateAdjustment::Previous => const_try_opt!(date.previous_day()),
608 util::DateAdjustment::Next => const_try_opt!(date.next_day()),
609 util::DateAdjustment::None => date,
610 },
611 time,
612 offset: self.offset,
613 })
614 }
615 // endregion checked arithmetic
616
617 // region: saturating arithmetic
618 pub const fn saturating_add(self, duration: Duration) -> Self {
619 if let Some(datetime) = self.checked_add(duration) {
620 datetime
621 } else if duration.is_negative() {
622 Self {
623 date: Date::MIN,
624 time: Time::MIN,
625 offset: self.offset,
626 }
627 } else {
628 Self {
629 date: Date::MAX,
630 time: Time::MAX,
631 offset: self.offset,
632 }
633 }
634 }
635
636 pub const fn saturating_sub(self, duration: Duration) -> Self {
637 if let Some(datetime) = self.checked_sub(duration) {
638 datetime
639 } else if duration.is_negative() {
640 Self {
641 date: Date::MAX,
642 time: Time::MAX,
643 offset: self.offset,
644 }
645 } else {
646 Self {
647 date: Date::MIN,
648 time: Time::MIN,
649 offset: self.offset,
650 }
651 }
652 }
653 // endregion saturating arithmetic
654
655 // region: replacement
656 pub const fn replace_time(self, time: Time) -> Self {
657 Self {
658 date: self.date,
659 time,
660 offset: self.offset,
661 }
662 }
663
664 pub const fn replace_date(self, date: Date) -> Self {
665 Self {
666 date,
667 time: self.time,
668 offset: self.offset,
669 }
670 }
671
672 pub const fn replace_date_time(self, date_time: DateTime<offset_kind::None>) -> Self
673 where
674 O: HasLogicalOffset,
675 {
676 Self {
677 date: date_time.date,
678 time: date_time.time,
679 offset: self.offset,
680 }
681 }
682
683 pub const fn replace_year(self, year: i32) -> Result<Self, error::ComponentRange> {
684 Ok(Self {
685 date: const_try!(self.date.replace_year(year)),
686 time: self.time,
687 offset: self.offset,
688 })
689 }
690
691 pub const fn replace_month(self, month: Month) -> Result<Self, error::ComponentRange> {
692 Ok(Self {
693 date: const_try!(self.date.replace_month(month)),
694 time: self.time,
695 offset: self.offset,
696 })
697 }
698
699 pub const fn replace_day(self, day: u8) -> Result<Self, error::ComponentRange> {
700 Ok(Self {
701 date: const_try!(self.date.replace_day(day)),
702 time: self.time,
703 offset: self.offset,
704 })
705 }
706
707 pub const fn replace_hour(self, hour: u8) -> Result<Self, error::ComponentRange> {
708 Ok(Self {
709 date: self.date,
710 time: const_try!(self.time.replace_hour(hour)),
711 offset: self.offset,
712 })
713 }
714
715 pub const fn replace_minute(self, minute: u8) -> Result<Self, error::ComponentRange> {
716 Ok(Self {
717 date: self.date,
718 time: const_try!(self.time.replace_minute(minute)),
719 offset: self.offset,
720 })
721 }
722
723 pub const fn replace_second(self, second: u8) -> Result<Self, error::ComponentRange> {
724 Ok(Self {
725 date: self.date,
726 time: const_try!(self.time.replace_second(second)),
727 offset: self.offset,
728 })
729 }
730
731 pub const fn replace_millisecond(
732 self,
733 millisecond: u16,
734 ) -> Result<Self, error::ComponentRange> {
735 Ok(Self {
736 date: self.date,
737 time: const_try!(self.time.replace_millisecond(millisecond)),
738 offset: self.offset,
739 })
740 }
741
742 pub const fn replace_microsecond(
743 self,
744 microsecond: u32,
745 ) -> Result<Self, error::ComponentRange> {
746 Ok(Self {
747 date: self.date,
748 time: const_try!(self.time.replace_microsecond(microsecond)),
749 offset: self.offset,
750 })
751 }
752
753 pub const fn replace_nanosecond(self, nanosecond: u32) -> Result<Self, error::ComponentRange> {
754 Ok(Self {
755 date: self.date,
756 time: const_try!(self.time.replace_nanosecond(nanosecond)),
757 offset: self.offset,
758 })
759 }
760
761 // Don't gate this on just having an offset, as `ZonedDateTime` cannot be set to an arbitrary
762 // offset.
763 pub const fn replace_offset(self, offset: UtcOffset) -> DateTime<offset_kind::Fixed>
764 where
765 O: IsOffsetKindFixed,
766 {
767 DateTime {
768 date: self.date,
769 time: self.time,
770 offset,
771 }
772 }
773
774 // endregion replacement
775
776 // region: formatting & parsing
777 #[cfg(feature = "formatting")]
778 pub fn format_into(
779 self,
780 output: &mut impl io::Write,
781 format: &(impl Formattable + ?Sized),
782 ) -> Result<usize, error::Format> {
783 format.format_into(
784 output,
785 Some(self.date),
786 Some(self.time),
787 maybe_offset_as_offset_opt::<O>(self.offset),
788 )
789 }
790
791 #[cfg(feature = "formatting")]
792 pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
793 format.format(
794 Some(self.date),
795 Some(self.time),
796 maybe_offset_as_offset_opt::<O>(self.offset),
797 )
798 }
799
800 #[cfg(feature = "parsing")]
801 pub fn parse(
802 input: &str,
803 description: &(impl Parsable + ?Sized),
804 ) -> Result<Self, error::Parse> {
805 description.parse_date_time(input.as_bytes())
806 }
807
808 /// A helper method to check if the `OffsetDateTime` is a valid representation of a leap second.
809 /// Leap seconds, when parsed, are represented as the preceding nanosecond. However, leap
810 /// seconds can only occur as the last second of a month UTC.
811 #[cfg(feature = "parsing")]
812 pub(crate) const fn is_valid_leap_second_stand_in(self) -> bool {
813 // Leap seconds aren't allowed if there is no offset.
814 if !O::HAS_LOGICAL_OFFSET {
815 return false;
816 }
817
818 // This comparison doesn't need to be adjusted for the stored offset, so check it first for
819 // speed.
820 if self.nanosecond() != 999_999_999 {
821 return false;
822 }
823
824 let (year, ordinal, time) = self.to_offset_raw(UtcOffset::UTC);
825 let Ok(date) = Date::from_ordinal_date(year, ordinal) else { return false };
826
827 time.hour() == 23
828 && time.minute() == 59
829 && time.second() == 59
830 && date.day() == util::days_in_year_month(year, date.month())
831 }
832
833 // endregion formatting & parsing
834
835 // region: deprecated time getters
836
837 // All the way at the bottom as it's low priority. These methods only exist for when
838 // `OffsetDateTime` is made an alias of `DateTime<Fixed>`. Consider hiding these methods from
839 // documentation in the future.
840
841 #[doc(hidden)]
842 #[allow(dead_code)] // while functionally private
843 #[deprecated(since = "0.3.18", note = "use `as_hms` instead")]
844 pub const fn to_hms(self) -> (u8, u8, u8)
845 where
846 O: IsOffsetKindFixed,
847 {
848 self.time.as_hms()
849 }
850
851 #[doc(hidden)]
852 #[allow(dead_code)] // while functionally private
853 #[deprecated(since = "0.3.18", note = "use `as_hms_milli` instead")]
854 pub const fn to_hms_milli(self) -> (u8, u8, u8, u16)
855 where
856 O: IsOffsetKindFixed,
857 {
858 self.time.as_hms_milli()
859 }
860
861 #[doc(hidden)]
862 #[allow(dead_code)] // while functionally private
863 #[deprecated(since = "0.3.18", note = "use `as_hms_micro` instead")]
864 pub const fn to_hms_micro(self) -> (u8, u8, u8, u32)
865 where
866 O: IsOffsetKindFixed,
867 {
868 self.time.as_hms_micro()
869 }
870
871 #[doc(hidden)]
872 #[allow(dead_code)] // while functionally private
873 #[deprecated(since = "0.3.18", note = "use `as_hms_nano` instead")]
874 pub const fn to_hms_nano(self) -> (u8, u8, u8, u32)
875 where
876 O: IsOffsetKindFixed,
877 {
878 self.time.as_hms_nano()
879 }
880 // endregion deprecated time getters
881}
882
883impl<O: MaybeOffset> fmt::Debug for DateTime<O> {
884 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885 <Self as fmt::Display>::fmt(self, f)
886 }
887}
888
889impl<O: MaybeOffset> fmt::Display for DateTime<O> {
890 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
891 write!(f, "{} {}", self.date, self.time)?;
892 if let Some(offset: UtcOffset) = maybe_offset_as_offset_opt::<O>(self.offset) {
893 write!(f, " {offset}")?;
894 }
895 Ok(())
896 }
897}
898
899// region: trait impls
900impl<O: MaybeOffset> PartialEq for DateTime<O> {
901 fn eq(&self, rhs: &Self) -> bool {
902 if O::HAS_LOGICAL_OFFSET {
903 self.to_offset_raw(offset:UtcOffset::UTC) == rhs.to_offset_raw(offset:UtcOffset::UTC)
904 } else {
905 (self.date, self.time) == (rhs.date, rhs.time)
906 }
907 }
908}
909
910impl<O: MaybeOffset> Eq for DateTime<O> {}
911
912impl<O: MaybeOffset> PartialOrd for DateTime<O> {
913 fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
914 Some(self.cmp(rhs))
915 }
916}
917
918impl<O: MaybeOffset> Ord for DateTime<O> {
919 fn cmp(&self, rhs: &Self) -> Ordering {
920 if O::HAS_LOGICAL_OFFSET {
921 self.to_offset_raw(offset:UtcOffset::UTC)
922 .cmp(&rhs.to_offset_raw(offset:UtcOffset::UTC))
923 } else {
924 (self.date, self.time).cmp(&(rhs.date, rhs.time))
925 }
926 }
927}
928
929impl<O: MaybeOffset> Hash for DateTime<O> {
930 fn hash<H: Hasher>(&self, hasher: &mut H) {
931 if O::HAS_LOGICAL_OFFSET {
932 self.to_offset_raw(UtcOffset::UTC).hash(state:hasher);
933 } else {
934 (self.date, self.time).hash(state:hasher);
935 }
936 }
937}
938
939impl<O: MaybeOffset> Add<Duration> for DateTime<O> {
940 type Output = Self;
941
942 fn add(self, duration: Duration) -> Self {
943 self.checked_add(duration)
944 .expect(msg:"resulting value is out of range")
945 }
946}
947
948impl<O: MaybeOffset> Add<StdDuration> for DateTime<O> {
949 type Output = Self;
950
951 fn add(self, duration: StdDuration) -> Self::Output {
952 let (is_next_day: bool, time: Time) = self.time.adjusting_add_std(duration);
953
954 Self {
955 date: if is_next_day {
956 (self.date + duration)
957 .next_day()
958 .expect(msg:"resulting value is out of range")
959 } else {
960 self.date + duration
961 },
962 time,
963 offset: self.offset,
964 }
965 }
966}
967
968impl<O: MaybeOffset> AddAssign<Duration> for DateTime<O> {
969 fn add_assign(&mut self, rhs: Duration) {
970 *self = *self + rhs;
971 }
972}
973
974impl<O: MaybeOffset> AddAssign<StdDuration> for DateTime<O> {
975 fn add_assign(&mut self, rhs: StdDuration) {
976 *self = *self + rhs;
977 }
978}
979
980impl<O: MaybeOffset> Sub<Duration> for DateTime<O> {
981 type Output = Self;
982
983 fn sub(self, duration: Duration) -> Self {
984 self.checked_sub(duration)
985 .expect(msg:"resulting value is out of range")
986 }
987}
988
989impl<O: MaybeOffset> Sub<StdDuration> for DateTime<O> {
990 type Output = Self;
991
992 fn sub(self, duration: StdDuration) -> Self::Output {
993 let (is_previous_day: bool, time: Time) = self.time.adjusting_sub_std(duration);
994
995 Self {
996 date: if is_previous_day {
997 (self.date - duration)
998 .previous_day()
999 .expect(msg:"resulting value is out of range")
1000 } else {
1001 self.date - duration
1002 },
1003 time,
1004 offset: self.offset,
1005 }
1006 }
1007}
1008
1009impl<O: MaybeOffset> SubAssign<Duration> for DateTime<O> {
1010 fn sub_assign(&mut self, rhs: Duration) {
1011 *self = *self - rhs;
1012 }
1013}
1014
1015impl<O: MaybeOffset> SubAssign<StdDuration> for DateTime<O> {
1016 fn sub_assign(&mut self, rhs: StdDuration) {
1017 *self = *self - rhs;
1018 }
1019}
1020
1021impl<O: MaybeOffset> Sub<Self> for DateTime<O> {
1022 type Output = Duration;
1023
1024 fn sub(self, rhs: Self) -> Self::Output {
1025 let base = (self.date - rhs.date) + (self.time - rhs.time);
1026
1027 match (
1028 maybe_offset_as_offset_opt::<O>(self.offset),
1029 maybe_offset_as_offset_opt::<O>(rhs.offset),
1030 ) {
1031 (Some(self_offset), Some(rhs_offset)) => {
1032 let adjustment = Duration::seconds(
1033 (self_offset.whole_seconds() - rhs_offset.whole_seconds()) as i64,
1034 );
1035 base - adjustment
1036 }
1037 (left, right) => {
1038 debug_assert!(
1039 left.is_none() && right.is_none(),
1040 "offset type should not be different for the same type"
1041 );
1042 base
1043 }
1044 }
1045 }
1046}
1047
1048#[cfg(feature = "std")]
1049impl Add<Duration> for SystemTime {
1050 type Output = Self;
1051
1052 fn add(self, duration: Duration) -> Self::Output {
1053 if duration.is_zero() {
1054 self
1055 } else if duration.is_positive() {
1056 self + duration.unsigned_abs()
1057 } else {
1058 debug_assert!(duration.is_negative());
1059 self - duration.unsigned_abs()
1060 }
1061 }
1062}
1063
1064impl_add_assign!(SystemTime: #[cfg(feature = "std")] Duration);
1065
1066#[cfg(feature = "std")]
1067impl Sub<Duration> for SystemTime {
1068 type Output = Self;
1069
1070 fn sub(self, duration: Duration) -> Self::Output {
1071 (DateTime::from(self) - duration).into()
1072 }
1073}
1074
1075impl_sub_assign!(SystemTime: #[cfg(feature = "std")] Duration);
1076
1077#[cfg(feature = "std")]
1078impl Sub<SystemTime> for DateTime<offset_kind::Fixed> {
1079 type Output = Duration;
1080
1081 fn sub(self, rhs: SystemTime) -> Self::Output {
1082 self - Self::from(rhs)
1083 }
1084}
1085
1086#[cfg(feature = "std")]
1087impl Sub<DateTime<offset_kind::Fixed>> for SystemTime {
1088 type Output = Duration;
1089
1090 fn sub(self, rhs: DateTime<offset_kind::Fixed>) -> Self::Output {
1091 DateTime::<offset_kind::Fixed>::from(self) - rhs
1092 }
1093}
1094
1095#[cfg(feature = "std")]
1096impl PartialEq<SystemTime> for DateTime<offset_kind::Fixed> {
1097 fn eq(&self, rhs: &SystemTime) -> bool {
1098 self == &Self::from(*rhs)
1099 }
1100}
1101
1102#[cfg(feature = "std")]
1103impl PartialEq<DateTime<offset_kind::Fixed>> for SystemTime {
1104 fn eq(&self, rhs: &DateTime<offset_kind::Fixed>) -> bool {
1105 &DateTime::<offset_kind::Fixed>::from(*self) == rhs
1106 }
1107}
1108
1109#[cfg(feature = "std")]
1110impl PartialOrd<SystemTime> for DateTime<offset_kind::Fixed> {
1111 fn partial_cmp(&self, other: &SystemTime) -> Option<Ordering> {
1112 self.partial_cmp(&Self::from(*other))
1113 }
1114}
1115
1116#[cfg(feature = "std")]
1117impl PartialOrd<DateTime<offset_kind::Fixed>> for SystemTime {
1118 fn partial_cmp(&self, other: &DateTime<offset_kind::Fixed>) -> Option<Ordering> {
1119 DateTime::<offset_kind::Fixed>::from(*self).partial_cmp(other)
1120 }
1121}
1122
1123#[cfg(feature = "std")]
1124impl From<SystemTime> for DateTime<offset_kind::Fixed> {
1125 fn from(system_time: SystemTime) -> Self {
1126 match system_time.duration_since(earlier:SystemTime::UNIX_EPOCH) {
1127 Ok(duration: Duration) => Self::UNIX_EPOCH + duration,
1128 Err(err: SystemTimeError) => Self::UNIX_EPOCH - err.duration(),
1129 }
1130 }
1131}
1132
1133#[allow(clippy::fallible_impl_from)] // caused by `debug_assert!`
1134#[cfg(feature = "std")]
1135impl From<DateTime<offset_kind::Fixed>> for SystemTime {
1136 fn from(datetime: DateTime<offset_kind::Fixed>) -> Self {
1137 let duration: Duration = datetime - DateTime::<offset_kind::Fixed>::UNIX_EPOCH;
1138
1139 if duration.is_zero() {
1140 Self::UNIX_EPOCH
1141 } else if duration.is_positive() {
1142 Self::UNIX_EPOCH + duration.unsigned_abs()
1143 } else {
1144 debug_assert!(duration.is_negative());
1145 Self::UNIX_EPOCH - duration.unsigned_abs()
1146 }
1147 }
1148}
1149
1150#[allow(clippy::fallible_impl_from)]
1151#[cfg(all(
1152 target_family = "wasm",
1153 not(any(target_os = "emscripten", target_os = "wasi")),
1154 feature = "wasm-bindgen"
1155))]
1156impl From<js_sys::Date> for DateTime<offset_kind::Fixed> {
1157 fn from(js_date: js_sys::Date) -> Self {
1158 // get_time() returns milliseconds
1159 let timestamp_nanos = (js_date.get_time() * Nanosecond.per(Millisecond) as f64) as i128;
1160 Self::from_unix_timestamp_nanos(timestamp_nanos)
1161 .expect("invalid timestamp: Timestamp cannot fit in range")
1162 }
1163}
1164
1165#[cfg(all(
1166 target_family = "wasm",
1167 not(any(target_os = "emscripten", target_os = "wasi")),
1168 feature = "wasm-bindgen"
1169))]
1170impl From<DateTime<offset_kind::Fixed>> for js_sys::Date {
1171 fn from(datetime: DateTime<offset_kind::Fixed>) -> Self {
1172 // new Date() takes milliseconds
1173 let timestamp =
1174 (datetime.unix_timestamp_nanos() / Nanosecond.per(Millisecond) as i128) as f64;
1175 js_sys::Date::new(&timestamp.into())
1176 }
1177}
1178// endregion trait impls
1179