1#[cfg(not(feature = "chrono"))]
41pub type DateTime = time::OffsetDateTime;
42
43#[cfg(feature = "chrono")]
50pub type DateTime = chrono::DateTime<chrono::Utc>;
51
52#[cfg(not(feature = "chrono"))]
54pub fn parse_rfc3339(s: &str) -> Result<DateTime, crate::Error> {
55 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
56 .map(|dt| dt.to_offset(time::UtcOffset::UTC))
58 .map_err(|e| e.into())
59}
60
61#[cfg(feature = "chrono")]
63pub fn parse_rfc3339(s: &str) -> Result<DateTime, crate::Error> {
64 chrono::DateTime::parse_from_rfc3339(s)
65 .map(|dt| dt.with_timezone(&chrono::Utc))
66 .map_err(|e| e.into())
67}
68
69#[cfg(not(feature = "chrono"))]
71pub fn from_nanos(nanos: i128) -> Result<DateTime, crate::Error> {
72 time::OffsetDateTime::from_unix_timestamp_nanos(nanos).map_err(|e| e.into())
73}
74
75#[cfg(feature = "chrono")]
81pub fn from_nanos(nanos: i128) -> Result<DateTime, crate::Error> {
82 let nanos_i64: i64 = nanos.try_into().map_err(|_| {
83 std::io::Error::new(
84 std::io::ErrorKind::InvalidInput,
85 "nanoseconds value out of range for the chrono backend (supports ~1678 to 2262)",
86 )
87 })?;
88 let secs = nanos_i64.div_euclid(1_000_000_000);
91 let nsecs = nanos_i64.rem_euclid(1_000_000_000) as u32;
92 chrono::DateTime::from_timestamp(secs, nsecs).ok_or_else(|| {
93 Box::new(std::io::Error::new(
94 std::io::ErrorKind::InvalidInput,
95 "invalid timestamp",
96 )) as crate::Error
97 })
98}
99
100#[cfg(not(feature = "chrono"))]
102pub fn now() -> DateTime {
103 time::OffsetDateTime::now_utc()
104}
105
106#[cfg(feature = "chrono")]
108pub fn now() -> DateTime {
109 chrono::Utc::now()
110}
111
112#[cfg(not(feature = "chrono"))]
116pub(crate) use time::serde::rfc3339;
117
118#[cfg(feature = "chrono")]
123#[allow(dead_code)]
124pub(crate) mod rfc3339 {
125 use serde::{Deserialize, Deserializer, Serialize, Serializer};
126 pub(crate) fn serialize<S>(
127 dt: &chrono::DateTime<chrono::Utc>,
128 serializer: S,
129 ) -> Result<S::Ok, S::Error>
130 where
131 S: Serializer,
132 {
133 Serialize::serialize(dt, serializer)
134 }
135 pub(crate) fn deserialize<'de, D>(
136 deserializer: D,
137 ) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
138 where
139 D: Deserializer<'de>,
140 {
141 Deserialize::deserialize(deserializer)
142 }
143
144 pub(crate) mod option {
145 use serde::{Deserialize, Deserializer, Serialize, Serializer};
146 pub(crate) fn serialize<S>(
147 dt: &Option<chrono::DateTime<chrono::Utc>>,
148 serializer: S,
149 ) -> Result<S::Ok, S::Error>
150 where
151 S: Serializer,
152 {
153 Serialize::serialize(dt, serializer)
154 }
155 pub(crate) fn deserialize<'de, D>(
156 deserializer: D,
157 ) -> Result<Option<chrono::DateTime<chrono::Utc>>, D::Error>
158 where
159 D: Deserializer<'de>,
160 {
161 Deserialize::deserialize(deserializer)
162 }
163 }
164}
165
166#[cfg(not(feature = "chrono"))]
171pub fn add_std_duration(
172 dt: DateTime,
173 duration: std::time::Duration,
174) -> Result<DateTime, crate::Error> {
175 let time_duration = time::Duration::try_from(duration)?;
176 dt.checked_add(time_duration).ok_or_else(|| {
177 Box::new(std::io::Error::new(
178 std::io::ErrorKind::InvalidInput,
179 "datetime overflow",
180 )) as crate::Error
181 })
182}
183
184#[cfg(feature = "chrono")]
189pub fn add_std_duration(
190 dt: DateTime,
191 duration: std::time::Duration,
192) -> Result<DateTime, crate::Error> {
193 let chrono_duration = chrono::Duration::from_std(duration)?;
194 dt.checked_add_signed(chrono_duration).ok_or_else(|| {
195 Box::new(std::io::Error::new(
196 std::io::ErrorKind::InvalidInput,
197 "datetime overflow",
198 )) as crate::Error
199 })
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use serde::{Deserialize, Serialize};
206
207 #[derive(Serialize, Deserialize)]
208 struct Wrap(#[serde(with = "rfc3339")] DateTime);
209
210 #[derive(Serialize, Deserialize, Default)]
211 struct WrapOpt(
212 #[serde(
213 default,
214 with = "rfc3339::option",
215 skip_serializing_if = "Option::is_none"
216 )]
217 Option<DateTime>,
218 );
219
220 const FIXED_NANOS: i128 = 1_672_628_645_123_456_789;
223 const FIXED_RFC3339: &str = "2023-01-02T03:04:05.123456789Z";
224
225 #[test]
229 fn rfc3339_full_precision_is_byte_identical() {
230 let dt = from_nanos(FIXED_NANOS).unwrap();
231 let json = serde_json::to_string(&Wrap(dt)).unwrap();
232 assert_eq!(json, format!("\"{FIXED_RFC3339}\""));
233 }
234
235 #[test]
236 fn rfc3339_roundtrips() {
237 let dt = from_nanos(FIXED_NANOS).unwrap();
238 let json = serde_json::to_string(&Wrap(dt)).unwrap();
239 let back: Wrap = serde_json::from_str(&json).unwrap();
240 assert_eq!(serde_json::to_string(&back).unwrap(), json);
241 }
242
243 #[test]
244 fn parse_rfc3339_matches_from_nanos() {
245 let parsed = parse_rfc3339(FIXED_RFC3339).unwrap();
246 let from_ts = from_nanos(FIXED_NANOS).unwrap();
247 assert_eq!(parsed, from_ts);
248 }
249
250 #[test]
254 fn parse_rfc3339_server_format_variants() {
255 let cases: &[(&str, i128)] = &[
256 ("2023-01-02T03:04:05Z", 1_672_628_645_000_000_000),
258 ("2023-01-02T03:04:05.123Z", 1_672_628_645_123_000_000),
260 ("2023-01-02T03:04:05.123456Z", 1_672_628_645_123_456_000),
262 ("2023-01-02T03:04:05.123456789Z", 1_672_628_645_123_456_789),
264 ];
265 for (s, nanos) in cases {
266 let parsed = parse_rfc3339(s).unwrap_or_else(|e| panic!("parse {s:?}: {e}"));
267 assert_eq!(
268 parsed,
269 from_nanos(*nanos).unwrap(),
270 "instant mismatch for {s:?}"
271 );
272 let de: Wrap = serde_json::from_str(&format!("\"{s}\""))
274 .unwrap_or_else(|e| panic!("serde deserialize {s:?}: {e}"));
275 assert_eq!(
276 de.0,
277 from_nanos(*nanos).unwrap(),
278 "serde mismatch for {s:?}"
279 );
280 }
281 }
282
283 #[test]
287 fn rfc3339_trailing_zero_fractions_are_instant_equal() {
288 for nanos in [
289 1_672_628_645_120_000_000, 1_672_628_645_100_000_000, 1_672_628_645_000_000_000, ] {
293 let dt = from_nanos(nanos).unwrap();
294 let json = serde_json::to_string(&Wrap(dt)).unwrap();
295 let back: Wrap = serde_json::from_str(&json).unwrap();
296 assert_eq!(
297 back.0,
298 from_nanos(nanos).unwrap(),
299 "instant changed for {nanos}"
300 );
301 let s = json.trim_matches('"');
303 assert_eq!(parse_rfc3339(s).unwrap(), from_nanos(nanos).unwrap());
304 }
305 }
306
307 #[test]
310 fn from_nanos_handles_negative_sub_second() {
311 let cases: &[(i128, &str)] = &[
312 (-1, "1969-12-31T23:59:59.999999999Z"),
313 (-1_500_000_000, "1969-12-31T23:59:58.500000000Z"),
314 (-1_000_000_000, "1969-12-31T23:59:59Z"),
315 ];
316 for (nanos, expected) in cases {
317 let dt = from_nanos(*nanos).unwrap_or_else(|e| panic!("from_nanos({nanos}): {e}"));
318 assert_eq!(dt, parse_rfc3339(expected).unwrap(), "mismatch for {nanos}");
319 }
320 }
321
322 #[test]
324 fn rfc3339_option_serde() {
325 let some = WrapOpt(Some(from_nanos(FIXED_NANOS).unwrap()));
326 let json = serde_json::to_string(&some).unwrap();
327 assert_eq!(json, format!("\"{FIXED_RFC3339}\""));
328 let back: WrapOpt = serde_json::from_str(&json).unwrap();
329 assert_eq!(back.0, Some(from_nanos(FIXED_NANOS).unwrap()));
330
331 let null: WrapOpt = serde_json::from_str("null").unwrap();
332 assert_eq!(null.0, None);
333
334 #[derive(Deserialize)]
336 struct Outer {
337 #[serde(default, with = "rfc3339::option")]
338 ts: Option<DateTime>,
339 }
340 let outer: Outer = serde_json::from_str("{}").unwrap();
341 assert_eq!(outer.ts, None);
342 }
343
344 #[test]
346 fn parse_rfc3339_normalizes_offset() {
347 let with_offset = parse_rfc3339("2023-01-02T03:04:05+02:00").unwrap();
349 let utc = parse_rfc3339("2023-01-02T01:04:05Z").unwrap();
350 assert_eq!(with_offset, utc);
351 }
352
353 #[cfg(not(feature = "chrono"))]
356 #[test]
357 fn datetime_is_time_by_default() {
358 fn assert_offset_date_time(_: time::OffsetDateTime) {}
359 assert_offset_date_time(now());
360 }
361
362 #[cfg(feature = "chrono")]
363 #[test]
364 fn datetime_is_chrono_when_enabled() {
365 fn assert_chrono(_: chrono::DateTime<chrono::Utc>) {}
366 assert_chrono(now());
367 }
368}