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 candid::CandidType;
9use serde::{Deserialize, Deserializer, Serialize};
10use time::{Date as TimeDate, Month, PrimitiveDateTime, Time as TimeOfDay, UtcOffset};
11
12/// Compact scalar parsing failure.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum TypeParseError {
15    /// Date text is invalid.
16    InvalidDate,
17    /// Decimal text is invalid.
18    InvalidDecimal,
19    /// Duration text is invalid.
20    InvalidDuration,
21    /// Float text is invalid.
22    InvalidFloat,
23    /// Signed big-integer text is invalid.
24    InvalidIntBig,
25    /// Timestamp text is invalid.
26    InvalidTimestamp,
27    /// ULID text is invalid.
28    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/// Canonical millisecond duration.
38#[derive(CandidType, Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
39#[repr(transparent)]
40pub struct Duration(u64);
41
42impl Duration {
43    /// Zero milliseconds.
44    pub const ZERO: Self = Self(0);
45    /// Minimum duration.
46    pub const MIN: Self = Self(u64::MIN);
47    /// Maximum duration.
48    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    /// Construct from milliseconds.
57    #[must_use]
58    pub const fn from_millis(millis: u64) -> Self {
59        Self(millis)
60    }
61
62    /// Convert nonnegative signed milliseconds.
63    #[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    /// Convert unsigned milliseconds.
73    #[must_use]
74    pub const fn try_from_u64(millis: u64) -> Option<Self> {
75        Some(Self(millis))
76    }
77
78    /// Construct from microseconds, truncating sub-millisecond precision.
79    #[must_use]
80    pub const fn from_micros_truncating(micros: u64) -> Self {
81        Self(micros / Self::MS_PER_SEC)
82    }
83
84    /// Construct from nanoseconds, truncating sub-millisecond precision.
85    #[must_use]
86    pub const fn from_nanos_truncating(nanos: u64) -> Self {
87        Self(nanos / 1_000_000)
88    }
89
90    /// Construct from seconds with saturation.
91    #[must_use]
92    pub const fn from_secs(seconds: u64) -> Self {
93        Self(seconds.saturating_mul(Self::MS_PER_SEC))
94    }
95
96    /// Construct from minutes with saturation.
97    #[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    /// Construct from hours with saturation.
107    #[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    /// Construct from days with saturation.
118    #[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    /// Construct from weeks with saturation.
129    #[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    /// Return milliseconds.
142    #[must_use]
143    pub const fn as_millis(self) -> u64 {
144        self.0
145    }
146
147    /// Return whole seconds.
148    #[must_use]
149    pub const fn as_secs(self) -> u64 {
150        self.0 / Self::MS_PER_SEC
151    }
152
153    /// Return whole minutes.
154    #[must_use]
155    pub const fn as_minutes(self) -> u64 {
156        self.0 / (Self::SECS_PER_MIN * Self::MS_PER_SEC)
157    }
158
159    /// Return whole hours.
160    #[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    /// Return whole days.
166    #[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    /// Return whole weeks.
172    #[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    /// Parse integer milliseconds or `ms`, `s`, `m`, `h`, or `d` suffixes.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`TypeParseError::InvalidDuration`] for malformed or overflowing
187    /// input.
188    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/// Canonical Unix-millisecond timestamp.
300#[derive(CandidType, Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
301#[repr(transparent)]
302pub struct Timestamp(i64);
303
304impl Timestamp {
305    /// Unix epoch.
306    pub const EPOCH: Self = Self(0);
307    /// Minimum timestamp.
308    pub const MIN: Self = Self(i64::MIN);
309    /// Maximum timestamp.
310    pub const MAX: Self = Self(i64::MAX);
311
312    const MILLIS_PER_SEC: i64 = 1_000;
313
314    /// Construct from seconds with saturation.
315    #[must_use]
316    pub const fn from_secs(seconds: i64) -> Self {
317        Self(seconds.saturating_mul(Self::MILLIS_PER_SEC))
318    }
319
320    /// Construct from milliseconds.
321    #[must_use]
322    pub const fn from_millis(millis: i64) -> Self {
323        Self(millis)
324    }
325
326    /// Convert signed milliseconds.
327    #[must_use]
328    pub const fn try_from_i64(millis: i64) -> Option<Self> {
329        Some(Self(millis))
330    }
331
332    /// Convert unsigned milliseconds.
333    #[must_use]
334    pub fn try_from_u64(millis: u64) -> Option<Self> {
335        i64::try_from(millis).ok().map(Self)
336    }
337
338    /// Construct from microseconds, truncating sub-millisecond precision.
339    #[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    /// Construct from nanoseconds, truncating sub-millisecond precision.
348    #[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    /// Parse strict RFC3339 text.
362    ///
363    /// # Errors
364    ///
365    /// Returns [`TypeParseError::InvalidTimestamp`] for malformed or
366    /// out-of-range input.
367    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    /// Parse integer milliseconds or strict RFC3339 text.
421    ///
422    /// # Errors
423    ///
424    /// Returns a typed timestamp parse error.
425    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    /// Return Unix milliseconds.
433    #[must_use]
434    pub const fn as_millis(self) -> i64 {
435        self.0
436    }
437
438    /// Return whole Unix seconds.
439    #[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}