Skip to main content

cbor_core/ext/
time.rs

1use time::{OffsetDateTime, UtcDateTime, UtcOffset, format_description::well_known::Rfc3339};
2
3use crate::{Error, Result, Value};
4
5fn offset_date_time_from_str(s: &str) -> Result<OffsetDateTime> {
6    OffsetDateTime::parse(s, &Rfc3339).or(Err(Error::InvalidFormat))
7}
8
9fn utc_date_time_from_float(f: f64) -> Result<UtcDateTime> {
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 i64;
13        Ok(UtcDateTime::from_unix_timestamp(secs).or(Err(Error::Overflow))? + time::Duration::nanoseconds(nanos))
14    } else {
15        Err(Error::InvalidValue)
16    }
17}
18
19fn utc_date_time_from_u64(secs: u64) -> Result<UtcDateTime> {
20    let secs = secs.try_into().or(Err(Error::Overflow))?;
21    UtcDateTime::from_unix_timestamp(secs).or(Err(Error::Overflow))
22}
23
24// ---------------------------------------------------------------------------
25// time::OffsetDateTime → crate::DateTime
26// ---------------------------------------------------------------------------
27
28impl TryFrom<OffsetDateTime> for crate::DateTime {
29    type Error = Error;
30
31    /// Converts a `time::OffsetDateTime` to a CBOR date/time string (tag 0).
32    ///
33    /// Whole-second timestamps omit the fractional part and sub-second
34    /// timestamps include only the necessary digits.
35    fn try_from(dt: OffsetDateTime) -> Result<Self> {
36        crate::DateTime::try_from(dt.format(&Rfc3339).or(Err(Error::Overflow))?)
37    }
38}
39
40impl TryFrom<&OffsetDateTime> for crate::DateTime {
41    type Error = Error;
42
43    fn try_from(dt: &OffsetDateTime) -> Result<Self> {
44        crate::DateTime::try_from(dt.format(&Rfc3339).or(Err(Error::Overflow))?)
45    }
46}
47
48// ---------------------------------------------------------------------------
49// time::UtcDateTime → crate::DateTime
50// ---------------------------------------------------------------------------
51
52impl TryFrom<UtcDateTime> for crate::DateTime {
53    type Error = Error;
54
55    /// Converts a `time::UtcDateTime` to a CBOR date/time string (tag 0).
56    ///
57    /// Whole-second timestamps omit the fractional part and sub-second
58    /// timestamps include only the necessary digits.
59    fn try_from(dt: UtcDateTime) -> Result<Self> {
60        crate::DateTime::try_from(dt.format(&Rfc3339).or(Err(Error::Overflow))?)
61    }
62}
63
64impl TryFrom<&UtcDateTime> for crate::DateTime {
65    type Error = Error;
66
67    fn try_from(dt: &UtcDateTime) -> Result<Self> {
68        crate::DateTime::try_from(dt.format(&Rfc3339).or(Err(Error::Overflow))?)
69    }
70}
71
72// ---------------------------------------------------------------------------
73// time::OffsetDateTime → crate::EpochTime
74// ---------------------------------------------------------------------------
75
76impl TryFrom<OffsetDateTime> for crate::EpochTime {
77    type Error = Error;
78
79    /// Converts a `time::OffsetDateTime` to a CBOR epoch time (tag 1).
80    ///
81    /// Whole-second timestamps are stored as integers; sub-second
82    /// timestamps are stored as floats.
83    fn try_from(value: OffsetDateTime) -> Result<Self> {
84        offset_date_time_to_epoch(&value)
85    }
86}
87
88impl TryFrom<&OffsetDateTime> for crate::EpochTime {
89    type Error = Error;
90
91    fn try_from(value: &OffsetDateTime) -> Result<Self> {
92        offset_date_time_to_epoch(value)
93    }
94}
95
96fn offset_date_time_to_epoch(value: &OffsetDateTime) -> Result<crate::EpochTime> {
97    let secs = value.unix_timestamp();
98    let nanos = value.unix_timestamp_nanos();
99
100    if nanos % 1_000_000_000 == 0 {
101        crate::EpochTime::try_from(secs)
102    } else {
103        let f = nanos as f64 / 1_000_000_000.0;
104        crate::EpochTime::try_from(f)
105    }
106}
107
108// ---------------------------------------------------------------------------
109// time::UtcDateTime → crate::EpochTime
110// ---------------------------------------------------------------------------
111
112impl TryFrom<UtcDateTime> for crate::EpochTime {
113    type Error = Error;
114
115    /// Converts a `time::UtcDateTime` to a CBOR epoch time (tag 1).
116    ///
117    /// Whole-second timestamps are stored as integers; sub-second
118    /// timestamps are stored as floats.
119    fn try_from(value: UtcDateTime) -> Result<Self> {
120        utc_date_time_to_epoch(&value)
121    }
122}
123
124impl TryFrom<&UtcDateTime> for crate::EpochTime {
125    type Error = Error;
126
127    fn try_from(value: &UtcDateTime) -> Result<Self> {
128        utc_date_time_to_epoch(value)
129    }
130}
131
132fn utc_date_time_to_epoch(value: &UtcDateTime) -> Result<crate::EpochTime> {
133    let secs = value.unix_timestamp();
134    let nanos = value.unix_timestamp_nanos();
135
136    if nanos % 1_000_000_000 == 0 {
137        crate::EpochTime::try_from(secs)
138    } else {
139        let f = nanos as f64 / 1_000_000_000.0;
140        crate::EpochTime::try_from(f)
141    }
142}
143
144// ---------------------------------------------------------------------------
145// Value → time::OffsetDateTime
146// ---------------------------------------------------------------------------
147
148impl TryFrom<Value> for OffsetDateTime {
149    type Error = Error;
150
151    /// Extracts a `time::OffsetDateTime` from a CBOR time value.
152    ///
153    /// Date/time strings (tag 0) preserve the original timezone offset.
154    /// Epoch integers/floats are returned with a UTC offset.
155    fn try_from(value: Value) -> Result<Self> {
156        value_to_time_offset_data_time(&value)
157    }
158}
159
160impl TryFrom<&Value> for OffsetDateTime {
161    type Error = Error;
162
163    fn try_from(value: &Value) -> Result<Self> {
164        value_to_time_offset_data_time(value)
165    }
166}
167
168fn value_to_time_offset_data_time(value: &Value) -> Result<OffsetDateTime> {
169    if let Ok(s) = value.as_str() {
170        offset_date_time_from_str(s)
171    } else if let Ok(f) = value.to_f64() {
172        utc_date_time_from_float(f).map(|dt| dt.to_offset(UtcOffset::UTC))
173    } else {
174        match value.to_u64() {
175            Ok(secs) => utc_date_time_from_u64(secs).map(|dt| dt.to_offset(UtcOffset::UTC)),
176            Err(Error::NegativeUnsigned) => Err(Error::InvalidValue),
177            Err(other_error) => Err(other_error),
178        }
179    }
180}
181
182// ---------------------------------------------------------------------------
183// Value → time::UtcDateTime
184// ---------------------------------------------------------------------------
185
186impl TryFrom<Value> for UtcDateTime {
187    type Error = Error;
188
189    /// Extracts a `time::UtcDateTime` from a CBOR time value.
190    ///
191    /// Date/time strings (tag 0) preserve the original timezone offset.
192    /// Epoch integers/floats are returned with a UTC offset.
193    fn try_from(value: Value) -> Result<Self> {
194        value_to_time_utc_data_time(&value)
195    }
196}
197
198impl TryFrom<&Value> for UtcDateTime {
199    type Error = Error;
200
201    fn try_from(value: &Value) -> Result<Self> {
202        value_to_time_utc_data_time(value)
203    }
204}
205
206fn value_to_time_utc_data_time(value: &Value) -> Result<UtcDateTime> {
207    if let Ok(s) = value.as_str() {
208        offset_date_time_from_str(s).map(|dt| dt.to_utc())
209    } else if let Ok(f) = value.to_f64() {
210        utc_date_time_from_float(f)
211    } else {
212        match value.to_u64() {
213            Ok(secs) => utc_date_time_from_u64(secs),
214            Err(Error::NegativeUnsigned) => Err(Error::InvalidValue),
215            Err(other_error) => Err(other_error),
216        }
217    }
218}
219
220// ---------------------------------------------------------------------------
221// Tests
222// ---------------------------------------------------------------------------
223
224#[cfg(test)]
225mod tests {
226
227    use std::time::{Duration, SystemTime};
228
229    use time::{Date, Month, OffsetDateTime, Time, UtcDateTime, UtcOffset, format_description::well_known::Rfc3339};
230
231    use crate::{DataType, Error, Float, Value};
232
233    // ---- time::UtcDateTime → crate::DateTime (tag 0) ----
234
235    #[test]
236    fn time_to_date_time_utc() {
237        let d = Date::from_calendar_date(2000, Month::January, 1).unwrap();
238        let t = Time::from_hms(0, 0, 0).unwrap();
239        let dt = UtcDateTime::new(d, t);
240        let v = Value::date_time(dt);
241        assert_eq!(v.as_str(), Ok("2000-01-01T00:00:00Z"));
242    }
243
244    #[test]
245    fn time_to_date_time_utc_subsec() {
246        let d = Date::from_calendar_date(2000, Month::January, 1).unwrap();
247        let t = Time::from_hms_nano(12, 30, 45, 123456789).unwrap();
248        let dt = UtcDateTime::new(d, t);
249        let v = Value::date_time(dt);
250        assert_eq!(v.as_str(), Ok("2000-01-01T12:30:45.123456789Z"));
251    }
252
253    #[test]
254    fn time_to_date_time_utc_millis_only() {
255        let d = Date::from_calendar_date(2024, Month::June, 15).unwrap();
256        let t = Time::from_hms_milli(8, 0, 0, 123).unwrap();
257        let dt = UtcDateTime::new(d, t);
258        let v = Value::date_time(dt);
259        assert_eq!(v.as_str(), Ok("2024-06-15T08:00:00.123Z"));
260    }
261
262    // ---- time::UtcDateTime → crate::EpochTime (tag 1) ----
263
264    #[test]
265    fn time_to_epoch_time_whole_second() {
266        let dt = UtcDateTime::from_unix_timestamp(1_000_000).unwrap();
267        let v = Value::epoch_time(dt);
268        // Whole second → integer inside the tag
269        assert_eq!(v.into_untagged(), Value::Unsigned(1_000_000));
270    }
271
272    #[test]
273    fn time_to_epoch_time_subsec() {
274        let dt = UtcDateTime::from_unix_timestamp_nanos(1_000_000_500_000_000).unwrap();
275        let v = Value::epoch_time(dt);
276        // Sub-second → float inside the tag
277        assert_eq!(v.into_untagged(), Value::Float(Float::from(1000000.5)));
278    }
279
280    #[test]
281    fn time_to_epoch_time_negative() {
282        // Before epoch — should fail (our EpochTime is non-negative only)
283        let dt = UtcDateTime::from_unix_timestamp(-1).unwrap();
284        assert_eq!(crate::EpochTime::try_from(dt), Err(Error::InvalidValue));
285    }
286
287    // ---- Value → time::UtcDateTime ----
288
289    #[test]
290    fn value_date_time_string_to_time_utc() {
291        let v = Value::date_time("2000-01-01T00:00:00Z");
292        let dt = UtcDateTime::try_from(v).unwrap();
293        assert_eq!(dt.format(&Rfc3339).unwrap(), "2000-01-01T00:00:00Z");
294    }
295
296    #[test]
297    fn value_date_time_string_with_offset_to_time_utc() {
298        let v = Value::date_time("2000-01-01T01:00:00+01:00");
299        let dt = UtcDateTime::try_from(v).unwrap();
300        // Should be converted to UTC
301        assert_eq!(dt.format(&Rfc3339).unwrap(), "2000-01-01T00:00:00Z");
302    }
303
304    #[test]
305    fn value_epoch_int_to_time_utc() {
306        let v = Value::epoch_time(946684800_u64); // 2000-01-01T00:00:00Z
307        let dt = UtcDateTime::try_from(v).unwrap();
308        assert_eq!(dt.format(&Rfc3339).unwrap(), "2000-01-01T00:00:00Z");
309    }
310
311    #[test]
312    fn value_epoch_float_to_time_utc() {
313        let v = Value::epoch_time(946684800.5_f64);
314        let dt = UtcDateTime::try_from(v).unwrap();
315        assert_eq!(dt.unix_timestamp_nanos(), 946_684_800_500_000_000);
316    }
317
318    // ---- Value → time::OffsetDateTime ----
319
320    #[test]
321    fn value_date_time_string_to_time_fixed_preserves_offset() {
322        let v = Value::date_time("2000-01-01T01:00:00+01:00");
323        let dt = OffsetDateTime::try_from(&v).unwrap();
324        assert_eq!(dt.format(&Rfc3339).unwrap(), "2000-01-01T01:00:00+01:00".to_string());
325        assert_eq!(
326            v.to_system_time(),
327            Value::date_time("2000-01-01T00:00:00Z").to_system_time()
328        );
329    }
330
331    #[test]
332    fn value_epoch_to_time_fixed_is_utc() {
333        let v = Value::epoch_time(0_u64);
334        let dt = OffsetDateTime::try_from(&v).unwrap();
335        assert_eq!(dt.offset(), UtcOffset::UTC);
336    }
337
338    // ---- Error cases ----
339
340    #[test]
341    fn value_non_time_to_time_errors() {
342        assert_eq!(
343            UtcDateTime::try_from(Value::from("not a date")),
344            Err(Error::InvalidFormat)
345        );
346        assert_eq!(
347            UtcDateTime::try_from(Value::null()),
348            Err(Error::IncompatibleType(DataType::Null))
349        );
350    }
351
352    #[test]
353    fn value_negative_epoch_to_time_errors() {
354        let v = Value::from(-1);
355        assert_eq!(UtcDateTime::try_from(v), Err(Error::InvalidValue));
356    }
357
358    // ---- SystemTime ↔ Value ↔ chrono roundtrips ----
359
360    #[test]
361    fn time_roundtrip_unix_epoch() {
362        let st = SystemTime::UNIX_EPOCH;
363        let v = Value::date_time(st);
364        let dt = UtcDateTime::try_from(&v).unwrap();
365        assert_eq!(dt.unix_timestamp(), 0);
366        assert_eq!(dt.unix_timestamp_nanos(), 0);
367        // And back
368        let st2 = v.to_system_time().unwrap();
369        assert_eq!(st, st2);
370    }
371
372    #[test]
373    fn time_roundtrip_y2k() {
374        let st = SystemTime::UNIX_EPOCH + Duration::from_secs(946684800);
375        let v = Value::date_time(st);
376        let dt = UtcDateTime::try_from(&v).unwrap();
377        assert_eq!(dt.format(&Rfc3339).unwrap(), "2000-01-01T00:00:00Z".to_string());
378        let st2 = v.to_system_time().unwrap();
379        assert_eq!(st, st2);
380    }
381
382    #[test]
383    fn time_roundtrip_y2k38() {
384        // 2038-01-19T03:14:07Z — the 32-bit overflow moment
385        let st = SystemTime::UNIX_EPOCH + Duration::from_secs(2147483647);
386        let v = Value::date_time(st);
387        let dt = UtcDateTime::try_from(&v).unwrap();
388        assert_eq!(dt.format(&Rfc3339).unwrap(), "2038-01-19T03:14:07Z".to_string());
389        let st2 = v.to_system_time().unwrap();
390        assert_eq!(st, st2);
391    }
392
393    #[test]
394    fn time_roundtrip_subsec_via_epoch() {
395        let st = SystemTime::UNIX_EPOCH + Duration::new(1_000_000, 123_000_000);
396        let v = Value::epoch_time(st);
397        let dt = UtcDateTime::try_from(&v).unwrap();
398        assert_eq!(dt.unix_timestamp(), 1_000_000);
399        assert_eq!(dt.unix_timestamp_nanos(), 1_000_000_123_000_000);
400    }
401
402    #[test]
403    fn time_roundtrip_time_to_value_to_time() {
404        let d = Date::from_calendar_date(2024, Month::July, 4).unwrap();
405        let t = Time::from_hms(12, 0, 0).unwrap();
406        let original = UtcDateTime::new(d, t);
407        let v = Value::date_time(original);
408        let back = UtcDateTime::try_from(&v).unwrap();
409        assert_eq!(original, back);
410    }
411}