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