dtt/datetime/mod.rs
1// datetime.rs
2//
3// Copyright © 2025 DateTime (DTT) library.
4// SPDX-License-Identifier: Apache-2.0 OR MIT
5
6//! DateTime module for managing dates, times, and timezones in Rust.
7//!
8//! # Overview
9//!
10//! This module provides a comprehensive datetime manipulation API that includes:
11//! - Fixed offset timezone support
12//! - Date and time creation and parsing
13//! - Format conversion (RFC 3339, ISO 8601)
14//! - Date arithmetic and comparison operations
15//! - Validation utilities
16//!
17//! **Note**: Daylight Saving Time (DST) is **not automatically handled**. Users must
18//! manually manage DST transitions by selecting appropriate timezone offsets.
19//!
20//! # Examples
21//!
22//! ```rust
23//! use dtt::datetime::DateTime;
24//!
25//! // Create current UTC time
26//! let now = DateTime::new();
27//!
28//! // Parse specific datetime
29//! let maybe_dt = DateTime::parse("2024-01-01T12:00:00Z");
30//! if let Ok(dt) = maybe_dt {
31//! // Convert timezone
32//! let est = dt.convert_to_tz("EST_USA");
33//! if let Ok(est_dt) = est {
34//! // ...
35//! }
36//! }
37//! ```
38
39// Lints are enforced via [lints.clippy] and [lints.rust] in Cargo.toml.
40
41use crate::error::DateTimeError;
42#[cfg(feature = "serde")]
43use serde::{Deserialize, Deserializer, Serialize, Serializer};
44use std::{
45 cmp::Ordering,
46 collections::HashMap,
47 fmt,
48 hash::{Hash, Hasher},
49 ops::{Add, Sub},
50 str::FromStr,
51 sync::LazyLock,
52};
53use time::{
54 format_description, Date, Duration, Month, OffsetDateTime,
55 PrimitiveDateTime, Time, UtcOffset, Weekday,
56};
57
58// Submodules. `DateTimeBuilder` is re-exported below so callers can keep
59// using `dtt::datetime::DateTimeBuilder`. The `validate` module hosts an
60// additional `impl DateTime { ... }` block and needs no re-export.
61mod builder;
62#[cfg(test)]
63mod tests;
64mod validate;
65
66pub use builder::DateTimeBuilder;
67
68/// Maximum valid hour value (0-23)
69pub(super) const MAX_HOUR: u8 = 23;
70
71/// Maximum valid minute/second value (0-59)
72pub(super) const MAX_MIN_SEC: u8 = 59;
73
74/// Maximum valid day value (1-31)
75pub(super) const MAX_DAY: u8 = 31;
76
77/// Maximum valid month value (1-12)
78pub(super) const MAX_MONTH: u8 = 12;
79
80/// Maximum valid microsecond value (0-999_999)
81pub(super) const MAX_MICROSECOND: u32 = 999_999;
82
83/// Maximum valid ISO week number (1-53)
84pub(super) const MAX_ISO_WEEK: u8 = 53;
85
86/// Maximum valid ordinal day (1-366)
87pub(super) const MAX_ORDINAL_DAY: u16 = 366;
88
89/// Represents a date and time with timezone offset support.
90///
91/// This struct combines a UTC datetime with a timezone offset, allowing for
92/// timezone-aware datetime operations. While it supports fixed offsets,
93/// it does **not** automatically handle DST transitions.
94///
95/// # Examples
96///
97/// ```
98/// use dtt::datetime::DateTime;
99///
100/// let utc = DateTime::new();
101/// let maybe_est = utc.convert_to_tz("EST_USA");
102/// if let Ok(est) = maybe_est {
103/// // ...
104/// }
105/// ```
106#[derive(Copy, Clone, Debug)]
107pub struct DateTime {
108 /// The date and time in UTC (when offset = `UtcOffset::UTC`) or a
109 /// user-chosen offset if `offset != UtcOffset::UTC`.
110 pub(crate) datetime: PrimitiveDateTime,
111 /// The timezone offset from UTC.
112 pub(crate) offset: UtcOffset,
113}
114
115#[cfg(feature = "serde")]
116impl Serialize for DateTime {
117 /// Serializes a `DateTime` as a canonical RFC 3339 string. Two
118 /// `DateTime` values that compare equal under `PartialEq` always
119 /// produce equal serialized strings.
120 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121 where
122 S: Serializer,
123 {
124 let s =
125 self.format_rfc3339().map_err(serde::ser::Error::custom)?;
126 serializer.serialize_str(&s)
127 }
128}
129
130#[cfg(feature = "serde")]
131impl<'de> Deserialize<'de> for DateTime {
132 /// Deserializes a `DateTime` from an RFC 3339 string.
133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134 where
135 D: Deserializer<'de>,
136 {
137 let s = <&str>::deserialize(deserializer)?;
138 Self::parse(s).map_err(serde::de::Error::custom)
139 }
140}
141
142/// Static mapping of timezone abbreviations to their `UtcOffset`.
143///
144/// # Note
145///
146/// This is not an exhaustive list of timezones. It is a convenient subset
147/// for demonstration purposes. Real-world usage might integrate a
148/// more robust timezone library or database.
149///
150/// ## Disambiguation
151///
152/// Several common abbreviations refer to multiple zones in the real world.
153/// To avoid silent wrong-answer bugs, ambiguous bare codes (`IST`, `CST`,
154/// `EST`) are **not** accepted; callers must use an explicit suffixed form:
155///
156/// | Abbreviation | Resolves to |
157/// |--------------|-------------|
158/// | `IST_INDIA` | +05:30 (Indian Standard Time) |
159/// | `IST_IRELAND` | +01:00 (Irish Standard Time) |
160/// | `IST_ISRAEL` | +02:00 (Israel Standard Time) |
161/// | `CST_USA` | -06:00 (US Central Standard Time) |
162/// | `CST_CHINA` | +08:00 (China Standard Time) |
163/// | `EST_USA` | -05:00 (US Eastern Standard Time) |
164/// | `EST_AUS` | +10:00 (Australian Eastern Standard Time) |
165///
166/// `WADT` (which historically meant Western Australia DST) is not exposed
167/// because its `+08:45` offset corresponds to `ACWST` (Australian Central
168/// Western Standard Time); use `ACWST` instead.
169static TIMEZONE_OFFSETS: LazyLock<
170 HashMap<&'static str, Result<UtcOffset, DateTimeError>>,
171> = LazyLock::new(|| {
172 let mut m = HashMap::new();
173 let _ = m.insert("UTC", Ok(UtcOffset::UTC));
174 let _ = m.insert("GMT", Ok(UtcOffset::UTC));
175
176 // North American time zones (USA)
177 let _ = m.insert(
178 "EST_USA",
179 UtcOffset::from_hms(-5, 0, 0).map_err(DateTimeError::from),
180 );
181 let _ = m.insert(
182 "EDT",
183 UtcOffset::from_hms(-4, 0, 0).map_err(DateTimeError::from),
184 );
185 let _ = m.insert(
186 "CST_USA",
187 UtcOffset::from_hms(-6, 0, 0).map_err(DateTimeError::from),
188 );
189 let _ = m.insert(
190 "CDT",
191 UtcOffset::from_hms(-5, 0, 0).map_err(DateTimeError::from),
192 );
193 let _ = m.insert(
194 "MST",
195 UtcOffset::from_hms(-7, 0, 0).map_err(DateTimeError::from),
196 );
197 let _ = m.insert(
198 "MDT",
199 UtcOffset::from_hms(-6, 0, 0).map_err(DateTimeError::from),
200 );
201 let _ = m.insert(
202 "PST",
203 UtcOffset::from_hms(-8, 0, 0).map_err(DateTimeError::from),
204 );
205 let _ = m.insert(
206 "PDT",
207 UtcOffset::from_hms(-7, 0, 0).map_err(DateTimeError::from),
208 );
209
210 // European time zones
211 let _ = m.insert(
212 "CET",
213 UtcOffset::from_hms(1, 0, 0).map_err(DateTimeError::from),
214 );
215 let _ = m.insert(
216 "CEST",
217 UtcOffset::from_hms(2, 0, 0).map_err(DateTimeError::from),
218 );
219 let _ = m.insert(
220 "EET",
221 UtcOffset::from_hms(2, 0, 0).map_err(DateTimeError::from),
222 );
223 let _ = m.insert(
224 "EEST",
225 UtcOffset::from_hms(3, 0, 0).map_err(DateTimeError::from),
226 );
227 let _ = m.insert(
228 "IST_IRELAND",
229 UtcOffset::from_hms(1, 0, 0).map_err(DateTimeError::from),
230 );
231
232 // Middle East time zones
233 let _ = m.insert(
234 "IST_ISRAEL",
235 UtcOffset::from_hms(2, 0, 0).map_err(DateTimeError::from),
236 );
237
238 // Asian time zones
239 let _ = m.insert(
240 "JST",
241 UtcOffset::from_hms(9, 0, 0).map_err(DateTimeError::from),
242 );
243 let _ = m.insert(
244 "IST_INDIA",
245 UtcOffset::from_hms(5, 30, 0).map_err(DateTimeError::from),
246 );
247 let _ = m.insert(
248 "CST_CHINA",
249 UtcOffset::from_hms(8, 0, 0).map_err(DateTimeError::from),
250 );
251 let _ = m.insert(
252 "HKT",
253 UtcOffset::from_hms(8, 0, 0).map_err(DateTimeError::from),
254 );
255
256 // Australian time zones
257 let _ = m.insert(
258 "EST_AUS",
259 UtcOffset::from_hms(10, 0, 0).map_err(DateTimeError::from),
260 );
261 let _ = m.insert(
262 "AEDT",
263 UtcOffset::from_hms(11, 0, 0).map_err(DateTimeError::from),
264 );
265 let _ = m.insert(
266 "AEST",
267 UtcOffset::from_hms(10, 0, 0).map_err(DateTimeError::from),
268 );
269 let _ = m.insert(
270 "ACWST",
271 UtcOffset::from_hms(8, 45, 0).map_err(DateTimeError::from),
272 );
273
274 m
275});
276
277// -----------------------------------------------------------------------------
278// Core Implementations
279// -----------------------------------------------------------------------------
280
281impl DateTime {
282 // -------------------------------------------------------------------------
283 // Creation Methods
284 // -------------------------------------------------------------------------
285
286 /// Creates a new `DateTime` instance representing the current UTC time.
287 ///
288 /// # Examples
289 ///
290 /// ```
291 /// use dtt::datetime::DateTime;
292 ///
293 /// let now = DateTime::new();
294 /// ```
295 #[must_use]
296 pub fn new() -> Self {
297 // Directly obtain the current UTC time.
298 let now = OffsetDateTime::now_utc();
299 Self {
300 datetime: PrimitiveDateTime::new(now.date(), now.time()),
301 offset: UtcOffset::UTC,
302 }
303 }
304
305 /// Creates a new `DateTime` instance with the current time in the specified timezone.
306 ///
307 /// # Arguments
308 ///
309 /// * `tz` - A timezone abbreviation (e.g., "UTC", "`EST_USA`", "PST")
310 ///
311 /// # Returns
312 ///
313 /// Returns a `Result` containing either the new `DateTime` instance or a `DateTimeError`
314 /// if the timezone is invalid.
315 ///
316 /// # Examples
317 ///
318 /// ```
319 /// use dtt::datetime::DateTime;
320 ///
321 /// let maybe_est_time = DateTime::new_with_tz("EST_USA");
322 /// if let Ok(est_time) = maybe_est_time {
323 /// // ...
324 /// }
325 /// ```
326 ///
327 /// # Errors
328 ///
329 /// Returns a `DateTimeError` if the timezone is invalid.
330 ///
331 pub fn new_with_tz(tz: &str) -> Result<Self, DateTimeError> {
332 let offset = TIMEZONE_OFFSETS
333 .get(tz)
334 .ok_or(DateTimeError::InvalidTimezone)?
335 .as_ref()
336 .map_err(Clone::clone)?;
337
338 let now_utc = OffsetDateTime::now_utc();
339 let now_local = now_utc.to_offset(*offset);
340
341 Ok(Self {
342 datetime: PrimitiveDateTime::new(
343 now_local.date(),
344 now_local.time(),
345 ),
346 offset: *offset,
347 })
348 }
349
350 /// Creates a new `DateTime` instance with a custom UTC offset.
351 ///
352 /// # Arguments
353 ///
354 /// * `hours` - Hour offset from UTC (-23 to +23)
355 /// * `minutes` - Minute offset from UTC (-59 to +59). Must have the
356 /// same sign as `hours` unless one of the two is zero.
357 ///
358 /// # Returns
359 ///
360 /// Returns a `Result` containing either the new `DateTime` or a
361 /// `DateTimeError::InvalidTimezone` if any component is out of range
362 /// or if `hours` and `minutes` have opposing signs.
363 ///
364 /// # Examples
365 ///
366 /// ```
367 /// use dtt::datetime::DateTime;
368 ///
369 /// // Create time with UTC+5:30 offset (e.g., for India)
370 /// let maybe_ist = DateTime::new_with_custom_offset(5, 30);
371 /// if let Ok(ist) = maybe_ist {
372 /// // ...
373 /// }
374 /// ```
375 ///
376 /// # Errors
377 ///
378 /// Returns a `DateTimeError` if the timezone is invalid.
379 ///
380 pub fn new_with_custom_offset(
381 hours: i8,
382 minutes: i8,
383 ) -> Result<Self, DateTimeError> {
384 // Direct numeric checks (no casts needed)
385 if hours.abs() > 23 || minutes.abs() > 59 {
386 return Err(DateTimeError::InvalidTimezone);
387 }
388
389 // Reject ambiguous mixed-sign inputs. The `time` crate would
390 // silently coerce e.g. `(5, -30)` to `+05:30`, which is almost
391 // never what the caller wants. Same-sign inputs and inputs where
392 // one component is zero are still accepted.
393 if hours != 0
394 && minutes != 0
395 && hours.signum() != minutes.signum()
396 {
397 return Err(DateTimeError::InvalidTimezone);
398 }
399
400 let offset = UtcOffset::from_hms(hours, minutes, 0)
401 .map_err(|_| DateTimeError::InvalidTimezone)?;
402
403 let now_utc = OffsetDateTime::now_utc();
404 let now_local = now_utc.to_offset(offset);
405
406 Ok(Self {
407 datetime: PrimitiveDateTime::new(
408 now_local.date(),
409 now_local.time(),
410 ),
411 offset,
412 })
413 }
414
415 /// Returns a new `DateTime` which is exactly one day earlier.
416 ///
417 /// # Returns
418 ///
419 /// Returns a `Result` containing the new `DateTime` or a `DateTimeError`
420 /// if subtracting one day would result in an invalid date.
421 ///
422 /// # Examples
423 ///
424 /// ```
425 /// use dtt::datetime::DateTime;
426 ///
427 /// let now = DateTime::new();
428 /// let maybe_yesterday = now.previous_day();
429 /// assert!(maybe_yesterday.is_ok());
430 /// ```
431 ///
432 /// # Errors
433 ///
434 /// Returns a `DateTimeError` if the resulting date would be invalid.
435 ///
436 pub fn previous_day(&self) -> Result<Self, DateTimeError> {
437 self.add_days(-1)
438 }
439
440 /// Returns a new `DateTime` which is exactly one day later.
441 ///
442 /// # Returns
443 ///
444 /// Returns a `Result` containing the new `DateTime` or a `DateTimeError`
445 /// if adding one day would result in an invalid date.
446 ///
447 /// # Examples
448 ///
449 /// ```
450 /// use dtt::datetime::DateTime;
451 ///
452 /// let now = DateTime::new();
453 /// let maybe_tomorrow = now.next_day();
454 /// assert!(maybe_tomorrow.is_ok());
455 /// ```
456 ///
457 /// # Errors
458 ///
459 /// Returns a `DateTimeError` if the resulting date would be invalid.
460 ///
461 pub fn next_day(&self) -> Result<Self, DateTimeError> {
462 self.add_days(1)
463 }
464
465 /// Sets the time components (hour, minute, second) while preserving the current date
466 /// and timezone offset.
467 ///
468 /// # Arguments
469 ///
470 /// * `hour` - Hour (0-23)
471 /// * `minute` - Minute (0-59)
472 /// * `second` - Second (0-59)
473 ///
474 /// # Returns
475 ///
476 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
477 /// if the time components are invalid.
478 ///
479 /// # Examples
480 ///
481 /// ```
482 /// use dtt::datetime::DateTime;
483 ///
484 /// let dt = DateTime::new();
485 /// // Attempt to set the time to 10:30:45
486 /// let updated_dt = dt.set_time(10, 30, 45);
487 /// assert!(updated_dt.is_ok());
488 /// if let Ok(new_val) = updated_dt {
489 /// assert_eq!(new_val.hour(), 10);
490 /// assert_eq!(new_val.minute(), 30);
491 /// assert_eq!(new_val.second(), 45);
492 /// }
493 /// ```
494 ///
495 /// # Errors
496 ///
497 /// Returns a `DateTimeError` if the resulting time would be invalid.
498 ///
499 pub fn set_time(
500 &self,
501 hour: u8,
502 minute: u8,
503 second: u8,
504 ) -> Result<Self, DateTimeError> {
505 // Construct a new time; returns an error if invalid
506 let new_time = Time::from_hms(hour, minute, second)
507 .map_err(|_| DateTimeError::InvalidTime)?;
508
509 // Preserve the existing date
510 Ok(Self {
511 datetime: PrimitiveDateTime::new(
512 self.datetime.date(),
513 new_time,
514 ),
515 offset: self.offset,
516 })
517 }
518
519 /// Subtracts a specified number of years from the `DateTime`.
520 ///
521 /// Handles leap year transitions appropriately (e.g., if subtracting a year from
522 /// Feb 29 results in Feb 28).
523 ///
524 /// # Arguments
525 ///
526 /// * `years` - Number of years to subtract
527 ///
528 /// # Returns
529 ///
530 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
531 /// if the resulting date would be invalid.
532 ///
533 /// # Examples
534 ///
535 /// ```
536 /// use dtt::datetime::DateTime;
537 ///
538 /// let dt = DateTime::new();
539 /// let maybe_past = dt.sub_years(1);
540 /// assert!(maybe_past.is_ok());
541 /// ```
542 ///
543 /// # Errors
544 ///
545 /// Returns a `DateTimeError` if the resulting date would be invalid.
546 ///
547 pub fn sub_years(&self, years: i32) -> Result<Self, DateTimeError> {
548 self.add_years(-years)
549 }
550
551 /// Converts this `DateTime` to another timezone, then formats it
552 /// using the provided `format_str`.
553 ///
554 /// # Arguments
555 ///
556 /// * `tz` - Target timezone abbreviation (e.g., "UTC", "`EST_USA`", "PST").
557 /// * `format_str` - A format description (see the `time` crate documentation
558 /// for the supported syntax).
559 ///
560 /// # Returns
561 ///
562 /// Returns a `Result<String, DateTimeError>` containing either
563 /// the formatted datetime string or an error if conversion or
564 /// formatting fails.
565 ///
566 /// # Errors
567 ///
568 /// This function will return a [`DateTimeError`] if:
569 /// - The specified timezone is not recognized or invalid.
570 /// - The formatting operation fails due to an invalid `format_str`.
571 ///
572 /// # Examples
573 ///
574 /// ```
575 /// use dtt::datetime::DateTime;
576 ///
577 /// let dt = DateTime::new();
578 /// let result = dt.format_time_in_timezone("EST_USA", "[hour]:[minute]:[second]");
579 /// if let Ok(formatted_str) = result {
580 /// println!("Time in EST: {}", formatted_str);
581 /// }
582 /// ```
583 pub fn format_time_in_timezone(
584 &self,
585 tz: &str,
586 format_str: &str,
587 ) -> Result<String, DateTimeError> {
588 // 1. Convert this DateTime to the specified timezone
589 let dt_tz = self.convert_to_tz(tz)?;
590
591 // 2. Format the timezone-adjusted DateTime using the provided format string
592 dt_tz.format(format_str)
593 }
594
595 /// Returns `true` if the input string is a valid ISO 8601 or RFC 3339–like datetime/date.
596 ///
597 /// # Arguments
598 ///
599 /// * `input` - A string that might represent a date or datetime in ISO 8601/RFC 3339 format.
600 ///
601 /// # Returns
602 ///
603 /// `true` if the string can be successfully parsed as either:
604 /// - RFC 3339 datetime (e.g., "2024-01-01T12:00:00Z"), or
605 /// - ISO 8601 date (e.g., "2024-01-01")
606 /// `false` otherwise.
607 ///
608 /// # Examples
609 ///
610 /// ```
611 /// use dtt::datetime::DateTime;
612 ///
613 /// assert!(DateTime::is_valid_iso_8601("2024-01-01T12:00:00Z"));
614 /// assert!(DateTime::is_valid_iso_8601("2024-01-01"));
615 /// assert!(!DateTime::is_valid_iso_8601("2024-13-01")); // invalid month
616 /// assert!(!DateTime::is_valid_iso_8601("not a date"));
617 /// ```
618 #[must_use]
619 pub fn is_valid_iso_8601(input: &str) -> bool {
620 // Mirror the strictness of `parse` so that
621 // `is_valid_iso_8601(x) <=> parse(x).is_ok()`.
622
623 // 1. Try the strict offset-aware path (matches `parse`).
624 if OffsetDateTime::parse(
625 input,
626 &format_description::well_known::Rfc3339,
627 )
628 .is_ok()
629 {
630 return true;
631 }
632
633 // 2. Only accept date-only inputs that don't carry a time component.
634 // `time::Date::parse` with `Iso8601::DATE` is lenient with trailing
635 // `T<…>` content; gating on the absence of `T`/space prevents the
636 // validator from accepting strings the parser would reject.
637 if !input.contains('T') && !input.contains(' ') {
638 return Date::parse(
639 input,
640 &format_description::well_known::Iso8601::DATE,
641 )
642 .is_ok();
643 }
644
645 false
646 }
647
648 /// Creates a `DateTime` instance from individual components.
649 ///
650 /// # Arguments
651 ///
652 /// * `year` - Calendar year
653 /// * `month` - Month (1-12)
654 /// * `day` - Day of month (1-31, depending on month)
655 /// * `hour` - Hour (0-23)
656 /// * `minute` - Minute (0-59)
657 /// * `second` - Second (0-59)
658 /// * `offset` - Timezone offset from UTC
659 ///
660 /// # Returns
661 ///
662 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
663 /// if any component is invalid.
664 ///
665 /// # Examples
666 ///
667 /// ```
668 /// use dtt::datetime::DateTime;
669 /// use time::UtcOffset;
670 ///
671 /// let dt = DateTime::from_components(2024, 1, 1, 12, 0, 0, UtcOffset::UTC);
672 /// assert!(dt.is_ok());
673 /// ```
674 ///
675 /// # Errors
676 ///
677 /// Returns a `DateTimeError` if any component is invalid.
678 ///
679 pub fn from_components(
680 year: i32,
681 month: u8,
682 day: u8,
683 hour: u8,
684 minute: u8,
685 second: u8,
686 offset: UtcOffset,
687 ) -> Result<Self, DateTimeError> {
688 let month = Month::try_from(month)
689 .map_err(|_| DateTimeError::InvalidDate)?;
690 let date = Date::from_calendar_date(year, month, day)
691 .map_err(|_| DateTimeError::InvalidDate)?;
692 let time = Time::from_hms(hour, minute, second)
693 .map_err(|_| DateTimeError::InvalidTime)?;
694
695 Ok(Self {
696 datetime: PrimitiveDateTime::new(date, time),
697 offset,
698 })
699 }
700
701 // -------------------------------------------------------------------------
702 // Getter Methods
703 // -------------------------------------------------------------------------
704
705 /// Returns the year component of the `DateTime`.
706 #[must_use]
707 pub const fn year(&self) -> i32 {
708 self.datetime.date().year()
709 }
710
711 /// Returns the month component of the `DateTime`.
712 #[must_use]
713 pub const fn month(&self) -> Month {
714 self.datetime.date().month()
715 }
716
717 /// Returns the day component of the `DateTime`.
718 #[must_use]
719 pub const fn day(&self) -> u8 {
720 self.datetime.date().day()
721 }
722
723 /// Returns the hour component of the `DateTime`.
724 #[must_use]
725 pub const fn hour(&self) -> u8 {
726 self.datetime.time().hour()
727 }
728
729 /// Returns the minute component of the `DateTime`.
730 #[must_use]
731 pub const fn minute(&self) -> u8 {
732 self.datetime.time().minute()
733 }
734
735 /// Returns the second component of the `DateTime`.
736 #[must_use]
737 pub const fn second(&self) -> u8 {
738 self.datetime.time().second()
739 }
740
741 /// Returns the microsecond component of the `DateTime`.
742 #[must_use]
743 pub const fn microsecond(&self) -> u32 {
744 self.datetime.microsecond()
745 }
746
747 /// Returns the ISO week component of the `DateTime`.
748 #[must_use]
749 pub const fn iso_week(&self) -> u8 {
750 self.datetime.iso_week()
751 }
752
753 /// Returns the ISO 8601 week-numbering year.
754 ///
755 /// **Note:** This may differ from [`Self::year`] near year boundaries.
756 /// For example, `2022-01-01` has calendar year `2022` but ISO year
757 /// `2021` (because it falls in ISO week 52 of 2021).
758 #[must_use]
759 pub const fn iso_year(&self) -> i32 {
760 self.datetime.date().to_iso_week_date().0
761 }
762
763 /// Returns the ordinal day (day of year) component of the `DateTime`.
764 #[must_use]
765 pub const fn ordinal(&self) -> u16 {
766 self.datetime.ordinal()
767 }
768
769 /// Returns the timezone offset of the `DateTime`.
770 #[must_use]
771 pub const fn offset(&self) -> UtcOffset {
772 self.offset
773 }
774
775 /// Returns the weekday of the `DateTime`.
776 #[must_use]
777 pub const fn weekday(&self) -> Weekday {
778 self.datetime.date().weekday()
779 }
780
781 // -------------------------------------------------------------------------
782 // Parsing Methods
783 // -------------------------------------------------------------------------
784
785 /// Parses a string representation of a date and time.
786 ///
787 /// Supports both RFC 3339 and ISO 8601 formats.
788 ///
789 /// # Arguments
790 ///
791 /// * `input` - A string slice containing the date/time to parse
792 ///
793 /// # Returns
794 ///
795 /// Returns a `Result` containing either the parsed `DateTime` or a `DateTimeError`
796 /// if parsing fails.
797 ///
798 /// # Examples
799 ///
800 /// ```
801 /// use dtt::datetime::DateTime;
802 ///
803 /// // Parse RFC 3339 format
804 /// let dt1 = DateTime::parse("2024-01-01T12:00:00Z");
805 ///
806 /// // Parse ISO 8601 date
807 /// let dt2 = DateTime::parse("2024-01-01");
808 /// assert!(dt1.is_ok());
809 /// assert!(dt2.is_ok());
810 /// ```
811 ///
812 /// # Errors
813 ///
814 /// Returns a `DateTimeError` if the input string is not a valid date/time.
815 ///
816 pub fn parse(input: &str) -> Result<Self, DateTimeError> {
817 // Try RFC 3339 format first (preserves the offset).
818 if let Ok(odt) = OffsetDateTime::parse(
819 input,
820 &format_description::well_known::Rfc3339,
821 ) {
822 return Ok(Self {
823 datetime: PrimitiveDateTime::new(
824 odt.date(),
825 odt.time(),
826 ),
827 offset: odt.offset(),
828 });
829 }
830
831 // Only try date-only parsing if no time component is present.
832 // This prevents silently truncating "2024-01-01T12:34:56" to midnight.
833 if !input.contains('T') && !input.contains(' ') {
834 if let Ok(date) = Date::parse(
835 input,
836 &format_description::well_known::Iso8601::DATE,
837 ) {
838 return Ok(Self {
839 datetime: PrimitiveDateTime::new(
840 date,
841 Time::MIDNIGHT,
842 ),
843 offset: UtcOffset::UTC,
844 });
845 }
846 }
847
848 Err(DateTimeError::InvalidFormat)
849 }
850
851 /// Parses a date/time string using a custom format specification.
852 ///
853 /// # Arguments
854 ///
855 /// * `input` - The date/time string to parse
856 /// * `format` - Format specification string (see `time` crate documentation)
857 ///
858 /// # Returns
859 ///
860 /// Returns a `Result` containing either the parsed `DateTime` or a `DateTimeError`
861 /// if parsing fails.
862 ///
863 /// # Examples
864 ///
865 /// ```
866 /// use dtt::datetime::DateTime;
867 ///
868 /// let dt = DateTime::parse_custom_format(
869 /// "2024-01-01 12:00:00",
870 /// "[year]-[month]-[day] [hour]:[minute]:[second]"
871 /// );
872 /// assert!(dt.is_ok());
873 /// ```
874 ///
875 /// # Errors
876 ///
877 /// Returns a `DateTimeError` if the input string is not a valid date/time.
878 ///
879 pub fn parse_custom_format(
880 input: &str,
881 format: &str,
882 ) -> Result<Self, DateTimeError> {
883 let format_desc =
884 format_description::parse_borrowed::<1>(format)
885 .map_err(|_| DateTimeError::InvalidFormat)?;
886 let datetime = PrimitiveDateTime::parse(input, &format_desc)
887 .map_err(|_| DateTimeError::InvalidFormat)?;
888
889 Ok(Self {
890 datetime,
891 offset: UtcOffset::UTC,
892 })
893 }
894
895 // -------------------------------------------------------------------------
896 // Formatting Methods
897 // -------------------------------------------------------------------------
898
899 /// Formats the `DateTime` according to the specified format string.
900 ///
901 /// # Arguments
902 ///
903 /// * `format_str` - Format specification string (see `time` crate documentation)
904 ///
905 /// # Returns
906 ///
907 /// Returns a `Result` containing either the formatted string or a `DateTimeError`
908 /// if formatting fails.
909 ///
910 /// # Examples
911 ///
912 /// ```
913 /// use dtt::datetime::DateTime;
914 ///
915 /// let dt = DateTime::new();
916 /// let formatted = dt.format("[year]-[month]-[day]");
917 /// assert!(formatted.is_ok());
918 /// ```
919 ///
920 /// # Errors
921 ///
922 /// Returns a `DateTimeError` if the format string is invalid.
923 ///
924 pub fn format(
925 &self,
926 format_str: &str,
927 ) -> Result<String, DateTimeError> {
928 let format_desc =
929 format_description::parse_borrowed::<1>(format_str)
930 .map_err(|_| DateTimeError::InvalidFormat)?;
931 self.datetime
932 .format(&format_desc)
933 .map_err(|_| DateTimeError::InvalidFormat)
934 }
935
936 /// Formats the `DateTime` as an RFC 3339 string.
937 ///
938 /// # Returns
939 ///
940 /// Returns a `Result` containing either the formatted RFC 3339 string
941 /// or a `DateTimeError` if formatting fails.
942 ///
943 /// # Examples
944 ///
945 /// ```
946 /// use dtt::datetime::DateTime;
947 ///
948 /// let dt = DateTime::new();
949 /// let maybe_rfc3339 = dt.format_rfc3339();
950 /// assert!(maybe_rfc3339.is_ok());
951 /// ```
952 ///
953 /// # Errors
954 ///
955 /// Returns a `DateTimeError` if formatting fails.
956 ///
957 pub fn format_rfc3339(&self) -> Result<String, DateTimeError> {
958 self.datetime
959 .assume_offset(self.offset)
960 .format(&format_description::well_known::Rfc3339)
961 .map_err(|_| DateTimeError::InvalidFormat)
962 }
963
964 /// Updates the `DateTime` to the current time while preserving the timezone offset.
965 ///
966 /// # Returns
967 ///
968 /// Returns a `Result` containing either the updated `DateTime` or a `DateTimeError`
969 /// if the update fails.
970 ///
971 /// # Examples
972 ///
973 /// ```
974 /// use dtt::datetime::DateTime;
975 /// use std::thread::sleep;
976 /// use std::time::Duration;
977 ///
978 /// let dt = DateTime::new();
979 /// sleep(Duration::from_secs(1));
980 /// let updated_dt = dt.update();
981 /// assert!(updated_dt.is_ok());
982 /// ```
983 ///
984 /// # Errors
985 ///
986 /// Returns a `DateTimeError` if the update fails.
987 ///
988 pub fn update(&self) -> Result<Self, DateTimeError> {
989 let now = OffsetDateTime::now_utc().to_offset(self.offset);
990 Ok(Self {
991 datetime: PrimitiveDateTime::new(now.date(), now.time()),
992 offset: self.offset,
993 })
994 }
995
996 // -------------------------------------------------------------------------
997 // Timezone Conversion Method
998 // -------------------------------------------------------------------------
999
1000 /// Converts the current `DateTime` to another timezone.
1001 ///
1002 /// # Arguments
1003 ///
1004 /// * `new_tz` - Target timezone abbreviation (e.g., "UTC", "`EST_USA`", "PST")
1005 ///
1006 /// # Returns
1007 ///
1008 /// Returns a `Result` containing either the `DateTime` in the new timezone
1009 /// or a `DateTimeError` if the conversion fails.
1010 ///
1011 /// # Examples
1012 ///
1013 /// ```
1014 /// use dtt::datetime::DateTime;
1015 ///
1016 /// let utc = DateTime::new();
1017 /// let maybe_est = utc.convert_to_tz("EST_USA");
1018 /// assert!(maybe_est.is_ok());
1019 /// ```
1020 ///
1021 /// # Errors
1022 ///
1023 /// Returns a `DateTimeError` if the timezone is invalid.
1024 ///
1025 pub fn convert_to_tz(
1026 &self,
1027 new_tz: &str,
1028 ) -> Result<Self, DateTimeError> {
1029 let new_offset = TIMEZONE_OFFSETS
1030 .get(new_tz)
1031 .ok_or(DateTimeError::InvalidTimezone)?
1032 .as_ref()
1033 .map_err(Clone::clone)?;
1034
1035 let datetime_with_offset =
1036 self.datetime.assume_offset(self.offset);
1037 let new_datetime = datetime_with_offset.to_offset(*new_offset);
1038
1039 Ok(Self {
1040 datetime: PrimitiveDateTime::new(
1041 new_datetime.date(),
1042 new_datetime.time(),
1043 ),
1044 offset: *new_offset,
1045 })
1046 }
1047
1048 // -------------------------------------------------------------------------
1049 // Additional Utilities
1050 // -------------------------------------------------------------------------
1051
1052 /// Gets the Unix timestamp (seconds since Unix epoch).
1053 ///
1054 /// # Returns
1055 ///
1056 /// Returns the number of seconds from the Unix epoch (1970-01-01T00:00:00Z).
1057 ///
1058 /// # Examples
1059 ///
1060 /// ```
1061 /// use dtt::datetime::DateTime;
1062 ///
1063 /// let dt = DateTime::new();
1064 /// let ts = dt.unix_timestamp();
1065 /// ```
1066 #[must_use]
1067 pub const fn unix_timestamp(&self) -> i64 {
1068 self.datetime.assume_offset(self.offset).unix_timestamp()
1069 }
1070
1071 /// Calculates the duration between this `DateTime` and another.
1072 ///
1073 /// The result can be negative if `other` is later than `self`.
1074 ///
1075 /// # Arguments
1076 ///
1077 /// * `other` - The `DateTime` to compare with
1078 ///
1079 /// # Returns
1080 ///
1081 /// Returns a `Duration` representing the time difference.
1082 ///
1083 /// # Examples
1084 ///
1085 /// ```
1086 /// use dtt::datetime::DateTime;
1087 ///
1088 /// let dt1 = DateTime::new();
1089 /// let dt2 = dt1.add_days(1).unwrap_or(dt1);
1090 /// let duration = dt1.duration_since(&dt2);
1091 /// // duration could be negative if dt2 > dt1
1092 /// ```
1093 #[must_use]
1094 pub fn duration_since(&self, other: &Self) -> Duration {
1095 let self_offset = self.datetime.assume_offset(self.offset);
1096 let other_offset = other.datetime.assume_offset(other.offset);
1097
1098 let seconds_diff = self_offset.unix_timestamp()
1099 - other_offset.unix_timestamp();
1100 let nanos_diff = i64::from(self_offset.nanosecond())
1101 - i64::from(other_offset.nanosecond());
1102
1103 Duration::seconds(seconds_diff)
1104 + Duration::nanoseconds(nanos_diff)
1105 }
1106
1107 // -------------------------------------------------------------------------
1108 // Date Arithmetic Methods
1109 // -------------------------------------------------------------------------
1110
1111 /// Adds a specified number of days to the `DateTime`.
1112 ///
1113 /// # Arguments
1114 ///
1115 /// * `days` - Number of days to add (can be negative for subtraction)
1116 ///
1117 /// # Returns
1118 ///
1119 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1120 /// if the operation would result in an invalid date.
1121 ///
1122 /// # Errors
1123 ///
1124 /// This function returns a [`DateTimeError::InvalidDate`] if adding `days` results
1125 /// in a date overflow or otherwise invalid date.
1126 ///
1127 /// # Examples
1128 ///
1129 /// ```
1130 /// use dtt::datetime::DateTime;
1131 ///
1132 /// let dt = DateTime::new();
1133 /// let future = dt.add_days(7);
1134 /// assert!(future.is_ok());
1135 /// ```
1136 pub fn add_days(&self, days: i64) -> Result<Self, DateTimeError> {
1137 let new_datetime = self
1138 .datetime
1139 .checked_add(Duration::days(days))
1140 .ok_or(DateTimeError::InvalidDate)?;
1141
1142 Ok(Self {
1143 datetime: new_datetime,
1144 offset: self.offset,
1145 })
1146 }
1147
1148 /// Adds a specified number of months to the `DateTime`.
1149 ///
1150 /// Handles month-end dates and leap years appropriately.
1151 ///
1152 /// # Arguments
1153 ///
1154 /// * `months` - Number of months to add (can be negative for subtraction)
1155 ///
1156 /// # Returns
1157 ///
1158 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1159 /// if the operation would result in an invalid date.
1160 ///
1161 /// # Errors
1162 ///
1163 /// This function returns a [`DateTimeError`] if:
1164 /// - The calculated year, month, or day is invalid (e.g., out of range).
1165 /// - The underlying date library fails to construct a valid date.
1166 ///
1167 /// # Examples
1168 ///
1169 /// ```
1170 /// use dtt::datetime::DateTime;
1171 ///
1172 /// let dt = DateTime::new();
1173 /// let future = dt.add_months(3);
1174 /// assert!(future.is_ok());
1175 /// ```
1176 pub fn add_months(
1177 &self,
1178 months: i32,
1179 ) -> Result<Self, DateTimeError> {
1180 let current_date = self.datetime.date();
1181 let total_months = current_date
1182 .year()
1183 .checked_mul(12)
1184 .and_then(|v| {
1185 v.checked_add(i32::from(current_date.month() as u8))
1186 })
1187 .and_then(|v| v.checked_sub(1))
1188 .and_then(|v| v.checked_add(months))
1189 .ok_or(DateTimeError::InvalidDate)?;
1190
1191 let target_year = total_months.div_euclid(12);
1192 let target_month =
1193 u8::try_from(total_months.rem_euclid(12) + 1);
1194
1195 let target_month =
1196 target_month.map_err(|_| DateTimeError::InvalidDate)?;
1197 let days_in_target_month =
1198 days_in_month(target_year, target_month)?;
1199 let target_day = current_date.day().min(days_in_target_month);
1200
1201 let new_month = Month::try_from(target_month)
1202 .map_err(|_| DateTimeError::InvalidDate)?;
1203 let new_date = Date::from_calendar_date(
1204 target_year,
1205 new_month,
1206 target_day,
1207 )
1208 .map_err(|_| DateTimeError::InvalidDate)?;
1209
1210 Ok(Self {
1211 datetime: PrimitiveDateTime::new(
1212 new_date,
1213 self.datetime.time(),
1214 ),
1215 offset: self.offset,
1216 })
1217 }
1218
1219 /// Subtracts a specified number of months from the `DateTime`.
1220 ///
1221 /// # Arguments
1222 ///
1223 /// * `months` - Number of months to subtract
1224 ///
1225 /// # Returns
1226 ///
1227 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1228 /// if the operation would result in an invalid date.
1229 ///
1230 /// # Errors
1231 ///
1232 /// This function returns a [`DateTimeError::InvalidDate`] if:
1233 /// - The resulting date is out of valid range.
1234 /// - The underlying date library fails to construct a valid `DateTime`.
1235 ///
1236 /// # Examples
1237 ///
1238 /// ```
1239 /// use dtt::datetime::DateTime;
1240 ///
1241 /// let dt = DateTime::new();
1242 /// let past = dt.sub_months(3);
1243 /// assert!(past.is_ok());
1244 /// ```
1245 pub fn sub_months(
1246 &self,
1247 months: i32,
1248 ) -> Result<Self, DateTimeError> {
1249 self.add_months(-months)
1250 }
1251
1252 /// Adds a specified number of years to the `DateTime`.
1253 ///
1254 /// Handles leap-year transitions appropriately.
1255 ///
1256 /// # Arguments
1257 ///
1258 /// * `years` - Number of years to add (can be negative for subtraction)
1259 ///
1260 /// # Returns
1261 ///
1262 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1263 /// if the operation would result in an invalid date.
1264 ///
1265 /// # Errors
1266 ///
1267 /// This function returns a [`DateTimeError::InvalidDate`] if:
1268 /// - The resulting year is out of valid range.
1269 /// - A non-leap year cannot accommodate February 29th.
1270 /// - Any other invalid date scenario occurs during calculation.
1271 ///
1272 /// # Examples
1273 ///
1274 /// ```
1275 /// use dtt::datetime::DateTime;
1276 ///
1277 /// let dt = DateTime::new();
1278 /// let future = dt.add_years(5);
1279 /// assert!(future.is_ok());
1280 /// ```
1281 pub fn add_years(&self, years: i32) -> Result<Self, DateTimeError> {
1282 let current_date = self.datetime.date();
1283 let target_year = current_date
1284 .year()
1285 .checked_add(years)
1286 .ok_or(DateTimeError::InvalidDate)?;
1287
1288 // Handle February 29th in leap years
1289 let new_day = if current_date.month() == Month::February
1290 && current_date.day() == 29
1291 && !is_leap_year(target_year)
1292 {
1293 28
1294 } else {
1295 current_date.day()
1296 };
1297
1298 let new_date = Date::from_calendar_date(
1299 target_year,
1300 current_date.month(),
1301 new_day,
1302 )
1303 .map_err(|_| DateTimeError::InvalidDate)?;
1304
1305 Ok(Self {
1306 datetime: PrimitiveDateTime::new(
1307 new_date,
1308 self.datetime.time(),
1309 ),
1310 offset: self.offset,
1311 })
1312 }
1313
1314 // -------------------------------------------------------------------------
1315 // Range / Boundary Helper Methods
1316 // -------------------------------------------------------------------------
1317
1318 /// Returns a new `DateTime` for the start of the current week (Monday).
1319 ///
1320 /// # Errors
1321 ///
1322 /// This function can return a [`DateTimeError`] if an overflow or
1323 /// invalid date calculation occurs during date arithmetic.
1324 pub fn start_of_week(&self) -> Result<Self, DateTimeError> {
1325 let days_since_monday = i64::from(
1326 self.datetime.weekday().number_days_from_monday(),
1327 );
1328 self.add_days(-days_since_monday)
1329 }
1330
1331 /// Returns a new `DateTime` for the end of the current week (Sunday).
1332 ///
1333 /// # Errors
1334 ///
1335 /// This function can return a [`DateTimeError`] if an overflow or
1336 /// invalid date calculation occurs during date arithmetic.
1337 pub fn end_of_week(&self) -> Result<Self, DateTimeError> {
1338 let days_until_sunday = 6 - i64::from(
1339 self.datetime.weekday().number_days_from_monday(),
1340 );
1341 self.add_days(days_until_sunday)
1342 }
1343
1344 /// Returns a new `DateTime` for the start of the current month.
1345 ///
1346 /// # Errors
1347 ///
1348 /// This function can return a [`DateTimeError`] if the date cannot be
1349 /// constructed (e.g., due to an invalid year or month).
1350 pub fn start_of_month(&self) -> Result<Self, DateTimeError> {
1351 self.set_date(
1352 self.datetime.year(),
1353 self.datetime.month() as u8,
1354 1,
1355 )
1356 }
1357
1358 /// Returns a new `DateTime` for the end of the current month.
1359 ///
1360 /// # Errors
1361 ///
1362 /// This function can return a [`DateTimeError`] if the date cannot be
1363 /// constructed (e.g., `days_in_month` fails to provide a valid day).
1364 pub fn end_of_month(&self) -> Result<Self, DateTimeError> {
1365 let year = self.datetime.year();
1366 let month = self.datetime.month() as u8;
1367 let last_day = days_in_month(year, month)?;
1368 self.set_date(year, month, last_day)
1369 }
1370
1371 /// Returns a new `DateTime` for the start of the current year.
1372 ///
1373 /// # Errors
1374 ///
1375 /// This function can return a [`DateTimeError`] if the date cannot
1376 /// be constructed (e.g., invalid year).
1377 pub fn start_of_year(&self) -> Result<Self, DateTimeError> {
1378 self.set_date(self.datetime.year(), 1, 1)
1379 }
1380
1381 /// Returns a new `DateTime` for the end of the current year.
1382 ///
1383 /// # Errors
1384 ///
1385 /// This function can return a [`DateTimeError`] if the date cannot
1386 /// be constructed (e.g., invalid year).
1387 pub fn end_of_year(&self) -> Result<Self, DateTimeError> {
1388 self.set_date(self.datetime.year(), 12, 31)
1389 }
1390
1391 // -------------------------------------------------------------------------
1392 // Range Validation
1393 // -------------------------------------------------------------------------
1394
1395 /// Checks if the current `DateTime` falls within a specific date range (inclusive).
1396 ///
1397 /// # Arguments
1398 ///
1399 /// * `start` - Start of the date range (inclusive)
1400 /// * `end` - End of the date range (inclusive)
1401 ///
1402 /// # Returns
1403 ///
1404 /// Returns `true` if the current `DateTime` falls within the range, `false` otherwise.
1405 ///
1406 /// # Examples
1407 ///
1408 /// ```
1409 /// use dtt::datetime::DateTime;
1410 ///
1411 /// let dt = DateTime::new();
1412 /// let start = dt.add_days(-1).unwrap_or(dt);
1413 /// let end = dt.add_days(1).unwrap_or(dt);
1414 ///
1415 /// assert!(dt.is_within_range(&start, &end));
1416 /// ```
1417 #[must_use]
1418 pub fn is_within_range(&self, start: &Self, end: &Self) -> bool {
1419 self >= start && self <= end
1420 }
1421
1422 // -------------------------------------------------------------------------
1423 // Mutation Helpers
1424 // -------------------------------------------------------------------------
1425
1426 /// Sets the date components while maintaining the current time.
1427 ///
1428 /// # Arguments
1429 ///
1430 /// * `year` - Calendar year
1431 /// * `month` - Month (1-12)
1432 /// * `day` - Day of month (1-31)
1433 ///
1434 /// # Returns
1435 ///
1436 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1437 /// if the date is invalid.
1438 ///
1439 /// # Examples
1440 ///
1441 /// ```
1442 /// use dtt::datetime::DateTime;
1443 ///
1444 /// let dt = DateTime::new();
1445 /// let new_dt = dt.set_date(2024, 1, 1);
1446 /// assert!(new_dt.is_ok());
1447 /// ```
1448 ///
1449 /// # Errors
1450 ///
1451 /// Returns a `DateTimeError` if the resulting date would be invalid.
1452 ///
1453 pub fn set_date(
1454 &self,
1455 year: i32,
1456 month: u8,
1457 day: u8,
1458 ) -> Result<Self, DateTimeError> {
1459 let month = Month::try_from(month)
1460 .map_err(|_| DateTimeError::InvalidDate)?;
1461 let new_date = Date::from_calendar_date(year, month, day)
1462 .map_err(|_| DateTimeError::InvalidDate)?;
1463
1464 Ok(Self {
1465 datetime: PrimitiveDateTime::new(
1466 new_date,
1467 self.datetime.time(),
1468 ),
1469 offset: self.offset,
1470 })
1471 }
1472}
1473
1474// -----------------------------------------------------------------------------
1475// Standard Trait Implementations
1476// -----------------------------------------------------------------------------
1477
1478impl fmt::Display for DateTime {
1479 /// Formats the `DateTime` using RFC 3339 format.
1480 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1481 self.format_rfc3339()
1482 .map_or(Err(fmt::Error), |s| write!(f, "{s}"))
1483 }
1484}
1485
1486impl FromStr for DateTime {
1487 type Err = DateTimeError;
1488
1489 /// Parses a string into a `DateTime` instance (RFC 3339 or ISO 8601).
1490 fn from_str(s: &str) -> Result<Self, Self::Err> {
1491 Self::parse(s)
1492 }
1493}
1494
1495impl Default for DateTime {
1496 /// Returns the Unix epoch (1970-01-01T00:00:00Z) as the default value.
1497 ///
1498 /// `Default` is intentionally deterministic; for the current wall-clock
1499 /// time use [`DateTime::new`].
1500 fn default() -> Self {
1501 // Safe by construction: 1970-01-01 is a valid calendar date and
1502 // 00:00:00 is a valid time, so neither call can fail in practice.
1503 let date = Date::from_calendar_date(1970, Month::January, 1)
1504 .unwrap_or(Date::MIN);
1505 Self {
1506 datetime: PrimitiveDateTime::new(date, Time::MIDNIGHT),
1507 offset: UtcOffset::UTC,
1508 }
1509 }
1510}
1511
1512impl Add<Duration> for DateTime {
1513 type Output = Result<Self, DateTimeError>;
1514
1515 /// Adds a Duration to the `DateTime`.
1516 ///
1517 /// # Arguments
1518 ///
1519 /// * `rhs` - Duration to add
1520 ///
1521 /// # Returns
1522 ///
1523 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`.
1524 fn add(self, rhs: Duration) -> Self::Output {
1525 let maybe_new = self.datetime.checked_add(rhs);
1526 maybe_new.map_or(
1527 Err(DateTimeError::InvalidDate),
1528 |new_datetime| {
1529 Ok(Self {
1530 datetime: new_datetime,
1531 offset: self.offset,
1532 })
1533 },
1534 )
1535 }
1536}
1537
1538impl Sub<Duration> for DateTime {
1539 type Output = Result<Self, DateTimeError>;
1540
1541 /// Subtracts a Duration from the `DateTime`.
1542 ///
1543 /// # Arguments
1544 ///
1545 /// * `rhs` - Duration to subtract
1546 ///
1547 /// # Returns
1548 ///
1549 /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`.
1550 fn sub(self, rhs: Duration) -> Self::Output {
1551 let maybe_new = self.datetime.checked_sub(rhs);
1552 maybe_new.map_or(
1553 Err(DateTimeError::InvalidDate),
1554 |new_datetime| {
1555 Ok(Self {
1556 datetime: new_datetime,
1557 offset: self.offset,
1558 })
1559 },
1560 )
1561 }
1562}
1563
1564impl PartialEq for DateTime {
1565 /// Compares two `DateTime` values by their absolute instant (normalized to UTC).
1566 fn eq(&self, other: &Self) -> bool {
1567 self.cmp(other) == Ordering::Equal
1568 }
1569}
1570
1571impl Eq for DateTime {}
1572
1573impl PartialOrd for DateTime {
1574 /// Compares two `DateTime` for ordering, returning `Some(Ordering)`.
1575 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1576 Some(self.cmp(other))
1577 }
1578}
1579
1580impl Ord for DateTime {
1581 /// Compares two `DateTime` values by their absolute instant (normalized to UTC).
1582 fn cmp(&self, other: &Self) -> Ordering {
1583 let self_utc = self.datetime.assume_offset(self.offset);
1584 let other_utc = other.datetime.assume_offset(other.offset);
1585 self_utc.cmp(&other_utc)
1586 }
1587}
1588
1589impl Hash for DateTime {
1590 /// Computes a hash value for the `DateTime` based on its absolute UTC instant.
1591 fn hash<H: Hasher>(&self, state: &mut H) {
1592 self.datetime
1593 .assume_offset(self.offset)
1594 .unix_timestamp()
1595 .hash(state);
1596 self.datetime
1597 .assume_offset(self.offset)
1598 .nanosecond()
1599 .hash(state);
1600 }
1601}
1602
1603// -----------------------------------------------------------------------------
1604// Helper Functions
1605// -----------------------------------------------------------------------------
1606
1607/// Helper function to determine the number of days in a given month and year.
1608///
1609/// # Arguments
1610///
1611/// * `year` - Calendar year
1612/// * `month` - Month number (1-12)
1613///
1614/// # Returns
1615///
1616/// Returns a `Result` containing either the number of days or a `DateTimeError`.
1617///
1618/// # Errors
1619///
1620/// Returns a `DateTimeError` if the day in the month is invalid.
1621///
1622pub const fn days_in_month(
1623 year: i32,
1624 month: u8,
1625) -> Result<u8, DateTimeError> {
1626 match month {
1627 1 | 3 | 5 | 7 | 8 | 10 | 12 => Ok(31),
1628 4 | 6 | 9 | 11 => Ok(30),
1629 2 => Ok(if is_leap_year(year) { 29 } else { 28 }),
1630 _ => Err(DateTimeError::InvalidDate),
1631 }
1632}
1633
1634/// Helper function to determine if a year is a leap year.
1635///
1636/// # Arguments
1637///
1638/// * `year` - Calendar year to check
1639///
1640/// # Returns
1641///
1642/// Returns `true` if the year is a leap year, `false` otherwise.
1643///
1644/// # Examples
1645///
1646/// ```
1647/// use dtt::datetime::is_leap_year;
1648///
1649/// assert!(is_leap_year(2024));
1650/// assert!(!is_leap_year(2023));
1651/// assert!(is_leap_year(2000));
1652/// assert!(!is_leap_year(1900));
1653/// ```
1654#[must_use]
1655pub const fn is_leap_year(year: i32) -> bool {
1656 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
1657}