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 = format_description::parse(format)
884            .map_err(|_| DateTimeError::InvalidFormat)?;
885        let datetime = PrimitiveDateTime::parse(input, &format_desc)
886            .map_err(|_| DateTimeError::InvalidFormat)?;
887
888        Ok(Self {
889            datetime,
890            offset: UtcOffset::UTC,
891        })
892    }
893
894    // -------------------------------------------------------------------------
895    // Formatting Methods
896    // -------------------------------------------------------------------------
897
898    /// Formats the `DateTime` according to the specified format string.
899    ///
900    /// # Arguments
901    ///
902    /// * `format_str` - Format specification string (see `time` crate documentation)
903    ///
904    /// # Returns
905    ///
906    /// Returns a `Result` containing either the formatted string or a `DateTimeError`
907    /// if formatting fails.
908    ///
909    /// # Examples
910    ///
911    /// ```
912    /// use dtt::datetime::DateTime;
913    ///
914    /// let dt = DateTime::new();
915    /// let formatted = dt.format("[year]-[month]-[day]");
916    /// assert!(formatted.is_ok());
917    /// ```
918    ///
919    /// # Errors
920    ///
921    /// Returns a `DateTimeError` if the format string is invalid.
922    ///
923    pub fn format(
924        &self,
925        format_str: &str,
926    ) -> Result<String, DateTimeError> {
927        let format_desc = format_description::parse(format_str)
928            .map_err(|_| DateTimeError::InvalidFormat)?;
929        self.datetime
930            .format(&format_desc)
931            .map_err(|_| DateTimeError::InvalidFormat)
932    }
933
934    /// Formats the `DateTime` as an RFC 3339 string.
935    ///
936    /// # Returns
937    ///
938    /// Returns a `Result` containing either the formatted RFC 3339 string
939    /// or a `DateTimeError` if formatting fails.
940    ///
941    /// # Examples
942    ///
943    /// ```
944    /// use dtt::datetime::DateTime;
945    ///
946    /// let dt = DateTime::new();
947    /// let maybe_rfc3339 = dt.format_rfc3339();
948    /// assert!(maybe_rfc3339.is_ok());
949    /// ```
950    ///
951    /// # Errors
952    ///
953    /// Returns a `DateTimeError` if formatting fails.
954    ///
955    pub fn format_rfc3339(&self) -> Result<String, DateTimeError> {
956        self.datetime
957            .assume_offset(self.offset)
958            .format(&format_description::well_known::Rfc3339)
959            .map_err(|_| DateTimeError::InvalidFormat)
960    }
961
962    /// Updates the `DateTime` to the current time while preserving the timezone offset.
963    ///
964    /// # Returns
965    ///
966    /// Returns a `Result` containing either the updated `DateTime` or a `DateTimeError`
967    /// if the update fails.
968    ///
969    /// # Examples
970    ///
971    /// ```
972    /// use dtt::datetime::DateTime;
973    /// use std::thread::sleep;
974    /// use std::time::Duration;
975    ///
976    /// let dt = DateTime::new();
977    /// sleep(Duration::from_secs(1));
978    /// let updated_dt = dt.update();
979    /// assert!(updated_dt.is_ok());
980    /// ```
981    ///
982    /// # Errors
983    ///
984    /// Returns a `DateTimeError` if the update fails.
985    ///
986    pub fn update(&self) -> Result<Self, DateTimeError> {
987        let now = OffsetDateTime::now_utc().to_offset(self.offset);
988        Ok(Self {
989            datetime: PrimitiveDateTime::new(now.date(), now.time()),
990            offset: self.offset,
991        })
992    }
993
994    // -------------------------------------------------------------------------
995    // Timezone Conversion Method
996    // -------------------------------------------------------------------------
997
998    /// Converts the current `DateTime` to another timezone.
999    ///
1000    /// # Arguments
1001    ///
1002    /// * `new_tz` - Target timezone abbreviation (e.g., "UTC", "`EST_USA`", "PST")
1003    ///
1004    /// # Returns
1005    ///
1006    /// Returns a `Result` containing either the `DateTime` in the new timezone
1007    /// or a `DateTimeError` if the conversion fails.
1008    ///
1009    /// # Examples
1010    ///
1011    /// ```
1012    /// use dtt::datetime::DateTime;
1013    ///
1014    /// let utc = DateTime::new();
1015    /// let maybe_est = utc.convert_to_tz("EST_USA");
1016    /// assert!(maybe_est.is_ok());
1017    /// ```
1018    ///
1019    /// # Errors
1020    ///
1021    /// Returns a `DateTimeError` if the timezone is invalid.
1022    ///
1023    pub fn convert_to_tz(
1024        &self,
1025        new_tz: &str,
1026    ) -> Result<Self, DateTimeError> {
1027        let new_offset = TIMEZONE_OFFSETS
1028            .get(new_tz)
1029            .ok_or(DateTimeError::InvalidTimezone)?
1030            .as_ref()
1031            .map_err(Clone::clone)?;
1032
1033        let datetime_with_offset =
1034            self.datetime.assume_offset(self.offset);
1035        let new_datetime = datetime_with_offset.to_offset(*new_offset);
1036
1037        Ok(Self {
1038            datetime: PrimitiveDateTime::new(
1039                new_datetime.date(),
1040                new_datetime.time(),
1041            ),
1042            offset: *new_offset,
1043        })
1044    }
1045
1046    // -------------------------------------------------------------------------
1047    // Additional Utilities
1048    // -------------------------------------------------------------------------
1049
1050    /// Gets the Unix timestamp (seconds since Unix epoch).
1051    ///
1052    /// # Returns
1053    ///
1054    /// Returns the number of seconds from the Unix epoch (1970-01-01T00:00:00Z).
1055    ///
1056    /// # Examples
1057    ///
1058    /// ```
1059    /// use dtt::datetime::DateTime;
1060    ///
1061    /// let dt = DateTime::new();
1062    /// let ts = dt.unix_timestamp();
1063    /// ```
1064    #[must_use]
1065    pub const fn unix_timestamp(&self) -> i64 {
1066        self.datetime.assume_offset(self.offset).unix_timestamp()
1067    }
1068
1069    /// Calculates the duration between this `DateTime` and another.
1070    ///
1071    /// The result can be negative if `other` is later than `self`.
1072    ///
1073    /// # Arguments
1074    ///
1075    /// * `other` - The `DateTime` to compare with
1076    ///
1077    /// # Returns
1078    ///
1079    /// Returns a `Duration` representing the time difference.
1080    ///
1081    /// # Examples
1082    ///
1083    /// ```
1084    /// use dtt::datetime::DateTime;
1085    ///
1086    /// let dt1 = DateTime::new();
1087    /// let dt2 = dt1.add_days(1).unwrap_or(dt1);
1088    /// let duration = dt1.duration_since(&dt2);
1089    /// // duration could be negative if dt2 > dt1
1090    /// ```
1091    #[must_use]
1092    pub fn duration_since(&self, other: &Self) -> Duration {
1093        let self_offset = self.datetime.assume_offset(self.offset);
1094        let other_offset = other.datetime.assume_offset(other.offset);
1095
1096        let seconds_diff = self_offset.unix_timestamp()
1097            - other_offset.unix_timestamp();
1098        let nanos_diff = i64::from(self_offset.nanosecond())
1099            - i64::from(other_offset.nanosecond());
1100
1101        Duration::seconds(seconds_diff)
1102            + Duration::nanoseconds(nanos_diff)
1103    }
1104
1105    // -------------------------------------------------------------------------
1106    // Date Arithmetic Methods
1107    // -------------------------------------------------------------------------
1108
1109    /// Adds a specified number of days to the `DateTime`.
1110    ///
1111    /// # Arguments
1112    ///
1113    /// * `days` - Number of days to add (can be negative for subtraction)
1114    ///
1115    /// # Returns
1116    ///
1117    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1118    /// if the operation would result in an invalid date.
1119    ///
1120    /// # Errors
1121    ///
1122    /// This function returns a [`DateTimeError::InvalidDate`] if adding `days` results
1123    /// in a date overflow or otherwise invalid date.
1124    ///
1125    /// # Examples
1126    ///
1127    /// ```
1128    /// use dtt::datetime::DateTime;
1129    ///
1130    /// let dt = DateTime::new();
1131    /// let future = dt.add_days(7);
1132    /// assert!(future.is_ok());
1133    /// ```
1134    pub fn add_days(&self, days: i64) -> Result<Self, DateTimeError> {
1135        let new_datetime = self
1136            .datetime
1137            .checked_add(Duration::days(days))
1138            .ok_or(DateTimeError::InvalidDate)?;
1139
1140        Ok(Self {
1141            datetime: new_datetime,
1142            offset: self.offset,
1143        })
1144    }
1145
1146    /// Adds a specified number of months to the `DateTime`.
1147    ///
1148    /// Handles month-end dates and leap years appropriately.
1149    ///
1150    /// # Arguments
1151    ///
1152    /// * `months` - Number of months to add (can be negative for subtraction)
1153    ///
1154    /// # Returns
1155    ///
1156    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1157    /// if the operation would result in an invalid date.
1158    ///
1159    /// # Errors
1160    ///
1161    /// This function returns a [`DateTimeError`] if:
1162    /// - The calculated year, month, or day is invalid (e.g., out of range).
1163    /// - The underlying date library fails to construct a valid date.
1164    ///
1165    /// # Examples
1166    ///
1167    /// ```
1168    /// use dtt::datetime::DateTime;
1169    ///
1170    /// let dt = DateTime::new();
1171    /// let future = dt.add_months(3);
1172    /// assert!(future.is_ok());
1173    /// ```
1174    pub fn add_months(
1175        &self,
1176        months: i32,
1177    ) -> Result<Self, DateTimeError> {
1178        let current_date = self.datetime.date();
1179        let total_months = current_date
1180            .year()
1181            .checked_mul(12)
1182            .and_then(|v| {
1183                v.checked_add(i32::from(current_date.month() as u8))
1184            })
1185            .and_then(|v| v.checked_sub(1))
1186            .and_then(|v| v.checked_add(months))
1187            .ok_or(DateTimeError::InvalidDate)?;
1188
1189        let target_year = total_months.div_euclid(12);
1190        let target_month =
1191            u8::try_from(total_months.rem_euclid(12) + 1);
1192
1193        let target_month =
1194            target_month.map_err(|_| DateTimeError::InvalidDate)?;
1195        let days_in_target_month =
1196            days_in_month(target_year, target_month)?;
1197        let target_day = current_date.day().min(days_in_target_month);
1198
1199        let new_month = Month::try_from(target_month)
1200            .map_err(|_| DateTimeError::InvalidDate)?;
1201        let new_date = Date::from_calendar_date(
1202            target_year,
1203            new_month,
1204            target_day,
1205        )
1206        .map_err(|_| DateTimeError::InvalidDate)?;
1207
1208        Ok(Self {
1209            datetime: PrimitiveDateTime::new(
1210                new_date,
1211                self.datetime.time(),
1212            ),
1213            offset: self.offset,
1214        })
1215    }
1216
1217    /// Subtracts a specified number of months from the `DateTime`.
1218    ///
1219    /// # Arguments
1220    ///
1221    /// * `months` - Number of months to subtract
1222    ///
1223    /// # Returns
1224    ///
1225    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1226    /// if the operation would result in an invalid date.
1227    ///
1228    /// # Errors
1229    ///
1230    /// This function returns a [`DateTimeError::InvalidDate`] if:
1231    /// - The resulting date is out of valid range.
1232    /// - The underlying date library fails to construct a valid `DateTime`.
1233    ///
1234    /// # Examples
1235    ///
1236    /// ```
1237    /// use dtt::datetime::DateTime;
1238    ///
1239    /// let dt = DateTime::new();
1240    /// let past = dt.sub_months(3);
1241    /// assert!(past.is_ok());
1242    /// ```
1243    pub fn sub_months(
1244        &self,
1245        months: i32,
1246    ) -> Result<Self, DateTimeError> {
1247        self.add_months(-months)
1248    }
1249
1250    /// Adds a specified number of years to the `DateTime`.
1251    ///
1252    /// Handles leap-year transitions appropriately.
1253    ///
1254    /// # Arguments
1255    ///
1256    /// * `years` - Number of years to add (can be negative for subtraction)
1257    ///
1258    /// # Returns
1259    ///
1260    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1261    /// if the operation would result in an invalid date.
1262    ///
1263    /// # Errors
1264    ///
1265    /// This function returns a [`DateTimeError::InvalidDate`] if:
1266    /// - The resulting year is out of valid range.
1267    /// - A non-leap year cannot accommodate February 29th.
1268    /// - Any other invalid date scenario occurs during calculation.
1269    ///
1270    /// # Examples
1271    ///
1272    /// ```
1273    /// use dtt::datetime::DateTime;
1274    ///
1275    /// let dt = DateTime::new();
1276    /// let future = dt.add_years(5);
1277    /// assert!(future.is_ok());
1278    /// ```
1279    pub fn add_years(&self, years: i32) -> Result<Self, DateTimeError> {
1280        let current_date = self.datetime.date();
1281        let target_year = current_date
1282            .year()
1283            .checked_add(years)
1284            .ok_or(DateTimeError::InvalidDate)?;
1285
1286        // Handle February 29th in leap years
1287        let new_day = if current_date.month() == Month::February
1288            && current_date.day() == 29
1289            && !is_leap_year(target_year)
1290        {
1291            28
1292        } else {
1293            current_date.day()
1294        };
1295
1296        let new_date = Date::from_calendar_date(
1297            target_year,
1298            current_date.month(),
1299            new_day,
1300        )
1301        .map_err(|_| DateTimeError::InvalidDate)?;
1302
1303        Ok(Self {
1304            datetime: PrimitiveDateTime::new(
1305                new_date,
1306                self.datetime.time(),
1307            ),
1308            offset: self.offset,
1309        })
1310    }
1311
1312    // -------------------------------------------------------------------------
1313    // Range / Boundary Helper Methods
1314    // -------------------------------------------------------------------------
1315
1316    /// Returns a new `DateTime` for the start of the current week (Monday).
1317    ///
1318    /// # Errors
1319    ///
1320    /// This function can return a [`DateTimeError`] if an overflow or
1321    /// invalid date calculation occurs during date arithmetic.
1322    pub fn start_of_week(&self) -> Result<Self, DateTimeError> {
1323        let days_since_monday = i64::from(
1324            self.datetime.weekday().number_days_from_monday(),
1325        );
1326        self.add_days(-days_since_monday)
1327    }
1328
1329    /// Returns a new `DateTime` for the end of the current week (Sunday).
1330    ///
1331    /// # Errors
1332    ///
1333    /// This function can return a [`DateTimeError`] if an overflow or
1334    /// invalid date calculation occurs during date arithmetic.
1335    pub fn end_of_week(&self) -> Result<Self, DateTimeError> {
1336        let days_until_sunday = 6 - i64::from(
1337            self.datetime.weekday().number_days_from_monday(),
1338        );
1339        self.add_days(days_until_sunday)
1340    }
1341
1342    /// Returns a new `DateTime` for the start of the current month.
1343    ///
1344    /// # Errors
1345    ///
1346    /// This function can return a [`DateTimeError`] if the date cannot be
1347    /// constructed (e.g., due to an invalid year or month).
1348    pub fn start_of_month(&self) -> Result<Self, DateTimeError> {
1349        self.set_date(
1350            self.datetime.year(),
1351            self.datetime.month() as u8,
1352            1,
1353        )
1354    }
1355
1356    /// Returns a new `DateTime` for the end of the current month.
1357    ///
1358    /// # Errors
1359    ///
1360    /// This function can return a [`DateTimeError`] if the date cannot be
1361    /// constructed (e.g., `days_in_month` fails to provide a valid day).
1362    pub fn end_of_month(&self) -> Result<Self, DateTimeError> {
1363        let year = self.datetime.year();
1364        let month = self.datetime.month() as u8;
1365        let last_day = days_in_month(year, month)?;
1366        self.set_date(year, month, last_day)
1367    }
1368
1369    /// Returns a new `DateTime` for the start of the current year.
1370    ///
1371    /// # Errors
1372    ///
1373    /// This function can return a [`DateTimeError`] if the date cannot
1374    /// be constructed (e.g., invalid year).
1375    pub fn start_of_year(&self) -> Result<Self, DateTimeError> {
1376        self.set_date(self.datetime.year(), 1, 1)
1377    }
1378
1379    /// Returns a new `DateTime` for the end of the current year.
1380    ///
1381    /// # Errors
1382    ///
1383    /// This function can return a [`DateTimeError`] if the date cannot
1384    /// be constructed (e.g., invalid year).
1385    pub fn end_of_year(&self) -> Result<Self, DateTimeError> {
1386        self.set_date(self.datetime.year(), 12, 31)
1387    }
1388
1389    // -------------------------------------------------------------------------
1390    // Range Validation
1391    // -------------------------------------------------------------------------
1392
1393    /// Checks if the current `DateTime` falls within a specific date range (inclusive).
1394    ///
1395    /// # Arguments
1396    ///
1397    /// * `start` - Start of the date range (inclusive)
1398    /// * `end` - End of the date range (inclusive)
1399    ///
1400    /// # Returns
1401    ///
1402    /// Returns `true` if the current `DateTime` falls within the range, `false` otherwise.
1403    ///
1404    /// # Examples
1405    ///
1406    /// ```
1407    /// use dtt::datetime::DateTime;
1408    ///
1409    /// let dt = DateTime::new();
1410    /// let start = dt.add_days(-1).unwrap_or(dt);
1411    /// let end = dt.add_days(1).unwrap_or(dt);
1412    ///
1413    /// assert!(dt.is_within_range(&start, &end));
1414    /// ```
1415    #[must_use]
1416    pub fn is_within_range(&self, start: &Self, end: &Self) -> bool {
1417        self >= start && self <= end
1418    }
1419
1420    // -------------------------------------------------------------------------
1421    // Mutation Helpers
1422    // -------------------------------------------------------------------------
1423
1424    /// Sets the date components while maintaining the current time.
1425    ///
1426    /// # Arguments
1427    ///
1428    /// * `year` - Calendar year
1429    /// * `month` - Month (1-12)
1430    /// * `day` - Day of month (1-31)
1431    ///
1432    /// # Returns
1433    ///
1434    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`
1435    /// if the date is invalid.
1436    ///
1437    /// # Examples
1438    ///
1439    /// ```
1440    /// use dtt::datetime::DateTime;
1441    ///
1442    /// let dt = DateTime::new();
1443    /// let new_dt = dt.set_date(2024, 1, 1);
1444    /// assert!(new_dt.is_ok());
1445    /// ```
1446    ///
1447    /// # Errors
1448    ///
1449    /// Returns a `DateTimeError` if the resulting date would be invalid.
1450    ///
1451    pub fn set_date(
1452        &self,
1453        year: i32,
1454        month: u8,
1455        day: u8,
1456    ) -> Result<Self, DateTimeError> {
1457        let month = Month::try_from(month)
1458            .map_err(|_| DateTimeError::InvalidDate)?;
1459        let new_date = Date::from_calendar_date(year, month, day)
1460            .map_err(|_| DateTimeError::InvalidDate)?;
1461
1462        Ok(Self {
1463            datetime: PrimitiveDateTime::new(
1464                new_date,
1465                self.datetime.time(),
1466            ),
1467            offset: self.offset,
1468        })
1469    }
1470}
1471
1472// -----------------------------------------------------------------------------
1473// Standard Trait Implementations
1474// -----------------------------------------------------------------------------
1475
1476impl fmt::Display for DateTime {
1477    /// Formats the `DateTime` using RFC 3339 format.
1478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1479        self.format_rfc3339()
1480            .map_or(Err(fmt::Error), |s| write!(f, "{s}"))
1481    }
1482}
1483
1484impl FromStr for DateTime {
1485    type Err = DateTimeError;
1486
1487    /// Parses a string into a `DateTime` instance (RFC 3339 or ISO 8601).
1488    fn from_str(s: &str) -> Result<Self, Self::Err> {
1489        Self::parse(s)
1490    }
1491}
1492
1493impl Default for DateTime {
1494    /// Returns the Unix epoch (1970-01-01T00:00:00Z) as the default value.
1495    ///
1496    /// `Default` is intentionally deterministic; for the current wall-clock
1497    /// time use [`DateTime::new`].
1498    fn default() -> Self {
1499        // Safe by construction: 1970-01-01 is a valid calendar date and
1500        // 00:00:00 is a valid time, so neither call can fail in practice.
1501        let date = Date::from_calendar_date(1970, Month::January, 1)
1502            .unwrap_or(Date::MIN);
1503        Self {
1504            datetime: PrimitiveDateTime::new(date, Time::MIDNIGHT),
1505            offset: UtcOffset::UTC,
1506        }
1507    }
1508}
1509
1510impl Add<Duration> for DateTime {
1511    type Output = Result<Self, DateTimeError>;
1512
1513    /// Adds a Duration to the `DateTime`.
1514    ///
1515    /// # Arguments
1516    ///
1517    /// * `rhs` - Duration to add
1518    ///
1519    /// # Returns
1520    ///
1521    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`.
1522    fn add(self, rhs: Duration) -> Self::Output {
1523        let maybe_new = self.datetime.checked_add(rhs);
1524        maybe_new.map_or(
1525            Err(DateTimeError::InvalidDate),
1526            |new_datetime| {
1527                Ok(Self {
1528                    datetime: new_datetime,
1529                    offset: self.offset,
1530                })
1531            },
1532        )
1533    }
1534}
1535
1536impl Sub<Duration> for DateTime {
1537    type Output = Result<Self, DateTimeError>;
1538
1539    /// Subtracts a Duration from the `DateTime`.
1540    ///
1541    /// # Arguments
1542    ///
1543    /// * `rhs` - Duration to subtract
1544    ///
1545    /// # Returns
1546    ///
1547    /// Returns a `Result` containing either the new `DateTime` or a `DateTimeError`.
1548    fn sub(self, rhs: Duration) -> Self::Output {
1549        let maybe_new = self.datetime.checked_sub(rhs);
1550        maybe_new.map_or(
1551            Err(DateTimeError::InvalidDate),
1552            |new_datetime| {
1553                Ok(Self {
1554                    datetime: new_datetime,
1555                    offset: self.offset,
1556                })
1557            },
1558        )
1559    }
1560}
1561
1562impl PartialEq for DateTime {
1563    /// Compares two `DateTime` values by their absolute instant (normalized to UTC).
1564    fn eq(&self, other: &Self) -> bool {
1565        self.cmp(other) == Ordering::Equal
1566    }
1567}
1568
1569impl Eq for DateTime {}
1570
1571impl PartialOrd for DateTime {
1572    /// Compares two `DateTime` for ordering, returning `Some(Ordering)`.
1573    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1574        Some(self.cmp(other))
1575    }
1576}
1577
1578impl Ord for DateTime {
1579    /// Compares two `DateTime` values by their absolute instant (normalized to UTC).
1580    fn cmp(&self, other: &Self) -> Ordering {
1581        let self_utc = self.datetime.assume_offset(self.offset);
1582        let other_utc = other.datetime.assume_offset(other.offset);
1583        self_utc.cmp(&other_utc)
1584    }
1585}
1586
1587impl Hash for DateTime {
1588    /// Computes a hash value for the `DateTime` based on its absolute UTC instant.
1589    fn hash<H: Hasher>(&self, state: &mut H) {
1590        self.datetime
1591            .assume_offset(self.offset)
1592            .unix_timestamp()
1593            .hash(state);
1594        self.datetime
1595            .assume_offset(self.offset)
1596            .nanosecond()
1597            .hash(state);
1598    }
1599}
1600
1601// -----------------------------------------------------------------------------
1602// Helper Functions
1603// -----------------------------------------------------------------------------
1604
1605/// Helper function to determine the number of days in a given month and year.
1606///
1607/// # Arguments
1608///
1609/// * `year` - Calendar year
1610/// * `month` - Month number (1-12)
1611///
1612/// # Returns
1613///
1614/// Returns a `Result` containing either the number of days or a `DateTimeError`.
1615///
1616/// # Errors
1617///
1618/// Returns a `DateTimeError` if the day in the month is invalid.
1619///
1620pub const fn days_in_month(
1621    year: i32,
1622    month: u8,
1623) -> Result<u8, DateTimeError> {
1624    match month {
1625        1 | 3 | 5 | 7 | 8 | 10 | 12 => Ok(31),
1626        4 | 6 | 9 | 11 => Ok(30),
1627        2 => Ok(if is_leap_year(year) { 29 } else { 28 }),
1628        _ => Err(DateTimeError::InvalidDate),
1629    }
1630}
1631
1632/// Helper function to determine if a year is a leap year.
1633///
1634/// # Arguments
1635///
1636/// * `year` - Calendar year to check
1637///
1638/// # Returns
1639///
1640/// Returns `true` if the year is a leap year, `false` otherwise.
1641///
1642/// # Examples
1643///
1644/// ```
1645/// use dtt::datetime::is_leap_year;
1646///
1647/// assert!(is_leap_year(2024));
1648/// assert!(!is_leap_year(2023));
1649/// assert!(is_leap_year(2000));
1650/// assert!(!is_leap_year(1900));
1651/// ```
1652#[must_use]
1653pub const fn is_leap_year(year: i32) -> bool {
1654    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
1655}