nautilus_core/
datetime.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Common data and time functions.
17use std::convert::TryFrom;
18
19use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Utc, Weekday};
20
21use crate::UnixNanos;
22
23/// Number of milliseconds in one second.
24pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
25
26/// Number of nanoseconds in one second.
27pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
28
29/// Number of nanoseconds in one millisecond.
30pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
31
32/// Number of nanoseconds in one microsecond.
33pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
34
35/// List of weekdays (Monday to Friday).
36pub const WEEKDAYS: [Weekday; 5] = [
37    Weekday::Mon,
38    Weekday::Tue,
39    Weekday::Wed,
40    Weekday::Thu,
41    Weekday::Fri,
42];
43
44/// Converts seconds to nanoseconds (ns).
45///
46/// Casting f64 to u64 by truncating the fractional part is intentional for unit conversion,
47/// which may lose precision and drop negative values after clamping.
48#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
49#[must_use]
50pub fn secs_to_nanos(secs: f64) -> u64 {
51    let nanos = secs * NANOSECONDS_IN_SECOND as f64;
52    nanos.max(0.0).trunc() as u64
53}
54
55/// Converts seconds to milliseconds (ms).
56///
57/// Casting f64 to u64 by truncating the fractional part is intentional for unit conversion,
58/// which may lose precision and drop negative values after clamping.
59#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
60#[must_use]
61pub fn secs_to_millis(secs: f64) -> u64 {
62    let millis = secs * MILLISECONDS_IN_SECOND as f64;
63    millis.max(0.0).trunc() as u64
64}
65
66/// Converts milliseconds (ms) to nanoseconds (ns).
67///
68/// Casting f64 to u64 by truncating the fractional part is intentional for unit conversion,
69/// which may lose precision and drop negative values after clamping.
70#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
71#[must_use]
72pub fn millis_to_nanos(millis: f64) -> u64 {
73    let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
74    nanos.max(0.0).trunc() as u64
75}
76
77/// Converts microseconds (μs) to nanoseconds (ns).
78///
79/// Casting f64 to u64 by truncating the fractional part is intentional for unit conversion,
80/// which may lose precision and drop negative values after clamping.
81#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
82#[must_use]
83pub fn micros_to_nanos(micros: f64) -> u64 {
84    let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
85    nanos.max(0.0).trunc() as u64
86}
87
88/// Converts nanoseconds (ns) to seconds.
89///
90/// Casting u64 to f64 may lose precision for large values,
91/// but is acceptable when computing fractional seconds.
92#[allow(clippy::cast_precision_loss)]
93#[must_use]
94pub fn nanos_to_secs(nanos: u64) -> f64 {
95    let seconds = nanos / NANOSECONDS_IN_SECOND;
96    let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
97    (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
98}
99
100/// Converts nanoseconds (ns) to milliseconds (ms).
101#[must_use]
102pub const fn nanos_to_millis(nanos: u64) -> u64 {
103    nanos / NANOSECONDS_IN_MILLISECOND
104}
105
106/// Converts nanoseconds (ns) to microseconds (μs).
107#[must_use]
108pub const fn nanos_to_micros(nanos: u64) -> u64 {
109    nanos / NANOSECONDS_IN_MICROSECOND
110}
111
112/// Converts a UNIX nanoseconds timestamp to an ISO 8601 (RFC 3339) format string.
113#[inline]
114#[must_use]
115pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
116    let datetime = unix_nanos.to_datetime_utc();
117    datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
118}
119
120/// Converts an ISO 8601 (RFC 3339) format string to UNIX nanoseconds timestamp.
121///
122/// This function accepts various ISO 8601 formats including:
123/// - Full RFC 3339 with nanosecond precision: "2024-02-10T14:58:43.456789Z"
124/// - RFC 3339 without fractional seconds: "2024-02-10T14:58:43Z"
125/// - Simple date format: "2024-02-10" (interpreted as midnight UTC)
126///
127/// # Parameters
128///
129/// - `date_string`: The ISO 8601 formatted date string to parse
130///
131/// # Returns
132///
133/// Returns `Ok(UnixNanos)` if the string is successfully parsed, or an error if the format
134/// is invalid or the timestamp is out of range.
135///
136/// # Errors
137///
138/// Returns an error if:
139/// - The string format is not a valid ISO 8601 format
140/// - The timestamp is out of range for `UnixNanos`
141/// - The date/time values are invalid
142///
143/// # Examples
144///
145/// ```rust
146/// use nautilus_core::datetime::iso8601_to_unix_nanos;
147/// use nautilus_core::UnixNanos;
148///
149/// // Full RFC 3339 format
150/// let nanos = iso8601_to_unix_nanos("2024-02-10T14:58:43.456789Z".to_string())?;
151/// assert_eq!(nanos, UnixNanos::from(1_707_577_123_456_789_000));
152///
153/// // Without fractional seconds
154/// let nanos = iso8601_to_unix_nanos("2024-02-10T14:58:43Z".to_string())?;
155/// assert_eq!(nanos, UnixNanos::from(1_707_577_123_000_000_000));
156///
157/// // Simple date format (midnight UTC)
158/// let nanos = iso8601_to_unix_nanos("2024-02-10".to_string())?;
159/// assert_eq!(nanos, UnixNanos::from(1_707_523_200_000_000_000));
160/// # Ok::<(), anyhow::Error>(())
161/// ```
162#[inline]
163pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
164    date_string
165        .parse::<UnixNanos>()
166        .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{}': {}", date_string, e))
167}
168
169/// Converts a UNIX nanoseconds timestamp to an ISO 8601 (RFC 3339) format string
170/// with millisecond precision.
171#[inline]
172#[must_use]
173pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
174    let datetime = unix_nanos.to_datetime_utc();
175    datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
176}
177
178/// Floor the given UNIX nanoseconds to the nearest microsecond.
179#[must_use]
180pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
181    (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
182}
183
184/// Calculates the last weekday (Mon-Fri) from the given `year`, `month` and `day`.
185///
186/// # Errors
187///
188/// Returns an error if the date is invalid.
189pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
190    let date =
191        NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
192    let current_weekday = date.weekday().number_from_monday();
193
194    // Calculate the offset in days for closest weekday (Mon-Fri)
195    let offset = i64::from(match current_weekday {
196        1..=5 => 0, // Monday to Friday, no adjustment needed
197        6 => 1,     // Saturday, adjust to previous Friday
198        _ => 2,     // Sunday, adjust to previous Friday
199    });
200    // Calculate last closest weekday
201    let last_closest = date - TimeDelta::days(offset);
202
203    // Convert to UNIX nanoseconds
204    let unix_timestamp_ns = last_closest
205        .and_hms_nano_opt(0, 0, 0, 0)
206        .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
207
208    // Convert timestamp nanos safely from i64 to u64
209    let raw_ns = unix_timestamp_ns
210        .and_utc()
211        .timestamp_nanos_opt()
212        .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
213    let ns_u64 =
214        u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
215    Ok(UnixNanos::from(ns_u64))
216}
217
218/// Check whether the given UNIX nanoseconds timestamp is within the last 24 hours.
219///
220/// # Errors
221///
222/// Returns an error if the timestamp is invalid.
223pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
224    let timestamp_ns = timestamp_ns.as_u64();
225    let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
226    let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
227    // Convert seconds to i64 safely
228    let secs_i64 = i64::try_from(seconds)
229        .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
230    let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
231        .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
232    let now = Utc::now();
233
234    Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
235}
236
237/// Subtract `n` months from a chrono `DateTime<Utc>`.
238///
239/// # Errors
240///
241/// Returns an error if the resulting date would be invalid or out of range.
242pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
243    match datetime.checked_sub_months(chrono::Months::new(n)) {
244        Some(result) => Ok(result),
245        None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
246    }
247}
248
249/// Add `n` months to a chrono `DateTime<Utc>`.
250///
251/// # Errors
252///
253/// Returns an error if the resulting date would be invalid or out of range.
254pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
255    match datetime.checked_add_months(chrono::Months::new(n)) {
256        Some(result) => Ok(result),
257        None => anyhow::bail!("Failed to add {n} months to {datetime}"),
258    }
259}
260
261/// Subtract `n` months from a given UNIX nanoseconds timestamp.
262///
263/// # Errors
264///
265/// Returns an error if the resulting timestamp is out of range or invalid.
266pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
267    let datetime = unix_nanos.to_datetime_utc();
268    let result = subtract_n_months(datetime, n)?;
269    let timestamp = match result.timestamp_nanos_opt() {
270        Some(ts) => ts,
271        None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
272    };
273
274    if timestamp < 0 {
275        anyhow::bail!("Negative timestamp not allowed");
276    }
277
278    Ok(UnixNanos::from(timestamp as u64))
279}
280
281/// Add `n` months to a given UNIX nanoseconds timestamp.
282///
283/// # Errors
284///
285/// Returns an error if the resulting timestamp is out of range or invalid.
286pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
287    let datetime = unix_nanos.to_datetime_utc();
288    let result = add_n_months(datetime, n)?;
289    let timestamp = match result.timestamp_nanos_opt() {
290        Some(ts) => ts,
291        None => anyhow::bail!("Timestamp out of range after adding {n} months"),
292    };
293
294    if timestamp < 0 {
295        anyhow::bail!("Negative timestamp not allowed");
296    }
297
298    Ok(UnixNanos::from(timestamp as u64))
299}
300
301/// Returns the last valid day of `(year, month)`.
302#[must_use]
303pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
304    // Validate month range 1-12
305    assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
306
307    // February leap-year logic
308    match month {
309        2 => {
310            if is_leap_year(year) {
311                29
312            } else {
313                28
314            }
315        }
316        4 | 6 | 9 | 11 => 30,
317        _ => 31, // January, March, May, July, August, October, December
318    }
319}
320
321/// Basic leap-year check
322#[must_use]
323pub const fn is_leap_year(year: i32) -> bool {
324    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
325}
326
327////////////////////////////////////////////////////////////////////////////////
328// Tests
329////////////////////////////////////////////////////////////////////////////////
330#[cfg(test)]
331#[allow(clippy::float_cmp)]
332mod tests {
333    use chrono::{DateTime, TimeDelta, TimeZone, Utc};
334    use rstest::rstest;
335
336    use super::*;
337
338    #[rstest]
339    #[case(0.0, 0)]
340    #[case(1.0, 1_000_000_000)]
341    #[case(1.1, 1_100_000_000)]
342    #[case(42.0, 42_000_000_000)]
343    #[case(0.000_123_5, 123_500)]
344    #[case(0.000_000_01, 10)]
345    #[case(0.000_000_001, 1)]
346    #[case(9.999_999_999, 9_999_999_999)]
347    fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
348        let result = secs_to_nanos(value);
349        assert_eq!(result, expected);
350    }
351
352    #[rstest]
353    #[case(0.0, 0)]
354    #[case(1.0, 1_000)]
355    #[case(1.1, 1_100)]
356    #[case(42.0, 42_000)]
357    #[case(0.012_34, 12)]
358    #[case(0.001, 1)]
359    fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
360        let result = secs_to_millis(value);
361        assert_eq!(result, expected);
362    }
363
364    #[rstest]
365    #[should_panic(expected = "`month` must be in 1..=12")]
366    fn test_last_day_of_month_invalid_month() {
367        let _ = last_day_of_month(2024, 0);
368    }
369
370    #[rstest]
371    #[case(0.0, 0)]
372    #[case(1.0, 1_000_000)]
373    #[case(1.1, 1_100_000)]
374    #[case(42.0, 42_000_000)]
375    #[case(0.000_123_4, 123)]
376    #[case(0.000_01, 10)]
377    #[case(0.000_001, 1)]
378    #[case(9.999_999, 9_999_999)]
379    fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
380        let result = millis_to_nanos(value);
381        assert_eq!(result, expected);
382    }
383
384    #[rstest]
385    #[case(0.0, 0)]
386    #[case(1.0, 1_000)]
387    #[case(1.1, 1_100)]
388    #[case(42.0, 42_000)]
389    #[case(0.1234, 123)]
390    #[case(0.01, 10)]
391    #[case(0.001, 1)]
392    #[case(9.999, 9_999)]
393    fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
394        let result = micros_to_nanos(value);
395        assert_eq!(result, expected);
396    }
397
398    #[rstest]
399    #[case(0, 0.0)]
400    #[case(1, 1e-09)]
401    #[case(1_000_000_000, 1.0)]
402    #[case(42_897_123_111, 42.897_123_111)]
403    fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
404        let result = nanos_to_secs(value);
405        assert_eq!(result, expected);
406    }
407
408    #[rstest]
409    #[case(0, 0)]
410    #[case(1_000_000, 1)]
411    #[case(1_000_000_000, 1000)]
412    #[case(42_897_123_111, 42897)]
413    fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
414        let result = nanos_to_millis(value);
415        assert_eq!(result, expected);
416    }
417
418    #[rstest]
419    #[case(0, 0)]
420    #[case(1_000, 1)]
421    #[case(1_000_000_000, 1_000_000)]
422    #[case(42_897_123, 42_897)]
423    fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
424        let result = nanos_to_micros(value);
425        assert_eq!(result, expected);
426    }
427
428    #[rstest]
429    #[case(0, "1970-01-01T00:00:00.000000000Z")] // Unix epoch
430    #[case(1, "1970-01-01T00:00:00.000000001Z")] // 1 nanosecond
431    #[case(1_000, "1970-01-01T00:00:00.000001000Z")] // 1 microsecond
432    #[case(1_000_000, "1970-01-01T00:00:00.001000000Z")] // 1 millisecond
433    #[case(1_000_000_000, "1970-01-01T00:00:01.000000000Z")] // 1 second
434    #[case(1_702_857_600_000_000_000, "2023-12-18T00:00:00.000000000Z")] // Specific date
435    fn test_unix_nanos_to_iso8601(#[case] nanos: u64, #[case] expected: &str) {
436        let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
437        assert_eq!(result, expected);
438    }
439
440    #[rstest]
441    #[case(0, "1970-01-01T00:00:00.000Z")] // Unix epoch
442    #[case(1_000_000, "1970-01-01T00:00:00.001Z")] // 1 millisecond
443    #[case(1_000_000_000, "1970-01-01T00:00:01.000Z")] // 1 second
444    #[case(1_702_857_600_123_456_789, "2023-12-18T00:00:00.123Z")] // With millisecond precision
445    fn test_unix_nanos_to_iso8601_millis(#[case] nanos: u64, #[case] expected: &str) {
446        let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
447        assert_eq!(result, expected);
448    }
449
450    #[rstest]
451    #[case(2023, 12, 15, 1_702_598_400_000_000_000)] // Fri
452    #[case(2023, 12, 16, 1_702_598_400_000_000_000)] // Sat
453    #[case(2023, 12, 17, 1_702_598_400_000_000_000)] // Sun
454    #[case(2023, 12, 18, 1_702_857_600_000_000_000)] // Mon
455    fn test_last_closest_weekday_nanos_with_valid_date(
456        #[case] year: i32,
457        #[case] month: u32,
458        #[case] day: u32,
459        #[case] expected: u64,
460    ) {
461        let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
462        assert_eq!(result, expected);
463    }
464
465    #[rstest]
466    fn test_last_closest_weekday_nanos_with_invalid_date() {
467        let result = last_weekday_nanos(2023, 4, 31);
468        assert!(result.is_err());
469    }
470
471    #[rstest]
472    fn test_last_closest_weekday_nanos_with_nonexistent_date() {
473        let result = last_weekday_nanos(2023, 2, 30);
474        assert!(result.is_err());
475    }
476
477    #[rstest]
478    fn test_last_closest_weekday_nanos_with_invalid_conversion() {
479        let result = last_weekday_nanos(9999, 12, 31);
480        assert!(result.is_err());
481    }
482
483    #[rstest]
484    fn test_is_within_last_24_hours_when_now() {
485        let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
486        assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
487    }
488
489    #[rstest]
490    fn test_is_within_last_24_hours_when_two_days_ago() {
491        let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
492            .timestamp_nanos_opt()
493            .unwrap();
494        assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
495    }
496
497    #[rstest]
498    #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] // Leap year February
499    #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 12, Utc.with_ymd_and_hms(2023, 3, 31, 12, 0, 0).unwrap())] // One year earlier
500    #[case(Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2023, 12, 31, 12, 0, 0).unwrap())] // Wrapping to previous year
501    #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 2, Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap())] // Multiple months back
502    fn test_subtract_n_months(
503        #[case] input: DateTime<Utc>,
504        #[case] months: u32,
505        #[case] expected: DateTime<Utc>,
506    ) {
507        let result = subtract_n_months(input, months).unwrap();
508        assert_eq!(result, expected);
509    }
510
511    #[rstest]
512    #[case(Utc.with_ymd_and_hms(2023, 2, 28, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2023, 3, 28, 12, 0, 0).unwrap())] // Simple month addition
513    #[case(Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] // Leap year February
514    #[case(Utc.with_ymd_and_hms(2023, 12, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap())] // Wrapping to next year
515    #[case(Utc.with_ymd_and_hms(2023, 1, 31, 12, 0, 0).unwrap(), 13, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] // Crossing year boundary with multiple months
516    fn test_add_n_months(
517        #[case] input: DateTime<Utc>,
518        #[case] months: u32,
519        #[case] expected: DateTime<Utc>,
520    ) {
521        let result = add_n_months(input, months).unwrap();
522        assert_eq!(result, expected);
523    }
524
525    #[rstest]
526    #[case(2024, 2, 29)] // Leap year February
527    #[case(2023, 2, 28)] // Non-leap year February
528    #[case(2024, 12, 31)] // December
529    #[case(2023, 11, 30)] // November
530    fn test_last_day_of_month(#[case] year: i32, #[case] month: u32, #[case] expected: u32) {
531        let result = last_day_of_month(year, month);
532        assert_eq!(result, expected);
533    }
534
535    #[rstest]
536    #[case(2024, true)] // Leap year divisible by 4
537    #[case(1900, false)] // Not leap year, divisible by 100 but not 400
538    #[case(2000, true)] // Leap year, divisible by 400
539    #[case(2023, false)] // Non-leap year
540    fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
541        let result = is_leap_year(year);
542        assert_eq!(result, expected);
543    }
544
545    #[rstest]
546    #[case("1970-01-01T00:00:00.000000000Z", 0)] // Unix epoch
547    #[case("1970-01-01T00:00:00.000000001Z", 1)] // 1 nanosecond
548    #[case("1970-01-01T00:00:00.001000000Z", 1_000_000)] // 1 millisecond
549    #[case("1970-01-01T00:00:01.000000000Z", 1_000_000_000)] // 1 second
550    #[case("2023-12-18T00:00:00.000000000Z", 1_702_857_600_000_000_000)] // Specific date
551    #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
552    #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
553    #[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date format
554    fn test_iso8601_to_unix_nanos(#[case] input: &str, #[case] expected: u64) {
555        let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
556        assert_eq!(result.as_u64(), expected);
557    }
558
559    #[rstest]
560    #[case("invalid-date")] // Invalid format
561    #[case("2024-02-30")] // Invalid date
562    #[case("2024-13-01")] // Invalid month
563    #[case("not a timestamp")] // Random string
564    fn test_iso8601_to_unix_nanos_invalid(#[case] input: &str) {
565        let result = iso8601_to_unix_nanos(input.to_string());
566        assert!(result.is_err());
567    }
568
569    #[rstest]
570    fn test_iso8601_roundtrip() {
571        let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
572        let iso8601_string = unix_nanos_to_iso8601(original_nanos);
573        let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
574        assert_eq!(parsed_nanos, original_nanos);
575    }
576}