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::{Deserialize, Serialize};
27
28/// Represents a timestamp in nanoseconds since the UNIX epoch.
29#[repr(C)]
30#[derive(
31    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
32)]
33pub struct UnixNanos(u64);
34
35impl UnixNanos {
36    /// Returns the underlying value as `u64`.
37    #[must_use]
38    pub const fn as_u64(&self) -> u64 {
39        self.0
40    }
41
42    /// Returns the underlying value as `i64`.
43    #[must_use]
44    pub const fn as_i64(&self) -> i64 {
45        self.0 as i64
46    }
47
48    /// Returns the underlying value as `f64`.
49    #[must_use]
50    pub const fn as_f64(&self) -> f64 {
51        self.0 as f64
52    }
53}
54
55impl Deref for UnixNanos {
56    type Target = u64;
57
58    fn deref(&self) -> &Self::Target {
59        &self.0
60    }
61}
62
63impl PartialEq<u64> for UnixNanos {
64    fn eq(&self, other: &u64) -> bool {
65        self.0 == *other
66    }
67}
68
69impl PartialOrd<u64> for UnixNanos {
70    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
71        self.0.partial_cmp(other)
72    }
73}
74
75impl PartialEq<Option<u64>> for UnixNanos {
76    fn eq(&self, other: &Option<u64>) -> bool {
77        match other {
78            Some(value) => self.0 == *value,
79            None => false,
80        }
81    }
82}
83
84impl PartialOrd<Option<u64>> for UnixNanos {
85    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
86        match other {
87            Some(value) => self.0.partial_cmp(value),
88            None => Some(Ordering::Greater),
89        }
90    }
91}
92
93impl From<u64> for UnixNanos {
94    fn from(value: u64) -> Self {
95        Self(value)
96    }
97}
98
99impl From<UnixNanos> for u64 {
100    fn from(value: UnixNanos) -> Self {
101        value.0
102    }
103}
104
105impl From<&str> for UnixNanos {
106    fn from(value: &str) -> Self {
107        Self(
108            value
109                .parse()
110                .expect("`value` should be a valid integer string"),
111        )
112    }
113}
114
115impl From<String> for UnixNanos {
116    fn from(value: String) -> Self {
117        Self::from(value.as_str())
118    }
119}
120
121impl From<DateTime<Utc>> for UnixNanos {
122    fn from(value: DateTime<Utc>) -> Self {
123        Self::from(value.timestamp_nanos_opt().expect("Invalid timestamp") as u64)
124    }
125}
126
127impl FromStr for UnixNanos {
128    type Err = std::num::ParseIntError;
129
130    fn from_str(s: &str) -> Result<Self, Self::Err> {
131        s.parse().map(UnixNanos)
132    }
133}
134
135impl Add for UnixNanos {
136    type Output = Self;
137    fn add(self, rhs: Self) -> Self::Output {
138        Self(
139            self.0
140                .checked_add(rhs.0)
141                .expect("Error adding with overflow"),
142        )
143    }
144}
145
146impl Sub for UnixNanos {
147    type Output = Self;
148    fn sub(self, rhs: Self) -> Self::Output {
149        Self(
150            self.0
151                .checked_sub(rhs.0)
152                .expect("Error subtracting with underflow"),
153        )
154    }
155}
156
157impl Add<u64> for UnixNanos {
158    type Output = Self;
159
160    fn add(self, rhs: u64) -> Self::Output {
161        Self(self.0.checked_add(rhs).expect("Error adding with overflow"))
162    }
163}
164
165impl Sub<u64> for UnixNanos {
166    type Output = Self;
167
168    fn sub(self, rhs: u64) -> Self::Output {
169        Self(
170            self.0
171                .checked_sub(rhs)
172                .expect("Error subtracting with underflow"),
173        )
174    }
175}
176
177impl<T: Into<u64>> AddAssign<T> for UnixNanos {
178    fn add_assign(&mut self, other: T) {
179        let other_u64 = other.into();
180        self.0 = self
181            .0
182            .checked_add(other_u64)
183            .expect("Error adding with overflow");
184    }
185}
186
187impl<T: Into<u64>> SubAssign<T> for UnixNanos {
188    fn sub_assign(&mut self, other: T) {
189        let other_u64 = other.into();
190        self.0 = self
191            .0
192            .checked_sub(other_u64)
193            .expect("Error subtracting with underflow");
194    }
195}
196
197impl Display for UnixNanos {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        write!(f, "{}", self.0)
200    }
201}
202
203/// Represents a duration in nanoseconds.
204pub type DurationNanos = u64;
205
206////////////////////////////////////////////////////////////////////////////////
207// Tests
208////////////////////////////////////////////////////////////////////////////////
209#[cfg(test)]
210mod tests {
211    use rstest::rstest;
212
213    use super::*;
214
215    #[rstest]
216    fn test_new() {
217        let nanos = UnixNanos::from(123);
218        assert_eq!(nanos.as_u64(), 123);
219        assert_eq!(nanos.as_i64(), 123);
220    }
221
222    #[rstest]
223    fn test_default() {
224        let nanos = UnixNanos::default();
225        assert_eq!(nanos.as_u64(), 0);
226        assert_eq!(nanos.as_i64(), 0);
227    }
228
229    #[rstest]
230    fn test_into_from() {
231        let nanos: UnixNanos = 456.into();
232        let value: u64 = nanos.into();
233        assert_eq!(value, 456);
234    }
235
236    #[rstest]
237    fn test_from_str() {
238        let nanos: UnixNanos = "123".parse().unwrap();
239        assert_eq!(nanos.as_u64(), 123);
240    }
241
242    #[rstest]
243    fn test_from_str_invalid() {
244        let result = "abc".parse::<UnixNanos>();
245        assert!(result.is_err());
246    }
247
248    #[rstest]
249    fn test_try_from_datetime_valid() {
250        use chrono::TimeZone;
251        let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); // 1 billion seconds since epoch
252        let nanos = UnixNanos::from(datetime);
253        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
254    }
255
256    #[rstest]
257    fn test_eq() {
258        let nanos = UnixNanos::from(100);
259        assert_eq!(nanos, 100);
260        assert_eq!(nanos, Some(100));
261        assert_ne!(nanos, 200);
262        assert_ne!(nanos, Some(200));
263        assert_ne!(nanos, None);
264    }
265
266    #[rstest]
267    fn test_partial_cmp() {
268        let nanos = UnixNanos::from(100);
269        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
270        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
271        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
272        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
273    }
274
275    #[rstest]
276    fn test_edge_case_max_value() {
277        let nanos = UnixNanos::from(u64::MAX);
278        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
279    }
280
281    #[rstest]
282    fn test_display() {
283        let nanos = UnixNanos::from(123);
284        assert_eq!(format!("{nanos}"), "123");
285    }
286
287    #[rstest]
288    fn test_addition() {
289        let nanos1 = UnixNanos::from(100);
290        let nanos2 = UnixNanos::from(200);
291        let result = nanos1 + nanos2;
292        assert_eq!(result.as_u64(), 300);
293    }
294
295    #[rstest]
296    fn test_add_assign() {
297        let mut nanos = UnixNanos::from(100);
298        nanos += 50_u64;
299        assert_eq!(nanos.as_u64(), 150);
300    }
301
302    #[rstest]
303    fn test_subtraction() {
304        let nanos1 = UnixNanos::from(200);
305        let nanos2 = UnixNanos::from(100);
306        let result = nanos1 - nanos2;
307        assert_eq!(result.as_u64(), 100);
308    }
309
310    #[rstest]
311    fn test_sub_assign() {
312        let mut nanos = UnixNanos::from(200);
313        nanos -= 50_u64;
314        assert_eq!(nanos.as_u64(), 150);
315    }
316
317    #[rstest]
318    #[should_panic(expected = "Error adding with overflow")]
319    fn test_overflow_add() {
320        let nanos = UnixNanos::from(u64::MAX);
321        let _ = nanos + UnixNanos::from(1); // This should panic due to overflow
322    }
323
324    #[rstest]
325    #[should_panic(expected = "Error adding with overflow")]
326    fn test_overflow_add_u64() {
327        let nanos = UnixNanos::from(u64::MAX);
328        let _ = nanos + 1_u64; // This should panic due to overflow
329    }
330
331    #[rstest]
332    #[should_panic(expected = "Error subtracting with underflow")]
333    fn test_overflow_sub() {
334        let _ = UnixNanos::default() - UnixNanos::from(1); // This should panic due to underflow
335    }
336
337    #[rstest]
338    #[should_panic(expected = "Error subtracting with underflow")]
339    fn test_overflow_sub_u64() {
340        let _ = UnixNanos::default() - 1_u64; // This should panic due to underflow
341    }
342
343    #[rstest]
344    fn test_serde_json() {
345        let nanos = UnixNanos::from(123);
346        let json = serde_json::to_string(&nanos).unwrap();
347        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
348        assert_eq!(deserialized, nanos);
349    }
350
351    #[rstest]
352    fn test_serde_edge_cases() {
353        let nanos = UnixNanos::from(u64::MAX);
354        let json = serde_json::to_string(&nanos).unwrap();
355        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
356        assert_eq!(deserialized, nanos);
357    }
358}