Skip to main content

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}