Skip to main content

defuse_time/
lib.rs

1#[cfg(feature = "arbitrary")]
2pub mod arbitrary;
3#[cfg(feature = "borsh")]
4pub mod borsh;
5#[cfg(feature = "serde")]
6pub mod serde;
7
8mod error;
9pub use self::error::*;
10
11use core::{
12    ops::{Add, AddAssign, Sub, SubAssign},
13    time::Duration,
14};
15
16/// A Unix timestamp
17#[cfg_attr(
18    feature = "serde",
19    derive(::serde_with::SerializeDisplay, ::serde_with::DeserializeFromStr),
20    cfg_attr(
21        feature = "schemars-v0_8",
22        derive(::schemars::JsonSchema),
23        schemars(example = "Self::default", example = "Self::example")
24    )
25)]
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct Timestamp(
28    // schemars@0.8 ignores `with` at struct level for newtypes; must be on the field
29    #[cfg_attr(feature = "schemars-v0_8", schemars(with = "String"))] ::time::Timestamp,
30);
31
32impl Timestamp {
33    pub const MIN: Self = Self(::time::Timestamp::MIN);
34    pub const UNIX_EPOCH: Self = Self(::time::Timestamp::UNIX_EPOCH);
35    pub const MAX: Self = Self(::time::Timestamp::MAX);
36
37    #[cfg(feature = "std")]
38    #[must_use]
39    #[inline]
40    pub fn now() -> Self {
41        cfg_select! {
42            near => {
43                Self::from_nanos(
44                    ::near_sdk::env::block_timestamp().into(),
45                ).ok_or(Overflow).unwrap()
46            }
47            _ => Self(::time::Timestamp::now()),
48        }
49    }
50
51    #[must_use]
52    #[inline]
53    pub const fn from_nanos(nanos: i128) -> Option<Self> {
54        let Ok(ts) = ::time::Timestamp::from_nanoseconds(nanos) else {
55            return None;
56        };
57        Some(Self(ts))
58    }
59
60    #[must_use]
61    #[inline]
62    pub const fn from_micros(micros: i128) -> Option<Self> {
63        let Ok(ts) = ::time::Timestamp::from_microseconds(micros) else {
64            return None;
65        };
66        Some(Self(ts))
67    }
68
69    #[must_use]
70    #[inline]
71    pub const fn from_millis(millis: i64) -> Option<Self> {
72        let Ok(ts) = ::time::Timestamp::from_milliseconds(millis) else {
73            return None;
74        };
75        Some(Self(ts))
76    }
77
78    #[must_use]
79    #[inline]
80    pub const fn from_secs(secs: i64) -> Option<Self> {
81        let Ok(ts) = ::time::Timestamp::from_seconds(secs) else {
82            return None;
83        };
84        Some(Self(ts))
85    }
86
87    #[must_use]
88    #[inline]
89    pub fn checked_add_unsigned(self, rhs: Duration) -> Option<Self> {
90        let rhs: ::time::Duration = rhs.try_into().ok()?;
91        self.0.checked_add(rhs).map(Self)
92    }
93
94    #[must_use]
95    #[inline]
96    pub fn checked_sub_unsigned(self, rhs: Duration) -> Option<Self> {
97        let rhs: ::time::Duration = rhs.try_into().ok()?;
98        self.0.checked_sub(rhs).map(Self)
99    }
100
101    #[must_use]
102    #[inline]
103    pub fn saturating_add_unsigned(self, rhs: Duration) -> Self {
104        Self(
105            self.0
106                .saturating_add(rhs.try_into().unwrap_or(::time::Duration::MAX)),
107        )
108    }
109
110    #[must_use]
111    #[inline]
112    pub fn saturating_sub_unsigned(self, rhs: Duration) -> Self {
113        Self(
114            self.0
115                .saturating_sub(rhs.try_into().unwrap_or(::time::Duration::MAX)),
116        )
117    }
118
119    #[inline]
120    pub fn duration_since(&self, other: Self) -> Result<Duration, Duration> {
121        let dur = self.0 - other.0;
122        if dur.is_negative() {
123            return Err(dur.unsigned_abs());
124        }
125        Ok(dur.unsigned_abs())
126    }
127
128    #[must_use]
129    #[inline]
130    pub const fn truncate_subsecs(self) -> Self {
131        let Ok(ts) = self.0.replace_nanosecond(0) else {
132            unreachable!()
133        };
134        Self(ts)
135    }
136
137    #[must_use]
138    #[inline]
139    pub const fn as_nanos(&self) -> i128 {
140        self.0.as_nanoseconds()
141    }
142
143    #[must_use]
144    #[inline]
145    pub const fn as_micros(&self) -> i128 {
146        self.0.as_microseconds()
147    }
148
149    #[must_use]
150    #[inline]
151    pub const fn as_millis(&self) -> i64 {
152        self.0.as_milliseconds()
153    }
154
155    #[must_use]
156    #[inline]
157    pub const fn as_secs(&self) -> i64 {
158        self.0.as_seconds()
159    }
160
161    #[cfg(feature = "schemars-v0_8")]
162    const fn example() -> Self {
163        #[allow(clippy::inconsistent_digit_grouping)]
164        Self::from_nanos(1782395622_123456789).unwrap()
165    }
166}
167
168impl Default for Timestamp {
169    #[inline]
170    fn default() -> Self {
171        Self::UNIX_EPOCH
172    }
173}
174
175impl Add<Duration> for Timestamp {
176    type Output = Self;
177
178    #[inline]
179    fn add(self, rhs: Duration) -> Self::Output {
180        self.checked_add_unsigned(rhs).ok_or(Overflow).unwrap()
181    }
182}
183
184impl AddAssign<Duration> for Timestamp {
185    #[inline]
186    fn add_assign(&mut self, rhs: Duration) {
187        *self = *self + rhs;
188    }
189}
190
191impl Sub<Duration> for Timestamp {
192    type Output = Self;
193
194    #[inline]
195    fn sub(self, rhs: Duration) -> Self::Output {
196        self.checked_sub_unsigned(rhs).ok_or(Overflow).unwrap()
197    }
198}
199
200impl SubAssign<Duration> for Timestamp {
201    #[inline]
202    fn sub_assign(&mut self, rhs: Duration) {
203        *self = *self - rhs;
204    }
205}
206
207#[cfg(feature = "formatting")]
208const _: () = {
209    use core::fmt::{self, Display};
210
211    use time::format_description::well_known::Rfc3339;
212
213    impl Display for Timestamp {
214        #[inline]
215        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216            f.write_str(&self.0.format(&Rfc3339).map_err(|_| fmt::Error)?)
217        }
218    }
219};
220
221#[cfg(feature = "parsing")]
222const _: () = {
223    use core::str::FromStr;
224
225    use time::format_description::well_known::Rfc3339;
226
227    impl FromStr for Timestamp {
228        type Err = time::error::Parse;
229
230        #[inline]
231        fn from_str(s: &str) -> Result<Self, Self::Err> {
232            ::time::Timestamp::parse(s, &Rfc3339).map(Self)
233        }
234    }
235};
236
237#[cfg(test)]
238#[allow(clippy::inconsistent_digit_grouping)]
239mod tests {
240    use rstest::rstest;
241
242    use super::*;
243
244    #[rstest]
245    fn nanos_roundtrip(
246        #[values(
247            0, 123, -123,
248            123456, -123456,
249            1782395622_123456789, -1782395622_123456789,
250        )]
251        nanos: i128,
252    ) {
253        assert_eq!(nanos, Timestamp::from_nanos(nanos).unwrap().as_nanos());
254    }
255
256    #[rstest]
257    fn micros_roundtrip(
258        #[values(
259            0, 123, -123,
260            123456, -123456,
261            1782395622_123456, -1782395622_123456,
262        )]
263        micros: i128,
264    ) {
265        assert_eq!(micros, Timestamp::from_micros(micros).unwrap().as_micros());
266    }
267
268    #[rstest]
269    fn millis_roundtrip(
270        #[values(
271            0, 123, -123,
272            123456, -123456,
273            1782395622_123, -1782395622_123,
274        )]
275        millis: i64,
276    ) {
277        assert_eq!(millis, Timestamp::from_millis(millis).unwrap().as_millis());
278    }
279
280    #[rstest]
281    fn secs_roundtrip(
282        #[values(
283            0, 123, -123,
284            123456, -123456,
285            1782395622, -1782395622,
286        )]
287        secs: i64,
288    ) {
289        assert_eq!(secs, Timestamp::from_secs(secs).unwrap().as_secs());
290    }
291
292    #[rstest]
293    #[case(0, "1970-01-01T00:00:00Z")]
294    #[case(1782395622_123456789, "2026-06-25T13:53:42.123456789Z")]
295    fn rfc3339_roundtrip(#[case] nanos: i128, #[case] s: &str) {
296        let ts = Timestamp::from_nanos(nanos).unwrap();
297        assert_eq!(ts.to_string(), s);
298
299        let got: Timestamp = s.parse().expect("parse");
300        assert_eq!(got.as_nanos(), nanos);
301    }
302
303    #[rstest]
304    #[case("1970-01-01T00:00:00+00:00", "1970-01-01T00:00:00Z")]
305    #[case(
306        "2026-06-25T09:53:42.123456789-04:00",
307        "2026-06-25T13:53:42.123456789Z"
308    )]
309    #[case(
310        "2026-06-25T13:53:42.123456789+00:00",
311        "2026-06-25T13:53:42.123456789Z"
312    )]
313    #[case(
314        "2026-06-25T17:53:42.123456789+04:00",
315        "2026-06-25T13:53:42.123456789Z"
316    )]
317    fn rfc3339_normalize_offset(#[case] offset: &str, #[case] normalized: &str) {
318        let ts: Timestamp = offset.parse().unwrap();
319        assert_eq!(ts.to_string(), normalized);
320    }
321}