#![warn(missing_docs)]
pub use chrono;
pub use pyo3;
use chrono::{Datelike as _, Timelike as _};
use pyo3::types::{PyDateAccess as _, PyDeltaAccess as _, PyTimeAccess as _};
use std::convert::TryInto as _;
fn chrono_to_micros_and_fold(time: impl chrono::Timelike) -> (u32, bool) {
if let Some(folded_nanos) = time.nanosecond().checked_sub(1_000_000_000) {
(folded_nanos / 1000, true)
} else {
(time.nanosecond() / 1000, false)
}
}
fn py_to_micros(time: &impl pyo3::types::PyTimeAccess) -> u32 {
if time.get_fold() > 0 {
time.get_microsecond() + 1_000_000
} else {
time.get_microsecond()
}
}
trait PyDateTimeWithFoldExtensionTrait {
#[allow(clippy::too_many_arguments)]
fn new_with_fold<'p>(
py: pyo3::Python<'p>,
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
microsecond: u32,
tzinfo: Option<&pyo3::PyObject>,
fold: bool,
) -> pyo3::PyResult<&'p pyo3::types::PyDateTime>;
}
impl PyDateTimeWithFoldExtensionTrait for pyo3::types::PyDateTime {
fn new_with_fold<'p>(
py: pyo3::Python<'p>,
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
microsecond: u32,
tzinfo: Option<&pyo3::PyObject>,
fold: bool,
) -> pyo3::PyResult<&'p pyo3::types::PyDateTime> {
use pyo3::ffi::PyDateTimeAPI;
use std::os::raw::c_int;
unsafe fn opt_to_pyobj(
py: pyo3::Python,
opt: Option<&pyo3::PyObject>,
) -> *mut pyo3::ffi::PyObject {
use pyo3::AsPyPointer as _;
match opt {
Some(tzi) => tzi.as_ptr(),
None => py.None().as_ptr(),
}
}
unsafe {
let ptr = (PyDateTimeAPI.DateTime_FromDateAndTimeAndFold)(
year,
c_int::from(month),
c_int::from(day),
c_int::from(hour),
c_int::from(minute),
c_int::from(second),
microsecond as c_int,
opt_to_pyobj(py, tzinfo),
c_int::from(fold),
PyDateTimeAPI.DateTimeType,
);
py.from_owned_ptr_or_err(ptr)
}
}
}
macro_rules! new_type {
($doc:literal, $name:ident, $inner_type:ty) => {
#[doc = $doc]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct $name(pub $inner_type);
impl From<$inner_type> for $name {
fn from(inner: $inner_type) -> Self {
Self(inner)
}
}
impl From<$name> for $inner_type {
fn from(wrapper: $name) -> Self {
wrapper.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
};
}
new_type!(
"A wrapper around [`chrono::NaiveDateTime`] that can be converted to and from Python's `datetime.datetime`",
NaiveDateTime,
chrono::NaiveDateTime
);
impl pyo3::ToPyObject for NaiveDateTime {
fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
let (micros, fold) = chrono_to_micros_and_fold(self.0);
pyo3::types::PyDateTime::new_with_fold(
py,
self.0.year(),
self.0.month() as u8,
self.0.day() as u8,
self.0.hour() as u8,
self.0.minute() as u8,
self.0.second() as u8,
micros,
None,
fold,
)
.unwrap()
.to_object(py)
}
}
impl pyo3::IntoPy<pyo3::PyObject> for NaiveDateTime {
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
pyo3::ToPyObject::to_object(&self, py)
}
}
impl pyo3::FromPyObject<'_> for NaiveDateTime {
fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
let pydatetime: &pyo3::types::PyDateTime = pyo3::PyTryFrom::try_from(ob)?;
Ok(NaiveDateTime(
chrono::NaiveDate::from_ymd(
pydatetime.get_year(),
pydatetime.get_month() as u32,
pydatetime.get_day() as u32,
)
.and_hms_micro(
pydatetime.get_hour() as u32,
pydatetime.get_minute() as u32,
pydatetime.get_second() as u32,
py_to_micros(pydatetime),
),
))
}
}
new_type!(
"A wrapper around [`chrono::NaiveDate`] that can be converted to and from Python's `datetime.date`",
NaiveDate,
chrono::NaiveDate
);
impl pyo3::ToPyObject for NaiveDate {
fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
pyo3::types::PyDate::new(py, self.0.year(), self.0.month() as u8, self.0.day() as u8)
.unwrap()
.to_object(py)
}
}
impl pyo3::IntoPy<pyo3::PyObject> for NaiveDate {
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
pyo3::ToPyObject::to_object(&self, py)
}
}
impl pyo3::FromPyObject<'_> for NaiveDate {
fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
let pydate: &pyo3::types::PyDate = pyo3::PyTryFrom::try_from(ob)?;
Ok(NaiveDate(chrono::NaiveDate::from_ymd(
pydate.get_year(),
pydate.get_month() as u32,
pydate.get_day() as u32,
)))
}
}
new_type!(
"A wrapper around [`chrono::NaiveTime`] that can be converted to and from Python's `datetime.time`",
NaiveTime,
chrono::NaiveTime
);
impl pyo3::ToPyObject for NaiveTime {
fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
let (micros, fold) = chrono_to_micros_and_fold(self.0);
pyo3::types::PyTime::new_with_fold(
py,
self.0.hour() as u8,
self.0.minute() as u8,
self.0.second() as u8,
micros,
None,
fold,
)
.unwrap()
.to_object(py)
}
}
impl pyo3::IntoPy<pyo3::PyObject> for NaiveTime {
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
pyo3::ToPyObject::to_object(&self, py)
}
}
impl pyo3::FromPyObject<'_> for NaiveTime {
fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
let pytime: &pyo3::types::PyTime = pyo3::PyTryFrom::try_from(ob)?;
Ok(NaiveTime(chrono::NaiveTime::from_hms_micro(
pytime.get_hour() as u32,
pytime.get_minute() as u32,
pytime.get_second() as u32,
py_to_micros(pytime),
)))
}
}
new_type!(
"A wrapper around [`chrono::Duration`] that can be converted to and from Python's `datetime.timedelta`",
Duration,
chrono::Duration
);
impl pyo3::ToPyObject for Duration {
fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
const MICROSECONDS_PER_DAY: i64 = 60 * 60 * 24 * 1_000_000;
let total_micros = self.0.num_microseconds().unwrap_or(i64::MAX);
let total_days = (total_micros / MICROSECONDS_PER_DAY)
.try_into()
.unwrap_or(i32::MAX);
let subday_micros = (total_micros % MICROSECONDS_PER_DAY) as i32;
pyo3::types::PyDelta::new(py, total_days, 0, subday_micros, true)
.unwrap()
.into()
}
}
impl pyo3::IntoPy<pyo3::PyObject> for Duration {
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
pyo3::ToPyObject::to_object(&self, py)
}
}
impl pyo3::FromPyObject<'_> for Duration {
fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult<Self> {
let pydelta: &pyo3::types::PyDelta = pyo3::PyTryFrom::try_from(ob)?;
let total_days = pydelta.get_days() as i64;
let total_seconds = total_days * 24 * 60 * 60 + pydelta.get_seconds() as i64;
let total_microseconds = total_seconds
.saturating_mul(1_000_000)
.saturating_add(pydelta.get_microseconds() as i64);
Ok(Duration(chrono::Duration::microseconds(total_microseconds)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pyo3::ToPyObject as _;
fn assert_py_eq(
native_python_object: &(impl pyo3::PyNativeType + pyo3::ToPyObject + std::fmt::Display),
foreign_object: &pyo3::PyObject,
) {
if native_python_object
.to_object(native_python_object.py())
.cast_as::<pyo3::PyAny>(native_python_object.py())
.unwrap()
.compare(foreign_object)
.unwrap()
!= std::cmp::Ordering::Equal
{
panic!(
r#"assertion failed: converted Rust object is not equal to Python reference object
Python: `{}`
Converted Rust: `{}`"#,
native_python_object, foreign_object,
);
}
}
#[test]
fn test_datetime() {
let py = pyo3::Python::acquire_gil();
let py = py.python();
for &(year, month, day, hour, min, sec, micro, is_leap) in &[
(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),
] {
let py_date = pyo3::types::PyDate::new(py, year, month, day).unwrap();
let chrono_date =
NaiveDate(chrono::NaiveDate::from_ymd(year, month.into(), day.into()));
assert_eq!(py_date.extract::<NaiveDate>().unwrap(), chrono_date);
assert_py_eq(py_date, &chrono_date.to_object(py));
let py_time =
pyo3::types::PyTime::new_with_fold(py, hour, min, sec, micro, None, is_leap)
.unwrap();
let chrono_time = NaiveTime(chrono::NaiveTime::from_hms_micro(
hour.into(),
min.into(),
sec.into(),
micro + if is_leap { 1_000_000 } else { 0 },
));
assert_eq!(py_time.extract::<NaiveTime>().unwrap(), chrono_time);
assert_py_eq(py_time, &chrono_time.to_object(py));
let py_datetime = pyo3::types::PyDateTime::new_with_fold(
py, year, month, day, hour, min, sec, micro, None, is_leap,
)
.unwrap();
let chrono_datetime =
NaiveDateTime(chrono::NaiveDateTime::new(chrono_date.0, chrono_time.0));
assert_eq!(
py_datetime.extract::<NaiveDateTime>().unwrap(),
chrono_datetime
);
assert_py_eq(py_datetime, &chrono_datetime.to_object(py))
}
}
#[test]
fn test_duration() {
let py = pyo3::Python::acquire_gil();
let py = py.python();
for &(days, seconds, micros, total_micros, test_to_python_conversion) in &[
(0, 0, 0, 0, true),
(0, 0, 1, 1, true),
(0, 0, -1, -1, true),
(156, 32, 415178, 13478432415178, true),
(-10000, 0, 0, -864000000000000, true),
(0, 0, 999_999, 999_999, true),
(0, 0, 1_000_000, 1_000_000, true),
(0, 0, i32::MAX, i32::MAX as i64, true),
(0, 0, i32::MIN, i32::MIN as i64, true),
(
999999999,
86399,
999999,
i64::MAX,
false,
),
(
-999999999,
0,
0,
i64::MIN,
false,
),
] {
let py_duration = pyo3::types::PyDelta::new(py, days, seconds, micros, true).unwrap();
let chrono_duration = Duration(chrono::Duration::microseconds(total_micros));
assert_eq!(py_duration.extract::<Duration>().unwrap(), chrono_duration);
if test_to_python_conversion {
assert_py_eq(py_duration, &chrono_duration.to_object(py));
}
}
}
}