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        let seconds = days.checked_mul(86400)?.checked_add(
291            i64::from(time.hour) * 3600
292                    + i64::from(time.minute) * 60
293                    // leap seconds are clamped
294                    + i64::from(time.second.min(59))
295                - i64::from(offset.minutes()) * 60,
296        )?;
297        Some(Timestamp {
298            seconds,
299            nanosecond: time.nanosecond,
300        })
301    }
302}
303
304impl TryFrom<Datetime> for Timestamp {
305    type Error = Error;
306
307    /// Converts an offset date-time into a timestamp.
308    ///
309    /// Leap seconds are clamped to the previous second.
310    fn try_from(value: Datetime) -> Result<Timestamp, Error> {
311        let (date, time, offset) = value.expect_offset_datetime()?;
312        if !value.is_valid() {
313            return Err(invalid("invalid date-time"));
314        }
315        Timestamp::from_civil(i64::from(date.year), date, time, offset)
316            .ok_or_else(|| out_of_range("date-time out of range"))
317    }
318}
319
320impl TryFrom<Timestamp> for Datetime {
321    type Error = Error;
322
323    /// Converts a timestamp into an offset date-time in UTC.
324    fn try_from(value: Timestamp) -> Result<Datetime, Error> {
325        value
326            .to_datetime()
327            .ok_or_else(|| out_of_range("timestamp out of range for date-time"))
328    }
329}
330
331impl From<Date> for Datetime {
332    fn from(date: Date) -> Datetime {
333        Datetime {
334            date: Some(date),
335            time: None,
336            offset: None,
337        }
338    }
339}
340
341impl From<Time> for Datetime {
342    fn from(time: Time) -> Datetime {
343        Datetime {
344            date: None,
345            time: Some(time),
346            offset: None,
347        }
348    }
349}
350
351pub(crate) fn is_leap_year(year: i64) -> bool {
352    year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
353}
354
355pub(crate) fn days_in_month(year: i64, month: u8) -> u8 {
356    match month {
357        2 if is_leap_year(year) => 29,
358        2 => 28,
359        4 | 6 | 9 | 11 => 30,
360        _ => 31,
361    }
362}
363
364/// Returns the days since the Unix epoch for a proleptic Gregorian date.
365///
366/// See <http://howardhinnant.github.io/date_algorithms.html>.
367pub(crate) fn days_from_civil(year: i64, month: u8, day: u8) -> i64 {
368    let (month, day) = (i64::from(month), i64::from(day));
369    let year = if month <= 2 { year - 1 } else { year };
370    let era = year.div_euclid(400);
371    let yoe = year - era * 400;
372    let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
373    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
374    era * 146097 + doe - 719468
375}
376
377/// Returns the proleptic Gregorian date for days since the Unix epoch.
378pub(crate) fn civil_from_days(days: i64) -> (i64, u8, u8) {
379    let z = days + 719468;
380    let era = z.div_euclid(146097);
381    let doe = z - era * 146097;
382    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
383    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
384    let mp = (5 * doy + 2) / 153;
385    let day = (doy - (153 * mp + 2) / 5 + 1) as u8;
386    let month = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
387    let year = yoe + era * 400 + i64::from(month <= 2);
388    (year, month, day)
389}
390
391impl fmt::Display for Date {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
394    }
395}
396
397impl fmt::Display for Time {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
400        write_fraction(f, self.nanosecond)
401    }
402}
403
404/// Writes the fraction of a second without trailing zeros.
405pub(crate) fn write_fraction(f: &mut fmt::Formatter<'_>, nanosecond: u32) -> fmt::Result {
406    if nanosecond != 0 {
407        let mut digits = 9;
408        let mut value = nanosecond;
409        while value.is_multiple_of(10) {
410            value /= 10;
411            digits -= 1;
412        }
413        write!(f, ".{:0width$}", value, width = digits)?;
414    }
415    Ok(())
416}
417
418impl fmt::Display for Offset {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        match *self {
421            Offset::Z => f.write_str("Z"),
422            Offset::Custom { minutes } => {
423                let sign = if minutes < 0 { '-' } else { '+' };
424                let minutes = minutes.unsigned_abs();
425                write!(f, "{}{:02}:{:02}", sign, minutes / 60, minutes % 60)
426            }
427        }
428    }
429}
430
431impl fmt::Display for Datetime {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        if let Some(ref date) = self.date {
434            write!(f, "{}", date)?;
435            if self.time.is_some() {
436                f.write_str("T")?;
437            }
438        }
439        if let Some(ref time) = self.time {
440            write!(f, "{}", time)?;
441        }
442        if let Some(ref offset) = self.offset {
443            write!(f, "{}", offset)?;
444        }
445        Ok(())
446    }
447}
448
449impl fmt::Debug for Datetime {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        write!(f, "Datetime({})", self)
452    }
453}
454
455impl fmt::Display for Timestamp {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        let (year, (month, day), time) = self.to_civil();
458        if (0..=9999).contains(&year) {
459            write!(f, "{:04}", year)?;
460        } else {
461            // expanded year representation of ISO 8601
462            write!(
463                f,
464                "{}{:06}",
465                if year < 0 { '-' } else { '+' },
466                year.unsigned_abs()
467            )?;
468        }
469        write!(f, "-{:02}-{:02}T{}Z", month, day, time)
470    }
471}
472
473impl FromStr for Datetime {
474    type Err = Error;
475
476    fn from_str(s: &str) -> Result<Datetime, Error> {
477        let (year, dt) = parse_datetime(s, false)?;
478        debug_assert!(year.is_none());
479        Ok(dt)
480    }
481}
482
483impl FromStr for Timestamp {
484    type Err = Error;
485
486    /// Parses an offset date-time.
487    ///
488    /// Years outside of 0 to 9999 can be given in the expanded
489    /// representation of ISO 8601 (a sign and at least six digits).
490    fn from_str(s: &str) -> Result<Timestamp, Error> {
491        match parse_datetime(s, true)? {
492            (Some(year), dt) => {
493                let (date, time, offset) = dt.expect_offset_datetime()?;
494                Timestamp::from_civil(year, date, time, offset)
495                    .ok_or_else(|| out_of_range("timestamp out of range"))
496            }
497            (None, dt) => Timestamp::try_from(dt),
498        }
499    }
500}
501
502/// Parses a date-time.
503///
504/// If `expanded_years` is set, years with a sign and at least six digits
505/// are accepted.  These are returned separately, the date of the returned
506/// value then has the year `2000`.
507fn parse_datetime(s: &str, expanded_years: bool) -> Result<(Option<i64>, Datetime), Error> {
508    let bytes = s.as_bytes();
509    let mut pos = 0;
510    let mut rv = Datetime {
511        date: None,
512        time: None,
513        offset: None,
514    };
515    let mut expanded_year = None;
516
517    let has_date = if expanded_years && matches!(bytes.first(), Some(b'+' | b'-')) {
518        let digits = bytes[1..].iter().take_while(|x| x.is_ascii_digit()).count();
519        if digits < 6 {
520            return Err(invalid("invalid expanded year"));
521        }
522        let year: i64 = s[1..1 + digits]
523            .parse()
524            .map_err(|_| out_of_range("year out of range"))?;
525        expanded_year = Some(if bytes[0] == b'-' { -year } else { year });
526        pos = 1 + digits;
527        true
528    } else {
529        bytes.get(4) == Some(&b'-')
530    };
531
532    if has_date {
533        let year = match expanded_year {
534            Some(_) => 2000,
535            None => digits(bytes, &mut pos, 4)? as u16,
536        };
537        expect(bytes, &mut pos, b'-')?;
538        let month = digits(bytes, &mut pos, 2)? as u8;
539        expect(bytes, &mut pos, b'-')?;
540        let day = digits(bytes, &mut pos, 2)? as u8;
541        let date = Date { year, month, day };
542        let valid_day = match expanded_year {
543            Some(year) => {
544                (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
545            }
546            None => date.is_valid(),
547        };
548        if !valid_day {
549            return Err(invalid("invalid date"));
550        }
551        rv.date = Some(date);
552        match bytes.get(pos) {
553            Some(b'T' | b't' | b' ') => pos += 1,
554            None => return Ok((expanded_year, rv)),
555            _ => return Err(invalid("invalid date-time")),
556        }
557    }
558
559    let mut time = Time {
560        hour: digits(bytes, &mut pos, 2)? as u8,
561        minute: {
562            expect(bytes, &mut pos, b':')?;
563            digits(bytes, &mut pos, 2)? as u8
564        },
565        second: 0,
566        nanosecond: 0,
567    };
568    if bytes.get(pos) == Some(&b':') {
569        pos += 1;
570        time.second = digits(bytes, &mut pos, 2)? as u8;
571        if bytes.get(pos) == Some(&b'.') {
572            pos += 1;
573            let start = pos;
574            let mut nanos = 0u32;
575            while let Some(&c) = bytes.get(pos).filter(|x| x.is_ascii_digit()) {
576                // digits beyond nanoseconds are truncated
577                if pos - start < 9 {
578                    nanos = nanos * 10 + u32::from(c - b'0');
579                }
580                pos += 1;
581            }
582            if pos == start {
583                return Err(invalid("expected digits after decimal point"));
584            }
585            for _ in (pos - start)..9 {
586                nanos *= 10;
587            }
588            time.nanosecond = nanos;
589        }
590    }
591    if !time.is_valid() {
592        return Err(invalid("invalid time"));
593    }
594    rv.time = Some(time);
595
596    if rv.date.is_some() {
597        match bytes.get(pos) {
598            Some(b'Z' | b'z') => {
599                pos += 1;
600                rv.offset = Some(Offset::Z);
601            }
602            Some(&sign @ (b'+' | b'-')) => {
603                pos += 1;
604                let hours = digits(bytes, &mut pos, 2)?;
605                expect(bytes, &mut pos, b':')?;
606                let minutes = digits(bytes, &mut pos, 2)?;
607                if hours > 23 || minutes > 59 {
608                    return Err(invalid("invalid offset"));
609                }
610                let minutes = (hours * 60 + minutes) as i16;
611                rv.offset = Some(Offset::Custom {
612                    minutes: if sign == b'-' { -minutes } else { minutes },
613                });
614            }
615            _ => {}
616        }
617    }
618
619    if pos != bytes.len() {
620        return Err(invalid("unexpected characters after date-time"));
621    }
622    Ok((expanded_year, rv))
623}
624
625fn digits(bytes: &[u8], pos: &mut usize, n: usize) -> Result<u32, Error> {
626    let mut rv = 0;
627    for _ in 0..n {
628        match bytes.get(*pos) {
629            Some(&c) if c.is_ascii_digit() => rv = rv * 10 + u32::from(c - b'0'),
630            _ => return Err(invalid("invalid date-time")),
631        }
632        *pos += 1;
633    }
634    Ok(rv)
635}
636
637fn expect(bytes: &[u8], pos: &mut usize, c: u8) -> Result<(), Error> {
638    if bytes.get(*pos) == Some(&c) {
639        *pos += 1;
640        Ok(())
641    } else {
642        Err(invalid("invalid date-time"))
643    }
644}
645
646impl Extension for Datetime {
647    fn name(&self) -> &str {
648        "datetime"
649    }
650
651    fn fallback(&self) -> Atom<'_> {
652        Atom::Str(self.to_string().into())
653    }
654}
655
656impl WellKnown for Datetime {
657    const EXPECTING: &'static str = "datetime";
658
659    /// Accepts date-times, timestamps and strings.
660    fn from_atom(atom: &Atom) -> Result<Option<Datetime>, Error> {
661        Ok(Some(match *atom {
662            Atom::Ext(ref ext) => {
663                if let Some(value) = ext.downcast_ref::<Datetime>() {
664                    *value
665                } else if let Some(value) = ext.downcast_ref::<Timestamp>() {
666                    Datetime::try_from(*value)?
667                } else {
668                    return Ok(None);
669                }
670            }
671            Atom::Str(ref value) => value.parse()?,
672            _ => return Ok(None),
673        }))
674    }
675}
676
677impl_well_known!(Datetime);
678
679impl Extension for Timestamp {
680    fn name(&self) -> &str {
681        "timestamp"
682    }
683
684    fn fallback(&self) -> Atom<'_> {
685        Atom::Str(self.to_string().into())
686    }
687}
688
689impl WellKnown for Timestamp {
690    const EXPECTING: &'static str = "timestamp";
691
692    /// Accepts timestamps, offset date-times, strings and numbers (seconds
693    /// since the epoch).
694    fn from_atom(atom: &Atom) -> Result<Option<Timestamp>, Error> {
695        Ok(Some(match *atom {
696            Atom::Ext(ref ext) => {
697                if let Some(value) = ext.downcast_ref::<Timestamp>() {
698                    *value
699                } else if let Some(value) = ext.downcast_ref::<Datetime>() {
700                    Timestamp::try_from(*value)?
701                } else {
702                    return Ok(None);
703                }
704            }
705            Atom::Str(ref value) => value.parse()?,
706            Atom::U64(value) => Timestamp {
707                seconds: i64::try_from(value)
708                    .map_err(|_| out_of_range("timestamp out of range"))?,
709                nanosecond: 0,
710            },
711            Atom::I64(value) => Timestamp {
712                seconds: value,
713                nanosecond: 0,
714            },
715            Atom::F64(value) => {
716                if !value.is_finite() || value.abs() >= 9.2e18 {
717                    return Err(out_of_range("timestamp out of range"));
718                }
719                let seconds = floor(value);
720                Timestamp {
721                    seconds: seconds as i64,
722                    nanosecond: (round((value - seconds) * 1e9) as u32).min(999_999_999),
723                }
724            }
725            _ => return Ok(None),
726        }))
727    }
728}
729
730impl_well_known!(Timestamp);
731
732#[test]
733fn test_civil_roundtrip() {
734    for days in [
735        -1_000_000i64,
736        -719468,
737        -1,
738        0,
739        1,
740        11016,
741        2_932_896,
742        10_000_000,
743    ] {
744        let (y, m, d) = civil_from_days(days);
745        assert_eq!(days_from_civil(y, m, d), days);
746    }
747    assert_eq!(civil_from_days(0), (1970, 1, 1));
748    assert_eq!(days_from_civil(2000, 3, 1), 11017);
749}
750
751#[test]
752fn test_timestamp_format() {
753    let ts = Timestamp {
754        seconds: -1,
755        nanosecond: 500_000_000,
756    };
757    assert_eq!(ts.to_string(), "1969-12-31T23:59:59.5Z");
758    assert_eq!(ts.to_string().parse::<Timestamp>().unwrap(), ts);
759    let far = Timestamp {
760        seconds: 253402300800,
761        nanosecond: 0,
762    };
763    assert_eq!(far.to_string(), "+010000-01-01T00:00:00Z");
764    assert_eq!(far.to_string().parse::<Timestamp>().unwrap(), far);
765    let before = Timestamp {
766        seconds: -62167219201,
767        nanosecond: 0,
768    };
769    assert_eq!(before.to_string(), "-000001-12-31T23:59:59Z");
770    assert_eq!(before.to_string().parse::<Timestamp>().unwrap(), before);
771    assert!("1979-05-27T07:32:00".parse::<Timestamp>().is_err());
772    assert!("+10000-01-01T00:00:00Z".parse::<Timestamp>().is_err());
773}