Skip to main content

nautilus_core/
nanos.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A `UnixNanos` type for working with timestamps in nanoseconds since the UNIX epoch.
17//!
18//! This module provides a strongly-typed representation of timestamps as nanoseconds
19//! since the UNIX epoch (January 1, 1970, 00:00:00 UTC). The `UnixNanos` type offers
20//! conversion utilities, arithmetic operations, and comparison methods.
21//!
22//! # Features
23//!
24//! - Zero-cost abstraction with appropriate operator implementations.
25//! - Conversion to/from `Timestamp`.
26//! - RFC 3339 string formatting.
27//! - Duration calculations.
28//! - Flexible parsing and serialization.
29//!
30//! # Parsing and Serialization
31//!
32//! `UnixNanos` can be created from and serialized to various formats:
33//!
34//! - Integer values are interpreted as nanoseconds since the UNIX epoch.
35//! - Floating-point values are interpreted as seconds since the UNIX epoch (converted to nanoseconds
36//!   using truncation, not rounding, for consistency with [`secs_to_nanos`](crate::datetime::secs_to_nanos)).
37//! - String values may be:
38//!   - A numeric string (interpreted as nanoseconds).
39//!   - A floating-point string (interpreted as seconds, converted to nanoseconds).
40//!   - An RFC 3339 formatted timestamp (ISO 8601 with timezone).
41//!   - A simple date string in YYYY-MM-DD format (interpreted as midnight UTC on that date).
42//!
43//! # Limitations
44//!
45//! - Negative timestamps are invalid and will result in an error.
46//! - Arithmetic operations will panic on overflow/underflow rather than wrapping.
47//! - The `as_i64()` method will panic for timestamps beyond approximately year 2262
48//!   (when nanoseconds exceed `i64::MAX`).
49
50use std::{
51    cmp::Ordering,
52    fmt::Display,
53    ops::{Add, AddAssign, Deref, Sub, SubAssign},
54    str::FromStr,
55    time::SystemTime,
56};
57
58use jiff::{Timestamp, civil::Date, tz::Offset};
59use serde::{
60    Deserialize, Deserializer, Serialize,
61    de::{self, Visitor},
62};
63
64use crate::datetime::{
65    NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND,
66    U64_UPPER_BOUND_F64,
67};
68
69/// Represents a duration in nanoseconds.
70pub type DurationNanos = u64;
71
72/// Represents a timestamp in nanoseconds since the UNIX epoch.
73#[repr(C)]
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
75pub struct UnixNanos(u64);
76
77impl UnixNanos {
78    /// Creates a new [`UnixNanos`] instance.
79    #[must_use]
80    pub const fn new(value: u64) -> Self {
81        Self(value)
82    }
83
84    /// Creates a new [`UnixNanos`] instance with the maximum valid value.
85    #[must_use]
86    pub const fn max() -> Self {
87        Self(u64::MAX)
88    }
89
90    /// Returns `true` if the value of this instance is zero.
91    #[must_use]
92    pub const fn is_zero(&self) -> bool {
93        self.0 == 0
94    }
95
96    /// Returns the underlying value as `u64`.
97    #[must_use]
98    pub const fn as_u64(&self) -> u64 {
99        self.0
100    }
101
102    /// Returns the timestamp as seconds, truncating sub-second precision.
103    #[must_use]
104    pub const fn as_seconds(&self) -> u64 {
105        self.0 / NANOSECONDS_IN_SECOND
106    }
107
108    /// Returns the timestamp as milliseconds, truncating sub-millisecond precision.
109    #[must_use]
110    pub const fn as_millis(&self) -> u64 {
111        self.0 / NANOSECONDS_IN_MILLISECOND
112    }
113
114    /// Returns the timestamp as microseconds, truncating sub-microsecond precision.
115    #[must_use]
116    pub const fn as_micros(&self) -> u64 {
117        self.0 / NANOSECONDS_IN_MICROSECOND
118    }
119
120    /// Creates a new [`UnixNanos`] from a second timestamp.
121    ///
122    /// # Panics
123    ///
124    /// Panics if the result overflows `u64`.
125    #[must_use]
126    pub const fn from_seconds(seconds: u64) -> Self {
127        match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
128            Some(nanos) => Self(nanos),
129            None => panic!("UnixNanos overflow in from_seconds"),
130        }
131    }
132
133    /// Creates a new [`UnixNanos`] from a millisecond timestamp.
134    ///
135    /// # Panics
136    ///
137    /// Panics if the result overflows `u64`.
138    #[must_use]
139    pub const fn from_millis(millis: u64) -> Self {
140        match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
141            Some(nanos) => Self(nanos),
142            None => panic!("UnixNanos overflow in from_millis"),
143        }
144    }
145
146    /// Creates a new [`UnixNanos`] from a signed millisecond timestamp.
147    ///
148    /// Returns `None` if `millis` is negative or the result overflows `u64`.
149    #[must_use]
150    pub const fn from_millis_checked(millis: i64) -> Option<Self> {
151        Self::from_units_checked(millis, NANOSECONDS_IN_MILLISECOND)
152    }
153
154    /// Creates a new [`UnixNanos`] from a microsecond timestamp.
155    ///
156    /// # Panics
157    ///
158    /// Panics if the result overflows `u64`.
159    #[must_use]
160    pub const fn from_micros(micros: u64) -> Self {
161        match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
162            Some(nanos) => Self(nanos),
163            None => panic!("UnixNanos overflow in from_micros"),
164        }
165    }
166
167    /// Creates a new [`UnixNanos`] from a signed microsecond timestamp.
168    ///
169    /// Returns `None` if `micros` is negative or the result overflows `u64`.
170    #[must_use]
171    pub const fn from_micros_checked(micros: i64) -> Option<Self> {
172        Self::from_units_checked(micros, NANOSECONDS_IN_MICROSECOND)
173    }
174
175    const fn from_units_checked(value: i64, nanos_per_unit: u64) -> Option<Self> {
176        if value < 0 {
177            return None;
178        }
179
180        match value.cast_unsigned().checked_mul(nanos_per_unit) {
181            Some(nanos) => Some(Self(nanos)),
182            None => None,
183        }
184    }
185
186    /// Returns the underlying value as `i64`.
187    ///
188    /// # Panics
189    ///
190    /// Panics if the value exceeds `i64::MAX` (approximately year 2262).
191    #[must_use]
192    pub const fn as_i64(&self) -> i64 {
193        assert!(
194            self.0 <= i64::MAX.cast_unsigned(),
195            "UnixNanos value exceeds i64::MAX"
196        );
197        self.0.cast_signed()
198    }
199
200    /// Returns the underlying value as `f64`.
201    #[must_use]
202    #[expect(
203        clippy::cast_precision_loss,
204        reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
205    )]
206    pub const fn as_f64(&self) -> f64 {
207        self.0 as f64
208    }
209
210    /// Converts the underlying value to a datetime (UTC).
211    ///
212    /// # Panics
213    ///
214    /// Panics if Jiff's supported timestamp range no longer includes all `u64` nanosecond values.
215    #[must_use]
216    pub fn to_datetime_utc(&self) -> Timestamp {
217        Timestamp::from_nanosecond(i128::from(self.0))
218            .expect("UnixNanos is within Jiff's timestamp range")
219    }
220
221    /// Converts the underlying value to an ISO 8601 (RFC 3339) string.
222    #[must_use]
223    pub fn to_rfc3339(&self) -> String {
224        let datetime = self.to_datetime_utc();
225        let display = datetime.display_with_offset(Offset::UTC);
226
227        match datetime.subsec_nanosecond() {
228            0 => format!("{display:.0}"),
229            nanos if nanos % 1_000_000 == 0 => format!("{display:.3}"),
230            nanos if nanos % 1_000 == 0 => format!("{display:.6}"),
231            _ => format!("{display:.9}"),
232        }
233    }
234
235    /// Calculates the duration in nanoseconds since another [`UnixNanos`] instance.
236    ///
237    /// Returns `Some(duration)` if `self` is later than `other`, otherwise `None` if `other` is
238    /// greater than `self` (indicating a negative duration is not possible with `DurationNanos`).
239    #[must_use]
240    pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
241        self.0.checked_sub(other.0)
242    }
243
244    fn parse_string(s: &str) -> Result<Self, String> {
245        // Try parsing as an integer (nanoseconds)
246        if let Ok(int_value) = s.parse::<u64>() {
247            return Ok(Self(int_value));
248        }
249
250        // If the string is composed solely of digits but didn't fit in a u64 we
251        // treat that as an overflow error rather than attempting to interpret
252        // it as seconds in floating-point form. This avoids the surprising
253        // situation where a caller provides nanoseconds but gets an out-of-
254        // range float interpretation instead.
255        if s.chars().all(|c| c.is_ascii_digit()) {
256            return Err("Unix timestamp is out of range".into());
257        }
258
259        // Try parsing as a floating point number (seconds)
260        if let Ok(float_value) = s.parse::<f64>() {
261            return f64_seconds_to_nanos(float_value).map(Self);
262        }
263
264        // The legacy parser accepted upper/lowercase `T` and a space separator, but not RFC 9557
265        // annotations. Preserve that input contract instead of adopting Jiff's broader grammar.
266        let is_compatible_rfc3339 = matches!(s.as_bytes().get(10), Some(b'T' | b't' | b' '))
267            && !s.as_bytes().contains(&b'[');
268        if is_compatible_rfc3339 && let Ok(datetime) = s.parse::<Timestamp>() {
269            let nanos = datetime.as_nanosecond();
270            let nanos = u64::try_from(nanos)
271                .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
272            return Ok(Self(nanos));
273        }
274
275        // The legacy `%Y-%m-%d` parser accepted one- or two-digit months and days.
276        if let Ok(date) = Date::strptime("%Y-%m-%d", s) {
277            let datetime = date
278                .at(0, 0, 0, 0)
279                .to_zoned(jiff::tz::TimeZone::UTC)
280                .map_err(|e| e.to_string())?;
281            let nanos = datetime.timestamp().as_nanosecond();
282            let nanos = u64::try_from(nanos)
283                .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
284            return Ok(Self(nanos));
285        }
286
287        Err(format!("Invalid format: {s}"))
288    }
289
290    /// Returns `Some(self + rhs)` or `None` if the addition would overflow
291    #[must_use]
292    pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
293        self.0.checked_add(rhs.into()).map(Self)
294    }
295
296    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow
297    #[must_use]
298    pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
299        self.0.checked_sub(rhs.into()).map(Self)
300    }
301
302    /// Saturating addition - if overflow occurs the value is clamped to `u64::MAX`.
303    #[must_use]
304    pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
305        Self(self.0.saturating_add(rhs.into()))
306    }
307
308    /// Saturating subtraction - if underflow occurs the value is clamped to `0`.
309    #[must_use]
310    pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
311        Self(self.0.saturating_sub(rhs.into()))
312    }
313}
314
315// Converts non-negative float seconds to nanoseconds, truncating (not rounding)
316// sub-nanosecond precision for consistency with `datetime::secs_to_nanos`.
317#[expect(
318    clippy::cast_possible_truncation,
319    clippy::cast_sign_loss,
320    reason = "value is checked finite, non-negative, and within u64 range before the cast"
321)]
322fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
323    if !value.is_finite() {
324        return Err(format!("Unix timestamp must be finite, was {value}"));
325    }
326
327    if value < 0.0 {
328        return Err("Unix timestamp cannot be negative".to_string());
329    }
330
331    // Convert seconds to nanoseconds while checking for overflow.
332    // We perform the multiplication in `f64`, then validate the
333    // result fits inside `u64` *before* truncating / casting.
334    let nanos_f64 = value * 1_000_000_000.0;
335
336    if nanos_f64 >= U64_UPPER_BOUND_F64 {
337        return Err(format!("Unix timestamp {value} seconds is out of range"));
338    }
339
340    Ok(nanos_f64.trunc() as u64)
341}
342
343impl Deref for UnixNanos {
344    type Target = u64;
345
346    fn deref(&self) -> &Self::Target {
347        &self.0
348    }
349}
350
351impl PartialEq<u64> for UnixNanos {
352    fn eq(&self, other: &u64) -> bool {
353        self.0 == *other
354    }
355}
356
357impl PartialOrd<u64> for UnixNanos {
358    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
359        self.0.partial_cmp(other)
360    }
361}
362
363impl PartialEq<Option<u64>> for UnixNanos {
364    fn eq(&self, other: &Option<u64>) -> bool {
365        match other {
366            Some(value) => self.0 == *value,
367            None => false,
368        }
369    }
370}
371
372impl PartialOrd<Option<u64>> for UnixNanos {
373    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
374        match other {
375            Some(value) => self.0.partial_cmp(value),
376            None => Some(Ordering::Greater),
377        }
378    }
379}
380
381impl PartialEq<UnixNanos> for u64 {
382    fn eq(&self, other: &UnixNanos) -> bool {
383        *self == other.0
384    }
385}
386
387impl PartialOrd<UnixNanos> for u64 {
388    fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
389        self.partial_cmp(&other.0)
390    }
391}
392
393impl From<u64> for UnixNanos {
394    fn from(value: u64) -> Self {
395        Self(value)
396    }
397}
398
399impl From<UnixNanos> for u64 {
400    fn from(value: UnixNanos) -> Self {
401        value.0
402    }
403}
404
405/// Converts a string slice to [`UnixNanos`].
406///
407/// # Panics
408///
409/// This implementation will panic if the string cannot be parsed into a valid [`UnixNanos`].
410/// This is intentional fail-fast behavior where invalid timestamps indicate a critical
411/// logic error that should halt execution rather than silently propagate incorrect data.
412///
413/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
414/// a [`Result`].
415impl From<&str> for UnixNanos {
416    fn from(value: &str) -> Self {
417        value
418            .parse()
419            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
420    }
421}
422
423/// Converts a [`String`] to [`UnixNanos`].
424///
425/// # Panics
426///
427/// This implementation will panic if the string cannot be parsed into a valid [`UnixNanos`].
428/// This is intentional fail-fast behavior where invalid timestamps indicate a critical
429/// logic error that should halt execution rather than silently propagate incorrect data.
430///
431/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
432/// a [`Result`].
433impl From<String> for UnixNanos {
434    fn from(value: String) -> Self {
435        value
436            .parse()
437            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
438    }
439}
440
441impl From<Timestamp> for UnixNanos {
442    fn from(value: Timestamp) -> Self {
443        let nanos = value.as_nanosecond();
444
445        assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
446
447        Self::from(u64::try_from(nanos).expect("DateTime timestamp out of range for UnixNanos"))
448    }
449}
450
451impl From<SystemTime> for UnixNanos {
452    fn from(value: SystemTime) -> Self {
453        let duration = value
454            .duration_since(std::time::UNIX_EPOCH)
455            .expect("SystemTime before UNIX EPOCH");
456
457        let nanos =
458            u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
459
460        Self::from(nanos)
461    }
462}
463
464impl FromStr for UnixNanos {
465    type Err = Box<dyn std::error::Error>;
466
467    fn from_str(s: &str) -> Result<Self, Self::Err> {
468        Self::parse_string(s).map_err(std::convert::Into::into)
469    }
470}
471
472/// Adds two [`UnixNanos`] values.
473///
474/// # Panics
475///
476/// Panics on overflow. This is intentional fail-fast behavior: overflow in timestamp
477/// arithmetic indicates a logic error in calculations that would corrupt data.
478/// Use [`UnixNanos::checked_add()`] or [`UnixNanos::saturating_add_ns()`] if you need
479/// explicit overflow handling.
480impl Add for UnixNanos {
481    type Output = Self;
482
483    fn add(self, rhs: Self) -> Self::Output {
484        Self(
485            self.0
486                .checked_add(rhs.0)
487                .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
488        )
489    }
490}
491
492/// Subtracts one [`UnixNanos`] from another.
493///
494/// # Panics
495///
496/// Panics on underflow. This is intentional fail-fast behavior: underflow in timestamp
497/// arithmetic indicates a logic error in calculations that would corrupt data.
498/// Use [`UnixNanos::checked_sub()`] or [`UnixNanos::saturating_sub_ns()`] if you need
499/// explicit underflow handling.
500impl Sub for UnixNanos {
501    type Output = Self;
502
503    fn sub(self, rhs: Self) -> Self::Output {
504        Self(
505            self.0
506                .checked_sub(rhs.0)
507                .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
508        )
509    }
510}
511
512/// Adds a `u64` nanosecond value to [`UnixNanos`].
513///
514/// # Panics
515///
516/// Panics on overflow. This is intentional fail-fast behavior for timestamp arithmetic.
517/// Use [`UnixNanos::checked_add()`] for explicit overflow handling.
518impl Add<u64> for UnixNanos {
519    type Output = Self;
520
521    fn add(self, rhs: u64) -> Self::Output {
522        Self(
523            self.0
524                .checked_add(rhs)
525                .expect("UnixNanos overflow in addition"),
526        )
527    }
528}
529
530/// Subtracts a `u64` nanosecond value from [`UnixNanos`].
531///
532/// # Panics
533///
534/// Panics on underflow. This is intentional fail-fast behavior for timestamp arithmetic.
535/// Use [`UnixNanos::checked_sub()`] for explicit underflow handling.
536impl Sub<u64> for UnixNanos {
537    type Output = Self;
538
539    fn sub(self, rhs: u64) -> Self::Output {
540        Self(
541            self.0
542                .checked_sub(rhs)
543                .expect("UnixNanos underflow in subtraction"),
544        )
545    }
546}
547
548/// Add-assigns a value to [`UnixNanos`].
549///
550/// # Panics
551///
552/// Panics on overflow. This is intentional fail-fast behavior for timestamp arithmetic.
553impl<T: Into<u64>> AddAssign<T> for UnixNanos {
554    fn add_assign(&mut self, other: T) {
555        let other_u64 = other.into();
556        self.0 = self
557            .0
558            .checked_add(other_u64)
559            .expect("UnixNanos overflow in add_assign");
560    }
561}
562
563/// Sub-assigns a value from [`UnixNanos`].
564///
565/// # Panics
566///
567/// Panics on underflow. This is intentional fail-fast behavior for timestamp arithmetic.
568impl<T: Into<u64>> SubAssign<T> for UnixNanos {
569    fn sub_assign(&mut self, other: T) {
570        let other_u64 = other.into();
571        self.0 = self
572            .0
573            .checked_sub(other_u64)
574            .expect("UnixNanos underflow in sub_assign");
575    }
576}
577
578impl Display for UnixNanos {
579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580        write!(f, "{}", self.0)
581    }
582}
583
584impl From<UnixNanos> for Timestamp {
585    fn from(value: UnixNanos) -> Self {
586        value.to_datetime_utc()
587    }
588}
589
590impl<'de> Deserialize<'de> for UnixNanos {
591    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
592    where
593        D: Deserializer<'de>,
594    {
595        struct UnixNanosVisitor;
596
597        impl Visitor<'_> for UnixNanosVisitor {
598            type Value = UnixNanos;
599
600            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
601                formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
602            }
603
604            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
605            where
606                E: de::Error,
607            {
608                Ok(UnixNanos(value))
609            }
610
611            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
612            where
613                E: de::Error,
614            {
615                u64::try_from(value)
616                    .map(UnixNanos)
617                    .map_err(|_| E::custom("Unix timestamp cannot be negative"))
618            }
619
620            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
621            where
622                E: de::Error,
623            {
624                f64_seconds_to_nanos(value)
625                    .map(UnixNanos)
626                    .map_err(E::custom)
627            }
628
629            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
630            where
631                E: de::Error,
632            {
633                UnixNanos::parse_string(value).map_err(E::custom)
634            }
635        }
636
637        deserializer.deserialize_any(UnixNanosVisitor)
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use jiff::SignedDuration;
644    use rstest::rstest;
645
646    use super::*;
647
648    fn timestamp(value: &str) -> Timestamp {
649        value.parse().unwrap()
650    }
651
652    #[rstest]
653    fn test_new() {
654        let nanos = UnixNanos::new(123);
655        assert_eq!(nanos.as_u64(), 123);
656        assert_eq!(nanos.as_i64(), 123);
657    }
658
659    #[rstest]
660    fn test_max() {
661        let nanos = UnixNanos::max();
662        assert_eq!(nanos.as_u64(), u64::MAX);
663    }
664
665    #[rstest]
666    fn test_is_zero() {
667        assert!(UnixNanos::default().is_zero());
668        assert!(!UnixNanos::max().is_zero());
669    }
670
671    #[rstest]
672    fn test_from_u64() {
673        let nanos = UnixNanos::from(123);
674        assert_eq!(nanos.as_u64(), 123);
675        assert_eq!(nanos.as_i64(), 123);
676    }
677
678    #[rstest]
679    fn test_default() {
680        let nanos = UnixNanos::default();
681        assert_eq!(nanos.as_u64(), 0);
682        assert_eq!(nanos.as_i64(), 0);
683    }
684
685    #[rstest]
686    fn test_into_from() {
687        let nanos: UnixNanos = 456.into();
688        let value: u64 = nanos.into();
689        assert_eq!(value, 456);
690    }
691
692    #[rstest]
693    #[case(0, "1970-01-01T00:00:00+00:00")]
694    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
695    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
696    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
697    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
698    fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
699        let nanos = UnixNanos::from(nanos);
700        let datetime = nanos.to_datetime_utc();
701        assert_eq!(
702            datetime.display_with_offset(Offset::UTC).to_string(),
703            expected
704        );
705    }
706
707    #[rstest]
708    #[case(0, "1970-01-01T00:00:00+00:00")]
709    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
710    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
711    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
712    #[case(1_500_000_000_500_000_000, "2017-07-14T02:40:00.500+00:00")]
713    #[case(1_500_000_000_123_456_000, "2017-07-14T02:40:00.123456+00:00")]
714    #[case(1_500_000_000_123_456_789, "2017-07-14T02:40:00.123456789+00:00")]
715    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
716    fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
717        let nanos = UnixNanos::from(nanos);
718        assert_eq!(nanos.to_rfc3339(), expected);
719    }
720
721    #[rstest]
722    fn test_from_str() {
723        let nanos: UnixNanos = "123".parse().unwrap();
724        assert_eq!(nanos.as_u64(), 123);
725    }
726
727    #[rstest]
728    fn test_from_str_invalid() {
729        let result = "abc".parse::<UnixNanos>();
730        assert!(result.is_err());
731    }
732
733    #[rstest]
734    fn test_from_str_date() {
735        let nanos: UnixNanos = "2024-02-10".parse().unwrap();
736        assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
737    }
738
739    #[rstest]
740    fn test_from_str_pre_epoch_date() {
741        let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
742        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
743    }
744
745    #[rstest]
746    fn test_from_str_pre_epoch_rfc3339() {
747        let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
748        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
749    }
750
751    #[rstest]
752    fn test_try_from_datetime_valid() {
753        let datetime = Timestamp::from_second(1_000_000_000).unwrap();
754        let nanos = UnixNanos::from(datetime);
755        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
756    }
757
758    #[rstest]
759    fn test_from_system_time() {
760        let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
761        let nanos = UnixNanos::from(system_time);
762        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
763    }
764
765    #[rstest]
766    #[should_panic(expected = "SystemTime before UNIX EPOCH")]
767    fn test_from_system_time_before_epoch() {
768        let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
769        let _ = UnixNanos::from(system_time);
770    }
771
772    #[rstest]
773    #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
774    fn test_from_system_time_overflow_panics() {
775        // One second beyond the largest whole-second duration representable in u64 nanoseconds
776        let system_time =
777            std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
778        let _ = UnixNanos::from(system_time);
779    }
780
781    #[rstest]
782    fn test_eq() {
783        let nanos = UnixNanos::from(100);
784        assert_eq!(nanos, 100);
785        assert_eq!(nanos, Some(100));
786        assert_ne!(nanos, 200);
787        assert_ne!(nanos, Some(200));
788        assert_ne!(nanos, None);
789    }
790
791    #[rstest]
792    fn test_partial_cmp() {
793        let nanos = UnixNanos::from(100);
794        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
795        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
796        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
797        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
798    }
799
800    #[rstest]
801    fn test_edge_case_max_value() {
802        let nanos = UnixNanos::from(u64::MAX);
803        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
804    }
805
806    #[rstest]
807    fn test_display() {
808        let nanos = UnixNanos::from(123);
809        assert_eq!(format!("{nanos}"), "123");
810    }
811
812    #[rstest]
813    fn test_addition() {
814        let nanos1 = UnixNanos::from(100);
815        let nanos2 = UnixNanos::from(200);
816        let result = nanos1 + nanos2;
817        assert_eq!(result.as_u64(), 300);
818    }
819
820    #[rstest]
821    fn test_add_assign() {
822        let mut nanos = UnixNanos::from(100);
823        nanos += 50_u64;
824        assert_eq!(nanos.as_u64(), 150);
825    }
826
827    #[rstest]
828    fn test_subtraction() {
829        let nanos1 = UnixNanos::from(200);
830        let nanos2 = UnixNanos::from(100);
831        let result = nanos1 - nanos2;
832        assert_eq!(result.as_u64(), 100);
833    }
834
835    #[rstest]
836    fn test_sub_assign() {
837        let mut nanos = UnixNanos::from(200);
838        nanos -= 50_u64;
839        assert_eq!(nanos.as_u64(), 150);
840    }
841
842    #[rstest]
843    #[should_panic(expected = "UnixNanos overflow")]
844    fn test_overflow_add() {
845        let nanos = UnixNanos::from(u64::MAX);
846        let _ = nanos + UnixNanos::from(1); // This should panic due to overflow
847    }
848
849    #[rstest]
850    #[should_panic(expected = "UnixNanos overflow")]
851    fn test_overflow_add_u64() {
852        let nanos = UnixNanos::from(u64::MAX);
853        let _ = nanos + 1_u64; // This should panic due to overflow
854    }
855
856    #[rstest]
857    #[should_panic(expected = "UnixNanos underflow")]
858    fn test_overflow_sub() {
859        let _ = UnixNanos::default() - UnixNanos::from(1); // This should panic due to underflow
860    }
861
862    #[rstest]
863    #[should_panic(expected = "UnixNanos underflow")]
864    fn test_overflow_sub_u64() {
865        let _ = UnixNanos::default() - 1_u64; // This should panic due to underflow
866    }
867
868    #[rstest]
869    #[case(100, 50, Some(50))]
870    #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
871    #[case(u64::MAX, u64::MAX - 1, Some(1))]
872    #[case(50, 50, Some(0))]
873    #[case(50, 100, None)]
874    #[case(0, 1, None)]
875    fn test_duration_since(
876        #[case] time1: u64,
877        #[case] time2: u64,
878        #[case] expected: Option<DurationNanos>,
879    ) {
880        let nanos1 = UnixNanos::from(time1);
881        let nanos2 = UnixNanos::from(time2);
882        assert_eq!(nanos1.duration_since(&nanos2), expected);
883    }
884
885    #[rstest]
886    fn test_duration_since_same_moment() {
887        let moment = UnixNanos::from(1_707_577_123_456_789_000);
888        assert_eq!(moment.duration_since(&moment), Some(0));
889    }
890
891    #[rstest]
892    fn test_duration_since_chronological() {
893        // Create a reference time (Feb 10, 2024)
894        let earlier = timestamp("2024-02-10T12:00:00Z");
895
896        // Create a time 1 hour, 30 minutes, and 45 seconds later (with nanoseconds)
897        let later = earlier
898            + SignedDuration::from_hours(1)
899            + SignedDuration::from_mins(30)
900            + SignedDuration::from_secs(45)
901            + SignedDuration::from_nanos(500_000_000);
902
903        let earlier_nanos = UnixNanos::from(earlier);
904        let later_nanos = UnixNanos::from(later);
905
906        // Calculate expected duration in nanoseconds
907        let expected_duration =
908            (60 * 60 + 30 * 60 + 45) * NANOSECONDS_IN_SECOND + 500 * NANOSECONDS_IN_MILLISECOND;
909
910        assert_eq!(
911            later_nanos.duration_since(&earlier_nanos),
912            Some(expected_duration)
913        );
914        assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
915    }
916
917    #[rstest]
918    fn test_duration_since_with_edge_cases() {
919        // Test with maximum value
920        let max = UnixNanos::from(u64::MAX);
921        let smaller = UnixNanos::from(u64::MAX - 1000);
922
923        assert_eq!(max.duration_since(&smaller), Some(1000));
924        assert_eq!(smaller.duration_since(&max), None);
925
926        // Test with minimum value
927        let min = UnixNanos::default(); // Zero timestamp
928        let larger = UnixNanos::from(1000);
929
930        assert_eq!(min.duration_since(&min), Some(0));
931        assert_eq!(larger.duration_since(&min), Some(1000));
932        assert_eq!(min.duration_since(&larger), None);
933    }
934
935    #[rstest]
936    fn test_serde_json() {
937        let nanos = UnixNanos::from(123);
938        let json = serde_json::to_string(&nanos).unwrap();
939        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
940        assert_eq!(deserialized, nanos);
941    }
942
943    #[rstest]
944    fn test_serde_edge_cases() {
945        let nanos = UnixNanos::from(u64::MAX);
946        let json = serde_json::to_string(&nanos).unwrap();
947        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
948        assert_eq!(deserialized, nanos);
949    }
950
951    #[rstest]
952    #[case("123", 123)] // Integer string
953    #[case("1234.567", 1_234_567_000_000)] // Float string (seconds to nanos)
954    #[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date (midnight UTC)
955    #[case("2024-2-10", 1_707_523_200_000_000_000)] // Legacy-compatible short month
956    #[case("2024-02-1", 1_706_745_600_000_000_000)] // Legacy-compatible short day
957    #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
958    #[case("2024-02-10t14:58:43Z", 1_707_577_123_000_000_000)] // Lowercase RFC3339 separator
959    #[case("2024-02-10 14:58:43Z", 1_707_577_123_000_000_000)] // Space RFC3339 separator
960    #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
961    fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
962        let parsed: UnixNanos = input.parse().unwrap();
963        assert_eq!(parsed.as_u64(), expected);
964    }
965
966    #[rstest]
967    #[case("abc")] // Random string
968    #[case("not a timestamp")] // Non-timestamp string
969    #[case("2024-02-10 14:58:43")] // Space-separated format (not RFC3339)
970    #[case("2024-02-10T14:58:43Z[UTC]")] // RFC 9557 annotation was not accepted previously
971    fn test_from_str_invalid_formats(#[case] input: &str) {
972        let result = input.parse::<UnixNanos>();
973        assert!(result.is_err());
974    }
975
976    #[rstest]
977    fn test_from_str_integer_overflow() {
978        // One more digit than u64::MAX (20 digits) so definitely overflows
979        let input = "184467440737095516160";
980        let result = input.parse::<UnixNanos>();
981        assert!(result.is_err());
982    }
983
984    #[rstest]
985    fn test_checked_add_overflow_returns_none() {
986        let max = UnixNanos::from(u64::MAX);
987        assert_eq!(max.checked_add(1_u64), None);
988    }
989
990    #[rstest]
991    fn test_checked_sub_underflow_returns_none() {
992        let zero = UnixNanos::default();
993        assert_eq!(zero.checked_sub(1_u64), None);
994    }
995
996    #[rstest]
997    fn test_saturating_add_overflow() {
998        let max = UnixNanos::from(u64::MAX);
999        let result = max.saturating_add_ns(1_u64);
1000        assert_eq!(result, UnixNanos::from(u64::MAX));
1001    }
1002
1003    #[rstest]
1004    fn test_saturating_sub_underflow() {
1005        let zero = UnixNanos::default();
1006        let result = zero.saturating_sub_ns(1_u64);
1007        assert_eq!(result, UnixNanos::default());
1008    }
1009
1010    #[rstest]
1011    fn test_from_str_float_overflow() {
1012        // Use scientific notation so we take the floating-point parsing path.
1013        let input = "2e10"; // 20 billion seconds ~ 634 years (> u64::MAX nanoseconds)
1014        let err = input.parse::<UnixNanos>().unwrap_err();
1015        assert!(err.to_string().contains("out of range"));
1016    }
1017
1018    #[rstest]
1019    #[case("NaN")]
1020    #[case("nan")]
1021    #[case("inf")]
1022    #[case("-inf")]
1023    fn test_from_str_non_finite_float_errors(#[case] input: &str) {
1024        let err = input.parse::<UnixNanos>().unwrap_err();
1025        assert!(err.to_string().contains("must be finite"));
1026    }
1027
1028    #[rstest]
1029    #[case("-1.5")]
1030    #[case("-0.000001")]
1031    fn test_from_str_negative_float_errors(#[case] input: &str) {
1032        let err = input.parse::<UnixNanos>().unwrap_err();
1033        assert!(err.to_string().contains("cannot be negative"));
1034    }
1035
1036    #[rstest]
1037    fn test_deserialize_u64() {
1038        let json = "123456789";
1039        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1040        assert_eq!(deserialized.as_u64(), 123_456_789);
1041    }
1042
1043    #[rstest]
1044    fn test_deserialize_string_with_int() {
1045        let json = "\"123456789\"";
1046        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1047        assert_eq!(deserialized.as_u64(), 123_456_789);
1048    }
1049
1050    #[rstest]
1051    fn test_deserialize_float() {
1052        let json = "1234.567";
1053        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1054        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1055    }
1056
1057    #[rstest]
1058    fn test_deserialize_string_with_float() {
1059        let json = "\"1234.567\"";
1060        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1061        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1062    }
1063
1064    #[rstest]
1065    fn test_deserialize_float_uses_truncation() {
1066        // Truncation (not rounding) for consistency with secs_to_nanos() etc
1067        let json = "0.9999999999";
1068        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1069        assert_eq!(deserialized.as_u64(), 999_999_999); // Truncated, not rounded to 1B
1070    }
1071
1072    #[rstest]
1073    #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1074    #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1075    fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1076        let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1077        assert_eq!(deserialized.as_u64(), expected);
1078    }
1079
1080    #[rstest]
1081    fn test_deserialize_negative_int_fails() {
1082        let json = "-123456789";
1083        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1084        assert!(
1085            result
1086                .unwrap_err()
1087                .to_string()
1088                .contains("cannot be negative")
1089        );
1090    }
1091
1092    #[rstest]
1093    fn test_deserialize_negative_float_fails() {
1094        let json = "-1234.567";
1095        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1096        assert!(
1097            result
1098                .unwrap_err()
1099                .to_string()
1100                .contains("cannot be negative")
1101        );
1102    }
1103
1104    #[rstest]
1105    fn test_deserialize_nan_fails() {
1106        // JSON doesn't support NaN directly, test the internal deserializer
1107        use serde::de::{
1108            IntoDeserializer,
1109            value::{Error as ValueError, F64Deserializer},
1110        };
1111        let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1112        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1113        assert!(result.is_err());
1114        assert!(result.unwrap_err().to_string().contains("must be finite"));
1115    }
1116
1117    #[rstest]
1118    fn test_deserialize_infinity_fails() {
1119        use serde::de::{
1120            IntoDeserializer,
1121            value::{Error as ValueError, F64Deserializer},
1122        };
1123        let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1124        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1125        assert!(result.is_err());
1126        assert!(result.unwrap_err().to_string().contains("must be finite"));
1127    }
1128
1129    #[rstest]
1130    fn test_deserialize_negative_infinity_fails() {
1131        use serde::de::{
1132            IntoDeserializer,
1133            value::{Error as ValueError, F64Deserializer},
1134        };
1135        let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1136        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1137        assert!(result.is_err());
1138        assert!(result.unwrap_err().to_string().contains("must be finite"));
1139    }
1140
1141    #[rstest]
1142    fn test_deserialize_overflow_float_fails() {
1143        // Test a float that would overflow u64 when converted to nanoseconds
1144        // u64::MAX is ~18.4e18, so u64::MAX / 1e9 = ~18.4e9 seconds
1145        let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1146        assert!(result.is_err());
1147        assert!(result.unwrap_err().to_string().contains("out of range"));
1148    }
1149
1150    #[rstest]
1151    fn test_deserialize_float_u64_boundary_fails() {
1152        let deserializer = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(
1153            18_446_744_073.709_553,
1154        );
1155        let err = UnixNanos::deserialize(deserializer).unwrap_err();
1156        assert!(err.to_string().contains("out of range"));
1157    }
1158
1159    #[rstest]
1160    fn test_deserialize_invalid_string_fails() {
1161        let json = "\"not a timestamp\"";
1162        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1163        assert!(result.is_err());
1164    }
1165
1166    #[rstest]
1167    fn test_deserialize_edge_cases() {
1168        // Test zero
1169        let json = "0";
1170        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1171        assert_eq!(deserialized.as_u64(), 0);
1172
1173        // Test large value
1174        let json = "18446744073709551615"; // u64::MAX
1175        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1176        assert_eq!(deserialized.as_u64(), u64::MAX);
1177    }
1178
1179    #[rstest]
1180    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1181    fn test_as_i64_overflow_panics() {
1182        let nanos = UnixNanos::from(u64::MAX);
1183        let _ = nanos.as_i64(); // Should panic
1184    }
1185
1186    #[rstest]
1187    fn test_as_i64_at_i64_max_boundary() {
1188        let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
1189        assert_eq!(nanos.as_i64(), i64::MAX);
1190    }
1191
1192    #[rstest]
1193    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1194    fn test_as_i64_just_above_i64_max_panics() {
1195        let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
1196        let _ = nanos.as_i64();
1197    }
1198
1199    use proptest::prelude::*;
1200
1201    fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1202        prop_oneof![
1203            // Small values
1204            0u64..1_000_000u64,
1205            // Medium values (microseconds range)
1206            1_000_000u64..1_000_000_000_000u64,
1207            // Large values (nanoseconds since 1970)
1208            1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
1209            // Values above i64::MAX (sentinel range, GTC/infinity)
1210            (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
1211            // Edge cases
1212            Just(0u64),
1213            Just(1u64),
1214            Just(1_000_000_000u64),               // 1 second in nanos
1215            Just(1_000_000_000_000u64),           // ~2001 timestamp
1216            Just(1_700_000_000_000_000_000u64),   // ~2023 timestamp
1217            Just((i64::MAX / 2).cast_unsigned()), // Safe for doubling
1218            Just(i64::MAX.cast_unsigned()),       // i64 boundary
1219            Just(u64::MAX),                       // Sentinel / max value
1220        ]
1221        .prop_map(UnixNanos::from)
1222    }
1223
1224    fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1225        (unix_nanos_strategy(), unix_nanos_strategy())
1226    }
1227
1228    proptest! {
1229        #[rstest]
1230        #[expect(
1231            clippy::float_cmp,
1232            clippy::cast_precision_loss,
1233            reason = "roundtrip: both sides go through the same u64->f64 cast"
1234        )]
1235        fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
1236            let value = nanos.as_u64();
1237            prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
1238            prop_assert_eq!(nanos.as_f64(), value as f64);
1239
1240            // Test i64 conversion only for values within i64 range
1241            if i64::try_from(value).is_ok() {
1242                prop_assert_eq!(nanos.as_i64(), value.cast_signed());
1243            }
1244        }
1245
1246        #[rstest]
1247        fn prop_unix_nanos_addition_commutative(
1248            (nanos1, nanos2) in unix_nanos_pair_strategy()
1249        ) {
1250            // Addition should be commutative when no overflow occurs
1251            if let (Some(sum1), Some(sum2)) = (
1252                nanos1.checked_add(nanos2.as_u64()),
1253                nanos2.checked_add(nanos1.as_u64())
1254            ) {
1255                prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1256            }
1257        }
1258
1259        #[rstest]
1260        fn prop_unix_nanos_addition_associative(
1261            nanos1 in unix_nanos_strategy(),
1262            nanos2 in unix_nanos_strategy(),
1263            nanos3 in unix_nanos_strategy(),
1264        ) {
1265            let expected = nanos1
1266                .as_u64()
1267                .checked_add(nanos2.as_u64())
1268                .and_then(|sum| sum.checked_add(nanos3.as_u64()));
1269
1270            if let Some(expected) = expected {
1271                let left = (nanos1 + nanos2) + nanos3;
1272                let right = nanos1 + (nanos2 + nanos3);
1273                prop_assert_eq!(left.as_u64(), expected);
1274                prop_assert_eq!(right.as_u64(), expected);
1275            }
1276        }
1277
1278        #[rstest]
1279        fn prop_unix_nanos_subtraction_inverse(
1280            (nanos1, nanos2) in unix_nanos_pair_strategy()
1281        ) {
1282            // Subtraction should be the inverse of addition when no underflow occurs
1283            if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1284                let diff = sum - nanos2;
1285                prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1286            }
1287        }
1288
1289        #[rstest]
1290        fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1291            // Zero should be additive identity
1292            let zero = UnixNanos::default();
1293            prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1294            prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1295            prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1296        }
1297
1298        #[rstest]
1299        fn prop_unix_nanos_ordering_consistency(
1300            (nanos1, nanos2) in unix_nanos_pair_strategy()
1301        ) {
1302            // Ordering operations should be consistent
1303            let eq = nanos1 == nanos2;
1304            let lt = nanos1 < nanos2;
1305            let gt = nanos1 > nanos2;
1306            let le = nanos1 <= nanos2;
1307            let ge = nanos1 >= nanos2;
1308
1309            // Exactly one of eq, lt, gt should be true
1310            let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1311            prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1312
1313            // Consistency checks
1314            prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1315            prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1316            prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1317            prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1318        }
1319
1320        #[rstest]
1321        fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1322            // String serialization should round-trip correctly
1323            let string_repr = nanos.to_string();
1324            let parsed = UnixNanos::from_str(&string_repr);
1325            prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1326            if let Ok(parsed_nanos) = parsed {
1327                prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1328            }
1329        }
1330
1331        #[rstest]
1332        fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1333            // DateTime conversion should be consistent (only test values within i64 range)
1334            if i64::try_from(nanos.as_u64()).is_ok() {
1335                let datetime = nanos.to_datetime_utc();
1336                let converted_back = UnixNanos::from(datetime);
1337                prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1338
1339                // RFC3339 string should also round-trip for valid dates
1340                let rfc3339 = nanos.to_rfc3339();
1341                if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1342                    prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1343                }
1344            }
1345        }
1346
1347        #[rstest]
1348        fn prop_unix_nanos_duration_since(
1349            (nanos1, nanos2) in unix_nanos_pair_strategy()
1350        ) {
1351            // duration_since should be consistent with comparison and arithmetic
1352            let duration = nanos1.duration_since(&nanos2);
1353
1354            if nanos1 >= nanos2 {
1355                // If nanos1 >= nanos2, duration should be Some and equal to difference
1356                prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1357                if let Some(dur) = duration {
1358                    prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1359                        "Duration should equal the difference");
1360                    prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1361                        "second + duration should equal first");
1362                }
1363            } else {
1364                // If nanos1 < nanos2, duration should be None
1365                prop_assert!(duration.is_none(), "Duration should be None when first < second");
1366            }
1367        }
1368
1369        #[rstest]
1370        fn prop_unix_nanos_checked_arithmetic(
1371            (nanos1, nanos2) in unix_nanos_pair_strategy()
1372        ) {
1373            // Checked arithmetic should be consistent with regular arithmetic when no overflow/underflow
1374            let checked_add = nanos1.checked_add(nanos2.as_u64());
1375            let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1376
1377            // If checked_add succeeds, regular addition should produce the same result
1378            if let Some(sum) = checked_add
1379                && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1380                    prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1381                }
1382
1383            // If checked_sub succeeds, regular subtraction should produce the same result
1384            if let Some(diff) = checked_sub
1385                && nanos1.as_u64() >= nanos2.as_u64() {
1386                    prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1387                }
1388        }
1389
1390        #[rstest]
1391        fn prop_unix_nanos_saturating_arithmetic(
1392            (nanos1, nanos2) in unix_nanos_pair_strategy()
1393        ) {
1394            // Saturating arithmetic should never panic and produce reasonable results
1395            let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1396            let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1397
1398            // Saturating add should be >= both operands
1399            prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1400            prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1401
1402            // Saturating sub should be <= first operand
1403            prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1404
1405            // If no overflow/underflow would occur, saturating should match checked
1406            if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1407                prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1408            } else {
1409                prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1410            }
1411
1412            if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1413                prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1414            } else {
1415                prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1416            }
1417        }
1418
1419        #[rstest]
1420        fn prop_unix_nanos_assign_mirrors_op(
1421            (nanos1, nanos2) in unix_nanos_pair_strategy()
1422        ) {
1423            // AddAssign should produce the same result as Add
1424            if let Some(expected) = nanos1.checked_add(nanos2.as_u64()) {
1425                let mut add_result = nanos1;
1426                add_result += nanos2;
1427                prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
1428            }
1429
1430            // SubAssign should produce the same result as Sub
1431            if nanos1.as_u64() >= nanos2.as_u64() {
1432                let expected = nanos1 - nanos2;
1433                let mut sub_result = nanos1;
1434                sub_result -= nanos2;
1435                prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
1436            }
1437        }
1438
1439        #[rstest]
1440        fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
1441            let json = serde_json::to_string(&nanos).unwrap();
1442            let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1443            prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
1444        }
1445
1446        #[rstest]
1447        fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
1448            // Use IntoDeserializer to hit visit_f64 directly,
1449            // bypassing JSON text encoding ambiguity
1450            use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
1451            let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
1452            let result = UnixNanos::deserialize(deserializer);
1453
1454            let upper_bound = 2.0_f64.powi(64);
1455            if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 < upper_bound {
1456                prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
1457            } else {
1458                prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
1459            }
1460        }
1461    }
1462
1463    #[rstest]
1464    fn test_from_seconds_zero() {
1465        let nanos = UnixNanos::from_seconds(0);
1466        assert_eq!(nanos.as_u64(), 0);
1467    }
1468
1469    #[rstest]
1470    fn test_from_seconds_one() {
1471        let nanos = UnixNanos::from_seconds(1);
1472        assert_eq!(nanos.as_u64(), 1_000_000_000);
1473    }
1474
1475    #[rstest]
1476    fn test_from_seconds_realistic_timestamp() {
1477        let nanos = UnixNanos::from_seconds(1_700_000_000);
1478        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1479        assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
1480    }
1481
1482    #[rstest]
1483    fn test_from_seconds_max_safe() {
1484        let max_seconds = u64::MAX / 1_000_000_000;
1485        let nanos = UnixNanos::from_seconds(max_seconds);
1486        assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
1487    }
1488
1489    #[rstest]
1490    fn test_from_millis_zero() {
1491        let nanos = UnixNanos::from_millis(0);
1492        assert_eq!(nanos.as_u64(), 0);
1493    }
1494
1495    #[rstest]
1496    fn test_from_millis_one() {
1497        let nanos = UnixNanos::from_millis(1);
1498        assert_eq!(nanos.as_u64(), 1_000_000);
1499    }
1500
1501    #[rstest]
1502    fn test_from_millis_one_second() {
1503        let nanos = UnixNanos::from_millis(1_000);
1504        assert_eq!(nanos.as_u64(), 1_000_000_000);
1505    }
1506
1507    #[rstest]
1508    fn test_from_millis_realistic_timestamp() {
1509        // 2023-11-14T22:13:20Z = 1700000000000 ms
1510        let nanos = UnixNanos::from_millis(1_700_000_000_000);
1511        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1512        assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
1513    }
1514
1515    #[rstest]
1516    fn test_from_millis_max_safe() {
1517        let max_ms = u64::MAX / 1_000_000;
1518        let nanos = UnixNanos::from_millis(max_ms);
1519        assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
1520    }
1521
1522    #[rstest]
1523    fn test_from_millis_matches_manual_conversion() {
1524        let ms = 1_625_474_304_765_u64;
1525        let expected = ms * 1_000_000;
1526        assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
1527    }
1528
1529    #[rstest]
1530    #[case::valid(1_700_000_000_123, Some(1_700_000_000_123_000_000))]
1531    #[case::negative(-1, None)]
1532    #[case::overflow(i64::MAX, None)]
1533    fn test_from_millis_checked(#[case] millis: i64, #[case] expected: Option<u64>) {
1534        assert_eq!(
1535            UnixNanos::from_millis_checked(millis).map(|value| value.as_u64()),
1536            expected
1537        );
1538    }
1539
1540    #[rstest]
1541    #[case(0, 0)]
1542    #[case(999_999_999, 0)]
1543    #[case(1_000_000_000, 1)]
1544    #[case(1_700_000_000_123_456_789, 1_700_000_000)]
1545    fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
1546        assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
1547    }
1548
1549    #[rstest]
1550    #[case(0, 0)]
1551    #[case(999_999, 0)]
1552    #[case(1_000_000, 1)]
1553    #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
1554    fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
1555        assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
1556    }
1557
1558    #[rstest]
1559    #[case(0, 0)]
1560    #[case(999, 0)]
1561    #[case(1_000, 1)]
1562    #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
1563    fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
1564        assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
1565    }
1566
1567    #[rstest]
1568    fn test_from_micros_zero() {
1569        let nanos = UnixNanos::from_micros(0);
1570        assert_eq!(nanos.as_u64(), 0);
1571    }
1572
1573    #[rstest]
1574    fn test_from_micros_one() {
1575        let nanos = UnixNanos::from_micros(1);
1576        assert_eq!(nanos.as_u64(), 1_000);
1577    }
1578
1579    #[rstest]
1580    fn test_from_micros_one_second() {
1581        let nanos = UnixNanos::from_micros(1_000_000);
1582        assert_eq!(nanos.as_u64(), 1_000_000_000);
1583    }
1584
1585    #[rstest]
1586    fn test_from_micros_one_millisecond() {
1587        let nanos = UnixNanos::from_micros(1_000);
1588        assert_eq!(nanos.as_u64(), 1_000_000);
1589        assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
1590    }
1591
1592    #[rstest]
1593    fn test_from_micros_realistic_timestamp() {
1594        let micros = 1_700_000_000_000_000_u64;
1595        let nanos = UnixNanos::from_micros(micros);
1596        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1597    }
1598
1599    #[rstest]
1600    fn test_from_micros_max_safe() {
1601        let max_us = u64::MAX / 1_000;
1602        let nanos = UnixNanos::from_micros(max_us);
1603        assert_eq!(nanos.as_u64(), max_us * 1_000);
1604    }
1605
1606    #[rstest]
1607    fn test_from_micros_matches_manual_conversion() {
1608        let us = 1_000_000_123_456_u64;
1609        let expected = us * 1_000;
1610        assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
1611    }
1612
1613    #[rstest]
1614    #[case::valid(1_700_000_000_123_456, Some(1_700_000_000_123_456_000))]
1615    #[case::negative(-1, None)]
1616    #[case::overflow(i64::MAX, None)]
1617    fn test_from_micros_checked(#[case] micros: i64, #[case] expected: Option<u64>) {
1618        assert_eq!(
1619            UnixNanos::from_micros_checked(micros).map(|value| value.as_u64()),
1620            expected
1621        );
1622    }
1623
1624    #[rstest]
1625    fn test_from_seconds_millis_and_micros_consistency() {
1626        assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
1627        assert_eq!(
1628            UnixNanos::from_seconds(60),
1629            UnixNanos::from_micros(60_000_000)
1630        );
1631        assert_eq!(
1632            UnixNanos::from_millis(1_000),
1633            UnixNanos::from_micros(1_000_000)
1634        );
1635        assert_eq!(
1636            UnixNanos::from_millis(60_000),
1637            UnixNanos::from_micros(60_000_000)
1638        );
1639    }
1640
1641    #[rstest]
1642    fn test_from_millis_round_trip_to_datetime() {
1643        let ms = 1_707_577_123_456_u64;
1644        let nanos = UnixNanos::from_millis(ms);
1645        let dt = nanos.to_datetime_utc();
1646        assert_eq!(dt.as_millisecond().cast_unsigned(), ms);
1647    }
1648
1649    #[rstest]
1650    fn test_from_micros_preserves_sub_millisecond() {
1651        let micros = 1_700_000_000_000_123_u64;
1652        let nanos = UnixNanos::from_micros(micros);
1653        assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
1654    }
1655
1656    #[rstest]
1657    #[should_panic(expected = "UnixNanos overflow in from_seconds")]
1658    fn test_from_seconds_overflow_panics() {
1659        let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
1660    }
1661
1662    #[rstest]
1663    #[should_panic(expected = "UnixNanos overflow in from_millis")]
1664    fn test_from_millis_overflow_panics() {
1665        let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
1666    }
1667
1668    #[rstest]
1669    #[should_panic(expected = "UnixNanos overflow in from_micros")]
1670    fn test_from_micros_overflow_panics() {
1671        let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
1672    }
1673}