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
13 changes: 12 additions & 1 deletion src/pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,19 @@ def set(
if tz is None:
tz = self.tz

fold = self.fold
if (
self.tz is not None
and self.replace(fold=0).utcoffset() == self.replace(fold=1).utcoffset()
):
# The current time is not ambiguous, so its fold attribute
# carries no meaning and must not decide how the new time
# is resolved if it falls in a DST gap or overlap.
# Use the default resolution of create() instead.
fold = 1

return self.__class__.create(
year, month, day, hour, minute, second, microsecond, tz=tz, fold=self.fold
year, month, day, hour, minute, second, microsecond, tz=tz, fold=fold
)

@property
Expand Down
11 changes: 10 additions & 1 deletion src/pendulum/tz/timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,16 @@ def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
# Repeated time
raise AmbiguousTime(dt)

return dt.replace(tzinfo=self)
dt = dt.replace(tzinfo=self)

if self.utcoffset(dt.replace(fold=0)) == self.utcoffset(dt.replace(fold=1)):
# The local time is not ambiguous, so the fold attribute
# carries no meaning for it and must be set to 0 (PEP 495).
# Without this, times created with the default fold=1,
# e.g. via DateTime.create(), keep a bogus fold value.
dt = dt.replace(fold=0)

return dt

return cast("_DT", dt.astimezone(self))

Expand Down
9 changes: 9 additions & 0 deletions tests/datetime/test_fluent_setters.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ def test_fluid_at_with_transition():
assert new.second == 0


def test_set_keeps_fold_of_ambiguous_time():
d = pendulum.datetime(2023, 11, 5, 1, 30, tz="America/New_York", fold=0)
new = d.set(minute=45)

assert isinstance(new, datetime)
assert new.fold == 0
assert new.offset == -4 * 3600


def test_replace_tzinfo_dst_off():
d = pendulum.datetime(2016, 3, 27, 0, 30) # 30 min before DST turning on
new = d.replace(tzinfo=pendulum.timezone("Europe/Paris"))
Expand Down
29 changes: 29 additions & 0 deletions tests/tz/test_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,35 @@ def test_convert_fold_attribute_is_honored():
assert new.strftime("%z") == "-0500"


def test_convert_unambiguous_time_has_fold_zero():
tz = pendulum.timezone("CET")
dt = datetime(2024, 5, 1)

new = tz.convert(dt)
assert new.fold == 0

new = tz.convert(dt.replace(fold=1))
assert new.fold == 0


def test_datetime_unambiguous_time_has_fold_zero():
dt = pendulum.datetime(2024, 5, 1, tz="CET")

assert dt.fold == 0
assert dt.add(minutes=1).fold == 0
assert dt.add(days=1).fold == 0


def test_datetime_ambiguous_time_keeps_fold():
dt = pendulum.datetime(2023, 11, 5, 1, 30, tz="America/New_York")

assert dt.fold == 1

dt = pendulum.datetime(2023, 11, 5, 1, 30, tz="America/New_York", fold=0)

assert dt.fold == 0


def test_utcoffset_fold_attribute_is_honored():
tz = pendulum.timezone("US/Eastern")
dt = datetime(2014, 11, 2, 1, 30)
Expand Down