Skip to main content

datetime/
lib.rs

1//! `datetime-rs` provides a representation of a date and time.
2//!
3//! Internal storage is a Unix timestamp and, if the `tz` feature is enabled (which it is not by
4//! default), optionally a `TimeZone`.
5
6#![doc(html_root_url = "https://docs.rs/datetime-rs/latest")]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9use std::cmp::Ordering;
10use std::fmt;
11use std::str::FromStr;
12use std::time::SystemTime;
13
14use format::FormattedDateTime;
15use strptime::ParseError;
16use strptime::ParseResult;
17use strptime::Parser;
18use strptime::RawDateTime;
19
20/// Construct a date and time from a `YYYY-MM-DD HH:MM:SS[.fraction]` literal.
21///
22/// The seconds component may be an integer (`45`) or a decimal (`45.5`, `45.123_456_789`).
23/// Fractional digits beyond nanosecond precision are ignored.
24#[macro_export]
25macro_rules! datetime {
26  ($y:literal-$m:literal-$d:literal $h:literal : $mi:literal : $s:literal) => {{
27    #[allow(clippy::zero_prefixed_literal)]
28    {
29      const __SN: (u8, u32) = $crate::__private::parse_second(::core::stringify!($s));
30      $crate::DateTime::ymd($y, $m, $d).hms($h, $mi, __SN.0).nanos(__SN.1).build()
31    }
32  }};
33  ($y:literal-$m:literal-$d:literal $h:literal : $mi:literal : $s:literal $($tz:ident)::+) => {{
34    #[cfg(feature = "tz")]
35    #[allow(clippy::zero_prefixed_literal)]
36    {
37      const __SN: (u8, u32) = $crate::__private::parse_second(::core::stringify!($s));
38      match $crate::DateTime::ymd($y, $m, $d)
39        .hms($h, $mi, __SN.0)
40        .nanos(__SN.1)
41        .tz($crate::tz::$($tz)::+)
42      {
43        Ok(dt) => dt.build(),
44        Err(_) => panic!("invalid date/time and time zone combination"),
45      }
46    }
47    #[cfg(not(feature = "tz"))]
48    {
49      compile_error!("The `tz` feature must be enabled to specify a time zone.");
50    }
51  }};
52}
53
54/// Implementation details exposed solely for use by this crate's macros. Not public API.
55#[doc(hidden)]
56pub mod __private {
57  /// Parse the second component of a `datetime!` literal at compile time.
58  ///
59  /// Accepts either an integer (`"45"`) or a decimal (`"45.5"`, `"45.123456789"`) and returns
60  /// `(whole_seconds, nanoseconds)`. Fractional digits past nine are truncated; underscores and
61  /// trailing type suffixes (e.g. `_f64`) are tolerated.
62  pub const fn parse_second(s: &str) -> (u8, u32) {
63    let b = s.as_bytes();
64    let mut sec: u8 = 0;
65    let mut i = 0;
66    while i < b.len() {
67      let c = b[i];
68      if c == b'.' {
69        break;
70      }
71      if c == b'_' {
72        i += 1;
73        continue;
74      }
75      assert!(c >= b'0' && c <= b'9', "datetime! second must be a numeric literal");
76      sec = sec * 10 + (c - b'0');
77      i += 1;
78    }
79    if i == b.len() {
80      return (sec, 0);
81    }
82    i += 1; // skip '.'
83    let mut nanos: u32 = 0;
84    let mut digits = 0;
85    while i < b.len() && digits < 9 {
86      let c = b[i];
87      if c == b'_' {
88        i += 1;
89        continue;
90      }
91      if c < b'0' || c > b'9' {
92        break;
93      }
94      nanos = nanos * 10 + (c - b'0') as u32;
95      i += 1;
96      digits += 1;
97    }
98    while digits < 9 {
99      nanos *= 10;
100      digits += 1;
101    }
102    (sec, nanos)
103  }
104}
105
106#[cfg(feature = "diesel-pg")]
107mod diesel_pg;
108#[cfg(feature = "duckdb")]
109mod duckdb;
110mod format;
111pub mod interval;
112#[cfg(feature = "serde")]
113mod serde;
114
115pub use date::Date;
116pub use date::Weekday;
117pub use date::date;
118
119/// Time zone compnents.
120///
121/// These are re-exported from the `date-rs` crate.
122#[cfg(feature = "tz")]
123#[cfg_attr(docsrs, doc(cfg(feature = "tz")))]
124pub mod tz {
125  pub use date::tz::*;
126
127  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
128  pub(crate) enum TimeZone {
129    Unspecified,
130    Tz(crate::tz::TimeZoneRef<'static>),
131    FixedOffset(i32),
132  }
133
134  impl TimeZone {
135    pub(crate) const fn ut_offset(&self, timestamp: i64) -> TzResult<i32> {
136      match self {
137        Self::Unspecified => Ok(0),
138        Self::FixedOffset(offset) => Ok(*offset),
139        Self::Tz(tz) => match tz.find_local_time_type(timestamp) {
140          Ok(t) => Ok(t.ut_offset()),
141          Err(e) => Err(e),
142        },
143      }
144    }
145  }
146}
147
148/// A representation of a date and time.
149#[derive(Clone, Copy, Eq)]
150#[cfg_attr(feature = "diesel-pg", derive(diesel::AsExpression, diesel::FromSqlRow))]
151#[cfg_attr(feature = "diesel-pg", diesel(
152    sql_type = diesel::sql_types::Timestamp,
153    sql_type = diesel::sql_types::Timestamptz))]
154pub struct DateTime {
155  seconds: i64,
156  nanos: u32,
157  #[cfg(feature = "tz")]
158  tz: tz::TimeZone,
159}
160
161impl DateTime {
162  /// Create a new date and time object.
163  pub const fn ymd(year: i16, month: u8, day: u8) -> DateTimeBuilder {
164    DateTimeBuilder {
165      date: Date::new(year, month, day),
166      seconds: 0,
167      nanos: 0,
168      #[cfg(feature = "tz")]
169      tz: tz::TimeZone::Unspecified,
170      offset: 0,
171    }
172  }
173
174  /// Create a new date and time object from the given Unix timestamp.
175  pub const fn from_timestamp(timestamp: i64, nanos: u32) -> Self {
176    let mut timestamp = timestamp;
177    let mut nanos = nanos;
178    while nanos >= 1_000_000_000 {
179      nanos -= 1_000_000_000;
180      timestamp += 1;
181    }
182    Self {
183      seconds: timestamp,
184      nanos,
185      #[cfg(feature = "tz")]
186      tz: tz::TimeZone::Unspecified,
187    }
188  }
189
190  /// Create a new date and time object from the given Unix timestamp in milliseconds.
191  pub const fn from_timestamp_millis(millis: i64) -> Self {
192    Self::from_timestamp(millis.div_euclid(1_000), millis.rem_euclid(1_000) as u32)
193  }
194
195  /// Create a new date and time object from the given Unix timestamp in microseconds.
196  pub const fn from_timestamp_micros(micros: i64) -> Self {
197    Self::from_timestamp(micros.div_euclid(1_000_000), micros.rem_euclid(1_000_000) as u32)
198  }
199
200  /// Create a new date and time object from the given Unix timestamp in nanoseconds.
201  pub const fn from_timestamp_nanos(nanos: i128) -> Self {
202    Self::from_timestamp(
203      nanos.div_euclid(1_000_000_000) as i64,
204      nanos.rem_euclid(1_000_000_000) as u32,
205    )
206  }
207
208  /// Return the current timestamp.
209  ///
210  /// ## Panic
211  ///
212  /// Panics if the system clock is set prior to January 1, 1970.
213  pub fn now() -> Self {
214    let dur = SystemTime::now()
215      .duration_since(SystemTime::UNIX_EPOCH)
216      .expect("System clock set prior to January 1, 1970");
217    Self::from_timestamp(dur.as_secs() as i64, dur.subsec_nanos())
218  }
219}
220
221#[cfg(feature = "tz")]
222impl DateTime {
223  /// Set the time zone to the provided time zone, without adjusting the underlying absolute
224  /// timestamp.
225  ///
226  /// This method modifies the wall clock time while maintaining the underlying absolute timestamp.
227  /// To modify the timestamp instead, use `in_tz`.
228  #[inline]
229  pub const fn with_tz(mut self, tz: tz::TimeZoneRef<'static>) -> Self {
230    self.tz = tz::TimeZone::Tz(tz);
231    self
232  }
233
234  /// Set the timestamp to the same wall clock time in the provided time zone.
235  ///
236  /// This method modifies the underlying timestamp while maintaining the wall clock time.
237  /// To maintain the timestamp instead, use `with_tz`.
238  #[inline]
239  pub const fn in_tz(mut self, tz: tz::TimeZoneRef<'static>) -> Self {
240    let existing_ut_offset = match self.tz.ut_offset(self.seconds) {
241      Ok(offset) => offset as i64,
242      Err(_) => panic!("Invalid time zone."),
243    };
244    let desired_ut_offset = match tz.find_local_time_type(self.seconds) {
245      Ok(t) => t.ut_offset() as i64,
246      Err(_) => panic!("Invalid time zone for this timestamp."),
247    };
248    self.seconds += existing_ut_offset - desired_ut_offset;
249    self.tz = tz::TimeZone::Tz(tz);
250    self
251  }
252}
253
254/// Accessors
255impl DateTime {
256  /// The year for this date.
257  #[inline]
258  pub const fn year(&self) -> i16 {
259    Date::from_timestamp(self.tz_adjusted_seconds()).year()
260  }
261
262  /// The month for this date.
263  #[inline]
264  pub const fn month(&self) -> u8 {
265    Date::from_timestamp(self.tz_adjusted_seconds()).month()
266  }
267
268  /// The day of the month for this date.
269  #[inline]
270  pub const fn day(&self) -> u8 {
271    Date::from_timestamp(self.tz_adjusted_seconds()).day()
272  }
273
274  /// The day of the week for this date.
275  #[inline]
276  pub const fn weekday(&self) -> Weekday {
277    Date::from_timestamp(self.tz_adjusted_seconds()).weekday()
278  }
279
280  /// The hour of the day for this date and time. Range: `[0, 24)`
281  #[inline]
282  pub const fn hour(&self) -> u8 {
283    (self.tz_adjusted_seconds() % 86_400 / 3_600) as u8
284  }
285
286  /// The minute of the hour for this date and time. Range: `[0, 60)`
287  #[inline]
288  pub const fn minute(&self) -> u8 {
289    ((self.tz_adjusted_seconds() % 3600) / 60) as u8
290  }
291
292  /// The second of the minute for this date and time. Range: `[0, 60)`
293  #[inline]
294  pub const fn second(&self) -> u8 {
295    (self.tz_adjusted_seconds() % 60) as u8
296  }
297
298  /// The nanosecond of the second for this date and time. Range: `[0, 1_000_000_000)`
299  #[inline]
300  pub const fn nanosecond(&self) -> u32 {
301    self.nanos
302  }
303
304  /// The ordinal day of the year.
305  #[inline]
306  pub const fn day_of_year(&self) -> u16 {
307    self.date().day_of_year()
308  }
309
310  /// The date corresponding to this datetime.
311  #[inline]
312  pub const fn date(&self) -> Date {
313    Date::from_timestamp(self.tz_adjusted_seconds())
314  }
315
316  /// The number of seconds since the Unix epoch for this date and time.
317  #[inline]
318  pub const fn as_seconds(&self) -> i64 {
319    self.seconds
320  }
321
322  /// The number of milliseconds since the Unix epoch for this date and time.
323  #[inline]
324  pub const fn as_milliseconds(&self) -> i64 {
325    self.seconds * 1_000 + (self.nanos / 1_000_000) as i64
326  }
327
328  /// The number of microseconds since the Unix epoch for this date and time.
329  #[inline]
330  pub const fn as_microseconds(&self) -> i64 {
331    self.seconds * 1_000_000 + (self.nanos / 1_000) as i64
332  }
333
334  /// The number of nanoseconds since the Unix epoch for this date and time.
335  #[inline]
336  pub const fn as_nanoseconds(&self) -> i128 {
337    self.seconds as i128 * 1_000_000_000 + self.nanos as i128
338  }
339
340  /// The precision required to represent this timestamp with no fidelity loss.
341  #[inline]
342  pub const fn precision(&self) -> Precision {
343    if self.nanos == 0 {
344      Precision::Second
345    } else if self.nanos % 1_000_000 == 0 {
346      Precision::Millisecond
347    } else if self.nanos % 1_000 == 0 {
348      Precision::Microsecond
349    } else {
350      Precision::Nanosecond
351    }
352  }
353
354  /// Provide the number of seconds since the epoch in the time zone with the same offset as this
355  /// datetime's time zone.
356  #[inline(always)]
357  const fn tz_adjusted_seconds(&self) -> i64 {
358    self.seconds + self.tz_offset()
359  }
360
361  /// Provide the offset, in seconds
362  const fn tz_offset(&self) -> i64 {
363    #[cfg(feature = "tz")]
364    {
365      match self.tz.ut_offset(self.seconds) {
366        Ok(offset) => offset as i64,
367        Err(_) => panic!("Invalid time zone"),
368      }
369    }
370    #[cfg(not(feature = "tz"))]
371    0
372  }
373}
374
375impl DateTime {
376  /// Format the given date and time according to the provided `strftime`-like string.
377  pub fn format(&self, format: &'static str) -> FormattedDateTime<'_> {
378    FormattedDateTime { dt: self, format }
379  }
380}
381
382impl DateTime {
383  /// Parse a date from a string, according to the provided format string.
384  pub fn parse(datetime_str: impl AsRef<str>, fmt: &'static str) -> ParseResult<Self> {
385    let parser = Parser::new(fmt);
386    parser.parse(datetime_str)?.try_into()
387  }
388}
389
390impl PartialEq for DateTime {
391  fn eq(&self, other: &Self) -> bool {
392    self.seconds == other.seconds && self.nanos == other.nanos
393  }
394}
395
396impl PartialOrd for DateTime {
397  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
398    Some(self.cmp(other))
399  }
400}
401
402impl Ord for DateTime {
403  fn cmp(&self, other: &Self) -> Ordering {
404    let seconds_cmp = self.seconds.cmp(&other.seconds);
405    match seconds_cmp {
406      Ordering::Equal => self.nanos.cmp(&other.nanos),
407      _ => seconds_cmp,
408    }
409  }
410}
411
412impl FromStr for DateTime {
413  type Err = ParseError;
414
415  #[rustfmt::skip]
416  fn from_str(s: &str) -> ParseResult<Self> {
417    // Attempt several common formats.
418    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S").parse(s) { return dt.try_into(); }
419    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%z").parse(s) { return dt.try_into(); }
420    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S").parse(s) { return dt.try_into(); }
421    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%z").parse(s) { return dt.try_into(); }
422    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.6f").parse(s) { return dt.try_into(); }
423    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.6f%z").parse(s) { return dt.try_into(); }
424    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.6f").parse(s) { return dt.try_into(); }
425    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.6f%z").parse(s) { return dt.try_into(); }
426    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.9f").parse(s) { return dt.try_into(); }
427    if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.9f%z").parse(s) { return dt.try_into(); }
428    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.9f").parse(s) { return dt.try_into(); }
429    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.9f%z").parse(s) { return dt.try_into(); }
430    if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%SZ").parse(s) { return dt.try_into(); }
431    Parser::new("%Y-%m-%dT%H:%M:%SZ").parse(s)?.try_into()
432  }
433}
434
435impl TryFrom<RawDateTime> for DateTime {
436  type Error = ParseError;
437
438  fn try_from(value: RawDateTime) -> ParseResult<Self> {
439    let date = value.date()?;
440    let time = value.time().unwrap_or_default();
441    Ok(match time.utc_offset() {
442      #[cfg(feature = "tz")]
443      Some(utc_offset) => Self::ymd(date.year(), date.month(), date.day())
444        .hms(time.hour(), time.minute(), time.second())
445        .nanos(time.nanosecond() as u32)
446        .utc_offset(utc_offset)
447        .build(),
448      #[cfg(not(feature = "tz"))]
449      Some(_) => panic!("Enable the `tz` feature to parse datetimes with UTC offsets."),
450      None => Self::ymd(date.year(), date.month(), date.day())
451        .hms(time.hour(), time.minute(), time.second())
452        .nanos(time.nanosecond() as u32)
453        .build(),
454    })
455  }
456}
457
458impl fmt::Debug for DateTime {
459  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460    if self.nanos == 0 {
461      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S"))
462    } else if self.nanos % 1_000_000 == 0 {
463      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.3f"))
464    } else if self.nanos % 1_000 == 0 {
465      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.6f"))
466    } else {
467      write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.9f"))
468    }
469  }
470}
471
472#[cfg(feature = "log")]
473impl log::kv::ToValue for DateTime {
474  fn to_value(&self) -> log::kv::Value<'_> {
475    log::kv::Value::from_debug(self)
476  }
477}
478
479/// An intermediate builder for [`DateTime`].
480#[must_use]
481pub struct DateTimeBuilder {
482  date: Date,
483  seconds: i64,
484  nanos: u32,
485  #[cfg(feature = "tz")]
486  tz: tz::TimeZone,
487  offset: i64,
488}
489
490impl DateTimeBuilder {
491  /// Attach an hour, minute, and second to the datetime.
492  pub const fn hms(mut self, hour: u8, minute: u8, second: u8) -> Self {
493    assert!(hour < 24, "Hour out of bounds");
494    assert!(minute < 60, "Minute out of bounds");
495    assert!(second < 60, "Second out of bounds");
496    self.seconds = (hour as i64 * 3600) + (minute as i64 * 60) + second as i64;
497    self
498  }
499
500  /// Attach fractional to the datetime.
501  pub const fn nanos(mut self, nanos: u32) -> Self {
502    assert!(nanos < 1_000_000_000, "Nanos out of bounds.");
503    self.nanos = nanos;
504    self
505  }
506
507  /// Attach a timezone to the datetime.
508  ///
509  /// This method assumes that the timezone _modifies_ the underlying timestamp; in other words,
510  /// the YMD/HMS specified to the date and time builder should be preserved, and the time zone's
511  /// offset applied to the underlying timestamp to preserve the date and time on the wall clock.
512  #[cfg(feature = "tz")]
513  pub const fn tz(mut self, tz: tz::TimeZoneRef<'static>) -> tz::TzResult<Self> {
514    self.offset = match tz.find_local_time_type(self.date.timestamp() + self.seconds) {
515      Ok(t) => t.ut_offset() as i64,
516      Err(e) => return Err(e),
517    };
518    self.tz = tz::TimeZone::Tz(tz);
519    Ok(self)
520  }
521
522  /// Attach a UTC offset to the datetime.
523  ///
524  /// This method assumes that the offset _modifies_ the underlying timestamp; in other words, the
525  /// YMD/HMS specified to the date and time builder should be preserved, and the offset applied to
526  /// the underlying timestamp to preserve the date and time on the wall clock.
527  #[cfg(feature = "tz")]
528  pub(crate) const fn utc_offset(mut self, offset: i32) -> Self {
529    self.offset = offset as i64;
530    self.tz = tz::TimeZone::FixedOffset(offset);
531    self
532  }
533
534  /// Build the final [`DateTime`] object.
535  pub const fn build(self) -> DateTime {
536    DateTime {
537      seconds: self.date.timestamp() + self.seconds - self.offset,
538      nanos: self.nanos,
539      #[cfg(feature = "tz")]
540      tz: self.tz,
541    }
542  }
543}
544
545trait Sealed {}
546impl Sealed for date::Date {}
547
548/// Convert from a date into a datetime, by way of a builder.
549#[allow(private_bounds)]
550pub trait FromDate: Sealed {
551  /// Create a `DateTimeBuilder` for this Date.
552  fn hms(self, hour: u8, minute: u8, second: u8) -> DateTimeBuilder;
553}
554
555impl FromDate for date::Date {
556  fn hms(self, hour: u8, minute: u8, second: u8) -> DateTimeBuilder {
557    DateTimeBuilder {
558      date: self,
559      seconds: 0,
560      nanos: 0,
561      #[cfg(feature = "tz")]
562      tz: tz::TimeZone::Unspecified,
563      offset: 0,
564    }
565    .hms(hour, minute, second)
566  }
567}
568
569/// The precision that this timestamp requires in order to represent with no fidelity loss.
570#[derive(Clone, Copy, Debug, Eq, PartialEq)]
571pub enum Precision {
572  Second,
573  Millisecond,
574  Microsecond,
575  Nanosecond,
576}
577
578#[cfg(test)]
579mod tests {
580  use assert2::check;
581  use strptime::ParseResult;
582
583  use crate::DateTime;
584  use crate::FromDate;
585  use crate::Precision;
586  use crate::interval::TimeInterval;
587  #[cfg(feature = "tz")]
588  use crate::tz;
589
590  #[test]
591  fn test_zero() {
592    let dt = datetime! { 1970-01-01 00:00:00 };
593    check!(dt.seconds == 0);
594  }
595
596  #[test]
597  fn test_accessors() {
598    let dt = datetime! { 2012-04-21 11:00:00 };
599    check!(dt.year() == 2012);
600    check!(dt.month() == 4);
601    check!(dt.day() == 21);
602    check!(dt.hour() == 11);
603    check!(dt.minute() == 0);
604    check!(dt.second() == 0);
605  }
606
607  #[test]
608  fn test_more_accessors() {
609    let dt = datetime! { 2024-02-29 13:15:45 };
610    check!(dt.year() == 2024);
611    check!(dt.month() == 2);
612    check!(dt.day() == 29);
613    check!(dt.hour() == 13);
614    check!(dt.minute() == 15);
615    check!(dt.second() == 45);
616  }
617
618  #[test]
619  fn test_fractional_seconds_literal() {
620    let dt = datetime! { 2024-02-29 13:15:45.5 };
621    check!(dt.second() == 45);
622    check!(dt.nanosecond() == 500_000_000);
623
624    let dt = datetime! { 2024-02-29 13:15:45.123_456_789 };
625    check!(dt.second() == 45);
626    check!(dt.nanosecond() == 123_456_789);
627
628    // Integer form still works and yields zero nanos.
629    let dt = datetime! { 2024-02-29 13:15:45 };
630    check!(dt.second() == 45);
631    check!(dt.nanosecond() == 0);
632  }
633
634  #[test]
635  #[cfg(feature = "tz")]
636  fn test_fractional_seconds_literal_tz() {
637    let dt = datetime! { 2012-04-21 11:00:00.250 us::EASTERN };
638    check!(dt.hour() == 11);
639    check!(dt.nanosecond() == 250_000_000);
640  }
641
642  #[test]
643  fn test_parse_str() -> ParseResult<()> {
644    for s in [
645      "2012-04-21 11:00:00",
646      "2012-04-21T11:00:00",
647      "2012-04-21 11:00:00.000000",
648      "2012-04-21 11:00:00Z",
649      "2012-04-21T11:00:00.000000",
650      "2012-04-21T11:00:00Z",
651    ] {
652      let dt = s.parse::<DateTime>()?;
653      check!(dt.year() == 2012);
654      check!(dt.month() == 4);
655      check!(dt.day() == 21);
656      check!(dt.hour() == 11);
657    }
658
659    Ok(())
660  }
661
662  #[test]
663  #[cfg(feature = "tz")]
664  fn test_parse_str_tz() -> ParseResult<()> {
665    for s in
666      ["2012-04-21 11:00:00-0400", "2012-04-21T11:00:00-0400", "2012-04-21 11:00:00.000000-0400"]
667    {
668      let dt = s.parse::<DateTime>()?;
669      check!(dt.year() == 2012);
670      check!(dt.month() == 4);
671      check!(dt.day() == 21);
672      check!(dt.hour() == 11);
673    }
674    Ok(())
675  }
676
677  #[test]
678  #[allow(clippy::inconsistent_digit_grouping)]
679  fn test_as_precision() {
680    let dt = DateTime::ymd(2012, 4, 21).hms(15, 0, 0).build();
681    check!(dt.as_seconds() == 1335020400);
682    check!(dt.as_milliseconds() == 1335020400_000);
683    check!(dt.as_microseconds() == 1335020400_000_000);
684    check!(dt.as_nanoseconds() == 1335020400_000_000_000);
685  }
686
687  #[test]
688  fn test_precision() {
689    let mut dt = DateTime::ymd(2012, 4, 21).hms(15, 0, 0).build();
690    check!(dt.precision() == Precision::Second);
691    dt += TimeInterval::new(0, 1_000_000);
692    check!(dt.precision() == Precision::Millisecond);
693    dt += TimeInterval::new(0, 1_000);
694    check!(dt.precision() == Precision::Microsecond);
695    dt += TimeInterval::new(0, 1);
696    check!(dt.precision() == Precision::Nanosecond);
697  }
698
699  #[cfg(feature = "tz")]
700  #[test]
701  fn test_tz() -> tz::TzResult<()> {
702    let dt = DateTime::ymd(2012, 4, 21).hms(11, 0, 0).tz(tz::us::EASTERN)?.build();
703    check!(dt.as_seconds() == 1335020400);
704    check!(dt.year() == 2012);
705    check!(dt.month() == 4);
706    check!(dt.day() == 21);
707    check!(dt.hour() == 11);
708    let dt = DateTime::ymd(1970, 1, 1).tz(tz::us::PACIFIC)?.build();
709    check!(dt.as_seconds() == 3600 * 8);
710    Ok(())
711  }
712
713  #[cfg(feature = "tz")]
714  #[test]
715  fn test_unix_tz() {
716    #[allow(clippy::inconsistent_digit_grouping)]
717    for dt in [
718      DateTime::from_timestamp(1335020400, 0),
719      DateTime::from_timestamp_millis(1335020400_000),
720      DateTime::from_timestamp_micros(1335020400_000_000),
721      DateTime::from_timestamp_nanos(1335020400_000_000_000),
722    ] {
723      let dt = dt.with_tz(tz::us::EASTERN);
724      check!(dt.as_seconds() == 1335020400);
725      check!(dt.year() == 2012);
726      check!(dt.month() == 4);
727      check!(dt.day() == 21);
728      check!(dt.hour() == 11);
729    }
730  }
731
732  #[cfg(feature = "tz")]
733  #[test]
734  fn test_in_tz() {
735    let dt = DateTime::from_timestamp(1335020400, 0).with_tz(tz::us::EASTERN);
736    check!(dt.hour() == 11);
737    check!(dt.in_tz(tz::us::CENTRAL).hour() == 11);
738    check!(dt.as_seconds() - dt.in_tz(tz::us::CENTRAL).as_seconds() == -3600);
739    check!(dt.in_tz(tz::europe::LONDON).hour() == 11);
740    check!(dt.as_seconds() - dt.in_tz(tz::europe::LONDON).as_seconds() == 3600 * 5);
741  }
742
743  #[test]
744  fn test_from_date_trait() {
745    let dt = date::date! { 2012-04-21 }.hms(11, 0, 0).build();
746    check!(dt.year() == 2012);
747    check!(dt.month() == 4);
748    check!(dt.day() == 21);
749    check!(dt.hour() == 11);
750  }
751
752  #[test]
753  fn test_debug() {
754    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).build();
755    check!(format!("{:?}", dt) == "2012-04-21 15:00:00");
756    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(500_000_000).build();
757    check!(format!("{:?}", dt) == "2012-04-21 15:00:00.500");
758    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(123_450_000).build();
759    check!(format!("{:?}", dt) == "2012-04-21 15:00:00.123450");
760    let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(123_456_789).build();
761    check!(format!("{:?}", dt) == "2012-04-21 15:00:00.123456789");
762  }
763}