1use 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#[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#[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 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 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 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 #[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 #[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
163impl DateTime {
165 #[inline]
167 pub const fn year(&self) -> i16 {
168 Date::from_timestamp(self.tz_adjusted_seconds()).year()
169 }
170
171 #[inline]
173 pub const fn month(&self) -> u8 {
174 Date::from_timestamp(self.tz_adjusted_seconds()).month()
175 }
176
177 #[inline]
179 pub const fn day(&self) -> u8 {
180 Date::from_timestamp(self.tz_adjusted_seconds()).day()
181 }
182
183 #[inline]
185 pub const fn weekday(&self) -> Weekday {
186 Date::from_timestamp(self.tz_adjusted_seconds()).weekday()
187 }
188
189 #[inline]
191 pub const fn hour(&self) -> u8 {
192 (self.tz_adjusted_seconds() % 86_400 / 3_600) as u8
193 }
194
195 #[inline]
197 pub const fn minute(&self) -> u8 {
198 ((self.tz_adjusted_seconds() % 3600) / 60) as u8
199 }
200
201 #[inline]
203 pub const fn second(&self) -> u8 {
204 (self.tz_adjusted_seconds() % 60) as u8
205 }
206
207 #[inline]
209 pub const fn nanosecond(&self) -> u32 {
210 self.nanos
211 }
212
213 #[inline]
215 pub const fn day_of_year(&self) -> u16 {
216 self.date().day_of_year()
217 }
218
219 #[inline]
221 pub const fn date(&self) -> Date {
222 Date::from_timestamp(self.tz_adjusted_seconds())
223 }
224
225 #[inline]
227 pub const fn as_seconds(&self) -> i64 {
228 self.seconds
229 }
230
231 #[inline]
233 pub const fn as_milliseconds(&self) -> i64 {
234 self.seconds * 1_000 + (self.nanos / 1_000_000) as i64
235 }
236
237 #[inline]
239 pub const fn as_microseconds(&self) -> i64 {
240 self.seconds * 1_000_000 + (self.nanos / 1_000) as i64
241 }
242
243 #[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 #[inline(always)]
252 const fn tz_adjusted_seconds(&self) -> i64 {
253 self.seconds + self.tz_offset()
254 }
255
256 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 pub fn format(&self, format: &'static str) -> FormattedDateTime {
273 FormattedDateTime { dt: self, format }
274 }
275}
276
277impl DateTime {
278 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 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#[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 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 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 #[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 #[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 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#[allow(private_bounds)]
445pub trait FromDate: Sealed {
446 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}