nautilus_core/
nanos.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A `UnixNanos` type for working with timestamps in nanoseconds since the UNIX epoch.
17
18use std::{
19    cmp::Ordering,
20    fmt::Display,
21    ops::{Add, AddAssign, Deref, Sub, SubAssign},
22    str::FromStr,
23};
24
25use chrono::{DateTime, Utc};
26use serde::{
27    Deserialize, Deserializer, Serialize,
28    de::{self, Visitor},
29};
30
31/// Represents a duration in nanoseconds.
32pub type DurationNanos = u64;
33
34/// Represents a timestamp in nanoseconds since the UNIX epoch.
35#[repr(C)]
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
37pub struct UnixNanos(u64);
38
39impl UnixNanos {
40    /// Creates a new [`UnixNanos`] instance.
41    pub const fn new(value: u64) -> Self {
42        Self(value)
43    }
44
45    /// Returns the underlying value as `u64`.
46    #[must_use]
47    pub const fn as_u64(&self) -> u64 {
48        self.0
49    }
50
51    /// Returns the underlying value as `i64`.
52    #[must_use]
53    pub const fn as_i64(&self) -> i64 {
54        self.0 as i64
55    }
56
57    /// Returns the underlying value as `f64`.
58    #[must_use]
59    pub const fn as_f64(&self) -> f64 {
60        self.0 as f64
61    }
62
63    /// Converts the underlying value to a datetime (UTC).
64    #[must_use]
65    pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
66        DateTime::from_timestamp_nanos(self.0 as i64)
67    }
68
69    /// Calculates the duration in nanoseconds since another [`UnixNanos`] instance.
70    ///
71    /// Returns `Some(duration)` if `self` is later than `other`, otherwise `None` if `other` is
72    /// greater than `self` (indicating a negative duration is not possible with `DurationNanos`).
73    #[must_use]
74    pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
75        self.0.checked_sub(other.0)
76    }
77}
78
79impl Deref for UnixNanos {
80    type Target = u64;
81
82    fn deref(&self) -> &Self::Target {
83        &self.0
84    }
85}
86
87impl PartialEq<u64> for UnixNanos {
88    fn eq(&self, other: &u64) -> bool {
89        self.0 == *other
90    }
91}
92
93impl PartialOrd<u64> for UnixNanos {
94    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
95        self.0.partial_cmp(other)
96    }
97}
98
99impl PartialEq<Option<u64>> for UnixNanos {
100    fn eq(&self, other: &Option<u64>) -> bool {
101        match other {
102            Some(value) => self.0 == *value,
103            None => false,
104        }
105    }
106}
107
108impl PartialOrd<Option<u64>> for UnixNanos {
109    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
110        match other {
111            Some(value) => self.0.partial_cmp(value),
112            None => Some(Ordering::Greater),
113        }
114    }
115}
116
117impl From<u64> for UnixNanos {
118    fn from(value: u64) -> Self {
119        Self(value)
120    }
121}
122
123impl From<UnixNanos> for u64 {
124    fn from(value: UnixNanos) -> Self {
125        value.0
126    }
127}
128
129impl From<&str> for UnixNanos {
130    fn from(value: &str) -> Self {
131        Self(
132            value
133                .parse()
134                .expect("`value` should be a valid integer string"),
135        )
136    }
137}
138
139impl From<String> for UnixNanos {
140    fn from(value: String) -> Self {
141        Self::from(value.as_str())
142    }
143}
144
145impl From<DateTime<Utc>> for UnixNanos {
146    fn from(value: DateTime<Utc>) -> Self {
147        Self::from(value.timestamp_nanos_opt().expect("Invalid timestamp") as u64)
148    }
149}
150
151impl FromStr for UnixNanos {
152    type Err = std::num::ParseIntError;
153
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        s.parse().map(UnixNanos)
156    }
157}
158
159impl Add for UnixNanos {
160    type Output = Self;
161    fn add(self, rhs: Self) -> Self::Output {
162        Self(
163            self.0
164                .checked_add(rhs.0)
165                .expect("Error adding with overflow"),
166        )
167    }
168}
169
170impl Sub for UnixNanos {
171    type Output = Self;
172    fn sub(self, rhs: Self) -> Self::Output {
173        Self(
174            self.0
175                .checked_sub(rhs.0)
176                .expect("Error subtracting with underflow"),
177        )
178    }
179}
180
181impl Add<u64> for UnixNanos {
182    type Output = Self;
183
184    fn add(self, rhs: u64) -> Self::Output {
185        Self(self.0.checked_add(rhs).expect("Error adding with overflow"))
186    }
187}
188
189impl Sub<u64> for UnixNanos {
190    type Output = Self;
191
192    fn sub(self, rhs: u64) -> Self::Output {
193        Self(
194            self.0
195                .checked_sub(rhs)
196                .expect("Error subtracting with underflow"),
197        )
198    }
199}
200
201impl<T: Into<u64>> AddAssign<T> for UnixNanos {
202    fn add_assign(&mut self, other: T) {
203        let other_u64 = other.into();
204        self.0 = self
205            .0
206            .checked_add(other_u64)
207            .expect("Error adding with overflow");
208    }
209}
210
211impl<T: Into<u64>> SubAssign<T> for UnixNanos {
212    fn sub_assign(&mut self, other: T) {
213        let other_u64 = other.into();
214        self.0 = self
215            .0
216            .checked_sub(other_u64)
217            .expect("Error subtracting with underflow");
218    }
219}
220
221impl Display for UnixNanos {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        write!(f, "{}", self.0)
224    }
225}
226
227impl From<UnixNanos> for DateTime<Utc> {
228    fn from(value: UnixNanos) -> Self {
229        value.to_datetime_utc()
230    }
231}
232
233impl<'de> Deserialize<'de> for UnixNanos {
234    /// Deserializes a `UnixNanos` from various formats:
235    /// * Integer values are interpreted as nanoseconds since the UNIX epoch
236    /// * Floating-point values are interpreted as seconds since the UNIX epoch (converted to nanoseconds)
237    /// * String values may be:
238    ///   - A numeric string (interpreted as nanoseconds).
239    ///   - A floating-point string (interpreted as seconds, converted to nanoseconds).
240    ///   - An RFC 3339 formatted timestamp (ISO 8601 with timezone).
241    ///
242    /// Negative timestamps are rejected with an error.
243    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
244    where
245        D: Deserializer<'de>,
246    {
247        struct UnixNanosVisitor;
248
249        impl Visitor<'_> for UnixNanosVisitor {
250            type Value = UnixNanos;
251
252            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
253                formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
254            }
255
256            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
257            where
258                E: de::Error,
259            {
260                Ok(UnixNanos(value))
261            }
262
263            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
264            where
265                E: de::Error,
266            {
267                if value < 0 {
268                    return Err(E::custom("Unix timestamp cannot be negative"));
269                }
270                Ok(UnixNanos(value as u64))
271            }
272
273            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
274            where
275                E: de::Error,
276            {
277                if value < 0.0 {
278                    return Err(E::custom("Unix timestamp cannot be negative"));
279                }
280                // Convert from seconds to nanoseconds
281                let nanos = (value * 1_000_000_000.0).round() as u64;
282                Ok(UnixNanos(nanos))
283            }
284
285            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
286            where
287                E: de::Error,
288            {
289                // Try parsing as an integer (nanoseconds)
290                if let Ok(int_value) = value.parse::<u64>() {
291                    return Ok(UnixNanos(int_value));
292                }
293
294                // Try parsing as a floating point number (seconds)
295                if let Ok(float_value) = value.parse::<f64>() {
296                    if float_value < 0.0 {
297                        return Err(E::custom("Unix timestamp cannot be negative"));
298                    }
299                    let nanos = (float_value * 1_000_000_000.0).round() as u64;
300                    return Ok(UnixNanos(nanos));
301                }
302
303                // Try parsing as an RFC 3339 timestamp
304                if let Ok(datetime) = DateTime::parse_from_rfc3339(value) {
305                    let nanos = datetime
306                        .timestamp_nanos_opt()
307                        .ok_or_else(|| E::custom("Timestamp out of range"))?;
308                    if nanos < 0 {
309                        return Err(E::custom("Unix timestamp cannot be negative"));
310                    }
311                    return Ok(UnixNanos(nanos as u64));
312                }
313
314                // If none of the above works, fail with an error
315                Err(E::custom(format!("Invalid format: {value}")))
316            }
317        }
318
319        deserializer.deserialize_any(UnixNanosVisitor)
320    }
321}
322
323////////////////////////////////////////////////////////////////////////////////
324// Tests
325////////////////////////////////////////////////////////////////////////////////
326#[cfg(test)]
327mod tests {
328    use chrono::{Duration, TimeZone};
329    use rstest::rstest;
330
331    use super::*;
332
333    #[rstest]
334    fn test_new() {
335        let nanos = UnixNanos::new(123);
336        assert_eq!(nanos.as_u64(), 123);
337        assert_eq!(nanos.as_i64(), 123);
338    }
339
340    #[rstest]
341    fn test_from_u64() {
342        let nanos = UnixNanos::from(123);
343        assert_eq!(nanos.as_u64(), 123);
344        assert_eq!(nanos.as_i64(), 123);
345    }
346
347    #[rstest]
348    fn test_default() {
349        let nanos = UnixNanos::default();
350        assert_eq!(nanos.as_u64(), 0);
351        assert_eq!(nanos.as_i64(), 0);
352    }
353
354    #[rstest]
355    fn test_into_from() {
356        let nanos: UnixNanos = 456.into();
357        let value: u64 = nanos.into();
358        assert_eq!(value, 456);
359    }
360
361    #[rstest]
362    #[case(0, "1970-01-01T00:00:00+00:00")]
363    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
364    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
365    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
366    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
367    fn test_as_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
368        let nanos = UnixNanos::from(nanos);
369        let datetime = nanos.to_datetime_utc();
370        assert_eq!(datetime.to_rfc3339(), expected);
371    }
372
373    #[rstest]
374    fn test_from_str() {
375        let nanos: UnixNanos = "123".parse().unwrap();
376        assert_eq!(nanos.as_u64(), 123);
377    }
378
379    #[rstest]
380    fn test_from_str_invalid() {
381        let result = "abc".parse::<UnixNanos>();
382        assert!(result.is_err());
383    }
384
385    #[rstest]
386    fn test_try_from_datetime_valid() {
387        use chrono::TimeZone;
388        let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); // 1 billion seconds since epoch
389        let nanos = UnixNanos::from(datetime);
390        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
391    }
392
393    #[rstest]
394    fn test_eq() {
395        let nanos = UnixNanos::from(100);
396        assert_eq!(nanos, 100);
397        assert_eq!(nanos, Some(100));
398        assert_ne!(nanos, 200);
399        assert_ne!(nanos, Some(200));
400        assert_ne!(nanos, None);
401    }
402
403    #[rstest]
404    fn test_partial_cmp() {
405        let nanos = UnixNanos::from(100);
406        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
407        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
408        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
409        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
410    }
411
412    #[rstest]
413    fn test_edge_case_max_value() {
414        let nanos = UnixNanos::from(u64::MAX);
415        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
416    }
417
418    #[rstest]
419    fn test_display() {
420        let nanos = UnixNanos::from(123);
421        assert_eq!(format!("{nanos}"), "123");
422    }
423
424    #[rstest]
425    fn test_addition() {
426        let nanos1 = UnixNanos::from(100);
427        let nanos2 = UnixNanos::from(200);
428        let result = nanos1 + nanos2;
429        assert_eq!(result.as_u64(), 300);
430    }
431
432    #[rstest]
433    fn test_add_assign() {
434        let mut nanos = UnixNanos::from(100);
435        nanos += 50_u64;
436        assert_eq!(nanos.as_u64(), 150);
437    }
438
439    #[rstest]
440    fn test_subtraction() {
441        let nanos1 = UnixNanos::from(200);
442        let nanos2 = UnixNanos::from(100);
443        let result = nanos1 - nanos2;
444        assert_eq!(result.as_u64(), 100);
445    }
446
447    #[rstest]
448    fn test_sub_assign() {
449        let mut nanos = UnixNanos::from(200);
450        nanos -= 50_u64;
451        assert_eq!(nanos.as_u64(), 150);
452    }
453
454    #[rstest]
455    #[should_panic(expected = "Error adding with overflow")]
456    fn test_overflow_add() {
457        let nanos = UnixNanos::from(u64::MAX);
458        let _ = nanos + UnixNanos::from(1); // This should panic due to overflow
459    }
460
461    #[rstest]
462    #[should_panic(expected = "Error adding with overflow")]
463    fn test_overflow_add_u64() {
464        let nanos = UnixNanos::from(u64::MAX);
465        let _ = nanos + 1_u64; // This should panic due to overflow
466    }
467
468    #[rstest]
469    #[should_panic(expected = "Error subtracting with underflow")]
470    fn test_overflow_sub() {
471        let _ = UnixNanos::default() - UnixNanos::from(1); // This should panic due to underflow
472    }
473
474    #[rstest]
475    #[should_panic(expected = "Error subtracting with underflow")]
476    fn test_overflow_sub_u64() {
477        let _ = UnixNanos::default() - 1_u64; // This should panic due to underflow
478    }
479
480    #[rstest]
481    #[case(100, 50, Some(50))]
482    #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
483    #[case(u64::MAX, u64::MAX - 1, Some(1))]
484    #[case(50, 50, Some(0))]
485    #[case(50, 100, None)]
486    #[case(0, 1, None)]
487    fn test_duration_since(
488        #[case] time1: u64,
489        #[case] time2: u64,
490        #[case] expected: Option<DurationNanos>,
491    ) {
492        let nanos1 = UnixNanos::from(time1);
493        let nanos2 = UnixNanos::from(time2);
494        assert_eq!(nanos1.duration_since(&nanos2), expected);
495    }
496
497    #[rstest]
498    fn test_duration_since_same_moment() {
499        let moment = UnixNanos::from(1_707_577_123_456_789_000);
500        assert_eq!(moment.duration_since(&moment), Some(0));
501    }
502
503    #[rstest]
504    fn test_duration_since_chronological() {
505        // Create a reference time (Feb 10, 2024)
506        let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
507
508        // Create a time 1 hour, 30 minutes, and 45 seconds later (with nanoseconds)
509        let later = earlier
510            + Duration::hours(1)
511            + Duration::minutes(30)
512            + Duration::seconds(45)
513            + Duration::nanoseconds(500_000_000);
514
515        let earlier_nanos = UnixNanos::from(earlier);
516        let later_nanos = UnixNanos::from(later);
517
518        // Calculate expected duration in nanoseconds
519        let expected_duration = 1 * 60 * 60 * 1_000_000_000 + // 1 hour
520        30 * 60 * 1_000_000_000 + // 30 minutes
521        45 * 1_000_000_000 + // 45 seconds
522        500_000_000; // 500 million nanoseconds
523
524        assert_eq!(
525            later_nanos.duration_since(&earlier_nanos),
526            Some(expected_duration)
527        );
528        assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
529    }
530
531    #[rstest]
532    fn test_duration_since_with_edge_cases() {
533        // Test with maximum value
534        let max = UnixNanos::from(u64::MAX);
535        let smaller = UnixNanos::from(u64::MAX - 1000);
536
537        assert_eq!(max.duration_since(&smaller), Some(1000));
538        assert_eq!(smaller.duration_since(&max), None);
539
540        // Test with minimum value
541        let min = UnixNanos::default(); // Zero timestamp
542        let larger = UnixNanos::from(1000);
543
544        assert_eq!(min.duration_since(&min), Some(0));
545        assert_eq!(larger.duration_since(&min), Some(1000));
546        assert_eq!(min.duration_since(&larger), None);
547    }
548
549    #[rstest]
550    fn test_serde_json() {
551        let nanos = UnixNanos::from(123);
552        let json = serde_json::to_string(&nanos).unwrap();
553        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
554        assert_eq!(deserialized, nanos);
555    }
556
557    #[rstest]
558    fn test_serde_edge_cases() {
559        let nanos = UnixNanos::from(u64::MAX);
560        let json = serde_json::to_string(&nanos).unwrap();
561        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
562        assert_eq!(deserialized, nanos);
563    }
564
565    #[rstest]
566    fn test_deserialize_u64() {
567        let json = "123456789";
568        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
569        assert_eq!(deserialized.as_u64(), 123456789);
570    }
571
572    #[rstest]
573    fn test_deserialize_string_with_int() {
574        let json = "\"123456789\"";
575        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
576        assert_eq!(deserialized.as_u64(), 123456789);
577    }
578
579    #[rstest]
580    fn test_deserialize_float() {
581        let json = "1234.567";
582        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
583        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
584    }
585
586    #[rstest]
587    fn test_deserialize_string_with_float() {
588        let json = "\"1234.567\"";
589        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
590        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
591    }
592
593    #[rstest]
594    #[case("\"2024-02-10T14:58:43.456789Z\"", 1707577123456789000)]
595    #[case("\"2024-02-10T14:58:43Z\"", 1707577123000000000)]
596    fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
597        let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
598        assert_eq!(deserialized.as_u64(), expected);
599    }
600
601    #[rstest]
602    fn test_deserialize_negative_int_fails() {
603        let json = "-123456789";
604        let result: Result<UnixNanos, _> = serde_json::from_str(json);
605        assert!(result.is_err());
606    }
607
608    #[rstest]
609    fn test_deserialize_negative_float_fails() {
610        let json = "-1234.567";
611        let result: Result<UnixNanos, _> = serde_json::from_str(json);
612        assert!(result.is_err());
613    }
614
615    #[rstest]
616    fn test_deserialize_invalid_string_fails() {
617        let json = "\"not a timestamp\"";
618        let result: Result<UnixNanos, _> = serde_json::from_str(json);
619        assert!(result.is_err());
620    }
621
622    #[rstest]
623    fn test_non_rfc3339_formats_fail() {
624        // Space-separated format should fail
625        let json = "\"2024-02-10 14:58:43.456789\"";
626        let result: Result<UnixNanos, _> = serde_json::from_str(json);
627        assert!(result.is_err());
628
629        // Simple date format should fail
630        let json = "\"2024-02-10\"";
631        let result: Result<UnixNanos, _> = serde_json::from_str(json);
632        assert!(result.is_err());
633    }
634
635    #[rstest]
636    fn test_deserialize_edge_cases() {
637        // Test zero
638        let json = "0";
639        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
640        assert_eq!(deserialized.as_u64(), 0);
641
642        // Test large value
643        let json = "18446744073709551615"; // u64::MAX
644        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
645        assert_eq!(deserialized.as_u64(), u64::MAX);
646    }
647}