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