1use std::{
4 fmt::{self, Display, Formatter},
5 ops::{Add, AddAssign, Sub, SubAssign},
6};
7
8use crate::TypeParseError;
9use candid::{CandidType, types::Serializer as CandidSerializer, types::Type};
10use serde::{Deserialize, Deserializer, Serialize};
11use time::{Date as TimeDate, Month, PrimitiveDateTime, Time as TimeOfDay, UtcOffset};
12
13#[cfg(test)]
14mod tests;
15
16#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
18#[repr(transparent)]
19pub struct Duration(u64);
20
21impl Duration {
22 pub const ZERO: Self = Self(0);
24 pub const MIN: Self = Self(u64::MIN);
26 pub const MAX: Self = Self(u64::MAX);
28
29 const MS_PER_SEC: u64 = 1_000;
30 const SECS_PER_MIN: u64 = 60;
31 const MINS_PER_HOUR: u64 = 60;
32 const HOURS_PER_DAY: u64 = 24;
33 const DAYS_PER_WEEK: u64 = 7;
34
35 #[must_use]
37 pub const fn from_millis(millis: u64) -> Self {
38 Self(millis)
39 }
40
41 #[must_use]
43 pub const fn try_from_i64(millis: i64) -> Option<Self> {
44 if millis < 0 {
45 None
46 } else {
47 Some(Self(millis.cast_unsigned()))
48 }
49 }
50
51 #[must_use]
53 pub const fn from_micros_truncating(micros: u64) -> Self {
54 Self(micros / Self::MS_PER_SEC)
55 }
56
57 #[must_use]
59 pub const fn from_nanos_truncating(nanos: u64) -> Self {
60 Self(nanos / 1_000_000)
61 }
62
63 #[must_use]
65 pub const fn from_secs(seconds: u64) -> Self {
66 Self(seconds.saturating_mul(Self::MS_PER_SEC))
67 }
68
69 #[must_use]
71 pub const fn from_minutes(minutes: u64) -> Self {
72 Self(
73 minutes
74 .saturating_mul(Self::SECS_PER_MIN)
75 .saturating_mul(Self::MS_PER_SEC),
76 )
77 }
78
79 #[must_use]
81 pub const fn from_hours(hours: u64) -> Self {
82 Self(
83 hours
84 .saturating_mul(Self::MINS_PER_HOUR)
85 .saturating_mul(Self::SECS_PER_MIN)
86 .saturating_mul(Self::MS_PER_SEC),
87 )
88 }
89
90 #[must_use]
92 pub const fn from_days(days: u64) -> Self {
93 Self(
94 days.saturating_mul(Self::HOURS_PER_DAY)
95 .saturating_mul(Self::MINS_PER_HOUR)
96 .saturating_mul(Self::SECS_PER_MIN)
97 .saturating_mul(Self::MS_PER_SEC),
98 )
99 }
100
101 #[must_use]
103 pub const fn from_weeks(weeks: u64) -> Self {
104 Self(
105 weeks
106 .saturating_mul(Self::DAYS_PER_WEEK)
107 .saturating_mul(Self::HOURS_PER_DAY)
108 .saturating_mul(Self::MINS_PER_HOUR)
109 .saturating_mul(Self::SECS_PER_MIN)
110 .saturating_mul(Self::MS_PER_SEC),
111 )
112 }
113
114 #[must_use]
116 pub const fn as_millis(self) -> u64 {
117 self.0
118 }
119
120 #[must_use]
122 pub const fn as_secs(self) -> u64 {
123 self.0 / Self::MS_PER_SEC
124 }
125
126 #[must_use]
128 pub const fn as_minutes(self) -> u64 {
129 self.0 / (Self::SECS_PER_MIN * Self::MS_PER_SEC)
130 }
131
132 #[must_use]
134 pub const fn as_hours(self) -> u64 {
135 self.0 / (Self::MINS_PER_HOUR * Self::SECS_PER_MIN * Self::MS_PER_SEC)
136 }
137
138 #[must_use]
140 pub const fn as_days(self) -> u64 {
141 self.0 / (Self::HOURS_PER_DAY * Self::MINS_PER_HOUR * Self::SECS_PER_MIN * Self::MS_PER_SEC)
142 }
143
144 #[must_use]
146 pub const fn as_weeks(self) -> u64 {
147 self.0
148 / (Self::DAYS_PER_WEEK
149 * Self::HOURS_PER_DAY
150 * Self::MINS_PER_HOUR
151 * Self::SECS_PER_MIN
152 * Self::MS_PER_SEC)
153 }
154
155 pub fn parse_flexible(input: &str) -> Result<Self, TypeParseError> {
162 let (digits, multiplier) = if let Some(value) = input.strip_suffix("ms") {
163 (value, 1)
164 } else if let Some(value) = input.strip_suffix('s') {
165 (value, Self::MS_PER_SEC)
166 } else if let Some(value) = input.strip_suffix('m') {
167 (value, Self::SECS_PER_MIN * Self::MS_PER_SEC)
168 } else if let Some(value) = input.strip_suffix('h') {
169 (
170 value,
171 Self::MINS_PER_HOUR * Self::SECS_PER_MIN * Self::MS_PER_SEC,
172 )
173 } else if let Some(value) = input.strip_suffix('d') {
174 (
175 value,
176 Self::HOURS_PER_DAY * Self::MINS_PER_HOUR * Self::SECS_PER_MIN * Self::MS_PER_SEC,
177 )
178 } else {
179 (input, 1)
180 };
181 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
182 return Err(TypeParseError::InvalidDuration);
183 }
184 let value = digits
185 .parse::<u64>()
186 .map_err(|_| TypeParseError::InvalidDuration)?;
187 value
188 .checked_mul(multiplier)
189 .map(Self)
190 .ok_or(TypeParseError::InvalidDuration)
191 }
192}
193
194impl Add for Duration {
195 type Output = Self;
196
197 fn add(self, other: Self) -> Self::Output {
198 Self(self.0.saturating_add(other.0))
199 }
200}
201
202impl AddAssign for Duration {
203 fn add_assign(&mut self, other: Self) {
204 self.0 = self.0.saturating_add(other.0);
205 }
206}
207
208impl CandidType for Duration {
209 fn ty() -> Type {
210 <u64 as CandidType>::ty()
211 }
212
213 fn _ty() -> Type {
214 <u64 as CandidType>::_ty()
215 }
216
217 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
218 where
219 S: CandidSerializer,
220 {
221 serializer.serialize_nat64(self.0)
222 }
223}
224
225impl<'de> Deserialize<'de> for Duration {
226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227 where
228 D: Deserializer<'de>,
229 {
230 struct DurationVisitor;
231
232 impl serde::de::Visitor<'_> for DurationVisitor {
233 type Value = Duration;
234
235 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
236 formatter.write_str("milliseconds or duration string")
237 }
238
239 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
240 where
241 E: serde::de::Error,
242 {
243 Duration::try_from_i64(value)
244 .ok_or_else(|| E::custom("duration must be non-negative"))
245 }
246
247 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
248 Ok(Duration::from_millis(value))
249 }
250
251 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
252 where
253 E: serde::de::Error,
254 {
255 Duration::parse_flexible(value).map_err(E::custom)
256 }
257 }
258
259 if !deserializer.is_human_readable() {
260 return u64::deserialize(deserializer).map(Self);
261 }
262
263 deserializer.deserialize_any(DurationVisitor)
264 }
265}
266
267impl From<u64> for Duration {
268 fn from(value: u64) -> Self {
269 Self(value)
270 }
271}
272
273impl Serialize for Duration {
274 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
275 where
276 S: serde::Serializer,
277 {
278 serializer.serialize_u64(self.0)
279 }
280}
281
282impl Sub for Duration {
283 type Output = Self;
284
285 fn sub(self, other: Self) -> Self::Output {
286 Self(self.0.saturating_sub(other.0))
287 }
288}
289
290impl SubAssign for Duration {
291 fn sub_assign(&mut self, other: Self) {
292 self.0 = self.0.saturating_sub(other.0);
293 }
294}
295
296#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
298#[repr(transparent)]
299pub struct Timestamp(i64);
300
301impl Timestamp {
302 pub const EPOCH: Self = Self(0);
304 pub const MIN: Self = Self(i64::MIN);
306 pub const MAX: Self = Self(i64::MAX);
308
309 const MILLIS_PER_SEC: i64 = 1_000;
310
311 #[must_use]
313 pub const fn from_secs(seconds: i64) -> Self {
314 Self(seconds.saturating_mul(Self::MILLIS_PER_SEC))
315 }
316
317 #[must_use]
319 pub const fn from_millis(millis: i64) -> Self {
320 Self(millis)
321 }
322
323 #[must_use]
325 pub const fn try_from_i64(millis: i64) -> Option<Self> {
326 Some(Self(millis))
327 }
328
329 #[must_use]
331 pub fn try_from_u64(millis: u64) -> Option<Self> {
332 i64::try_from(millis).ok().map(Self)
333 }
334
335 #[must_use]
337 pub const fn from_micros(micros: i64) -> Self {
338 Self(micros.div_euclid(Self::MILLIS_PER_SEC))
339 }
340
341 #[must_use]
343 pub const fn from_nanos(nanos: i64) -> Self {
344 Self(nanos.div_euclid(1_000_000))
345 }
346
347 pub fn parse_rfc3339(input: &str) -> Result<Self, TypeParseError> {
354 let bytes = input.as_bytes();
355 if bytes.len() < 20
356 || bytes.get(4) != Some(&b'-')
357 || bytes.get(7) != Some(&b'-')
358 || bytes.get(10) != Some(&b'T')
359 || bytes.get(13) != Some(&b':')
360 || bytes.get(16) != Some(&b':')
361 {
362 return Err(TypeParseError::InvalidTimestamp);
363 }
364 let year = parse_i32(&bytes[0..4])?;
365 let month = Month::try_from(parse_u8(&bytes[5..7])?)
366 .map_err(|_| TypeParseError::InvalidTimestamp)?;
367 let day = parse_u8(&bytes[8..10])?;
368 let hour = parse_u8(&bytes[11..13])?;
369 let minute = parse_u8(&bytes[14..16])?;
370 let second = parse_u8(&bytes[17..19])?;
371
372 let mut cursor = 19;
373 let nanoseconds = if bytes.get(cursor) == Some(&b'.') {
374 cursor += 1;
375 let start = cursor;
376 while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
377 cursor += 1;
378 }
379 if cursor == start {
380 return Err(TypeParseError::InvalidTimestamp);
381 }
382 parse_fractional_nanoseconds(&bytes[start..cursor])?
383 } else {
384 0
385 };
386 let (sign, offset_hour, offset_minute) = parse_offset(&bytes[cursor..])?;
387 let date = TimeDate::from_calendar_date(year, month, day)
388 .map_err(|_| TypeParseError::InvalidTimestamp)?;
389 let time = TimeOfDay::from_hms_nano(hour, minute, second, nanoseconds)
390 .map_err(|_| TypeParseError::InvalidTimestamp)?;
391 let offset = UtcOffset::from_hms(
392 sign * i8::try_from(offset_hour).unwrap_or(i8::MAX),
393 sign * i8::try_from(offset_minute).unwrap_or(i8::MAX),
394 0,
395 )
396 .map_err(|_| TypeParseError::InvalidTimestamp)?;
397 let millis = PrimitiveDateTime::new(date, time)
398 .assume_offset(offset)
399 .unix_timestamp_nanos()
400 .div_euclid(1_000_000);
401 i64::try_from(millis)
402 .map(Self)
403 .map_err(|_| TypeParseError::InvalidTimestamp)
404 }
405
406 pub fn parse_flexible(input: &str) -> Result<Self, TypeParseError> {
412 input
413 .parse::<i64>()
414 .map(Self)
415 .or_else(|_| Self::parse_rfc3339(input))
416 }
417
418 #[must_use]
420 pub const fn as_millis(self) -> i64 {
421 self.0
422 }
423
424 #[must_use]
426 pub const fn as_secs(self) -> i64 {
427 self.0.div_euclid(Self::MILLIS_PER_SEC)
428 }
429
430 fn from_wide_millis_saturating(millis: i128) -> Self {
431 match i64::try_from(millis) {
432 Ok(millis) => Self(millis),
433 Err(_) if millis.is_negative() => Self::MIN,
434 Err(_) => Self::MAX,
435 }
436 }
437}
438
439impl Add<Duration> for Timestamp {
440 type Output = Self;
441
442 fn add(self, duration: Duration) -> Self::Output {
443 Self::from_wide_millis_saturating(i128::from(self.0) + i128::from(duration.as_millis()))
444 }
445}
446
447impl AddAssign<Duration> for Timestamp {
448 fn add_assign(&mut self, duration: Duration) {
449 *self = *self + duration;
450 }
451}
452
453impl CandidType for Timestamp {
454 fn ty() -> Type {
455 <i64 as CandidType>::ty()
456 }
457
458 fn _ty() -> Type {
459 <i64 as CandidType>::_ty()
460 }
461
462 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
463 where
464 S: CandidSerializer,
465 {
466 serializer.serialize_int64(self.0)
467 }
468}
469
470impl<'de> Deserialize<'de> for Timestamp {
471 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
472 where
473 D: Deserializer<'de>,
474 {
475 struct TimestampVisitor;
476
477 impl serde::de::Visitor<'_> for TimestampVisitor {
478 type Value = Timestamp;
479
480 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
481 formatter.write_str("unix millis or RFC3339 timestamp")
482 }
483
484 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
485 Ok(Timestamp::from_millis(value))
486 }
487
488 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
489 where
490 E: serde::de::Error,
491 {
492 Timestamp::try_from_u64(value)
493 .ok_or_else(|| E::custom("unix millis exceeds i64 timestamp range"))
494 }
495
496 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
497 where
498 E: serde::de::Error,
499 {
500 Timestamp::parse_flexible(value).map_err(E::custom)
501 }
502 }
503
504 if !deserializer.is_human_readable() {
505 return i64::deserialize(deserializer).map(Self);
506 }
507
508 deserializer.deserialize_any(TimestampVisitor)
509 }
510}
511
512impl Display for Timestamp {
513 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
514 Display::fmt(&self.0, formatter)
515 }
516}
517
518impl From<i64> for Timestamp {
519 fn from(value: i64) -> Self {
520 Self(value)
521 }
522}
523
524impl Serialize for Timestamp {
525 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
526 where
527 S: serde::Serializer,
528 {
529 serializer.serialize_i64(self.0)
530 }
531}
532
533impl Sub<Duration> for Timestamp {
534 type Output = Self;
535
536 fn sub(self, duration: Duration) -> Self::Output {
537 Self::from_wide_millis_saturating(i128::from(self.0) - i128::from(duration.as_millis()))
538 }
539}
540
541impl Sub for Timestamp {
542 type Output = Duration;
543
544 fn sub(self, other: Self) -> Self::Output {
545 if self.0 <= other.0 {
546 return Duration::ZERO;
547 }
548 Duration::from_millis(
549 u64::try_from(i128::from(self.0) - i128::from(other.0)).unwrap_or(u64::MAX),
550 )
551 }
552}
553
554impl SubAssign<Duration> for Timestamp {
555 fn sub_assign(&mut self, duration: Duration) {
556 *self = *self - duration;
557 }
558}
559
560fn parse_i32(bytes: &[u8]) -> Result<i32, TypeParseError> {
561 bytes
562 .iter()
563 .try_fold(0_i32, |value, byte| {
564 byte.checked_sub(b'0')
565 .filter(|digit| *digit <= 9)
566 .and_then(|digit| value.checked_mul(10)?.checked_add(i32::from(digit)))
567 })
568 .ok_or(TypeParseError::InvalidTimestamp)
569}
570
571fn parse_u8(bytes: &[u8]) -> Result<u8, TypeParseError> {
572 bytes
573 .iter()
574 .try_fold(0_u8, |value, byte| {
575 byte.checked_sub(b'0')
576 .filter(|digit| *digit <= 9)
577 .and_then(|digit| value.checked_mul(10)?.checked_add(digit))
578 })
579 .ok_or(TypeParseError::InvalidTimestamp)
580}
581
582fn parse_fractional_nanoseconds(bytes: &[u8]) -> Result<u32, TypeParseError> {
583 let mut value = 0_u32;
584 for byte in bytes.iter().take(9) {
585 let digit = byte
586 .checked_sub(b'0')
587 .filter(|digit| *digit <= 9)
588 .ok_or(TypeParseError::InvalidTimestamp)?;
589 value = value
590 .checked_mul(10)
591 .and_then(|current| current.checked_add(u32::from(digit)))
592 .ok_or(TypeParseError::InvalidTimestamp)?;
593 }
594 for _ in bytes.len().min(9)..9 {
595 value = value
596 .checked_mul(10)
597 .ok_or(TypeParseError::InvalidTimestamp)?;
598 }
599 Ok(value)
600}
601
602fn parse_offset(bytes: &[u8]) -> Result<(i8, u8, u8), TypeParseError> {
603 match bytes {
604 [b'Z'] => Ok((1, 0, 0)),
605 [sign @ (b'+' | b'-'), hour0, hour1, b':', minute0, minute1] => Ok((
606 if *sign == b'+' { 1 } else { -1 },
607 parse_u8(&[*hour0, *hour1])?,
608 parse_u8(&[*minute0, *minute1])?,
609 )),
610 _ => Err(TypeParseError::InvalidTimestamp),
611 }
612}