1#![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#[macro_export]
22macro_rules! datetime {
23 ($y:literal-$m:literal-$d:literal $h:literal : $mi:literal : $s:literal) => {{
24 #[allow(clippy::zero_prefixed_literal)]
25 {
26 $crate::DateTime::ymd($y, $m, $d).hms($h, $mi, $s).build()
27 }
28 }};
29 ($y:literal-$m:literal-$d:literal $h:literal : $mi:literal : $s:literal $($tz:ident)::+) => {{
30 #[cfg(feature = "tz")]
31 #[allow(clippy::zero_prefixed_literal)]
32 {
33 match $crate::DateTime::ymd($y, $m, $d).hms($h, $mi, $s).tz($crate::tz::$($tz)::+) {
34 Ok(dt) => dt.build(),
35 Err(_) => panic!("invalid date/time and time zone combination"),
36 }
37 }
38 #[cfg(not(feature = "tz"))]
39 {
40 compile_error!("The `tz` feature must be enabled to specify a time zone.");
41 }
42 }};
43}
44
45#[cfg(feature = "diesel-pg")]
46mod diesel_pg;
47#[cfg(feature = "duckdb")]
48mod duckdb;
49mod format;
50pub mod interval;
51#[cfg(feature = "serde")]
52mod serde;
53
54pub use date::Date;
55pub use date::Weekday;
56pub use date::date;
57
58#[cfg(feature = "tz")]
62#[cfg_attr(docsrs, doc(cfg(feature = "tz")))]
63pub mod tz {
64 pub use date::tz::*;
65
66 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
67 pub(crate) enum TimeZone {
68 Unspecified,
69 Tz(crate::tz::TimeZoneRef<'static>),
70 FixedOffset(i32),
71 }
72
73 impl TimeZone {
74 pub(crate) const fn ut_offset(&self, timestamp: i64) -> TzResult<i32> {
75 match self {
76 Self::Unspecified => Ok(0),
77 Self::FixedOffset(offset) => Ok(*offset),
78 Self::Tz(tz) => match tz.find_local_time_type(timestamp) {
79 Ok(t) => Ok(t.ut_offset()),
80 Err(e) => Err(e),
81 },
82 }
83 }
84 }
85}
86
87#[derive(Clone, Copy, Eq)]
89#[cfg_attr(feature = "diesel-pg", derive(diesel::AsExpression, diesel::FromSqlRow))]
90#[cfg_attr(feature = "diesel-pg", diesel(
91 sql_type = diesel::sql_types::Timestamp,
92 sql_type = diesel::sql_types::Timestamptz))]
93pub struct DateTime {
94 seconds: i64,
95 nanos: u32,
96 #[cfg(feature = "tz")]
97 tz: tz::TimeZone,
98}
99
100impl DateTime {
101 pub const fn ymd(year: i16, month: u8, day: u8) -> DateTimeBuilder {
103 DateTimeBuilder {
104 date: Date::new(year, month, day),
105 seconds: 0,
106 nanos: 0,
107 #[cfg(feature = "tz")]
108 tz: tz::TimeZone::Unspecified,
109 offset: 0,
110 }
111 }
112
113 pub const fn from_timestamp(timestamp: i64, nanos: u32) -> Self {
115 let mut timestamp = timestamp;
116 let mut nanos = nanos;
117 while nanos >= 1_000_000_000 {
118 nanos -= 1_000_000_000;
119 timestamp += 1;
120 }
121 Self {
122 seconds: timestamp,
123 nanos,
124 #[cfg(feature = "tz")]
125 tz: tz::TimeZone::Unspecified,
126 }
127 }
128
129 pub const fn from_timestamp_millis(millis: i64) -> Self {
131 Self::from_timestamp(millis.div_euclid(1_000), millis.rem_euclid(1_000) as u32)
132 }
133
134 pub const fn from_timestamp_micros(micros: i64) -> Self {
136 Self::from_timestamp(micros.div_euclid(1_000_000), micros.rem_euclid(1_000_000) as u32)
137 }
138
139 pub const fn from_timestamp_nanos(nanos: i128) -> Self {
141 Self::from_timestamp(
142 nanos.div_euclid(1_000_000_000) as i64,
143 nanos.rem_euclid(1_000_000_000) as u32,
144 )
145 }
146
147 pub fn now() -> Self {
153 let dur = SystemTime::now()
154 .duration_since(SystemTime::UNIX_EPOCH)
155 .expect("System clock set prior to January 1, 1970");
156 Self::from_timestamp(dur.as_secs() as i64, dur.subsec_nanos())
157 }
158}
159
160#[cfg(feature = "tz")]
161impl DateTime {
162 #[inline]
168 pub const fn with_tz(mut self, tz: tz::TimeZoneRef<'static>) -> Self {
169 self.tz = tz::TimeZone::Tz(tz);
170 self
171 }
172
173 #[inline]
178 pub const fn in_tz(mut self, tz: tz::TimeZoneRef<'static>) -> Self {
179 let existing_ut_offset = match self.tz.ut_offset(self.seconds) {
180 Ok(offset) => offset as i64,
181 Err(_) => panic!("Invalid time zone."),
182 };
183 let desired_ut_offset = match tz.find_local_time_type(self.seconds) {
184 Ok(t) => t.ut_offset() as i64,
185 Err(_) => panic!("Invalid time zone for this timestamp."),
186 };
187 self.seconds += existing_ut_offset - desired_ut_offset;
188 self.tz = tz::TimeZone::Tz(tz);
189 self
190 }
191}
192
193impl DateTime {
195 #[inline]
197 pub const fn year(&self) -> i16 {
198 Date::from_timestamp(self.tz_adjusted_seconds()).year()
199 }
200
201 #[inline]
203 pub const fn month(&self) -> u8 {
204 Date::from_timestamp(self.tz_adjusted_seconds()).month()
205 }
206
207 #[inline]
209 pub const fn day(&self) -> u8 {
210 Date::from_timestamp(self.tz_adjusted_seconds()).day()
211 }
212
213 #[inline]
215 pub const fn weekday(&self) -> Weekday {
216 Date::from_timestamp(self.tz_adjusted_seconds()).weekday()
217 }
218
219 #[inline]
221 pub const fn hour(&self) -> u8 {
222 (self.tz_adjusted_seconds() % 86_400 / 3_600) as u8
223 }
224
225 #[inline]
227 pub const fn minute(&self) -> u8 {
228 ((self.tz_adjusted_seconds() % 3600) / 60) as u8
229 }
230
231 #[inline]
233 pub const fn second(&self) -> u8 {
234 (self.tz_adjusted_seconds() % 60) as u8
235 }
236
237 #[inline]
239 pub const fn nanosecond(&self) -> u32 {
240 self.nanos
241 }
242
243 #[inline]
245 pub const fn day_of_year(&self) -> u16 {
246 self.date().day_of_year()
247 }
248
249 #[inline]
251 pub const fn date(&self) -> Date {
252 Date::from_timestamp(self.tz_adjusted_seconds())
253 }
254
255 #[inline]
257 pub const fn as_seconds(&self) -> i64 {
258 self.seconds
259 }
260
261 #[inline]
263 pub const fn as_milliseconds(&self) -> i64 {
264 self.seconds * 1_000 + (self.nanos / 1_000_000) as i64
265 }
266
267 #[inline]
269 pub const fn as_microseconds(&self) -> i64 {
270 self.seconds * 1_000_000 + (self.nanos / 1_000) as i64
271 }
272
273 #[inline]
275 pub const fn as_nanoseconds(&self) -> i128 {
276 self.seconds as i128 * 1_000_000_000 + self.nanos as i128
277 }
278
279 #[inline]
281 pub const fn precision(&self) -> Precision {
282 if self.nanos == 0 {
283 Precision::Second
284 } else if self.nanos % 1_000_000 == 0 {
285 Precision::Millisecond
286 } else if self.nanos % 1_000 == 0 {
287 Precision::Microsecond
288 } else {
289 Precision::Nanosecond
290 }
291 }
292
293 #[inline(always)]
296 const fn tz_adjusted_seconds(&self) -> i64 {
297 self.seconds + self.tz_offset()
298 }
299
300 const fn tz_offset(&self) -> i64 {
302 #[cfg(feature = "tz")]
303 {
304 match self.tz.ut_offset(self.seconds) {
305 Ok(offset) => offset as i64,
306 Err(_) => panic!("Invalid time zone"),
307 }
308 }
309 #[cfg(not(feature = "tz"))]
310 0
311 }
312}
313
314impl DateTime {
315 pub fn format(&self, format: &'static str) -> FormattedDateTime {
317 FormattedDateTime { dt: self, format }
318 }
319}
320
321impl DateTime {
322 pub fn parse(datetime_str: impl AsRef<str>, fmt: &'static str) -> ParseResult<Self> {
324 let parser = Parser::new(fmt);
325 parser.parse(datetime_str)?.try_into()
326 }
327}
328
329impl PartialEq for DateTime {
330 fn eq(&self, other: &Self) -> bool {
331 self.seconds == other.seconds && self.nanos == other.nanos
332 }
333}
334
335impl PartialOrd for DateTime {
336 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
337 Some(self.cmp(other))
338 }
339}
340
341impl Ord for DateTime {
342 fn cmp(&self, other: &Self) -> Ordering {
343 let seconds_cmp = self.seconds.cmp(&other.seconds);
344 match seconds_cmp {
345 Ordering::Equal => self.nanos.cmp(&other.nanos),
346 _ => seconds_cmp,
347 }
348 }
349}
350
351impl FromStr for DateTime {
352 type Err = ParseError;
353
354 #[rustfmt::skip]
355 fn from_str(s: &str) -> ParseResult<Self> {
356 if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S").parse(s) { return dt.try_into(); }
358 if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%z").parse(s) { return dt.try_into(); }
359 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S").parse(s) { return dt.try_into(); }
360 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%z").parse(s) { return dt.try_into(); }
361 if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.6f").parse(s) { return dt.try_into(); }
362 if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.6f%z").parse(s) { return dt.try_into(); }
363 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.6f").parse(s) { return dt.try_into(); }
364 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.6f%z").parse(s) { return dt.try_into(); }
365 if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.9f").parse(s) { return dt.try_into(); }
366 if let Ok(dt) = Parser::new("%Y-%m-%dT%H:%M:%S%.9f%z").parse(s) { return dt.try_into(); }
367 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.9f").parse(s) { return dt.try_into(); }
368 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%S%.9f%z").parse(s) { return dt.try_into(); }
369 if let Ok(dt) = Parser::new("%Y-%m-%d %H:%M:%SZ").parse(s) { return dt.try_into(); }
370 Parser::new("%Y-%m-%dT%H:%M:%SZ").parse(s)?.try_into()
371 }
372}
373
374impl TryFrom<RawDateTime> for DateTime {
375 type Error = ParseError;
376
377 fn try_from(value: RawDateTime) -> ParseResult<Self> {
378 let date = value.date()?;
379 let time = value.time().unwrap_or_default();
380 Ok(match time.utc_offset() {
381 #[cfg(feature = "tz")]
382 Some(utc_offset) => Self::ymd(date.year(), date.month(), date.day())
383 .hms(time.hour(), time.minute(), time.second())
384 .nanos(time.nanosecond() as u32)
385 .utc_offset(utc_offset)
386 .build(),
387 #[cfg(not(feature = "tz"))]
388 Some(_) => panic!("Enable the `tz` feature to parse datetimes with UTC offsets."),
389 None => Self::ymd(date.year(), date.month(), date.day())
390 .hms(time.hour(), time.minute(), time.second())
391 .nanos(time.nanosecond() as u32)
392 .build(),
393 })
394 }
395}
396
397impl fmt::Debug for DateTime {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 if self.nanos == 0 {
400 write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S"))
401 } else if self.nanos % 1_000_000 == 0 {
402 write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.3f"))
403 } else if self.nanos % 1_000 == 0 {
404 write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.6f"))
405 } else {
406 write!(f, "{}", self.format("%Y-%m-%d %H:%M:%S%.9f"))
407 }
408 }
409}
410
411#[cfg(feature = "log")]
412impl log::kv::ToValue for DateTime {
413 fn to_value(&self) -> log::kv::Value<'_> {
414 log::kv::Value::from_debug(self)
415 }
416}
417
418#[must_use]
420pub struct DateTimeBuilder {
421 date: Date,
422 seconds: i64,
423 nanos: u32,
424 #[cfg(feature = "tz")]
425 tz: tz::TimeZone,
426 offset: i64,
427}
428
429impl DateTimeBuilder {
430 pub const fn hms(mut self, hour: u8, minute: u8, second: u8) -> Self {
432 assert!(hour < 24, "Hour out of bounds");
433 assert!(minute < 60, "Minute out of bounds");
434 assert!(second < 60, "Second out of bounds");
435 self.seconds = (hour as i64 * 3600) + (minute as i64 * 60) + second as i64;
436 self
437 }
438
439 pub const fn nanos(mut self, nanos: u32) -> Self {
441 assert!(nanos < 1_000_000_000, "Nanos out of bounds.");
442 self.nanos = nanos;
443 self
444 }
445
446 #[cfg(feature = "tz")]
452 pub const fn tz(mut self, tz: tz::TimeZoneRef<'static>) -> tz::TzResult<Self> {
453 self.offset = match tz.find_local_time_type(self.date.timestamp() + self.seconds) {
454 Ok(t) => t.ut_offset() as i64,
455 Err(e) => return Err(e),
456 };
457 self.tz = tz::TimeZone::Tz(tz);
458 Ok(self)
459 }
460
461 #[cfg(feature = "tz")]
467 pub(crate) const fn utc_offset(mut self, offset: i32) -> Self {
468 self.offset = offset as i64;
469 self.tz = tz::TimeZone::FixedOffset(offset);
470 self
471 }
472
473 pub const fn build(self) -> DateTime {
475 DateTime {
476 seconds: self.date.timestamp() + self.seconds - self.offset,
477 nanos: self.nanos,
478 #[cfg(feature = "tz")]
479 tz: self.tz,
480 }
481 }
482}
483
484trait Sealed {}
485impl Sealed for date::Date {}
486
487#[allow(private_bounds)]
489pub trait FromDate: Sealed {
490 fn hms(self, hour: u8, minute: u8, second: u8) -> DateTimeBuilder;
492}
493
494impl FromDate for date::Date {
495 fn hms(self, hour: u8, minute: u8, second: u8) -> DateTimeBuilder {
496 DateTimeBuilder {
497 date: self,
498 seconds: 0,
499 nanos: 0,
500 #[cfg(feature = "tz")]
501 tz: tz::TimeZone::Unspecified,
502 offset: 0,
503 }
504 .hms(hour, minute, second)
505 }
506}
507
508#[derive(Clone, Copy, Debug, Eq, PartialEq)]
510pub enum Precision {
511 Second,
512 Millisecond,
513 Microsecond,
514 Nanosecond,
515}
516
517#[cfg(test)]
518mod tests {
519 use assert2::check;
520 use strptime::ParseResult;
521
522 use crate::DateTime;
523 use crate::FromDate;
524 use crate::Precision;
525 use crate::interval::TimeInterval;
526 #[cfg(feature = "tz")]
527 use crate::tz;
528
529 #[test]
530 fn test_zero() {
531 let dt = datetime! { 1970-01-01 00:00:00 };
532 check!(dt.seconds == 0);
533 }
534
535 #[test]
536 fn test_accessors() {
537 let dt = datetime! { 2012-04-21 11:00:00 };
538 check!(dt.year() == 2012);
539 check!(dt.month() == 4);
540 check!(dt.day() == 21);
541 check!(dt.hour() == 11);
542 check!(dt.minute() == 0);
543 check!(dt.second() == 0);
544 }
545
546 #[test]
547 fn test_more_accessors() {
548 let dt = datetime! { 2024-02-29 13:15:45 };
549 check!(dt.year() == 2024);
550 check!(dt.month() == 2);
551 check!(dt.day() == 29);
552 check!(dt.hour() == 13);
553 check!(dt.minute() == 15);
554 check!(dt.second() == 45);
555 }
556
557 #[test]
558 fn test_parse_str() -> ParseResult<()> {
559 for s in [
560 "2012-04-21 11:00:00",
561 "2012-04-21T11:00:00",
562 "2012-04-21 11:00:00.000000",
563 "2012-04-21 11:00:00Z",
564 "2012-04-21T11:00:00.000000",
565 "2012-04-21T11:00:00Z",
566 ] {
567 let dt = s.parse::<DateTime>()?;
568 check!(dt.year() == 2012);
569 check!(dt.month() == 4);
570 check!(dt.day() == 21);
571 check!(dt.hour() == 11);
572 }
573
574 Ok(())
575 }
576
577 #[test]
578 #[cfg(feature = "tz")]
579 fn test_parse_str_tz() -> ParseResult<()> {
580 for s in
581 ["2012-04-21 11:00:00-0400", "2012-04-21T11:00:00-0400", "2012-04-21 11:00:00.000000-0400"]
582 {
583 let dt = s.parse::<DateTime>()?;
584 check!(dt.year() == 2012);
585 check!(dt.month() == 4);
586 check!(dt.day() == 21);
587 check!(dt.hour() == 11);
588 }
589 Ok(())
590 }
591
592 #[test]
593 #[allow(clippy::inconsistent_digit_grouping)]
594 fn test_as_precision() {
595 let dt = DateTime::ymd(2012, 4, 21).hms(15, 0, 0).build();
596 check!(dt.as_seconds() == 1335020400);
597 check!(dt.as_milliseconds() == 1335020400_000);
598 check!(dt.as_microseconds() == 1335020400_000_000);
599 check!(dt.as_nanoseconds() == 1335020400_000_000_000);
600 }
601
602 #[test]
603 fn test_precision() {
604 let mut dt = DateTime::ymd(2012, 4, 21).hms(15, 0, 0).build();
605 check!(dt.precision() == Precision::Second);
606 dt += TimeInterval::new(0, 1_000_000);
607 check!(dt.precision() == Precision::Millisecond);
608 dt += TimeInterval::new(0, 1_000);
609 check!(dt.precision() == Precision::Microsecond);
610 dt += TimeInterval::new(0, 1);
611 check!(dt.precision() == Precision::Nanosecond);
612 }
613
614 #[cfg(feature = "tz")]
615 #[test]
616 fn test_tz() -> tz::TzResult<()> {
617 let dt = DateTime::ymd(2012, 4, 21).hms(11, 0, 0).tz(tz::us::EASTERN)?.build();
618 check!(dt.as_seconds() == 1335020400);
619 check!(dt.year() == 2012);
620 check!(dt.month() == 4);
621 check!(dt.day() == 21);
622 check!(dt.hour() == 11);
623 let dt = DateTime::ymd(1970, 1, 1).tz(tz::us::PACIFIC)?.build();
624 check!(dt.as_seconds() == 3600 * 8);
625 Ok(())
626 }
627
628 #[cfg(feature = "tz")]
629 #[test]
630 fn test_unix_tz() {
631 #[allow(clippy::inconsistent_digit_grouping)]
632 for dt in [
633 DateTime::from_timestamp(1335020400, 0),
634 DateTime::from_timestamp_millis(1335020400_000),
635 DateTime::from_timestamp_micros(1335020400_000_000),
636 DateTime::from_timestamp_nanos(1335020400_000_000_000),
637 ] {
638 let dt = dt.with_tz(tz::us::EASTERN);
639 check!(dt.as_seconds() == 1335020400);
640 check!(dt.year() == 2012);
641 check!(dt.month() == 4);
642 check!(dt.day() == 21);
643 check!(dt.hour() == 11);
644 }
645 }
646
647 #[cfg(feature = "tz")]
648 #[test]
649 fn test_in_tz() {
650 let dt = DateTime::from_timestamp(1335020400, 0).with_tz(tz::us::EASTERN);
651 check!(dt.hour() == 11);
652 check!(dt.in_tz(tz::us::CENTRAL).hour() == 11);
653 check!(dt.as_seconds() - dt.in_tz(tz::us::CENTRAL).as_seconds() == -3600);
654 check!(dt.in_tz(tz::europe::LONDON).hour() == 11);
655 check!(dt.as_seconds() - dt.in_tz(tz::europe::LONDON).as_seconds() == 3600 * 5);
656 }
657
658 #[test]
659 fn test_from_date_trait() {
660 let dt = date::date! { 2012-04-21 }.hms(11, 0, 0).build();
661 check!(dt.year() == 2012);
662 check!(dt.month() == 4);
663 check!(dt.day() == 21);
664 check!(dt.hour() == 11);
665 }
666
667 #[test]
668 fn test_debug() {
669 let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).build();
670 check!(format!("{:?}", dt) == "2012-04-21 15:00:00");
671 let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(500_000_000).build();
672 check!(format!("{:?}", dt) == "2012-04-21 15:00:00.500");
673 let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(123_450_000).build();
674 check!(format!("{:?}", dt) == "2012-04-21 15:00:00.123450");
675 let dt = date::date! { 2012-04-21 }.hms(15, 0, 0).nanos(123_456_789).build();
676 check!(format!("{:?}", dt) == "2012-04-21 15:00:00.123456789");
677 }
678}