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