nautilus_core/
nanos.rs

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