Skip to main content

apple_plist/
date.rs

1//! UTC instants with nanosecond precision, plus the three plist date wire
2//! encodings (Apple-epoch seconds, lenient RFC 3339, and the OpenStep text
3//! layout), each hand-rolled.
4
5use std::time::{Duration, SystemTime};
6
7#[cfg(any(test, feature = "serde", feature = "xml", feature = "openstep"))]
8use time::{Month, Time};
9use time::{OffsetDateTime, PrimitiveDateTime};
10
11/// The Apple epoch (2001-01-01T00:00:00Z) expressed as Unix seconds.
12#[cfg(any(test, feature = "binary"))]
13const APPLE_EPOCH_UNIX_SECONDS: f64 = 978_307_200.0;
14
15#[cfg(any(
16    test,
17    feature = "serde",
18    feature = "binary",
19    feature = "xml",
20    feature = "openstep"
21))]
22const NANOS_PER_SECOND: i128 = 1_000_000_000;
23
24const MIN_INSTANT: OffsetDateTime = PrimitiveDateTime::MIN.assume_utc();
25const MAX_INSTANT: OffsetDateTime = PrimitiveDateTime::MAX.assume_utc();
26
27/// An absolute point in time: a UTC instant with nanosecond precision.
28///
29/// `Date` exposes no time zone — every constructor normalizes to UTC at its
30/// codec boundaries. The representable
31/// range spans the years -9999 through 9999; values outside it (reachable
32/// only through extreme binary-plist payloads) clamp to the nearest bound.
33///
34/// # Examples
35///
36/// ```
37/// use std::time::SystemTime;
38///
39/// use apple_plist::Date;
40///
41/// let now = SystemTime::now();
42/// let date = Date::from(now);
43/// assert_eq!(SystemTime::from(date), now);
44/// ```
45#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub struct Date(OffsetDateTime);
47
48impl Date {
49    /// Builds the instant `secs + nanos / 1e9` relative to the Unix epoch,
50    /// normalizing out-of-range nanoseconds (carrying into the seconds field)
51    /// and clamping at the representable bounds.
52    #[cfg(any(test, feature = "serde", feature = "binary"))]
53    pub(crate) fn from_unix(secs: i64, nanos: i64) -> Self {
54        Self::clamped(i128::from(secs) * NANOS_PER_SECOND + i128::from(nanos))
55    }
56
57    fn clamped(unix_nanos: i128) -> Self {
58        OffsetDateTime::from_unix_timestamp_nanos(unix_nanos).map_or_else(
59            |_| {
60                if unix_nanos < 0 {
61                    Self(MIN_INSTANT)
62                } else {
63                    Self(MAX_INSTANT)
64                }
65            },
66            Self,
67        )
68    }
69
70    /// Returns `(seconds, subsecond nanoseconds)` since the Unix epoch, with
71    /// nanoseconds in `0..1_000_000_000` (the floor convention).
72    pub(crate) const fn unix_parts(self) -> (i64, u32) {
73        (self.0.unix_timestamp(), self.0.time().nanosecond())
74    }
75
76    /// Decodes a binary-plist date payload: seconds since the Apple epoch.
77    ///
78    /// Never fails: every bit pattern produces a date. Splits the value into
79    /// integer and fractional parts (both keep the sign, fractional
80    /// nanoseconds truncate toward zero); non-finite and out-of-range values
81    /// saturate through the `as` casts and clamp at the representable bounds.
82    #[cfg(any(test, feature = "binary"))]
83    pub(crate) fn from_apple_epoch(seconds: f64) -> Self {
84        let val = seconds + APPLE_EPOCH_UNIX_SECONDS;
85        #[expect(
86            clippy::cast_possible_truncation,
87            reason = "float-to-int conversion site; saturating `as` keeps every payload succeeding (spec 02 §2.7.4)"
88        )]
89        let (secs, nanos) = (val.trunc() as i64, (val.fract() * 1e9) as i64);
90        Self::from_unix(secs, nanos)
91    }
92
93    /// Encodes this date as seconds since the Apple epoch.
94    ///
95    /// Operation order is fixed: one rounding from the combined
96    /// integer nanosecond count to `f64`, divide by 1e9, subtract the epoch.
97    #[cfg(any(test, feature = "binary"))]
98    pub(crate) fn to_apple_epoch(self) -> f64 {
99        let (secs, nanos) = self.unix_parts();
100        let total = i128::from(secs) * NANOS_PER_SECOND + i128::from(nanos);
101        #[expect(
102            clippy::cast_precision_loss,
103            reason = "single rounding of the combined nanosecond count to f64"
104        )]
105        let total_f64 = total as f64;
106        total_f64 / 1e9 - APPLE_EPOCH_UNIX_SECONDS
107    }
108
109    /// Parses an XML plist date with the lenient RFC 3339 grammar: 4-digit
110    /// year, 1-or-2-digit hour, optional `.`/`,` fraction,
111    /// and a mandatory `Z` or `±hh:mm` zone (offset hour at most 24, minute
112    /// at most 60). Returns `None` on any mismatch or range violation.
113    #[cfg(any(test, feature = "serde", feature = "xml"))]
114    pub(crate) fn parse_rfc3339(input: &str) -> Option<Self> {
115        let mut cursor = Cursor::new(input);
116        let year = cursor.fixed_digits(4)?;
117        cursor.literal(b'-')?;
118        let month = cursor.fixed_digits(2)?;
119        cursor.literal(b'-')?;
120        let day = cursor.fixed_digits(2)?;
121        cursor.literal(b'T')?;
122        let (hour, minute, second, nanos) = cursor.clock()?;
123        let offset_seconds = cursor.rfc3339_offset()?;
124        if !cursor.done() {
125            return None;
126        }
127        Self::from_civil(
128            year,
129            month,
130            day,
131            hour,
132            minute,
133            second,
134            nanos,
135            offset_seconds,
136        )
137    }
138
139    /// Formats this date for the XML codec: RFC 3339 in UTC, always the `Z`
140    /// suffix, sub-second precision silently dropped. Years outside 0..=9999
141    /// widen or take a leading `-`.
142    #[cfg(any(test, feature = "serde", feature = "xml"))]
143    pub(crate) fn format_rfc3339(self) -> String {
144        let (year, month, day, hour, minute, second) = self.civil_parts();
145        let year = format_year(year);
146        format!("{year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
147    }
148
149    /// Parses a text-plist date in the `YYYY-MM-DD HH:MM:SS ±hhmm` layout:
150    /// 1-or-2-digit hour, optional fraction after the seconds, and a mandatory
151    /// `±hhmm` zone (no colon; hour at most 24, minute at most 60).
152    #[cfg(any(test, feature = "serde", feature = "openstep"))]
153    pub(crate) fn parse_text_layout(input: &str) -> Option<Self> {
154        let mut cursor = Cursor::new(input);
155        let year = cursor.fixed_digits(4)?;
156        cursor.literal(b'-')?;
157        let month = cursor.fixed_digits(2)?;
158        cursor.literal(b'-')?;
159        let day = cursor.fixed_digits(2)?;
160        cursor.literal(b' ')?;
161        let (hour, minute, second, nanos) = cursor.clock()?;
162        cursor.literal(b' ')?;
163        let offset_seconds = cursor.numeric_offset()?;
164        if !cursor.done() {
165            return None;
166        }
167        Self::from_civil(
168            year,
169            month,
170            day,
171            hour,
172            minute,
173            second,
174            nanos,
175            offset_seconds,
176        )
177    }
178
179    /// Formats this date for the text codec: the `YYYY-MM-DD HH:MM:SS ±hhmm`
180    /// layout in UTC, so the zone is always `+0000` and sub-second precision
181    /// is silently dropped.
182    #[cfg(any(test, feature = "openstep"))]
183    pub(crate) fn format_text_layout(self) -> String {
184        let (year, month, day, hour, minute, second) = self.civil_parts();
185        let year = format_year(year);
186        format!("{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} +0000")
187    }
188
189    #[cfg(any(test, feature = "serde", feature = "xml", feature = "openstep"))]
190    fn from_civil(
191        year: i64,
192        month: i64,
193        day: i64,
194        hour: i64,
195        minute: i64,
196        second: i64,
197        nanos: u32,
198        offset_seconds: i64,
199    ) -> Option<Self> {
200        let month = Month::try_from(u8::try_from(month).ok()?).ok()?;
201        let date = time::Date::from_calendar_date(
202            i32::try_from(year).ok()?,
203            month,
204            u8::try_from(day).ok()?,
205        )
206        .ok()?;
207        let clock = Time::from_hms_nano(
208            u8::try_from(hour).ok()?,
209            u8::try_from(minute).ok()?,
210            u8::try_from(second).ok()?,
211            nanos,
212        )
213        .ok()?;
214        let civil = PrimitiveDateTime::new(date, clock).assume_utc();
215        let unix_nanos =
216            civil.unix_timestamp_nanos() - i128::from(offset_seconds) * NANOS_PER_SECOND;
217        Some(Self::clamped(unix_nanos))
218    }
219
220    #[cfg(any(test, feature = "serde", feature = "xml", feature = "openstep"))]
221    fn civil_parts(self) -> (i32, u8, u8, u8, u8, u8) {
222        let date = self.0.date();
223        let clock = self.0.time();
224        (
225            date.year(),
226            u8::from(date.month()),
227            date.day(),
228            clock.hour(),
229            clock.minute(),
230            clock.second(),
231        )
232    }
233}
234
235impl From<SystemTime> for Date {
236    fn from(value: SystemTime) -> Self {
237        match value.duration_since(SystemTime::UNIX_EPOCH) {
238            Ok(after) => Self::clamped(i128::try_from(after.as_nanos()).unwrap_or(i128::MAX)),
239            Err(before) => {
240                let nanos = i128::try_from(before.duration().as_nanos()).unwrap_or(i128::MAX);
241                Self::clamped(-nanos)
242            }
243        }
244    }
245}
246
247impl From<Date> for SystemTime {
248    fn from(value: Date) -> Self {
249        let (secs, nanos) = value.unix_parts();
250        let result = if secs >= 0 {
251            Self::UNIX_EPOCH.checked_add(Duration::new(secs.cast_unsigned(), nanos))
252        } else if nanos == 0 {
253            Self::UNIX_EPOCH.checked_sub(Duration::new(secs.unsigned_abs(), 0))
254        } else {
255            let back = Duration::new(secs.unsigned_abs() - 1, 1_000_000_000 - nanos);
256            Self::UNIX_EPOCH.checked_sub(back)
257        };
258        // Unreachable on platforms whose SystemTime spans years ±9999.
259        result.unwrap_or(Self::UNIX_EPOCH)
260    }
261}
262
263#[cfg(any(test, feature = "serde", feature = "xml", feature = "openstep"))]
264fn format_year(year: i32) -> String {
265    if year < 0 {
266        format!("-{:04}", year.unsigned_abs())
267    } else {
268        format!("{year:04}")
269    }
270}
271
272#[cfg(any(test, feature = "serde", feature = "xml", feature = "openstep"))]
273struct Cursor<'a> {
274    bytes: &'a [u8],
275    pos: usize,
276}
277
278#[cfg(any(test, feature = "serde", feature = "xml", feature = "openstep"))]
279impl<'a> Cursor<'a> {
280    const fn new(input: &'a str) -> Self {
281        Self {
282            bytes: input.as_bytes(),
283            pos: 0,
284        }
285    }
286
287    fn literal(&mut self, expected: u8) -> Option<()> {
288        if self.bytes.get(self.pos) == Some(&expected) {
289            self.pos += 1;
290            Some(())
291        } else {
292            None
293        }
294    }
295
296    fn digit(&mut self) -> Option<i64> {
297        let &c = self.bytes.get(self.pos)?;
298        if c.is_ascii_digit() {
299            self.pos += 1;
300            Some(i64::from(c - b'0'))
301        } else {
302            None
303        }
304    }
305
306    fn fixed_digits(&mut self, count: u32) -> Option<i64> {
307        let mut value = 0;
308        for _ in 0..count {
309            value = value * 10 + self.digit()?;
310        }
311        Some(value)
312    }
313
314    /// Non-fixed numeric field: one digit, or two when a second one follows.
315    fn one_or_two_digits(&mut self) -> Option<i64> {
316        let first = self.digit()?;
317        Some(self.digit().map_or(first, |second| first * 10 + second))
318    }
319
320    /// `HH:MM:SS` with a 1-or-2-digit hour and a parse-only fractional
321    /// second: `.` or `,` plus at least one digit, kept to 9 digits.
322    fn clock(&mut self) -> Option<(i64, i64, i64, u32)> {
323        let hour = self.one_or_two_digits()?;
324        self.literal(b':')?;
325        let minute = self.fixed_digits(2)?;
326        self.literal(b':')?;
327        let second = self.fixed_digits(2)?;
328        Some((hour, minute, second, self.fraction_nanos()))
329    }
330
331    fn fraction_nanos(&mut self) -> u32 {
332        if !matches!(self.bytes.get(self.pos), Some(b'.' | b',')) {
333            return 0;
334        }
335        if !self.bytes.get(self.pos + 1).is_some_and(u8::is_ascii_digit) {
336            return 0;
337        }
338        self.pos += 1;
339        let mut nanos: u32 = 0;
340        let mut digits = 0;
341        while let Some(&c) = self.bytes.get(self.pos) {
342            if !c.is_ascii_digit() {
343                break;
344            }
345            if digits < 9 {
346                nanos = nanos * 10 + u32::from(c - b'0');
347                digits += 1;
348            }
349            self.pos += 1;
350        }
351        while digits < 9 {
352            nanos *= 10;
353            digits += 1;
354        }
355        nanos
356    }
357
358    /// `Z` or `±hh:mm`, returning the offset in seconds.
359    #[cfg(any(test, feature = "serde", feature = "xml"))]
360    fn rfc3339_offset(&mut self) -> Option<i64> {
361        if self.literal(b'Z').is_some() {
362            return Some(0);
363        }
364        let negative = self.sign()?;
365        let hours = self.fixed_digits(2)?;
366        self.literal(b':')?;
367        let minutes = self.fixed_digits(2)?;
368        Self::zone_seconds(hours, minutes, negative)
369    }
370
371    /// `±hhmm`, returning the offset in seconds.
372    #[cfg(any(test, feature = "serde", feature = "openstep"))]
373    fn numeric_offset(&mut self) -> Option<i64> {
374        let negative = self.sign()?;
375        let hours = self.fixed_digits(2)?;
376        let minutes = self.fixed_digits(2)?;
377        Self::zone_seconds(hours, minutes, negative)
378    }
379
380    fn sign(&mut self) -> Option<bool> {
381        match self.bytes.get(self.pos) {
382            Some(b'+') => {
383                self.pos += 1;
384                Some(false)
385            }
386            Some(b'-') => {
387                self.pos += 1;
388                Some(true)
389            }
390            _ => None,
391        }
392    }
393
394    /// Lenient zone ranges: hour at most 24, minute at most 60.
395    const fn zone_seconds(hours: i64, minutes: i64, negative: bool) -> Option<i64> {
396        if hours > 24 || minutes > 60 {
397            return None;
398        }
399        let seconds = (hours * 60 + minutes) * 60;
400        Some(if negative { -seconds } else { seconds })
401    }
402
403    const fn done(&self) -> bool {
404        self.pos == self.bytes.len()
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    #![expect(
411        clippy::unwrap_used,
412        clippy::float_cmp,
413        reason = "test code: unwrap is the assertion; float expectations are bit-exact"
414    )]
415
416    use super::*;
417
418    fn rfc3339(s: &str) -> Date {
419        Date::parse_rfc3339(s).unwrap()
420    }
421
422    #[test]
423    fn apple_epoch_round_trips_the_golden_fixture() {
424        let date = rfc3339("2013-11-27T00:34:00Z");
425        let encoded = date.to_apple_epoch();
426        assert_eq!(encoded, 407_205_240.0);
427        assert_eq!(encoded.to_bits(), 0x41B8_4575_7800_0000);
428        assert_eq!(Date::from_apple_epoch(407_205_240.0), date);
429    }
430
431    #[test]
432    fn apple_epoch_parse_truncates_fractional_nanos_toward_zero() {
433        let date = Date::from_apple_epoch(0.5);
434        assert_eq!(date.unix_parts(), (978_307_200, 500_000_000));
435
436        // Pre-Unix-epoch: the split yields a negative fraction; the seconds
437        // field borrows to normalize the nanoseconds back into range.
438        let date = Date::from_apple_epoch(-978_307_200.5);
439        assert_eq!(date.unix_parts(), (-1, 500_000_000));
440    }
441
442    #[test]
443    fn apple_epoch_parse_never_fails_and_clamps() {
444        let max = Date::from_apple_epoch(f64::INFINITY);
445        assert_eq!(max, Date(MAX_INSTANT));
446        assert_eq!(Date::from_apple_epoch(1e300), Date(MAX_INSTANT));
447        assert_eq!(Date::from_apple_epoch(f64::NEG_INFINITY), Date(MIN_INSTANT));
448        assert_eq!(Date::from_apple_epoch(-1e300), Date(MIN_INSTANT));
449        // Rust's saturating cast maps NaN to 0 seconds (the exact value here
450        // is implementation-defined; only success parity is contractual).
451        assert_eq!(Date::from_apple_epoch(f64::NAN).unix_parts(), (0, 0));
452    }
453
454    #[test]
455    fn apple_epoch_encode_rounds_through_the_nanosecond_intermediate() {
456        // `float64(unix_nanos)/1e9 - epoch` rounds the 61-bit nano count
457        // once; the pinned bits below are the expected result.
458        let date = Date::from_unix(1_385_512_440, 250_000_000);
459        assert_eq!(date.to_apple_epoch().to_bits(), 0x41B8_4575_783F_FFFC);
460    }
461
462    #[test]
463    fn rfc3339_parse_accepts_the_grammar() {
464        assert_eq!(
465            rfc3339("2013-11-27T00:34:00Z").unix_parts(),
466            (1_385_512_440, 0)
467        );
468        assert_eq!(
469            rfc3339("2013-11-27T00:34:00.5Z").unix_parts(),
470            (1_385_512_440, 500_000_000)
471        );
472        assert_eq!(
473            rfc3339("2013-11-27T00:34:00,5Z").unix_parts(),
474            (1_385_512_440, 500_000_000)
475        );
476        assert_eq!(
477            rfc3339("2013-11-27T1:34:00Z").unix_parts(),
478            (1_385_516_040, 0)
479        );
480        assert_eq!(
481            rfc3339("2013-11-27T00:34:00+07:00").unix_parts(),
482            (1_385_487_240, 0)
483        );
484        assert_eq!(
485            rfc3339("2013-11-27T00:34:00-00:30").unix_parts(),
486            (1_385_514_240, 0)
487        );
488        assert_eq!(
489            rfc3339("2013-11-27T00:34:00+24:00").unix_parts(),
490            (1_385_426_040, 0)
491        );
492        assert_eq!(
493            rfc3339("2013-11-27T00:34:00+23:60").unix_parts(),
494            (1_385_426_040, 0)
495        );
496        assert_eq!(
497            rfc3339("2013-11-27T00:34:00.123456789123Z").unix_parts(),
498            (1_385_512_440, 123_456_789)
499        );
500        assert_eq!(
501            rfc3339("0000-01-01T00:00:00+24:00").unix_parts(),
502            (-62_167_305_600, 0)
503        );
504    }
505
506    #[test]
507    fn rfc3339_parse_rejects_malformed_input() {
508        for s in [
509            "",
510            "2013-11-27t00:34:00Z",
511            "2013-11-27T00:34:00",
512            "2013-11-27T00:34:00z",
513            "2013-11-27T00:34:00+0700",
514            "2013-11-27T00:34:00+25:00",
515            "2013-02-30T00:34:00Z",
516            "12013-11-27T00:34:00Z",
517            "2013-11-27T00:34:60Z",
518            "2013-11-27T24:34:00Z",
519            "2013-1-27T00:34:00Z",
520            "2013-11-27T0:4:00Z",
521            "2013-11-27T00:34:00.Z",
522            "2013-11-27T00:34:00Z ",
523            "2013-13-01T00:00:00Z",
524            "2013-00-01T00:00:00Z",
525            "2013-11-00T00:00:00Z",
526        ] {
527            assert!(Date::parse_rfc3339(s).is_none(), "{s}");
528        }
529    }
530
531    #[test]
532    fn rfc3339_parse_clamps_past_the_calendar_edge() {
533        // The offset rolls this into year 10000; the time crate tops out at
534        // 9999, so the result clamps to the maximum instant.
535        let date = rfc3339("9999-12-31T23:59:59-24:00");
536        assert_eq!(date, Date(MAX_INSTANT));
537    }
538
539    #[test]
540    fn rfc3339_format_is_utc_z_with_subseconds_dropped() {
541        assert_eq!(
542            rfc3339("2013-11-27T00:34:00Z").format_rfc3339(),
543            "2013-11-27T00:34:00Z"
544        );
545        assert_eq!(
546            rfc3339("2013-11-27T00:34:00.75Z").format_rfc3339(),
547            "2013-11-27T00:34:00Z"
548        );
549        assert_eq!(
550            rfc3339("2013-11-27T05:34:00+05:00").format_rfc3339(),
551            "2013-11-27T00:34:00Z"
552        );
553        assert_eq!(Date(MIN_INSTANT).format_rfc3339(), "-9999-01-01T00:00:00Z");
554        assert_eq!(
555            rfc3339("0001-02-03T04:05:06Z").format_rfc3339(),
556            "0001-02-03T04:05:06Z"
557        );
558    }
559
560    #[test]
561    fn text_layout_parse_accepts_the_grammar() {
562        let parse = Date::parse_text_layout;
563        assert_eq!(
564            parse("2013-11-27 00:34:00 +0000").unwrap().unix_parts(),
565            (1_385_512_440, 0)
566        );
567        assert_eq!(
568            parse("2013-11-27 0:34:00 +0000").unwrap().unix_parts(),
569            (1_385_512_440, 0)
570        );
571        assert_eq!(
572            parse("2013-11-27 00:34:00.25 +0000").unwrap().unix_parts(),
573            (1_385_512_440, 250_000_000)
574        );
575        assert_eq!(
576            parse("2013-11-27 00:34:00,5 +0000").unwrap().unix_parts(),
577            (1_385_512_440, 500_000_000)
578        );
579        assert_eq!(
580            parse("2013-11-27 00:34:00 -0500").unwrap().unix_parts(),
581            (1_385_530_440, 0)
582        );
583        assert_eq!(
584            parse("2013-11-27 00:34:00 +0060").unwrap().unix_parts(),
585            (1_385_508_840, 0)
586        );
587    }
588
589    #[test]
590    fn text_layout_parse_rejects_malformed_input() {
591        for s in [
592            "",
593            "2013-11-27 00:34:00 Z",
594            "2013-11-27 00:34:00 +00:00",
595            "2013-11-27 00:34:00",
596            "2013-11-27 00:34:00 +000",
597            "2013-11-27 00:34:00 +9900",
598            "2013-11-27 00:34:00 +2500",
599            "2013-11-27 00:34:00 +0061",
600            "2013-02-30 00:34:00 +0000",
601            "2013-11-27T00:34:00 +0000",
602            "2013-11-27 00:34:00 +0000 ",
603        ] {
604            assert!(Date::parse_text_layout(s).is_none(), "{s}");
605        }
606    }
607
608    #[test]
609    fn text_layout_format_is_utc_plus_zero_zero() {
610        let date = rfc3339("2013-11-27T00:34:00.9Z");
611        assert_eq!(date.format_text_layout(), "2013-11-27 00:34:00 +0000");
612    }
613
614    #[test]
615    fn text_and_rfc3339_round_trip_whole_second_dates() {
616        let date = Date::parse_text_layout("2013-11-27 05:34:00 -0500").unwrap();
617        assert_eq!(date.format_text_layout(), "2013-11-27 10:34:00 +0000");
618        assert_eq!(Date::parse_rfc3339(&date.format_rfc3339()).unwrap(), date);
619        assert_eq!(
620            Date::parse_text_layout(&date.format_text_layout()).unwrap(),
621            date
622        );
623    }
624
625    #[test]
626    fn system_time_conversions_round_trip() {
627        let after = SystemTime::UNIX_EPOCH + Duration::new(1_385_512_440, 123);
628        assert_eq!(SystemTime::from(Date::from(after)), after);
629
630        let before = SystemTime::UNIX_EPOCH - Duration::new(86_400, 250_000_000);
631        let date = Date::from(before);
632        assert_eq!(date.unix_parts(), (-86_401, 750_000_000));
633        assert_eq!(SystemTime::from(date), before);
634    }
635
636    #[test]
637    fn ordering_and_hashing_are_instant_based() {
638        let earlier = rfc3339("2013-11-27T00:34:00Z");
639        let later = rfc3339("2013-11-27T00:34:01Z");
640        assert!(earlier < later);
641        assert_eq!(rfc3339("2013-11-27T01:34:00+01:00"), earlier);
642    }
643
644    #[test]
645    fn from_unix_normalizes_negative_nanos() {
646        assert_eq!(
647            Date::from_unix(0, -500_000_000).unix_parts(),
648            (-1, 500_000_000)
649        );
650        assert_eq!(
651            Date::from_unix(1, 1_500_000_000).unix_parts(),
652            (2, 500_000_000)
653        );
654        assert_eq!(Date::from_unix(i64::MAX, i64::MAX), Date(MAX_INSTANT));
655        assert_eq!(Date::from_unix(i64::MIN, i64::MIN), Date(MIN_INSTANT));
656    }
657}