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