Skip to main content

icydb_schema/
time_atoms.rs

1//! Canonical millisecond-native time atoms.
2
3use 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/// Canonical millisecond duration.
17#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
18#[repr(transparent)]
19pub struct Duration(u64);
20
21impl Duration {
22    /// Zero milliseconds.
23    pub const ZERO: Self = Self(0);
24    /// Minimum duration.
25    pub const MIN: Self = Self(u64::MIN);
26    /// Maximum duration.
27    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    /// Construct from milliseconds.
36    #[must_use]
37    pub const fn from_millis(millis: u64) -> Self {
38        Self(millis)
39    }
40
41    /// Convert nonnegative signed milliseconds.
42    #[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    /// Construct from microseconds, truncating sub-millisecond precision.
52    #[must_use]
53    pub const fn from_micros_truncating(micros: u64) -> Self {
54        Self(micros / Self::MS_PER_SEC)
55    }
56
57    /// Construct from nanoseconds, truncating sub-millisecond precision.
58    #[must_use]
59    pub const fn from_nanos_truncating(nanos: u64) -> Self {
60        Self(nanos / 1_000_000)
61    }
62
63    /// Construct from seconds with saturation.
64    #[must_use]
65    pub const fn from_secs(seconds: u64) -> Self {
66        Self(seconds.saturating_mul(Self::MS_PER_SEC))
67    }
68
69    /// Construct from minutes with saturation.
70    #[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    /// Construct from hours with saturation.
80    #[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    /// Construct from days with saturation.
91    #[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    /// Construct from weeks with saturation.
102    #[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    /// Return milliseconds.
115    #[must_use]
116    pub const fn as_millis(self) -> u64 {
117        self.0
118    }
119
120    /// Return whole seconds.
121    #[must_use]
122    pub const fn as_secs(self) -> u64 {
123        self.0 / Self::MS_PER_SEC
124    }
125
126    /// Return whole minutes.
127    #[must_use]
128    pub const fn as_minutes(self) -> u64 {
129        self.0 / (Self::SECS_PER_MIN * Self::MS_PER_SEC)
130    }
131
132    /// Return whole hours.
133    #[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    /// Return whole days.
139    #[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    /// Return whole weeks.
145    #[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    /// Parse integer milliseconds or `ms`, `s`, `m`, `h`, or `d` suffixes.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`TypeParseError::InvalidDuration`] for malformed or overflowing
160    /// input.
161    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/// Canonical Unix-millisecond timestamp.
297#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
298#[repr(transparent)]
299pub struct Timestamp(i64);
300
301impl Timestamp {
302    /// Unix epoch.
303    pub const EPOCH: Self = Self(0);
304    /// Minimum timestamp.
305    pub const MIN: Self = Self(i64::MIN);
306    /// Maximum timestamp.
307    pub const MAX: Self = Self(i64::MAX);
308
309    const MILLIS_PER_SEC: i64 = 1_000;
310
311    /// Construct from seconds with saturation.
312    #[must_use]
313    pub const fn from_secs(seconds: i64) -> Self {
314        Self(seconds.saturating_mul(Self::MILLIS_PER_SEC))
315    }
316
317    /// Construct from milliseconds.
318    #[must_use]
319    pub const fn from_millis(millis: i64) -> Self {
320        Self(millis)
321    }
322
323    /// Convert signed milliseconds.
324    #[must_use]
325    pub const fn try_from_i64(millis: i64) -> Option<Self> {
326        Some(Self(millis))
327    }
328
329    /// Convert unsigned milliseconds.
330    #[must_use]
331    pub fn try_from_u64(millis: u64) -> Option<Self> {
332        i64::try_from(millis).ok().map(Self)
333    }
334
335    /// Construct from microseconds, flooring to Unix milliseconds.
336    #[must_use]
337    pub const fn from_micros(micros: i64) -> Self {
338        Self(micros.div_euclid(Self::MILLIS_PER_SEC))
339    }
340
341    /// Construct from nanoseconds, flooring to Unix milliseconds.
342    #[must_use]
343    pub const fn from_nanos(nanos: i64) -> Self {
344        Self(nanos.div_euclid(1_000_000))
345    }
346
347    /// Parse strict RFC3339 text.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`TypeParseError::InvalidTimestamp`] for malformed or
352    /// out-of-range input.
353    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    /// Parse integer milliseconds or strict RFC3339 text.
407    ///
408    /// # Errors
409    ///
410    /// Returns a typed timestamp parse error.
411    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    /// Return Unix milliseconds.
419    #[must_use]
420    pub const fn as_millis(self) -> i64 {
421        self.0
422    }
423
424    /// Return whole Unix seconds.
425    #[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}