Skip to main content

deser_core/ext/
datetime.rs

1use alloc::format;
2use alloc::string::ToString;
3use core::fmt;
4use core::str::FromStr;
5
6use crate::error::{Error, ErrorKind};
7use crate::event::Atom;
8use crate::ext::Extension;
9use crate::ext::known::{WellKnown, floor, impl_well_known, invalid, out_of_range, round};
10
11/// A calendar date (`1979-05-27`).
12///
13/// See [`Datetime`].
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct Date {
16    /// The year (`0` to `9999`).
17    pub year: u16,
18    /// The month (`1` to `12`).
19    pub month: u8,
20    /// The day of the month (`1` to `31`).
21    pub day: u8,
22}
23
24/// A time of the day (`07:32:00.999`).
25///
26/// See [`Datetime`].
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct Time {
29    /// The hour (`0` to `23`).
30    pub hour: u8,
31    /// The minute (`0` to `59`).
32    pub minute: u8,
33    /// The second (`0` to `60`, `60` for leap seconds).
34    pub second: u8,
35    /// The fraction of the second in nanoseconds.
36    pub nanosecond: u32,
37}
38
39/// The offset of an offset date-time.
40///
41/// See [`Datetime`].
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum Offset {
44    /// UTC, written as `Z`.
45    Z,
46    /// An offset from UTC in minutes (`-07:00` is `-420`).
47    Custom {
48        /// The offset in minutes.
49        minutes: i16,
50    },
51}
52
53/// A date, a time of the day, or both with an optional offset.
54///
55/// This is a well-known extension type (see [`ext`](crate::ext)) for the
56/// date and time values of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339):
57///
58/// | Kind                                      | `date`  | `time`  | `offset` |
59/// |-------------------------------------------|---------|---------|----------|
60/// | offset date-time (`1979-05-27T07:32:00Z`) | `Some`  | `Some`  | `Some`   |
61/// | local date-time (`1979-05-27T07:32:00`)   | `Some`  | `Some`  | `None`   |
62/// | local date (`1979-05-27`)                 | `Some`  | `None`  | `None`   |
63/// | local time (`07:32:00`)                   | `None`  | `Some`  | `None`   |
64///
65/// The fallback is the RFC 3339 representation as string.
66///
67/// Parsing is lenient: the separator between date and time can be `T`, `t`
68/// or a space, `Z` can be lowercase and seconds can be omitted.  Formatting
69/// always produces RFC 3339.  Digits of the fraction beyond nanoseconds are
70/// truncated.
71///
72/// ```
73/// use deser::ext::Datetime;
74///
75/// let dt: Datetime = "1979-05-27 07:32:00.5-07:00".parse().unwrap();
76/// assert_eq!(dt.to_string(), "1979-05-27T07:32:00.5-07:00");
77/// ```
78///
79/// With the `jiff`, `chrono` and `time` features the date and time types of
80/// these crates serialize as [`Datetime`] (or [`Timestamp`]).
81#[derive(Clone, Copy, PartialEq, Eq, Hash)]
82pub struct Datetime {
83    /// The date.  `None` for local times.
84    pub date: Option<Date>,
85    /// The time.  `None` for local dates.
86    pub time: Option<Time>,
87    /// The offset.  Only set for offset date-times.
88    pub offset: Option<Offset>,
89}
90
91/// An instant in time.
92///
93/// This is a well-known extension type (see [`ext`](crate::ext)) for
94/// points in time independent of a time zone, such as Unix timestamps.  It
95/// holds the time since the Unix epoch (`1970-01-01T00:00:00Z`), ignoring
96/// leap seconds.  The nanoseconds are always positive: `-0.5` seconds are
97/// `seconds: -1` and `nanosecond: 500_000_000`.
98///
99/// The fallback is the RFC 3339 representation in UTC as string.  Instants
100/// outside of the years 0 to 9999 are written with an expanded year as in
101/// ISO 8601 (`+10000-01-01T00:00:00Z`).
102///
103/// ```
104/// use deser::ext::Timestamp;
105///
106/// let ts = Timestamp { seconds: 296638320, nanosecond: 0 };
107/// assert_eq!(ts.to_string(), "1979-05-27T07:32:00Z");
108/// assert_eq!("1979-05-27T00:32:00-07:00".parse::<Timestamp>().unwrap(), ts);
109/// ```
110///
111/// [`std::time::SystemTime`] serializes as [`Timestamp`], as do the
112/// timestamp types of `jiff`, `chrono` and `time` with the respective
113/// features.
114///
115#[cfg_attr(
116    not(feature = "std"),
117    doc = "[`std::time::SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html"
118)]
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub struct Timestamp {
121    /// The seconds since the Unix epoch.
122    pub seconds: i64,
123    /// The fraction of the second in nanoseconds (`0` to `999_999_999`).
124    pub nanosecond: u32,
125}
126
127impl Date {
128    /// Returns `true` if this is a valid calendar date.
129    pub fn is_valid(&self) -> bool {
130        self.year <= 9999
131            && (1..=12).contains(&self.month)
132            && self.day >= 1
133            && self.day <= days_in_month(i64::from(self.year), self.month)
134    }
135}
136
137impl Time {
138    /// Returns `true` if this is a valid time.
139    pub fn is_valid(&self) -> bool {
140        self.hour <= 23 && self.minute <= 59 && self.second <= 60 && self.nanosecond < 1_000_000_000
141    }
142}
143
144impl Offset {
145    /// Returns the offset in minutes.
146    pub fn minutes(&self) -> i16 {
147        match *self {
148            Offset::Z => 0,
149            Offset::Custom { minutes } => minutes,
150        }
151    }
152
153    /// Returns `true` if the offset is less than 24 hours.
154    pub fn is_valid(&self) -> bool {
155        self.minutes().unsigned_abs() < 24 * 60
156    }
157}
158
159impl Datetime {
160    /// Returns `true` if the value is a valid date-time.
161    ///
162    /// The fields are public, so a [`Datetime`] can be constructed that
163    /// is invalid (for instance an offset without date and time or February
164    /// 30th).
165    pub fn is_valid(&self) -> bool {
166        match (self.date, self.time, self.offset) {
167            (None, None, _) => false,
168            (date, time, Some(offset)) => {
169                date.is_some_and(|x| x.is_valid())
170                    && time.is_some_and(|x| x.is_valid())
171                    && offset.is_valid()
172            }
173            (date, time, None) => {
174                date.is_none_or(|x| x.is_valid()) && time.is_none_or(|x| x.is_valid())
175            }
176        }
177    }
178
179    /// Returns a human readable name for the kind of date-time.
180    ///
181    /// This is one of `"offset date-time"`, `"local date-time"`,
182    /// `"local date"` and `"local time"`.
183    pub fn kind(&self) -> &'static str {
184        match (self.date, self.time, self.offset) {
185            (Some(_), Some(_), Some(_)) => "offset date-time",
186            (Some(_), Some(_), None) => "local date-time",
187            (Some(_), None, _) => "local date",
188            (None, Some(_), _) => "local time",
189            (None, None, _) => "invalid date-time",
190        }
191    }
192
193    /// Creates an error for a date-time of the wrong kind.
194    pub(crate) fn unexpected_kind(&self, expected: &str) -> Error {
195        Error::new(
196            ErrorKind::InvalidType,
197            format!("unexpected {}, expected {}", self.kind(), expected),
198        )
199    }
200
201    /// Returns the date if this is a local date.
202    #[cfg(any(feature = "chrono", feature = "time", feature = "jiff"))]
203    pub(crate) fn expect_local_date(&self) -> Result<Date, Error> {
204        match (self.date, self.time) {
205            (Some(date), None) => Ok(date),
206            _ => Err(self.unexpected_kind("local date")),
207        }
208    }
209
210    /// Returns the time if this is a local time.
211    #[cfg(any(feature = "chrono", feature = "time", feature = "jiff"))]
212    pub(crate) fn expect_local_time(&self) -> Result<Time, Error> {
213        match (self.date, self.time) {
214            (None, Some(time)) => Ok(time),
215            _ => Err(self.unexpected_kind("local time")),
216        }
217    }
218
219    /// Returns date and time if this is a local date-time.
220    #[cfg(any(feature = "chrono", feature = "time", feature = "jiff"))]
221    pub(crate) fn expect_local_datetime(&self) -> Result<(Date, Time), Error> {
222        match (self.date, self.time, self.offset) {
223            (Some(date), Some(time), None) => Ok((date, time)),
224            _ => Err(self.unexpected_kind("local date-time")),
225        }
226    }
227
228    /// Returns date, time and offset if this is an offset date-time.
229    pub(crate) fn expect_offset_datetime(&self) -> Result<(Date, Time, Offset), Error> {
230        match (self.date, self.time, self.offset) {
231            (Some(date), Some(time), Some(offset)) => Ok((date, time, offset)),
232            _ => Err(self.unexpected_kind("offset date-time")),
233        }
234    }
235}
236
237impl Timestamp {
238    /// Creates a timestamp from seconds and nanoseconds that can be out of
239    /// range or negative.
240    #[cfg(feature = "std")]
241    pub(crate) fn normalized(seconds: i64, nanosecond: i64) -> Option<Timestamp> {
242        let seconds = seconds.checked_add(nanosecond.div_euclid(1_000_000_000))?;
243        Some(Timestamp {
244            seconds,
245            nanosecond: nanosecond.rem_euclid(1_000_000_000) as u32,
246        })
247    }
248
249    /// Returns the time since the epoch in seconds as float.
250    pub fn as_secs_f64(&self) -> f64 {
251        self.seconds as f64 + f64::from(self.nanosecond) / 1e9
252    }
253
254    /// Converts the timestamp into an offset date-time in UTC.
255    ///
256    /// Returns `None` if the year is outside of `0` to `9999`.
257    pub fn to_datetime(&self) -> Option<Datetime> {
258        let (year, date, time) = self.to_civil();
259        Some(Datetime {
260            date: Some(Date {
261                year: u16::try_from(year).ok().filter(|&x| x <= 9999)?,
262                month: date.0,
263                day: date.1,
264            }),
265            time: Some(time),
266            offset: Some(Offset::Z),
267        })
268    }
269
270    /// Converts the timestamp into the year, month and day and time in UTC.
271    fn to_civil(self) -> (i64, (u8, u8), Time) {
272        let days = self.seconds.div_euclid(86400);
273        let secs = self.seconds.rem_euclid(86400);
274        let (year, month, day) = civil_from_days(days);
275        (
276            year,
277            (month, day),
278            Time {
279                hour: (secs / 3600) as u8,
280                minute: (secs / 60 % 60) as u8,
281                second: (secs % 60) as u8,
282                nanosecond: self.nanosecond,
283            },
284        )
285    }
286
287    /// Creates a timestamp from a civil date-time with an offset.
288    fn from_civil(year: i64, date: Date, time: Time, offset: Offset) -> Option<Timestamp> {
289        let days = days_from_civil(year, date.month, date.day);
290        // Use a wider intermediate: midnight can be below i64::MIN even
291        // when the time and offset bring the timestamp back into range.
292        let seconds = i64::try_from(
293            days * 86400
294                + i128::from(time.hour) * 3600
295                + i128::from(time.minute) * 60
296                // leap seconds are clamped
297                + i128::from(time.second.min(59))
298                - i128::from(offset.minutes()) * 60,
299        )
300        .ok()?;
301        Some(Timestamp {
302            seconds,
303            nanosecond: time.nanosecond,
304        })
305    }
306}
307
308impl TryFrom<Datetime> for Timestamp {
309    type Error = Error;
310
311    /// Converts an offset date-time into a timestamp.
312    ///
313    /// Leap seconds are clamped to the previous second.
314    fn try_from(value: Datetime) -> Result<Timestamp, Error> {
315        let (date, time, offset) = value.expect_offset_datetime()?;
316        if !value.is_valid() {
317            return Err(invalid("invalid date-time"));
318        }
319        Timestamp::from_civil(i64::from(date.year), date, time, offset)
320            .ok_or_else(|| out_of_range("date-time out of range"))
321    }
322}
323
324impl TryFrom<Timestamp> for Datetime {
325    type Error = Error;
326
327    /// Converts a timestamp into an offset date-time in UTC.
328    fn try_from(value: Timestamp) -> Result<Datetime, Error> {
329        value
330            .to_datetime()
331            .ok_or_else(|| out_of_range("timestamp out of range for date-time"))
332    }
333}
334
335impl From<Date> for Datetime {
336    fn from(date: Date) -> Datetime {
337        Datetime {
338            date: Some(date),
339            time: None,
340            offset: None,
341        }
342    }
343}
344
345impl From<Time> for Datetime {
346    fn from(time: Time) -> Datetime {
347        Datetime {
348            date: None,
349            time: Some(time),
350            offset: None,
351        }
352    }
353}
354
355pub(crate) fn is_leap_year(year: i64) -> bool {
356    year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
357}
358
359pub(crate) fn days_in_month(year: i64, month: u8) -> u8 {
360    match month {
361        2 if is_leap_year(year) => 29,
362        2 => 28,
363        4 | 6 | 9 | 11 => 30,
364        _ => 31,
365    }
366}
367
368/// Returns the days since the Unix epoch for a proleptic Gregorian date.
369///
370/// See <http://howardhinnant.github.io/date_algorithms.html>.
371pub(crate) fn days_from_civil(year: i64, month: u8, day: u8) -> i128 {
372    // Expanded years can span i64, so their day counts need more bits.
373    let year = i128::from(year);
374    let (month, day) = (i128::from(month), i128::from(day));
375    let year = if month <= 2 { year - 1 } else { year };
376    let era = year.div_euclid(400);
377    let yoe = year - era * 400;
378    let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
379    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
380    era * 146097 + doe - 719468
381}
382
383/// Returns the proleptic Gregorian date for days since the Unix epoch.
384pub(crate) fn civil_from_days(days: i64) -> (i64, u8, u8) {
385    let z = days + 719468;
386    let era = z.div_euclid(146097);
387    let doe = z - era * 146097;
388    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
389    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
390    let mp = (5 * doy + 2) / 153;
391    let day = (doy - (153 * mp + 2) / 5 + 1) as u8;
392    let month = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
393    let year = yoe + era * 400 + i64::from(month <= 2);
394    (year, month, day)
395}
396
397impl fmt::Display for Date {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
400    }
401}
402
403impl fmt::Display for Time {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
406        write_fraction(f, self.nanosecond)
407    }
408}
409
410/// Writes the fraction of a second without trailing zeros.
411pub(crate) fn write_fraction(f: &mut fmt::Formatter<'_>, nanosecond: u32) -> fmt::Result {
412    if nanosecond != 0 {
413        let mut digits = 9;
414        let mut value = nanosecond;
415        while value.is_multiple_of(10) {
416            value /= 10;
417            digits -= 1;
418        }
419        write!(f, ".{:0width$}", value, width = digits)?;
420    }
421    Ok(())
422}
423
424impl fmt::Display for Offset {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        match *self {
427            Offset::Z => f.write_str("Z"),
428            Offset::Custom { minutes } => {
429                let sign = if minutes < 0 { '-' } else { '+' };
430                let minutes = minutes.unsigned_abs();
431                write!(f, "{}{:02}:{:02}", sign, minutes / 60, minutes % 60)
432            }
433        }
434    }
435}
436
437impl fmt::Display for Datetime {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        if let Some(ref date) = self.date {
440            write!(f, "{}", date)?;
441            if self.time.is_some() {
442                f.write_str("T")?;
443            }
444        }
445        if let Some(ref time) = self.time {
446            write!(f, "{}", time)?;
447        }
448        if let Some(ref offset) = self.offset {
449            write!(f, "{}", offset)?;
450        }
451        Ok(())
452    }
453}
454
455impl fmt::Debug for Datetime {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        write!(f, "Datetime({})", self)
458    }
459}
460
461impl fmt::Display for Timestamp {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        let (year, (month, day), time) = self.to_civil();
464        if (0..=9999).contains(&year) {
465            write!(f, "{:04}", year)?;
466        } else {
467            // expanded year representation of ISO 8601
468            write!(
469                f,
470                "{}{:06}",
471                if year < 0 { '-' } else { '+' },
472                year.unsigned_abs()
473            )?;
474        }
475        write!(f, "-{:02}-{:02}T{}Z", month, day, time)
476    }
477}
478
479impl FromStr for Datetime {
480    type Err = Error;
481
482    fn from_str(s: &str) -> Result<Datetime, Error> {
483        let (year, dt) = parse_datetime(s, false)?;
484        debug_assert!(year.is_none());
485        Ok(dt)
486    }
487}
488
489impl FromStr for Timestamp {
490    type Err = Error;
491
492    /// Parses an offset date-time.
493    ///
494    /// Years outside of 0 to 9999 can be given in the expanded
495    /// representation of ISO 8601 (a sign and at least six digits).
496    fn from_str(s: &str) -> Result<Timestamp, Error> {
497        match parse_datetime(s, true)? {
498            (Some(year), dt) => {
499                let (date, time, offset) = dt.expect_offset_datetime()?;
500                Timestamp::from_civil(year, date, time, offset)
501                    .ok_or_else(|| out_of_range("timestamp out of range"))
502            }
503            (None, dt) => Timestamp::try_from(dt),
504        }
505    }
506}
507
508/// Parses a date-time.
509///
510/// If `expanded_years` is set, years with a sign and at least six digits
511/// are accepted.  These are returned separately, the date of the returned
512/// value then has the year `2000`.
513fn parse_datetime(s: &str, expanded_years: bool) -> Result<(Option<i64>, Datetime), Error> {
514    let bytes = s.as_bytes();
515    let mut pos = 0;
516    let mut rv = Datetime {
517        date: None,
518        time: None,
519        offset: None,
520    };
521    let mut expanded_year = None;
522
523    let has_date = if expanded_years && matches!(bytes.first(), Some(b'+' | b'-')) {
524        let digits = bytes[1..].iter().take_while(|x| x.is_ascii_digit()).count();
525        if digits < 6 {
526            return Err(invalid("invalid expanded year"));
527        }
528        let year: i64 = s[1..1 + digits]
529            .parse()
530            .map_err(|_| out_of_range("year out of range"))?;
531        expanded_year = Some(if bytes[0] == b'-' { -year } else { year });
532        pos = 1 + digits;
533        true
534    } else {
535        bytes.get(4) == Some(&b'-')
536    };
537
538    if has_date {
539        let year = match expanded_year {
540            Some(_) => 2000,
541            None => digits(bytes, &mut pos, 4)? as u16,
542        };
543        expect(bytes, &mut pos, b'-')?;
544        let month = digits(bytes, &mut pos, 2)? as u8;
545        expect(bytes, &mut pos, b'-')?;
546        let day = digits(bytes, &mut pos, 2)? as u8;
547        let date = Date { year, month, day };
548        let valid_day = match expanded_year {
549            Some(year) => {
550                (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
551            }
552            None => date.is_valid(),
553        };
554        if !valid_day {
555            return Err(invalid("invalid date"));
556        }
557        rv.date = Some(date);
558        match bytes.get(pos) {
559            Some(b'T' | b't' | b' ') => pos += 1,
560            None => return Ok((expanded_year, rv)),
561            _ => return Err(invalid("invalid date-time")),
562        }
563    }
564
565    let mut time = Time {
566        hour: digits(bytes, &mut pos, 2)? as u8,
567        minute: {
568            expect(bytes, &mut pos, b':')?;
569            digits(bytes, &mut pos, 2)? as u8
570        },
571        second: 0,
572        nanosecond: 0,
573    };
574    if bytes.get(pos) == Some(&b':') {
575        pos += 1;
576        time.second = digits(bytes, &mut pos, 2)? as u8;
577        if bytes.get(pos) == Some(&b'.') {
578            pos += 1;
579            let start = pos;
580            let mut nanos = 0u32;
581            while let Some(&c) = bytes.get(pos).filter(|x| x.is_ascii_digit()) {
582                // digits beyond nanoseconds are truncated
583                if pos - start < 9 {
584                    nanos = nanos * 10 + u32::from(c - b'0');
585                }
586                pos += 1;
587            }
588            if pos == start {
589                return Err(invalid("expected digits after decimal point"));
590            }
591            for _ in (pos - start)..9 {
592                nanos *= 10;
593            }
594            time.nanosecond = nanos;
595        }
596    }
597    if !time.is_valid() {
598        return Err(invalid("invalid time"));
599    }
600    rv.time = Some(time);
601
602    if rv.date.is_some() {
603        match bytes.get(pos) {
604            Some(b'Z' | b'z') => {
605                pos += 1;
606                rv.offset = Some(Offset::Z);
607            }
608            Some(&sign @ (b'+' | b'-')) => {
609                pos += 1;
610                let hours = digits(bytes, &mut pos, 2)?;
611                expect(bytes, &mut pos, b':')?;
612                let minutes = digits(bytes, &mut pos, 2)?;
613                if hours > 23 || minutes > 59 {
614                    return Err(invalid("invalid offset"));
615                }
616                let minutes = (hours * 60 + minutes) as i16;
617                rv.offset = Some(Offset::Custom {
618                    minutes: if sign == b'-' { -minutes } else { minutes },
619                });
620            }
621            _ => {}
622        }
623    }
624
625    if pos != bytes.len() {
626        return Err(invalid("unexpected characters after date-time"));
627    }
628    Ok((expanded_year, rv))
629}
630
631fn digits(bytes: &[u8], pos: &mut usize, n: usize) -> Result<u32, Error> {
632    let mut rv = 0;
633    for _ in 0..n {
634        match bytes.get(*pos) {
635            Some(&c) if c.is_ascii_digit() => rv = rv * 10 + u32::from(c - b'0'),
636            _ => return Err(invalid("invalid date-time")),
637        }
638        *pos += 1;
639    }
640    Ok(rv)
641}
642
643fn expect(bytes: &[u8], pos: &mut usize, c: u8) -> Result<(), Error> {
644    if bytes.get(*pos) == Some(&c) {
645        *pos += 1;
646        Ok(())
647    } else {
648        Err(invalid("invalid date-time"))
649    }
650}
651
652impl Extension for Datetime {
653    fn name(&self) -> &str {
654        "datetime"
655    }
656
657    fn fallback(&self) -> Atom<'_> {
658        Atom::Str(self.to_string().into())
659    }
660}
661
662impl WellKnown for Datetime {
663    const EXPECTING: &'static str = "datetime";
664
665    /// Accepts date-times, timestamps and strings.
666    fn from_atom(atom: &Atom) -> Result<Option<Datetime>, Error> {
667        Ok(Some(match *atom {
668            Atom::Ext(ref ext) => {
669                if let Some(value) = ext.downcast_ref::<Datetime>() {
670                    *value
671                } else if let Some(value) = ext.downcast_ref::<Timestamp>() {
672                    Datetime::try_from(*value)?
673                } else {
674                    return Ok(None);
675                }
676            }
677            Atom::Str(ref value) => value.parse()?,
678            _ => return Ok(None),
679        }))
680    }
681}
682
683impl_well_known!(Datetime);
684
685impl Extension for Timestamp {
686    fn name(&self) -> &str {
687        "timestamp"
688    }
689
690    fn fallback(&self) -> Atom<'_> {
691        Atom::Str(self.to_string().into())
692    }
693}
694
695impl WellKnown for Timestamp {
696    const EXPECTING: &'static str = "timestamp";
697
698    /// Accepts timestamps, offset date-times, strings and numbers (seconds
699    /// since the epoch).
700    fn from_atom(atom: &Atom) -> Result<Option<Timestamp>, Error> {
701        Ok(Some(match *atom {
702            Atom::Ext(ref ext) => {
703                if let Some(value) = ext.downcast_ref::<Timestamp>() {
704                    *value
705                } else if let Some(value) = ext.downcast_ref::<Datetime>() {
706                    Timestamp::try_from(*value)?
707                } else {
708                    return Ok(None);
709                }
710            }
711            Atom::Str(ref value) => value.parse()?,
712            Atom::U64(value) => Timestamp {
713                seconds: i64::try_from(value)
714                    .map_err(|_| out_of_range("timestamp out of range"))?,
715                nanosecond: 0,
716            },
717            Atom::I64(value) => Timestamp {
718                seconds: value,
719                nanosecond: 0,
720            },
721            Atom::F64(value) => {
722                if !value.is_finite() || value.abs() >= 9.2e18 {
723                    return Err(out_of_range("timestamp out of range"));
724                }
725                let seconds = floor(value);
726                Timestamp {
727                    seconds: seconds as i64,
728                    nanosecond: (round((value - seconds) * 1e9) as u32).min(999_999_999),
729                }
730            }
731            _ => return Ok(None),
732        }))
733    }
734}
735
736impl_well_known!(Timestamp);
737
738#[test]
739fn test_civil_roundtrip() {
740    for days in [
741        -1_000_000i64,
742        -719468,
743        -1,
744        0,
745        1,
746        11016,
747        2_932_896,
748        10_000_000,
749    ] {
750        let (y, m, d) = civil_from_days(days);
751        assert_eq!(days_from_civil(y, m, d), i128::from(days));
752    }
753    assert_eq!(civil_from_days(0), (1970, 1, 1));
754    assert_eq!(days_from_civil(2000, 3, 1), 11017);
755}
756
757#[test]
758fn test_timestamp_format() {
759    let ts = Timestamp {
760        seconds: -1,
761        nanosecond: 500_000_000,
762    };
763    assert_eq!(ts.to_string(), "1969-12-31T23:59:59.5Z");
764    assert_eq!(ts.to_string().parse::<Timestamp>().unwrap(), ts);
765    let far = Timestamp {
766        seconds: 253402300800,
767        nanosecond: 0,
768    };
769    assert_eq!(far.to_string(), "+010000-01-01T00:00:00Z");
770    assert_eq!(far.to_string().parse::<Timestamp>().unwrap(), far);
771    let before = Timestamp {
772        seconds: -62167219201,
773        nanosecond: 0,
774    };
775    assert_eq!(before.to_string(), "-000001-12-31T23:59:59Z");
776    assert_eq!(before.to_string().parse::<Timestamp>().unwrap(), before);
777    assert!("1979-05-27T07:32:00".parse::<Timestamp>().is_err());
778    assert!("+10000-01-01T00:00:00Z".parse::<Timestamp>().is_err());
779}