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
24 changes: 22 additions & 2 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,27 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr
tzinfo = timezone.utc
else:
tzinfo = dt.tzinfo

fold = getattr(dt, "fold", 0)
else:
# When replacing tzinfo, infer the correct fold by comparing
# the original UTC offset to what fold=1 produces in the new tz.
# This correctly handles ambiguous times (e.g. DST fall-back) issue #1162.
fold = getattr(dt, "fold", 0)
if dt.tzinfo is not None and dt.utcoffset() is not None:
resolved = cls._get_tzinfo(tzinfo)
dt_fold1 = dt_datetime(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
tzinfo=resolved,
fold=1,
)
if dt_fold1.utcoffset() == dt.utcoffset():
fold = 1
return cls(
dt.year,
dt.month,
Expand All @@ -331,7 +351,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr
dt.second,
dt.microsecond,
tzinfo,
fold=getattr(dt, "fold", 0),
fold=fold,
)

@classmethod
Expand Down
5 changes: 5 additions & 0 deletions arrow/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow:
# (str) -> parse @ tzinfo
elif isinstance(arg, str):
dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace)
if tz is not None and dt.tzinfo is not None:
# If the parsed string already has offset info, convert to
# the target timezone (preserving the UTC moment) rather than
# replacing the tzinfo, so ambiguous times are resolved correctly issue #1162.
return self.type.fromdatetime(dt).to(tz)
return self.type.fromdatetime(dt, tzinfo=tz)

# (struct_time) -> from struct_time
Expand Down
15 changes: 15 additions & 0 deletions tests/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,21 @@ def test_range_over_year_maintains_end_date_across_leap_year(self):
arrow.Arrow(2016, 2, 29),
]

def test_range_dst_fall_back_includes_repeated_hour(self):
# Issue #1162: When iterating over a DST fall-back period, the repeated
# hour should not be skipped. A string with an explicit UTC offset and a
# named tzinfo kwarg should be converted (not replaced), preserving the
# correct UTC moment and fold.
import arrow as arrow_module

dts = arrow_module.get("2021-11-07T01:00:00-05:00", tzinfo="US/Eastern")
dte = arrow_module.get("2021-11-07T02:00:00-06:00", tzinfo="US/Eastern")
result = list(arrow.Arrow.range("hours", dts, dte))
assert len(result) == 3
assert result[0].utcoffset().total_seconds() == -5 * 3600
assert result[1].utcoffset().total_seconds() == -5 * 3600
assert result[2].utcoffset().total_seconds() == -5 * 3600


class TestArrowSpanRange:
def test_year(self):
Expand Down
Loading