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