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