reqsign_core/
time.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Time related utils.
19
20use crate::Error;
21use std::fmt;
22use std::ops::{Add, AddAssign, Sub, SubAssign};
23use std::str::FromStr;
24use std::time::{Duration, SystemTime};
25
26/// An instant in time represented as the number of nanoseconds since the Unix epoch.
27#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct Timestamp(jiff::Timestamp);
29
30impl FromStr for Timestamp {
31    type Err = Error;
32
33    /// Parse a timestamp by the default [`DateTimeParser`].
34    ///
35    /// All of them are valid time:
36    ///
37    /// - `2022-03-13T07:20:04Z`
38    /// - `2022-03-01T08:12:34+00:00`
39    /// - `2022-03-01T08:12:34.00+00:00`
40    /// - `2022-07-08T02:14:07+02:00[Europe/Paris]`
41    ///
42    /// [`DateTimeParser`]: jiff::fmt::temporal::DateTimeParser
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s.parse() {
45            Ok(t) => Ok(Timestamp(t)),
46            Err(err) => Err(
47                Error::unexpected(format!("parse '{s}' into timestamp failed")).with_source(err),
48            ),
49        }
50    }
51}
52
53impl fmt::Display for Timestamp {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}", self.0)
56    }
57}
58
59impl Timestamp {
60    /// Create the timestamp of now.
61    pub fn now() -> Self {
62        Self(jiff::Timestamp::now())
63    }
64
65    /// Format the timestamp into date: `20220301`
66    pub fn format_date(self) -> String {
67        self.0.strftime("%Y%m%d").to_string()
68    }
69
70    /// Format the timestamp into ISO8601: `20220313T072004Z`
71    pub fn format_iso8601(self) -> String {
72        self.0.strftime("%Y%m%dT%H%M%SZ").to_string()
73    }
74
75    /// Format the timestamp into http date: `Sun, 06 Nov 1994 08:49:37 GMT`
76    ///
77    /// ## Note
78    ///
79    /// HTTP date is slightly different from RFC2822.
80    ///
81    /// - Timezone is fixed to GMT.
82    /// - Day must be 2 digit.
83    pub fn format_http_date(self) -> String {
84        self.0.strftime("%a, %d %b %Y %T GMT").to_string()
85    }
86
87    /// Format the timestamp into RFC3339 in Zulu: `2022-03-13T07:20:04Z`
88    pub fn format_rfc3339_zulu(self) -> String {
89        self.0.strftime("%FT%TZ").to_string()
90    }
91
92    /// Returns this timestamp as a number of seconds since the Unix epoch.
93    ///
94    /// This only returns the number of whole seconds. That is, if there are
95    /// any fractional seconds in this timestamp, then they are truncated.
96    pub fn as_second(self) -> i64 {
97        self.0.as_second()
98    }
99
100    /// Returns the fractional second component of this timestamp in units of
101    /// nanoseconds.
102    ///
103    /// It is guaranteed that this will never return a value that is greater
104    /// than 1 second (or less than -1 second).
105    pub fn subsec_nanosecond(self) -> i32 {
106        self.0.subsec_nanosecond()
107    }
108
109    /// Convert to `SystemTime`.
110    pub fn as_system_time(self) -> SystemTime {
111        SystemTime::from(self.0)
112    }
113
114    /// Creates a new instant in time from the number of milliseconds elapsed
115    /// since the Unix epoch.
116    ///
117    /// When `millisecond` is negative, it corresponds to an instant in time
118    /// before the Unix epoch. A smaller number corresponds to an instant in
119    /// time further into the past.
120    pub fn from_millisecond(millis: i64) -> crate::Result<Self> {
121        match jiff::Timestamp::from_millisecond(millis) {
122            Ok(t) => Ok(Timestamp(t)),
123            Err(err) => Err(Error::unexpected(format!(
124                "convert '{millis}' milliseconds into timestamp failed"
125            ))
126            .with_source(err)),
127        }
128    }
129
130    /// Creates a new instant in time from the number of seconds elapsed since
131    /// the Unix epoch.
132    ///
133    /// When `second` is negative, it corresponds to an instant in time before
134    /// the Unix epoch. A smaller number corresponds to an instant in time
135    /// further into the past.
136    pub fn from_second(second: i64) -> crate::Result<Self> {
137        match jiff::Timestamp::from_second(second) {
138            Ok(t) => Ok(Timestamp(t)),
139            Err(err) => Err(Error::unexpected(format!(
140                "convert '{second}' seconds into timestamp failed"
141            ))
142            .with_source(err)),
143        }
144    }
145
146    /// Parse a timestamp from RFC2822.
147    ///
148    /// All of them are valid time:
149    ///
150    /// - `Sat, 13 Jul 2024 15:09:59 -0400`
151    /// - `Mon, 15 Aug 2022 16:50:12 GMT`
152    pub fn parse_rfc2822(s: &str) -> crate::Result<Timestamp> {
153        match jiff::fmt::rfc2822::parse(s) {
154            Ok(zoned) => Ok(Timestamp(zoned.timestamp())),
155            Err(err) => {
156                Err(Error::unexpected(format!("parse '{s}' into rfc2822 failed")).with_source(err))
157            }
158        }
159    }
160
161    /// Parse the string format "2023-10-31 21:59:10.000000".
162    pub fn parse_datetime_utc(s: &str) -> crate::Result<Timestamp> {
163        let dt = s.parse::<jiff::civil::DateTime>().map_err(|err| {
164            Error::unexpected(format!("parse '{s}' into datetime failed")).with_source(err)
165        })?;
166
167        let ts = jiff::tz::TimeZone::UTC.to_timestamp(dt).map_err(|err| {
168            Error::unexpected(format!("convert '{s}' into timestamp failed")).with_source(err)
169        })?;
170
171        Ok(Timestamp(ts))
172    }
173}
174
175impl Add<Duration> for Timestamp {
176    type Output = Timestamp;
177
178    fn add(self, rhs: Duration) -> Timestamp {
179        let ts = self
180            .0
181            .checked_add(rhs)
182            .expect("adding unsigned duration to timestamp overflowed");
183
184        Timestamp(ts)
185    }
186}
187
188impl AddAssign<Duration> for Timestamp {
189    fn add_assign(&mut self, rhs: Duration) {
190        *self = *self + rhs
191    }
192}
193
194impl Sub<Duration> for Timestamp {
195    type Output = Timestamp;
196
197    fn sub(self, rhs: Duration) -> Timestamp {
198        let ts = self
199            .0
200            .checked_sub(rhs)
201            .expect("subtracting unsigned duration from timestamp overflowed");
202
203        Timestamp(ts)
204    }
205}
206
207impl SubAssign<Duration> for Timestamp {
208    fn sub_assign(&mut self, rhs: Duration) {
209        *self = *self - rhs
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn test_time() -> Timestamp {
218        Timestamp("2022-03-01T08:12:34Z".parse().unwrap())
219    }
220
221    #[test]
222    fn test_format_date() {
223        let t = test_time();
224        assert_eq!("20220301", t.format_date())
225    }
226
227    #[test]
228    fn test_format_ios8601() {
229        let t = test_time();
230        assert_eq!("20220301T081234Z", t.format_iso8601())
231    }
232
233    #[test]
234    fn test_format_http_date() {
235        let t = test_time();
236        assert_eq!("Tue, 01 Mar 2022 08:12:34 GMT", t.format_http_date())
237    }
238
239    #[test]
240    fn test_format_rfc3339() {
241        let t = test_time();
242        assert_eq!("2022-03-01T08:12:34Z", t.format_rfc3339_zulu())
243    }
244
245    #[test]
246    fn test_parse_rfc3339() {
247        let t = test_time();
248
249        for v in [
250            "2022-03-01T08:12:34Z",
251            "2022-03-01T08:12:34+00:00",
252            "2022-03-01T08:12:34.00+00:00",
253        ] {
254            assert_eq!(t, v.parse().expect("must be valid time"));
255        }
256    }
257}