Skip to main content

strand_datetime_conversion/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Convert between [chrono::DateTime] and f64 representation as used in [Strand
5//! Camera](https://strawlab.org/strand-cam) and
6//! [Braid](https://strawlab.org/braid).
7
8use chrono::{DateTime, TimeZone, Utc};
9
10/// Converts a [chrono::DateTime] to an f64 timestamp representation.
11///
12/// This function converts a datetime with any timezone to a floating-point
13/// timestamp where the integer part represents seconds since Unix epoch
14/// and the fractional part represents nanoseconds as a decimal fraction.
15///
16/// # Arguments
17///
18/// * `dt` - A reference to a [DateTime] with any timezone
19///
20/// # Returns
21///
22/// Returns an f64 timestamp where:
23/// - Integer part: seconds since Unix epoch (1970-01-01 00:00:00 UTC)
24/// - Fractional part: nanoseconds expressed as a decimal (e.g., 0.123456789)
25///
26/// # Example
27///
28/// ```rust
29/// use chrono::{DateTime, Utc, TimeZone, Timelike};
30/// use strand_datetime_conversion::datetime_to_f64;
31///
32/// let dt = Utc.with_ymd_and_hms(2023, 12, 25, 15, 30, 45).unwrap()
33///     .with_nanosecond(123456789).unwrap();
34/// let timestamp = datetime_to_f64(&dt);
35/// assert!( (timestamp - 1703518245.123456789).abs() < 1e-9 );
36/// ```
37pub fn datetime_to_f64<TZ>(dt: &DateTime<TZ>) -> f64
38where
39    TZ: TimeZone,
40{
41    let secs = dt.timestamp() as i32;
42    let nsecs = dt.timestamp_subsec_nanos() as i32;
43    (secs as f64) + (nsecs as f64 * 1e-9)
44}
45
46/// Converts an f64 timestamp to a [chrono::DateTime] in UTC timezone.
47///
48/// This is a convenience function that converts a floating-point timestamp
49/// to a UTC datetime. For timezone-specific conversion, use [f64_to_datetime_any].
50///
51/// # Arguments
52///
53/// * `timestamp_f64` - A floating-point timestamp where the integer part
54///   represents seconds since Unix epoch and the fractional part represents
55///   nanoseconds as a decimal fraction
56///
57/// # Returns
58///
59/// Returns a [DateTime] representing the timestamp in UTC timezone.
60///
61/// # Panics
62///
63/// Panics if the timestamp is invalid or out of range for the chrono library.
64///
65/// # Example
66///
67/// ```rust
68/// use strand_datetime_conversion::f64_to_datetime;
69///
70/// let timestamp = 1703518245.123456789;
71/// let dt = f64_to_datetime(timestamp);
72/// // dt will be 2023-12-25 15:30:45.123456789 UTC
73/// ```
74pub fn f64_to_datetime(timestamp_f64: f64) -> DateTime<Utc> {
75    f64_to_datetime_any(timestamp_f64, Utc)
76}
77
78/// Converts an f64 timestamp to a [chrono::DateTime] in the specified timezone.
79///
80/// This function provides full control over the target timezone for the
81/// converted datetime. The input timestamp is interpreted as seconds since
82/// Unix epoch (always in UTC), but the resulting DateTime will be in the
83/// specified timezone.
84///
85/// # Arguments
86///
87/// * `timestamp_f64` - A floating-point timestamp where the integer part
88///   represents seconds since Unix epoch and the fractional part represents
89///   nanoseconds as a decimal fraction
90/// * `tz` - The target timezone for the resulting DateTime
91///
92/// # Returns
93///
94/// Returns a [DateTime] representing the timestamp in the specified timezone.
95///
96/// # Panics
97///
98/// Panics if the timestamp is invalid or out of range for the chrono library.
99///
100/// # Example
101///
102/// ```rust
103/// use chrono::Utc;
104/// use strand_datetime_conversion::f64_to_datetime_any;
105///
106/// let timestamp = 1703518245.123456789;
107/// let dt_utc = f64_to_datetime_any(timestamp, Utc);
108/// // dt_utc will be 2023-12-25 15:30:45.123456789 UTC
109/// ```
110pub fn f64_to_datetime_any<TZ>(timestamp_f64: f64, tz: TZ) -> DateTime<TZ>
111where
112    TZ: chrono::TimeZone,
113{
114    let secs_f = timestamp_f64.floor();
115    let secs = secs_f as i64;
116    let nsecs = ((timestamp_f64 - secs_f) * 1e9) as u32;
117    tz.timestamp_opt(secs, nsecs).unwrap()
118}
119
120#[test]
121fn test_roundtrip() {
122    for orig in &[0.0, 123.456, 456.789, 1634378218.4130154] {
123        let rt = datetime_to_f64(&f64_to_datetime(*orig));
124        dbg!(orig);
125        dbg!(rt);
126        assert!((orig - rt).abs() < 1e-9);
127    }
128}
129
130#[test]
131fn test_precision() {
132    use chrono::TimeZone;
133
134    let t1_orig = 123.123456789;
135    let t2_orig = datetime_to_f64(&chrono::Utc.with_ymd_and_hms(2100, 1, 1, 0, 1, 1).unwrap());
136
137    // Ensure microsecond precision is kept in floating point representations.
138    let t1_bad = t1_orig + 1e-6;
139    assert!(t1_orig != t1_bad);
140
141    let t2_bad = t2_orig + 1e-6;
142    assert!(t2_orig != t2_bad);
143}