1use core::{
4 convert::{TryFrom, TryInto},
5 fmt,
6 ops::{Add, Sub},
7 str::FromStr,
8 time::Duration,
9};
10
11use celestia_tendermint_proto::{google::protobuf::Timestamp, serializers::timestamp, Protobuf};
12#[cfg(all(feature = "clock", target_arch = "wasm32", feature = "wasm-bindgen"))]
13use instant::SystemTime;
14use serde::{Deserialize, Serialize};
15use time::{
16 format_description::well_known::Rfc3339,
17 macros::{datetime, offset},
18 OffsetDateTime, PrimitiveDateTime,
19};
20
21use crate::{error::Error, prelude::*};
22
23#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
40#[serde(try_from = "Timestamp", into = "Timestamp")]
41pub struct Time(PrimitiveDateTime);
42
43impl Protobuf<Timestamp> for Time {}
44
45impl TryFrom<Timestamp> for Time {
46 type Error = Error;
47
48 fn try_from(value: Timestamp) -> Result<Self, Error> {
49 let nanos = value
50 .nanos
51 .try_into()
52 .map_err(|_| Error::timestamp_nanos_out_of_range())?;
53 Self::from_unix_timestamp(value.seconds, nanos)
54 }
55}
56
57impl From<Time> for Timestamp {
58 fn from(value: Time) -> Self {
59 let t = value.0.assume_utc();
60 let seconds = t.unix_timestamp();
61 let nanos = t.nanosecond() as i32;
64 Timestamp { seconds, nanos }
65 }
66}
67
68impl Time {
69 #[cfg(all(feature = "clock", not(target_arch = "wasm32")))]
70 pub fn now() -> Time {
71 OffsetDateTime::now_utc().try_into().unwrap()
72 }
73
74 #[cfg(all(feature = "clock", target_arch = "wasm32", feature = "wasm-bindgen"))]
75 pub fn now() -> Time {
76 SystemTime::now().try_into().unwrap()
77 }
78
79 fn from_utc(t: OffsetDateTime) -> Result<Self, Error> {
83 debug_assert_eq!(t.offset(), offset!(UTC));
84 match t.year() {
85 1..=9999 => Ok(Self(PrimitiveDateTime::new(t.date(), t.time()))),
86 _ => Err(Error::date_out_of_range()),
87 }
88 }
89
90 pub fn unix_epoch() -> Self {
92 Self(datetime!(1970-01-01 00:00:00))
93 }
94
95 pub fn from_unix_timestamp(secs: i64, nanos: u32) -> Result<Self, Error> {
96 if nanos > 999_999_999 {
97 return Err(Error::timestamp_nanos_out_of_range());
98 }
99 let total_nanos = secs as i128 * 1_000_000_000 + nanos as i128;
100 match OffsetDateTime::from_unix_timestamp_nanos(total_nanos) {
101 Ok(odt) => Self::from_utc(odt),
102 _ => Err(Error::timestamp_conversion()),
103 }
104 }
105
106 pub fn duration_since(&self, other: Time) -> Result<Duration, Error> {
109 let duration = self.0.assume_utc() - other.0.assume_utc();
110 duration
111 .try_into()
112 .map_err(|_| Error::duration_out_of_range())
113 }
114
115 pub fn parse_from_rfc3339(s: &str) -> Result<Self, Error> {
117 let date = OffsetDateTime::parse(s, &Rfc3339)
118 .map_err(Error::time_parse)?
119 .to_offset(offset!(UTC));
120 Self::from_utc(date)
121 }
122
123 pub fn to_rfc3339(&self) -> String {
125 timestamp::to_rfc3339_nanos(self.0.assume_utc())
126 }
127
128 pub fn unix_timestamp(&self) -> i64 {
130 self.0.assume_utc().unix_timestamp()
131 }
132
133 pub fn unix_timestamp_nanos(&self) -> i128 {
135 self.0.assume_utc().unix_timestamp_nanos()
136 }
137
138 pub fn checked_add(self, duration: Duration) -> Option<Self> {
140 let duration = duration.try_into().ok()?;
141 let t = self.0.checked_add(duration)?;
142 Self::from_utc(t.assume_utc()).ok()
143 }
144
145 pub fn checked_sub(self, duration: Duration) -> Option<Self> {
147 let duration = duration.try_into().ok()?;
148 let t = self.0.checked_sub(duration)?;
149 Self::from_utc(t.assume_utc()).ok()
150 }
151
152 pub fn before(&self, other: Time) -> bool {
154 self.0.assume_utc() < other.0.assume_utc()
155 }
156
157 pub fn after(&self, other: Time) -> bool {
159 self.0.assume_utc() > other.0.assume_utc()
160 }
161}
162
163impl fmt::Display for Time {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
165 timestamp::fmt_as_rfc3339_nanos(self.0.assume_utc(), f)
166 }
167}
168
169impl FromStr for Time {
170 type Err = Error;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
173 Self::parse_from_rfc3339(s)
174 }
175}
176
177impl TryFrom<OffsetDateTime> for Time {
178 type Error = Error;
179
180 fn try_from(t: OffsetDateTime) -> Result<Time, Error> {
181 Self::from_utc(t.to_offset(offset!(UTC)))
182 }
183}
184
185impl From<Time> for OffsetDateTime {
186 fn from(t: Time) -> OffsetDateTime {
187 t.0.assume_utc()
188 }
189}
190
191#[cfg(all(feature = "clock", target_arch = "wasm32", feature = "wasm-bindgen"))]
192impl TryFrom<SystemTime> for Time {
193 type Error = Error;
194
195 fn try_from(t: SystemTime) -> Result<Time, Self::Error> {
196 let since_epoch = t
197 .duration_since(SystemTime::UNIX_EPOCH)
198 .map_err(|_| Error::date_out_of_range())?;
199
200 Time::from_unix_timestamp(
201 since_epoch
202 .as_secs()
203 .try_into()
204 .map_err(|_| Error::date_out_of_range())?,
205 since_epoch.subsec_nanos(),
206 )
207 }
208}
209
210impl Add<Duration> for Time {
211 type Output = Result<Self, Error>;
212
213 fn add(self, rhs: Duration) -> Self::Output {
214 let duration = rhs.try_into().map_err(|_| Error::duration_out_of_range())?;
215 let t = self
216 .0
217 .checked_add(duration)
218 .ok_or_else(Error::duration_out_of_range)?;
219 Self::from_utc(t.assume_utc())
220 }
221}
222
223impl Sub<Duration> for Time {
224 type Output = Result<Self, Error>;
225
226 fn sub(self, rhs: Duration) -> Self::Output {
227 let duration = rhs.try_into().map_err(|_| Error::duration_out_of_range())?;
228 let t = self
229 .0
230 .checked_sub(duration)
231 .ok_or_else(Error::duration_out_of_range)?;
232 Self::from_utc(t.assume_utc())
233 }
234}
235
236pub trait ParseTimestamp {
238 fn parse_timestamp(&self) -> Result<Time, Error>;
240}
241
242#[cfg(test)]
243mod tests {
244 use proptest::{prelude::*, sample::select};
245 use tendermint_pbt_gen as pbt;
246 use time::{Date, Month::*};
247
248 use super::*;
249 use crate::error::ErrorDetail;
250
251 fn particular_rfc3339_timestamps() -> impl Strategy<Value = String> {
253 let strs: Vec<String> = vec![
254 "0001-01-01T00:00:00Z",
255 "9999-12-31T23:59:59.999999999Z",
256 "2020-09-14T16:33:54.21191421Z",
257 "2020-09-14T16:33:00Z",
258 "2020-09-14T16:33:00.1Z",
259 "2020-09-14T16:33:00.211914212Z",
260 "1970-01-01T00:00:00Z",
261 "2021-01-07T20:25:56.0455760Z",
262 "2021-01-07T20:25:57.039219Z",
263 "2021-01-07T20:25:58.03562100Z",
264 "2021-01-07T20:25:59.000955200Z",
265 "2021-01-07T20:26:04.0121030Z",
266 "2021-01-07T20:26:05.005096Z",
267 "2021-01-07T20:26:09.08488400Z",
268 "2021-01-07T20:26:11.0875340Z",
269 "2021-01-07T20:26:12.078268Z",
270 "2021-01-07T20:26:13.08074100Z",
271 "2021-01-07T20:26:15.079663000Z",
272 ]
273 .into_iter()
274 .map(String::from)
275 .collect();
276
277 select(strs)
278 }
279
280 fn particular_datetimes_out_of_range() -> impl Strategy<Value = OffsetDateTime> {
281 let dts = vec![
282 datetime!(0000-12-31 23:59:59.999999999 UTC),
283 datetime!(0001-01-01 00:00:00.999999999 +00:00:01),
284 Date::from_calendar_date(-1, October, 9)
285 .unwrap()
286 .midnight()
287 .assume_utc(),
288 ];
289 select(dts)
290 }
291
292 proptest! {
293 #[test]
294 fn can_parse_rfc3339_timestamps(stamp in pbt::time::arb_protobuf_safe_rfc3339_timestamp()) {
295 prop_assert!(stamp.parse::<Time>().is_ok())
296 }
297
298 #[test]
299 fn serde_from_value_is_the_inverse_of_to_value_within_reasonable_time_range(
300 datetime in pbt::time::arb_protobuf_safe_datetime()
301 ) {
302 let time: Time = datetime.try_into().unwrap();
305 let json_encoded_time = serde_json::to_value(time).unwrap();
306 let decoded_time: Time = serde_json::from_value(json_encoded_time).unwrap();
307 prop_assert_eq!(time, decoded_time);
308 }
309
310 #[test]
311 fn serde_of_rfc3339_timestamps_is_safe(
312 stamp in prop_oneof![
313 pbt::time::arb_protobuf_safe_rfc3339_timestamp(),
314 particular_rfc3339_timestamps(),
315 ]
316 ) {
317 let time: Time = stamp.parse().unwrap();
322 let json_encoded_time = serde_json::to_value(time).unwrap();
323 let decoded_time: Time = serde_json::from_value(json_encoded_time).unwrap();
324 prop_assert_eq!(time, decoded_time);
325 }
326
327 #[test]
328 fn conversion_unix_timestamp_is_safe(
329 stamp in prop_oneof![
330 pbt::time::arb_protobuf_safe_rfc3339_timestamp(),
331 particular_rfc3339_timestamps(),
332 ]
333 ) {
334 let time: Time = stamp.parse().unwrap();
335 let timestamp = time.unix_timestamp();
336 let parsed = Time::from_unix_timestamp(timestamp, 0).unwrap();
337 prop_assert_eq!(timestamp, parsed.unix_timestamp());
338 }
339
340 #[test]
341 fn conversion_from_datetime_succeeds_for_4_digit_ce_years(
342 datetime in prop_oneof![
343 pbt::time::arb_datetime_with_offset(),
344 particular_datetimes_out_of_range(),
345 ]
346 ) {
347 let res: Result<Time, _> = datetime.try_into();
348 match datetime.to_offset(offset!(UTC)).year() {
349 1 ..= 9999 => {
350 let t = res.unwrap();
351 let dt_converted_back: OffsetDateTime = t.into();
352 assert_eq!(dt_converted_back, datetime);
353 }
354 _ => {
355 let e = res.unwrap_err();
356 assert!(matches!(e.detail(), ErrorDetail::DateOutOfRange(_)))
357 }
358 }
359 }
360
361 #[test]
362 fn from_unix_timestamp_rejects_out_of_range_nanos(
363 datetime in pbt::time::arb_protobuf_safe_datetime(),
364 nanos in 1_000_000_000 ..= u32::MAX,
365 ) {
366 let secs = datetime.unix_timestamp();
367 let res = Time::from_unix_timestamp(secs, nanos);
368 let e = res.unwrap_err();
369 assert!(matches!(e.detail(), ErrorDetail::TimestampNanosOutOfRange(_)))
370 }
371 }
372
373 fn duration_from_nanos(whole_nanos: u128) -> Duration {
374 let secs: u64 = (whole_nanos / 1_000_000_000).try_into().unwrap();
375 let nanos = (whole_nanos % 1_000_000_000) as u32;
376 Duration::new(secs, nanos)
377 }
378
379 prop_compose! {
380 fn args_for_regular_add()
381 (t in pbt::time::arb_protobuf_safe_datetime())
382 (
383 t in Just(t),
384 d_nanos in 0 ..= (pbt::time::max_protobuf_time() - t).whole_nanoseconds() as u128,
385 ) -> (OffsetDateTime, Duration)
386 {
387 (t, duration_from_nanos(d_nanos))
388 }
389 }
390
391 prop_compose! {
392 fn args_for_regular_sub()
393 (t in pbt::time::arb_protobuf_safe_datetime())
394 (
395 t in Just(t),
396 d_nanos in 0 ..= (t - pbt::time::min_protobuf_time()).whole_nanoseconds() as u128,
397 ) -> (OffsetDateTime, Duration)
398 {
399 (t, duration_from_nanos(d_nanos))
400 }
401 }
402
403 prop_compose! {
404 fn args_for_overflowed_add()
405 (t in pbt::time::arb_protobuf_safe_datetime())
406 (
407 t in Just(t),
408 d_nanos in (
409 (pbt::time::max_protobuf_time() - t).whole_nanoseconds() as u128 + 1
410 ..=
411 Duration::MAX.as_nanos()
412 ),
413 ) -> (OffsetDateTime, Duration)
414 {
415 (t, duration_from_nanos(d_nanos))
416 }
417 }
418
419 prop_compose! {
420 fn args_for_overflowed_sub()
421 (t in pbt::time::arb_protobuf_safe_datetime())
422 (
423 t in Just(t),
424 d_nanos in (
425 (t - pbt::time::min_protobuf_time()).whole_nanoseconds() as u128 + 1
426 ..=
427 Duration::MAX.as_nanos()
428 ),
429 ) -> (OffsetDateTime, Duration)
430 {
431 (t, duration_from_nanos(d_nanos))
432 }
433 }
434
435 proptest! {
436 #[test]
437 fn checked_add_regular((dt, d) in args_for_regular_add()) {
438 let t: Time = dt.try_into().unwrap();
439 let t = t.checked_add(d).unwrap();
440 let res: OffsetDateTime = t.into();
441 assert_eq!(res, dt + d);
442 }
443
444 #[test]
445 fn checked_sub_regular((dt, d) in args_for_regular_sub()) {
446 let t: Time = dt.try_into().unwrap();
447 let t = t.checked_sub(d).unwrap();
448 let res: OffsetDateTime = t.into();
449 assert_eq!(res, dt - d);
450 }
451
452 #[test]
453 fn checked_add_overflow((dt, d) in args_for_overflowed_add()) {
454 let t: Time = dt.try_into().unwrap();
455 assert_eq!(t.checked_add(d), None);
456 }
457
458 #[test]
459 fn checked_sub_overflow((dt, d) in args_for_overflowed_sub()) {
460 let t: Time = dt.try_into().unwrap();
461 assert_eq!(t.checked_sub(d), None);
462 }
463 }
464}