Skip to main content

cbor_core/ext/
chrono.rs

1use chrono::{DateTime, FixedOffset, SecondsFormat, TimeZone, Utc};
2
3use crate::{Error, Result, Value};
4
5fn date_time_from_str(s: &str) -> Result<DateTime<FixedOffset>> {
6    DateTime::<FixedOffset>::parse_from_rfc3339(s).or(Err(Error::InvalidFormat))
7}
8
9fn date_time_from_float(f: f64) -> Result<DateTime<Utc>> {
10    if f.is_finite() && f >= 0.0 {
11        let secs = f.trunc() as i64;
12        let nanos = ((f - f.trunc()) * 1_000_000_000.0).round() as u32;
13        DateTime::from_timestamp(secs, nanos).ok_or(Error::InvalidValue)
14    } else {
15        Err(Error::InvalidValue)
16    }
17}
18
19fn date_time_from_u64(secs: u64) -> Result<DateTime<Utc>> {
20    let secs = secs.try_into().or(Err(Error::InvalidValue))?;
21    DateTime::from_timestamp(secs, 0).ok_or(Error::InvalidValue)
22}
23
24// ---------------------------------------------------------------------------
25// chrono::DateTime<Tz> → crate::DateTime
26// ---------------------------------------------------------------------------
27
28impl<Tz: TimeZone> TryFrom<DateTime<Tz>> for crate::DateTime
29where
30    Tz::Offset: std::fmt::Display,
31{
32    type Error = Error;
33
34    /// Converts a `chrono::DateTime` to a CBOR date/time string (tag 0).
35    ///
36    /// Whole-second timestamps omit the fractional part and sub-second
37    /// timestamps include only the necessary digits.
38    fn try_from(value: DateTime<Tz>) -> Result<Self> {
39        crate::DateTime::try_from(value.to_rfc3339_opts(SecondsFormat::AutoSi, true))
40    }
41}
42
43impl<Tz: TimeZone> TryFrom<&DateTime<Tz>> for crate::DateTime
44where
45    Tz::Offset: std::fmt::Display,
46{
47    type Error = Error;
48
49    fn try_from(value: &DateTime<Tz>) -> Result<Self> {
50        crate::DateTime::try_from(value.to_rfc3339_opts(SecondsFormat::AutoSi, true))
51    }
52}
53
54// ---------------------------------------------------------------------------
55// chrono::DateTime<Tz> → crate::EpochTime
56// ---------------------------------------------------------------------------
57
58impl<Tz: TimeZone> TryFrom<DateTime<Tz>> for crate::EpochTime {
59    type Error = Error;
60
61    /// Converts a `chrono::DateTime` to a CBOR epoch time (tag 1).
62    ///
63    /// Whole-second timestamps are stored as integers; sub-second
64    /// timestamps are stored as floats.
65    fn try_from(value: DateTime<Tz>) -> Result<Self> {
66        chrono_to_epoch(&value)
67    }
68}
69
70impl<Tz: TimeZone> TryFrom<&DateTime<Tz>> for crate::EpochTime {
71    type Error = Error;
72
73    fn try_from(value: &DateTime<Tz>) -> Result<Self> {
74        chrono_to_epoch(value)
75    }
76}
77
78fn chrono_to_epoch<Tz: TimeZone>(value: &DateTime<Tz>) -> Result<crate::EpochTime> {
79    let secs = value.timestamp();
80    let nanos = value.timestamp_subsec_nanos();
81
82    if nanos == 0 {
83        crate::EpochTime::try_from(secs)
84    } else {
85        let f = secs as f64 + nanos as f64 / 1_000_000_000.0;
86        crate::EpochTime::try_from(f)
87    }
88}
89
90// ---------------------------------------------------------------------------
91// Value → chrono::DateTime<Utc>
92// ---------------------------------------------------------------------------
93
94impl TryFrom<Value> for DateTime<Utc> {
95    type Error = Error;
96
97    /// Extracts a `chrono::DateTime<Utc>` from a CBOR time value.
98    ///
99    /// Accepts date/time strings (tag 0), epoch integers/floats (tag 1),
100    /// and untagged integers, floats, or text strings.
101    fn try_from(value: Value) -> Result<Self> {
102        value_to_chrono_utc(&value)
103    }
104}
105
106impl TryFrom<&Value> for DateTime<Utc> {
107    type Error = Error;
108
109    fn try_from(value: &Value) -> Result<Self> {
110        value_to_chrono_utc(value)
111    }
112}
113
114fn value_to_chrono_utc(value: &Value) -> Result<DateTime<Utc>> {
115    if let Ok(s) = value.as_str() {
116        date_time_from_str(s).map(|dt| dt.to_utc())
117    } else if let Ok(f) = value.to_f64() {
118        date_time_from_float(f)
119    } else {
120        match value.to_u64() {
121            Ok(secs) => date_time_from_u64(secs),
122            Err(Error::NegativeUnsigned) => Err(Error::InvalidValue),
123            Err(other_error) => Err(other_error),
124        }
125    }
126}
127
128// ---------------------------------------------------------------------------
129// Value → chrono::DateTime<FixedOffset>
130// ---------------------------------------------------------------------------
131
132impl TryFrom<Value> for DateTime<FixedOffset> {
133    type Error = Error;
134
135    /// Extracts a `chrono::DateTime<FixedOffset>` from a CBOR time value.
136    ///
137    /// Date/time strings (tag 0) preserve the original timezone offset.
138    /// Epoch integers/floats are returned with a UTC offset.
139    fn try_from(value: Value) -> Result<Self> {
140        value_to_chrono_fixed(&value)
141    }
142}
143
144impl TryFrom<&Value> for DateTime<FixedOffset> {
145    type Error = Error;
146
147    fn try_from(value: &Value) -> Result<Self> {
148        value_to_chrono_fixed(value)
149    }
150}
151
152fn value_to_chrono_fixed(value: &Value) -> Result<DateTime<FixedOffset>> {
153    if let Ok(s) = value.as_str() {
154        date_time_from_str(s)
155    } else if let Ok(f) = value.to_f64() {
156        date_time_from_float(f).map(|dt| dt.into())
157    } else {
158        match value.to_u64() {
159            Ok(secs) => date_time_from_u64(secs).map(|dt| dt.into()),
160            Err(Error::NegativeUnsigned) => Err(Error::InvalidValue),
161            Err(other_error) => Err(other_error),
162        }
163    }
164}
165
166// ---------------------------------------------------------------------------
167// Tests
168// ---------------------------------------------------------------------------
169
170#[cfg(test)]
171mod tests {
172    use std::time::{Duration, UNIX_EPOCH};
173
174    use chrono::{DateTime, FixedOffset, NaiveDate, SecondsFormat, Utc};
175
176    use crate::{DataType, Error, Float, Value};
177
178    // ---- chrono::DateTime → crate::DateTime (tag 0) ----
179
180    #[test]
181    fn chrono_to_date_time_utc() {
182        let chrono_dt = NaiveDate::from_ymd_opt(2000, 1, 1)
183            .unwrap()
184            .and_hms_opt(0, 0, 0)
185            .unwrap()
186            .and_utc();
187        let v = Value::date_time(chrono_dt);
188        assert_eq!(v.as_str(), Ok("2000-01-01T00:00:00Z"));
189    }
190
191    #[test]
192    fn chrono_to_date_time_subsec() {
193        let chrono_dt = NaiveDate::from_ymd_opt(2000, 1, 1)
194            .unwrap()
195            .and_hms_nano_opt(12, 30, 45, 123_456_789)
196            .unwrap()
197            .and_utc();
198        let v = Value::date_time(chrono_dt);
199        assert_eq!(v.as_str(), Ok("2000-01-01T12:30:45.123456789Z"));
200    }
201
202    #[test]
203    fn chrono_to_date_time_millis_only() {
204        let chrono_dt = NaiveDate::from_ymd_opt(2024, 6, 15)
205            .unwrap()
206            .and_hms_milli_opt(8, 0, 0, 500) // cspell::disable-line
207            .unwrap()
208            .and_utc();
209        let v = Value::date_time(chrono_dt);
210        assert_eq!(v.as_str(), Ok("2024-06-15T08:00:00.500Z"));
211    }
212
213    // ---- chrono::DateTime → crate::EpochTime (tag 1) ----
214
215    #[test]
216    fn chrono_to_epoch_time_whole_second() {
217        let chrono_dt = DateTime::from_timestamp(1_000_000, 0).unwrap();
218        let v = Value::epoch_time(chrono_dt);
219        // Whole second → integer inside the tag
220        assert_eq!(v.into_untagged(), Value::Unsigned(1_000_000));
221    }
222
223    #[test]
224    fn chrono_to_epoch_time_subsec() {
225        let chrono_dt = DateTime::from_timestamp(1_000_000, 500_000_000).unwrap();
226        let v = Value::epoch_time(chrono_dt);
227        // Sub-second → float inside the tag
228        assert_eq!(v.into_untagged(), Value::Float(Float::from(1000000.5)));
229    }
230
231    #[test]
232    fn chrono_to_epoch_time_negative() {
233        // Before epoch — should fail (CBOR EpochTime is non-negative only)
234        let chrono_dt = DateTime::from_timestamp(-1, 0).unwrap();
235        assert_eq!(crate::EpochTime::try_from(chrono_dt), Err(Error::InvalidValue));
236    }
237
238    // ---- Value → chrono::DateTime<Utc> ----
239
240    #[test]
241    fn value_date_time_string_to_chrono_utc() {
242        let v = Value::date_time("2000-01-01T00:00:00Z");
243        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
244        assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), "2000-01-01T00:00:00Z");
245    }
246
247    #[test]
248    fn value_date_time_string_with_offset_to_chrono_utc() {
249        let v = Value::date_time("2000-01-01T01:00:00+01:00");
250        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
251        // Should be converted to UTC
252        assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), "2000-01-01T00:00:00Z");
253    }
254
255    #[test]
256    fn value_epoch_int_to_chrono_utc() {
257        let v = Value::epoch_time(946684800_u64); // 2000-01-01T00:00:00Z
258        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
259        assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), "2000-01-01T00:00:00Z");
260    }
261
262    #[test]
263    fn value_epoch_float_to_chrono_utc() {
264        let v = Value::epoch_time(946684800.5_f64);
265        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
266        assert_eq!(dt.timestamp(), 946684800);
267        assert_eq!(dt.timestamp_subsec_nanos(), 500_000_000);
268    }
269
270    // ---- Value → chrono::DateTime<FixedOffset> ----
271
272    #[test]
273    fn value_date_time_string_to_chrono_fixed_preserves_offset() {
274        let v = Value::date_time("2000-01-01T01:00:00+01:00");
275        let dt: DateTime<FixedOffset> = DateTime::try_from(&v).unwrap();
276        assert_eq!(dt.to_rfc3339(), "2000-01-01T01:00:00+01:00");
277        assert_eq!(
278            v.to_system_time(),
279            Value::date_time("2000-01-01T00:00:00Z").to_system_time()
280        );
281    }
282
283    #[test]
284    fn value_epoch_to_chrono_fixed_is_utc() {
285        let v = Value::epoch_time(0_u64);
286        let dt: DateTime<FixedOffset> = DateTime::try_from(&v).unwrap();
287        assert_eq!(dt.offset().local_minus_utc(), 0);
288    }
289
290    // ---- Error cases ----
291
292    #[test]
293    fn value_non_time_to_chrono_errors() {
294        assert_eq!(
295            DateTime::<Utc>::try_from(Value::from("not a date")),
296            Err(Error::InvalidFormat)
297        );
298        assert_eq!(
299            DateTime::<Utc>::try_from(Value::null()),
300            Err(Error::IncompatibleType(DataType::Null))
301        );
302    }
303
304    #[test]
305    fn value_negative_epoch_to_chrono_errors() {
306        let v = Value::from(-1);
307        assert_eq!(DateTime::<Utc>::try_from(v), Err(Error::InvalidValue));
308    }
309
310    // ---- SystemTime ↔ Value ↔ chrono roundtrips ----
311
312    #[test]
313    fn chrono_roundtrip_unix_epoch() {
314        let st = UNIX_EPOCH;
315        let v = Value::date_time(st);
316        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
317        assert_eq!(dt.timestamp(), 0);
318        assert_eq!(dt.timestamp_subsec_nanos(), 0);
319        // And back
320        let st2 = v.to_system_time().unwrap();
321        assert_eq!(st, st2);
322    }
323
324    #[test]
325    fn chrono_roundtrip_y2k() {
326        let st = UNIX_EPOCH + Duration::from_secs(946684800);
327        let v = Value::date_time(st);
328        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
329        assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), "2000-01-01T00:00:00Z");
330        let st2 = v.to_system_time().unwrap();
331        assert_eq!(st, st2);
332    }
333
334    #[test]
335    fn chrono_roundtrip_y2k38() {
336        // 2038-01-19T03:14:07Z — the 32-bit overflow moment
337        let st = UNIX_EPOCH + Duration::from_secs(2147483647);
338        let v = Value::date_time(st);
339        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
340        assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), "2038-01-19T03:14:07Z");
341        let st2 = v.to_system_time().unwrap();
342        assert_eq!(st, st2);
343    }
344
345    #[test]
346    fn chrono_roundtrip_subsec_via_epoch() {
347        let st = UNIX_EPOCH + Duration::new(1_000_000, 123_000_000);
348        let v = Value::epoch_time(st);
349        let dt: DateTime<Utc> = DateTime::try_from(&v).unwrap();
350        assert_eq!(dt.timestamp(), 1_000_000);
351        assert_eq!(dt.timestamp_subsec_millis(), 123);
352    }
353
354    #[test]
355    fn chrono_roundtrip_chrono_to_value_to_chrono() {
356        let original = NaiveDate::from_ymd_opt(2024, 7, 4)
357            .unwrap()
358            .and_hms_opt(12, 0, 0)
359            .unwrap()
360            .and_utc();
361        let cbor_dt = crate::DateTime::try_from(original).unwrap();
362        let v = Value::date_time(cbor_dt);
363        let back: DateTime<Utc> = DateTime::try_from(&v).unwrap();
364        assert_eq!(original, back);
365    }
366}