datetime/
lib.rs

1//! `datetime-rs` provides a representation of a date and time.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::str::FromStr;
6use std::time::SystemTime;
7
8use format::FormattedDateTime;
9use strptime::ParseError;
10use strptime::ParseResult;
11use strptime::Parser;
12use strptime::RawDateTime;
13
14#[macro_export]
15macro_rules! datetime {
16  ($y:literal-$m:literal-$d:literal $h:literal : $mi:literal : $s:literal) => {{
17    #[allow(clippy::zero_prefixed_literal)]
18    {
19      $crate::DateTime::ymd($y, $m, $d).hms($h, $mi, $s).build()
20    }
21  }};
22  ($y:literal-$m:literal-$d:literal $h:literal : $mi:literal : $s:literal $($tz:ident)::+) => {{
23    #[cfg(feature = "tz")]
24    #[allow(clippy::zero_prefixed_literal)]
25    {
26      match $crate::DateTime::ymd($y, $m, $d).hms($h, $mi, $s).tz($crate::tz::$($tz)::+) {
27        Ok(dt) => dt.build(),
28        Err(_) => panic!("invalid date/time and time zone combination"),
29      }
30    }
31    #[cfg(not(feature = "tz"))]
32    {
33      compile_error!("The `tz` feature must be enabled to specify a time zone.");
34    }
35  }};
36}
37
38#[cfg(feature = "diesel-pg")]
39mod db;
40mod format;
41pub mod interval;
42#[cfg(feature = "serde")]
43mod serde;
44
45pub use date::Date;
46pub use date::Weekday;
47pub use date::date;
48
49/// Time zone compnents (re-exported from `date-rs` crate).
50#[cfg(feature = "tz")]
51pub mod tz {
52  pub use date::tz::*;
53
54  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
55  pub(crate) enum TimeZone {
56    Unspecified,
57    Tz(::tz::TimeZoneRef<'static>),
58    FixedOffset(i32),
59  }
60
61  impl TimeZone {
62    pub(crate) const fn ut_offset(&self, timestamp: i64) -> TzResult<i32> {
63      match self {
64        Self::Unspecified => Ok(0),
65        Self::FixedOffset(offset) => Ok(*offset),
66        Self::Tz(tz) => match tz.find_local_time_type(timestamp) {
67          Ok(t) => Ok(t.ut_offset()),
68          Err(e) => Err(e),
69        },
70      }
71    }
72  }
73}
74
75/// A representation of a date and time.
76#[derive(Clone, Copy, Eq)]
77#[cfg_attr(feature = "diesel-pg", derive(diesel::AsExpression, diesel::FromSqlRow))]
78#[cfg_attr(feature = "diesel-pg", diesel(
79    sql_type = diesel::sql_types::Timestamp,
80    sql_type = diesel::sql_types::Timestamptz))]
81pub struct DateTime {
82  seconds: i64,
83  nanos: u32,
84  #[cfg(feature = "tz")]
85  tz: tz::TimeZone,
86}
87
88impl DateTime {
89  /// Create a new date and time object.
90  pub const fn ymd(year: i16, month: u8, day: u8) -> DateTimeBuilder {
91    DateTimeBuilder {
92      date: Date::new(year, month, day),
93      seconds: 0,
94      nanos: 0,
95      #[cfg(feature = "tz")]
96      tz: tz::TimeZone::Unspecified,
97      offset: 0,
98    }
99  }
100
101  /// Create a new date and time object from the given Unix timestamp.
102  pub const fn from_timestamp(timestamp: i64, nanos: u32) -> Self {
103    let mut timestamp = timestamp;
104    let mut nanos = nanos;
105    while nanos >= 1_000_000_000 {
106      nanos -= 1_000_000_000;
107      timestamp += 1;
108    }
109    Self {
110      seconds: timestamp,
111      nanos,
112      #[cfg(feature = "tz")]
113      tz: tz::TimeZone::Unspecified,
114    }
115  }
116
117  /// Return the current timestamp.
118  ///
119  /// ## Panic
120  ///
121  /// Panics if the system clock is set prior to January 1, 1970.
122  pub fn now() -> Self {
123    let dur = SystemTime::now()
124      .duration_since(SystemTime::UNIX_EPOCH)
125      .expect("System clock set prior to January 1, 1970");
126    Self::from_timestamp(dur.as_secs() as i64, dur.subsec_nanos())
127  }
128}
129
130#[cfg(feature = "tz")]
131impl DateTime {
132  /// Set the time zone to the provided time zone, without adjusting the underlying absolute
133  /// timestamp.
134  ///
135  /// This method modifies the wall clock time while maintaining the underlying absolute timestamp.
136  /// To modify the timestamp instead, use `in_tz`.
137  #[inline]
138  pub const fn with_tz(mut self, tz: tz::TimeZoneRef<'static>) -> Self {
139    self.tz = tz::TimeZone::Tz(tz);
140    self
141  }
142
143  /// Set the timestamp to the same wall clock time in the provided time zone.
144  ///
145  /// This method modifies the underlying timestamp while maintaining the wall clock time.
146  /// To maintain the timestamp instead, use `with_tz`.
147  #[inline]
148  pub const fn in_tz(mut self, tz: tz::TimeZoneRef<'static>) -> Self {
149    let existing_ut_offset = match self.tz.ut_offset(self.seconds) {
150      Ok(offset) => offset as i64,
151      Err(_) => panic!("Invalid time zone."),
152    };
153    let desired_ut_offset = match tz.find_local_time_type(self.seconds) {
154      Ok(t) => t.ut_offset() as i64,
155      Err(_) => panic!("Invalid time zone for this timestamp."),
156    };
157    self.seconds += existing_ut_offset - desired_ut_offset;
158    self.tz = tz::TimeZone::Tz(tz);
159    self
160  }
161}
162
163/// Accessors
164impl DateTime {
165  /// The year for this date.
166  #[inline]
167  pub const fn year(&self) -> i16 {
168    Date::from_timestamp(self.tz_adjusted_seconds()).year()
169  }
170
171  /// The month for this date.
172  #[inline]
173  pub const fn month(&self) -> u8 {
174    Date::from_timestamp(self.tz_adjusted_seconds()).month()
175  }
176
177  /// The day of the month for this date.
178  #[inline]
179  pub const fn day(&self) -> u8 {
180    Date::from_timestamp(self.tz_adjusted_seconds()).day()
181  }
182
183  /// The day of the week for this date.
184  #[inline]
185  pub const fn weekday(&self) -> Weekday {
186    Date::from_timestamp(self.tz_adjusted_seconds()).weekday()
187  }
188
189  /// The hour of the day for this date and time. Range: `[0, 24)`
190  #[inline]
191  pub const fn hour(&self) -> u8 {
192    (self.tz_adjusted_seconds() % 86_400 / 3_600) as u8
193  }
194
195  /// The minute of the hour for this date and time. Range: `[0, 60)`
196  #[inline]
197  pub const fn minute(&self) -> u8 {
198    ((self.tz_adjusted_seconds() % 3600) / 60) as u8
199  }
200
201  /// The second of the minute for this date and time. Range: `[0, 60)`
202  #[inline]
203  pub const fn second(&self) -> u8 {
204    (self.tz_adjusted_seconds() % 60) as u8
205  }
206
207  /// The nanosecond of the second for this date and time. Range: `[0, 1_000_000_000)`
208  #[inline]
209  pub const fn nanosecond(&self) -> u32 {
210    self.nanos
211  }
212
213  /// The ordinal day of the year.
214  #[inline]
215  pub const fn day_of_year(&self) -> u16 {
216    self.date().day_of_year()
217  }
218
219  /// The date corresponding to this datetime.
220  #[inline]
221  pub const fn date(&self) -> Date {
222    Date::from_timestamp(self.tz_adjusted_seconds())
223  }
224
225  /// The number of seconds since the Unix epoch for this date and time.
226  #[inline]
227  pub const fn as_seconds(&self) -> i64 {
228    self.seconds
229  }
230
231  /// The number of milliseconds since the Unix epoch for this date and time.
232  #[inline]
233  pub const fn as_milliseconds(&self) -> i64 {
234    self.seconds * 1_000 + (self.nanos / 1_000_000) as i64
235  }
236
237  /// The number of microseconds since the Unix epoch for this date and time.
238  #[inline]
239  pub const fn as_microseconds(&self) -> i64 {
240    self.seconds * 1_000_000 + (self.nanos / 1_000) as i64
241  }
242
243  /// The number of nanoseconds since the Unix epoch for this date and time.
244  #[inline]
245  pub const fn as_nanoseconds(&self) -> i128 {
246    self.seconds as i128 * 1_000_000_000 + self.nanos as i128
247  }
248
249  /// Provide the number of seconds since the epoch in the time zone with the same offset as this
250  /// datetime's time zone.
251  #[inline(always)]
252  const fn tz_adjusted_seconds(&self) -> i64 {
253    self.seconds + self.tz_offset()
254  }
255
256  /// Provide the offset, in seconds
257  const fn tz_offset(&self) -> i64 {
258    #[cfg(feature = "tz")]
259    {
260      match self.tz.ut_offset(self.seconds) {
261        Ok(offset) => offset as i64,
262        Err(_) => panic!("Invalid time zone"),
263      }
264    }
265    #[cfg(not(feature = "tz"))]
266    0
267  }
268}
269
270impl DateTime {
271  /// Format the given date and time according to the provided `strftime`-like string.
272  pub fn format(&self, format: &'static str) -> FormattedDateTime {
273    FormattedDateTime { dt: self, format }
274  }
275}
276
277impl DateTime {
278  /// Parse a date from a string, according to the provided format string.
279  pub fn parse(datetime_str: impl AsRef<str>, fmt: &'static str) -> ParseResult<Self> {
280    let parser = Parser::new(fmt);
281    parser.parse(datetime_str)?.try_into()
282  }
283}
284
285impl PartialEq for DateTime {
286  fn eq(&self, other: &Self) -> bool {
287    self.seconds == other.seconds && self.nanos == other.nanos
288  }
289}
290
291impl PartialOrd for DateTime {
292  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
293    Some(self.cmp(other))
294  }
295}
296
297impl Ord for DateTime {
298  fn cmp(&self, other: &Self) -> Ordering {
299    let seconds_cmp = self.seconds.cmp(&other.seconds);
300    match seconds_cmp {
301      Ordering::Equal => self.nanos.cmp(&other.nanos),
302      _ => seconds_cmp,
303    }
304  }
305}
306
307impl FromStr for DateTime {
308  type Err = ParseError;
309
310  #[rustfmt::skip]
311  fn from_str(s: &str) -> ParseResult<Self> {
312    // Attempt several common formats.
313    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S").parse(s) { return dt.try_into(); }
314    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%z").parse(s) { return dt.try_into(); }
315    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S").parse(s) { return dt.try_into(); }
316    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%z").parse(s) { return dt.try_into(); }
317    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.6f").parse(s) { return dt.try_into(); }
318    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.6f%z").parse(s) { return dt.try_into(); }
319    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.6f").parse(s) { return dt.try_into(); }
320    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.6f%z").parse(s) { return dt.try_into(); }
321    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.9f").parse(s) { return dt.try_into(); }
322    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.9f%z").parse(s) { return dt.try_into(); }
323    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.9f").parse(s) { return dt.try_into(); }
324    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.9f%z").parse(s) { return dt.try_into(); }
325    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%SZ").parse(s) { return dt.try_into(); }
326    Parser::new("%Y-%m-%dT%H:%M:%SZ").parse(s)?.try_into()
327  }
328}
329
330impl TryFrom<RawDateTime> for DateTime {
331  type Error = ParseError;
332
333  fn try_from(value: RawDateTime) -> ParseResult<Self> {
334    let date = value.date()?;
335    let time = value.time().unwrap_or_default();
336    Ok(match time.utc_offset() {
337      #[cfg(feature = "tz")]
338      Some(utc_offset) => Self::ymd(date.year(), date.month(), date.day())
339        .hms(time.hour(), time.minute(), time.second())
340        .nanos(time.nanosecond() as u32)
341        .utc_offset(utc_offset)
342        .build(),
343      #[cfg(not(feature = "tz"))]
344      Some(_) => panic!("Enable the `tz` feature to parse datetimes with UTC offsets."),
345      None => Self::ymd(date.year(), date.month(), date.day())
346        .hms(time.hour(), time.minute(), time.second())
347        .nanos(time.nanosecond() as u32)
348        .build(),
349    })
350  }
351}
352
353impl fmt::Debug for DateTime {
354  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355    if self.nanos == 0 {
356      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S"))
357    } else if self.nanos % 1_000_000 == 0 {
358      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.3f"))
359    } else if self.nanos % 1_000 == 0 {
360      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.6f"))
361    } else {
362      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.9f"))
363    }
364  }
365}
366
367#[cfg(feature = "log")]
368impl log::kv::ToValue for DateTime {
369  fn to_value(&self) -> log::kv::Value<'_> {
370    log::kv::Value::from_debug(self)
371  }
372}
373
374/// An intermediate builder for [`DateTime`].
375#[must_use]
376pub struct DateTimeBuilder {
377  date: Date,
378  seconds: i64,
379  nanos: u32,
380  #[cfg(feature = "tz")]
381  tz: tz::TimeZone,
382  offset: i64,
383}
384
385impl DateTimeBuilder {
386  /// Attach an hour, minute, and second to the datetime.
387  pub const fn hms(mut self, hour: u8, minute: u8, second: u8) -> Self {
388    assert!(hour < 24, "Hour out of bounds");
389    assert!(minute < 60, "Minute out of bounds");
390    assert!(second < 60, "Second out of bounds");
391    self.seconds = (hour as i64 * 3600) + (minute as i64 * 60) + second as i64;
392    self
393  }
394
395  /// Attach fractional to the datetime.
396  pub const fn nanos(mut self, nanos: u32) -> Self {
397    assert!(nanos < 1_000_000_000, "Nanos out of bounds.");
398    self.nanos = nanos;
399    self
400  }
401
402  /// Attach a timezone to the datetime.
403  ///
404  /// This method assumes that the timezone _modifies_ the underlying timestamp; in other words,
405  /// the YMD/HMS specified to the date and time builder should be preserved, and the time zone's
406  /// offset applied to the underlying timestamp to preserve the date and time on the wall clock.
407  #[cfg(feature = "tz")]
408  pub const fn tz(mut self, tz: tz::TimeZoneRef<'static>) -> tz::TzResult<Self> {
409    self.offset = match tz.find_local_time_type(self.date.timestamp() + self.seconds) {
410      Ok(t) => t.ut_offset() as i64,
411      Err(e) => return Err(e),
412    };
413    self.tz = tz::TimeZone::Tz(tz);
414    Ok(self)
415  }
416
417  /// Attach a UTC offset to the datetime.
418  ///
419  /// This method assumes that the offset _modifies_ the underlying timestamp; in other words, the
420  /// YMD/HMS specified to the date and time builder should be preserved, and the offset applied to
421  /// the underlying timestamp to preserve the date and time on the wall clock.
422  #[cfg(feature = "tz")]
423  pub(crate) const fn utc_offset(mut self, offset: i32) -> Self {
424    self.offset = offset as i64;
425    self.tz = tz::TimeZone::FixedOffset(offset);
426    self
427  }
428
429  /// Build the final [`DateTime`] object.
430  pub const fn build(self) -> DateTime {
431    DateTime {
432      seconds: self.date.timestamp() + self.seconds - self.offset,
433      nanos: self.nanos,
434      #[cfg(feature = "tz")]
435      tz: self.tz,
436    }
437  }
438}
439
440trait Sealed {}
441impl Sealed for date::Date {}
442
443/// Convert from a date into a datetime, by way of a builder.
444#[allow(private_bounds)]
445pub trait FromDate: Sealed {
446  /// Create a `DateTimeBuilder` for this Date.
447  fn hms(self, hour: u8, minute: u8, second: u8) -> DateTimeBuilder;
448}
449
450impl FromDate for date::Date {
451  fn hms(self, hour: u8, minute: u8, second: u8) -> DateTimeBuilder {
452    DateTimeBuilder {
453      date: self,
454      seconds: 0,
455      nanos: 0,
456      #[cfg(feature = "tz")]
457      tz: tz::TimeZone::Unspecified,
458      offset: 0,
459    }
460    .hms(hour, minute, second)
461  }
462}
463
464#[cfg(test)]
465mod tests {
466  use assert2::check;
467  use strptime::ParseResult;
468
469  use crate::DateTime;
470  use crate::FromDate;
471  #[cfg(feature = "tz")]
472  use crate::tz;
473
474  #[test]
475  fn test_zero() {
476    let dt = datetime! { 1970-01-01 00:00:00 };
477    check!(dt.seconds == 0);
478  }
479
480  #[test]
481  fn test_accessors() {
482    let dt = datetime! { 2012-04-21 11:00:00 };
483    check!(dt.year() == 2012);
484    check!(dt.month() == 4);
485    check!(dt.day() == 21);
486    check!(dt.hour() == 11);
487    check!(dt.minute() == 0);
488    check!(dt.second() == 0);
489  }
490
491  #[test]
492  fn test_more_accessors() {
493    let dt = datetime! { 2024-02-29 13:15:45 };
494    check!(dt.year() == 2024);
495    check!(dt.month() == 2);
496    check!(dt.day() == 29);
497    check!(dt.hour() == 13);
498    check!(dt.minute() == 15);
499    check!(dt.second() == 45);
500  }
501
502  #[test]
503  fn test_parse_str() -> ParseResult<()> {
504    for s in [
505      "2012-04-21 11:00:00",
506      "2012-04-21T11:00:00",
507      "2012-04-21 11:00:00.000000",
508      "2012-04-21 11:00:00Z",
509      "2012-04-21T11:00:00.000000",
510      "2012-04-21T11:00:00Z",
511    ] {
512      let dt = s.parse::<DateTime>()?;
513      check!(dt.year() == 2012);
514      check!(dt.month() == 4);
515      check!(dt.day() == 21);
516      check!(dt.hour() == 11);
517    }
518
519    Ok(())
520  }
521
522  #[test]
523  #[cfg(feature = "tz")]
524  fn test_parse_str_tz() -> ParseResult<()> {
525    for s in
526      ["2012-04-21 11:00:00-0400", "2012-04-21T11:00:00-0400", "2012-04-21 11:00:00.000000-0400"]
527    {
528      let dt = s.parse::<DateTime>()?;
529      check!(dt.year() == 2012);
530      check!(dt.month() == 4);
531      check!(dt.day() == 21);
532      check!(dt.hour() == 11);
533    }
534    Ok(())
535  }
536
537  #[test]
538  #[allow(clippy::inconsistent_digit_grouping)]
539  fn test_precision() {
540    let dt = DateTime::ymd(2012, 4, 21).hms(15, 0, 0).build();
541    check!(dt.as_seconds() == 1335020400);
542    check!(dt.as_milliseconds() == 1335020400_000);
543    check!(dt.as_microseconds() == 1335020400_000_000);
544    check!(dt.as_nanoseconds() == 1335020400_000_000_000);
545  }
546
547  #[cfg(feature = "tz")]
548  #[test]
549  fn test_tz() -> tz::TzResult<()> {
550    let dt = DateTime::ymd(2012, 4, 21).hms(11, 0, 0).tz(tz::us::EASTERN)?.build();
551    check!(dt.as_seconds() == 1335020400);
552    check!(dt.year() == 2012);
553    check!(dt.month() == 4);
554    check!(dt.day() == 21);
555    check!(dt.hour() == 11);
556    let dt = DateTime::ymd(1970, 1, 1).tz(tz::us::PACIFIC)?.build();
557    check!(dt.as_seconds() == 3600 * 8);
558    Ok(())
559  }
560
561  #[cfg(feature = "tz")]
562  #[test]
563  fn test_unix_tz() {
564    let dt = DateTime::from_timestamp(1335020400, 0).with_tz(tz::us::EASTERN);
565    check!(dt.as_seconds() == 1335020400);
566    check!(dt.year() == 2012);
567    check!(dt.month() == 4);
568    check!(dt.day() == 21);
569    check!(dt.hour() == 11);
570  }
571
572  #[cfg(feature = "tz")]
573  #[test]
574  fn test_in_tz() {
575    let dt = DateTime::from_timestamp(1335020400, 0).with_tz(tz::us::EASTERN);
576    check!(dt.hour() == 11);
577    check!(dt.in_tz(tz::us::CENTRAL).hour() == 11);
578    check!(dt.as_seconds() - dt.in_tz(tz::us::CENTRAL).as_seconds() == -3600);
579    check!(dt.in_tz(tz::europe::LONDON).hour() == 11);
580    check!(dt.as_seconds() - dt.in_tz(tz::europe::LONDON).as_seconds() == 3600 * 5);
581  }
582
583  #[test]
584  fn test_from_date_trait() {
585    let dt = date::date! { 2012-04-21 }.hms(11, 0, 0).build();
586    check!(dt.year() == 2012);
587    check!(dt.month() == 4);
588    check!(dt.day() == 21);
589    check!(dt.hour() == 11);
590  }
591
592  #[test]
593  fn test_debug() {
594    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).build();
595    check!(format!("{:?}", dt) == "2012-04-21 15:00:00");
596    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(500_000_000).build();
597    check!(format!("{:?}", dt) == "2012-04-21 15:00:00.500");
598    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(123_450_000).build();
599    check!(format!("{:?}", dt) == "2012-04-21 15:00:00.123450");
600    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(123_456_789).build();
601    check!(format!("{:?}", dt) == "2012-04-21 15:00:00.123456789");
602  }
603}