1use jiff::{
2 Timestamp, Zoned,
3 fmt::temporal::Pieces,
4 tz::{Offset, TimeZone},
5};
6
7use crate::{Error, Result, Value};
8
9fn timestamp_from_str(s: &str) -> Result<Timestamp> {
10 s.parse::<Timestamp>().or(Err(Error::InvalidFormat))
11}
12
13fn zoned_from_str(s: &str) -> Result<Zoned> {
14 let ts = timestamp_from_str(s)?;
15 let pieces = Pieces::parse(s).or(Err(Error::InvalidFormat))?;
16 let offset = pieces.to_numeric_offset().unwrap_or(Offset::UTC);
17 Ok(ts.to_zoned(TimeZone::fixed(offset)))
18}
19
20fn timestamp_from_float(f: f64) -> Result<Timestamp> {
21 if f.is_finite() && f >= 0.0 {
22 let secs = f.trunc() as i64;
23 let nanos = ((f - f.trunc()) * 1_000_000_000.0).round() as i32;
24 Timestamp::new(secs, nanos).or(Err(Error::InvalidValue))
25 } else {
26 Err(Error::InvalidValue)
27 }
28}
29
30fn timestamp_from_u64(secs: u64) -> Result<Timestamp> {
31 let secs: i64 = secs.try_into().or(Err(Error::Overflow))?;
32 Timestamp::from_second(secs).or(Err(Error::Overflow))
33}
34
35impl TryFrom<Timestamp> for crate::DateTime {
40 type Error = Error;
41
42 fn try_from(value: Timestamp) -> Result<Self> {
47 crate::DateTime::try_from(value.to_string())
48 }
49}
50
51impl TryFrom<&Timestamp> for crate::DateTime {
52 type Error = Error;
53
54 fn try_from(value: &Timestamp) -> Result<Self> {
55 crate::DateTime::try_from(value.to_string())
56 }
57}
58
59impl TryFrom<Zoned> for crate::DateTime {
64 type Error = Error;
65
66 fn try_from(value: Zoned) -> Result<Self> {
73 crate::DateTime::try_from(&value)
74 }
75}
76
77impl TryFrom<&Zoned> for crate::DateTime {
78 type Error = Error;
79
80 fn try_from(value: &Zoned) -> Result<Self> {
81 let s = value.timestamp().display_with_offset(value.offset()).to_string();
82 crate::DateTime::try_from(s)
83 }
84}
85
86impl TryFrom<Timestamp> for crate::EpochTime {
91 type Error = Error;
92
93 fn try_from(value: Timestamp) -> Result<Self> {
98 timestamp_to_epoch(&value)
99 }
100}
101
102impl TryFrom<&Timestamp> for crate::EpochTime {
103 type Error = Error;
104
105 fn try_from(value: &Timestamp) -> Result<Self> {
106 timestamp_to_epoch(value)
107 }
108}
109
110fn timestamp_to_epoch(value: &Timestamp) -> Result<crate::EpochTime> {
111 let secs = value.as_second();
112 let nanos = value.subsec_nanosecond();
113
114 if nanos == 0 {
115 crate::EpochTime::try_from(secs)
116 } else {
117 let f = secs as f64 + nanos as f64 / 1_000_000_000.0;
118 crate::EpochTime::try_from(f)
119 }
120}
121
122impl TryFrom<Zoned> for crate::EpochTime {
127 type Error = Error;
128
129 fn try_from(value: Zoned) -> Result<Self> {
134 timestamp_to_epoch(&value.timestamp())
135 }
136}
137
138impl TryFrom<&Zoned> for crate::EpochTime {
139 type Error = Error;
140
141 fn try_from(value: &Zoned) -> Result<Self> {
142 timestamp_to_epoch(&value.timestamp())
143 }
144}
145
146impl TryFrom<Value> for Timestamp {
151 type Error = Error;
152
153 fn try_from(value: Value) -> Result<Self> {
158 value_to_timestamp(&value)
159 }
160}
161
162impl TryFrom<&Value> for Timestamp {
163 type Error = Error;
164
165 fn try_from(value: &Value) -> Result<Self> {
166 value_to_timestamp(value)
167 }
168}
169
170fn value_to_timestamp(value: &Value) -> Result<Timestamp> {
171 if let Ok(s) = value.as_str() {
172 timestamp_from_str(s)
173 } else if let Ok(f) = value.to_f64() {
174 timestamp_from_float(f)
175 } else {
176 match value.to_u64() {
177 Ok(secs) => timestamp_from_u64(secs),
178 Err(Error::NegativeUnsigned) => Err(Error::InvalidValue),
179 Err(other_error) => Err(other_error),
180 }
181 }
182}
183
184impl TryFrom<Value> for Zoned {
189 type Error = Error;
190
191 fn try_from(value: Value) -> Result<Self> {
196 value_to_zoned(&value)
197 }
198}
199
200impl TryFrom<&Value> for Zoned {
201 type Error = Error;
202
203 fn try_from(value: &Value) -> Result<Self> {
204 value_to_zoned(value)
205 }
206}
207
208fn value_to_zoned(value: &Value) -> Result<Zoned> {
209 if let Ok(s) = value.as_str() {
210 zoned_from_str(s)
211 } else if let Ok(f) = value.to_f64() {
212 timestamp_from_float(f).map(|ts| ts.to_zoned(TimeZone::UTC))
213 } else {
214 match value.to_u64() {
215 Ok(secs) => timestamp_from_u64(secs).map(|ts| ts.to_zoned(TimeZone::UTC)),
216 Err(Error::NegativeUnsigned) => Err(Error::InvalidValue),
217 Err(other_error) => Err(other_error),
218 }
219 }
220}
221
222#[cfg(test)]
227mod tests {
228 use std::time::{Duration, UNIX_EPOCH};
229
230 use jiff::{Timestamp, Zoned, tz::TimeZone};
231
232 use crate::{DataType, Error, Float, Value};
233
234 #[test]
237 fn jiff_to_date_time_utc() {
238 let ts: Timestamp = "2000-01-01T00:00:00Z".parse().unwrap();
239 let v = Value::date_time(ts);
240 assert_eq!(v.as_str(), Ok("2000-01-01T00:00:00Z"));
241 }
242
243 #[test]
244 fn jiff_to_date_time_subsec() {
245 let ts = Timestamp::new(946_684_800, 123_456_789).unwrap();
246 let v = Value::date_time(ts);
247 assert_eq!(v.as_str(), Ok("2000-01-01T00:00:00.123456789Z"));
248 }
249
250 #[test]
251 fn jiff_to_date_time_millis_only() {
252 let ts = Timestamp::new(1_718_438_400, 500_000_000).unwrap();
253 let v = Value::date_time(ts);
254 assert_eq!(v.as_str(), Ok("2024-06-15T08:00:00.5Z"));
255 }
256
257 #[test]
260 fn jiff_zoned_to_date_time_preserves_offset() {
261 let z: Zoned = "2000-01-01T01:00:00+01:00[+01:00]".parse().unwrap();
262 let v = Value::date_time(z);
263 assert_eq!(v.as_str(), Ok("2000-01-01T01:00:00+01:00"));
264 }
265
266 #[test]
269 fn jiff_to_epoch_time_whole_second() {
270 let ts = Timestamp::from_second(1_000_000).unwrap();
271 let v = Value::epoch_time(ts);
272 assert_eq!(v.into_untagged(), Value::Unsigned(1_000_000));
273 }
274
275 #[test]
276 fn jiff_to_epoch_time_subsec() {
277 let ts = Timestamp::new(1_000_000, 500_000_000).unwrap();
278 let v = Value::epoch_time(ts);
279 assert_eq!(v.into_untagged(), Value::Float(Float::from(1000000.5)));
280 }
281
282 #[test]
283 fn jiff_to_epoch_time_negative() {
284 let ts = Timestamp::from_second(-1).unwrap();
285 assert_eq!(crate::EpochTime::try_from(ts), Err(Error::InvalidValue));
286 }
287
288 #[test]
291 fn value_date_time_string_to_jiff_timestamp() {
292 let v = Value::date_time("2000-01-01T00:00:00Z");
293 let ts = Timestamp::try_from(v).unwrap();
294 assert_eq!(ts.to_string(), "2000-01-01T00:00:00Z");
295 }
296
297 #[test]
298 fn value_date_time_string_with_offset_to_jiff_timestamp() {
299 let v = Value::date_time("2000-01-01T01:00:00+01:00");
300 let ts = Timestamp::try_from(v).unwrap();
301 assert_eq!(ts.to_string(), "2000-01-01T00:00:00Z");
302 }
303
304 #[test]
305 fn value_epoch_int_to_jiff_timestamp() {
306 let v = Value::epoch_time(946684800_u64);
307 let ts = Timestamp::try_from(v).unwrap();
308 assert_eq!(ts.to_string(), "2000-01-01T00:00:00Z");
309 }
310
311 #[test]
312 fn value_epoch_float_to_jiff_timestamp() {
313 let v = Value::epoch_time(946684800.5_f64);
314 let ts = Timestamp::try_from(v).unwrap();
315 assert_eq!(ts.as_second(), 946684800);
316 assert_eq!(ts.subsec_nanosecond(), 500_000_000);
317 }
318
319 #[test]
322 fn value_date_time_string_to_jiff_zoned_preserves_offset() {
323 let v = Value::date_time("2000-01-01T01:00:00+01:00");
324 let z = Zoned::try_from(&v).unwrap();
325 assert_eq!(z.offset().seconds(), 3600);
326 assert_eq!(z.timestamp().to_string(), "2000-01-01T00:00:00Z");
327 }
328
329 #[test]
330 fn value_epoch_to_jiff_zoned_is_utc() {
331 let v = Value::epoch_time(0_u64);
332 let z = Zoned::try_from(&v).unwrap();
333 assert_eq!(z.offset().seconds(), 0);
334 assert_eq!(z.time_zone(), &TimeZone::UTC);
335 }
336
337 #[test]
340 fn value_non_time_to_jiff_errors() {
341 assert_eq!(
342 Timestamp::try_from(Value::from("not a date")),
343 Err(Error::InvalidFormat)
344 );
345 assert_eq!(
346 Timestamp::try_from(Value::null()),
347 Err(Error::IncompatibleType(DataType::Null))
348 );
349 }
350
351 #[test]
352 fn value_negative_epoch_to_jiff_errors() {
353 let v = Value::from(-1);
354 assert_eq!(Timestamp::try_from(v), Err(Error::InvalidValue));
355 }
356
357 #[test]
360 fn jiff_roundtrip_unix_epoch() {
361 let st = UNIX_EPOCH;
362 let v = Value::date_time(st);
363 let ts = Timestamp::try_from(&v).unwrap();
364 assert_eq!(ts.as_second(), 0);
365 assert_eq!(ts.subsec_nanosecond(), 0);
366 let st2 = v.to_system_time().unwrap();
367 assert_eq!(st, st2);
368 }
369
370 #[test]
371 fn jiff_roundtrip_y2k() {
372 let st = UNIX_EPOCH + Duration::from_secs(946684800);
373 let v = Value::date_time(st);
374 let ts = Timestamp::try_from(&v).unwrap();
375 assert_eq!(ts.to_string(), "2000-01-01T00:00:00Z");
376 let st2 = v.to_system_time().unwrap();
377 assert_eq!(st, st2);
378 }
379
380 #[test]
381 fn jiff_roundtrip_y2k38() {
382 let st = UNIX_EPOCH + Duration::from_secs(2147483647);
383 let v = Value::date_time(st);
384 let ts = Timestamp::try_from(&v).unwrap();
385 assert_eq!(ts.to_string(), "2038-01-19T03:14:07Z");
386 let st2 = v.to_system_time().unwrap();
387 assert_eq!(st, st2);
388 }
389
390 #[test]
391 fn jiff_roundtrip_subsec_via_epoch() {
392 let st = UNIX_EPOCH + Duration::new(1_000_000, 123_000_000);
393 let v = Value::epoch_time(st);
394 let ts = Timestamp::try_from(&v).unwrap();
395 assert_eq!(ts.as_second(), 1_000_000);
396 assert_eq!(ts.subsec_nanosecond(), 123_000_000);
397 }
398
399 #[test]
400 fn jiff_roundtrip_timestamp_to_value_to_timestamp() {
401 let original = Timestamp::new(1_720_094_400, 0).unwrap();
402 let cbor_dt = crate::DateTime::try_from(original).unwrap();
403 let v = Value::date_time(cbor_dt);
404 let back = Timestamp::try_from(&v).unwrap();
405 assert_eq!(original, back);
406 }
407}