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}