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