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#[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 #[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}