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 | |
8 | use core::cmp::Ordering; |
9 | use core::fmt; |
10 | use core::hash::{Hash, Hasher}; |
11 | use core::mem::size_of; |
12 | use core::ops::{Add, AddAssign, Sub, SubAssign}; |
13 | use core::time::Duration as StdDuration; |
14 | #[cfg (feature = "formatting" )] |
15 | use std::io; |
16 | #[cfg (feature = "std" )] |
17 | use std::time::SystemTime; |
18 | |
19 | use crate::convert::*; |
20 | use crate::date::{MAX_YEAR, MIN_YEAR}; |
21 | #[cfg (feature = "formatting" )] |
22 | use crate::formatting::Formattable; |
23 | #[cfg (feature = "parsing" )] |
24 | use crate::parsing::{Parsable, Parsed}; |
25 | use crate::{error, util, Date, Duration, Month, Time, UtcOffset, Weekday}; |
26 | |
27 | #[allow (missing_debug_implementations, missing_copy_implementations)] |
28 | pub(crate) mod offset_kind { |
29 | pub enum None {} |
30 | pub enum Fixed {} |
31 | } |
32 | |
33 | pub(crate) use sealed::MaybeOffset; |
34 | use sealed::*; |
35 | mod 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 | |
113 | impl 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 | |
127 | impl 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`. |
144 | const 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 | |
164 | const 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 | |
173 | pub(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. |
194 | const UNIX_EPOCH_JULIAN_DAY: i32 = Date::__from_ordinal_date_unchecked(year:1970, ordinal:1).to_julian_day(); |
195 | |
196 | pub 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. |
203 | impl<O: MaybeOffset> Clone for DateTime<O> { |
204 | fn clone(&self) -> Self { |
205 | *self |
206 | } |
207 | } |
208 | |
209 | // Manual impl to remove extraneous bounds. |
210 | impl<O: MaybeOffset> Copy for DateTime<O> {} |
211 | |
212 | // region: constructors |
213 | impl 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 | |
227 | impl 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 | |
235 | impl<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 | |
883 | impl<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 | |
889 | impl<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 |
900 | impl<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 | |
910 | impl<O: MaybeOffset> Eq for DateTime<O> {} |
911 | |
912 | impl<O: MaybeOffset> PartialOrd for DateTime<O> { |
913 | fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> { |
914 | Some(self.cmp(rhs)) |
915 | } |
916 | } |
917 | |
918 | impl<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 | |
929 | impl<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 | |
939 | impl<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 | |
948 | impl<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 | |
968 | impl<O: MaybeOffset> AddAssign<Duration> for DateTime<O> { |
969 | fn add_assign(&mut self, rhs: Duration) { |
970 | *self = *self + rhs; |
971 | } |
972 | } |
973 | |
974 | impl<O: MaybeOffset> AddAssign<StdDuration> for DateTime<O> { |
975 | fn add_assign(&mut self, rhs: StdDuration) { |
976 | *self = *self + rhs; |
977 | } |
978 | } |
979 | |
980 | impl<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 | |
989 | impl<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 | |
1009 | impl<O: MaybeOffset> SubAssign<Duration> for DateTime<O> { |
1010 | fn sub_assign(&mut self, rhs: Duration) { |
1011 | *self = *self - rhs; |
1012 | } |
1013 | } |
1014 | |
1015 | impl<O: MaybeOffset> SubAssign<StdDuration> for DateTime<O> { |
1016 | fn sub_assign(&mut self, rhs: StdDuration) { |
1017 | *self = *self - rhs; |
1018 | } |
1019 | } |
1020 | |
1021 | impl<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" )] |
1049 | impl 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 | |
1064 | impl_add_assign!(SystemTime: #[cfg (feature = "std" )] Duration); |
1065 | |
1066 | #[cfg (feature = "std" )] |
1067 | impl 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 | |
1075 | impl_sub_assign!(SystemTime: #[cfg (feature = "std" )] Duration); |
1076 | |
1077 | #[cfg (feature = "std" )] |
1078 | impl 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" )] |
1087 | impl 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" )] |
1096 | impl 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" )] |
1103 | impl 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" )] |
1110 | impl 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" )] |
1117 | impl 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" )] |
1124 | impl 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" )] |
1135 | impl 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 | ))] |
1156 | impl 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 | ))] |
1170 | impl 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(×tamp.into()) |
1176 | } |
1177 | } |
1178 | // endregion trait impls |
1179 | |