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//! Nanosecond timestamp and duration types.
17//!
18//! [`UnixNanos`] represents a timestamp since the UNIX epoch, while [`DurationNanos`]
19//! represents an unsigned elapsed duration. Timestamp differences produce durations, and
20//! timestamps accept durations for arithmetic so the two concepts cannot be mixed implicitly.
21//!
22//! # Features
23//!
24//! - Zero-cost abstraction with appropriate operator implementations.
25//! - Conversion to/from `Timestamp` and [`std::time::Duration`].
26//! - RFC 3339 string formatting.
27//! - Duration calculations.
28//! - Flexible parsing and serialization.
29//!
30//! # Parsing and Serialization
31//!
32//! `UnixNanos` can be created from and serialized to various formats:
33//!
34//! - Integer values are interpreted as nanoseconds since the UNIX epoch.
35//! - Floating-point values are interpreted as seconds since the UNIX epoch (converted to nanoseconds
36//!   using truncation, not rounding, for consistency with [`secs_to_nanos`](crate::datetime::secs_to_nanos)).
37//! - String values may be:
38//!   - A numeric string (interpreted as nanoseconds).
39//!   - A floating-point string (interpreted as seconds, converted to nanoseconds).
40//!   - An RFC 3339 formatted timestamp (ISO 8601 with timezone).
41//!   - A simple date string in YYYY-MM-DD format (interpreted as midnight UTC on that date).
42//!
43//! # Limitations
44//!
45//! - Negative timestamps are invalid and will result in an error.
46//! - Arithmetic operations will panic on overflow/underflow rather than wrapping.
47//! - The `as_i64()` method will panic for timestamps beyond approximately year 2262
48//!   (when nanoseconds exceed `i64::MAX`).
49
50use std::{
51    cmp::Ordering,
52    fmt::Display,
53    ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
54    str::FromStr,
55    time::{Duration, SystemTime},
56};
57
58use jiff::{SignedDuration, Timestamp, civil::Date, tz::Offset};
59use serde::{
60    Deserialize, Deserializer, Serialize,
61    de::{self, Visitor},
62};
63use thiserror::Error;
64
65use crate::datetime::{
66    NANOSECONDS_IN_DAY, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
67    NANOSECONDS_IN_MINUTE, NANOSECONDS_IN_SECOND, SECONDS_IN_HOUR, U64_UPPER_BOUND_F64,
68};
69
70/// Represents an unsigned duration in nanoseconds.
71#[repr(transparent)]
72#[derive(
73    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
74)]
75#[serde(transparent)]
76pub struct DurationNanos(u64);
77
78/// Error returned when a duration cannot be represented as nanoseconds.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
80#[error("duration {value} {unit} exceeds the nanosecond range")]
81pub struct DurationNanosOutOfRangeError {
82    value: u64,
83    unit: &'static str,
84}
85
86impl DurationNanos {
87    /// A duration of zero nanoseconds.
88    pub const ZERO: Self = Self(0);
89
90    /// The maximum duration representable by this type.
91    pub const MAX: Self = Self(u64::MAX);
92
93    /// Creates a duration from an exact nanosecond count.
94    #[must_use]
95    pub const fn new(nanos: u64) -> Self {
96        Self(nanos)
97    }
98
99    /// Creates a duration from a number of whole microseconds.
100    ///
101    /// # Panics
102    ///
103    /// Panics if the result exceeds [`DurationNanos::MAX`].
104    #[must_use]
105    pub const fn from_micros(micros: u64) -> Self {
106        match Self::try_from_micros(micros) {
107            Ok(duration) => duration,
108            Err(_) => panic!("DurationNanos overflow in from_micros"),
109        }
110    }
111
112    /// Creates a duration from a number of whole microseconds.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
117    pub const fn try_from_micros(micros: u64) -> Result<Self, DurationNanosOutOfRangeError> {
118        Self::try_from_units(micros, NANOSECONDS_IN_MICROSECOND, "microseconds")
119    }
120
121    /// Creates a duration from a number of whole milliseconds.
122    ///
123    /// # Panics
124    ///
125    /// Panics if the result exceeds [`DurationNanos::MAX`].
126    #[must_use]
127    pub const fn from_millis(millis: u64) -> Self {
128        match Self::try_from_millis(millis) {
129            Ok(duration) => duration,
130            Err(_) => panic!("DurationNanos overflow in from_millis"),
131        }
132    }
133
134    /// Creates a duration from a number of whole milliseconds.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
139    pub const fn try_from_millis(millis: u64) -> Result<Self, DurationNanosOutOfRangeError> {
140        Self::try_from_units(millis, NANOSECONDS_IN_MILLISECOND, "milliseconds")
141    }
142
143    /// Creates a duration from a number of whole seconds.
144    ///
145    /// # Panics
146    ///
147    /// Panics if the result exceeds [`DurationNanos::MAX`].
148    #[must_use]
149    pub const fn from_secs(secs: u64) -> Self {
150        match Self::try_from_secs(secs) {
151            Ok(duration) => duration,
152            Err(_) => panic!("DurationNanos overflow in from_secs"),
153        }
154    }
155
156    /// Creates a duration from a number of whole seconds.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
161    pub const fn try_from_secs(secs: u64) -> Result<Self, DurationNanosOutOfRangeError> {
162        Self::try_from_units(secs, NANOSECONDS_IN_SECOND, "seconds")
163    }
164
165    /// Creates a duration from a number of whole minutes.
166    ///
167    /// # Panics
168    ///
169    /// Panics if the result exceeds [`DurationNanos::MAX`].
170    #[must_use]
171    pub const fn from_mins(mins: u64) -> Self {
172        match Self::try_from_mins(mins) {
173            Ok(duration) => duration,
174            Err(_) => panic!("DurationNanos overflow in from_mins"),
175        }
176    }
177
178    /// Creates a duration from a number of whole minutes.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
183    pub const fn try_from_mins(mins: u64) -> Result<Self, DurationNanosOutOfRangeError> {
184        Self::try_from_units(mins, NANOSECONDS_IN_MINUTE, "minutes")
185    }
186
187    /// Creates a duration from a number of whole hours.
188    ///
189    /// # Panics
190    ///
191    /// Panics if the result exceeds [`DurationNanos::MAX`].
192    #[must_use]
193    pub const fn from_hours(hours: u64) -> Self {
194        match Self::try_from_hours(hours) {
195            Ok(duration) => duration,
196            Err(_) => panic!("DurationNanos overflow in from_hours"),
197        }
198    }
199
200    /// Creates a duration from a number of whole hours.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
205    pub const fn try_from_hours(hours: u64) -> Result<Self, DurationNanosOutOfRangeError> {
206        Self::try_from_units(hours, SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND, "hours")
207    }
208
209    /// Creates a duration from a number of whole days.
210    ///
211    /// # Panics
212    ///
213    /// Panics if the result exceeds [`DurationNanos::MAX`].
214    #[must_use]
215    pub const fn from_days(days: u64) -> Self {
216        match Self::try_from_days(days) {
217            Ok(duration) => duration,
218            Err(_) => panic!("DurationNanos overflow in from_days"),
219        }
220    }
221
222    /// Creates a duration from a number of whole days.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
227    pub const fn try_from_days(days: u64) -> Result<Self, DurationNanosOutOfRangeError> {
228        Self::try_from_units(days, NANOSECONDS_IN_DAY, "days")
229    }
230
231    const fn try_from_units(
232        value: u64,
233        nanos_per_unit: u64,
234        unit: &'static str,
235    ) -> Result<Self, DurationNanosOutOfRangeError> {
236        match value.checked_mul(nanos_per_unit) {
237            Some(nanos) => Ok(Self(nanos)),
238            None => Err(DurationNanosOutOfRangeError { value, unit }),
239        }
240    }
241
242    /// Returns `true` if the duration is zero.
243    #[must_use]
244    pub const fn is_zero(&self) -> bool {
245        self.0 == 0
246    }
247
248    /// Returns the exact duration in nanoseconds as `u64`.
249    #[must_use]
250    pub const fn as_u64(&self) -> u64 {
251        self.0
252    }
253
254    /// Returns the total duration in whole microseconds.
255    #[must_use]
256    pub const fn as_micros(&self) -> u64 {
257        self.0 / NANOSECONDS_IN_MICROSECOND
258    }
259
260    /// Returns the total duration in whole milliseconds.
261    #[must_use]
262    pub const fn as_millis(&self) -> u64 {
263        self.0 / NANOSECONDS_IN_MILLISECOND
264    }
265
266    /// Returns the total duration in whole seconds.
267    #[must_use]
268    pub const fn as_secs(&self) -> u64 {
269        self.0 / NANOSECONDS_IN_SECOND
270    }
271
272    /// Returns the total duration in seconds as `f64`.
273    #[must_use]
274    #[expect(
275        clippy::cast_precision_loss,
276        reason = "subnanosecond precision is unavailable and large durations may lose precision"
277    )]
278    pub const fn as_secs_f64(&self) -> f64 {
279        self.as_secs() as f64 + self.subsec_nanos() as f64 / NANOSECONDS_IN_SECOND as f64
280    }
281
282    /// Returns the total duration in whole minutes.
283    #[must_use]
284    pub const fn as_mins(&self) -> u64 {
285        self.0 / NANOSECONDS_IN_MINUTE
286    }
287
288    /// Returns the total duration in whole hours.
289    #[must_use]
290    pub const fn as_hours(&self) -> u64 {
291        self.0 / (SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND)
292    }
293
294    /// Returns the total duration in whole 24-hour days.
295    #[must_use]
296    pub const fn as_days(&self) -> u64 {
297        self.0 / NANOSECONDS_IN_DAY
298    }
299
300    /// Returns the fractional part of this duration in whole milliseconds.
301    #[must_use]
302    pub const fn subsec_millis(&self) -> u64 {
303        (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MILLISECOND
304    }
305
306    /// Returns the fractional part of this duration in whole microseconds.
307    #[must_use]
308    pub const fn subsec_micros(&self) -> u64 {
309        (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MICROSECOND
310    }
311
312    /// Returns the fractional part of this duration in nanoseconds.
313    #[must_use]
314    pub const fn subsec_nanos(&self) -> u64 {
315        self.0 % NANOSECONDS_IN_SECOND
316    }
317
318    /// Returns `Some(self + rhs)` or `None` if the addition would overflow.
319    #[must_use]
320    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
321        match self.0.checked_add(rhs.0) {
322            Some(value) => Some(Self(value)),
323            None => None,
324        }
325    }
326
327    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow.
328    #[must_use]
329    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
330        match self.0.checked_sub(rhs.0) {
331            Some(value) => Some(Self(value)),
332            None => None,
333        }
334    }
335
336    /// Adds `rhs`, saturating at [`DurationNanos::MAX`].
337    #[must_use]
338    pub const fn saturating_add(self, rhs: Self) -> Self {
339        Self(self.0.saturating_add(rhs.0))
340    }
341
342    /// Subtracts `rhs`, saturating at zero.
343    #[must_use]
344    pub const fn saturating_sub(self, rhs: Self) -> Self {
345        Self(self.0.saturating_sub(rhs.0))
346    }
347
348    /// Returns `Some(self * rhs)` or `None` if the multiplication would overflow.
349    #[must_use]
350    pub const fn checked_mul(self, rhs: u64) -> Option<Self> {
351        match self.0.checked_mul(rhs) {
352            Some(value) => Some(Self(value)),
353            None => None,
354        }
355    }
356
357    /// Multiplies by `rhs`, saturating at [`DurationNanos::MAX`].
358    #[must_use]
359    pub const fn saturating_mul(self, rhs: u64) -> Self {
360        Self(self.0.saturating_mul(rhs))
361    }
362
363    /// Returns `Some(self / rhs)` or `None` if `rhs` is zero.
364    #[must_use]
365    pub const fn checked_div(self, rhs: u64) -> Option<Self> {
366        match self.0.checked_div(rhs) {
367            Some(value) => Some(Self(value)),
368            None => None,
369        }
370    }
371}
372
373/// Represents a timestamp in nanoseconds since the UNIX epoch.
374#[repr(C)]
375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
376pub struct UnixNanos(u64);
377
378impl UnixNanos {
379    /// Creates a new [`UnixNanos`] instance.
380    #[must_use]
381    pub const fn new(value: u64) -> Self {
382        Self(value)
383    }
384
385    /// Creates a new [`UnixNanos`] instance with the maximum valid value.
386    #[must_use]
387    pub const fn max() -> Self {
388        Self(u64::MAX)
389    }
390
391    /// Returns `true` if the value of this instance is zero.
392    #[must_use]
393    pub const fn is_zero(&self) -> bool {
394        self.0 == 0
395    }
396
397    /// Returns the underlying value as `u64`.
398    #[must_use]
399    pub const fn as_u64(&self) -> u64 {
400        self.0
401    }
402
403    /// Returns the timestamp as seconds, truncating sub-second precision.
404    #[must_use]
405    pub const fn as_seconds(&self) -> u64 {
406        self.0 / NANOSECONDS_IN_SECOND
407    }
408
409    /// Returns the timestamp as milliseconds, truncating sub-millisecond precision.
410    #[must_use]
411    pub const fn as_millis(&self) -> u64 {
412        self.0 / NANOSECONDS_IN_MILLISECOND
413    }
414
415    /// Returns the timestamp as microseconds, truncating sub-microsecond precision.
416    #[must_use]
417    pub const fn as_micros(&self) -> u64 {
418        self.0 / NANOSECONDS_IN_MICROSECOND
419    }
420
421    /// Creates a new [`UnixNanos`] from a second timestamp.
422    ///
423    /// # Panics
424    ///
425    /// Panics if the result overflows `u64`.
426    #[must_use]
427    pub const fn from_seconds(seconds: u64) -> Self {
428        match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
429            Some(nanos) => Self(nanos),
430            None => panic!("UnixNanos overflow in from_seconds"),
431        }
432    }
433
434    /// Creates a new [`UnixNanos`] from a millisecond timestamp.
435    ///
436    /// # Panics
437    ///
438    /// Panics if the result overflows `u64`.
439    #[must_use]
440    pub const fn from_millis(millis: u64) -> Self {
441        match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
442            Some(nanos) => Self(nanos),
443            None => panic!("UnixNanos overflow in from_millis"),
444        }
445    }
446
447    /// Creates a new [`UnixNanos`] from a signed millisecond timestamp.
448    ///
449    /// Returns `None` if `millis` is negative or the result overflows `u64`.
450    #[must_use]
451    pub const fn from_millis_checked(millis: i64) -> Option<Self> {
452        Self::from_units_checked(millis, NANOSECONDS_IN_MILLISECOND)
453    }
454
455    /// Creates a new [`UnixNanos`] from a microsecond timestamp.
456    ///
457    /// # Panics
458    ///
459    /// Panics if the result overflows `u64`.
460    #[must_use]
461    pub const fn from_micros(micros: u64) -> Self {
462        match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
463            Some(nanos) => Self(nanos),
464            None => panic!("UnixNanos overflow in from_micros"),
465        }
466    }
467
468    /// Creates a new [`UnixNanos`] from a signed microsecond timestamp.
469    ///
470    /// Returns `None` if `micros` is negative or the result overflows `u64`.
471    #[must_use]
472    pub const fn from_micros_checked(micros: i64) -> Option<Self> {
473        Self::from_units_checked(micros, NANOSECONDS_IN_MICROSECOND)
474    }
475
476    const fn from_units_checked(value: i64, nanos_per_unit: u64) -> Option<Self> {
477        if value < 0 {
478            return None;
479        }
480
481        match value.cast_unsigned().checked_mul(nanos_per_unit) {
482            Some(nanos) => Some(Self(nanos)),
483            None => None,
484        }
485    }
486
487    /// Returns the underlying value as `i64`.
488    ///
489    /// # Panics
490    ///
491    /// Panics if the value exceeds `i64::MAX` (approximately year 2262).
492    #[must_use]
493    pub const fn as_i64(&self) -> i64 {
494        assert!(
495            self.0 <= i64::MAX.cast_unsigned(),
496            "UnixNanos value exceeds i64::MAX"
497        );
498        self.0.cast_signed()
499    }
500
501    /// Returns the underlying value as `f64`.
502    #[must_use]
503    #[expect(
504        clippy::cast_precision_loss,
505        reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
506    )]
507    pub const fn as_f64(&self) -> f64 {
508        self.0 as f64
509    }
510
511    /// Converts the underlying value to a datetime (UTC).
512    ///
513    /// # Panics
514    ///
515    /// Panics if Jiff's supported timestamp range no longer includes all `u64` nanosecond values.
516    #[must_use]
517    pub fn to_datetime_utc(&self) -> Timestamp {
518        Timestamp::from_nanosecond(i128::from(self.0))
519            .expect("UnixNanos is within Jiff's timestamp range")
520    }
521
522    /// Converts the underlying value to an ISO 8601 (RFC 3339) string.
523    #[must_use]
524    pub fn to_rfc3339(&self) -> String {
525        let datetime = self.to_datetime_utc();
526        let display = datetime.display_with_offset(Offset::UTC);
527
528        match datetime.subsec_nanosecond() {
529            0 => format!("{display:.0}"),
530            nanos if nanos % 1_000_000 == 0 => format!("{display:.3}"),
531            nanos if nanos % 1_000 == 0 => format!("{display:.6}"),
532            _ => format!("{display:.9}"),
533        }
534    }
535
536    /// Calculates the duration in nanoseconds since another [`UnixNanos`] instance.
537    ///
538    /// Returns `Some(duration)` if `self` is later than `other`, otherwise `None` if `other` is
539    /// greater than `self` (indicating a negative duration is not possible with `DurationNanos`).
540    #[must_use]
541    pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
542        match self.0.checked_sub(other.0) {
543            Some(duration) => Some(DurationNanos(duration)),
544            None => None,
545        }
546    }
547
548    /// Calculates the duration in nanoseconds since `earlier`, saturating at zero.
549    #[must_use]
550    pub const fn saturating_duration_since(&self, earlier: Self) -> DurationNanos {
551        DurationNanos(self.0.saturating_sub(earlier.0))
552    }
553
554    /// Rounds this timestamp down to the nearest multiple of `interval` since the UNIX epoch.
555    ///
556    /// # Panics
557    ///
558    /// Panics if `interval` is zero.
559    #[must_use]
560    pub const fn floor(self, interval: DurationNanos) -> Self {
561        assert!(
562            !interval.is_zero(),
563            "cannot floor UnixNanos to a zero interval"
564        );
565        Self(self.0 - self.0 % interval.0)
566    }
567
568    fn parse_string(s: &str) -> Result<Self, String> {
569        // Try parsing as an integer (nanoseconds)
570        if let Ok(int_value) = s.parse::<u64>() {
571            return Ok(Self(int_value));
572        }
573
574        // If the string is composed solely of digits but didn't fit in a u64 we
575        // treat that as an overflow error rather than attempting to interpret
576        // it as seconds in floating-point form. This avoids the surprising
577        // situation where a caller provides nanoseconds but gets an out-of-
578        // range float interpretation instead.
579        if s.chars().all(|c| c.is_ascii_digit()) {
580            return Err("Unix timestamp is out of range".into());
581        }
582
583        // Try parsing as a floating point number (seconds)
584        if let Ok(float_value) = s.parse::<f64>() {
585            return f64_seconds_to_nanos(float_value).map(Self);
586        }
587
588        // The legacy parser accepted upper/lowercase `T` and a space separator, but not RFC 9557
589        // annotations. Preserve that input contract instead of adopting Jiff's broader grammar.
590        let is_compatible_rfc3339 = matches!(s.as_bytes().get(10), Some(b'T' | b't' | b' '))
591            && !s.as_bytes().contains(&b'[');
592        if is_compatible_rfc3339 && let Ok(datetime) = s.parse::<Timestamp>() {
593            let nanos = datetime.as_nanosecond();
594            let nanos = u64::try_from(nanos)
595                .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
596            return Ok(Self(nanos));
597        }
598
599        // The legacy `%Y-%m-%d` parser accepted one- or two-digit months and days.
600        if let Ok(date) = Date::strptime("%Y-%m-%d", s) {
601            let datetime = date
602                .at(0, 0, 0, 0)
603                .to_zoned(jiff::tz::TimeZone::UTC)
604                .map_err(|e| e.to_string())?;
605            let nanos = datetime.timestamp().as_nanosecond();
606            let nanos = u64::try_from(nanos)
607                .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
608            return Ok(Self(nanos));
609        }
610
611        Err(format!("Invalid format: {s}"))
612    }
613
614    /// Returns `Some(self + rhs)` or `None` if the addition would overflow
615    #[must_use]
616    pub const fn checked_add(self, rhs: DurationNanos) -> Option<Self> {
617        match self.0.checked_add(rhs.0) {
618            Some(value) => Some(Self(value)),
619            None => None,
620        }
621    }
622
623    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow
624    #[must_use]
625    pub const fn checked_sub(self, rhs: DurationNanos) -> Option<Self> {
626        match self.0.checked_sub(rhs.0) {
627            Some(value) => Some(Self(value)),
628            None => None,
629        }
630    }
631
632    /// Adds `rhs`, saturating at [`UnixNanos::max`].
633    #[must_use]
634    pub const fn saturating_add(self, rhs: DurationNanos) -> Self {
635        Self(self.0.saturating_add(rhs.0))
636    }
637
638    /// Subtracts `rhs`, saturating at zero.
639    #[must_use]
640    pub const fn saturating_sub(self, rhs: DurationNanos) -> Self {
641        Self(self.0.saturating_sub(rhs.0))
642    }
643}
644
645// Converts non-negative float seconds to nanoseconds, truncating (not rounding)
646// sub-nanosecond precision for consistency with `datetime::secs_to_nanos`.
647#[expect(
648    clippy::cast_possible_truncation,
649    clippy::cast_sign_loss,
650    reason = "value is checked finite, non-negative, and within u64 range before the cast"
651)]
652fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
653    if !value.is_finite() {
654        return Err(format!("Unix timestamp must be finite, was {value}"));
655    }
656
657    if value < 0.0 {
658        return Err("Unix timestamp cannot be negative".to_string());
659    }
660
661    // Convert seconds to nanoseconds while checking for overflow.
662    // We perform the multiplication in `f64`, then validate the
663    // result fits inside `u64` *before* truncating / casting.
664    let nanos_f64 = value * 1_000_000_000.0;
665
666    if nanos_f64 >= U64_UPPER_BOUND_F64 {
667        return Err(format!("Unix timestamp {value} seconds is out of range"));
668    }
669
670    Ok(nanos_f64.trunc() as u64)
671}
672
673impl From<DurationNanos> for Duration {
674    fn from(value: DurationNanos) -> Self {
675        Self::from_nanos(value.0)
676    }
677}
678
679impl From<DurationNanos> for SignedDuration {
680    fn from(value: DurationNanos) -> Self {
681        Self::from_nanos_i128(i128::from(value.0))
682    }
683}
684
685impl TryFrom<SignedDuration> for DurationNanos {
686    type Error = std::num::TryFromIntError;
687
688    fn try_from(value: SignedDuration) -> Result<Self, Self::Error> {
689        u64::try_from(value.as_nanos()).map(Self)
690    }
691}
692
693impl TryFrom<Duration> for DurationNanos {
694    type Error = std::num::TryFromIntError;
695
696    fn try_from(value: Duration) -> Result<Self, Self::Error> {
697        u64::try_from(value.as_nanos()).map(Self)
698    }
699}
700
701/// Adds two [`DurationNanos`] values.
702///
703/// # Panics
704///
705/// Panics if the result exceeds [`DurationNanos::MAX`]. Use
706/// [`DurationNanos::checked_add`] or [`DurationNanos::saturating_add`] for explicit overflow
707/// handling.
708impl Add for DurationNanos {
709    type Output = Self;
710
711    fn add(self, rhs: Self) -> Self::Output {
712        self.checked_add(rhs)
713            .expect("DurationNanos overflow in addition")
714    }
715}
716
717/// Subtracts one [`DurationNanos`] value from another.
718///
719/// # Panics
720///
721/// Panics if `rhs` exceeds `self`. Use [`DurationNanos::checked_sub`] or
722/// [`DurationNanos::saturating_sub`] for explicit underflow handling.
723impl Sub for DurationNanos {
724    type Output = Self;
725
726    fn sub(self, rhs: Self) -> Self::Output {
727        self.checked_sub(rhs)
728            .expect("DurationNanos underflow in subtraction")
729    }
730}
731
732/// Add-assigns a duration.
733///
734/// # Panics
735///
736/// Panics if the result exceeds [`DurationNanos::MAX`].
737impl AddAssign for DurationNanos {
738    fn add_assign(&mut self, rhs: Self) {
739        *self = *self + rhs;
740    }
741}
742
743/// Sub-assigns a duration.
744///
745/// # Panics
746///
747/// Panics if `rhs` exceeds `self`.
748impl SubAssign for DurationNanos {
749    fn sub_assign(&mut self, rhs: Self) {
750        *self = *self - rhs;
751    }
752}
753
754/// Multiplies a duration by an unsigned scalar.
755///
756/// # Panics
757///
758/// Panics if the result exceeds [`DurationNanos::MAX`]. Use
759/// [`DurationNanos::checked_mul`] or [`DurationNanos::saturating_mul`] for explicit overflow
760/// handling.
761impl Mul<u64> for DurationNanos {
762    type Output = Self;
763
764    fn mul(self, rhs: u64) -> Self::Output {
765        self.checked_mul(rhs)
766            .expect("DurationNanos overflow in multiplication")
767    }
768}
769
770/// Multiply-assigns a duration by an unsigned scalar.
771///
772/// # Panics
773///
774/// Panics if the result exceeds [`DurationNanos::MAX`].
775impl MulAssign<u64> for DurationNanos {
776    fn mul_assign(&mut self, rhs: u64) {
777        *self = *self * rhs;
778    }
779}
780
781/// Divides a duration by an unsigned scalar.
782///
783/// # Panics
784///
785/// Panics if `rhs` is zero. Use [`DurationNanos::checked_div`] when the divisor may be zero.
786impl Div<u64> for DurationNanos {
787    type Output = Self;
788
789    fn div(self, rhs: u64) -> Self::Output {
790        self.checked_div(rhs)
791            .expect("DurationNanos division by zero")
792    }
793}
794
795/// Divide-assigns a duration by an unsigned scalar.
796///
797/// # Panics
798///
799/// Panics if `rhs` is zero.
800impl DivAssign<u64> for DurationNanos {
801    fn div_assign(&mut self, rhs: u64) {
802        *self = *self / rhs;
803    }
804}
805
806impl Display for DurationNanos {
807    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808        write!(f, "{}", self.0)
809    }
810}
811
812impl Deref for UnixNanos {
813    type Target = u64;
814
815    fn deref(&self) -> &Self::Target {
816        &self.0
817    }
818}
819
820impl PartialEq<u64> for UnixNanos {
821    fn eq(&self, other: &u64) -> bool {
822        self.0 == *other
823    }
824}
825
826impl PartialOrd<u64> for UnixNanos {
827    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
828        self.0.partial_cmp(other)
829    }
830}
831
832impl PartialEq<Option<u64>> for UnixNanos {
833    fn eq(&self, other: &Option<u64>) -> bool {
834        match other {
835            Some(value) => self.0 == *value,
836            None => false,
837        }
838    }
839}
840
841impl PartialOrd<Option<u64>> for UnixNanos {
842    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
843        match other {
844            Some(value) => self.0.partial_cmp(value),
845            None => Some(Ordering::Greater),
846        }
847    }
848}
849
850impl PartialEq<UnixNanos> for u64 {
851    fn eq(&self, other: &UnixNanos) -> bool {
852        *self == other.0
853    }
854}
855
856impl PartialOrd<UnixNanos> for u64 {
857    fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
858        self.partial_cmp(&other.0)
859    }
860}
861
862impl From<u64> for UnixNanos {
863    fn from(value: u64) -> Self {
864        Self(value)
865    }
866}
867
868impl From<UnixNanos> for u64 {
869    fn from(value: UnixNanos) -> Self {
870        value.0
871    }
872}
873
874/// Converts a string slice to [`UnixNanos`].
875///
876/// # Panics
877///
878/// This implementation will panic if the string cannot be parsed into a valid [`UnixNanos`].
879/// This is intentional fail-fast behavior where invalid timestamps indicate a critical
880/// logic error that should halt execution rather than silently propagate incorrect data.
881///
882/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
883/// a [`Result`].
884impl From<&str> for UnixNanos {
885    fn from(value: &str) -> Self {
886        value
887            .parse()
888            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
889    }
890}
891
892/// Converts a [`String`] to [`UnixNanos`].
893///
894/// # Panics
895///
896/// This implementation will panic if the string cannot be parsed into a valid [`UnixNanos`].
897/// This is intentional fail-fast behavior where invalid timestamps indicate a critical
898/// logic error that should halt execution rather than silently propagate incorrect data.
899///
900/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
901/// a [`Result`].
902impl From<String> for UnixNanos {
903    fn from(value: String) -> Self {
904        value
905            .parse()
906            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
907    }
908}
909
910impl From<Timestamp> for UnixNanos {
911    fn from(value: Timestamp) -> Self {
912        let nanos = value.as_nanosecond();
913
914        assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
915
916        Self::from(u64::try_from(nanos).expect("DateTime timestamp out of range for UnixNanos"))
917    }
918}
919
920impl From<SystemTime> for UnixNanos {
921    fn from(value: SystemTime) -> Self {
922        let duration = value
923            .duration_since(std::time::UNIX_EPOCH)
924            .expect("SystemTime before UNIX EPOCH");
925
926        let nanos =
927            u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
928
929        Self::from(nanos)
930    }
931}
932
933impl FromStr for UnixNanos {
934    type Err = Box<dyn std::error::Error>;
935
936    fn from_str(s: &str) -> Result<Self, Self::Err> {
937        Self::parse_string(s).map_err(std::convert::Into::into)
938    }
939}
940
941/// Returns the elapsed duration between two timestamps.
942///
943/// # Panics
944///
945/// Panics if `rhs` is later than `self`. Use [`UnixNanos::duration_since`] or
946/// [`UnixNanos::saturating_duration_since`] when the timestamps may be out of order.
947impl Sub for UnixNanos {
948    type Output = DurationNanos;
949
950    fn sub(self, rhs: Self) -> Self::Output {
951        self.duration_since(&rhs)
952            .expect("UnixNanos underflow in timestamp subtraction")
953    }
954}
955
956/// Adds a duration to a timestamp.
957///
958/// # Panics
959///
960/// Panics on overflow. Use [`UnixNanos::checked_add`] or [`UnixNanos::saturating_add`] for
961/// explicit overflow handling.
962impl Add<DurationNanos> for UnixNanos {
963    type Output = Self;
964
965    fn add(self, rhs: DurationNanos) -> Self::Output {
966        self.checked_add(rhs)
967            .expect("UnixNanos overflow in duration addition")
968    }
969}
970
971/// Subtracts a duration from a timestamp.
972///
973/// # Panics
974///
975/// Panics on underflow. Use [`UnixNanos::checked_sub`] or [`UnixNanos::saturating_sub`] for
976/// explicit underflow handling.
977impl Sub<DurationNanos> for UnixNanos {
978    type Output = Self;
979
980    fn sub(self, rhs: DurationNanos) -> Self::Output {
981        self.checked_sub(rhs)
982            .expect("UnixNanos underflow in duration subtraction")
983    }
984}
985
986/// Add-assigns a duration to [`UnixNanos`].
987///
988/// # Panics
989///
990/// Panics on overflow. This is intentional fail-fast behavior for timestamp arithmetic.
991impl AddAssign<DurationNanos> for UnixNanos {
992    fn add_assign(&mut self, rhs: DurationNanos) {
993        *self = *self + rhs;
994    }
995}
996
997/// Sub-assigns a duration from [`UnixNanos`].
998///
999/// # Panics
1000///
1001/// Panics on underflow. This is intentional fail-fast behavior for timestamp arithmetic.
1002impl SubAssign<DurationNanos> for UnixNanos {
1003    fn sub_assign(&mut self, rhs: DurationNanos) {
1004        *self = *self - rhs;
1005    }
1006}
1007
1008impl Display for UnixNanos {
1009    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1010        write!(f, "{}", self.0)
1011    }
1012}
1013
1014impl From<UnixNanos> for Timestamp {
1015    fn from(value: UnixNanos) -> Self {
1016        value.to_datetime_utc()
1017    }
1018}
1019
1020impl<'de> Deserialize<'de> for UnixNanos {
1021    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1022    where
1023        D: Deserializer<'de>,
1024    {
1025        struct UnixNanosVisitor;
1026
1027        impl Visitor<'_> for UnixNanosVisitor {
1028            type Value = UnixNanos;
1029
1030            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1031                formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
1032            }
1033
1034            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1035            where
1036                E: de::Error,
1037            {
1038                Ok(UnixNanos(value))
1039            }
1040
1041            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1042            where
1043                E: de::Error,
1044            {
1045                u64::try_from(value)
1046                    .map(UnixNanos)
1047                    .map_err(|_| E::custom("Unix timestamp cannot be negative"))
1048            }
1049
1050            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1051            where
1052                E: de::Error,
1053            {
1054                f64_seconds_to_nanos(value)
1055                    .map(UnixNanos)
1056                    .map_err(E::custom)
1057            }
1058
1059            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1060            where
1061                E: de::Error,
1062            {
1063                UnixNanos::parse_string(value).map_err(E::custom)
1064            }
1065        }
1066
1067        deserializer.deserialize_any(UnixNanosVisitor)
1068    }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use jiff::SignedDuration;
1074    use rstest::rstest;
1075
1076    use super::*;
1077    use crate::approx_eq;
1078
1079    fn timestamp(value: &str) -> Timestamp {
1080        value.parse().unwrap()
1081    }
1082
1083    #[rstest]
1084    fn test_duration_nanos_construction_and_conversion() {
1085        let duration = DurationNanos::new(123);
1086        let standard = Duration::from(duration);
1087        let signed = SignedDuration::from(duration);
1088        let signed_max = SignedDuration::from(DurationNanos::MAX);
1089
1090        assert_eq!(duration.as_u64(), 123);
1091        assert_eq!(standard, Duration::from_nanos(123));
1092        assert_eq!(DurationNanos::try_from(standard), Ok(duration));
1093        assert_eq!(signed, SignedDuration::from_nanos(123));
1094        assert_eq!(DurationNanos::try_from(signed), Ok(duration));
1095        assert_eq!(signed_max.as_nanos(), i128::from(u64::MAX));
1096        assert_eq!(DurationNanos::try_from(signed_max), Ok(DurationNanos::MAX));
1097        assert!(DurationNanos::try_from(SignedDuration::from_nanos(-1)).is_err());
1098    }
1099
1100    #[rstest]
1101    fn test_duration_nanos_unit_construction_and_accessors() {
1102        let duration = DurationNanos::from_hours(1)
1103            + DurationNanos::from_mins(2)
1104            + DurationNanos::from_secs(3)
1105            + DurationNanos::new(456_789_123);
1106
1107        assert_eq!(DurationNanos::from_micros(1), DurationNanos::new(1_000));
1108        assert_eq!(DurationNanos::from_millis(1), DurationNanos::new(1_000_000));
1109        assert_eq!(DurationNanos::from_days(1), DurationNanos::from_hours(24));
1110        assert_eq!(duration.as_micros(), 3_723_456_789);
1111        assert_eq!(duration.as_millis(), 3_723_456);
1112        assert_eq!(duration.as_secs(), 3_723);
1113        assert!(approx_eq!(
1114            f64,
1115            duration.as_secs_f64(),
1116            3_723.456_789_123,
1117            epsilon = 1e-12
1118        ));
1119        let expected_max_secs = Duration::from_nanos(u64::MAX).as_secs_f64();
1120        assert!(approx_eq!(
1121            f64,
1122            DurationNanos::MAX.as_secs_f64(),
1123            expected_max_secs,
1124            epsilon = expected_max_secs * f64::EPSILON
1125        ));
1126        assert_eq!(duration.as_mins(), 62);
1127        assert_eq!(duration.as_hours(), 1);
1128        assert_eq!(DurationNanos::from_hours(49).as_days(), 2);
1129        assert_eq!(duration.subsec_millis(), 456);
1130        assert_eq!(duration.subsec_micros(), 456_789);
1131        assert_eq!(duration.subsec_nanos(), 456_789_123);
1132    }
1133
1134    #[rstest]
1135    fn test_duration_nanos_fallible_unit_construction() {
1136        assert_eq!(
1137            DurationNanos::try_from_secs(1),
1138            Ok(DurationNanos::from_secs(1))
1139        );
1140        assert_eq!(
1141            DurationNanos::try_from_secs(u64::MAX)
1142                .unwrap_err()
1143                .to_string(),
1144            "duration 18446744073709551615 seconds exceeds the nanosecond range"
1145        );
1146        assert!(DurationNanos::try_from_micros(u64::MAX).is_err());
1147        assert!(DurationNanos::try_from_millis(u64::MAX).is_err());
1148        assert!(DurationNanos::try_from_secs(u64::MAX).is_err());
1149        assert!(DurationNanos::try_from_mins(u64::MAX).is_err());
1150        assert!(DurationNanos::try_from_hours(u64::MAX).is_err());
1151        assert!(DurationNanos::try_from_days(u64::MAX).is_err());
1152
1153        let max_hours = DurationNanos::MAX.as_hours();
1154        assert_eq!(
1155            DurationNanos::try_from_hours(max_hours),
1156            Ok(DurationNanos::from_hours(max_hours))
1157        );
1158        assert!(DurationNanos::try_from_hours(max_hours + 1).is_err());
1159
1160        let max_days = DurationNanos::MAX.as_days();
1161        assert_eq!(
1162            DurationNanos::try_from_days(max_days),
1163            Ok(DurationNanos::from_days(max_days))
1164        );
1165        assert!(DurationNanos::try_from_days(max_days + 1).is_err());
1166    }
1167
1168    #[rstest]
1169    fn test_duration_nanos_zero_and_max() {
1170        assert_eq!(DurationNanos::default(), DurationNanos::ZERO);
1171        assert!(DurationNanos::ZERO.is_zero());
1172        assert_eq!(DurationNanos::MAX.as_u64(), u64::MAX);
1173        assert!(!DurationNanos::MAX.is_zero());
1174    }
1175
1176    #[rstest]
1177    fn test_duration_nanos_layout_matches_u64() {
1178        assert_eq!(
1179            std::mem::size_of::<DurationNanos>(),
1180            std::mem::size_of::<u64>()
1181        );
1182        assert_eq!(
1183            std::mem::align_of::<DurationNanos>(),
1184            std::mem::align_of::<u64>()
1185        );
1186    }
1187
1188    #[rstest]
1189    fn test_duration_nanos_format_and_ordering() {
1190        let shorter = DurationNanos::new(123);
1191        let longer = DurationNanos::new(456);
1192
1193        assert_eq!(shorter.to_string(), "123");
1194        assert_eq!(format!("{shorter:?}"), "DurationNanos(123)");
1195        assert!(shorter < longer);
1196        assert_eq!(shorter, DurationNanos::new(123));
1197    }
1198
1199    #[rstest]
1200    fn test_duration_nanos_serde_preserves_u64_format() {
1201        let duration = DurationNanos::MAX;
1202        let json = serde_json::to_string(&duration).unwrap();
1203        let deserialized: DurationNanos = serde_json::from_str(&json).unwrap();
1204
1205        assert_eq!(json, u64::MAX.to_string());
1206        assert_eq!(deserialized, duration);
1207    }
1208
1209    #[rstest]
1210    #[case("-1")]
1211    #[case("1.5")]
1212    #[case("\"1\"")]
1213    fn test_duration_nanos_serde_rejects_non_u64_formats(#[case] json: &str) {
1214        assert!(serde_json::from_str::<DurationNanos>(json).is_err());
1215    }
1216
1217    #[rstest]
1218    fn test_duration_nanos_checked_arithmetic_boundaries() {
1219        let zero = DurationNanos::ZERO;
1220        let one = DurationNanos::new(1);
1221        let max = DurationNanos::MAX;
1222
1223        assert_eq!(zero.checked_sub(one), None);
1224        assert_eq!(max.checked_add(one), None);
1225        assert_eq!(one.checked_sub(one), Some(zero));
1226        assert_eq!(zero.checked_add(max), Some(max));
1227    }
1228
1229    #[rstest]
1230    fn test_duration_nanos_saturating_arithmetic_boundaries() {
1231        let zero = DurationNanos::ZERO;
1232        let one = DurationNanos::new(1);
1233        let max = DurationNanos::MAX;
1234
1235        assert_eq!(zero.saturating_sub(one), zero);
1236        assert_eq!(max.saturating_add(one), max);
1237        assert_eq!(max.saturating_mul(2), max);
1238    }
1239
1240    #[rstest]
1241    fn test_duration_nanos_scalar_arithmetic() {
1242        let duration = DurationNanos::new(12);
1243
1244        assert_eq!(duration.checked_mul(3), Some(DurationNanos::new(36)));
1245        assert_eq!(DurationNanos::MAX.checked_mul(2), None);
1246        assert_eq!(duration.checked_div(3), Some(DurationNanos::new(4)));
1247        assert_eq!(duration.checked_div(0), None);
1248        assert_eq!(duration * 3, DurationNanos::new(36));
1249        assert_eq!(duration / 3, DurationNanos::new(4));
1250    }
1251
1252    #[rstest]
1253    fn test_duration_nanos_assign_operators() {
1254        let mut value = DurationNanos::new(100);
1255        value += DurationNanos::new(23);
1256        assert_eq!(value, DurationNanos::new(123));
1257        value -= DurationNanos::new(23);
1258        assert_eq!(value, DurationNanos::new(100));
1259        value *= 3;
1260        assert_eq!(value, DurationNanos::new(300));
1261        value /= 4;
1262        assert_eq!(value, DurationNanos::new(75));
1263    }
1264
1265    #[rstest]
1266    #[should_panic(expected = "DurationNanos overflow in addition")]
1267    fn test_duration_nanos_addition_panics_on_overflow() {
1268        let _ = DurationNanos::MAX + DurationNanos::new(1);
1269    }
1270
1271    #[rstest]
1272    #[should_panic(expected = "DurationNanos underflow in subtraction")]
1273    fn test_duration_nanos_subtraction_panics_on_underflow() {
1274        let _ = DurationNanos::default() - DurationNanos::new(1);
1275    }
1276
1277    #[rstest]
1278    #[should_panic(expected = "DurationNanos overflow in multiplication")]
1279    fn test_duration_nanos_multiplication_panics_on_overflow() {
1280        let _ = DurationNanos::MAX * 2;
1281    }
1282
1283    #[rstest]
1284    #[should_panic(expected = "DurationNanos division by zero")]
1285    fn test_duration_nanos_division_panics_on_zero() {
1286        let _ = DurationNanos::new(1) / 0;
1287    }
1288
1289    #[rstest]
1290    #[should_panic(expected = "DurationNanos overflow in from_micros")]
1291    fn test_duration_nanos_from_micros_panics_on_overflow() {
1292        let _ = DurationNanos::from_micros(u64::MAX);
1293    }
1294
1295    #[rstest]
1296    #[should_panic(expected = "DurationNanos overflow in from_millis")]
1297    fn test_duration_nanos_from_millis_panics_on_overflow() {
1298        let _ = DurationNanos::from_millis(u64::MAX);
1299    }
1300
1301    #[rstest]
1302    #[should_panic(expected = "DurationNanos overflow in from_secs")]
1303    fn test_duration_nanos_from_secs_panics_on_overflow() {
1304        let _ = DurationNanos::from_secs(u64::MAX);
1305    }
1306
1307    #[rstest]
1308    #[should_panic(expected = "DurationNanos overflow in from_mins")]
1309    fn test_duration_nanos_from_mins_panics_on_overflow() {
1310        let _ = DurationNanos::from_mins(u64::MAX);
1311    }
1312
1313    #[rstest]
1314    #[should_panic(expected = "DurationNanos overflow in from_hours")]
1315    fn test_duration_nanos_from_hours_panics_on_overflow() {
1316        let _ = DurationNanos::from_hours(u64::MAX);
1317    }
1318
1319    #[rstest]
1320    #[should_panic(expected = "DurationNanos overflow in from_days")]
1321    fn test_duration_nanos_from_days_panics_on_overflow() {
1322        let _ = DurationNanos::from_days(u64::MAX);
1323    }
1324
1325    #[rstest]
1326    fn test_new() {
1327        let nanos = UnixNanos::new(123);
1328        assert_eq!(nanos.as_u64(), 123);
1329        assert_eq!(nanos.as_i64(), 123);
1330    }
1331
1332    #[rstest]
1333    fn test_max() {
1334        let nanos = UnixNanos::max();
1335        assert_eq!(nanos.as_u64(), u64::MAX);
1336    }
1337
1338    #[rstest]
1339    fn test_is_zero() {
1340        assert!(UnixNanos::default().is_zero());
1341        assert!(!UnixNanos::max().is_zero());
1342    }
1343
1344    #[rstest]
1345    fn test_from_u64() {
1346        let nanos = UnixNanos::from(123);
1347        assert_eq!(nanos.as_u64(), 123);
1348        assert_eq!(nanos.as_i64(), 123);
1349    }
1350
1351    #[rstest]
1352    fn test_default() {
1353        let nanos = UnixNanos::default();
1354        assert_eq!(nanos.as_u64(), 0);
1355        assert_eq!(nanos.as_i64(), 0);
1356    }
1357
1358    #[rstest]
1359    fn test_into_from() {
1360        let nanos: UnixNanos = 456.into();
1361        let value: u64 = nanos.into();
1362        assert_eq!(value, 456);
1363    }
1364
1365    #[rstest]
1366    #[case(0, "1970-01-01T00:00:00+00:00")]
1367    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1368    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1369    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1370    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1371    fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
1372        let nanos = UnixNanos::from(nanos);
1373        let datetime = nanos.to_datetime_utc();
1374        assert_eq!(
1375            datetime.display_with_offset(Offset::UTC).to_string(),
1376            expected
1377        );
1378    }
1379
1380    #[rstest]
1381    #[case(0, "1970-01-01T00:00:00+00:00")]
1382    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1383    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1384    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1385    #[case(1_500_000_000_500_000_000, "2017-07-14T02:40:00.500+00:00")]
1386    #[case(1_500_000_000_123_456_000, "2017-07-14T02:40:00.123456+00:00")]
1387    #[case(1_500_000_000_123_456_789, "2017-07-14T02:40:00.123456789+00:00")]
1388    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1389    fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
1390        let nanos = UnixNanos::from(nanos);
1391        assert_eq!(nanos.to_rfc3339(), expected);
1392    }
1393
1394    #[rstest]
1395    fn test_from_str() {
1396        let nanos: UnixNanos = "123".parse().unwrap();
1397        assert_eq!(nanos.as_u64(), 123);
1398    }
1399
1400    #[rstest]
1401    fn test_from_str_invalid() {
1402        let result = "abc".parse::<UnixNanos>();
1403        assert!(result.is_err());
1404    }
1405
1406    #[rstest]
1407    fn test_from_str_date() {
1408        let nanos: UnixNanos = "2024-02-10".parse().unwrap();
1409        assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
1410    }
1411
1412    #[rstest]
1413    fn test_from_str_pre_epoch_date() {
1414        let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
1415        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1416    }
1417
1418    #[rstest]
1419    fn test_from_str_pre_epoch_rfc3339() {
1420        let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
1421        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1422    }
1423
1424    #[rstest]
1425    fn test_from_borrowed_str_and_string() {
1426        assert_eq!(UnixNanos::from("123").as_u64(), 123);
1427        assert_eq!(UnixNanos::from("123".to_string()).as_u64(), 123);
1428    }
1429
1430    #[rstest]
1431    #[should_panic(expected = "Failed to parse string")]
1432    fn test_from_borrowed_str_panics_on_invalid() {
1433        let _ = UnixNanos::from("abc");
1434    }
1435
1436    #[rstest]
1437    #[should_panic(expected = "Failed to parse string")]
1438    fn test_from_string_trait_panics_on_invalid() {
1439        let _ = UnixNanos::from("abc".to_string());
1440    }
1441
1442    #[rstest]
1443    fn test_into_timestamp() {
1444        let nanos = UnixNanos::from(1_000_000_000);
1445        let datetime = Timestamp::from(nanos);
1446        assert_eq!(datetime, timestamp("1970-01-01T00:00:01Z"));
1447        assert_eq!(UnixNanos::from(datetime), nanos);
1448    }
1449
1450    #[rstest]
1451    fn test_try_from_datetime_valid() {
1452        let datetime = Timestamp::from_second(1_000_000_000).unwrap();
1453        let nanos = UnixNanos::from(datetime);
1454        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1455    }
1456
1457    #[rstest]
1458    fn test_from_system_time() {
1459        let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
1460        let nanos = UnixNanos::from(system_time);
1461        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1462    }
1463
1464    #[rstest]
1465    #[should_panic(expected = "SystemTime before UNIX EPOCH")]
1466    fn test_from_system_time_before_epoch() {
1467        let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
1468        let _ = UnixNanos::from(system_time);
1469    }
1470
1471    #[rstest]
1472    #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
1473    fn test_from_system_time_overflow_panics() {
1474        // One second beyond the largest whole-second duration representable in u64 nanoseconds
1475        let system_time =
1476            std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
1477        let _ = UnixNanos::from(system_time);
1478    }
1479
1480    #[rstest]
1481    fn test_eq() {
1482        let nanos = UnixNanos::from(100);
1483        assert_eq!(nanos, 100);
1484        assert_eq!(nanos, Some(100));
1485        assert_ne!(nanos, 200);
1486        assert_ne!(nanos, Some(200));
1487        assert_ne!(nanos, None);
1488    }
1489
1490    #[rstest]
1491    fn test_partial_cmp() {
1492        let nanos = UnixNanos::from(100);
1493        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
1494        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
1495        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
1496        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
1497    }
1498
1499    #[rstest]
1500    fn test_u64_comparison_and_deref() {
1501        let nanos = UnixNanos::from(100);
1502        assert_eq!(100u64, nanos);
1503        assert_eq!(99u64.partial_cmp(&nanos), Some(Ordering::Less));
1504        assert_eq!(nanos.partial_cmp(&Some(100u64)), Some(Ordering::Equal));
1505        assert_eq!(*nanos, 100u64);
1506    }
1507
1508    #[rstest]
1509    fn test_edge_case_max_value() {
1510        let nanos = UnixNanos::from(u64::MAX);
1511        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
1512    }
1513
1514    #[rstest]
1515    fn test_display() {
1516        let nanos = UnixNanos::from(123);
1517        assert_eq!(format!("{nanos}"), "123");
1518    }
1519
1520    #[rstest]
1521    fn test_addition() {
1522        let nanos = UnixNanos::from(100);
1523        let duration = DurationNanos::new(200);
1524        let result = nanos + duration;
1525        assert_eq!(result.as_u64(), 300);
1526    }
1527
1528    #[rstest]
1529    fn test_add_assign() {
1530        let mut nanos = UnixNanos::from(100);
1531        nanos += DurationNanos::new(50);
1532        assert_eq!(nanos.as_u64(), 150);
1533    }
1534
1535    #[rstest]
1536    fn test_subtraction() {
1537        let nanos1 = UnixNanos::from(200);
1538        let nanos2 = UnixNanos::from(100);
1539        let result = nanos1 - nanos2;
1540        assert_eq!(result, DurationNanos::new(100));
1541    }
1542
1543    #[rstest]
1544    fn test_sub_assign() {
1545        let mut nanos = UnixNanos::from(200);
1546        nanos -= DurationNanos::new(50);
1547        assert_eq!(nanos.as_u64(), 150);
1548    }
1549
1550    #[rstest]
1551    #[should_panic(expected = "UnixNanos overflow")]
1552    fn test_overflow_add() {
1553        let nanos = UnixNanos::from(u64::MAX);
1554        let _ = nanos + DurationNanos::new(1);
1555    }
1556
1557    #[rstest]
1558    #[should_panic(expected = "UnixNanos underflow")]
1559    fn test_overflow_sub() {
1560        let _ = UnixNanos::default() - DurationNanos::new(1);
1561    }
1562
1563    #[rstest]
1564    #[case(100, 50, Some(DurationNanos::new(50)))]
1565    #[case(1_000_000_000, 500_000_000, Some(DurationNanos::new(500_000_000)))]
1566    #[case(u64::MAX, u64::MAX - 1, Some(DurationNanos::new(1)))]
1567    #[case(50, 50, Some(DurationNanos::ZERO))]
1568    #[case(50, 100, None)]
1569    #[case(0, 1, None)]
1570    fn test_duration_since(
1571        #[case] time1: u64,
1572        #[case] time2: u64,
1573        #[case] expected: Option<DurationNanos>,
1574    ) {
1575        let nanos1 = UnixNanos::from(time1);
1576        let nanos2 = UnixNanos::from(time2);
1577        assert_eq!(nanos1.duration_since(&nanos2), expected);
1578    }
1579
1580    #[rstest]
1581    fn test_duration_since_same_moment() {
1582        let moment = UnixNanos::from(1_707_577_123_456_789_000);
1583        assert_eq!(
1584            moment.duration_since(&moment),
1585            Some(DurationNanos::default())
1586        );
1587    }
1588
1589    #[rstest]
1590    #[case::later(100, 50, DurationNanos::new(50))]
1591    #[case::same(50, 50, DurationNanos::ZERO)]
1592    #[case::earlier(50, 100, DurationNanos::ZERO)]
1593    #[case::full_range(u64::MAX, 0, DurationNanos::new(u64::MAX))]
1594    fn test_saturating_duration_since(
1595        #[case] time: u64,
1596        #[case] earlier: u64,
1597        #[case] expected: DurationNanos,
1598    ) {
1599        assert_eq!(
1600            UnixNanos::from(time).saturating_duration_since(UnixNanos::from(earlier)),
1601            expected
1602        );
1603    }
1604
1605    #[rstest]
1606    #[case(100, 30, 90)]
1607    #[case(90, 30, 90)]
1608    #[case(100, 101, 0)]
1609    #[case(u64::MAX, u64::MAX, u64::MAX)]
1610    #[case(u64::MAX, 1_000_000_000, 18_446_744_073_000_000_000)]
1611    fn test_floor(#[case] time: u64, #[case] interval: u64, #[case] expected: u64) {
1612        assert_eq!(
1613            UnixNanos::new(time).floor(DurationNanos::new(interval)),
1614            UnixNanos::new(expected)
1615        );
1616    }
1617
1618    #[rstest]
1619    #[should_panic(expected = "cannot floor UnixNanos to a zero interval")]
1620    fn test_floor_panics_for_zero_interval() {
1621        let _ = UnixNanos::default().floor(DurationNanos::ZERO);
1622    }
1623
1624    #[rstest]
1625    fn test_duration_since_chronological() {
1626        // Create a reference time (Feb 10, 2024)
1627        let earlier = timestamp("2024-02-10T12:00:00Z");
1628
1629        // Create a time 1 hour, 30 minutes, and 45 seconds later (with nanoseconds)
1630        let later = earlier
1631            + SignedDuration::from_hours(1)
1632            + SignedDuration::from_mins(30)
1633            + SignedDuration::from_secs(45)
1634            + SignedDuration::from_nanos(500_000_000);
1635
1636        let earlier_nanos = UnixNanos::from(earlier);
1637        let later_nanos = UnixNanos::from(later);
1638
1639        // Calculate expected duration in nanoseconds
1640        let expected_duration =
1641            (60 * 60 + 30 * 60 + 45) * NANOSECONDS_IN_SECOND + 500 * NANOSECONDS_IN_MILLISECOND;
1642
1643        assert_eq!(
1644            later_nanos.duration_since(&earlier_nanos),
1645            Some(DurationNanos::new(expected_duration))
1646        );
1647        assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
1648    }
1649
1650    #[rstest]
1651    fn test_duration_since_with_edge_cases() {
1652        // Test with maximum value
1653        let max = UnixNanos::from(u64::MAX);
1654        let smaller = UnixNanos::from(u64::MAX - 1000);
1655
1656        assert_eq!(max.duration_since(&smaller), Some(DurationNanos::new(1000)));
1657        assert_eq!(smaller.duration_since(&max), None);
1658
1659        // Test with minimum value
1660        let min = UnixNanos::default(); // Zero timestamp
1661        let larger = UnixNanos::from(1000);
1662
1663        assert_eq!(min.duration_since(&min), Some(DurationNanos::default()));
1664        assert_eq!(larger.duration_since(&min), Some(DurationNanos::new(1000)));
1665        assert_eq!(min.duration_since(&larger), None);
1666    }
1667
1668    #[rstest]
1669    fn test_serde_json() {
1670        let nanos = UnixNanos::from(123);
1671        let json = serde_json::to_string(&nanos).unwrap();
1672        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1673        assert_eq!(deserialized, nanos);
1674    }
1675
1676    #[rstest]
1677    fn test_serde_edge_cases() {
1678        let nanos = UnixNanos::from(u64::MAX);
1679        let json = serde_json::to_string(&nanos).unwrap();
1680        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1681        assert_eq!(deserialized, nanos);
1682    }
1683
1684    #[rstest]
1685    #[case("123", 123)] // Integer string
1686    #[case("1234.567", 1_234_567_000_000)] // Float string (seconds to nanos)
1687    #[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date (midnight UTC)
1688    #[case("2024-2-10", 1_707_523_200_000_000_000)] // Legacy-compatible short month
1689    #[case("2024-02-1", 1_706_745_600_000_000_000)] // Legacy-compatible short day
1690    #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
1691    #[case("2024-02-10t14:58:43Z", 1_707_577_123_000_000_000)] // Lowercase RFC3339 separator
1692    #[case("2024-02-10 14:58:43Z", 1_707_577_123_000_000_000)] // Space RFC3339 separator
1693    #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
1694    fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
1695        let parsed: UnixNanos = input.parse().unwrap();
1696        assert_eq!(parsed.as_u64(), expected);
1697    }
1698
1699    #[rstest]
1700    #[case("abc")] // Random string
1701    #[case("not a timestamp")] // Non-timestamp string
1702    #[case("2024-02-10 14:58:43")] // Space-separated format (not RFC3339)
1703    #[case("2024-02-10T14:58:43Z[UTC]")] // RFC 9557 annotation was not accepted previously
1704    fn test_from_str_invalid_formats(#[case] input: &str) {
1705        let result = input.parse::<UnixNanos>();
1706        assert!(result.is_err());
1707    }
1708
1709    #[rstest]
1710    fn test_from_str_integer_overflow() {
1711        // One more digit than u64::MAX (20 digits) so definitely overflows
1712        let input = "184467440737095516160";
1713        let result = input.parse::<UnixNanos>();
1714        assert!(result.is_err());
1715    }
1716
1717    #[rstest]
1718    fn test_checked_add_overflow_returns_none() {
1719        let max = UnixNanos::from(u64::MAX);
1720        assert_eq!(max.checked_add(DurationNanos::new(1)), None);
1721    }
1722
1723    #[rstest]
1724    fn test_checked_sub_underflow_returns_none() {
1725        let zero = UnixNanos::default();
1726        assert_eq!(zero.checked_sub(DurationNanos::new(1)), None);
1727    }
1728
1729    #[rstest]
1730    fn test_saturating_add_overflow() {
1731        let max = UnixNanos::from(u64::MAX);
1732        let result = max.saturating_add(DurationNanos::new(1));
1733        assert_eq!(result, UnixNanos::from(u64::MAX));
1734    }
1735
1736    #[rstest]
1737    fn test_saturating_sub_underflow() {
1738        let zero = UnixNanos::default();
1739        let result = zero.saturating_sub(DurationNanos::new(1));
1740        assert_eq!(result, UnixNanos::default());
1741    }
1742
1743    #[rstest]
1744    fn test_from_str_float_overflow() {
1745        // Use scientific notation so we take the floating-point parsing path.
1746        let input = "2e10"; // 20 billion seconds ~ 634 years (> u64::MAX nanoseconds)
1747        let err = input.parse::<UnixNanos>().unwrap_err();
1748        assert!(err.to_string().contains("out of range"));
1749    }
1750
1751    #[rstest]
1752    #[case("NaN")]
1753    #[case("nan")]
1754    #[case("inf")]
1755    #[case("-inf")]
1756    fn test_from_str_non_finite_float_errors(#[case] input: &str) {
1757        let err = input.parse::<UnixNanos>().unwrap_err();
1758        assert!(err.to_string().contains("must be finite"));
1759    }
1760
1761    #[rstest]
1762    #[case("-1.5")]
1763    #[case("-0.000001")]
1764    fn test_from_str_negative_float_errors(#[case] input: &str) {
1765        let err = input.parse::<UnixNanos>().unwrap_err();
1766        assert!(err.to_string().contains("cannot be negative"));
1767    }
1768
1769    #[rstest]
1770    fn test_deserialize_u64() {
1771        let json = "123456789";
1772        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1773        assert_eq!(deserialized.as_u64(), 123_456_789);
1774    }
1775
1776    #[rstest]
1777    fn test_deserialize_string_with_int() {
1778        let json = "\"123456789\"";
1779        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1780        assert_eq!(deserialized.as_u64(), 123_456_789);
1781    }
1782
1783    #[rstest]
1784    fn test_deserialize_float() {
1785        let json = "1234.567";
1786        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1787        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1788    }
1789
1790    #[rstest]
1791    fn test_deserialize_string_with_float() {
1792        let json = "\"1234.567\"";
1793        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1794        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1795    }
1796
1797    #[rstest]
1798    fn test_deserialize_float_uses_truncation() {
1799        // Truncation (not rounding) for consistency with secs_to_nanos() etc
1800        let json = "0.9999999999";
1801        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1802        assert_eq!(deserialized.as_u64(), 999_999_999); // Truncated, not rounded to 1B
1803    }
1804
1805    #[rstest]
1806    #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1807    #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1808    fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1809        let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1810        assert_eq!(deserialized.as_u64(), expected);
1811    }
1812
1813    #[rstest]
1814    fn test_deserialize_negative_int_fails() {
1815        let json = "-123456789";
1816        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1817        assert!(
1818            result
1819                .unwrap_err()
1820                .to_string()
1821                .contains("cannot be negative")
1822        );
1823    }
1824
1825    #[rstest]
1826    fn test_deserialize_negative_float_fails() {
1827        let json = "-1234.567";
1828        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1829        assert!(
1830            result
1831                .unwrap_err()
1832                .to_string()
1833                .contains("cannot be negative")
1834        );
1835    }
1836
1837    #[rstest]
1838    fn test_deserialize_nan_fails() {
1839        // JSON doesn't support NaN directly, test the internal deserializer
1840        use serde::de::{
1841            IntoDeserializer,
1842            value::{Error as ValueError, F64Deserializer},
1843        };
1844        let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1845        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1846        assert!(result.is_err());
1847        assert!(result.unwrap_err().to_string().contains("must be finite"));
1848    }
1849
1850    #[rstest]
1851    fn test_deserialize_infinity_fails() {
1852        use serde::de::{
1853            IntoDeserializer,
1854            value::{Error as ValueError, F64Deserializer},
1855        };
1856        let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1857        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1858        assert!(result.is_err());
1859        assert!(result.unwrap_err().to_string().contains("must be finite"));
1860    }
1861
1862    #[rstest]
1863    fn test_deserialize_negative_infinity_fails() {
1864        use serde::de::{
1865            IntoDeserializer,
1866            value::{Error as ValueError, F64Deserializer},
1867        };
1868        let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1869        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1870        assert!(result.is_err());
1871        assert!(result.unwrap_err().to_string().contains("must be finite"));
1872    }
1873
1874    #[rstest]
1875    fn test_deserialize_overflow_float_fails() {
1876        // Test a float that would overflow u64 when converted to nanoseconds
1877        // u64::MAX is ~18.4e18, so u64::MAX / 1e9 = ~18.4e9 seconds
1878        let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1879        assert!(result.is_err());
1880        assert!(result.unwrap_err().to_string().contains("out of range"));
1881    }
1882
1883    #[rstest]
1884    fn test_deserialize_float_u64_boundary_fails() {
1885        let deserializer = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(
1886            18_446_744_073.709_553,
1887        );
1888        let err = UnixNanos::deserialize(deserializer).unwrap_err();
1889        assert!(err.to_string().contains("out of range"));
1890    }
1891
1892    #[rstest]
1893    fn test_deserialize_invalid_string_fails() {
1894        let json = "\"not a timestamp\"";
1895        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1896        assert!(result.is_err());
1897    }
1898
1899    #[rstest]
1900    fn test_deserialize_edge_cases() {
1901        // Test zero
1902        let json = "0";
1903        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1904        assert_eq!(deserialized.as_u64(), 0);
1905
1906        // Test large value
1907        let json = "18446744073709551615"; // u64::MAX
1908        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1909        assert_eq!(deserialized.as_u64(), u64::MAX);
1910    }
1911
1912    #[rstest]
1913    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1914    fn test_as_i64_overflow_panics() {
1915        let nanos = UnixNanos::from(u64::MAX);
1916        let _ = nanos.as_i64(); // Should panic
1917    }
1918
1919    #[rstest]
1920    fn test_as_i64_at_i64_max_boundary() {
1921        let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
1922        assert_eq!(nanos.as_i64(), i64::MAX);
1923    }
1924
1925    #[rstest]
1926    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1927    fn test_as_i64_just_above_i64_max_panics() {
1928        let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
1929        let _ = nanos.as_i64();
1930    }
1931
1932    use proptest::prelude::*;
1933
1934    fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1935        prop_oneof![
1936            // Small values
1937            0u64..1_000_000u64,
1938            // Medium values (microseconds range)
1939            1_000_000u64..1_000_000_000_000u64,
1940            // Large values (nanoseconds since 1970)
1941            1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
1942            // Values above i64::MAX (sentinel range, GTC/infinity)
1943            (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
1944            // Edge cases
1945            Just(0u64),
1946            Just(1u64),
1947            Just(1_000_000_000u64),               // 1 second in nanos
1948            Just(1_000_000_000_000u64),           // ~2001 timestamp
1949            Just(1_700_000_000_000_000_000u64),   // ~2023 timestamp
1950            Just((i64::MAX / 2).cast_unsigned()), // Safe for doubling
1951            Just(i64::MAX.cast_unsigned()),       // i64 boundary
1952            Just(u64::MAX),                       // Sentinel / max value
1953        ]
1954        .prop_map(UnixNanos::from)
1955    }
1956
1957    fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1958        (unix_nanos_strategy(), unix_nanos_strategy())
1959    }
1960
1961    fn duration_nanos_strategy() -> impl Strategy<Value = DurationNanos> {
1962        any::<u64>().prop_map(DurationNanos::new)
1963    }
1964
1965    fn duration_nanos_pair_strategy() -> impl Strategy<Value = (DurationNanos, DurationNanos)> {
1966        (duration_nanos_strategy(), duration_nanos_strategy())
1967    }
1968
1969    proptest! {
1970        #[rstest]
1971        #[expect(
1972            clippy::float_cmp,
1973            clippy::cast_precision_loss,
1974            reason = "roundtrip: both sides go through the same u64->f64 cast"
1975        )]
1976        fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
1977            let value = nanos.as_u64();
1978            prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
1979            prop_assert_eq!(nanos.as_f64(), value as f64);
1980
1981            // Test i64 conversion only for values within i64 range
1982            if i64::try_from(value).is_ok() {
1983                prop_assert_eq!(nanos.as_i64(), value.cast_signed());
1984            }
1985        }
1986
1987        #[rstest]
1988        fn prop_duration_nanos_addition_commutative(
1989            (duration1, duration2) in duration_nanos_pair_strategy()
1990        ) {
1991            if let (Some(sum1), Some(sum2)) = (
1992                duration1.checked_add(duration2),
1993                duration2.checked_add(duration1)
1994            ) {
1995                prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1996            }
1997        }
1998
1999        #[rstest]
2000        fn prop_duration_nanos_addition_associative(
2001            duration1 in duration_nanos_strategy(),
2002            duration2 in duration_nanos_strategy(),
2003            duration3 in duration_nanos_strategy(),
2004        ) {
2005            let expected = duration1
2006                .checked_add(duration2)
2007                .and_then(|sum| sum.checked_add(duration3));
2008
2009            if let Some(expected) = expected {
2010                let left = (duration1 + duration2) + duration3;
2011                let right = duration1 + (duration2 + duration3);
2012                prop_assert_eq!(left, expected);
2013                prop_assert_eq!(right, expected);
2014            }
2015        }
2016
2017        #[rstest]
2018        fn prop_unix_nanos_duration_arithmetic_roundtrip(
2019            nanos in unix_nanos_strategy(),
2020            duration in duration_nanos_strategy(),
2021        ) {
2022            if let Some(sum) = nanos.checked_add(duration) {
2023                prop_assert_eq!(sum - duration, nanos);
2024                prop_assert_eq!(sum - nanos, duration);
2025            }
2026        }
2027
2028        #[rstest]
2029        fn prop_duration_nanos_zero_identity(duration in duration_nanos_strategy()) {
2030            let zero = DurationNanos::default();
2031            prop_assert_eq!(duration + zero, duration);
2032            prop_assert_eq!(zero + duration, duration);
2033            prop_assert!(zero.is_zero());
2034        }
2035
2036        #[rstest]
2037        fn prop_unix_nanos_ordering_consistency(
2038            (nanos1, nanos2) in unix_nanos_pair_strategy()
2039        ) {
2040            // Ordering operations should be consistent
2041            let eq = nanos1 == nanos2;
2042            let lt = nanos1 < nanos2;
2043            let gt = nanos1 > nanos2;
2044            let le = nanos1 <= nanos2;
2045            let ge = nanos1 >= nanos2;
2046
2047            // Exactly one of eq, lt, gt should be true
2048            let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
2049            prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
2050
2051            // Consistency checks
2052            prop_assert_eq!(le, eq || lt, "<= should equal == || <");
2053            prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
2054            prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
2055            prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
2056        }
2057
2058        #[rstest]
2059        fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
2060            // String serialization should round-trip correctly
2061            let string_repr = nanos.to_string();
2062            let parsed = UnixNanos::from_str(&string_repr);
2063            prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
2064            if let Ok(parsed_nanos) = parsed {
2065                prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
2066            }
2067        }
2068
2069        #[rstest]
2070        fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
2071            // DateTime conversion should be consistent (only test values within i64 range)
2072            if i64::try_from(nanos.as_u64()).is_ok() {
2073                let datetime = nanos.to_datetime_utc();
2074                let converted_back = UnixNanos::from(datetime);
2075                prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
2076
2077                // RFC3339 string should also round-trip for valid dates
2078                let rfc3339 = nanos.to_rfc3339();
2079                if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
2080                    prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
2081                }
2082            }
2083        }
2084
2085        #[rstest]
2086        fn prop_unix_nanos_duration_since(
2087            (nanos1, nanos2) in unix_nanos_pair_strategy()
2088        ) {
2089            // duration_since should be consistent with comparison and arithmetic
2090            let duration = nanos1.duration_since(&nanos2);
2091            let saturating_duration = nanos1.saturating_duration_since(nanos2);
2092
2093            if nanos1 >= nanos2 {
2094                // If nanos1 >= nanos2, duration should be Some and equal to difference
2095                prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
2096                if let Some(dur) = duration {
2097                    prop_assert_eq!(dur.as_u64(), nanos1.as_u64() - nanos2.as_u64(),
2098                        "Duration should equal the difference");
2099                    prop_assert_eq!(saturating_duration, dur,
2100                        "Saturating duration should equal the difference");
2101                    prop_assert_eq!(nanos2 + dur, nanos1,
2102                        "second + duration should equal first");
2103                }
2104            } else {
2105                // If nanos1 < nanos2, duration should be None
2106                prop_assert!(duration.is_none(), "Duration should be None when first < second");
2107                prop_assert_eq!(saturating_duration, DurationNanos::default(),
2108                    "Saturating duration should be zero when first < second");
2109            }
2110        }
2111
2112        #[rstest]
2113        fn prop_unix_nanos_checked_arithmetic(
2114            nanos in unix_nanos_strategy(),
2115            duration in duration_nanos_strategy(),
2116        ) {
2117            let checked_add = nanos.checked_add(duration);
2118            let checked_sub = nanos.checked_sub(duration);
2119
2120            if let Some(sum) = checked_add {
2121                prop_assert_eq!(sum, nanos + duration, "Checked add should match regular add when no overflow");
2122            }
2123
2124            if let Some(diff) = checked_sub {
2125                prop_assert_eq!(diff, nanos - duration, "Checked sub should match regular sub when no underflow");
2126            }
2127        }
2128
2129        #[rstest]
2130        fn prop_unix_nanos_saturating_arithmetic(
2131            nanos in unix_nanos_strategy(),
2132            duration in duration_nanos_strategy(),
2133        ) {
2134            let sat_add = nanos.saturating_add(duration);
2135            let sat_sub = nanos.saturating_sub(duration);
2136
2137            prop_assert!(sat_add >= nanos, "Saturating add result should be >= timestamp");
2138            prop_assert!(sat_sub <= nanos, "Saturating sub result should be <= timestamp");
2139
2140            if let Some(checked_sum) = nanos.checked_add(duration) {
2141                prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
2142            } else {
2143                prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
2144            }
2145
2146            if let Some(checked_diff) = nanos.checked_sub(duration) {
2147                prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
2148            } else {
2149                prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
2150            }
2151        }
2152
2153        #[rstest]
2154        fn prop_unix_nanos_assign_mirrors_op(
2155            nanos in unix_nanos_strategy(),
2156            duration in duration_nanos_strategy(),
2157        ) {
2158            if let Some(expected) = nanos.checked_add(duration) {
2159                let mut add_result = nanos;
2160                add_result += duration;
2161                prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
2162            }
2163
2164            if let Some(expected) = nanos.checked_sub(duration) {
2165                let mut sub_result = nanos;
2166                sub_result -= duration;
2167                prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
2168            }
2169        }
2170
2171        #[rstest]
2172        fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
2173            let json = serde_json::to_string(&nanos).unwrap();
2174            let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
2175            prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
2176        }
2177
2178        #[rstest]
2179        fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
2180            // Use IntoDeserializer to hit visit_f64 directly,
2181            // bypassing JSON text encoding ambiguity
2182            use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
2183            let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
2184            let result = UnixNanos::deserialize(deserializer);
2185
2186            let upper_bound = 2.0_f64.powi(64);
2187            if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 < upper_bound {
2188                prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
2189            } else {
2190                prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
2191            }
2192        }
2193    }
2194
2195    #[rstest]
2196    fn test_from_seconds_zero() {
2197        let nanos = UnixNanos::from_seconds(0);
2198        assert_eq!(nanos.as_u64(), 0);
2199    }
2200
2201    #[rstest]
2202    fn test_from_seconds_one() {
2203        let nanos = UnixNanos::from_seconds(1);
2204        assert_eq!(nanos.as_u64(), 1_000_000_000);
2205    }
2206
2207    #[rstest]
2208    fn test_from_seconds_realistic_timestamp() {
2209        let nanos = UnixNanos::from_seconds(1_700_000_000);
2210        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2211        assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2212    }
2213
2214    #[rstest]
2215    fn test_from_seconds_max_safe() {
2216        let max_seconds = u64::MAX / 1_000_000_000;
2217        let nanos = UnixNanos::from_seconds(max_seconds);
2218        assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
2219    }
2220
2221    #[rstest]
2222    fn test_from_millis_zero() {
2223        let nanos = UnixNanos::from_millis(0);
2224        assert_eq!(nanos.as_u64(), 0);
2225    }
2226
2227    #[rstest]
2228    fn test_from_millis_one() {
2229        let nanos = UnixNanos::from_millis(1);
2230        assert_eq!(nanos.as_u64(), 1_000_000);
2231    }
2232
2233    #[rstest]
2234    fn test_from_millis_one_second() {
2235        let nanos = UnixNanos::from_millis(1_000);
2236        assert_eq!(nanos.as_u64(), 1_000_000_000);
2237    }
2238
2239    #[rstest]
2240    fn test_from_millis_realistic_timestamp() {
2241        // 2023-11-14T22:13:20Z = 1700000000000 ms
2242        let nanos = UnixNanos::from_millis(1_700_000_000_000);
2243        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2244        assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2245    }
2246
2247    #[rstest]
2248    fn test_from_millis_max_safe() {
2249        let max_ms = u64::MAX / 1_000_000;
2250        let nanos = UnixNanos::from_millis(max_ms);
2251        assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
2252    }
2253
2254    #[rstest]
2255    fn test_from_millis_matches_manual_conversion() {
2256        let ms = 1_625_474_304_765_u64;
2257        let expected = ms * 1_000_000;
2258        assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
2259    }
2260
2261    #[rstest]
2262    #[case::valid(1_700_000_000_123, Some(1_700_000_000_123_000_000))]
2263    #[case::negative(-1, None)]
2264    #[case::overflow(i64::MAX, None)]
2265    #[case::zero(0, Some(0))]
2266    fn test_from_millis_checked(#[case] millis: i64, #[case] expected: Option<u64>) {
2267        assert_eq!(
2268            UnixNanos::from_millis_checked(millis).map(|value| value.as_u64()),
2269            expected
2270        );
2271    }
2272
2273    #[rstest]
2274    #[case(0, 0)]
2275    #[case(999_999_999, 0)]
2276    #[case(1_000_000_000, 1)]
2277    #[case(1_700_000_000_123_456_789, 1_700_000_000)]
2278    fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
2279        assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
2280    }
2281
2282    #[rstest]
2283    #[case(0, 0)]
2284    #[case(999_999, 0)]
2285    #[case(1_000_000, 1)]
2286    #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
2287    fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
2288        assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
2289    }
2290
2291    #[rstest]
2292    #[case(0, 0)]
2293    #[case(999, 0)]
2294    #[case(1_000, 1)]
2295    #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
2296    fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
2297        assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
2298    }
2299
2300    #[rstest]
2301    fn test_from_micros_zero() {
2302        let nanos = UnixNanos::from_micros(0);
2303        assert_eq!(nanos.as_u64(), 0);
2304    }
2305
2306    #[rstest]
2307    fn test_from_micros_one() {
2308        let nanos = UnixNanos::from_micros(1);
2309        assert_eq!(nanos.as_u64(), 1_000);
2310    }
2311
2312    #[rstest]
2313    fn test_from_micros_one_second() {
2314        let nanos = UnixNanos::from_micros(1_000_000);
2315        assert_eq!(nanos.as_u64(), 1_000_000_000);
2316    }
2317
2318    #[rstest]
2319    fn test_from_micros_one_millisecond() {
2320        let nanos = UnixNanos::from_micros(1_000);
2321        assert_eq!(nanos.as_u64(), 1_000_000);
2322        assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
2323    }
2324
2325    #[rstest]
2326    fn test_from_micros_realistic_timestamp() {
2327        let micros = 1_700_000_000_000_000_u64;
2328        let nanos = UnixNanos::from_micros(micros);
2329        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2330    }
2331
2332    #[rstest]
2333    fn test_from_micros_max_safe() {
2334        let max_us = u64::MAX / 1_000;
2335        let nanos = UnixNanos::from_micros(max_us);
2336        assert_eq!(nanos.as_u64(), max_us * 1_000);
2337    }
2338
2339    #[rstest]
2340    fn test_from_micros_matches_manual_conversion() {
2341        let us = 1_000_000_123_456_u64;
2342        let expected = us * 1_000;
2343        assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
2344    }
2345
2346    #[rstest]
2347    #[case::valid(1_700_000_000_123_456, Some(1_700_000_000_123_456_000))]
2348    #[case::negative(-1, None)]
2349    #[case::overflow(i64::MAX, None)]
2350    #[case::zero(0, Some(0))]
2351    fn test_from_micros_checked(#[case] micros: i64, #[case] expected: Option<u64>) {
2352        assert_eq!(
2353            UnixNanos::from_micros_checked(micros).map(|value| value.as_u64()),
2354            expected
2355        );
2356    }
2357
2358    #[rstest]
2359    fn test_from_seconds_millis_and_micros_consistency() {
2360        assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
2361        assert_eq!(
2362            UnixNanos::from_seconds(60),
2363            UnixNanos::from_micros(60_000_000)
2364        );
2365        assert_eq!(
2366            UnixNanos::from_millis(1_000),
2367            UnixNanos::from_micros(1_000_000)
2368        );
2369        assert_eq!(
2370            UnixNanos::from_millis(60_000),
2371            UnixNanos::from_micros(60_000_000)
2372        );
2373    }
2374
2375    #[rstest]
2376    fn test_from_millis_round_trip_to_datetime() {
2377        let ms = 1_707_577_123_456_u64;
2378        let nanos = UnixNanos::from_millis(ms);
2379        let dt = nanos.to_datetime_utc();
2380        assert_eq!(dt.as_millisecond().cast_unsigned(), ms);
2381    }
2382
2383    #[rstest]
2384    fn test_from_micros_preserves_sub_millisecond() {
2385        let micros = 1_700_000_000_000_123_u64;
2386        let nanos = UnixNanos::from_micros(micros);
2387        assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
2388    }
2389
2390    #[rstest]
2391    #[should_panic(expected = "UnixNanos overflow in from_seconds")]
2392    fn test_from_seconds_overflow_panics() {
2393        let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
2394    }
2395
2396    #[rstest]
2397    #[should_panic(expected = "UnixNanos overflow in from_millis")]
2398    fn test_from_millis_overflow_panics() {
2399        let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
2400    }
2401
2402    #[rstest]
2403    #[should_panic(expected = "UnixNanos overflow in from_micros")]
2404    fn test_from_micros_overflow_panics() {
2405        let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
2406    }
2407}