diff --git a/pandas/_libs/tslibs/timestamps.pyi b/pandas/_libs/tslibs/timestamps.pyi index 3195ce9641f2b..301955d019075 100644 --- a/pandas/_libs/tslibs/timestamps.pyi +++ b/pandas/_libs/tslibs/timestamps.pyi @@ -23,7 +23,11 @@ from pandas._libs.tslibs import ( Tick, Timedelta, ) -from pandas._typing import TimestampNonexistent +from pandas._typing import ( + TimeAmbiguous, + TimeNonexistent, + TimestampNonexistent, +) _TimeZones: TypeAlias = str | _tzinfo | None | int @@ -191,7 +195,11 @@ class Timestamp(datetime): ambiguous: bool | Literal["raise", "NaT"] = ..., nonexistent: TimestampNonexistent = ..., ) -> Self: ... - def normalize(self) -> Self: ... + def normalize( + self, + ambiguous: TimeAmbiguous = ..., + nonexistent: TimeNonexistent = ..., + ) -> Self: ... # TODO: round/floor/ceil could return NaT? def round( self, diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 2f0c5fa9ef18e..13b06f0fbdb45 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1308,7 +1308,10 @@ cdef class _Timestamp(ABCTimestamp): # ----------------------------------------------------------------- # Transformation Methods - def normalize(self) -> "Timestamp": + def normalize(self, + ambiguous: TimeAmbiguous = "raise", + nonexistent: TimeNonexistent = "raise" + ) -> "Timestamp": """ Normalize Timestamp to midnight, preserving tz information. @@ -1346,7 +1349,7 @@ cdef class _Timestamp(ABCTimestamp): "Cannot normalize Timestamp without integer overflow" ) from err ts = type(self)._from_value_and_reso(normalized, reso=self._creso, tz=None) - return ts.tz_localize(self.tzinfo) + return ts.tz_localize(self.tzinfo, ambiguous=ambiguous, nonexistent=nonexistent) # ----------------------------------------------------------------- # Pickle Methods diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index d17ffbbfa5b4d..5468fa870e049 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -447,7 +447,9 @@ def _generate_range( end = end.as_unit(unit, round_ok=False) left_inclusive, right_inclusive = validate_inclusive(inclusive) - start, end = _maybe_normalize_endpoints(start, end, normalize) + start, end = _maybe_normalize_endpoints( + start, end, normalize, ambiguous, nonexistent + ) tz = _infer_tz_from_endpoints(start, end, tz) if tz is not None: @@ -2878,14 +2880,18 @@ def _infer_tz_from_endpoints( def _maybe_normalize_endpoints( - start: _TimestampNoneT1, end: _TimestampNoneT2, normalize: bool + start: _TimestampNoneT1, + end: _TimestampNoneT2, + normalize: bool, + ambiguous: TimeAmbiguous = "raise", + nonexistent: TimeNonexistent = "raise", ) -> tuple[_TimestampNoneT1, _TimestampNoneT2]: if normalize: if start is not None: - start = start.normalize() + start = start.normalize(ambiguous=ambiguous, nonexistent=nonexistent) if end is not None: - end = end.normalize() + end = end.normalize(ambiguous=ambiguous, nonexistent=nonexistent) return start, end diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index 199e3572732a0..f7273470c7371 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -844,3 +844,25 @@ def test_factorize_sort_without_freq(): tda = dta - dta[0] with pytest.raises(NotImplementedError, match=msg): tda.factorize(sort=True) + + +def test_date_range_dst_boundries(): + # GH#62602 + start = pd.Timestamp("2024-04-26 01:00:00", tz="Africa/Cairo") + end = pd.Timestamp("2024-04-27 00:00:00", tz="Africa/Cairo") + tz = "Africa/Cairo" + freq = "D" + + full_range = pd.date_range( + start=start, + end=end, + freq=freq, + tz=tz, + nonexistent="shift_forward", + ambiguous=True, + normalize=True, + ) + expected = pd.DatetimeIndex( + ["2024-04-26 01:00:00+03:00"], dtype="datetime64[ns, Africa/Cairo]", freq="D" + ) + assert full_range == expected