Skip to main content

rfc3339_fast/
lib.rs

1//! # RFC3339 Timestamp Library
2//!
3//! A high-performance library for parsing and formatting RFC3339/ISO8601
4//! timestamps, with support for nanosecond precision.
5//!
6//! ## Overview
7//!
8//! This library provides efficient serialization and deserialization of RFC3339/ISO8601
9//! timestamps in the format `YYYY-MM-DDTHH:mm:ss[.nnn]Z`. It supports:
10//!
11//! - Timestamps from year 1 to year 9999
12//! - Nanosecond precision (up to 9 decimal places)
13//! - Integration with `std::time::SystemTime` (with the default `std` feature)
14//! - `no_std` support by disabling default features
15//! - Optional support for `chrono` types via the `chrono` feature
16//! - Optional `serde` integration via the `serde` feature
17//! - Optional `tokio-postgres` / `postgres` integration via the `postgres` feature
18//! - SIMD acceleration on platforms supporting SSSE3 (`x86`/`x86_64`) or NEON (ARM)
19//!
20//! ## Examples
21//!
22//! Parsing a timestamp from a string:
23//!
24//! ```
25//! use std::str::FromStr;
26//! # use rfc3339_fast::Timestamp;
27//! let ts = Timestamp::from_str("2026-02-25T14:30:00Z").unwrap();
28//! ```
29//!
30//! Formatting a timestamp to a string:
31//!
32//! ```
33//! # use rfc3339_fast::{Timestamp, Buffer};
34//! let ts = Timestamp::now();
35//! let mut buf = Buffer::new();
36//! let formatted = buf.format(ts);
37//! ```
38
39#![cfg_attr(not(feature = "std"), no_std)]
40#![deny(unsafe_op_in_unsafe_fn)]
41// Crate-wide clippy::pedantic suppressions. Each one is intentional and
42// scoped narrowly enough that the local code makes the safety/perf reason
43// obvious; we silence them at the crate root to keep the hot paths
44// uncluttered by `#[allow]` attributes on every line.
45//
46// * `cast_possible_wrap`, `cast_sign_loss`, `cast_lossless`,
47//   `cast_possible_truncation`: the date-arithmetic and SIMD code does a
48//   lot of `u32 ↔ i32` and `usize → u16`/`u32` casts on values that are
49//   provably in range (years bounded by 1–9999, lengths bounded by
50//   `BUFFER_SIZE = 30`, JD math pre-biased into the positive range, etc.).
51//   Switching to `From::from` / `TryFrom` would either fail to compile
52//   (different signedness) or add a runtime check on a hot path.
53// * `unreadable_literal`: constants like `2440588` (Julian Day of the Unix
54//   epoch) and `253402300799` (max representable Unix-seconds value) are
55//   well-known reference numbers; underscore-grouping them obscures the
56//   reference more than it helps.
57// * `inline_always`: the per-byte `write_byte` / `write_number` /
58//   `jsonenc_nanos` helpers are tiny leaf functions on the format hot
59//   path; benchmarks regress noticeably if the inliner is allowed to
60//   second-guess them.
61// * `items_after_statements`: a few local `const`s are placed next to
62//   their first use inside `jsonenc_timestamp` to keep the algorithm and
63//   its magic numbers visually adjacent.
64#![allow(
65    clippy::cast_possible_wrap,
66    clippy::cast_sign_loss,
67    clippy::cast_lossless,
68    clippy::cast_possible_truncation,
69    clippy::unreadable_literal,
70    clippy::inline_always,
71    clippy::items_after_statements
72)]
73
74use core::{fmt, mem::MaybeUninit, ptr, str::FromStr};
75
76#[cfg(feature = "std")]
77use std::time::{Duration, SystemTime, UNIX_EPOCH};
78
79#[cfg(target_feature = "ssse3")]
80mod sse;
81
82#[cfg(target_feature = "neon")]
83mod neon;
84
85#[cfg(feature = "chrono")]
86mod chrono_impl;
87
88#[cfg(feature = "serde")]
89mod serde_impl;
90
91#[cfg(feature = "postgres")]
92mod postgres_impl;
93
94/// Error type for parsing or formatting JSON timestamps.
95///
96/// This enum represents errors that can occur when parsing timestamp strings
97/// or validating timestamp values.
98///
99/// Marked `#[non_exhaustive]` so additional variants can be introduced in a
100/// future release without a semver break; downstream `match` expressions
101/// must include a wildcard arm.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum TimestampError {
105    /// The input string had an invalid format.
106    InvalidFormat,
107    /// The timestamp value is out of the supported range
108    /// (year 1 through year 9999, with `nanos < 1_000_000_000`).
109    OutOfRange,
110}
111
112impl fmt::Display for TimestampError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            TimestampError::InvalidFormat => write!(f, "invalid timestamp format"),
116            TimestampError::OutOfRange => write!(f, "timestamp value out of range"),
117        }
118    }
119}
120
121impl core::error::Error for TimestampError {}
122
123#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
124/// A timestamp value that can be serialized to and deserialized from JSON.
125///
126/// `Timestamp` is stored internally as `(seconds: i64, nanos: u32)`, where
127/// `seconds` counts (signed) seconds since the Unix epoch and `nanos` is
128/// always in `[0, 1_000_000_000)`. This matches the
129/// [`google.protobuf.Timestamp`] convention and lets the type be
130/// `no_std`-compatible. Negative whole-second values combined with a
131/// positive `nanos` mean the same instant as the float
132/// `seconds + nanos / 1e9`, e.g. `(-5, 200_000_000)` is `-4.8s`.
133///
134/// ## Invariants
135///
136/// Every `Timestamp` value satisfies:
137///
138/// * `seconds` is in `SECONDS_MIN..=SECONDS_MAX`, i.e. it represents an
139///   instant between `0001-01-01T00:00:00Z` and `9999-12-31T23:59:59Z`
140///   inclusive.
141/// * `nanos` is in `[0, 1_000_000_000)`.
142///
143/// All public constructors enforce these invariants — [`Timestamp::now`],
144/// [`Timestamp::from_unix`], [`Timestamp::from_str`], `From<SystemTime>`,
145/// and the optional `From<chrono::DateTime<Tz>>` either validate, saturate,
146/// or are infallible by construction. As a result, [`Buffer::format`] and
147/// [`fmt::Display`] never fail.
148///
149/// With the default `std` feature, this type round-trips to and from
150/// [`std::time::SystemTime`] efficiently — the conversion is just a
151/// `duration_since(UNIX_EPOCH)` followed by storing the two fields
152/// (saturating at the supported range bounds).
153///
154/// [`google.protobuf.Timestamp`]: https://protobuf.dev/reference/protobuf/google.protobuf/#timestamp
155///
156/// ## Creating timestamps
157///
158/// ```
159/// # use rfc3339_fast::Timestamp;
160/// // From Unix seconds + nanoseconds.
161/// let ts = Timestamp::from_unix(1_641_006_000, 0).unwrap();
162///
163/// // Equivalent via the `TryFrom<(i64, u32)>` impl.
164/// let ts = Timestamp::try_from((1_641_006_000i64, 0u32)).unwrap();
165///
166/// // With the `std` feature: from current system time.
167/// # #[cfg(feature = "std")] {
168/// let now = Timestamp::now();
169/// let ts = Timestamp::from(std::time::SystemTime::now());
170/// # }
171/// ```
172///
173/// ## Inspecting timestamps
174///
175/// ```
176/// # use rfc3339_fast::Timestamp;
177/// let ts = Timestamp::from_unix(1_641_006_000, 250_000_000).unwrap();
178/// assert_eq!(ts.seconds(), 1_641_006_000);
179/// assert_eq!(ts.subsec_nanos(), 250_000_000);
180/// ```
181pub struct Timestamp {
182    /// Signed seconds since the Unix epoch.
183    seconds: i64,
184    /// Fractional seconds, always in `[0, 1_000_000_000)`.
185    nanos: u32,
186}
187
188impl Timestamp {
189    /// Constructs a `Timestamp` directly from canonical `(seconds, nanos)`
190    /// fields without range checks. Internal helper: callers must
191    /// guarantee `seconds` is in `SECONDS_MIN..=SECONDS_MAX` and `nanos`
192    /// is in `[0, 1_000_000_000)`.
193    #[inline]
194    fn new_unchecked(seconds: i64, nanos: u32) -> Self {
195        debug_assert!(nanos < 1_000_000_000);
196        Self { seconds, nanos }
197    }
198
199    /// Constructs a `Timestamp` from Unix seconds and nanoseconds.
200    ///
201    /// Returns [`TimestampError::OutOfRange`] if `seconds` is outside the
202    /// representable year 1..=9999 range, or if `nanos >= 1_000_000_000`.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// # use rfc3339_fast::Timestamp;
208    /// let ts = Timestamp::from_unix(0, 0).unwrap();
209    /// assert_eq!(ts.seconds(), 0);
210    /// assert_eq!(ts.subsec_nanos(), 0);
211    /// ```
212    pub fn from_unix(seconds: i64, nanos: u32) -> Result<Self, TimestampError> {
213        if !(SECONDS_MIN..=SECONDS_MAX).contains(&seconds) || nanos >= 1_000_000_000 {
214            return Err(TimestampError::OutOfRange);
215        }
216        Ok(Self::new_unchecked(seconds, nanos))
217    }
218
219    /// Returns the (signed) seconds component, counted from the Unix epoch.
220    #[inline]
221    #[must_use]
222    pub fn seconds(&self) -> i64 {
223        self.seconds
224    }
225
226    /// Returns the fractional-second component, in nanoseconds.
227    ///
228    /// The returned value is always in `[0, 1_000_000_000)`. The naming
229    /// matches [`std::time::Duration::subsec_nanos`].
230    #[inline]
231    #[must_use]
232    pub fn subsec_nanos(&self) -> u32 {
233        self.nanos
234    }
235
236    /// Returns a `Timestamp` representing the current system time.
237    ///
238    /// This is a convenience method for `Timestamp::from(SystemTime::now())`.
239    #[cfg(feature = "std")]
240    #[must_use]
241    pub fn now() -> Self {
242        Self::from(SystemTime::now())
243    }
244}
245
246/// Converts a `SystemTime` into a `Timestamp` by computing its offset from
247/// the Unix epoch.
248///
249/// `SystemTime` can in principle represent instants outside the
250/// `Timestamp` range (year 1 through year 9999); such values are
251/// **saturated** to the nearest in-range second. In practice this only
252/// affects deliberately constructed `SystemTime`s; wall-clock times from
253/// the system clock are well within range.
254#[cfg(feature = "std")]
255impl From<SystemTime> for Timestamp {
256    #[inline]
257    fn from(value: SystemTime) -> Self {
258        // Both branches collapse to a single `duration_since` plus a
259        // small fixup; LLVM inlines this away when `format` is called
260        // directly on a `SystemTime`. The final `clamp` enforces the
261        // `Timestamp` range invariant.
262        let (seconds, nanos) = match value.duration_since(UNIX_EPOCH) {
263            Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
264            Err(e) => {
265                let dur_before = e.duration();
266                let secs_before = -(dur_before.as_secs() as i64);
267                let nanos_before = dur_before.subsec_nanos();
268                if nanos_before > 0 {
269                    (secs_before - 1, 1_000_000_000 - nanos_before)
270                } else {
271                    (secs_before, 0)
272                }
273            }
274        };
275        // Saturate out-of-range values to the supported bounds. When
276        // saturating to `SECONDS_MAX`, drop sub-second precision so the
277        // result still represents `9999-12-31T23:59:59Z`.
278        if seconds < SECONDS_MIN {
279            Self::new_unchecked(SECONDS_MIN, 0)
280        } else if seconds > SECONDS_MAX {
281            Self::new_unchecked(SECONDS_MAX, 0)
282        } else {
283            Self::new_unchecked(seconds, nanos)
284        }
285    }
286}
287
288/// Wraps a `SystemTime` reference as a `Timestamp`. See [`From<SystemTime>`]
289/// for the saturation contract.
290#[cfg(feature = "std")]
291impl From<&SystemTime> for Timestamp {
292    #[inline]
293    fn from(value: &SystemTime) -> Self {
294        Self::from(*value)
295    }
296}
297
298/// Converts a `Timestamp` back into a `SystemTime` by adding (or
299/// subtracting) its offset from the Unix epoch.
300#[cfg(feature = "std")]
301impl From<Timestamp> for SystemTime {
302    #[inline]
303    fn from(value: Timestamp) -> Self {
304        if value.seconds >= 0 {
305            UNIX_EPOCH + Duration::new(value.seconds as u64, value.nanos)
306        } else {
307            // Canonical form has nanos in [0, 1e9). For negative seconds,
308            // a non-zero `nanos` adds time *forward*, so the magnitude of
309            // the offset is `-seconds - 1` whole seconds plus `1e9 - nanos`
310            // sub-second component (when nanos > 0).
311            let (mag_secs, mag_nanos) = if value.nanos == 0 {
312                ((-value.seconds) as u64, 0)
313            } else {
314                ((-value.seconds - 1) as u64, 1_000_000_000 - value.nanos)
315            };
316            UNIX_EPOCH - Duration::new(mag_secs, mag_nanos)
317        }
318    }
319}
320
321impl From<&Timestamp> for Timestamp {
322    #[inline]
323    fn from(value: &Timestamp) -> Self {
324        *value
325    }
326}
327
328impl TryFrom<(i64, u32)> for Timestamp {
329    type Error = TimestampError;
330
331    #[inline]
332    fn try_from(value: (i64, u32)) -> Result<Self, Self::Error> {
333        Self::from_unix(value.0, value.1)
334    }
335}
336
337impl fmt::Display for Timestamp {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        let mut buf = Buffer::new();
340        write!(f, "{}", buf.format(self))
341    }
342}
343
344impl FromStr for Timestamp {
345    type Err = TimestampError;
346
347    fn from_str(s: &str) -> Result<Self, Self::Err> {
348        let mut ascii = s.as_bytes();
349
350        #[cfg(target_feature = "ssse3")]
351        let (seconds, nanos) = unsafe {
352            (
353                sse::decode_seconds(&mut ascii)?,
354                sse::decode_nanos(&mut ascii)?,
355            )
356        };
357
358        #[cfg(target_feature = "neon")]
359        let (seconds, nanos) = unsafe {
360            (
361                neon::decode_seconds(&mut ascii)?,
362                neon::decode_nanos(&mut ascii)?,
363            )
364        };
365
366        #[cfg(not(any(target_feature = "ssse3", target_feature = "neon")))]
367        let (seconds, nanos) = (decode_seconds(&mut ascii)?, decode_nanos(&mut ascii)?);
368
369        let offset = match ascii.first() {
370            Some(b'Z') => 0,
371            Some(&c @ (b'+' | b'-')) => decode_offset(ascii, c)?,
372            _ => return Err(TimestampError::InvalidFormat),
373        };
374
375        // `decode_nanos` returns `i32` in `[0, 1_000_000_000)`, and
376        // `decode_seconds` keeps the year in 1..=9999, so the cast and
377        // unchecked constructor are sound here.
378        Ok(Self::new_unchecked(seconds + offset, nanos as u32))
379    }
380}
381
382/// Decodes an RFC3339 numeric timezone offset of the form `+HH:MM` or `-HH:MM`.
383///
384/// Returns the value (in seconds) that must be added to the local-time seconds
385/// to yield UTC seconds. For example, `+05:00` yields `-18000`.
386#[inline]
387fn decode_offset(ascii: &[u8], sign: u8) -> Result<i64, TimestampError> {
388    // Expect exactly 6 bytes: [+-]HH:MM
389    if ascii.len() != 6 || ascii[3] != b':' {
390        return Err(TimestampError::InvalidFormat);
391    }
392
393    let h10 = ascii[1].wrapping_sub(b'0');
394    let h1 = ascii[2].wrapping_sub(b'0');
395    let m10 = ascii[4].wrapping_sub(b'0');
396    let m1 = ascii[5].wrapping_sub(b'0');
397    if (h10 | h1 | m10 | m1) > 9 {
398        return Err(TimestampError::InvalidFormat);
399    }
400
401    let hours = h10 as i64 * 10 + h1 as i64;
402    let mins = m10 as i64 * 10 + m1 as i64;
403    if hours > 23 || mins > 59 {
404        return Err(TimestampError::InvalidFormat);
405    }
406
407    let magnitude = hours * 3600 + mins * 60;
408    // local = UTC + offset (when sign is '+'), so UTC = local - offset.
409    Ok(if sign == b'+' { -magnitude } else { magnitude })
410}
411
412// 30 bytes is exactly enough for the longest valid timestamp:
413//   `YYYY-MM-DDTHH:mm:ss.sssssssssZ` (4+1+2+1+2 + 1 + 2+1+2+1+2 + 1+9 + 1 = 30).
414// Combined with a `u16` length below, the whole `Buffer` is 32 bytes — one
415// cacheline / four qwords — which keeps it cheap to copy and well-aligned on
416// the stack.
417const BUFFER_SIZE: usize = 30; // YYYY-MM-DDTHH:mm:ss.sssssssssZ
418const SECONDS_MIN: i64 = -62135596800;
419const SECONDS_MAX: i64 = 253402300799;
420
421/// Pre-computed pair table: byte `2*N` and `2*N+1` are the ASCII digits of
422/// `N` for `N` in `0..=99`. Used by `write_2_at` / `write_4_at` /
423/// `write_9_at` to convert a value to its two-digit ASCII form with one
424/// indexed `u16` load instead of two scalar divides.
425const PAIR_TABLE: [u8; 200] = {
426    let mut t = [0u8; 200];
427    let mut i = 0;
428    while i < 100 {
429        t[i * 2] = b'0' + (i / 10) as u8;
430        t[i * 2 + 1] = b'0' + (i % 10) as u8;
431        i += 1;
432    }
433    t
434};
435
436/// A reusable buffer for formatting timestamps to strings.
437///
438/// `Buffer` provides an efficient way to format multiple timestamps without
439/// allocating memory. It uses a fixed-size stack-allocated buffer that is
440/// large enough to hold any valid ISO8601 timestamp with nanosecond precision.
441///
442/// The buffer size is 30 bytes, which is sufficient to hold the longest
443/// possible timestamp: `9999-12-31T23:59:59.999999999Z`.
444///
445/// ## Examples
446///
447/// ```
448/// # use rfc3339_fast::{Timestamp, Buffer};
449/// let mut buf = Buffer::new();
450/// let ts = Timestamp::now();
451/// let formatted = buf.format(ts);
452/// println!("{}", formatted);  // Prints the timestamp
453/// ```
454pub struct Buffer {
455    bytes: [MaybeUninit<u8>; BUFFER_SIZE],
456    // `len` is bounded by `BUFFER_SIZE` (30), so a `u16` is plenty and lets
457    // the whole struct round to a tidy 32 bytes. We `as`-cast freely between
458    // `len` and `usize` because the value is guaranteed to fit.
459    len: u16,
460}
461
462impl Default for Buffer {
463    #[inline]
464    fn default() -> Buffer {
465        Buffer::new()
466    }
467}
468
469impl Copy for Buffer {}
470
471// `Clone` is implemented manually rather than derived because the buffer
472// holds `MaybeUninit<u8>` whose contents are only valid for the
473// `..self.len` prefix; cloning by copying that uninitialized tail would
474// be wasteful and semantically meaningless. We instead return a fresh
475// empty buffer, which matches the typical usage pattern (a `Buffer` is
476// always reset before each `format` call). The two clippy lints below
477// flag the unusual semantics on purpose; we accept them.
478#[allow(clippy::non_canonical_clone_impl, clippy::expl_impl_clone_on_copy)]
479impl Clone for Buffer {
480    #[inline]
481    fn clone(&self) -> Self {
482        Buffer::new()
483    }
484}
485
486impl Buffer {
487    #[inline]
488    /// Creates a new empty `Buffer`.
489    ///
490    /// The buffer can then be used to format multiple timestamps.
491    #[must_use]
492    pub fn new() -> Buffer {
493        let bytes = [MaybeUninit::<u8>::uninit(); BUFFER_SIZE];
494        Buffer { bytes, len: 0 }
495    }
496
497    /// Formats a timestamp into an ISO8601 string.
498    ///
499    /// This method converts a timestamp (or anything convertible to `Timestamp`)
500    /// into a string in the format `YYYY-MM-DDTHH:mm:ss[.nnn]Z`.
501    ///
502    /// The returned string is a borrowed reference to data stored in this buffer.
503    /// To format another timestamp, call `format` again and it will overwrite
504    /// the previous contents.
505    ///
506    /// This method is infallible: every [`Timestamp`] value is guaranteed by
507    /// construction to lie in the representable range
508    /// (`0001-01-01T00:00:00Z` through `9999-12-31T23:59:59.999999999Z`),
509    /// and `Buffer` is sized to hold the longest such string.
510    ///
511    /// # Arguments
512    ///
513    /// * `timestamp` - A value that can be converted to `Timestamp` (includes
514    ///   `SystemTime`, `&SystemTime`, `&Timestamp`, and `chrono::DateTime` when
515    ///   the `chrono` feature is enabled).
516    ///
517    /// # Returns
518    ///
519    /// A string slice containing the formatted timestamp.
520    pub fn format<T: Into<Timestamp>>(&mut self, timestamp: T) -> &str {
521        let timestamp = timestamp.into();
522        self.reset();
523
524        let seconds = timestamp.seconds;
525        let nanos = timestamp.nanos as i32;
526
527        // SAFETY/correctness: `Timestamp`'s constructors guarantee
528        // `seconds` lies in `SECONDS_MIN..=SECONDS_MAX` and `nanos` is in
529        // `[0, 1_000_000_000)`, so the date arithmetic and the
530        // fixed-offset writes inside `jsonenc_timestamp` stay within the
531        // 30-byte buffer.
532        debug_assert!((SECONDS_MIN..=SECONDS_MAX).contains(&seconds));
533        debug_assert!((0..1_000_000_000).contains(&nanos));
534
535        self.jsonenc_timestamp(seconds, nanos);
536        self.as_str()
537    }
538
539    #[inline]
540    fn reset(&mut self) {
541        self.len = 0;
542    }
543
544    /// Writes a single byte to the buffer.
545    ///
546    /// This is a low-level method used internally for formatting. The buffer
547    /// is sized to fit the longest possible ISO8601 timestamp, so callers
548    /// inside [`Buffer::jsonenc_timestamp`] never overflow it; we assert
549    /// this only in debug builds to keep the hot path branch-free.
550    #[inline(always)]
551    fn write_byte(&mut self, value: u8) {
552        let len = self.len as usize;
553        debug_assert!(len < BUFFER_SIZE, "Buffer overflow in write_byte");
554        // SAFETY: caller ensures len < BUFFER_SIZE; checked in debug builds.
555        unsafe {
556            let end = self.bytes.as_mut_ptr().cast::<u8>().add(len);
557            ptr::write(end, value);
558        }
559        self.len = (len + 1) as u16;
560    }
561
562    /// Writes a numeric value with a fixed number of digits to the buffer.
563    ///
564    /// Pads with leading zeros as needed. For example, `write_number(42, 4)` writes
565    /// `0042`. Processes digits in pairs for efficiency.
566    ///
567    /// # Arguments
568    ///
569    /// * `value` - The number to write
570    /// * `digits` - The number of digits to write (with zero-padding)
571    #[inline(always)]
572    fn write_number(&mut self, mut value: u32, mut digits: usize) {
573        let len = self.len as usize + digits;
574        debug_assert!(len <= BUFFER_SIZE, "Buffer overflow in write_number");
575        if BUFFER_SIZE >= len {
576            unsafe {
577                self.len = len as u16;
578                let mut ptr = self.bytes.as_mut_ptr().cast::<u8>().add(len - 1);
579                // process 2 digits per iteration, this loop will likely be unrolled
580                while digits >= 2 {
581                    // combine these so the compiler can optimize both operations
582                    let d1;
583                    (value, d1) = (value / 100, value % 100);
584
585                    let (a, b) = (d1 / 10, d1 % 10);
586                    digits -= 1;
587                    ptr.write(b as u8 | b'0');
588                    ptr = ptr.sub(1);
589                    digits -= 1;
590                    ptr.write(a as u8 | b'0');
591                    ptr = ptr.sub(1);
592                }
593
594                // handle remainder
595                if digits == 1 {
596                    ptr.write(value as u8 | b'0');
597                }
598            }
599        }
600    }
601
602    /// Encodes a Unix timestamp into ISO8601 format in the buffer.
603    ///
604    /// Converts seconds and nanoseconds since the epoch into a formatted string
605    /// in the format `YYYY-MM-DDTHH:mm:ss[.nnn...]Z`.
606    ///
607    /// # Arguments
608    ///
609    /// * `seconds` - Seconds since the Unix epoch
610    /// * `nanos` - Nanoseconds (0-999,999,999)
611    ///
612    /// # Algorithm
613    ///
614    /// The date portion is computed by the Fliegel/Van Flandern algorithm,
615    /// which converts a Julian Day Number into a Gregorian (Y, M, D) triple
616    /// using only integer arithmetic — no lookup tables and no branches.
617    /// The original 1968 publication is:
618    ///
619    /// > Fliegel, H. F., and Van Flandern, T. C., "A Machine Algorithm for
620    /// > Processing Calendar Dates," Communications of the ACM, vol. 11
621    /// > no. 10 (October 1968), p. 657.
622    ///
623    /// The magic constants encode the Gregorian calendar's irregular cycle
624    /// of month lengths and leap years:
625    ///
626    /// * `146097` — days in a 400-year Gregorian cycle (the smallest cycle
627    ///   over which the calendar exactly repeats: `400*365 + 100 - 4 + 1`).
628    /// * `1461`   — days in a 4-year Julian cycle (`4*365 + 1`).
629    /// * `2447 / 80` — a piecewise-linear approximation of cumulative
630    ///   month lengths (March-based), exploiting truncating integer
631    ///   division so that successive months fall on the right day-of-year
632    ///   boundary without a lookup table.
633    /// * `68569`  — shifts the Julian Day Number into the algorithm's
634    ///   internal positive range.
635    /// * `49`     — recovers the original year after the 100-year
636    ///   regrouping done by the `n` term.
637    ///
638    /// We pre-bias `seconds` by the offset from 0001-01-01 to 1970-01-01 so
639    /// the value passed to the integer divisions is always non-negative,
640    /// which matches the algorithm's preconditions and avoids the
641    /// round-toward-zero pitfalls of signed division on negative operands.
642    ///
643    /// Background and a friendly walk-through of the Fortran original (and
644    /// of the related branchless variants) is in Josh Haberman's article
645    /// <https://blog.reverberate.org/2020/05/12/optimizing-date-algorithms.html>.
646    /// This implementation is a Rust port inspired by upb's C version:
647    /// <https://github.com/protocolbuffers/protobuf/blob/27421b97a0daa29e91460d377b0213f9e7be5d3f/upb/json/encode.c#L122>.
648    #[inline(always)]
649    fn jsonenc_timestamp(&mut self, mut seconds: i64, nanos: i32) {
650        const SECONDS_PER_DAY: i32 = 86400;
651
652        // Days from 0001-01-01 (proleptic Gregorian) to 1970-01-01.
653        const CE_EPOCH_TO_UNIX_EPOCH_DAYS: i32 = 719_162;
654        const CE_EPOCH_TO_UNIX_EPOCH_SECONDS: i64 =
655            CE_EPOCH_TO_UNIX_EPOCH_DAYS as i64 * SECONDS_PER_DAY as i64;
656
657        // Julian Day Number of the Unix epoch (1970-01-01). The Julian
658        // period starts on -4713-11-24 (proleptic Gregorian), so the
659        // Unix epoch is day 2_440_588 of that count.
660        const JD_UNIX_EPOCH: i32 = 2_440_588;
661
662        // Pre-fill the buffer with the fixed parts of the output:
663        //   YYYY-MM-DDTHH:MM:SS.NNNNNNNNNZ
664        //   0123456789012345678901234567890
665        //             1111111111222222222
666        // Underscores are placeholders for digits we'll overwrite below.
667        // Doing this as one 30-byte block lets LLVM lower it to a pair of
668        // 16-byte SSE stores; in exchange, the 9 separator bytes (`-`,
669        // `T`, `:`, `.`, `Z`) never need to be written individually inside
670        // the hot path. Just as importantly, by writing each digit at a
671        // *fixed* offset (rather than threading `self.len` through every
672        // call) we break the serial dependency chain between writes, so
673        // the back end can issue them in parallel.
674        const TEMPLATE: [u8; 30] = *b"____-__-__T__:__:__.000000000Z";
675        // SAFETY: `self.bytes` is `[MaybeUninit<u8>; 30]`, exactly the
676        // same size as `TEMPLATE`, and `MaybeUninit<u8>` has the same
677        // layout as `u8`.
678        unsafe {
679            ptr::copy_nonoverlapping(TEMPLATE.as_ptr(), self.bytes.as_mut_ptr().cast::<u8>(), 30);
680        }
681
682        // Bias into the positive range expected by the F/VF formula, then
683        // convert seconds-since-CE-epoch into a Julian Day Number plus the
684        // algorithm's internal offset of 68569.
685        seconds += CE_EPOCH_TO_UNIX_EPOCH_SECONDS;
686        let days = (seconds / SECONDS_PER_DAY as i64) as i32;
687        let mut l = days - CE_EPOCH_TO_UNIX_EPOCH_DAYS + JD_UNIX_EPOCH + 68569;
688
689        // `n` is the number of completed 400-year Gregorian cycles since
690        // the algorithm's internal epoch; subtracting them out leaves a
691        // residue `l` in [0, 146096] (one full cycle of days).
692        let n = 4 * l / 146097;
693        l -= (146097 * n + 3) / 4;
694
695        // Within the cycle, recover the year (March-based) and the
696        // remaining day-of-year, then split that into month and day using
697        // the (80 * l / 2447) piecewise-linear month formula.
698        let mut year = 4000 * (l + 1) / 1461001;
699        l = l - 1461 * year / 4 + 31;
700        let mut month = 80 * l / 2447;
701        let day = l - 2447 * month / 80;
702
703        // Shift March-based months back to January-based, carrying into
704        // the year when month is 11 or 12 (i.e. Jan/Feb of the next year).
705        l = month / 11;
706        month = month + 2 - 12 * l;
707        year = 100 * (n - 49) + year + l;
708
709        // Time-of-day from the seconds-of-day remainder. Doing one i32
710        // divmod for the day boundary, then deriving h/m/s from the
711        // u32 remainder, keeps these off the i64 critical path.
712        let sod = (seconds - days as i64 * SECONDS_PER_DAY as i64) as u32; // 0..86400
713        let hour = sod / 3600;
714        let rem = sod % 3600;
715        let min = rem / 60;
716        let sec = rem % 60;
717
718        // SAFETY: all offsets are < 30 == BUFFER_SIZE; values are bounded
719        // (year <= 9999, the rest <= 99) so the digit math stays in u32.
720        unsafe {
721            self.write_4_at(year as u32, 0);
722            self.write_2_at(month as u32, 5);
723            self.write_2_at(day as u32, 8);
724            self.write_2_at(hour, 11);
725            self.write_2_at(min, 14);
726            self.write_2_at(sec, 17);
727        }
728
729        // Nanoseconds: figure out how many trailing groups of 3 are zero
730        // and patch the buffer accordingly. The template already contains
731        // ".000000000Z" at offsets 19..30, so when we omit the fraction
732        // entirely we just overwrite the '.' at 19 with 'Z' and stop
733        // there; the trailing template bytes are unread because `len`
734        // bounds `as_str`.
735        let final_len = if nanos == 0 {
736            // SAFETY: 19 < 30.
737            unsafe { self.write_byte_at(b'Z', 19) };
738            20
739        } else {
740            // Always materialize all 9 digits into [20..29]; the 'Z' at
741            // [29] from the template stays put. Then re-place 'Z' at 24
742            // or 27 if the trailing 6 / 3 nano digits are zero.
743            // SAFETY: offset 20 + 9 == 29 < 30.
744            unsafe { self.write_9_at(nanos as u32, 20) };
745            // Trim trailing groups of 3 zeros. We compute the trim from
746            // `nanos` directly (cheap divmods on a constant divisor) so
747            // the back end can hoist these alongside the digit writes.
748            if nanos % 1000 != 0 {
749                30
750            } else if (nanos / 1000) % 1000 != 0 {
751                // SAFETY: 26 < 30.
752                unsafe { self.write_byte_at(b'Z', 26) };
753                27
754            } else {
755                // SAFETY: 23 < 30.
756                unsafe { self.write_byte_at(b'Z', 23) };
757                24
758            }
759        };
760        self.len = final_len;
761    }
762
763    /// Encodes the nanosecond component of a timestamp.
764    ///
765    /// (Retained for callers that may want a stand-alone helper; the hot
766    /// `jsonenc_timestamp` path now writes nanos via fixed-offset stores
767    /// directly into the templated buffer instead of going through this
768    /// `self.len`-threaded routine.)
769    #[allow(dead_code)]
770    #[inline(always)]
771    fn jsonenc_nanos(&mut self, mut nanos: u32) {
772        if nanos == 0 {
773            return;
774        }
775        let mut digits = 9;
776
777        let mut q;
778        let mut r;
779        (q, r) = (nanos / 1000, nanos % 1000);
780        if r != 0 {
781            self.write_byte(b'.');
782            self.write_number(nanos, digits);
783            return;
784        }
785        nanos = q;
786        digits -= 3;
787        (q, r) = (nanos / 1000, nanos % 1000);
788        if r != 0 {
789            self.write_byte(b'.');
790            self.write_number(nanos, digits);
791            return;
792        }
793        nanos = q;
794        digits -= 3;
795        r = nanos % 1000;
796        if r != 0 {
797            self.write_byte(b'.');
798            self.write_number(nanos, digits);
799        }
800    }
801
802    /// Writes a single byte at a fixed offset (no `self.len` update).
803    ///
804    /// # Safety
805    ///
806    /// `offset` must be `< BUFFER_SIZE`.
807    #[inline(always)]
808    unsafe fn write_byte_at(&mut self, value: u8, offset: usize) {
809        debug_assert!(offset < BUFFER_SIZE);
810        unsafe {
811            self.bytes
812                .as_mut_ptr()
813                .cast::<u8>()
814                .add(offset)
815                .write(value);
816        }
817    }
818
819    /// Writes a 2-digit zero-padded number at a fixed offset.
820    ///
821    /// Uses a 200-byte ASCII pair table (`"00", "01", …, "99"`) to do the
822    /// conversion as one 2-byte load + one 2-byte store, saving the two
823    /// `div_by_10`s the obvious code would use. The table is pre-built at
824    /// compile time.
825    ///
826    /// # Safety
827    ///
828    /// `offset + 1` must be `< BUFFER_SIZE` and `value` must be `< 100`.
829    #[inline(always)]
830    unsafe fn write_2_at(&mut self, value: u32, offset: usize) {
831        debug_assert!(offset + 1 < BUFFER_SIZE && value < 100);
832        unsafe {
833            let src = PAIR_TABLE.as_ptr().add(value as usize * 2);
834            let dst = self.bytes.as_mut_ptr().cast::<u8>().add(offset);
835            ptr::copy_nonoverlapping(src, dst, 2);
836        }
837    }
838
839    /// Writes a 4-digit zero-padded number at a fixed offset.
840    ///
841    /// Splits the value into two 2-digit halves and emits each via the
842    /// `PAIR_TABLE`, so the 4 ASCII bytes are produced by two 2-byte
843    /// table loads instead of four scalar divides.
844    ///
845    /// # Safety
846    ///
847    /// `offset + 3` must be `< BUFFER_SIZE` and `value` must be `< 10_000`.
848    #[inline(always)]
849    unsafe fn write_4_at(&mut self, value: u32, offset: usize) {
850        debug_assert!(offset + 3 < BUFFER_SIZE && value < 10_000);
851        let hi = (value / 100) as usize;
852        let lo = (value % 100) as usize;
853        unsafe {
854            let dst = self.bytes.as_mut_ptr().cast::<u8>().add(offset);
855            ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(hi * 2), dst, 2);
856            ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(lo * 2), dst.add(2), 2);
857        }
858    }
859
860    /// Writes a 9-digit zero-padded number at a fixed offset.
861    ///
862    /// # Safety
863    ///
864    /// `offset + 8` must be `< BUFFER_SIZE` and `value` must be
865    /// `< 1_000_000_000`.
866    #[inline(always)]
867    unsafe fn write_9_at(&mut self, value: u32, offset: usize) {
868        debug_assert!(offset + 8 < BUFFER_SIZE && value < 1_000_000_000);
869        // Split into 1 + 2 + 2 + 2 + 2 digits so we can use the pair
870        // table for everything but the leading digit.
871        let q1 = value / 100_000_000; // top 1 digit (0..9)
872        let r1 = value % 100_000_000;
873        let q2 = r1 / 1_000_000; // next 2 digits
874        let r2 = r1 % 1_000_000;
875        let q3 = r2 / 10_000; // next 2 digits
876        let r3 = r2 % 10_000;
877        let q4 = r3 / 100; // next 2 digits
878        let q5 = r3 % 100; // last 2 digits
879        unsafe {
880            let dst = self.bytes.as_mut_ptr().cast::<u8>().add(offset);
881            dst.write(q1 as u8 | b'0');
882            ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q2 as usize * 2), dst.add(1), 2);
883            ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q3 as usize * 2), dst.add(3), 2);
884            ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q4 as usize * 2), dst.add(5), 2);
885            ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q5 as usize * 2), dst.add(7), 2);
886        }
887    }
888
889    /// Returns the formatted timestamp as a string slice.
890    ///
891    /// This method safely converts the buffer's uninitialized bytes to a UTF-8 string.
892    fn as_str(&self) -> &str {
893        // SAFETY: `self.len` is only advanced by `write_byte`/`write_number`,
894        // which write valid bytes via raw pointers, so the prefix
895        // `..self.len` is fully initialized.
896        let written = unsafe { self.bytes.get_unchecked(..self.len as usize) };
897        // SAFETY: `MaybeUninit<u8>` and `u8` have identical layout, and the
898        // bytes are initialized (above). All writes use ASCII exclusively.
899        unsafe {
900            core::str::from_utf8_unchecked(
901                &*(ptr::from_ref::<[MaybeUninit<u8>]>(written) as *const [u8]),
902            )
903        }
904    }
905}
906
907#[inline]
908#[allow(dead_code)]
909fn atoi_consume(ascii: &mut &[u8]) -> i32 {
910    let mut n: i32 = 0;
911    let (s, neg) = match ascii[0] {
912        b'-' => (&ascii[1..], true),
913        b'+' => (&ascii[1..], false),
914        _ => (*ascii, false),
915    };
916
917    let mut idx: usize = 0;
918    // Compute n as a negative number to avoid overflow
919    for c in s {
920        if !c.is_ascii_digit() {
921            break;
922        }
923        idx += 1;
924        n = n * 10 - i32::from(c & 0x0f);
925    }
926
927    *ascii = &s[idx..];
928    if neg {
929        n
930    } else {
931        -n
932    }
933}
934
935/// Decodes the seconds component from an ISO8601 timestamp string.
936///
937/// Parses the date and time portion of an ISO8601 timestamp string
938/// (format: `YYYY-MM-DDTHH:mm:ss`) and returns the Unix timestamp
939/// (seconds since 1970-01-01T00:00:00Z).
940///
941/// # Arguments
942///
943/// * `ascii` - A mutable reference to a byte slice. On success, this is
944///   advanced past the parsed seconds field.
945///
946/// # Returns
947///
948/// - `Ok(seconds)` - The number of seconds since Unix epoch
949/// - `Err(TimestampError::InvalidFormat)` - If the format is invalid
950#[inline]
951#[allow(dead_code)]
952fn decode_seconds(ascii: &mut &[u8]) -> Result<i64, TimestampError> {
953    // 1972-01-01T01:00:00
954    let year = decode_tsdigits(ascii, 4, Some(b'-'))?;
955    let mon = decode_tsdigits(ascii, 2, Some(b'-'))?;
956    let day = decode_tsdigits(ascii, 2, Some(b'T'))?;
957    let hour = decode_tsdigits(ascii, 2, Some(b':'))?;
958    let min = decode_tsdigits(ascii, 2, Some(b':'))?;
959    let sec = decode_tsdigits(ascii, 2, None)?;
960
961    Ok(jsondec_unixtime(year, mon, day, hour, min, sec))
962}
963
964/// Decodes a sequence of digits from a timestamp string.
965///
966/// Parses a fixed number of ASCII digits from the input and optionally
967/// validates a delimiter character after the digits.
968///
969/// # Arguments
970///
971/// * `ascii` - A mutable reference to a byte slice to parse from
972/// * `digits` - The number of ASCII digits to parse
973/// * `after` - An optional expected delimiter character. If provided and
974///   doesn't match the character after the digits, returns an error.
975///
976/// # Returns
977///
978/// - `Ok(value)` - The parsed integer value
979/// - `Err(TimestampError::InvalidFormat)` - If parsing fails or delimiter doesn't match
980#[inline]
981#[allow(dead_code)]
982fn decode_tsdigits(
983    ascii: &mut &[u8],
984    mut digits: usize,
985    after: Option<u8>,
986) -> Result<i32, TimestampError> {
987    if after.is_some_and(|v| v != ascii[digits]) {
988        return Err(TimestampError::InvalidFormat);
989    }
990    let mut s = &ascii[..digits];
991    let i = atoi_consume(&mut s);
992    if !s.is_empty() {
993        return Err(TimestampError::InvalidFormat);
994    }
995
996    if after.is_some() {
997        digits += 1;
998    }
999    *ascii = &ascii[digits..];
1000    Ok(i)
1001}
1002
1003/// Decodes the nanoseconds component from an ISO8601 timestamp string.
1004///
1005/// Parses the optional fractional seconds portion of a timestamp
1006/// (format: `.nnn` where n is 3, 6, or 9 digits).
1007///
1008/// # Arguments
1009///
1010/// * `ascii` - A mutable reference to a byte slice. On success, this is
1011///   advanced past the parsed nanoseconds field.
1012///
1013/// # Returns
1014///
1015/// - `Ok(nanos)` - The nanosecond value (0-999,999,999)
1016/// - `Err(TimestampError::InvalidFormat)` - If the fractional seconds format is invalid
1017///   (must be 3, 6, or 9 digits)
1018#[inline]
1019#[allow(dead_code)]
1020fn decode_nanos(ascii: &mut &[u8]) -> Result<i32, TimestampError> {
1021    let mut nanos: i32 = 0;
1022    if ascii[0] == b'.' {
1023        let mut remaining = &ascii[1..];
1024        nanos = atoi_consume(&mut remaining);
1025        let digits = ascii.len() - 1 - remaining.len();
1026        match digits {
1027            3 | 6 | 9 => {}
1028            _ => {
1029                return Err(TimestampError::InvalidFormat);
1030            }
1031        }
1032        let mut exp_lg10 = 9 - digits as i32;
1033        while exp_lg10 > 0 {
1034            exp_lg10 -= 1;
1035            nanos *= 10;
1036        }
1037        *ascii = remaining;
1038    }
1039    Ok(nanos)
1040}
1041
1042/// Calculates the number of days from a given date to the Unix epoch (1970-01-01).
1043///
1044/// # Arguments
1045///
1046/// * `y` - Year
1047/// * `m` - Month (1-12)
1048/// * `d` - Day (1-31)
1049///
1050/// # Returns
1051///
1052/// The number of days since the Unix epoch (negative for dates before 1970-01-01).
1053///
1054/// # Note
1055///
1056/// `jsondec_epochdays(1970, 1, 1) == 0`.
1057///
1058/// # Algorithm
1059///
1060/// This is the inverse direction of the Fliegel/Van Flandern conversion
1061/// used by [`Buffer::jsonenc_timestamp`]: given (Y, M, D) it returns the
1062/// signed day count since 1970-01-01 without lookup tables and (after
1063/// optimization) without branches.
1064///
1065/// The shape of the formula is due to Howard Hinnant
1066/// (<http://howardhinnant.github.io/date_algorithms.html#days_from_civil>),
1067/// with the specific power-of-two divisor variant due to Gerben Stavenga
1068/// — both surveyed in Josh Haberman's article
1069/// <https://blog.reverberate.org/2020/05/12/optimizing-date-algorithms.html>.
1070/// This is a Rust port of the upb C implementation:
1071/// <https://github.com/protocolbuffers/protobuf/blob/27421b97a0daa29e91460d377b0213f9e7be5d3f/upb/json/encode.c>.
1072///
1073/// Key tricks:
1074///
1075/// * **March-based year.** Treating March as month 1 puts the leap day at
1076///   the *end* of the year, which makes the leap-year correction depend
1077///   only on the year (not on whether the month is past February). The
1078///   `carry` term subtracts 1 from the year for January and February.
1079/// * **Year base of 4800.** Adding 4800 (a multiple of 400) ensures `y_adj`
1080///   is always non-negative for any supported input, so the unsigned
1081///   divisions below behave like floor-division.
1082/// * **`(62719 * m_adj + 769) / 2048`.** A piecewise-linear approximation
1083///   of cumulative month lengths whose divisor is a power of two, so the
1084///   compiler lowers the division to a shift. Equivalent in output to the
1085///   more familiar `(153 * m_adj + 2) / 5` from Hinnant.
1086/// * **`y/4 - y/100 + y/400`.** Standard Gregorian leap-day count.
1087/// * **`-2472632`.** Re-bases the result onto the Unix epoch
1088///   (`365*4800 + leap_days(4800) + 0` for 1970-01-01).
1089#[inline]
1090fn jsondec_epochdays(y: i32, m: i32, d: i32) -> i32 {
1091    const YEAR_BASE: u32 = 4800; // Before min year, multiple of 400.
1092
1093    let m_adj: u32 = (m - 3) as u32; // March-based month.
1094
1095    // `m_adj` underflows in u32 for January/February (m < 3), wrapping to
1096    // a value much larger than `m`; that's the signal we need to borrow a
1097    // year and shift the month into the March-based [0, 11] range.
1098    let carry: u32 = u32::from(m_adj > m as u32);
1099
1100    let adjust: u32 = if carry == 1 { 12 } else { 0 };
1101
1102    let y_adj: u32 = y as u32 + YEAR_BASE - carry;
1103    let month_days: u32 = ((adjust.wrapping_add(m_adj)) * 62719 + 769) / 2048;
1104    let leap_days: u32 = y_adj / 4 - y_adj / 100 + y_adj / 400;
1105
1106    y_adj as i32 * 365 + leap_days as i32 + month_days as i32 + (d - 1) - 2472632
1107}
1108
1109/// Converts a date/time to Unix timestamp (seconds since epoch).
1110///
1111/// Combines the given date components into a single Unix timestamp value.
1112///
1113/// # Arguments
1114///
1115/// * `y` - Year
1116/// * `m` - Month (1-12)
1117/// * `d` - Day (1-31)
1118/// * `h` - Hour (0-23)
1119/// * `min` - Minute (0-59)
1120/// * `s` - Second (0-59)
1121///
1122/// # Returns
1123///
1124/// The number of seconds since the Unix epoch (1970-01-01T00:00:00Z).
1125#[allow(clippy::many_single_char_names)]
1126fn jsondec_unixtime(y: i32, m: i32, d: i32, h: i32, min: i32, s: i32) -> i64 {
1127    i64::from(jsondec_epochdays(y, m, d)) * 86400
1128        + i64::from(h) * 3600
1129        + i64::from(min) * 60
1130        + i64::from(s)
1131}
1132
1133#[cfg(all(test, feature = "std", feature = "serde"))]
1134mod tests {
1135    use serde_test::{assert_tokens, Token};
1136    use std::time::Duration;
1137
1138    use super::*;
1139
1140    /// Tests decoding of the seconds component from an ISO8601 timestamp.
1141    #[test]
1142    fn test_decode_seconds() {
1143        let s = "2026-02-25T14:30:00Z";
1144        let input = &mut s.as_bytes();
1145        assert_eq!(decode_seconds(input).unwrap(), 1772029800);
1146        assert_eq!(input, b"Z");
1147    }
1148
1149    /// Tests that invalid characters in the seconds field are properly rejected.
1150    #[test]
1151    fn test_decode_seconds_invalid_chars() {
1152        let s = "20/6-02-25T14:30:00Z";
1153        let input = &mut s.as_bytes();
1154        assert!(decode_seconds(input).is_err());
1155
1156        let s = "20:6-02-25T14:30:00Z";
1157        let input = &mut s.as_bytes();
1158        assert!(decode_seconds(input).is_err());
1159    }
1160
1161    /// Tests decoding of the nanoseconds component from an ISO8601 timestamp.
1162    #[test]
1163    fn test_decode_nanos() {
1164        let s = ".987654321Z";
1165        let input = &mut s.as_bytes();
1166        assert_eq!(decode_nanos(input).unwrap(), 987654321);
1167        assert_eq!(input, b"Z");
1168
1169        let s = ".987654+00:00";
1170        let input = &mut s.as_bytes();
1171        assert_eq!(decode_nanos(input).unwrap(), 987654000);
1172        assert_eq!(input, b"+00:00");
1173    }
1174
1175    /// Tests that invalid characters in the nanoseconds field are properly rejected.
1176    #[test]
1177    fn test_decode_nanos_invalid_chars() {
1178        let s = ".98/654321Z";
1179        let input = &mut s.as_bytes();
1180        assert!(decode_nanos(input).is_err());
1181
1182        let s = ".98:654321Z";
1183        let input = &mut s.as_bytes();
1184        assert!(decode_nanos(input).is_err());
1185    }
1186
1187    /// Tests ASCII-to-integer conversion with optional sign.
1188    #[test]
1189    fn test_atoi_consume() {
1190        let mut ascii = "1234ABCD".as_bytes();
1191        assert_eq!(atoi_consume(&mut ascii), 1234);
1192        assert_eq!(ascii, "ABCD".as_bytes());
1193
1194        let mut ascii = "-1234ABCD".as_bytes();
1195        assert_eq!(atoi_consume(&mut ascii), -1234);
1196        assert_eq!(ascii, "ABCD".as_bytes());
1197
1198        let mut ascii = "+1234ABCD".as_bytes();
1199        assert_eq!(atoi_consume(&mut ascii), 1234);
1200        assert_eq!(ascii, "ABCD".as_bytes());
1201    }
1202
1203    /// Tests writing zero-padded numbers to the buffer.
1204    #[test]
1205    fn test_buffer_write_number() {
1206        let mut buf = Buffer::new();
1207        buf.write_byte(b'A');
1208        buf.write_number(12345, 5);
1209        buf.write_byte(b'B');
1210        assert_eq!(buf.as_str(), "A12345B");
1211    }
1212
1213    /// Tests formatting of timestamps across various dates and precisions.
1214    #[test]
1215    fn test_buffer_format() {
1216        let mut buf = Buffer::new();
1217        for ts in timestamps() {
1218            assert_eq!(buf.format(ts.0), ts.1);
1219        }
1220    }
1221
1222    /// Tests parsing of ISO8601 timestamp strings across various dates and precisions.
1223    #[test]
1224    fn test_parse() {
1225        for ts in timestamps() {
1226            assert_eq!(Timestamp::from(ts.0), Timestamp::from_str(ts.1).unwrap());
1227        }
1228    }
1229
1230    /// Tests parsing of RFC3339 numeric timezone offsets.
1231    #[test]
1232    fn test_parse_offset() {
1233        // +05:00 means local is 5h ahead of UTC; the same instant in Z form is
1234        // 5h earlier.
1235        let utc = Timestamp::from_str("2026-02-25T09:30:00Z").unwrap();
1236        let off = Timestamp::from_str("2026-02-25T14:30:00+05:00").unwrap();
1237        assert_eq!(utc, off);
1238
1239        let utc = Timestamp::from_str("2026-02-25T19:30:00Z").unwrap();
1240        let off = Timestamp::from_str("2026-02-25T14:30:00-05:00").unwrap();
1241        assert_eq!(utc, off);
1242
1243        // +00:00 == Z
1244        let utc = Timestamp::from_str("2026-02-25T14:30:00Z").unwrap();
1245        let off = Timestamp::from_str("2026-02-25T14:30:00+00:00").unwrap();
1246        assert_eq!(utc, off);
1247
1248        // Fractional seconds preserved alongside an offset.
1249        let utc = Timestamp::from_str("2026-02-25T09:30:00.123456789Z").unwrap();
1250        let off = Timestamp::from_str("2026-02-25T14:30:00.123456789+05:00").unwrap();
1251        assert_eq!(utc, off);
1252
1253        // Half-hour offset.
1254        let utc = Timestamp::from_str("2026-02-25T09:00:00Z").unwrap();
1255        let off = Timestamp::from_str("2026-02-25T14:30:00+05:30").unwrap();
1256        assert_eq!(utc, off);
1257    }
1258
1259    /// Tests rejection of malformed timezone offsets and other trailing input.
1260    #[test]
1261    fn test_parse_offset_invalid() {
1262        // Missing colon.
1263        assert!(Timestamp::from_str("2026-02-25T14:30:00+0500").is_err());
1264        // Wrong colon position.
1265        assert!(Timestamp::from_str("2026-02-25T14:30:00+05.00").is_err());
1266        // Out-of-range hours/minutes.
1267        assert!(Timestamp::from_str("2026-02-25T14:30:00+24:00").is_err());
1268        assert!(Timestamp::from_str("2026-02-25T14:30:00+05:60").is_err());
1269        // Non-digit.
1270        assert!(Timestamp::from_str("2026-02-25T14:30:00+0a:00").is_err());
1271        // Trailing garbage after a valid offset.
1272        assert!(Timestamp::from_str("2026-02-25T14:30:00+05:00X").is_err());
1273        // Trailing garbage with no terminator at all.
1274        assert!(Timestamp::from_str("2026-02-25T14:30:00X").is_err());
1275        // Empty input after the time field.
1276        assert!(Timestamp::from_str("2026-02-25T14:30:00").is_err());
1277    }
1278
1279    /// Provides a collection of test timestamps with known string representations.
1280    fn timestamps() -> [(SystemTime, &'static str); 8] {
1281        [
1282            (
1283                UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 0),
1284                "1970-01-02T01:01:01Z",
1285            ),
1286            (
1287                UNIX_EPOCH + Duration::new(253402300799, 0),
1288                "9999-12-31T23:59:59Z",
1289            ),
1290            (
1291                UNIX_EPOCH + Duration::new(1641006000, 0),
1292                "2022-01-01T03:00:00Z",
1293            ),
1294            (
1295                UNIX_EPOCH - Duration::new(2208988800, 0),
1296                "1900-01-01T00:00:00Z",
1297            ),
1298            (
1299                UNIX_EPOCH - Duration::new(86400 + (60 * 60) + 60 + 1, 987654300),
1300                "1969-12-30T22:58:58.012345700Z",
1301            ),
1302            (
1303                UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 987654300),
1304                "1970-01-02T01:01:01.987654300Z",
1305            ),
1306            (
1307                UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 987654000),
1308                "1970-01-02T01:01:01.987654Z",
1309            ),
1310            (
1311                UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 987000000),
1312                "1970-01-02T01:01:01.987Z",
1313            ),
1314        ]
1315    }
1316
1317    /// Tests interoperability with chrono datetime types.
1318    #[test]
1319    #[cfg(feature = "chrono")]
1320    fn test_chrono() {
1321        let now = chrono::Utc::now();
1322        let ts: Timestamp = now.into();
1323        let st: SystemTime = ts.into();
1324        assert_eq!(st, now.into());
1325    }
1326
1327    /// Tests serialization and deserialization with serde.
1328    #[test]
1329    #[cfg(feature = "serde")]
1330    fn test_ser_de() {
1331        let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
1332        assert_tokens(&ts, &[Token::String("2026-02-26T00:31:30.042Z")]);
1333    }
1334
1335    /// Tests deserialization via `visit_bytes` (when the deserializer hands us
1336    /// the input as raw bytes instead of a `&str`).
1337    #[test]
1338    #[cfg(feature = "serde")]
1339    fn test_de_bytes() {
1340        use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};
1341
1342        let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
1343        assert_de_tokens(&ts, &[Token::Bytes(b"2026-02-26T00:31:30.042Z")]);
1344
1345        // Non-UTF8 bytes hit the error branch in visit_bytes.
1346        assert_de_tokens_error::<Timestamp>(&[Token::Bytes(b"\xff\xfe")], "Invalid Format");
1347        // Valid UTF-8 but malformed timestamp hits visit_str's error branch.
1348        assert_de_tokens_error::<Timestamp>(&[Token::Str("not a timestamp")], "Invalid Format");
1349        // A wrong-typed token forces the deserializer to call `expecting()`
1350        // on the visitor when constructing the error message.
1351        assert_de_tokens_error::<Timestamp>(
1352            &[Token::I32(42)],
1353            "invalid type: integer `42`, expected an ISO8601 Timestamp",
1354        );
1355    }
1356
1357    /// Tests `TimestampError`'s `Display` impl for both variants.
1358    #[test]
1359    fn test_error_display() {
1360        assert_eq!(
1361            TimestampError::InvalidFormat.to_string(),
1362            "invalid timestamp format"
1363        );
1364        assert_eq!(
1365            TimestampError::OutOfRange.to_string(),
1366            "timestamp value out of range"
1367        );
1368        // Exercise the `core::error::Error` blanket impl.
1369        let e: &dyn core::error::Error = &TimestampError::InvalidFormat;
1370        assert!(e.source().is_none());
1371    }
1372
1373    /// Tests `TryFrom<(i64, u32)>` and `Timestamp::from_unix` for both
1374    /// the success and out-of-range paths.
1375    #[test]
1376    fn test_try_from_seconds_nanos() {
1377        let ts = Timestamp::try_from((0i64, 0u32)).unwrap();
1378        assert_eq!(SystemTime::from(ts), UNIX_EPOCH);
1379        assert_eq!(ts.seconds(), 0);
1380        assert_eq!(ts.subsec_nanos(), 0);
1381
1382        // A valid pre-epoch timestamp also exercises the negative-seconds
1383        // branch of `From<Timestamp> for SystemTime` with `nanos > 0`.
1384        let ts = Timestamp::from_unix(-1, 500_000_000).unwrap();
1385        assert_eq!(
1386            SystemTime::from(ts),
1387            UNIX_EPOCH - Duration::from_millis(500)
1388        );
1389        assert_eq!(ts.seconds(), -1);
1390        assert_eq!(ts.subsec_nanos(), 500_000_000);
1391
1392        assert_eq!(
1393            Timestamp::try_from((SECONDS_MIN - 1, 0)),
1394            Err(TimestampError::OutOfRange),
1395        );
1396        assert_eq!(
1397            Timestamp::try_from((SECONDS_MAX + 1, 0)),
1398            Err(TimestampError::OutOfRange),
1399        );
1400        // Out-of-range nanos are now rejected (previously silently coerced).
1401        assert_eq!(
1402            Timestamp::from_unix(0, 1_000_000_000),
1403            Err(TimestampError::OutOfRange),
1404        );
1405    }
1406
1407    /// Tests `Timestamp::now`, `Display`, and `Debug` impls.
1408    #[test]
1409    fn test_now_display_debug() {
1410        let now = Timestamp::now();
1411        // Display round-trips through the buffer formatter.
1412        let s = now.to_string();
1413        assert!(s.ends_with('Z'));
1414        assert_eq!(now, Timestamp::from_str(&s).unwrap());
1415
1416        // Debug format includes the `Timestamp { ... }` derive output.
1417        let dbg = format!("{now:?}");
1418        assert!(dbg.starts_with("Timestamp "), "got: {dbg}");
1419    }
1420
1421    /// Tests the various `From` conversions into `Timestamp`.
1422    #[test]
1423    fn test_from_conversions() {
1424        let st = UNIX_EPOCH + Duration::from_secs(1641006000);
1425        let ts_owned: Timestamp = st.into();
1426        let ts_ref: Timestamp = (&st).into();
1427        assert_eq!(ts_owned, ts_ref);
1428
1429        // From<&Timestamp> for Timestamp (the reflexive copy).
1430        let ts_copy: Timestamp = (&ts_owned).into();
1431        assert_eq!(ts_owned, ts_copy);
1432    }
1433
1434    /// Tests the `chrono::DateTime` → `Timestamp` → `chrono::DateTime`
1435    /// round-trip (covers the `Timestamp → DateTime<Utc>` impl).
1436    #[test]
1437    #[cfg(feature = "chrono")]
1438    fn test_chrono_roundtrip() {
1439        let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
1440        let dt: chrono::DateTime<chrono::Utc> = ts.into();
1441        let back: Timestamp = dt.into();
1442        assert_eq!(ts, back);
1443    }
1444
1445    /// Tests `Buffer::default` and `Clone`.
1446    #[test]
1447    fn test_buffer_default_and_clone() {
1448        let mut buf = Buffer::default();
1449        let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
1450        assert_eq!(buf.format(ts), "2026-02-26T00:31:30.042Z");
1451
1452        // `Clone` for `Buffer` deliberately returns a fresh empty buffer
1453        // rather than a true copy; verify that contract here. The lint
1454        // would have us write `buf` directly, but the whole point of the
1455        // test is to exercise the explicit `Clone` impl.
1456        #[allow(clippy::clone_on_copy)]
1457        let cloned = buf.clone();
1458        assert_eq!(cloned.len, 0);
1459    }
1460
1461    /// Tests the 3-digit nanosecond branch (covers the inner multiplication
1462    /// loop running its full 6 iterations).
1463    #[test]
1464    fn test_decode_nanos_3digit() {
1465        let s = ".042Z";
1466        let input = &mut s.as_bytes();
1467        assert_eq!(decode_nanos(input).unwrap(), 42_000_000);
1468        assert_eq!(input, b"Z");
1469    }
1470
1471    /// Pins the in-memory layout of `Buffer` so that future changes to
1472    /// `BUFFER_SIZE` or the `len` field type don't accidentally regress the
1473    /// "fits in one cacheline / four qwords" property.
1474    #[test]
1475    fn test_buffer_size() {
1476        assert_eq!(core::mem::size_of::<Buffer>(), 32);
1477    }
1478}
1479
1480/// Tests for the optional `postgres` feature. Round-trips a `Timestamp`
1481/// through `ToSql`/`FromSql` using only the in-memory `BytesMut` / `&[u8]`
1482/// representations — no live Postgres server required.
1483#[cfg(all(test, feature = "postgres"))]
1484mod postgres_tests {
1485    use bytes::BytesMut;
1486    use postgres_types::{FromSql, ToSql, Type};
1487
1488    use super::Timestamp;
1489    use crate::postgres_impl::POSTGRES_EPOCH_UNIX_SECS;
1490
1491    /// Round-trips microsecond-aligned values via `to_sql` / `from_sql` and
1492    /// verifies the byte encoding matches what we expect on the wire.
1493    #[test]
1494    fn test_postgres_roundtrip() {
1495        let cases: &[(i64, u32)] = &[
1496            // epoch
1497            (0, 0),
1498            // a representative modern instant
1499            (1_772_029_800, 0),
1500            // microsecond-aligned fractional second
1501            (1_772_029_800, 123_456_000),
1502            // year-1 lower bound (Timestamp range)
1503            (-62_135_596_800, 0),
1504            // year-9999 upper bound
1505            (253_402_300_799, 999_999_000),
1506        ];
1507        for &(secs, nanos) in cases {
1508            let ts = Timestamp::from_unix(secs, nanos).unwrap();
1509
1510            let mut buf = BytesMut::new();
1511            ts.to_sql(&Type::TIMESTAMPTZ, &mut buf).unwrap();
1512            assert_eq!(buf.len(), 8, "expected 8-byte timestamptz encoding");
1513
1514            // Verify the wire format matches what we expect: microseconds
1515            // since Postgres epoch, big-endian.
1516            let micros = (secs - POSTGRES_EPOCH_UNIX_SECS) * 1_000_000 + i64::from(nanos / 1_000);
1517            assert_eq!(
1518                buf.as_ref(),
1519                micros.to_be_bytes().as_ref(),
1520                "encoding mismatch for secs={secs} nanos={nanos}",
1521            );
1522
1523            // And round-trip back.
1524            let back = Timestamp::from_sql(&Type::TIMESTAMPTZ, &buf).unwrap();
1525            assert_eq!(
1526                back, ts,
1527                "round-trip mismatch for secs={secs} nanos={nanos}"
1528            );
1529        }
1530    }
1531
1532    /// Sub-microsecond nanoseconds are truncated on encode and re-padded on
1533    /// decode. Verify that contract rather than masking it.
1534    #[test]
1535    fn test_postgres_submicrosecond_truncation() {
1536        let ts = Timestamp::from_unix(1_772_029_800, 123_456_789).unwrap();
1537
1538        let mut buf = BytesMut::new();
1539        ts.to_sql(&Type::TIMESTAMPTZ, &mut buf).unwrap();
1540
1541        let back = Timestamp::from_sql(&Type::TIMESTAMPTZ, &buf).unwrap();
1542        // The bottom 3 digits of `nanos` are dropped by the µs encoding.
1543        assert_eq!(back.subsec_nanos(), 123_456_000);
1544        assert_ne!(back, ts);
1545    }
1546
1547    /// Pre-1970 / pre-2000 timestamps exercise the `div_euclid` /
1548    /// `rem_euclid` branches in `FromSql`. A naive `/` and `%` would put
1549    /// `nanos` negative and `secs` off-by-one.
1550    #[test]
1551    fn test_postgres_pre_epoch_decode() {
1552        // 1969-12-31T23:00:00Z — Unix seconds = -3600. Postgres epoch is
1553        // 2000-01-01 = Unix seconds 946_684_800, so microseconds since
1554        // Postgres epoch for this instant is
1555        // `(-3600 - 946_684_800) * 1_000_000 = -946_688_400_000_000`.
1556        let micros: i64 = -946_688_400_000_000;
1557        let bytes = micros.to_be_bytes();
1558
1559        let ts = Timestamp::from_sql(&Type::TIMESTAMPTZ, &bytes).unwrap();
1560        assert_eq!(ts.seconds(), -3600);
1561        assert_eq!(ts.subsec_nanos(), 0);
1562    }
1563
1564    /// `accepts` admits `TIMESTAMPTZ` only — both `ToSql::accepts` and
1565    /// `FromSql::accepts` (which both produce identical predicates here)
1566    /// agree, and reject plain `TIMESTAMP`, `TEXT`, `INT8`, etc.
1567    #[test]
1568    fn test_postgres_accepts() {
1569        // Both traits' `accepts` methods are otherwise ambiguous when
1570        // resolved as `Timestamp::accepts`, so disambiguate via UFCS.
1571        assert!(<Timestamp as ToSql>::accepts(&Type::TIMESTAMPTZ));
1572        assert!(<Timestamp as FromSql>::accepts(&Type::TIMESTAMPTZ));
1573        assert!(!<Timestamp as ToSql>::accepts(&Type::TIMESTAMP));
1574        assert!(!<Timestamp as ToSql>::accepts(&Type::TEXT));
1575        assert!(!<Timestamp as ToSql>::accepts(&Type::INT8));
1576    }
1577
1578    /// `from_sql` rejects payloads that aren't exactly 8 bytes long.
1579    #[test]
1580    fn test_postgres_from_sql_bad_length() {
1581        for bad_len in [0usize, 4, 7, 9, 16] {
1582            let bytes = vec![0u8; bad_len];
1583            let err = Timestamp::from_sql(&Type::TIMESTAMPTZ, &bytes)
1584                .expect_err(&format!("should reject length {bad_len}"));
1585            assert!(
1586                err.to_string().contains("invalid timestamptz length"),
1587                "unexpected error for length {bad_len}: {err}",
1588            );
1589        }
1590    }
1591
1592    /// Out-of-range values (year 0 or year 10000) are rejected rather
1593    /// than silently saturated. We compute the exact underflow boundary
1594    /// rather than guessing.
1595    #[test]
1596    fn test_postgres_from_sql_out_of_range() {
1597        // `Timestamp` range starts at `SECONDS_MIN = -62_135_596_800`
1598        // Unix seconds. Anything one second before that translates to a
1599        // Postgres microsecond value one µs below this:
1600        let underflow_secs = -62_135_596_800_i64 - 1;
1601        let underflow_micros = (underflow_secs - POSTGRES_EPOCH_UNIX_SECS) * 1_000_000;
1602        let bytes = underflow_micros.to_be_bytes();
1603        let err = Timestamp::from_sql(&Type::TIMESTAMPTZ, &bytes).unwrap_err();
1604        let msg = err.to_string();
1605        assert!(
1606            msg.contains("out of range") || msg.contains("OutOfRange"),
1607            "expected out-of-range error, got: {msg}",
1608        );
1609    }
1610}