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(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/// Canonical millisecond duration.
46#[derive(CandidType, Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
47#[repr(transparent)]
48pub struct Duration(u64);
49
50impl Duration {
51    /// Zero milliseconds.
52    pub const ZERO: Self = Self(0);
53    /// Minimum duration.
54    pub const MIN: Self = Self(u64::MIN);
55    /// Maximum duration.
56    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    /// Construct from milliseconds.
65    #[must_use]
66    pub const fn from_millis(millis: u64) -> Self {
67        Self(millis)
68    }
69
70    /// Convert nonnegative signed milliseconds.
71    #[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    /// Convert unsigned milliseconds.
81    #[must_use]
82    pub const fn try_from_u64(millis: u64) -> Option<Self> {
83        Some(Self(millis))
84    }
85
86    /// Construct from microseconds, truncating sub-millisecond precision.
87    #[must_use]
88    pub const fn from_micros_truncating(micros: u64) -> Self {
89        Self(micros / Self::MS_PER_SEC)
90    }
91
92    /// Construct from nanoseconds, truncating sub-millisecond precision.
93    #[must_use]
94    pub const fn from_nanos_truncating(nanos: u64) -> Self {
95        Self(nanos / 1_000_000)
96    }
97
98    /// Construct from seconds with saturation.
99    #[must_use]
100    pub const fn from_secs(seconds: u64) -> Self {
101        Self(seconds.saturating_mul(Self::MS_PER_SEC))
102    }
103
104    /// Construct from minutes with saturation.
105    #[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    /// Construct from hours with saturation.
115    #[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    /// Construct from days with saturation.
126    #[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    /// Construct from weeks with saturation.
137    #[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    /// Return milliseconds.
150    #[must_use]
151    pub const fn as_millis(self) -> u64 {
152        self.0
153    }
154
155    /// Return whole seconds.
156    #[must_use]
157    pub const fn as_secs(self) -> u64 {
158        self.0 / Self::MS_PER_SEC
159    }
160
161    /// Return whole minutes.
162    #[must_use]
163    pub const fn as_minutes(self) -> u64 {
164        self.0 / (Self::SECS_PER_MIN * Self::MS_PER_SEC)
165    }
166
167    /// Return whole hours.
168    #[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    /// Return whole days.
174    #[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    /// Return whole weeks.
180    #[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    /// Parse integer milliseconds or `ms`, `s`, `m`, `h`, or `d` suffixes.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`TypeParseError::InvalidDuration`] for malformed or overflowing
195    /// input.
196    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/// Canonical Unix-millisecond timestamp.
308#[derive(CandidType, Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
309#[repr(transparent)]
310pub struct Timestamp(i64);
311
312impl Timestamp {
313    /// Unix epoch.
314    pub const EPOCH: Self = Self(0);
315    /// Minimum timestamp.
316    pub const MIN: Self = Self(i64::MIN);
317    /// Maximum timestamp.
318    pub const MAX: Self = Self(i64::MAX);
319
320    const MILLIS_PER_SEC: i64 = 1_000;
321
322    /// Construct from seconds with saturation.
323    #[must_use]
324    pub const fn from_secs(seconds: i64) -> Self {
325        Self(seconds.saturating_mul(Self::MILLIS_PER_SEC))
326    }
327
328    /// Construct from milliseconds.
329    #[must_use]
330    pub const fn from_millis(millis: i64) -> Self {
331        Self(millis)
332    }
333
334    /// Convert signed milliseconds.
335    #[must_use]
336    pub const fn try_from_i64(millis: i64) -> Option<Self> {
337        Some(Self(millis))
338    }
339
340    /// Convert unsigned milliseconds.
341    #[must_use]
342    pub fn try_from_u64(millis: u64) -> Option<Self> {
343        i64::try_from(millis).ok().map(Self)
344    }
345
346    /// Construct from microseconds, truncating sub-millisecond precision.
347    #[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    /// Construct from nanoseconds, truncating sub-millisecond precision.
356    #[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    /// Parse strict RFC3339 text.
370    ///
371    /// # Errors
372    ///
373    /// Returns [`TypeParseError::InvalidTimestamp`] for malformed or
374    /// out-of-range input.
375    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    /// Parse integer milliseconds or strict RFC3339 text.
429    ///
430    /// # Errors
431    ///
432    /// Returns a typed timestamp parse error.
433    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    /// Return Unix milliseconds.
441    #[must_use]
442    pub const fn as_millis(self) -> i64 {
443        self.0
444    }
445
446    /// Return whole Unix seconds.
447    #[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}