diff --git a/CMakeLists.txt b/CMakeLists.txt index 281de248..6124cf05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,6 +161,7 @@ if (PYBIND11_INSTALL) set(PYBIND11_HEADERS include/pybind11/attr.h include/pybind11/cast.h + include/pybind11/chrono.h include/pybind11/common.h include/pybind11/complex.h include/pybind11/descr.h diff --git a/docs/advanced.rst b/docs/advanced.rst index 1158f053..ae43d584 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -758,6 +758,29 @@ Please refer to the supplemental example for details. length queries (``__len__``), iterators (``__iter__``), the slicing protocol and other kinds of useful operations. +C++11 chrono datatypes +====================== +When including the additional header file :file:`pybind11/chrono.h` conversions from c++11 chrono datatypes +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. +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. +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. + +Objects of type ``std::chrono::duration`` are converted into datetime.timedelta objects. + +.. note:: + + 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). + Return value policies ===================== diff --git a/docs/basics.rst b/docs/basics.rst index 3e07e0e0..339d5595 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -297,6 +297,10 @@ as arguments and return values, refer to the section on binding :ref:`classes`. +---------------------------------+--------------------------+-------------------------------+ | ``std::function<...>`` | STL polymorphic function | :file:`pybind11/functional.h` | +---------------------------------+--------------------------+-------------------------------+ +| ``std::chrono::duration<...>`` | STL time duration | :file:`pybind11/chrono.h` | ++---------------------------------+--------------------------+-------------------------------+ +| ``std::chrono::time_point<...>``| STL date/time | :file:`pybind11/chrono.h` | ++---------------------------------+--------------------------+-------------------------------+ | ``Eigen::Matrix<...>`` | Eigen: dense matrix | :file:`pybind11/eigen.h` | +---------------------------------+--------------------------+-------------------------------+ | ``Eigen::Map<...>`` | Eigen: mapped memory | :file:`pybind11/eigen.h` | diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4a0c703a..b885298a 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -472,115 +472,6 @@ public: PYBIND11_TYPE_CASTER(bool, _("bool")); }; -template class type_caster> { -public: - typedef std::chrono::duration type; - typedef std::chrono::duration> days; - - bool load(handle src, bool) { - using namespace std::chrono; - PyDateTime_IMPORT; - - if (!src) return false; - if (PyDelta_Check(src.ptr())) { - const PyDateTime_Delta* delta = reinterpret_cast(src.ptr()); - value = duration_cast>( - days(delta->days) - + seconds(delta->seconds) - + microseconds(delta->microseconds)); - return true; - } - else return false; - } - - static handle cast(const std::chrono::duration &src, return_value_policy /* policy */, handle /* parent */) { - using namespace std::chrono; - PyDateTime_IMPORT; - - int dd = duration_cast(src).count(); - int ss = duration_cast(src % days(1)).count(); - int us = duration_cast(src % seconds(1)).count(); - - return PyDelta_FromDSU(dd, ss, us); - } - PYBIND11_TYPE_CASTER(type, _("datetime.timedelta")); -}; - -template class type_caster> { -public: - typedef std::chrono::time_point type; - bool load(handle src, bool) { - using namespace std::chrono; - PyDateTime_IMPORT; - - if (!src) return false; - if (PyDateTime_Check(src.ptr())) { - std::tm cal; - cal.tm_sec = PyDateTime_DATE_GET_SECOND(src.ptr()); - cal.tm_min = PyDateTime_DATE_GET_MINUTE(src.ptr()); - cal.tm_hour = PyDateTime_DATE_GET_HOUR(src.ptr()); - cal.tm_mday = PyDateTime_GET_DAY(src.ptr()); - cal.tm_mon = PyDateTime_GET_MONTH(src.ptr()) - 1; - cal.tm_year = PyDateTime_GET_YEAR(src.ptr()); - cal.tm_isdst = -1; - - value = system_clock::from_time_t(mktime(&cal)); - return true; - } - else return false; - } - - static handle cast(const std::chrono::time_point &src, return_value_policy /* policy */, handle /* parent */) { - using namespace std::chrono; - PyDateTime_IMPORT; - - time_t tt = system_clock::to_time_t(src); - tm *ltime = localtime(&tt); // this function uses static memory so it's best - tm localtime = *ltime; // to copy it out asap just in case - - return PyDateTime_FromDateAndTime(localtime.tm_year, localtime.tm_mon + 1 - , localtime.tm_mday - , localtime.tm_hour - , localtime.tm_min - , localtime.tm_sec - , (duration_cast(src.time_since_epoch()) % seconds(1)).count()); - } - PYBIND11_TYPE_CASTER(type, _("datetime.datetime")); -}; - -template class type_caster> { -public: - typedef std::chrono::time_point type; - bool load(handle src, bool) { - using namespace std::chrono; - 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())) - )); - return true; - } - else return false; - } - - static handle cast(const std::chrono::time_point &src, return_value_policy /* policy */, handle /* parent */) { - using namespace std::chrono; - PyDateTime_IMPORT; - - 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()); - } - PYBIND11_TYPE_CASTER(type, _("datetime.time")); -}; - template <> class type_caster { public: bool load(handle src, bool) { diff --git a/include/pybind11/chrono.h b/include/pybind11/chrono.h new file mode 100644 index 00000000..d82f1430 --- /dev/null +++ b/include/pybind11/chrono.h @@ -0,0 +1,146 @@ +/* + pybind11/chrono.h: Transparent conversion between std::chrono and python's datetime + + Copyright (c) 2016 Trent Houliston and + Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include "pybind11.h" +#include + +NAMESPACE_BEGIN(pybind11) +NAMESPACE_BEGIN(detail) + +template class type_caster> { +public: + typedef std::chrono::duration type; + typedef std::chrono::duration> days; + + bool load(handle src, bool) { + using namespace std::chrono; + PyDateTime_IMPORT; + + if (!src) return false; + 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 = duration_cast>( + days(delta->days) + + seconds(delta->seconds) + + microseconds(delta->microseconds)); + return true; + } + else return false; + } + + static handle cast(const std::chrono::duration &src, return_value_policy /* policy */, handle /* parent */) { + using namespace std::chrono; + 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; + + return PyDelta_FromDSU( + duration_cast(src).count() + , duration_cast(src % days(1)).count() + , duration_cast(src % seconds(1)).count()); + } + PYBIND11_TYPE_CASTER(type, _("datetime.timedelta")); +}; + +template class type_caster> { +public: + typedef std::chrono::time_point type; + bool load(handle src, bool) { + using namespace std::chrono; + PyDateTime_IMPORT; + + if (!src) return false; + if (PyDateTime_Check(src.ptr())) { + std::tm cal; + cal.tm_sec = PyDateTime_DATE_GET_SECOND(src.ptr()); + cal.tm_min = PyDateTime_DATE_GET_MINUTE(src.ptr()); + cal.tm_hour = PyDateTime_DATE_GET_HOUR(src.ptr()); + cal.tm_mday = PyDateTime_GET_DAY(src.ptr()); + cal.tm_mon = PyDateTime_GET_MONTH(src.ptr()) - 1; + cal.tm_year = PyDateTime_GET_YEAR(src.ptr()) - 1900; + cal.tm_isdst = -1; + + value = system_clock::from_time_t(mktime(&cal)) + microseconds(PyDateTime_DATE_GET_MICROSECOND(src.ptr())); + return true; + } + else return false; + } + + static handle cast(const std::chrono::time_point &src, return_value_policy /* policy */, handle /* parent */) { + using namespace std::chrono; + PyDateTime_IMPORT; + + time_t tt = system_clock::to_time_t(src); + // this function uses static memory so it's best to copy it out asap just in case + tm *ltime = localtime(&tt); + tm localtime = *ltime; + + // Declare these special duration types so the conversions happen with the correct primitive types (int) + using us_t = duration; + + return PyDateTime_FromDateAndTime(localtime.tm_year + 1900 + , localtime.tm_mon + 1 + , localtime.tm_mday + , localtime.tm_hour + , localtime.tm_min + , localtime.tm_sec + , (duration_cast(src.time_since_epoch() % seconds(1))).count()); + } + PYBIND11_TYPE_CASTER(type, _("datetime.datetime")); +}; + +template class type_caster> { +public: + typedef std::chrono::time_point type; + bool load(handle src, bool) { + using namespace std::chrono; + 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())) + )); + return true; + } + else return false; + } + + static handle cast(const std::chrono::time_point &src, return_value_policy /* policy */, handle /* parent */) { + using namespace std::chrono; + 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; + + 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()); + } + PYBIND11_TYPE_CASTER(type, _("datetime.time")); +}; + +NAMESPACE_END(detail) +NAMESPACE_END(pybind11) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3be8cf89..6274ca13 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ set(PYBIND11_TEST_FILES test_alias_initialization.cpp test_buffers.cpp test_callbacks.cpp + test_chrono.cpp test_class_args.cpp test_constants_and_functions.cpp test_eigen.cpp diff --git a/tests/test_chrono.cpp b/tests/test_chrono.cpp new file mode 100644 index 00000000..e0719240 --- /dev/null +++ b/tests/test_chrono.cpp @@ -0,0 +1,53 @@ +/* + tests/test_chrono.cpp -- test conversions to/from std::chrono types + + Copyright (c) 2016 Trent Houliston and + Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + + +#include "pybind11_tests.h" +#include "constructor_stats.h" +#include + +// Return the current time off the wall clock +std::chrono::system_clock::time_point test_chrono1() { + return std::chrono::system_clock::now(); +} + +// Round trip the passed in system clock time +std::chrono::system_clock::time_point test_chrono2(std::chrono::system_clock::time_point t) { + return t; +} + +// Round trip the passed in duration +std::chrono::system_clock::duration test_chrono3(std::chrono::system_clock::duration d) { + return d; +} + +// Difference between two passed in time_points +std::chrono::system_clock::duration test_chrono4(std::chrono::system_clock::time_point a, std::chrono::system_clock::time_point b) { + return a - b; +} + +// Return the current time off the steady_clock +std::chrono::steady_clock::time_point test_chrono5() { + return std::chrono::steady_clock::now(); +} + +// Round trip a steady clock timepoint +std::chrono::steady_clock::time_point test_chrono6(std::chrono::steady_clock::time_point t) { + return t; +} + +test_initializer chrono([] (py::module &m) { + m.def("test_chrono1", &test_chrono1); + m.def("test_chrono2", &test_chrono2); + m.def("test_chrono3", &test_chrono3); + m.def("test_chrono4", &test_chrono4); + m.def("test_chrono5", &test_chrono5); + m.def("test_chrono6", &test_chrono6); +}); diff --git a/tests/test_chrono.py b/tests/test_chrono.py new file mode 100644 index 00000000..aa474809 --- /dev/null +++ b/tests/test_chrono.py @@ -0,0 +1,102 @@ + + +def test_chrono_system_clock(): + from pybind11_tests import test_chrono1 + import datetime + + # Get the time from both c++ and datetime + date1 = test_chrono1() + date2 = datetime.datetime.today() + + # The returned value should be a datetime + assert isinstance(date1, datetime.datetime) + + # The numbers should vary by a very small amount (time it took to execute) + diff = abs(date1 - date2) + + # There should never be a days/seconds difference + assert diff.days == 0 + assert diff.seconds == 0 + + # 500 microseconds is a very long time to execute this + assert diff.microseconds < 500 + + +def test_chrono_system_clock_roundtrip(): + from pybind11_tests import test_chrono2 + import datetime + + date1 = datetime.datetime.today() + + # Roundtrip the time + date2 = test_chrono2(date1) + + # The returned value should be a datetime + assert isinstance(date2, datetime.datetime) + + # They should be identical (no information lost on roundtrip) + diff = abs(date1 - date2) + assert diff.days == 0 + assert diff.seconds == 0 + assert diff.microseconds == 0 + + +def test_chrono_duration_roundtrip(): + from pybind11_tests import test_chrono3 + import datetime + + # Get the difference betwen two times (a timedelta) + date1 = datetime.datetime.today() + date2 = datetime.datetime.today() + diff = date2 - date1 + + # Make sure this is a timedelta + assert isinstance(diff, datetime.timedelta) + + cpp_diff = test_chrono3(diff) + + assert cpp_diff.days == diff.days + assert cpp_diff.seconds == diff.seconds + assert cpp_diff.microseconds == diff.microseconds + + +def test_chrono_duration_subtraction_equivalence(): + from pybind11_tests import test_chrono4 + import datetime + + date1 = datetime.datetime.today() + date2 = datetime.datetime.today() + + diff = date2 - date1 + cpp_diff = test_chrono4(date2, date1) + + assert cpp_diff.days == diff.days + assert cpp_diff.seconds == diff.seconds + assert cpp_diff.microseconds == diff.microseconds + + +def test_chrono_steady_clock(): + from pybind11_tests import test_chrono5 + import datetime + + time1 = test_chrono5() + time2 = test_chrono5() + + assert isinstance(time1, datetime.time) + assert isinstance(time2, datetime.time) + + +def test_chrono_steady_clock_roundtrip(): + from pybind11_tests import test_chrono6 + import datetime + + time1 = datetime.time(second=10, microsecond=100) + time2 = test_chrono6(time1) + + assert isinstance(time2, datetime.time) + + # 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