diff --git a/docs/advanced.rst b/docs/advanced.rst index ae43d5845..abb9ec0e3 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -764,19 +764,25 @@ When including the additional header file :file:`pybind11/chrono.h` conversions to corresponding python datetime objects are automatically enabled. The following rules describe how the conversions are applied. -Objects of type ``std::chrono::system_clock::time_point`` are converted into datetime.datetime objects. +When passed to python objects of type ``std::chrono::system_clock::time_point`` are converted into datetime.datetime objects. These objects are those that specifically come from the system_clock as this is the only clock that measures wall time. -Objects of type ``std::chrono::[other_clock]::time_point`` are converted into datetime.time objects. +When passed to python of type ``std::chrono::[other_clock]::time_point`` are converted into datetime.timedelta objects. These objects are those that come from all clocks that are not the system_clock (e.g. steady_clock). Clocks other than the system_clock are not measured from wall date/time and instead have any start time (often when the computer was turned on). -Therefore as these clocks can only measure time from an arbitrary start point they are represented as time without date. +Therefore as these clocks can only measure time from an arbitrary start point they are represented as timedelta from this start point. -Objects of type ``std::chrono::duration`` are converted into datetime.timedelta objects. +When passed to python of type ``std::chrono::duration`` are converted into datetime.timedelta objects. + +When python objects are passed to c++ for the case of non system clocks and durations instances of both datetime.timedelta +and float are accepted. The float arguments are interpreted as a number of seconds since the epoch. .. note:: + Other clocks may be the same as system_clock. For example on many platforms std::high_resolution_clock is the same as system_clock. + Because of this if you are converting a timepoint from one of these clocks they may appear to python as a datetime.datetime object. + Pythons datetime implementation is limited to microsecond precision. The extra precision that c++11 clocks can have have (nanoseconds) will be lost upon conversion. The rounding policy from c++ to python is via ``std::chrono::duration_cast<>`` (rounding towards 0 in microseconds). diff --git a/include/pybind11/chrono.h b/include/pybind11/chrono.h index 4eaed2a9d..58ecb06d3 100644 --- a/include/pybind11/chrono.h +++ b/include/pybind11/chrono.h @@ -24,9 +24,12 @@ public: bool load(handle src, bool) { using namespace std::chrono; + + // Lazy initialise the PyDateTime import if(!PyDateTimeAPI) { PyDateTime_IMPORT; } if (!src) return false; + // If they have passed us a datetime.delta object if (PyDelta_Check(src.ptr())) { // The accessor macros for timedelta exist in some versions of python but not others (e.g. Mac OSX default python) // Therefore we are just doing what the macros do explicitly @@ -37,6 +40,13 @@ public: + microseconds(delta->microseconds)); return true; } + // If they have passed us a float we can assume it is seconds and convert + else if (PyFloat_Check(src.ptr())) { + double val = PyFloat_AsDouble(src.ptr()); + // Multiply by the reciprocal of the ratio and round + value = type(std::lround(val * type::period::den / type::period::num)); + return true; + } else return false; } @@ -45,9 +55,9 @@ public: if(!PyDateTimeAPI) { PyDateTime_IMPORT; } // Declare these special duration types so the conversions happen with the correct primitive types (int) - typedef duration> dd_t; - typedef duration> ss_t; - typedef duration us_t; + using dd_t = duration>; + using ss_t = duration>; + using us_t = duration; return PyDelta_FromDSU( duration_cast(src).count() @@ -57,11 +67,14 @@ public: PYBIND11_TYPE_CASTER(type, _("datetime.timedelta")); }; +// This is for casting times on the system clock into datetime.datetime instances template class type_caster> { public: typedef std::chrono::time_point type; bool load(handle src, bool) { using namespace std::chrono; + + // Lazy initialise the PyDateTime import if(!PyDateTimeAPI) { PyDateTime_IMPORT; } if (!src) return false; @@ -83,6 +96,8 @@ public: static handle cast(const std::chrono::time_point &src, return_value_policy /* policy */, handle /* parent */) { using namespace std::chrono; + + // Lazy initialise the PyDateTime import if(!PyDateTimeAPI) { PyDateTime_IMPORT; } time_t tt = system_clock::to_time_t(src); @@ -104,21 +119,33 @@ public: PYBIND11_TYPE_CASTER(type, _("datetime.datetime")); }; +// Other clocks that are not the system clock are not measured as datetime.datetime objects +// since they are not measured on calendar time. So instead we just make them timedeltas +// Or if they have passed us a time as a float we convert that template class type_caster> { public: typedef std::chrono::time_point type; + typedef std::chrono::duration> days; + bool load(handle src, bool) { using namespace std::chrono; if(!PyDateTimeAPI) { PyDateTime_IMPORT; } - if (!src) return false; - if (PyTime_Check(src.ptr())) { - value = type(duration_cast( - hours(PyDateTime_TIME_GET_HOUR(src.ptr())) - + minutes(PyDateTime_TIME_GET_MINUTE(src.ptr())) - + seconds(PyDateTime_TIME_GET_SECOND(src.ptr())) - + microseconds(PyDateTime_TIME_GET_MICROSECOND(src.ptr())) - )); + // If they have passed us a datetime.delta object + if (PyDelta_Check(src.ptr())) { + // The accessor macros for timedelta exist in some versions of python but not others (e.g. Mac OSX default python) + // Therefore we are just doing what the macros do explicitly + const PyDateTime_Delta* delta = reinterpret_cast(src.ptr()); + value = time_point( + days(delta->days) + + seconds(delta->seconds) + + microseconds(delta->microseconds)); + return true; + } + // If they have passed us a float we can assume it is seconds and convert + else if (PyFloat_Check(src.ptr())) { + double val = PyFloat_AsDouble(src.ptr()); + value = time_point(Duration(std::lround((val / Clock::period::num) * Clock::period::den))); return true; } else return false; @@ -126,21 +153,23 @@ public: static handle cast(const std::chrono::time_point &src, return_value_policy /* policy */, handle /* parent */) { using namespace std::chrono; + + // Lazy initialise the PyDateTime import if(!PyDateTimeAPI) { PyDateTime_IMPORT; } // Declare these special duration types so the conversions happen with the correct primitive types (int) - typedef duration> hh_t; - typedef duration> mm_t; - typedef duration> ss_t; - typedef duration us_t; + using dd_t = duration>; + using ss_t = duration>; + using us_t = duration; Duration d = src.time_since_epoch(); - return PyTime_FromTime(duration_cast(d).count() - , duration_cast(d % hours(1)).count() - , duration_cast(d % minutes(1)).count() - , duration_cast(d % seconds(1)).count()); + + return PyDelta_FromDSU( + duration_cast(d).count() + , duration_cast(d % days(1)).count() + , duration_cast(d % seconds(1)).count()); } - PYBIND11_TYPE_CASTER(type, _("datetime.time")); + PYBIND11_TYPE_CASTER(type, _("datetime.timedelta")); }; NAMESPACE_END(detail) diff --git a/tests/test_chrono.cpp b/tests/test_chrono.cpp index e07192404..b86f57adf 100644 --- a/tests/test_chrono.cpp +++ b/tests/test_chrono.cpp @@ -43,6 +43,11 @@ std::chrono::steady_clock::time_point test_chrono6(std::chrono::steady_clock::ti return t; } +// Roundtrip a duration in microseconds from a float argument +std::chrono::microseconds test_chrono7(std::chrono::microseconds t) { + return t; +} + test_initializer chrono([] (py::module &m) { m.def("test_chrono1", &test_chrono1); m.def("test_chrono2", &test_chrono2); @@ -50,4 +55,5 @@ test_initializer chrono([] (py::module &m) { m.def("test_chrono4", &test_chrono4); m.def("test_chrono5", &test_chrono5); m.def("test_chrono6", &test_chrono6); + m.def("test_chrono7", &test_chrono7); }); diff --git a/tests/test_chrono.py b/tests/test_chrono.py index 1f112131d..0253550d9 100644 --- a/tests/test_chrono.py +++ b/tests/test_chrono.py @@ -82,21 +82,33 @@ def test_chrono_steady_clock(): time1 = test_chrono5() time2 = test_chrono5() - assert isinstance(time1, datetime.time) - assert isinstance(time2, datetime.time) + assert isinstance(time1, datetime.timedelta) + assert isinstance(time2, datetime.timedelta) def test_chrono_steady_clock_roundtrip(): from pybind11_tests import test_chrono6 import datetime - time1 = datetime.time(second=10, microsecond=100) + time1 = datetime.timedelta(days=10, seconds=10, microseconds=100) time2 = test_chrono6(time1) - assert isinstance(time2, datetime.time) + assert isinstance(time2, datetime.timedelta) # They should be identical (no information lost on roundtrip) - assert time1.hour == time2.hour - assert time1.minute == time2.minute - assert time1.second == time2.second - assert time1.microsecond == time2.microsecond + assert time1.days == time2.days + assert time1.seconds == time2.seconds + assert time1.microseconds == time2.microseconds + + +def test_floating_point_duration(): + from pybind11_tests import test_chrono7 + import datetime + + # Test using 35.525123 seconds as an example floating point number in seconds + time = test_chrono7(35.525123) + + assert isinstance(time, datetime.timedelta) + + assert time.seconds == 35 + assert time.microseconds == 525123