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}