af_core/
time.rs

1// Copyright © 2020 Alexandra Frydl
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Utilities for working with time.
8
9mod date;
10mod duration;
11mod span;
12mod zone;
13
14pub use self::date::Date;
15pub use self::duration::Duration;
16pub use self::span::Span;
17pub use self::zone::{Zone, LOCAL, UTC};
18
19use crate::prelude::*;
20use chrono::{TimeZone, Timelike};
21
22/// A timestamp with a time zone.
23#[derive(Clone, Copy)]
24pub struct Time {
25  inner: chrono::DateTime<chrono::Utc>,
26  zone: Zone,
27}
28
29macro_rules! in_zone {
30  ($self:tt, |$var:ident| $expr:expr) => {
31    match $self.zone {
32      Zone::Local => {
33        let $var = $self.inner.with_timezone(&chrono::Local);
34        $expr
35      }
36
37      Zone::Tz(tz) => {
38        let $var = $self.inner.with_timezone(&tz);
39        $expr
40      }
41    }
42  };
43}
44
45impl Time {
46  /// Returns a value representing the maximum local date and time.
47  pub const fn max_value() -> Time {
48    Time { inner: chrono::MAX_DATETIME, zone: LOCAL }
49  }
50
51  /// Returns a value representing the minimum local date and time.
52  pub const fn min_value() -> Time {
53    Time { inner: chrono::MIN_DATETIME, zone: LOCAL }
54  }
55
56  /// Returns a value representing the current local date and time.
57  pub fn now() -> Time {
58    Time { inner: chrono::Utc::now(), zone: LOCAL }
59  }
60
61  /// Returns a value representing the given Unix timestamp in milliseconds.
62  pub fn from_unix_ms(timestamp: i64) -> Self {
63    Self { inner: chrono::Utc.timestamp_millis(timestamp), zone: Zone::Local }
64  }
65
66  /// Formats the time according to RFC 3339.
67  pub fn as_rfc3339(&self) -> impl Display {
68    match self.zone == UTC {
69      true => self.format("%FT%T%.fZ"),
70      false => self.format("%FT%T%.f%:z"),
71    }
72  }
73
74  /// Returns the date component of the time.
75  pub fn date(&self) -> Date {
76    in_zone!(self, |t| t.date().naive_local().into())
77  }
78
79  /// Returns the duration elapsed since this time occurred.
80  ///
81  /// If the time is in the future, this function returns [`Duration::ZERO`].
82  pub fn elapsed(&self) -> Duration {
83    Self::now() - *self
84  }
85
86  /// Format the time for display.
87  pub fn format<'a>(&self, fmt: &'a str) -> impl Display + 'a {
88    in_zone!(self, |t| t.format(fmt))
89  }
90
91  /// Returns the hour, minute, and second numbers.
92  ///
93  /// Equivalent to `(time.hour(), time.minute(), time.second())`.
94  pub fn hms(&self) -> (usize, usize, usize) {
95    in_zone!(self, |t| (t.hour() as usize, t.minute() as usize, t.second() as usize))
96  }
97
98  /// Returns the hour from 0 to 23.
99  pub fn hour(&self) -> usize {
100    in_zone!(self, |t| t.hour() as usize)
101  }
102
103  /// Returns the minute from 0 to 59.
104  pub fn minute(&self) -> usize {
105    in_zone!(self, |t| t.minute() as usize)
106  }
107
108  /// Returns the second number from 0 to 59.
109  pub fn second(&self) -> usize {
110    in_zone!(self, |t| t.second() as usize)
111  }
112
113  pub fn start_of_day(&self) -> Time {
114    Time {
115      inner: in_zone!(self, |t| t.date().and_hms(0, 0, 0).with_timezone(&chrono::Utc)),
116      zone: self.zone,
117    }
118  }
119
120  /// Returns a new time at the start of the hour of this time.
121  pub fn start_of_hour(&self) -> Time {
122    Time {
123      inner: in_zone!(self, |t| { t.date().and_hms(t.hour(), 0, 0).with_timezone(&chrono::Utc) }),
124      zone: self.zone,
125    }
126  }
127
128  /// Converts to the local time zone.
129  pub const fn to_local(&self) -> Self {
130    self.to_zone(LOCAL)
131  }
132
133  /// Converts to UTC.
134  pub const fn to_utc(&self) -> Self {
135    self.to_zone(UTC)
136  }
137
138  /// Converts to the given time zone.
139  pub const fn to_zone(&self, zone: Zone) -> Self {
140    Self { inner: self.inner, zone }
141  }
142
143  /// Converts to a `NaiveDateTime`.
144  #[cfg(feature = "postgres")]
145  fn to_naive(&self) -> chrono::NaiveDateTime {
146    in_zone!(self, |t| t.naive_local())
147  }
148}
149
150// Implement operators.
151
152impl PartialEq for Time {
153  fn eq(&self, other: &Self) -> bool {
154    self.inner == other.inner
155  }
156}
157
158impl Add<Duration> for Time {
159  type Output = Self;
160
161  fn add(self, rhs: Duration) -> Self::Output {
162    let rhs: chrono::Duration = rhs.into();
163
164    Self { inner: self.inner + rhs, zone: self.zone }
165  }
166}
167
168impl Debug for Time {
169  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170    write!(f, "\"{}\"", self.format("%+"))
171  }
172}
173
174impl Display for Time {
175  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176    self.as_rfc3339().fmt(f)
177  }
178}
179
180impl Eq for Time {}
181
182impl Hash for Time {
183  fn hash<H: Hasher>(&self, state: &mut H) {
184    self.inner.hash(state)
185  }
186}
187
188impl Ord for Time {
189  fn cmp(&self, other: &Self) -> cmp::Ordering {
190    self.inner.cmp(&other.inner)
191  }
192}
193
194impl PartialOrd for Time {
195  fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
196    self.inner.partial_cmp(&other.inner)
197  }
198}
199
200impl Sub<Duration> for Time {
201  type Output = Self;
202
203  fn sub(self, rhs: Duration) -> Self::Output {
204    let rhs: chrono::Duration = rhs.into();
205
206    Self { inner: self.inner - rhs, zone: self.zone }
207  }
208}
209
210impl Sub<Time> for Time {
211  type Output = Duration;
212
213  fn sub(self, rhs: Time) -> Self::Output {
214    (self.inner - rhs.inner).into()
215  }
216}
217
218// Implement conversion to and from postgres.
219
220cfg_if! {
221  if #[cfg(feature = "postgres")] {
222    use postgres_types as pg;
223
224    impl<'a> pg::FromSql<'a> for Time {
225      fn from_sql(ty: &pg::Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>>{
226        Ok(Self { inner: pg::FromSql::from_sql(ty, raw)?, zone: Zone::Local })
227      }
228
229      fn accepts(ty: &pg::Type) -> bool {
230        ty.oid() == pg::Type::TIMESTAMPTZ.oid()
231      }
232    }
233
234    impl pg::ToSql for Time {
235      fn to_sql(&self, ty: &pg::Type, out: &mut bytes::BytesMut) -> Result<pg::IsNull, Box<dyn Error + Sync + Send>>
236      where
237        Self: Sized,
238      {
239        if ty.oid() == pg::Type::TIMESTAMP.oid() {
240          self.to_naive().to_sql(ty, out)
241        } else {
242          self.inner.to_sql(ty, out)
243        }
244      }
245
246      fn accepts(ty: &pg::Type) -> bool
247      where
248        Self: Sized,
249      {
250        ty.oid() == pg::Type::TIMESTAMP.oid() || ty.oid() == pg::Type::TIMESTAMPTZ.oid()
251      }
252
253      fn to_sql_checked(&self, ty: &pg::Type, out: &mut bytes::BytesMut) -> Result<pg::IsNull, Box<dyn Error + Sync + Send>> {
254        if ty.oid() == pg::Type::TIMESTAMP.oid() {
255          self.to_naive().to_sql_checked(ty, out)
256        } else {
257          self.inner.to_sql_checked(ty, out)
258        }
259      }
260    }
261  }
262}