1#![warn(missing_docs)]
2
3pub 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);
226impl pyo3::ToPyObject for Duration {
229 fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
230 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 let total_micros = subminute_duration.num_microseconds().unwrap();
240
241 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 fn assert_py_eq(
281 native_python_object: &(impl pyo3::PyNativeType + pyo3::ToPyObject + std::fmt::Display),
282 foreign_object: &pyo3::PyObject,
284 ) {
285 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), (2020, 2, 29, 0, 0, 0, 0, false), (2016, 12, 31, 23, 59, 59, 123456, false), (2016, 12, 31, 23, 59, 59, 123456, true), (1156, 3, 31, 11, 22, 33, 445566, false), (1, 1, 1, 0, 0, 0, 0, false), (3000, 6, 5, 4, 3, 2, 1, false), (9999, 12, 31, 23, 59, 59, 999999, false), ] {
319 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 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 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 999999999,
379 86399,
380 999999,
381 std::i64::MAX,
384 false,
387 ),
388 (
389 -999999999,
391 0,
392 0,
393 std::i64::MIN,
396 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}