Skip to content

Commit 6a19278

Browse files
Fix bug roundtripping datetime.time objects after midnight in eastern hemisphere timezones (#2417) (#2438)
* Fix bug roundtripping datetime.time objects after midnight in eastern hemisphere timezones (#2417) * tests: check more timezones * Fix review remarks: remove useless comment and skip setting TZ environment variable on Windows
1 parent 1abc4a9 commit 6a19278

File tree

3 files changed

+40
-7
lines changed

3 files changed

+40
-7
lines changed

include/pybind11/chrono.h

+12-5
Original file line numberDiff line numberDiff line change
@@ -150,21 +150,28 @@ template <typename Duration> class type_caster<std::chrono::time_point<std::chro
150150
// Lazy initialise the PyDateTime import
151151
if (!PyDateTimeAPI) { PyDateTime_IMPORT; }
152152

153-
std::time_t tt = system_clock::to_time_t(time_point_cast<system_clock::duration>(src));
153+
// Get out microseconds, and make sure they are positive, to avoid bug in eastern hemisphere time zones
154+
// (cfr. https://github.com/pybind/pybind11/issues/2417)
155+
using us_t = duration<int, std::micro>;
156+
auto us = duration_cast<us_t>(src.time_since_epoch() % seconds(1));
157+
if (us.count() < 0)
158+
us += seconds(1);
159+
160+
// Subtract microseconds BEFORE `system_clock::to_time_t`, because:
161+
// > If std::time_t has lower precision, it is implementation-defined whether the value is rounded or truncated.
162+
// (https://en.cppreference.com/w/cpp/chrono/system_clock/to_time_t)
163+
std::time_t tt = system_clock::to_time_t(time_point_cast<system_clock::duration>(src - us));
154164
// this function uses static memory so it's best to copy it out asap just in case
155165
// otherwise other code that is using localtime may break this (not just python code)
156166
std::tm localtime = *std::localtime(&tt);
157167

158-
// Declare these special duration types so the conversions happen with the correct primitive types (int)
159-
using us_t = duration<int, std::micro>;
160-
161168
return PyDateTime_FromDateAndTime(localtime.tm_year + 1900,
162169
localtime.tm_mon + 1,
163170
localtime.tm_mday,
164171
localtime.tm_hour,
165172
localtime.tm_min,
166173
localtime.tm_sec,
167-
(duration_cast<us_t>(src.time_since_epoch() % seconds(1))).count());
174+
us.count());
168175
}
169176
PYBIND11_TYPE_CASTER(type, _("datetime.datetime"));
170177
};

tests/test_chrono.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include "pybind11_tests.h"
1212
#include <pybind11/chrono.h>
13+
#include <chrono>
1314

1415
TEST_SUBMODULE(chrono, m) {
1516
using system_time = std::chrono::system_clock::time_point;

tests/test_chrono.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# -*- coding: utf-8 -*-
22
from pybind11_tests import chrono as m
33
import datetime
4+
import pytest
5+
6+
import env # noqa: F401
47

58

69
def test_chrono_system_clock():
@@ -70,8 +73,30 @@ def test_chrono_system_clock_roundtrip_date():
7073
assert time2.microsecond == 0
7174

7275

73-
def test_chrono_system_clock_roundtrip_time():
74-
time1 = datetime.datetime.today().time()
76+
SKIP_TZ_ENV_ON_WIN = pytest.mark.skipif(
77+
"env.WIN", reason="TZ environment variable only supported on POSIX"
78+
)
79+
80+
81+
@pytest.mark.parametrize("time1", [
82+
datetime.datetime.today().time(),
83+
datetime.time(0, 0, 0),
84+
datetime.time(0, 0, 0, 1),
85+
datetime.time(0, 28, 45, 109827),
86+
datetime.time(0, 59, 59, 999999),
87+
datetime.time(1, 0, 0),
88+
datetime.time(5, 59, 59, 0),
89+
datetime.time(5, 59, 59, 1),
90+
])
91+
@pytest.mark.parametrize("tz", [
92+
None,
93+
pytest.param("Europe/Brussels", marks=SKIP_TZ_ENV_ON_WIN),
94+
pytest.param("Asia/Pyongyang", marks=SKIP_TZ_ENV_ON_WIN),
95+
pytest.param("America/New_York", marks=SKIP_TZ_ENV_ON_WIN),
96+
])
97+
def test_chrono_system_clock_roundtrip_time(time1, tz, monkeypatch):
98+
if tz is not None:
99+
monkeypatch.setenv("TZ", "/usr/share/zoneinfo/{}".format(tz))
75100

76101
# Roundtrip the time
77102
datetime2 = m.test_chrono2(time1)

0 commit comments

Comments
 (0)