Changed non system clocks to be time deltas

Allowed durations and non system clocks to be set from floats.
This commit is contained in:
Trent Houliston 2016-09-13 20:40:28 +10:00
parent 207d0da31c
commit 2f597687e7
4 changed files with 85 additions and 32 deletions

View File

@ -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).

View File

@ -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<int, std::ratio<86400>> dd_t;
typedef duration<int, std::ratio<1>> ss_t;
typedef duration<int, std::micro> us_t;
using dd_t = duration<int, std::ratio<86400>>;
using ss_t = duration<int, std::ratio<1>>;
using us_t = duration<int, std::micro>;
return PyDelta_FromDSU(
duration_cast<dd_t>(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 <typename Duration> class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>> {
public:
typedef std::chrono::time_point<std::chrono::system_clock, Duration> 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<std::chrono::system_clock, Duration> &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 <typename Clock, typename Duration> class type_caster<std::chrono::time_point<Clock, Duration>> {
public:
typedef std::chrono::time_point<Clock, Duration> type;
typedef std::chrono::duration<std::chrono::hours::rep, std::ratio<86400>> 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<Duration>(
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<PyDateTime_Delta*>(src.ptr());
value = time_point<Clock, Duration>(
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<Clock, Duration>(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<Clock, Duration> &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<int, std::ratio<3600>> hh_t;
typedef duration<int, std::ratio<60>> mm_t;
typedef duration<int, std::ratio<1>> ss_t;
typedef duration<int, std::micro> us_t;
using dd_t = duration<int, std::ratio<86400>>;
using ss_t = duration<int, std::ratio<1>>;
using us_t = duration<int, std::micro>;
Duration d = src.time_since_epoch();
return PyTime_FromTime(duration_cast<hh_t>(d).count()
, duration_cast<mm_t>(d % hours(1)).count()
, duration_cast<ss_t>(d % minutes(1)).count()
return PyDelta_FromDSU(
duration_cast<dd_t>(d).count()
, duration_cast<ss_t>(d % days(1)).count()
, duration_cast<us_t>(d % seconds(1)).count());
}
PYBIND11_TYPE_CASTER(type, _("datetime.time"));
PYBIND11_TYPE_CASTER(type, _("datetime.timedelta"));
};
NAMESPACE_END(detail)

View File

@ -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);
});

View File

@ -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