#![cfg_attr(not(feature = "std"), no_std)]
#![deny(unsafe_op_in_unsafe_fn)]
#![allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::unreadable_literal,
clippy::inline_always,
clippy::items_after_statements
)]
use core::{fmt, mem::MaybeUninit, ptr, str::FromStr};
#[cfg(feature = "std")]
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(target_feature = "ssse3")]
mod sse;
#[cfg(target_feature = "neon")]
mod neon;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimestampError {
InvalidFormat,
OutOfRange,
}
impl fmt::Display for TimestampError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimestampError::InvalidFormat => write!(f, "invalid timestamp format"),
TimestampError::OutOfRange => write!(f, "timestamp value out of range"),
}
}
}
impl core::error::Error for TimestampError {}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Timestamp {
seconds: i64,
nanos: u32,
}
impl Timestamp {
#[inline]
fn new_unchecked(seconds: i64, nanos: u32) -> Self {
debug_assert!(nanos < 1_000_000_000);
Self { seconds, nanos }
}
pub fn from_unix(seconds: i64, nanos: u32) -> Result<Self, TimestampError> {
if !(SECONDS_MIN..=SECONDS_MAX).contains(&seconds) || nanos >= 1_000_000_000 {
return Err(TimestampError::OutOfRange);
}
Ok(Self::new_unchecked(seconds, nanos))
}
#[inline]
#[must_use]
pub fn seconds(&self) -> i64 {
self.seconds
}
#[inline]
#[must_use]
pub fn subsec_nanos(&self) -> u32 {
self.nanos
}
#[cfg(feature = "std")]
#[must_use]
pub fn now() -> Self {
Self::from(SystemTime::now())
}
}
#[cfg(feature = "std")]
impl From<SystemTime> for Timestamp {
#[inline]
fn from(value: SystemTime) -> Self {
let (seconds, nanos) = match value.duration_since(UNIX_EPOCH) {
Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
Err(e) => {
let dur_before = e.duration();
let secs_before = -(dur_before.as_secs() as i64);
let nanos_before = dur_before.subsec_nanos();
if nanos_before > 0 {
(secs_before - 1, 1_000_000_000 - nanos_before)
} else {
(secs_before, 0)
}
}
};
if seconds < SECONDS_MIN {
Self::new_unchecked(SECONDS_MIN, 0)
} else if seconds > SECONDS_MAX {
Self::new_unchecked(SECONDS_MAX, 0)
} else {
Self::new_unchecked(seconds, nanos)
}
}
}
#[cfg(feature = "std")]
impl From<&SystemTime> for Timestamp {
#[inline]
fn from(value: &SystemTime) -> Self {
Self::from(*value)
}
}
#[cfg(feature = "std")]
impl From<Timestamp> for SystemTime {
#[inline]
fn from(value: Timestamp) -> Self {
if value.seconds >= 0 {
UNIX_EPOCH + Duration::new(value.seconds as u64, value.nanos)
} else {
let (mag_secs, mag_nanos) = if value.nanos == 0 {
((-value.seconds) as u64, 0)
} else {
((-value.seconds - 1) as u64, 1_000_000_000 - value.nanos)
};
UNIX_EPOCH - Duration::new(mag_secs, mag_nanos)
}
}
}
impl From<&Timestamp> for Timestamp {
#[inline]
fn from(value: &Timestamp) -> Self {
*value
}
}
#[cfg(feature = "chrono")]
impl<Tz: chrono::TimeZone> From<chrono::DateTime<Tz>> for Timestamp {
fn from(value: chrono::DateTime<Tz>) -> Self {
let seconds = value.timestamp();
let nanos = value.timestamp_subsec_nanos();
if seconds < SECONDS_MIN {
Self::new_unchecked(SECONDS_MIN, 0)
} else if seconds > SECONDS_MAX {
Self::new_unchecked(SECONDS_MAX, 0)
} else {
Self::new_unchecked(seconds, nanos)
}
}
}
#[cfg(feature = "chrono")]
impl From<Timestamp> for chrono::DateTime<chrono::Utc> {
fn from(value: Timestamp) -> Self {
chrono::DateTime::<chrono::Utc>::from_timestamp(value.seconds, value.nanos)
.expect("Timestamp out of range for chrono::DateTime")
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for Timestamp {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde_core::Serializer,
{
let mut buf = Buffer::new();
serializer.serialize_str(buf.format(self))
}
}
#[cfg(feature = "serde")]
impl<'de> serde_core::Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde_core::Deserializer<'de>,
{
struct TsVisitor;
impl serde_core::de::Visitor<'_> for TsVisitor {
type Value = Timestamp;
#[inline]
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an ISO8601 Timestamp")
}
#[inline]
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde_core::de::Error,
{
Timestamp::from_str(v).map_err(|_e| E::custom("Invalid Format"))
}
#[inline]
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde_core::de::Error,
{
let s = core::str::from_utf8(v).map_err(|_| E::custom("Invalid Format"))?;
self.visit_str(s)
}
}
deserializer.deserialize_str(TsVisitor)
}
}
impl TryFrom<(i64, u32)> for Timestamp {
type Error = TimestampError;
#[inline]
fn try_from(value: (i64, u32)) -> Result<Self, Self::Error> {
Self::from_unix(value.0, value.1)
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buf = Buffer::new();
write!(f, "{}", buf.format(self))
}
}
impl FromStr for Timestamp {
type Err = TimestampError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut ascii = s.as_bytes();
#[cfg(target_feature = "ssse3")]
let (seconds, nanos) = unsafe {
(
sse::decode_seconds(&mut ascii)?,
sse::decode_nanos(&mut ascii)?,
)
};
#[cfg(target_feature = "neon")]
let (seconds, nanos) = unsafe {
(
neon::decode_seconds(&mut ascii)?,
neon::decode_nanos(&mut ascii)?,
)
};
#[cfg(not(any(target_feature = "ssse3", target_feature = "neon")))]
let (seconds, nanos) = (decode_seconds(&mut ascii)?, decode_nanos(&mut ascii)?);
let offset = match ascii.first() {
Some(b'Z') => 0,
Some(&c @ (b'+' | b'-')) => decode_offset(ascii, c)?,
_ => return Err(TimestampError::InvalidFormat),
};
Ok(Self::new_unchecked(seconds + offset, nanos as u32))
}
}
#[inline]
fn decode_offset(ascii: &[u8], sign: u8) -> Result<i64, TimestampError> {
if ascii.len() != 6 || ascii[3] != b':' {
return Err(TimestampError::InvalidFormat);
}
let h10 = ascii[1].wrapping_sub(b'0');
let h1 = ascii[2].wrapping_sub(b'0');
let m10 = ascii[4].wrapping_sub(b'0');
let m1 = ascii[5].wrapping_sub(b'0');
if (h10 | h1 | m10 | m1) > 9 {
return Err(TimestampError::InvalidFormat);
}
let hours = h10 as i64 * 10 + h1 as i64;
let mins = m10 as i64 * 10 + m1 as i64;
if hours > 23 || mins > 59 {
return Err(TimestampError::InvalidFormat);
}
let magnitude = hours * 3600 + mins * 60;
Ok(if sign == b'+' { -magnitude } else { magnitude })
}
const BUFFER_SIZE: usize = 30; const SECONDS_MIN: i64 = -62135596800;
const SECONDS_MAX: i64 = 253402300799;
const PAIR_TABLE: [u8; 200] = {
let mut t = [0u8; 200];
let mut i = 0;
while i < 100 {
t[i * 2] = b'0' + (i / 10) as u8;
t[i * 2 + 1] = b'0' + (i % 10) as u8;
i += 1;
}
t
};
pub struct Buffer {
bytes: [MaybeUninit<u8>; BUFFER_SIZE],
len: u16,
}
impl Default for Buffer {
#[inline]
fn default() -> Buffer {
Buffer::new()
}
}
impl Copy for Buffer {}
#[allow(clippy::non_canonical_clone_impl, clippy::expl_impl_clone_on_copy)]
impl Clone for Buffer {
#[inline]
fn clone(&self) -> Self {
Buffer::new()
}
}
impl Buffer {
#[inline]
#[must_use]
pub fn new() -> Buffer {
let bytes = [MaybeUninit::<u8>::uninit(); BUFFER_SIZE];
Buffer { bytes, len: 0 }
}
pub fn format<T: Into<Timestamp>>(&mut self, timestamp: T) -> &str {
let timestamp = timestamp.into();
self.reset();
let seconds = timestamp.seconds;
let nanos = timestamp.nanos as i32;
debug_assert!((SECONDS_MIN..=SECONDS_MAX).contains(&seconds));
debug_assert!((0..1_000_000_000).contains(&nanos));
self.jsonenc_timestamp(seconds, nanos);
self.as_str()
}
#[inline]
fn reset(&mut self) {
self.len = 0;
}
#[inline(always)]
fn write_byte(&mut self, value: u8) {
let len = self.len as usize;
debug_assert!(len < BUFFER_SIZE, "Buffer overflow in write_byte");
unsafe {
let end = self.bytes.as_mut_ptr().cast::<u8>().add(len);
ptr::write(end, value);
}
self.len = (len + 1) as u16;
}
#[inline(always)]
fn write_number(&mut self, mut value: u32, mut digits: usize) {
let len = self.len as usize + digits;
debug_assert!(len <= BUFFER_SIZE, "Buffer overflow in write_number");
if BUFFER_SIZE >= len {
unsafe {
self.len = len as u16;
let mut ptr = self.bytes.as_mut_ptr().cast::<u8>().add(len - 1);
while digits >= 2 {
let d1;
(value, d1) = (value / 100, value % 100);
let (a, b) = (d1 / 10, d1 % 10);
digits -= 1;
ptr.write(b as u8 | b'0');
ptr = ptr.sub(1);
digits -= 1;
ptr.write(a as u8 | b'0');
ptr = ptr.sub(1);
}
if digits == 1 {
ptr.write(value as u8 | b'0');
}
}
}
}
#[inline(always)]
fn jsonenc_timestamp(&mut self, mut seconds: i64, nanos: i32) {
const SECONDS_PER_DAY: i32 = 86400;
const CE_EPOCH_TO_UNIX_EPOCH_DAYS: i32 = 719_162;
const CE_EPOCH_TO_UNIX_EPOCH_SECONDS: i64 =
CE_EPOCH_TO_UNIX_EPOCH_DAYS as i64 * SECONDS_PER_DAY as i64;
const JD_UNIX_EPOCH: i32 = 2_440_588;
const TEMPLATE: [u8; 30] = *b"____-__-__T__:__:__.000000000Z";
unsafe {
ptr::copy_nonoverlapping(TEMPLATE.as_ptr(), self.bytes.as_mut_ptr().cast::<u8>(), 30);
}
seconds += CE_EPOCH_TO_UNIX_EPOCH_SECONDS;
let days = (seconds / SECONDS_PER_DAY as i64) as i32;
let mut l = days - CE_EPOCH_TO_UNIX_EPOCH_DAYS + JD_UNIX_EPOCH + 68569;
let n = 4 * l / 146097;
l -= (146097 * n + 3) / 4;
let mut year = 4000 * (l + 1) / 1461001;
l = l - 1461 * year / 4 + 31;
let mut month = 80 * l / 2447;
let day = l - 2447 * month / 80;
l = month / 11;
month = month + 2 - 12 * l;
year = 100 * (n - 49) + year + l;
let sod = (seconds - days as i64 * SECONDS_PER_DAY as i64) as u32; let hour = sod / 3600;
let rem = sod % 3600;
let min = rem / 60;
let sec = rem % 60;
unsafe {
self.write_4_at(year as u32, 0);
self.write_2_at(month as u32, 5);
self.write_2_at(day as u32, 8);
self.write_2_at(hour, 11);
self.write_2_at(min, 14);
self.write_2_at(sec, 17);
}
let final_len = if nanos == 0 {
unsafe { self.write_byte_at(b'Z', 19) };
20
} else {
unsafe { self.write_9_at(nanos as u32, 20) };
if nanos % 1000 != 0 {
30
} else if (nanos / 1000) % 1000 != 0 {
unsafe { self.write_byte_at(b'Z', 26) };
27
} else {
unsafe { self.write_byte_at(b'Z', 23) };
24
}
};
self.len = final_len;
}
#[allow(dead_code)]
#[inline(always)]
fn jsonenc_nanos(&mut self, mut nanos: u32) {
if nanos == 0 {
return;
}
let mut digits = 9;
let mut q;
let mut r;
(q, r) = (nanos / 1000, nanos % 1000);
if r != 0 {
self.write_byte(b'.');
self.write_number(nanos, digits);
return;
}
nanos = q;
digits -= 3;
(q, r) = (nanos / 1000, nanos % 1000);
if r != 0 {
self.write_byte(b'.');
self.write_number(nanos, digits);
return;
}
nanos = q;
digits -= 3;
r = nanos % 1000;
if r != 0 {
self.write_byte(b'.');
self.write_number(nanos, digits);
}
}
#[inline(always)]
unsafe fn write_byte_at(&mut self, value: u8, offset: usize) {
debug_assert!(offset < BUFFER_SIZE);
unsafe {
self.bytes
.as_mut_ptr()
.cast::<u8>()
.add(offset)
.write(value);
}
}
#[inline(always)]
unsafe fn write_2_at(&mut self, value: u32, offset: usize) {
debug_assert!(offset + 1 < BUFFER_SIZE && value < 100);
unsafe {
let src = PAIR_TABLE.as_ptr().add(value as usize * 2);
let dst = self.bytes.as_mut_ptr().cast::<u8>().add(offset);
ptr::copy_nonoverlapping(src, dst, 2);
}
}
#[inline(always)]
unsafe fn write_4_at(&mut self, value: u32, offset: usize) {
debug_assert!(offset + 3 < BUFFER_SIZE && value < 10_000);
let hi = (value / 100) as usize;
let lo = (value % 100) as usize;
unsafe {
let dst = self.bytes.as_mut_ptr().cast::<u8>().add(offset);
ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(hi * 2), dst, 2);
ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(lo * 2), dst.add(2), 2);
}
}
#[inline(always)]
unsafe fn write_9_at(&mut self, value: u32, offset: usize) {
debug_assert!(offset + 8 < BUFFER_SIZE && value < 1_000_000_000);
let q1 = value / 100_000_000; let r1 = value % 100_000_000;
let q2 = r1 / 1_000_000; let r2 = r1 % 1_000_000;
let q3 = r2 / 10_000; let r3 = r2 % 10_000;
let q4 = r3 / 100; let q5 = r3 % 100; unsafe {
let dst = self.bytes.as_mut_ptr().cast::<u8>().add(offset);
dst.write(q1 as u8 | b'0');
ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q2 as usize * 2), dst.add(1), 2);
ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q3 as usize * 2), dst.add(3), 2);
ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q4 as usize * 2), dst.add(5), 2);
ptr::copy_nonoverlapping(PAIR_TABLE.as_ptr().add(q5 as usize * 2), dst.add(7), 2);
}
}
fn as_str(&self) -> &str {
let written = unsafe { self.bytes.get_unchecked(..self.len as usize) };
unsafe {
core::str::from_utf8_unchecked(
&*(ptr::from_ref::<[MaybeUninit<u8>]>(written) as *const [u8]),
)
}
}
}
#[inline]
#[allow(dead_code)]
fn atoi_consume(ascii: &mut &[u8]) -> i32 {
let mut n: i32 = 0;
let (s, neg) = match ascii[0] {
b'-' => (&ascii[1..], true),
b'+' => (&ascii[1..], false),
_ => (*ascii, false),
};
let mut idx: usize = 0;
for c in s {
if !c.is_ascii_digit() {
break;
}
idx += 1;
n = n * 10 - i32::from(c & 0x0f);
}
*ascii = &s[idx..];
if neg {
n
} else {
-n
}
}
#[inline]
#[allow(dead_code)]
fn decode_seconds(ascii: &mut &[u8]) -> Result<i64, TimestampError> {
let year = decode_tsdigits(ascii, 4, Some(b'-'))?;
let mon = decode_tsdigits(ascii, 2, Some(b'-'))?;
let day = decode_tsdigits(ascii, 2, Some(b'T'))?;
let hour = decode_tsdigits(ascii, 2, Some(b':'))?;
let min = decode_tsdigits(ascii, 2, Some(b':'))?;
let sec = decode_tsdigits(ascii, 2, None)?;
Ok(jsondec_unixtime(year, mon, day, hour, min, sec))
}
#[inline]
#[allow(dead_code)]
fn decode_tsdigits(
ascii: &mut &[u8],
mut digits: usize,
after: Option<u8>,
) -> Result<i32, TimestampError> {
if after.is_some_and(|v| v != ascii[digits]) {
return Err(TimestampError::InvalidFormat);
}
let mut s = &ascii[..digits];
let i = atoi_consume(&mut s);
if !s.is_empty() {
return Err(TimestampError::InvalidFormat);
}
if after.is_some() {
digits += 1;
}
*ascii = &ascii[digits..];
Ok(i)
}
#[inline]
#[allow(dead_code)]
fn decode_nanos(ascii: &mut &[u8]) -> Result<i32, TimestampError> {
let mut nanos: i32 = 0;
if ascii[0] == b'.' {
let mut remaining = &ascii[1..];
nanos = atoi_consume(&mut remaining);
let digits = ascii.len() - 1 - remaining.len();
match digits {
3 | 6 | 9 => {}
_ => {
return Err(TimestampError::InvalidFormat);
}
}
let mut exp_lg10 = 9 - digits as i32;
while exp_lg10 > 0 {
exp_lg10 -= 1;
nanos *= 10;
}
*ascii = remaining;
}
Ok(nanos)
}
#[inline]
fn jsondec_epochdays(y: i32, m: i32, d: i32) -> i32 {
const YEAR_BASE: u32 = 4800;
let m_adj: u32 = (m - 3) as u32;
let carry: u32 = u32::from(m_adj > m as u32);
let adjust: u32 = if carry == 1 { 12 } else { 0 };
let y_adj: u32 = y as u32 + YEAR_BASE - carry;
let month_days: u32 = ((adjust.wrapping_add(m_adj)) * 62719 + 769) / 2048;
let leap_days: u32 = y_adj / 4 - y_adj / 100 + y_adj / 400;
y_adj as i32 * 365 + leap_days as i32 + month_days as i32 + (d - 1) - 2472632
}
#[allow(clippy::many_single_char_names)]
fn jsondec_unixtime(y: i32, m: i32, d: i32, h: i32, min: i32, s: i32) -> i64 {
i64::from(jsondec_epochdays(y, m, d)) * 86400
+ i64::from(h) * 3600
+ i64::from(min) * 60
+ i64::from(s)
}
#[cfg(all(test, feature = "std", feature = "serde"))]
mod tests {
use serde_test::{assert_tokens, Token};
use std::time::Duration;
use super::*;
#[test]
fn test_decode_seconds() {
let s = "2026-02-25T14:30:00Z";
let input = &mut s.as_bytes();
assert_eq!(decode_seconds(input).unwrap(), 1772029800);
assert_eq!(input, b"Z");
}
#[test]
fn test_decode_seconds_invalid_chars() {
let s = "20/6-02-25T14:30:00Z";
let input = &mut s.as_bytes();
assert!(decode_seconds(input).is_err());
let s = "20:6-02-25T14:30:00Z";
let input = &mut s.as_bytes();
assert!(decode_seconds(input).is_err());
}
#[test]
fn test_decode_nanos() {
let s = ".987654321Z";
let input = &mut s.as_bytes();
assert_eq!(decode_nanos(input).unwrap(), 987654321);
assert_eq!(input, b"Z");
let s = ".987654+00:00";
let input = &mut s.as_bytes();
assert_eq!(decode_nanos(input).unwrap(), 987654000);
assert_eq!(input, b"+00:00");
}
#[test]
fn test_decode_nanos_invalid_chars() {
let s = ".98/654321Z";
let input = &mut s.as_bytes();
assert!(decode_nanos(input).is_err());
let s = ".98:654321Z";
let input = &mut s.as_bytes();
assert!(decode_nanos(input).is_err());
}
#[test]
fn test_atoi_consume() {
let mut ascii = "1234ABCD".as_bytes();
assert_eq!(atoi_consume(&mut ascii), 1234);
assert_eq!(ascii, "ABCD".as_bytes());
let mut ascii = "-1234ABCD".as_bytes();
assert_eq!(atoi_consume(&mut ascii), -1234);
assert_eq!(ascii, "ABCD".as_bytes());
let mut ascii = "+1234ABCD".as_bytes();
assert_eq!(atoi_consume(&mut ascii), 1234);
assert_eq!(ascii, "ABCD".as_bytes());
}
#[test]
fn test_buffer_write_number() {
let mut buf = Buffer::new();
buf.write_byte(b'A');
buf.write_number(12345, 5);
buf.write_byte(b'B');
assert_eq!(buf.as_str(), "A12345B");
}
#[test]
fn test_buffer_format() {
let mut buf = Buffer::new();
for ts in timestamps() {
assert_eq!(buf.format(ts.0), ts.1);
}
}
#[test]
fn test_parse() {
for ts in timestamps() {
assert_eq!(Timestamp::from(ts.0), Timestamp::from_str(ts.1).unwrap());
}
}
#[test]
fn test_parse_offset() {
let utc = Timestamp::from_str("2026-02-25T09:30:00Z").unwrap();
let off = Timestamp::from_str("2026-02-25T14:30:00+05:00").unwrap();
assert_eq!(utc, off);
let utc = Timestamp::from_str("2026-02-25T19:30:00Z").unwrap();
let off = Timestamp::from_str("2026-02-25T14:30:00-05:00").unwrap();
assert_eq!(utc, off);
let utc = Timestamp::from_str("2026-02-25T14:30:00Z").unwrap();
let off = Timestamp::from_str("2026-02-25T14:30:00+00:00").unwrap();
assert_eq!(utc, off);
let utc = Timestamp::from_str("2026-02-25T09:30:00.123456789Z").unwrap();
let off = Timestamp::from_str("2026-02-25T14:30:00.123456789+05:00").unwrap();
assert_eq!(utc, off);
let utc = Timestamp::from_str("2026-02-25T09:00:00Z").unwrap();
let off = Timestamp::from_str("2026-02-25T14:30:00+05:30").unwrap();
assert_eq!(utc, off);
}
#[test]
fn test_parse_offset_invalid() {
assert!(Timestamp::from_str("2026-02-25T14:30:00+0500").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00+05.00").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00+24:00").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00+05:60").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00+0a:00").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00+05:00X").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00X").is_err());
assert!(Timestamp::from_str("2026-02-25T14:30:00").is_err());
}
fn timestamps() -> [(SystemTime, &'static str); 8] {
[
(
UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 0),
"1970-01-02T01:01:01Z",
),
(
UNIX_EPOCH + Duration::new(253402300799, 0),
"9999-12-31T23:59:59Z",
),
(
UNIX_EPOCH + Duration::new(1641006000, 0),
"2022-01-01T03:00:00Z",
),
(
UNIX_EPOCH - Duration::new(2208988800, 0),
"1900-01-01T00:00:00Z",
),
(
UNIX_EPOCH - Duration::new(86400 + (60 * 60) + 60 + 1, 987654300),
"1969-12-30T22:58:58.012345700Z",
),
(
UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 987654300),
"1970-01-02T01:01:01.987654300Z",
),
(
UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 987654000),
"1970-01-02T01:01:01.987654Z",
),
(
UNIX_EPOCH + Duration::new(86400 + (60 * 60) + 60 + 1, 987000000),
"1970-01-02T01:01:01.987Z",
),
]
}
#[test]
#[cfg(feature = "chrono")]
fn test_chrono() {
let now = chrono::Utc::now();
let ts: Timestamp = now.into();
let st: SystemTime = ts.into();
assert_eq!(st, now.into());
}
#[test]
#[cfg(feature = "serde")]
fn test_ser_de() {
let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
assert_tokens(&ts, &[Token::String("2026-02-26T00:31:30.042Z")]);
}
#[test]
#[cfg(feature = "serde")]
fn test_de_bytes() {
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};
let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
assert_de_tokens(&ts, &[Token::Bytes(b"2026-02-26T00:31:30.042Z")]);
assert_de_tokens_error::<Timestamp>(&[Token::Bytes(b"\xff\xfe")], "Invalid Format");
assert_de_tokens_error::<Timestamp>(&[Token::Str("not a timestamp")], "Invalid Format");
assert_de_tokens_error::<Timestamp>(
&[Token::I32(42)],
"invalid type: integer `42`, expected an ISO8601 Timestamp",
);
}
#[test]
fn test_error_display() {
assert_eq!(
TimestampError::InvalidFormat.to_string(),
"invalid timestamp format"
);
assert_eq!(
TimestampError::OutOfRange.to_string(),
"timestamp value out of range"
);
let e: &dyn core::error::Error = &TimestampError::InvalidFormat;
assert!(e.source().is_none());
}
#[test]
fn test_try_from_seconds_nanos() {
let ts = Timestamp::try_from((0i64, 0u32)).unwrap();
assert_eq!(SystemTime::from(ts), UNIX_EPOCH);
assert_eq!(ts.seconds(), 0);
assert_eq!(ts.subsec_nanos(), 0);
let ts = Timestamp::from_unix(-1, 500_000_000).unwrap();
assert_eq!(
SystemTime::from(ts),
UNIX_EPOCH - Duration::from_millis(500)
);
assert_eq!(ts.seconds(), -1);
assert_eq!(ts.subsec_nanos(), 500_000_000);
assert_eq!(
Timestamp::try_from((SECONDS_MIN - 1, 0)),
Err(TimestampError::OutOfRange),
);
assert_eq!(
Timestamp::try_from((SECONDS_MAX + 1, 0)),
Err(TimestampError::OutOfRange),
);
assert_eq!(
Timestamp::from_unix(0, 1_000_000_000),
Err(TimestampError::OutOfRange),
);
}
#[test]
fn test_now_display_debug() {
let now = Timestamp::now();
let s = now.to_string();
assert!(s.ends_with('Z'));
assert_eq!(now, Timestamp::from_str(&s).unwrap());
let dbg = format!("{now:?}");
assert!(dbg.starts_with("Timestamp "), "got: {dbg}");
}
#[test]
fn test_from_conversions() {
let st = UNIX_EPOCH + Duration::from_secs(1641006000);
let ts_owned: Timestamp = st.into();
let ts_ref: Timestamp = (&st).into();
assert_eq!(ts_owned, ts_ref);
let ts_copy: Timestamp = (&ts_owned).into();
assert_eq!(ts_owned, ts_copy);
}
#[test]
#[cfg(feature = "chrono")]
fn test_chrono_roundtrip() {
let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
let dt: chrono::DateTime<chrono::Utc> = ts.into();
let back: Timestamp = dt.into();
assert_eq!(ts, back);
}
#[test]
fn test_buffer_default_and_clone() {
let mut buf = Buffer::default();
let ts: Timestamp = "2026-02-26T00:31:30.042Z".parse().unwrap();
assert_eq!(buf.format(ts), "2026-02-26T00:31:30.042Z");
#[allow(clippy::clone_on_copy)]
let cloned = buf.clone();
assert_eq!(cloned.len, 0);
}
#[test]
fn test_decode_nanos_3digit() {
let s = ".042Z";
let input = &mut s.as_bytes();
assert_eq!(decode_nanos(input).unwrap(), 42_000_000);
assert_eq!(input, b"Z");
}
#[test]
fn test_buffer_size() {
assert_eq!(core::mem::size_of::<Buffer>(), 32);
}
}