Skip to main content

deser_core/adapters/
time.rs

1//! Adapters for durations and timestamps as numbers of a unit:
2//! [`Seconds`], [`Millis`], [`Micros`] and [`Nanos`].
3use alloc::borrow::Cow;
4use alloc::string::{String, ToString};
5use core::marker::PhantomData;
6
7use crate::State;
8use crate::Text;
9use crate::de::{Deserialize, Slot, default_atom};
10use crate::error::{Error, ErrorKind};
11use crate::event::Atom;
12use crate::ext::known::{Bridge, WellKnown, out_of_range};
13use crate::ext::{Duration, ExtValue, Number, Timestamp};
14use crate::ser::{Emit, Serialize};
15
16const NANOS_PER_SECOND: i128 = 1_000_000_000;
17
18mod sealed {
19    pub trait Sealed {}
20}
21
22/// A duration or timestamp that the time unit adapters support.
23///
24/// This is implemented for [`ext::Duration`](Duration) and
25/// [`ext::Timestamp`](Timestamp) and for all types that serialize as one
26/// of them: [`core::time::Duration`] and [`SystemTime`], and
27/// with the features of the same names `chrono::TimeDelta`,
28/// `chrono::DateTime<Utc>`, `jiff::SignedDuration`, `jiff::Timestamp`,
29/// `time::Duration` and `time::UtcDateTime`.  Date-times with an offset
30/// or a time zone are not supported, as a number of seconds since the
31/// epoch loses them.
32///
33/// This trait is sealed.
34///
35#[cfg_attr(feature = "std", doc = "[`SystemTime`]: std::time::SystemTime")]
36#[cfg_attr(
37    not(feature = "std"),
38    doc = "[`SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html"
39)]
40pub trait TimeValue: Sized + Send + Sync + sealed::Sealed {
41    /// Returns the value in nanoseconds (since the epoch for timestamps).
42    #[doc(hidden)]
43    fn __to_nanos(&self) -> Result<i128, Error>;
44
45    /// Creates the value from nanoseconds.
46    #[doc(hidden)]
47    fn __from_nanos(nanos: i128) -> Result<Self, Error>;
48
49    /// Creates the value from the extension value it serializes as.
50    #[doc(hidden)]
51    fn __from_ext(ext: &ExtValue) -> Result<Option<Self>, Error>;
52
53    /// Returns `true` for timestamps, `false` for durations.
54    ///
55    /// Timestamps are truncated towards the past, durations towards zero.
56    #[doc(hidden)]
57    fn __is_instant() -> bool;
58}
59
60/// The well-known types the time unit adapters support.
61pub(crate) trait KnownTime: WellKnown + Copy {
62    const IS_INSTANT: bool;
63    fn to_nanos(&self) -> i128;
64    fn from_nanos(nanos: i128) -> Result<Self, Error>;
65}
66
67impl KnownTime for Duration {
68    const IS_INSTANT: bool = false;
69
70    fn to_nanos(&self) -> i128 {
71        i128::from(self.seconds) * NANOS_PER_SECOND + i128::from(self.nanosecond)
72    }
73
74    fn from_nanos(nanos: i128) -> Result<Duration, Error> {
75        // the seconds and nanoseconds have the same sign
76        Ok(Duration {
77            seconds: i64::try_from(nanos / NANOS_PER_SECOND)
78                .map_err(|_| out_of_range("duration out of range"))?,
79            nanosecond: (nanos % NANOS_PER_SECOND) as i32,
80        })
81    }
82}
83
84impl KnownTime for Timestamp {
85    const IS_INSTANT: bool = true;
86
87    fn to_nanos(&self) -> i128 {
88        i128::from(self.seconds) * NANOS_PER_SECOND + i128::from(self.nanosecond)
89    }
90
91    fn from_nanos(nanos: i128) -> Result<Timestamp, Error> {
92        // the nanoseconds are always positive
93        Ok(Timestamp {
94            seconds: i64::try_from(nanos.div_euclid(NANOS_PER_SECOND))
95                .map_err(|_| out_of_range("timestamp out of range"))?,
96            nanosecond: nanos.rem_euclid(NANOS_PER_SECOND) as u32,
97        })
98    }
99}
100
101/// Implements [`TimeValue`] for the well-known time types.
102macro_rules! known_time_value {
103    ($($ty:ty),*) => {
104        $(
105            impl sealed::Sealed for $ty {}
106
107            impl TimeValue for $ty {
108                fn __to_nanos(&self) -> Result<i128, Error> {
109                    Ok(self.to_nanos())
110                }
111
112                fn __from_nanos(nanos: i128) -> Result<Self, Error> {
113                    <$ty as KnownTime>::from_nanos(nanos)
114                }
115
116                fn __from_ext(ext: &ExtValue) -> Result<Option<Self>, Error> {
117                    Ok(ext.downcast_ref::<$ty>().copied())
118                }
119
120                fn __is_instant() -> bool {
121                    <$ty as KnownTime>::IS_INSTANT
122                }
123            }
124        )*
125    };
126}
127
128known_time_value!(Duration, Timestamp);
129
130/// Implements [`TimeValue`] for types that are bridged onto a well-known
131/// time type.
132macro_rules! bridged_time_value {
133    ($($ty:ty),* $(,)?) => {
134        $(
135            impl sealed::Sealed for $ty {}
136
137            impl TimeValue for $ty {
138                fn __to_nanos(&self) -> Result<i128, Error> {
139                    Ok(Bridge::to_known(self)?.to_nanos())
140                }
141
142                fn __from_nanos(nanos: i128) -> Result<Self, Error> {
143                    <$ty as Bridge>::from_known(KnownTime::from_nanos(nanos)?)
144                }
145
146                fn __from_ext(ext: &ExtValue) -> Result<Option<Self>, Error> {
147                    match ext.downcast_ref::<<$ty as Bridge>::Known>() {
148                        Some(known) => <$ty as Bridge>::from_known(*known).map(Some),
149                        None => Ok(None),
150                    }
151                }
152
153                fn __is_instant() -> bool {
154                    <<$ty as Bridge>::Known as KnownTime>::IS_INSTANT
155                }
156            }
157        )*
158    };
159}
160
161bridged_time_value!(core::time::Duration);
162
163#[cfg(feature = "std")]
164bridged_time_value!(std::time::SystemTime);
165
166#[cfg(feature = "chrono")]
167bridged_time_value!(::chrono::TimeDelta, ::chrono::DateTime<::chrono::Utc>);
168
169#[cfg(feature = "jiff")]
170bridged_time_value!(::jiff::SignedDuration, ::jiff::Timestamp);
171
172#[cfg(feature = "time")]
173bridged_time_value!(::time::Duration, ::time::UtcDateTime);
174
175/// How the time unit adapters write numbers: as `i64`, `f64` or `String`.
176///
177/// This trait is sealed.
178pub trait UnitFormat: unit_format::Sealed + 'static {
179    /// Returns the atom for a number of nanoseconds.
180    #[doc(hidden)]
181    fn __atom(nanos: i128, unit: i128, instant: bool) -> Atom<'static>;
182}
183
184mod unit_format {
185    pub trait Sealed {}
186    impl Sealed for i64 {}
187    impl Sealed for f64 {}
188    impl Sealed for alloc::string::String {}
189}
190
191/// Returns the number of units, truncated.
192///
193/// Timestamps are truncated towards the past and durations towards zero.
194fn whole_units(nanos: i128, unit: i128, instant: bool) -> i128 {
195    if instant {
196        nanos.div_euclid(unit)
197    } else {
198        nanos / unit
199    }
200}
201
202/// Returns the atom for an integer.
203fn integer_atom(value: i128) -> Atom<'static> {
204    match i64::try_from(value) {
205        Ok(value) => Atom::I64(value),
206        Err(_) => Atom::Ext(ExtValue::owned(value)),
207    }
208}
209
210/// Writes whole units as integer, the fraction of a unit is truncated
211/// (towards the past for timestamps, towards zero for durations).
212impl UnitFormat for i64 {
213    fn __atom(nanos: i128, unit: i128, instant: bool) -> Atom<'static> {
214        integer_atom(whole_units(nanos, unit, instant))
215    }
216}
217
218/// Writes units as float with the fraction of a unit.
219impl UnitFormat for f64 {
220    fn __atom(nanos: i128, unit: i128, instant: bool) -> Atom<'static> {
221        let whole = whole_units(nanos, unit, instant);
222        let fraction = nanos - whole * unit;
223        Atom::F64(whole as f64 + fraction as f64 / unit as f64)
224    }
225}
226
227/// Writes whole units as string, the fraction of a unit is truncated like
228/// for `i64`.
229impl UnitFormat for String {
230    fn __atom(nanos: i128, unit: i128, instant: bool) -> Atom<'static> {
231        Atom::Str(Text::owned(whole_units(nanos, unit, instant).to_string()))
232    }
233}
234
235/// Returns the error for a time that is out of range.
236#[cold]
237fn time_out_of_range() -> Error {
238    out_of_range("time out of range")
239}
240
241/// Converts a number of units into nanoseconds.
242fn int_nanos(value: i128, unit_exp: u32) -> Result<i128, Error> {
243    value
244        .checked_mul(10i128.pow(unit_exp))
245        .ok_or_else(time_out_of_range)
246}
247
248/// Converts the decimal text of a number of units into nanoseconds.
249///
250/// The number can have a fraction and an exponent (`-1.5`, `2.5e3`), it's
251/// rounded to the nearest nanosecond (half way cases away from zero).
252/// Returns `None` if the text is not a number.
253fn decimal_nanos(text: &str, unit_exp: u32) -> Option<Result<i128, Error>> {
254    let text = text.as_bytes();
255    let (negative, text) = match text.first() {
256        Some(b'-') => (true, &text[1..]),
257        Some(b'+') => (false, &text[1..]),
258        _ => (false, text),
259    };
260    let (mantissa, exp) = match text.iter().position(|&x| x == b'e' || x == b'E') {
261        Some(index) => {
262            let exp = core::str::from_utf8(&text[index + 1..]).ok()?;
263            (&text[..index], exp.parse::<i32>().ok()?)
264        }
265        None => (text, 0),
266    };
267    let (int_part, frac_part) = match mantissa.iter().position(|&x| x == b'.') {
268        Some(index) => (&mantissa[..index], &mantissa[index + 1..]),
269        None => (mantissa, &[][..]),
270    };
271    if (int_part.is_empty() && frac_part.is_empty())
272        || !int_part.iter().chain(frac_part).all(u8::is_ascii_digit)
273    {
274        return None;
275    }
276
277    // the number of digits before the decimal point of the nanoseconds
278    let point = int_part.len() as i64 + i64::from(exp) + i64::from(unit_exp);
279    let mut value = 0i128;
280    let mut round_up = false;
281    let mut index = 0i64;
282    for &digit in int_part.iter().chain(frac_part) {
283        if index < point {
284            value = match value
285                .checked_mul(10)
286                .and_then(|x| x.checked_add(i128::from(digit - b'0')))
287            {
288                Some(value) => value,
289                None => return Some(Err(time_out_of_range())),
290            };
291        } else if index == point {
292            // only the first digit after the point decides the rounding
293            round_up = digit >= b'5';
294            break;
295        }
296        index += 1;
297    }
298    // the zeros between the digits and the decimal point
299    if value != 0 && point > index {
300        let zeros = u32::try_from(point - index).unwrap_or(u32::MAX);
301        value = match 10i128.checked_pow(zeros).and_then(|x| value.checked_mul(x)) {
302            Some(value) => value,
303            None => return Some(Err(time_out_of_range())),
304        };
305    }
306    if round_up {
307        value += 1;
308    }
309    Some(Ok(if negative { -value } else { value }))
310}
311
312/// A buffer on the stack for the text of a float.
313struct FloatText {
314    buf: [u8; 48],
315    len: usize,
316}
317
318impl core::fmt::Write for FloatText {
319    fn write_str(&mut self, s: &str) -> core::fmt::Result {
320        let end = self.len + s.len();
321        self.buf
322            .get_mut(self.len..end)
323            .ok_or(core::fmt::Error)?
324            .copy_from_slice(s.as_bytes());
325        self.len = end;
326        Ok(())
327    }
328}
329
330/// Converts a float number of units into nanoseconds.
331///
332/// The float is converted as the shortest decimal number that reads back
333/// as the same float (`0.1` is exactly 100 milliseconds), rounded to the
334/// nearest nanosecond.
335fn float_nanos(value: f64, unit_exp: u32) -> Result<i128, Error> {
336    if !value.is_finite() {
337        return Err(time_out_of_range());
338    }
339    let mut text = FloatText {
340        buf: [0; 48],
341        len: 0,
342    };
343    // the exponent form is short also for very large and small values
344    core::fmt::write(&mut text, format_args!("{:e}", value)).map_err(|_| time_out_of_range())?;
345    let text = core::str::from_utf8(&text.buf[..text.len]).map_err(|_| time_out_of_range())?;
346    decimal_nanos(text, unit_exp).unwrap_or_else(|| Err(time_out_of_range()))
347}
348
349/// Converts the text of a number of units into nanoseconds.
350fn text_nanos(text: &str, unit_exp: u32, name: &str) -> Result<i128, Error> {
351    decimal_nanos(text, unit_exp).unwrap_or_else(|| {
352        Err(Error::new(
353            ErrorKind::InvalidValue,
354            alloc::format!("invalid number of {}", name),
355        ))
356    })
357}
358
359/// Deserializes an atom with a number of units into a value.
360fn unit_atom<'de, T, A>(
361    slot: &mut Slot<T, A>,
362    atom: Atom,
363    unit_exp: u32,
364    name: &str,
365    state: &mut State,
366) -> Result<(), Error>
367where
368    T: TimeValue,
369    A: Deserialize<'de, T>,
370{
371    let nanos = match atom {
372        Atom::U64(value) => int_nanos(i128::from(value), unit_exp)?,
373        Atom::I64(value) => int_nanos(i128::from(value), unit_exp)?,
374        Atom::F64(value) => float_nanos(value, unit_exp)?,
375        Atom::Str(ref text) | Atom::Lexical(ref text) => text_nanos(text, unit_exp, name)?,
376        Atom::Ext(ref ext) => {
377            if let Some(&value) = ext.downcast_ref::<i128>() {
378                int_nanos(value, unit_exp)?
379            } else if let Some(&value) = ext.downcast_ref::<u128>() {
380                let value = i128::try_from(value).map_err(|_| time_out_of_range())?;
381                int_nanos(value, unit_exp)?
382            } else if let Some(number) = ext.downcast_value_ref::<Number>() {
383                // exact numbers keep their text
384                text_nanos(number.as_str(), unit_exp, name)?
385            } else if let Some(value) = T::__from_ext(ext)? {
386                // the value is written as the type it serializes as
387                slot.set(value);
388                return Ok(());
389            } else {
390                return default_atom(slot, atom, state);
391            }
392        }
393        other => return default_atom(slot, other, state),
394    };
395    slot.set(T::__from_nanos(nanos)?);
396    Ok(())
397}
398
399/// Implements the time unit adapters.
400macro_rules! time_unit {
401    ($(#[$meta:meta])* $name:ident, $unit_exp:expr, $expecting:literal;) => {
402        $(#[$meta])*
403        pub struct $name<F = i64>(PhantomData<fn() -> F>);
404
405        impl<'de, T: TimeValue, F: UnitFormat> Deserialize<'de, T> for $name<F> {
406            fn deserialize_atom(
407                slot: &mut Slot<T, Self>,
408                atom: Atom,
409                state: &mut State,
410            ) -> Result<(), Error> {
411                unit_atom(slot, atom, $unit_exp, $expecting, state)
412            }
413
414            fn expecting() -> Cow<'static, str> {
415                Cow::Borrowed($expecting)
416            }
417
418            slot_atom_into!(T);
419        }
420
421        impl<T: TimeValue, F: UnitFormat> Serialize<T> for $name<F> {
422            fn serialize<'a>(value: &'a T, _state: &mut State) -> Result<Emit<'a>, Error> {
423                Ok(Emit::Atom(F::__atom(
424                    value.__to_nanos()?,
425                    10i128.pow($unit_exp),
426                    T::__is_instant(),
427                )))
428            }
429
430            begin_without_finish!(T);
431        }
432    };
433}
434
435time_unit! {
436    /// A duration or timestamp as number of seconds.
437    ///
438    /// Durations are written as their length and timestamps as the time
439    /// since the Unix epoch (`1970-01-01T00:00:00Z`).  `F` is how the
440    /// number is written:
441    ///
442    /// * `i64` (the default): an integer.  The fraction of a second is
443    ///   truncated, towards the past for timestamps and towards zero for
444    ///   durations (like the seconds of the types themselves).
445    /// * `f64`: a float with the fraction of a second (which loses
446    ///   precision for large values).
447    /// * `String`: an integer as string, truncated like `i64`.
448    ///
449    /// When deserializing, all of these are accepted independent of `F`:
450    /// integers, floats and strings that contain a number (also with a
451    /// fraction or an exponent, like `1.5` or `2e3`), as well as the value
452    /// in the form it serializes as without the adapter.  Fractions are
453    /// rounded to the nearest nanosecond, floats are read as the shortest
454    /// decimal number that is the same float (`0.1` seconds are exactly 100
455    /// milliseconds).
456    ///
457    /// The supported types are listed in [`TimeValue`].  [`Millis`],
458    /// [`Micros`] and [`Nanos`] are the same for smaller units.
459    ///
460    /// ```
461    /// use std::time::{Duration, SystemTime};
462    /// use deser::adapters::{Millis, Seconds};
463    /// use deser::{Deserialize, Serialize};
464    ///
465    /// #[derive(Serialize, Deserialize)]
466    /// pub struct Session {
467    ///     #[deser(as = Seconds)]
468    ///     created: SystemTime,
469    ///     #[deser(as = Seconds<f64>)]
470    ///     timeout: Duration,
471    ///     #[deser(as = Millis)]
472    ///     latency: Duration,
473    /// }
474    ///
475    /// let input = r#"{"created":1700000000,"timeout":1.5,"latency":250}"#;
476    /// let session: Session = deser_json::from_str(input).unwrap();
477    /// assert_eq!(session.timeout, Duration::from_millis(1500));
478    /// assert_eq!(session.latency, Duration::from_millis(250));
479    /// assert_eq!(deser_json::to_string(&session).unwrap(), input);
480    /// ```
481    Seconds, 9, "seconds";
482}
483
484time_unit! {
485    /// A duration or timestamp as number of milliseconds.
486    ///
487    /// This is like [`Seconds`] in milliseconds.
488    ///
489    /// ```
490    /// use std::time::SystemTime;
491    /// use deser::adapters::Millis;
492    /// use deser::Serialize;
493    ///
494    /// #[derive(Serialize)]
495    /// pub struct Event {
496    ///     #[deser(as = Millis)]
497    ///     at: SystemTime,
498    /// }
499    /// ```
500    Millis, 6, "milliseconds";
501}
502
503time_unit! {
504    /// A duration or timestamp as number of microseconds.
505    ///
506    /// This is like [`Seconds`] in microseconds.
507    Micros, 3, "microseconds";
508}
509
510time_unit! {
511    /// A duration or timestamp as number of nanoseconds.
512    ///
513    /// This is like [`Seconds`] in nanoseconds.  Timestamps in nanoseconds
514    /// exceed `i64` after the year 2262, integers that do not fit are
515    /// written as 128 bit integers.
516    Nanos, 0, "nanoseconds";
517}