pyo3_chrono/
lib.rs

1#![warn(missing_docs)]
2
3//! This crate provides newtype wrappers around chrono's [`NaiveDateTime`], [`NaiveDate`],
4//! [`NaiveTime`], and [`Duration`] structs, that can be used in [`PyO3`](pyo3) applications.
5//!
6//! Leap seconds are handled correctly, however timezones are not supported because Python itself
7//! doesn't inherently support timezones in its datetimes.
8//!
9//! Implementations for the [`serde::Serialize`] and [`serde::Deserialize`] traits can be enabled via the
10//! `serde` feature flag.
11//!
12//! # Truncation
13//! Python can store durations from negative one billion days up to positive one billion days long,
14//! in microsecond precision. However,
15//! Chrono only accepts microseconds as i64:
16//! ```text
17//! Python's max duration: 84599999999999999999 microseconds
18//! Chrono's max duration: 9223372036854775807 microseconds
19//!
20//! Python's min duration: -84599999915400000000 microseconds
21//! Chrono's min duration: -9223372036854775808 microseconds
22//! ```
23//! As you can see, Chrono doesn't support the entire range of durations that Python supports.
24//! When encountering durations that are unrepresentable in Chrono, this library truncates the
25//! duration to the nearest supported duration.
26
27pub use chrono;
28pub use pyo3;
29#[cfg(feature = "serde")]
30pub use serde_ as serde;
31
32use chrono::{Datelike as _, Timelike as _};
33use pyo3::types::{PyDateAccess as _, PyDeltaAccess as _, PyTimeAccess as _};
34
35fn chrono_to_micros_and_fold(time: impl chrono::Timelike) -> (u32, bool) {
36    if let Some(folded_nanos) = time.nanosecond().checked_sub(1_000_000_000) {
37        (folded_nanos / 1000, true)
38    } else {
39        (time.nanosecond() / 1000, false)
40    }
41}
42
43fn py_to_micros(time: &impl pyo3::types::PyTimeAccess) -> u32 {
44    if time.get_fold() {
45        time.get_microsecond() + 1_000_000
46    } else {
47        time.get_microsecond()
48    }
49}
50
51macro_rules! new_type {
52    ($doc:literal, $name:ident, $inner_type:ty) => {
53        #[doc = $doc]
54        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
55        pub struct $name(pub $inner_type);
56
57        impl From<$inner_type> for $name {
58            fn from(inner: $inner_type) -> Self {
59                Self(inner)
60            }
61        }
62
63        impl From<$name> for $inner_type {
64            fn from(wrapper: $name) -> Self {
65                wrapper.0
66            }
67        }
68
69        impl std::fmt::Display for $name {
70            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
71                self.0.fmt(f)
72            }
73        }
74    };
75}
76
77macro_rules! impl_serde_traits {
78    ($new_type:ty, $inner_type:ty) => {
79        #[cfg(feature = "serde")]
80        impl serde::Serialize for $new_type {
81            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
82                self.0.serialize(serializer)
83            }
84        }
85
86        #[cfg(feature = "serde")]
87        impl<'de> serde::Deserialize<'de> for $new_type {
88            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
89                <$inner_type>::deserialize(deserializer).map(Self)
90            }
91        }
92    };
93}
94
95new_type!(
96    "A wrapper around [`chrono::NaiveDateTime`] that can be converted to and from Python's `datetime.datetime`",
97    NaiveDateTime,
98    chrono::NaiveDateTime
99);
100impl_serde_traits!(NaiveDateTime, chrono::NaiveDateTime);
101
102impl pyo3::ToPyObject for NaiveDateTime {
103    fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
104        let (micros, fold) = chrono_to_micros_and_fold(self.0);
105        pyo3::types::PyDateTime::new_with_fold(
106            py,
107            self.0.year(),
108            self.0.month() as u8,
109            self.0.day() as u8,
110            self.0.hour() as u8,
111            self.0.minute() as u8,
112            self.0.second() as u8,
113            micros,
114            None,
115            fold,
116        )
117        .unwrap()
118        .to_object(py)
119    }
120}
121
122impl pyo3::IntoPy<pyo3::PyObject> for NaiveDateTime {
123    fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
124        pyo3::ToPyObject::to_object(&self, py)
125    }
126}
127
128impl pyo3::FromPyObject<'_> for NaiveDateTime {
129    fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
130        let pydatetime: &pyo3::types::PyDateTime = pyo3::PyTryFrom::try_from(ob)?;
131        Ok(NaiveDateTime(
132            chrono::NaiveDate::from_ymd(
133                pydatetime.get_year(),
134                pydatetime.get_month() as u32,
135                pydatetime.get_day() as u32,
136            )
137            .and_hms_micro(
138                pydatetime.get_hour() as u32,
139                pydatetime.get_minute() as u32,
140                pydatetime.get_second() as u32,
141                py_to_micros(pydatetime),
142            ),
143        ))
144    }
145}
146
147new_type!(
148    "A wrapper around [`chrono::NaiveDate`] that can be converted to and from Python's `datetime.date`",
149    NaiveDate,
150    chrono::NaiveDate
151);
152impl_serde_traits!(NaiveDate, chrono::NaiveDate);
153
154impl pyo3::ToPyObject for NaiveDate {
155    fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
156        pyo3::types::PyDate::new(py, self.0.year(), self.0.month() as u8, self.0.day() as u8)
157            .unwrap()
158            .to_object(py)
159    }
160}
161
162impl pyo3::IntoPy<pyo3::PyObject> for NaiveDate {
163    fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
164        pyo3::ToPyObject::to_object(&self, py)
165    }
166}
167
168impl pyo3::FromPyObject<'_> for NaiveDate {
169    fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
170        let pydate: &pyo3::types::PyDate = pyo3::PyTryFrom::try_from(ob)?;
171        Ok(NaiveDate(chrono::NaiveDate::from_ymd(
172            pydate.get_year(),
173            pydate.get_month() as u32,
174            pydate.get_day() as u32,
175        )))
176    }
177}
178
179new_type!(
180    "A wrapper around [`chrono::NaiveTime`] that can be converted to and from Python's `datetime.time`",
181    NaiveTime,
182    chrono::NaiveTime
183);
184impl_serde_traits!(NaiveTime, chrono::NaiveTime);
185
186impl pyo3::ToPyObject for NaiveTime {
187    fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
188        let (micros, fold) = chrono_to_micros_and_fold(self.0);
189        pyo3::types::PyTime::new_with_fold(
190            py,
191            self.0.hour() as u8,
192            self.0.minute() as u8,
193            self.0.second() as u8,
194            micros,
195            None,
196            fold,
197        )
198        .unwrap()
199        .to_object(py)
200    }
201}
202
203impl pyo3::IntoPy<pyo3::PyObject> for NaiveTime {
204    fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
205        pyo3::ToPyObject::to_object(&self, py)
206    }
207}
208
209impl pyo3::FromPyObject<'_> for NaiveTime {
210    fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
211        let pytime: &pyo3::types::PyTime = pyo3::PyTryFrom::try_from(ob)?;
212        Ok(NaiveTime(chrono::NaiveTime::from_hms_micro(
213            pytime.get_hour() as u32,
214            pytime.get_minute() as u32,
215            pytime.get_second() as u32,
216            py_to_micros(pytime),
217        )))
218    }
219}
220
221new_type!(
222    "A wrapper around [`chrono::Duration`] that can be converted to and from Python's `datetime.timedelta`",
223    Duration,
224    chrono::Duration
225);
226// impl_serde_traits!(Duration, chrono::Duration); // chrono doesn't yet support serde traits for it
227
228impl pyo3::ToPyObject for Duration {
229    fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
230        // There's a lot of clamping involved here because chrono doesn't expose enough
231        // functionality for clean 1:1 conversions
232        let total_days = self.0.num_days();
233
234        let subday_duration = self.0 - chrono::Duration::days(total_days);
235        let total_minutes = subday_duration.num_minutes();
236
237        let subminute_duration = subday_duration - chrono::Duration::minutes(total_minutes);
238        // it's safe to unwrap as it is less then chrono::Duration::minutes(1)
239        let total_micros = subminute_duration.num_microseconds().unwrap();
240
241        // Can safely cast to i32 as the total number of days in chrono::Duration::microseconds(std::i64::MAX) < std::i32::MAX
242        pyo3::types::PyDelta::new(
243            py,
244            total_days as i32,
245            total_minutes as i32 * 60,
246            total_micros as i32,
247            true,
248        )
249        .unwrap()
250        .into()
251    }
252}
253
254impl pyo3::IntoPy<pyo3::PyObject> for Duration {
255    fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
256        pyo3::ToPyObject::to_object(&self, py)
257    }
258}
259
260impl pyo3::FromPyObject<'_> for Duration {
261    fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
262        let pydelta: &pyo3::types::PyDelta = pyo3::PyTryFrom::try_from(ob)?;
263
264        let total_days = pydelta.get_days() as i64;
265        let total_seconds = total_days * 24 * 60 * 60 + pydelta.get_seconds() as i64;
266        let total_microseconds = total_seconds
267            .saturating_mul(1_000_000)
268            .saturating_add(pydelta.get_microseconds() as i64);
269
270        Ok(Duration(chrono::Duration::microseconds(total_microseconds)))
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use pyo3::ToPyObject as _;
278
279    /// Assert that a Python-native object is equal to a converted Rust object
280    fn assert_py_eq(
281        native_python_object: &(impl pyo3::PyNativeType + pyo3::ToPyObject + std::fmt::Display),
282        // foreign_object: &(impl pyo3::ToPyObject + std::fmt::Display),
283        foreign_object: &pyo3::PyObject,
284    ) {
285        // Cast python object to PyAny because PyO3 only implements comparing for PyAny
286        // Then, actually compare against foreign object
287        if native_python_object
288            .to_object(native_python_object.py())
289            .cast_as::<pyo3::PyAny>(native_python_object.py())
290            .unwrap()
291            .compare(foreign_object)
292            .unwrap()
293            != std::cmp::Ordering::Equal
294        {
295            panic!(
296                r#"assertion failed: converted Rust object is not equal to Python reference object
297         Python: `{}`
298 Converted Rust: `{}`"#,
299                native_python_object, foreign_object,
300            );
301        }
302    }
303
304    #[test]
305    fn test_datetime() {
306        let py = pyo3::Python::acquire_gil();
307        let py = py.python();
308
309        for &(year, month, day, hour, min, sec, micro, is_leap) in &[
310            (2021, 1, 20, 22, 39, 46, 186605, false), // time of writing :)
311            (2020, 2, 29, 0, 0, 0, 0, false),         // leap day hehe
312            (2016, 12, 31, 23, 59, 59, 123456, false), // latest leap second
313            (2016, 12, 31, 23, 59, 59, 123456, true), // latest leap second
314            (1156, 3, 31, 11, 22, 33, 445566, false), // long ago
315            (1, 1, 1, 0, 0, 0, 0, false),             // Jan 01, 1 AD - can't go further than this
316            (3000, 6, 5, 4, 3, 2, 1, false),          // the future
317            (9999, 12, 31, 23, 59, 59, 999999, false), // Dec 31, 9999 AD - can't go further than this
318        ] {
319            // Check if date conversion works
320
321            let py_date = pyo3::types::PyDate::new(py, year, month, day).unwrap();
322            let chrono_date =
323                NaiveDate(chrono::NaiveDate::from_ymd(year, month.into(), day.into()));
324
325            assert_eq!(py_date.extract::<NaiveDate>().unwrap(), chrono_date);
326            assert_py_eq(py_date, &chrono_date.to_object(py));
327
328            // Check if time conversion works
329
330            let py_time =
331                pyo3::types::PyTime::new_with_fold(py, hour, min, sec, micro, None, is_leap)
332                    .unwrap();
333            let chrono_time = NaiveTime(chrono::NaiveTime::from_hms_micro(
334                hour.into(),
335                min.into(),
336                sec.into(),
337                micro + if is_leap { 1_000_000 } else { 0 },
338            ));
339
340            assert_eq!(py_time.extract::<NaiveTime>().unwrap(), chrono_time);
341            assert_py_eq(py_time, &chrono_time.to_object(py));
342
343            // Check if datetime conversion works
344
345            let py_datetime = pyo3::types::PyDateTime::new_with_fold(
346                py, year, month, day, hour, min, sec, micro, None, is_leap,
347            )
348            .unwrap();
349            let chrono_datetime =
350                NaiveDateTime(chrono::NaiveDateTime::new(chrono_date.0, chrono_time.0));
351
352            assert_eq!(
353                py_datetime.extract::<NaiveDateTime>().unwrap(),
354                chrono_datetime
355            );
356            assert_py_eq(py_datetime, &chrono_datetime.to_object(py))
357        }
358    }
359
360    #[test]
361    fn test_duration() {
362        let py = pyo3::Python::acquire_gil();
363        let py = py.python();
364
365        for &(days, seconds, micros, total_micros, test_to_python_conversion) in &[
366            (0, 0, 0, 0, true),
367            (0, 0, 1, 1, true),
368            (0, 0, -1, -1, true),
369            (156, 32, 415178, 13478432415178, true),
370            (-10000, 0, 0, -864000000000000, true),
371            (0, 0, 999_999, 999_999, true),
372            (0, 0, 1_000_000, 1_000_000, true),
373            (0, 36 * 60, 0, 36 * 60 * 1_000_000, true),
374            (0, 0, std::i32::MAX, std::i32::MAX as i64, true),
375            (0, 0, std::i32::MIN, std::i32::MIN as i64, true),
376            (
377                // Python's max duration is 1 billion days...
378                999999999,
379                86399,
380                999999,
381                // ...which is not representable in Chrono, hence our library aims to clamp to the
382                // nearest value:
383                std::i64::MAX,
384                // Don't check if Chrono conversion to Python fails - it will definitely fail
385                // because Chrono's truncated duration doesn't match Python's full duration
386                false,
387            ),
388            (
389                // Python's max duration is negative 1 billion days...
390                -999999999,
391                0,
392                0,
393                // ...which is not representable in Chrono, hence our library aims to clamp to the
394                // nearest value when converting from Python to Rust:
395                std::i64::MIN,
396                // Don't check if Chrono conversion to Python fails - it will definitely fail
397                // because Chrono's truncated duration doesn't match Python's full duration
398                false,
399            ),
400        ] {
401            let py_duration = pyo3::types::PyDelta::new(py, days, seconds, micros, true).unwrap();
402            let chrono_duration = Duration(chrono::Duration::microseconds(total_micros));
403
404            assert_eq!(py_duration.extract::<Duration>().unwrap(), chrono_duration);
405            if test_to_python_conversion {
406                assert_py_eq(py_duration, &chrono_duration.to_object(py));
407            }
408        }
409    }
410}