iso8601_timestamp/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3#![deny(
4    missing_docs,
5    clippy::missing_safety_doc,
6    clippy::undocumented_unsafe_blocks,
7    clippy::must_use_candidate,
8    clippy::perf,
9    clippy::complexity,
10    clippy::suspicious
11)]
12
13use core::ops::{AddAssign, Deref, DerefMut, SubAssign};
14
15#[cfg(feature = "std")]
16use std::time::SystemTime;
17
18pub extern crate time;
19
20pub use time::{Duration, UtcOffset};
21use time::{OffsetDateTime, PrimitiveDateTime};
22
23pub use generic_array::typenum;
24use typenum as t;
25
26#[macro_use]
27mod macros;
28
29mod format;
30mod impls;
31mod parse;
32mod ts_str;
33
34use ts_str::IsValidFormat;
35pub use ts_str::{FormatString, TimestampStr};
36
37/// UTC Timestamp with nanosecond precision, millisecond-precision when serialized to serde (JSON).
38///
39/// A `Deref`/`DerefMut` implementation is provided to gain access to the inner `PrimitiveDateTime` object.
40#[cfg_attr(feature = "diesel", derive(diesel::AsExpression, diesel::FromSqlRow))]
41#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Timestamp))]
42#[cfg_attr(feature = "diesel-pg", diesel(sql_type = diesel::sql_types::Timestamptz))]
43#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44#[repr(transparent)]
45pub struct Timestamp(PrimitiveDateTime);
46
47use core::fmt;
48
49impl fmt::Debug for Timestamp {
50    #[inline]
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.debug_tuple("Timestamp")
53            .field(&self.format_nanoseconds())
54            .finish()
55    }
56}
57
58impl fmt::Display for Timestamp {
59    #[inline]
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str(&self.format())
62    }
63}
64
65#[cfg(feature = "std")]
66impl From<SystemTime> for Timestamp {
67    fn from(ts: SystemTime) -> Self {
68        Timestamp(match ts.duration_since(SystemTime::UNIX_EPOCH) {
69            Ok(dur) => *Self::UNIX_EPOCH + dur,
70            Err(err) => *Self::UNIX_EPOCH - err.duration(),
71        })
72    }
73}
74
75#[cfg(feature = "std")]
76impl From<Timestamp> for SystemTime {
77    fn from(ts: Timestamp) -> Self {
78        SystemTime::UNIX_EPOCH + ts.duration_since(Timestamp::UNIX_EPOCH)
79    }
80}
81
82impl From<OffsetDateTime> for Timestamp {
83    fn from(ts: OffsetDateTime) -> Self {
84        let utc_datetime = ts.to_offset(UtcOffset::UTC);
85        let date = utc_datetime.date();
86        let time = utc_datetime.time();
87        Timestamp(PrimitiveDateTime::new(date, time))
88    }
89}
90
91impl From<PrimitiveDateTime> for Timestamp {
92    #[inline]
93    fn from(ts: PrimitiveDateTime) -> Self {
94        Timestamp(ts)
95    }
96}
97
98// SystemTime::now() is not implemented on wasm32
99#[cfg(all(feature = "std", not(any(target_arch = "wasm64", target_arch = "wasm32"))))]
100impl Timestamp {
101    /// Get the current time, assuming UTC.
102    #[inline]
103    #[must_use]
104    pub fn now_utc() -> Self {
105        Timestamp::from(SystemTime::now())
106    }
107}
108
109#[cfg(all(feature = "worker", target_arch = "wasm32", not(feature = "js")))]
110impl From<worker::Date> for Timestamp {
111    fn from(d: worker::Date) -> Self {
112        match Timestamp::UNIX_EPOCH.checked_add(Duration::milliseconds(d.as_millis() as i64)) {
113            Some(ts) => ts,
114            None => panic!("Invalid Date value"),
115        }
116    }
117}
118
119#[cfg(all(feature = "worker", target_arch = "wasm32", not(feature = "js")))]
120impl Timestamp {
121    /// Get the current time, assuming UTC.
122    ///
123    /// # Panics
124    ///
125    /// Panics if the current time is before the UNIX Epoch.
126    #[inline]
127    #[must_use]
128    pub fn now_utc() -> Self {
129        worker::Date::now().into()
130    }
131}
132
133#[cfg(all(
134    feature = "js",
135    any(target_arch = "wasm32", target_arch = "wasm64"),
136    not(feature = "worker")
137))]
138impl Timestamp {
139    /// Get the current time, assuming UTC.
140    ///
141    /// # Panics
142    ///
143    /// Panics if the current time is before the UNIX Epoch.
144    #[must_use]
145    pub fn now_utc() -> Self {
146        match Timestamp::UNIX_EPOCH.checked_add(Duration::milliseconds(js_sys::Date::now() as i64)) {
147            Some(ts) => ts,
148            None => panic!("Invalid Date::now() value"),
149        }
150    }
151}
152
153/// Preconfigured formats
154pub mod formats {
155    use super::*;
156
157    /// `2023-03-24T07:05:59.005Z`
158    pub type FullMilliseconds = FormatString<t::True, t::False, t::U3>;
159    /// `2023-03-24T07:05:59.005000Z`
160    pub type FullMicroseconds = FormatString<t::True, t::False, t::U6>;
161    /// `2023-03-24T07:05:59.005432101Z`
162    pub type FullNanoseconds = FormatString<t::True, t::False, t::U9>;
163
164    /// `2023-03-24T07:05:59.005+05:00`
165    pub type FullMillisecondsOffset = FormatString<t::True, t::True, t::U3>;
166
167    /// `20230324T070559.005Z`
168    pub type ShortMilliseconds = FormatString<t::False, t::False, t::U3>;
169
170    #[test]
171    #[allow(clippy::assertions_on_constants)]
172    fn test_short_ms_length() {
173        // ensure the short format could fit within a smolstr/compact_str
174        assert_eq!(
175            <<ShortMilliseconds as crate::ts_str::IsValidFormat>::Length as super::t::Unsigned>::USIZE,
176            "+20230324T070559.005Z".len()
177        );
178
179        assert!("+20230324T070559.005Z".len() <= 23);
180    }
181}
182
183/// Construct a [`Timestamp`] with a statically known value.
184///
185/// The resulting expression can be used in `const` or `static` declarations.
186///
187/// See [`time::macros::datetime`](time::macros) for more information.
188///
189/// The variation presented here does not support timezone offsets.
190#[macro_export]
191macro_rules! datetime {
192    ($($tt:tt)*) => {
193        $crate::Timestamp::from_primitive_datetime(time::macros::datetime!($($tt)*))
194    };
195}
196
197impl Timestamp {
198    /// Unix Epoch -- 1970-01-01 Midnight
199    pub const UNIX_EPOCH: Self = datetime!(1970 - 01 - 01 00:00);
200
201    /// Constructs a [`Timestamp`] from a [`PrimitiveDateTime`]
202    #[inline(always)]
203    #[must_use]
204    pub const fn from_primitive_datetime(dt: PrimitiveDateTime) -> Self {
205        Timestamp(dt)
206    }
207
208    /// Returns the amount of time elapsed from an earlier point in time.
209    #[inline]
210    #[must_use]
211    pub fn duration_since(self, earlier: Self) -> Duration {
212        self.0 - earlier.0
213    }
214
215    /// Formats the timestamp given the provided formatting parameters
216    #[must_use]
217    pub fn format_raw<F: t::Bit, O: t::Bit, P: t::Unsigned>(
218        &self,
219        offset: UtcOffset,
220    ) -> TimestampStr<FormatString<F, O, P>>
221    where
222        FormatString<F, O, P>: IsValidFormat,
223    {
224        format::do_format(self.0, offset)
225    }
226
227    /// Formats a full timestamp without offset, using the given subsecond precision level.
228    #[inline(always)]
229    #[must_use]
230    pub fn format_with_precision<P: t::Unsigned>(&self) -> TimestampStr<FormatString<t::True, t::False, P>>
231    where
232        FormatString<t::True, t::False, P>: IsValidFormat,
233    {
234        self.format_raw(UtcOffset::UTC)
235    }
236
237    /// Format timestamp to ISO8601 with full punctuation, to millisecond precision.
238    #[inline(always)]
239    #[must_use]
240    pub fn format(&self) -> TimestampStr<formats::FullMilliseconds> {
241        self.format_with_precision()
242    }
243
244    /// Format timestamp to ISO8601 with extended precision to nanoseconds.
245    #[inline(always)]
246    #[must_use]
247    pub fn format_nanoseconds(&self) -> TimestampStr<formats::FullNanoseconds> {
248        self.format_with_precision()
249    }
250
251    /// Format timestamp to ISO8601 with extended precision to microseconds.
252    #[inline(always)]
253    #[must_use]
254    pub fn format_microseconds(&self) -> TimestampStr<formats::FullMicroseconds> {
255        self.format_with_precision()
256    }
257
258    /// Format timestamp to ISO8601 without most punctuation, to millisecond precision.
259    #[inline(always)]
260    #[must_use]
261    pub fn format_short(&self) -> TimestampStr<formats::ShortMilliseconds> {
262        self.format_raw(UtcOffset::UTC)
263    }
264
265    /// Format timestamp to ISO8601 with arbitrary UTC offset. Any offset is formatted as `+HH:MM`,
266    /// and no timezone conversions are done. It is interpreted literally.
267    #[inline(always)]
268    #[must_use]
269    pub fn format_with_offset(&self, offset: UtcOffset) -> TimestampStr<formats::FullMillisecondsOffset> {
270        self.format_raw(offset)
271    }
272
273    /// Formats a full timestamp with timezone offset, and the provided level of subsecond precision.
274    #[inline(always)]
275    #[must_use]
276    pub fn format_with_offset_and_precision<P: t::Unsigned>(
277        &self,
278        offset: UtcOffset,
279    ) -> TimestampStr<FormatString<t::True, t::True, P>>
280    where
281        FormatString<t::True, t::True, P>: IsValidFormat,
282    {
283        self.format_raw(offset)
284    }
285
286    /// Parse to UTC timestamp from any ISO8601 string. Offsets are applied during parsing.
287    #[inline(never)]
288    #[must_use] // Avoid deoptimizing the general &str case when presented with a fixed-size string
289    pub fn parse(ts: &str) -> Option<Self> {
290        parse::parse_iso8601(ts.as_bytes()).map(Timestamp)
291    }
292
293    /// Convert to `time::OffsetDateTime` with the given offset.
294    #[inline(always)]
295    #[must_use]
296    pub const fn assume_offset(self, offset: UtcOffset) -> time::OffsetDateTime {
297        self.0.assume_offset(offset)
298    }
299
300    /// Computes `self + duration`, returning `None` if an overflow occurred.
301    ///
302    /// See [`PrimitiveDateTime::checked_add`] for more implementation details
303    #[inline]
304    #[must_use]
305    pub const fn checked_add(self, duration: Duration) -> Option<Self> {
306        match self.0.checked_add(duration) {
307            Some(ts) => Some(Timestamp(ts)),
308            None => None,
309        }
310    }
311
312    /// Computes `self - duration`, returning `None` if an overflow occurred.
313    ///
314    /// See [`PrimitiveDateTime::checked_sub`] for more implementation details
315    #[inline]
316    #[must_use]
317    pub const fn checked_sub(self, duration: Duration) -> Option<Self> {
318        match self.0.checked_sub(duration) {
319            Some(ts) => Some(Timestamp(ts)),
320            None => None,
321        }
322    }
323
324    /// Computes `self + duration`, saturating value on overflow.
325    ///
326    /// See [`PrimitiveDateTime::saturating_add`] for more implementation details
327    #[inline]
328    #[must_use]
329    pub const fn saturating_add(self, duration: Duration) -> Self {
330        Timestamp(self.0.saturating_add(duration))
331    }
332
333    /// Computes `self - duration`, saturating value on overflow.
334    ///
335    /// See [`PrimitiveDateTime::saturating_sub`] for more implementation details
336    #[inline]
337    #[must_use]
338    pub const fn saturating_sub(self, duration: Duration) -> Self {
339        Timestamp(self.0.saturating_sub(duration))
340    }
341}
342
343impl Deref for Timestamp {
344    type Target = PrimitiveDateTime;
345
346    #[inline(always)]
347    fn deref(&self) -> &Self::Target {
348        &self.0
349    }
350}
351
352impl DerefMut for Timestamp {
353    #[inline(always)]
354    fn deref_mut(&mut self) -> &mut Self::Target {
355        &mut self.0
356    }
357}
358
359use core::ops::{Add, Sub};
360
361impl<T> Add<T> for Timestamp
362where
363    PrimitiveDateTime: Add<T, Output = PrimitiveDateTime>,
364{
365    type Output = Self;
366
367    #[inline]
368    fn add(self, rhs: T) -> Self::Output {
369        Timestamp(self.0 + rhs)
370    }
371}
372
373impl<T> Sub<T> for Timestamp
374where
375    PrimitiveDateTime: Sub<T, Output = PrimitiveDateTime>,
376{
377    type Output = Self;
378
379    #[inline]
380    fn sub(self, rhs: T) -> Self::Output {
381        Timestamp(self.0 - rhs)
382    }
383}
384
385impl<T> AddAssign<T> for Timestamp
386where
387    PrimitiveDateTime: AddAssign<T>,
388{
389    #[inline]
390    fn add_assign(&mut self, rhs: T) {
391        self.0 += rhs;
392    }
393}
394
395impl<T> SubAssign<T> for Timestamp
396where
397    PrimitiveDateTime: SubAssign<T>,
398{
399    #[inline]
400    fn sub_assign(&mut self, rhs: T) {
401        self.0 -= rhs;
402    }
403}
404
405#[cfg(feature = "serde")]
406mod serde_impl {
407    use serde_core::de::{Deserialize, Deserializer, Error, Visitor};
408    use serde_core::ser::{Serialize, Serializer};
409
410    #[cfg(feature = "bson")]
411    use serde_core::de::MapAccess;
412
413    use super::Timestamp;
414
415    impl Serialize for Timestamp {
416        #[inline]
417        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
418        where
419            S: Serializer,
420        {
421            if serializer.is_human_readable() {
422                self.format().serialize(serializer)
423            } else {
424                (self.duration_since(Timestamp::UNIX_EPOCH).whole_milliseconds() as i64).serialize(serializer)
425            }
426        }
427    }
428
429    const OUT_OF_RANGE: &str = "Milliseconds out of range";
430
431    impl<'de> Deserialize<'de> for Timestamp {
432        #[inline]
433        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
434        where
435            D: Deserializer<'de>,
436        {
437            use core::fmt;
438
439            struct TsVisitor;
440
441            #[allow(clippy::needless_lifetimes)] // breaks bson support if removed
442            impl<'de> Visitor<'de> for TsVisitor {
443                type Value = Timestamp;
444
445                #[inline]
446                fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
447                    formatter.write_str("an ISO8601 Timestamp")
448                }
449
450                #[inline]
451                fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
452                where
453                    E: Error,
454                {
455                    match Timestamp::parse(v) {
456                        Some(ts) => Ok(ts),
457                        None => Err(E::custom("Invalid Format")),
458                    }
459                }
460
461                #[cfg(feature = "bson")]
462                fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
463                where
464                    M: MapAccess<'de>,
465                {
466                    // In the MongoDB database, or generally with BSON, dates
467                    // are serialized into `{ $date: string }` where `$date`
468                    // is what we actually want. However, in some cases if the year is < 1970 or > 9999, it will be:
469                    // `{ $date: { $numberLong: string } }` where `$numberLong` is a signed integer (as a string)
470
471                    // in either case, to simplify things we recurse through the map until we find a primitive value
472
473                    let Some(key) = access.next_key::<&str>()? else {
474                        return Err(M::Error::custom("Map Is Empty"));
475                    };
476
477                    match key {
478                        // technically could parse non-string fields here, but it's unlikely and I don't care
479                        "$date" => access.next_value::<Timestamp>(), // recurse
480
481                        // technically this could occur at the top level, but same as above
482                        "$numberLong" => match access.next_value::<&str>()?.parse() {
483                            Ok(ms) => self.visit_i64(ms),
484                            Err(_) => Err(M::Error::custom("Invalid Number in `$numberLong` field")),
485                        },
486
487                        #[cfg(not(feature = "std"))]
488                        _ => Err(M::Error::custom("Unexpected key in map")),
489
490                        #[cfg(feature = "std")]
491                        _ => Err(M::Error::custom(format_args!("Unexpected key in map: {key}"))),
492                    }
493                }
494
495                #[inline]
496                fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
497                where
498                    E: Error,
499                {
500                    Timestamp::UNIX_EPOCH
501                        .checked_add(time::Duration::milliseconds(v))
502                        .ok_or_else(|| E::custom(OUT_OF_RANGE))
503                }
504
505                #[inline]
506                fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
507                where
508                    E: Error,
509                {
510                    let seconds = v / 1000;
511                    let nanoseconds = (v % 1_000) * 1_000_000;
512
513                    Timestamp::UNIX_EPOCH
514                        .checked_add(time::Duration::new(seconds as i64, nanoseconds as i32))
515                        .ok_or_else(|| E::custom(OUT_OF_RANGE))
516                }
517            }
518
519            deserializer.deserialize_any(TsVisitor)
520        }
521    }
522}
523
524#[cfg(feature = "diesel")]
525mod diesel_impl {
526    #[cfg(feature = "diesel-pg")]
527    use diesel::sql_types::Timestamptz as DbTimestamptz;
528    use diesel::{
529        backend::Backend,
530        deserialize::{self, FromSql},
531        serialize::{self, ToSql},
532        sql_types::Timestamp as DbTimestamp,
533    };
534    use time::PrimitiveDateTime;
535
536    use super::Timestamp;
537
538    impl<DB> FromSql<DbTimestamp, DB> for Timestamp
539    where
540        DB: Backend,
541        PrimitiveDateTime: FromSql<DbTimestamp, DB>,
542    {
543        fn from_sql(bytes: <DB as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
544            <PrimitiveDateTime as FromSql<DbTimestamp, DB>>::from_sql(bytes).map(Timestamp::from)
545        }
546    }
547
548    #[cfg(feature = "diesel-pg")]
549    impl<DB> FromSql<DbTimestamptz, DB> for Timestamp
550    where
551        DB: Backend,
552        PrimitiveDateTime: FromSql<DbTimestamptz, DB>,
553    {
554        fn from_sql(bytes: <DB as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
555            <PrimitiveDateTime as FromSql<DbTimestamptz, DB>>::from_sql(bytes).map(Timestamp::from)
556        }
557    }
558
559    impl<DB> ToSql<DbTimestamp, DB> for Timestamp
560    where
561        DB: Backend,
562        PrimitiveDateTime: ToSql<DbTimestamp, DB>,
563    {
564        fn to_sql<'b>(&'b self, out: &mut serialize::Output<'b, '_, DB>) -> serialize::Result {
565            <PrimitiveDateTime as ToSql<DbTimestamp, DB>>::to_sql(self, out)
566        }
567    }
568
569    #[cfg(feature = "diesel-pg")]
570    impl<DB> ToSql<DbTimestamptz, DB> for Timestamp
571    where
572        DB: Backend,
573        PrimitiveDateTime: ToSql<DbTimestamptz, DB>,
574    {
575        fn to_sql<'b>(&'b self, out: &mut serialize::Output<'b, '_, DB>) -> serialize::Result {
576            <PrimitiveDateTime as ToSql<DbTimestamptz, DB>>::to_sql(self, out)
577        }
578    }
579
580    #[cfg(feature = "rkyv_08")]
581    const _: () = {
582        use diesel::query_builder::bind_collector::RawBytesBindCollector;
583
584        use super::ArchivedTimestamp;
585
586        impl<DB> ToSql<DbTimestamp, DB> for ArchivedTimestamp
587        where
588            for<'c> DB: Backend<BindCollector<'c> = RawBytesBindCollector<DB>>,
589            Timestamp: ToSql<DbTimestamp, DB>,
590        {
591            fn to_sql<'b>(&'b self, out: &mut serialize::Output<'b, '_, DB>) -> serialize::Result {
592                <Timestamp as ToSql<DbTimestamp, DB>>::to_sql(&Timestamp::from(*self), &mut out.reborrow())
593            }
594        }
595
596        #[cfg(feature = "diesel-pg")]
597        impl<DB> ToSql<DbTimestamptz, DB> for ArchivedTimestamp
598        where
599            for<'c> DB: Backend<BindCollector<'c> = RawBytesBindCollector<DB>>,
600            Timestamp: ToSql<DbTimestamptz, DB>,
601        {
602            fn to_sql<'b>(&'b self, out: &mut serialize::Output<'b, '_, DB>) -> serialize::Result {
603                <Timestamp as ToSql<DbTimestamptz, DB>>::to_sql(&Timestamp::from(*self), &mut out.reborrow())
604            }
605        }
606    };
607}
608
609#[cfg(feature = "pg")]
610mod pg_impl {
611    use postgres_types::{accepts, to_sql_checked, FromSql, IsNull, ToSql, Type};
612    use time::PrimitiveDateTime;
613
614    use super::Timestamp;
615
616    impl ToSql for Timestamp {
617        #[inline]
618        fn to_sql(
619            &self,
620            ty: &Type,
621            out: &mut bytes::BytesMut,
622        ) -> Result<IsNull, Box<dyn core::error::Error + Sync + Send>>
623        where
624            Self: Sized,
625        {
626            self.0.to_sql(ty, out)
627        }
628
629        accepts!(TIMESTAMP, TIMESTAMPTZ);
630        to_sql_checked!();
631    }
632
633    impl<'a> FromSql<'a> for Timestamp {
634        #[inline]
635        fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn core::error::Error + Sync + Send>> {
636            PrimitiveDateTime::from_sql(ty, raw).map(Timestamp)
637        }
638
639        accepts!(TIMESTAMP, TIMESTAMPTZ);
640    }
641
642    #[cfg(feature = "rkyv_08")]
643    const _: () = {
644        impl ToSql for super::ArchivedTimestamp {
645            fn to_sql(
646                &self,
647                _ty: &Type,
648                out: &mut bytes::BytesMut,
649            ) -> Result<IsNull, Box<dyn core::error::Error + Sync + Send>> {
650                const EPOCH_OFFSET: i64 = 946684800000000; // 2000-01-01T00:00:00Z
651
652                // convert to microseconds
653                let Some(ts) = self.0.to_native().checked_mul(1000) else {
654                    return Err("Timestamp out of range".into());
655                };
656
657                // convert to postgres timestamp
658                let Some(pts) = ts.checked_sub(EPOCH_OFFSET) else {
659                    return Err("Timestamp out of range".into());
660                };
661
662                postgres_protocol::types::time_to_sql(pts, out);
663
664                Ok(IsNull::No)
665            }
666
667            accepts!(TIMESTAMP, TIMESTAMPTZ);
668            to_sql_checked!();
669        }
670    };
671}
672
673#[cfg(feature = "rusqlite")]
674mod rusqlite_impl {
675    use super::{Duration, Timestamp};
676
677    use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, Value, ValueRef};
678    use rusqlite::Error;
679
680    #[derive(Debug)]
681    struct InvalidTimestamp;
682
683    use core::{error, fmt, str};
684
685    extern crate alloc;
686
687    use alloc::borrow::ToOwned;
688
689    impl error::Error for InvalidTimestamp {}
690    impl fmt::Display for InvalidTimestamp {
691        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692            f.write_str("Invalid ISO8601 Timestamp")
693        }
694    }
695
696    impl FromSql for Timestamp {
697        fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
698            // https://www.sqlite.org/lang_datefunc.html
699            match value {
700                ValueRef::Text(bytes) => match str::from_utf8(bytes) {
701                    Err(e) => Err(FromSqlError::Other(Error::Utf8Error(e).into())),
702                    Ok(ts) => match Timestamp::parse(ts) {
703                        Some(ts) => Ok(ts),
704                        None => Err(FromSqlError::Other(InvalidTimestamp.into())),
705                    },
706                },
707
708                // according to the link above, dates stored as integers are seconds since unix epoch
709                ValueRef::Integer(ts) => Timestamp::UNIX_EPOCH
710                    .checked_add(Duration::seconds(ts))
711                    .ok_or_else(|| FromSqlError::OutOfRange(ts)),
712
713                // according to the link above, dates stored as floats are the number of
714                // fractional days since -4713-11-24 12:00:00, and 2440587.5 is the
715                // number of days between -4713-11-24 12:00:00 and 1970-01-01 00:00:00
716                ValueRef::Real(ts) => {
717                    let ts = Duration::seconds_f64((ts - 2440587.5) * 86_400.0);
718
719                    Timestamp::UNIX_EPOCH
720                        .checked_add(ts)
721                        .ok_or_else(|| FromSqlError::OutOfRange(ts.whole_seconds()))
722                }
723
724                _ => Err(FromSqlError::InvalidType),
725            }
726        }
727    }
728
729    impl ToSql for Timestamp {
730        fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
731            Ok(ToSqlOutput::Owned(Value::Text(self.format().to_owned())))
732        }
733    }
734
735    #[cfg(feature = "rkyv_08")]
736    const _: () = {
737        impl ToSql for super::ArchivedTimestamp {
738            fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
739                Ok(ToSqlOutput::Owned(Value::Text(
740                    Timestamp::from(*self).format().to_owned(),
741                )))
742            }
743        }
744    };
745}
746
747#[cfg(feature = "schema")]
748mod schema_impl {
749    use schemars::{json_schema, JsonSchema, Schema, SchemaGenerator};
750
751    extern crate alloc;
752
753    use alloc::borrow::Cow;
754
755    use super::Timestamp;
756
757    impl JsonSchema for Timestamp {
758        fn schema_name() -> Cow<'static, str> {
759            Cow::Borrowed("ISO8601 Timestamp")
760        }
761
762        fn schema_id() -> Cow<'static, str> {
763            Cow::Borrowed("iso8601_timestamp::Timestamp")
764        }
765
766        fn json_schema(_: &mut SchemaGenerator) -> Schema {
767            json_schema!({
768                "type": "string",
769                "format": "date-time",
770                "description": "ISO8601 formatted timestamp",
771                "examples": ["1970-01-01T00:00:00Z"],
772            })
773        }
774    }
775}
776
777#[cfg(feature = "rand")]
778mod rand_impl {
779    use rand::distr::{Distribution, StandardUniform};
780    use rand::Rng;
781
782    use super::Timestamp;
783
784    impl Distribution<Timestamp> for StandardUniform {
785        #[inline]
786        fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Timestamp {
787            Timestamp(rng.random())
788        }
789    }
790}
791
792#[cfg(feature = "quickcheck")]
793mod quickcheck_impl {
794    extern crate alloc;
795
796    use alloc::boxed::Box;
797    use quickcheck::{Arbitrary, Gen};
798
799    use super::Timestamp;
800
801    impl Arbitrary for Timestamp {
802        #[inline(always)]
803        fn arbitrary(g: &mut Gen) -> Self {
804            Timestamp(Arbitrary::arbitrary(g))
805        }
806
807        fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
808            Box::new(
809                (self.date(), self.time())
810                    .shrink()
811                    .map(|(d, t)| Timestamp(time::PrimitiveDateTime::new(d, t))),
812            )
813        }
814    }
815}
816
817#[cfg(feature = "ramhorns")]
818mod ramhorns_impl {
819    use super::{formats::FullMilliseconds, ts_str::IsValidFormat, Timestamp};
820
821    use generic_array::typenum::Unsigned;
822    use ramhorns::{encoding::Encoder, Content};
823
824    impl Content for Timestamp {
825        fn capacity_hint(&self, _tpl: &ramhorns::Template) -> usize {
826            <FullMilliseconds as IsValidFormat>::Length::USIZE
827        }
828
829        fn render_escaped<E: Encoder>(&self, encoder: &mut E) -> Result<(), E::Error> {
830            encoder.write_unescaped(&self.format())
831        }
832    }
833
834    #[cfg(feature = "rkyv_08")]
835    const _: () = {
836        impl Content for super::ArchivedTimestamp {
837            fn capacity_hint(&self, _tpl: &ramhorns::Template) -> usize {
838                <FullMilliseconds as IsValidFormat>::Length::USIZE
839            }
840
841            fn render_escaped<E: Encoder>(&self, encoder: &mut E) -> Result<(), E::Error> {
842                encoder.write_unescaped(&Timestamp::from(*self).format())
843            }
844        }
845    };
846}
847
848#[cfg(feature = "rkyv_08")]
849pub use rkyv_08_impl::ArchivedTimestamp;
850
851#[cfg(feature = "rkyv_08")]
852mod rkyv_08_impl {
853    use super::*;
854
855    use rkyv_08::{
856        bytecheck::CheckBytes,
857        place::Place,
858        rancor::{Fallible, Source},
859        traits::NoUndef,
860        Archive, Archived, Deserialize, Serialize,
861    };
862
863    /// `rkyv`-ed Timestamp as a 64-bit signed millisecond offset from the UNIX Epoch.
864    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, rkyv_08::Portable)]
865    #[rkyv(crate = rkyv_08)]
866    #[repr(transparent)]
867    pub struct ArchivedTimestamp(pub Archived<i64>);
868
869    // SAFETY: ArchivedTimestamp is repr(transparent) over i64_le
870    unsafe impl NoUndef for ArchivedTimestamp {}
871
872    impl ArchivedTimestamp {
873        /// Get the raw millisecond offset
874        #[inline(always)]
875        #[must_use]
876        pub const fn get(self) -> i64 {
877            self.0.to_native()
878        }
879    }
880
881    impl From<ArchivedTimestamp> for Timestamp {
882        fn from(value: ArchivedTimestamp) -> Self {
883            Timestamp::UNIX_EPOCH
884                .checked_add(Duration::milliseconds(value.get()))
885                .unwrap_or(Timestamp::UNIX_EPOCH) // should never fail, but provide a sane fallback anyway
886        }
887    }
888
889    impl Archive for Timestamp {
890        type Archived = ArchivedTimestamp;
891        type Resolver = ();
892
893        fn resolve(&self, _resolver: Self::Resolver, out: Place<Self::Archived>) {
894            out.write(ArchivedTimestamp(<Archived<i64>>::from_native(
895                self.duration_since(Timestamp::UNIX_EPOCH).whole_milliseconds() as i64,
896            )));
897        }
898    }
899
900    impl<S: Fallible + ?Sized> Serialize<S> for Timestamp {
901        #[inline(always)]
902        fn serialize(&self, _serializer: &mut S) -> Result<Self::Resolver, S::Error> {
903            Ok(())
904        }
905    }
906
907    impl<D: Fallible + ?Sized> Deserialize<Timestamp, D> for ArchivedTimestamp {
908        #[inline]
909        fn deserialize(&self, _deserializer: &mut D) -> Result<Timestamp, <D as Fallible>::Error> {
910            Ok(Timestamp::from(*self))
911        }
912    }
913
914    // SAFETY: ArchivedTimestamp is repr(transparent) over i64_le
915    unsafe impl<C> CheckBytes<C> for ArchivedTimestamp
916    where
917        C: Fallible + ?Sized,
918        <C as Fallible>::Error: Source,
919    {
920        #[inline(always)]
921        unsafe fn check_bytes<'a>(value: *const Self, context: &mut C) -> Result<(), C::Error> {
922            CheckBytes::<C>::check_bytes(value as *const Archived<i64>, context)
923        }
924    }
925}
926
927#[cfg(feature = "fred")]
928mod fred_impl {
929    use fred::{
930        error::{Error, ErrorKind},
931        types::{Expiration, FromKey, FromValue, Key, Value},
932    };
933
934    use super::{Duration, Timestamp};
935
936    impl From<Timestamp> for Value {
937        fn from(ts: Timestamp) -> Self {
938            Value::Integer(ts.duration_since(Timestamp::UNIX_EPOCH).whole_milliseconds() as i64)
939        }
940    }
941
942    impl From<Timestamp> for Key {
943        fn from(ts: Timestamp) -> Self {
944            Key::from(&*ts.format())
945        }
946    }
947
948    impl FromValue for Timestamp {
949        fn from_value(value: Value) -> Result<Self, Error> {
950            match value {
951                Value::String(ts) => Timestamp::parse(&ts)
952                    .ok_or_else(|| Error::new(ErrorKind::Parse, "Invalid Timestamp format")),
953                Value::Bytes(ts) => match core::str::from_utf8(&ts) {
954                    Ok(ts) => Timestamp::parse(ts)
955                        .ok_or_else(|| Error::new(ErrorKind::Parse, "Invalid Timestamp format")),
956                    Err(_) => Err(Error::new(ErrorKind::Parse, "Invalid UTF-8 Timestamp")),
957                },
958                Value::Integer(ts) => Timestamp::UNIX_EPOCH
959                    .checked_add(Duration::seconds(ts))
960                    .ok_or_else(|| Error::new(ErrorKind::Parse, "Timestamp out of range")),
961                _ => Err(Error::new(ErrorKind::Parse, "Invalid Timestamp type")),
962            }
963        }
964    }
965
966    impl FromKey for Timestamp {
967        fn from_key(value: Key) -> Result<Self, Error> {
968            let Ok(value) = core::str::from_utf8(value.as_bytes()) else {
969                return Err(Error::new(ErrorKind::Parse, "Invalid UTF-8 Key"));
970            };
971
972            Timestamp::parse(value).ok_or_else(|| Error::new(ErrorKind::Parse, "Invalid Timestamp format"))
973        }
974    }
975
976    impl From<Timestamp> for Expiration {
977        fn from(ts: Timestamp) -> Self {
978            Expiration::PXAT(ts.duration_since(Timestamp::UNIX_EPOCH).whole_milliseconds() as i64)
979        }
980    }
981
982    #[cfg(feature = "rkyv_08")]
983    const _: () = {
984        use super::ArchivedTimestamp;
985
986        impl From<ArchivedTimestamp> for Value {
987            fn from(ts: ArchivedTimestamp) -> Self {
988                Value::Integer(ts.get())
989            }
990        }
991
992        impl From<ArchivedTimestamp> for Key {
993            fn from(value: ArchivedTimestamp) -> Self {
994                Key::from(&*Timestamp::from(value).format())
995            }
996        }
997
998        impl From<ArchivedTimestamp> for Expiration {
999            fn from(ts: ArchivedTimestamp) -> Self {
1000                Expiration::PXAT(ts.get())
1001            }
1002        }
1003    };
1004}
1005
1006#[cfg(feature = "borsh")]
1007mod borsh_impl {
1008    use super::{Duration, Timestamp};
1009
1010    use borsh::{io, BorshDeserialize, BorshSerialize};
1011
1012    impl BorshSerialize for Timestamp {
1013        fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
1014            let ts = self.duration_since(Timestamp::UNIX_EPOCH).whole_milliseconds() as i64;
1015
1016            ts.serialize(writer)
1017        }
1018    }
1019
1020    impl BorshDeserialize for Timestamp {
1021        fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
1022            let ts = i64::deserialize_reader(reader)?;
1023
1024            Timestamp::UNIX_EPOCH
1025                .checked_add(Duration::milliseconds(ts))
1026                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Timestamp out of range"))
1027        }
1028    }
1029}