Skip to main content

tick/fmt/
unix_seconds.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use std::fmt::{self, Debug, Display, Formatter};
5use std::str::FromStr;
6use std::time::{Duration, SystemTime};
7
8use crate::Error;
9use crate::fmt::{Iso8601, Rfc2822};
10
11/// A system time represented as the number of whole seconds since the Unix epoch.
12///
13/// Examples:
14///
15/// - `0` is equal to `Thu, 1 Jan 1970 00:00:00 -0000`
16/// - `951786000` is equal to `Tue, 29 Feb 2000 01:00:00 -0000`
17///
18/// # UTC and time zones
19///
20/// The seconds are always represented in the UTC time zone.
21///
22/// # Serialization and deserialization
23///
24/// `UnixSeconds` implements the `Serialize` and `Deserialize` traits from the `serde_core` crate.
25/// The system time is serialized as whole seconds. Fractional seconds are rounded down.
26///
27/// The serialization support is available when `serde` feature is enabled.
28///
29/// # Leap seconds
30///
31/// This value represents the number of non-leap seconds since the Unix epoch.
32///
33/// # Examples
34///
35/// ### Parsing and formatting
36///
37/// This example demonstrates how to parse Unix seconds and convert them to [`SystemTime`].
38///
39/// ```
40/// use std::time::{Duration, SystemTime};
41///
42/// use tick::fmt::UnixSeconds;
43///
44/// let unix_seconds = "9999".parse::<UnixSeconds>()?;
45/// assert_eq!(unix_seconds.to_string(), "9999");
46///
47/// let system_time: SystemTime = unix_seconds.into();
48/// assert_eq!(
49///     system_time,
50///     SystemTime::UNIX_EPOCH + Duration::from_secs(9999)
51/// );
52///
53/// # Ok::<(), Box<dyn std::error::Error>>(())
54/// ```
55#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub struct UnixSeconds(pub(super) Duration);
57
58crate::thread_aware_move!(UnixSeconds);
59
60impl UnixSeconds {
61    /// The largest value that be can represented by `UnixSeconds`.
62    ///
63    /// This represents a Unix system time of `31 December 9999 23:59:59 UTC`.
64    // NOTE: This value is aligned with the max jiff timestamp for easier interoperability.
65    pub const MAX: Self = Self(Duration::new(253_402_207_200, 999_999_999));
66
67    /// The Unix epoch represented as `UnixSeconds`.
68    ///
69    /// This represents a Unix system time of `1 January 1970 00:00:00 UTC`.
70    pub const UNIX_EPOCH: Self = Self(Duration::ZERO);
71
72    /// Creates a new `UnixSeconds` from the given number of seconds since the Unix epoch.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the provided seconds are out of range.
77    ///
78    /// ```
79    /// use tick::fmt::UnixSeconds;
80    ///
81    /// UnixSeconds::from_secs(u64::MAX).unwrap_err();
82    /// ```
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use std::time::{Duration, SystemTime};
88    ///
89    /// use tick::fmt::UnixSeconds;
90    ///
91    /// let unix_seconds = UnixSeconds::from_secs(10).unwrap();
92    /// let system_time: SystemTime = unix_seconds.into();
93    ///
94    /// assert_eq!(
95    ///     system_time,
96    ///     SystemTime::UNIX_EPOCH + Duration::from_secs(10)
97    /// );
98    /// ```
99    pub fn from_secs(seconds: u64) -> Result<Self, Error> {
100        Self::try_from(Duration::from_secs(seconds)).map_err(|_error| {
101            Error::out_of_range("the `seconds` is greater than the maximum value that can be represented by `UnixSeconds`")
102        })
103    }
104
105    /// Returns the number of whole seconds since the Unix epoch.
106    #[must_use]
107    pub fn to_secs(self) -> u64 {
108        self.0.as_secs()
109    }
110}
111
112impl FromStr for UnixSeconds {
113    type Err = Error;
114
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        let secs: u64 = s.parse().map_err(Error::other)?;
117        Self::from_secs(secs)
118    }
119}
120
121impl Display for UnixSeconds {
122    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
123        write!(f, "{}", self.to_secs())
124    }
125}
126
127impl From<UnixSeconds> for SystemTime {
128    fn from(value: UnixSeconds) -> Self {
129        Self::UNIX_EPOCH + value.0
130    }
131}
132
133impl TryFrom<Duration> for UnixSeconds {
134    type Error = Error;
135
136    fn try_from(value: Duration) -> Result<Self, Self::Error> {
137        if value > Self::MAX.0 {
138            return Err(Error::out_of_range(
139                "the `duration` is greater than the maximum value that can be represented by `UnixSeconds`",
140            ));
141        }
142
143        Ok(Self(value))
144    }
145}
146
147impl TryFrom<SystemTime> for UnixSeconds {
148    type Error = crate::Error;
149
150    fn try_from(value: SystemTime) -> Result<Self, Self::Error> {
151        Self::try_from(value.duration_since(SystemTime::UNIX_EPOCH).unwrap_or(Duration::ZERO))
152    }
153}
154
155impl From<Rfc2822> for UnixSeconds {
156    fn from(value: Rfc2822) -> Self {
157        Self(value.to_unix_epoch_duration())
158    }
159}
160
161impl From<Iso8601> for UnixSeconds {
162    fn from(value: Iso8601) -> Self {
163        Self(value.to_unix_epoch_duration())
164    }
165}
166
167#[cfg(any(feature = "serde", test))]
168impl serde_core::Serialize for UnixSeconds {
169    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170    where
171        S: serde_core::Serializer,
172    {
173        serializer.serialize_u64(self.to_secs())
174    }
175}
176
177#[cfg(any(feature = "serde", test))]
178impl<'de> serde_core::Deserialize<'de> for UnixSeconds {
179    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
180    where
181        D: serde_core::Deserializer<'de>,
182    {
183        let secs = u64::deserialize(deserializer).map_err(serde_core::de::Error::custom)?;
184        Self::from_secs(secs).map_err(serde_core::de::Error::custom)
185    }
186}
187
188#[cfg_attr(coverage_nightly, coverage(off))]
189#[cfg(test)]
190mod tests {
191    use std::hash::Hash;
192
193    use jiff::Timestamp;
194
195    use super::*;
196
197    static_assertions::assert_impl_all!(UnixSeconds: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, TryFrom<SystemTime>, From<Iso8601>, FromStr);
198
199    #[test]
200    fn max_duration_is_jiff_duration() {
201        let jiff_max = Timestamp::MAX.duration_since(Timestamp::UNIX_EPOCH).unsigned_abs();
202
203        // equals to 123
204        assert_eq!(UnixSeconds::MAX.0, jiff_max);
205    }
206
207    #[test]
208    fn from_secs() {
209        let ts = UnixSeconds::from_secs(10).unwrap();
210
211        assert_eq!(ts.to_secs(), 10);
212    }
213
214    #[test]
215    fn try_from_duration() {
216        let ts = UnixSeconds::try_from(Duration::MAX).unwrap_err();
217        assert_eq!(
218            ts.to_string(),
219            "the `duration` is greater than the maximum value that can be represented by `UnixSeconds`"
220        );
221    }
222
223    #[test]
224    fn from_secs_error() {
225        let error = UnixSeconds::from_secs(u64::MAX).unwrap_err();
226
227        assert_eq!(
228            error.to_string(),
229            "the `seconds` is greater than the maximum value that can be represented by `UnixSeconds`"
230        );
231    }
232
233    #[test]
234    fn to_system_time() {
235        let ts = UnixSeconds::from_secs(10).unwrap();
236        let system_time: SystemTime = ts.into();
237
238        assert_eq!(system_time, SystemTime::UNIX_EPOCH + Duration::from_secs(10));
239    }
240
241    #[test]
242    fn parse_err() {
243        "date".parse::<UnixSeconds>().unwrap_err();
244    }
245
246    #[test]
247    fn parse_min() {
248        let stamp: UnixSeconds = "0".parse().unwrap();
249        assert_eq!(SystemTime::from(stamp), SystemTime::UNIX_EPOCH);
250    }
251
252    #[test]
253    fn parse_then_display() {
254        let stamp: UnixSeconds = "3600".parse().unwrap();
255
256        // Display should return the timestamp as seconds
257        assert_eq!(stamp.to_string(), "3600");
258    }
259
260    #[test]
261    fn parse_max() {
262        let max = UnixSeconds::MAX;
263
264        let stamp: UnixSeconds = max.to_string().parse().unwrap();
265
266        assert_eq!(stamp.to_string(), max.to_string());
267    }
268
269    #[test]
270    fn parse_max_overflow() {
271        "99999999999999999999999".parse::<UnixSeconds>().unwrap_err();
272    }
273
274    #[test]
275    #[cfg(feature = "serde")]
276    fn serialize_deserialize() {
277        let iso: UnixSeconds = "9999".parse().unwrap();
278        let serialized = serde_json::to_string(&iso).unwrap();
279        let deserialized: UnixSeconds = serde_json::from_str(&serialized).unwrap();
280
281        assert_eq!(iso, deserialized);
282    }
283
284    #[test]
285    fn iso_8601_roundtrip() {
286        let iso: UnixSeconds = "9999".parse().unwrap();
287        let iso_str = iso.to_string();
288        let parsed_iso: UnixSeconds = iso_str.parse().unwrap();
289
290        assert_eq!(iso, parsed_iso);
291    }
292
293    #[test]
294    fn iso_8601_roundtrip_with_timezone() {
295        let unix_seconds: UnixSeconds = "9999".parse().unwrap();
296        let iso: Iso8601 = unix_seconds.into();
297
298        assert_eq!(iso.to_string(), "1970-01-01T02:46:39Z");
299
300        let iso: Iso8601 = UnixSeconds::MAX.into();
301        assert_eq!(iso, Iso8601::MAX);
302
303        let iso: Iso8601 = UnixSeconds::UNIX_EPOCH.into();
304        assert_eq!(iso, Iso8601::UNIX_EPOCH);
305    }
306
307    #[test]
308    fn try_from_max_ensure_accepted() {
309        let unix_seconds = UnixSeconds::try_from(UnixSeconds::MAX.0).unwrap();
310        assert_eq!(unix_seconds, UnixSeconds::MAX);
311    }
312}