Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/advanced/cast/chrono.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ Provided conversions
- ``std::chrono::system_clock::time_point`` → ``datetime.datetime``
System clock times are converted to python datetime instances. They are
in the local timezone, but do not have any timezone information attached
to them (they are naive datetime objects).
to them (they are naive datetime objects). The ``fold`` attribute
distinguishes the two occurrences of an ambiguous local time when
clocks move backward.

- ``std::chrono::duration`` → ``datetime.timedelta``
Durations are converted to timedeltas, any precision in the duration
Expand All @@ -62,7 +64,10 @@ Provided conversions
- ``datetime.datetime`` or ``datetime.date`` or ``datetime.time`` → ``std::chrono::system_clock::time_point``
Date/time objects are converted into system clock timepoints. Any
timezone information is ignored and the type is treated as a naive
object.
object in the local timezone. The ``fold`` attribute selects the earlier
(``0``) or later (``1``) occurrence of an ambiguous local time.
Nonexistent local times when clocks move forward follow Python's
``datetime.timestamp()`` behavior and cannot round-trip unchanged.

- ``datetime.timedelta`` → ``std::chrono::duration``
Time delta are converted into durations with microsecond precision.
Expand Down
77 changes: 47 additions & 30 deletions include/pybind11/chrono.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#include <cmath>
#include <ctime>
#include <datetime.h>
#include <mutex>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_NAMESPACE_BEGIN(detail)
Expand Down Expand Up @@ -101,22 +100,6 @@ class duration_caster {
PYBIND11_TYPE_CASTER(type, const_name("datetime.timedelta"));
};

inline std::tm *localtime_thread_safe(const std::time_t *time, std::tm *buf) {
#if (defined(__STDC_LIB_EXT1__) && defined(__STDC_WANT_LIB_EXT1__)) || defined(_MSC_VER)
if (localtime_s(buf, time))
return nullptr;
return buf;
#else
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
std::tm *tm_ptr = std::localtime(time);
if (tm_ptr != nullptr) {
*buf = *tm_ptr;
}
return tm_ptr;
#endif
}

// 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>> {
Expand All @@ -134,8 +117,9 @@ class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>>
return false;
}

std::tm cal;
std::tm cal{};
microseconds msecs;
int fold = 0;

if (PyDateTime_Check(src.ptr())) {
cal.tm_sec = PyDateTime_DATE_GET_SECOND(src.ptr());
Expand All @@ -146,6 +130,7 @@ class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>>
cal.tm_year = PyDateTime_GET_YEAR(src.ptr()) - 1900;
cal.tm_isdst = -1;
msecs = microseconds(PyDateTime_DATE_GET_MICROSECOND(src.ptr()));
fold = PyDateTime_DATE_GET_FOLD(src.ptr());
} else if (PyDate_Check(src.ptr())) {
cal.tm_sec = 0;
cal.tm_min = 0;
Expand All @@ -164,11 +149,41 @@ class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>>
cal.tm_year = 70; // earliest available date for Python's datetime
cal.tm_isdst = -1;
msecs = microseconds(PyDateTime_TIME_GET_MICROSECOND(src.ptr()));
fold = PyDateTime_TIME_GET_FOLD(src.ptr());
} else {
return false;
}

value = time_point_cast<Duration>(system_clock::from_time_t(std::mktime(&cal)) + msecs);
#if defined(_WIN32)
// Python's fold detection probes up to a day before the timestamp. Windows
// cannot represent those probes during the first day after the epoch, and
// datetime.fromtimestamp() also skips fold detection there.
if (cal.tm_year == 70 && cal.tm_mon == 0 && cal.tm_mday <= 2) {
std::tm cal_copy = cal;
const std::time_t tt = std::mktime(&cal_copy);
if (tt >= 0 && tt <= 86400) {
value = time_point_cast<Duration>(system_clock::from_time_t(tt) + msecs);
return true;
}
}
#endif

// Use Python's local-time disambiguation, preserving fold while ignoring
// tzinfo and datetime subclass overrides. Keep microseconds separate to
// avoid losing precision through timestamp()'s floating-point result.
auto dt = reinterpret_steal<object>(PyDateTime_FromDateAndTimeAndFold(cal.tm_year + 1900,
cal.tm_mon + 1,
cal.tm_mday,
cal.tm_hour,
cal.tm_min,
cal.tm_sec,
0,
fold));
if (!dt) {
throw error_already_set();
}
const auto tt = int_(dt.attr("timestamp")()).cast<std::time_t>();
value = time_point_cast<Duration>(system_clock::from_time_t(tt) + msecs);
return true;
}

Expand Down Expand Up @@ -196,18 +211,20 @@ class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>>
std::time_t tt
= system_clock::to_time_t(time_point_cast<system_clock::duration>(src - us));

std::tm localtime;
std::tm *localtime_ptr = localtime_thread_safe(&tt, &localtime);
if (!localtime_ptr) {
throw cast_error("Unable to represent system_clock in local time");
// Python determines the local calendar fields and fold from whole seconds.
auto args = make_tuple(tt);
auto dt = reinterpret_steal<object>(PyDateTime_FromTimestamp(args.ptr()));
if (!dt) {
throw error_already_set();
}
return PyDateTime_FromDateAndTime(localtime.tm_year + 1900,
localtime.tm_mon + 1,
localtime.tm_mday,
localtime.tm_hour,
localtime.tm_min,
localtime.tm_sec,
us.count());
return PyDateTime_FromDateAndTimeAndFold(PyDateTime_GET_YEAR(dt.ptr()),
PyDateTime_GET_MONTH(dt.ptr()),
PyDateTime_GET_DAY(dt.ptr()),
PyDateTime_DATE_GET_HOUR(dt.ptr()),
PyDateTime_DATE_GET_MINUTE(dt.ptr()),
PyDateTime_DATE_GET_SECOND(dt.ptr()),
us.count(),
PyDateTime_DATE_GET_FOLD(dt.ptr()));
}
PYBIND11_TYPE_CASTER(type, const_name("datetime.datetime"));
};
Expand Down
9 changes: 9 additions & 0 deletions tests/test_chrono.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ TEST_SUBMODULE(chrono, m) {
// Round trip the passed in system clock time
m.def("test_chrono2", [](system_time t) { return t; });

// Check each direction independently, without an ambiguous local datetime
// hiding which instant the system-clock caster selected.
using timestamp_us
= std::chrono::time_point<std::chrono::system_clock, std::chrono::microseconds>;
m.def("test_chrono_system_clock_as_us",
[](timestamp_us t) { return t.time_since_epoch().count(); });
m.def("test_chrono_system_clock_from_us",
[](int64_t us) { return timestamp_us(std::chrono::microseconds(us)); });

// test_chrono_duration_roundtrip
// Round trip the passed in duration
m.def("test_chrono3", [](std::chrono::system_clock::duration d) { return d; });
Expand Down
Loading
Loading