diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 3a941deb2c68d..b6025d6294a0b 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -167,6 +167,7 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.tseries.offsets.DateOffset.normalize GL08" \ -i "pandas.tseries.offsets.Day.is_on_offset GL08" \ -i "pandas.tseries.offsets.Day.n GL08" \ + -i "pandas.tseries.offsets.Day.freqstr SA01" \ -i "pandas.tseries.offsets.Day.normalize GL08" \ -i "pandas.tseries.offsets.Easter.is_on_offset GL08" \ -i "pandas.tseries.offsets.Easter.n GL08" \ diff --git a/doc/source/user_guide/timedeltas.rst b/doc/source/user_guide/timedeltas.rst index 01df17bac5fd7..a90ddd4da8c36 100644 --- a/doc/source/user_guide/timedeltas.rst +++ b/doc/source/user_guide/timedeltas.rst @@ -63,7 +63,7 @@ Further, operations among the scalars yield another scalar ``Timedelta``. .. ipython:: python - pd.Timedelta(pd.offsets.Day(2)) + pd.Timedelta(pd.offsets.Second(2)) + pd.Timedelta( + pd.Timedelta(pd.offsets.Hour(48)) + pd.Timedelta(pd.offsets.Second(2)) + pd.Timedelta( "00:00:00.000123" ) diff --git a/pandas/_libs/tslibs/__init__.py b/pandas/_libs/tslibs/__init__.py index f433a3acf356f..4c6bbb87baa2c 100644 --- a/pandas/_libs/tslibs/__init__.py +++ b/pandas/_libs/tslibs/__init__.py @@ -1,5 +1,6 @@ __all__ = [ "BaseOffset", + "Day", "IncompatibleFrequency", "NaT", "NaTType", @@ -61,6 +62,7 @@ ) from pandas._libs.tslibs.offsets import ( BaseOffset, + Day, Tick, to_offset, ) diff --git a/pandas/_libs/tslibs/offsets.pyi b/pandas/_libs/tslibs/offsets.pyi index a71aa42b4f671..f220366be1387 100644 --- a/pandas/_libs/tslibs/offsets.pyi +++ b/pandas/_libs/tslibs/offsets.pyi @@ -92,6 +92,7 @@ class BaseOffset: def __getstate__(self): ... @property def nanos(self) -> int: ... + def _maybe_to_hours(self) -> BaseOffset: ... def _get_offset(name: str) -> BaseOffset: ... @@ -116,7 +117,9 @@ class Tick(SingleConstructorOffset): def delta_to_tick(delta: timedelta) -> Tick: ... -class Day(Tick): ... +class Day(BaseOffset): + def _maybe_to_hours(self) -> Hour: ... + class Hour(Tick): ... class Minute(Tick): ... class Second(Tick): ... diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 87214c3758d5c..a96a75d26a8b0 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -845,6 +845,11 @@ cdef class BaseOffset: """ raise ValueError(f"{self} is a non-fixed frequency") + def _maybe_to_hours(self): + if not isinstance(self, Day): + return self + return Hour(self.n * 24) + # ------------------------------------------------------------------ def is_month_start(self, _Timestamp ts): @@ -1023,8 +1028,6 @@ cdef class Tick(SingleConstructorOffset): # Note: Without making this cpdef, we get AttributeError when calling # from __mul__ cpdef Tick _next_higher_resolution(Tick self): - if type(self) is Day: - return Hour(self.n * 24) if type(self) is Hour: return Minute(self.n * 60) if type(self) is Minute: @@ -1173,7 +1176,7 @@ cdef class Tick(SingleConstructorOffset): self.normalize = False -cdef class Day(Tick): +cdef class Day(SingleConstructorOffset): """ Offset ``n`` days. @@ -1203,11 +1206,51 @@ cdef class Day(Tick): >>> ts + Day(-4) Timestamp('2022-12-05 15:00:00') """ + _adjust_dst = True + _attributes = tuple(["n", "normalize"]) _nanos_inc = 24 * 3600 * 1_000_000_000 _prefix = "D" _period_dtype_code = PeriodDtypeCode.D _creso = NPY_DATETIMEUNIT.NPY_FR_D + def __init__(self, n=1, normalize=False): + BaseOffset.__init__(self, n) + if normalize: + # GH#21427 + raise ValueError( + "Day offset with `normalize=True` are not allowed." + ) + + def is_on_offset(self, dt) -> bool: + return True + + @apply_wraps + def _apply(self, other): + if isinstance(other, Day): + # TODO: why isn't this handled in __add__? + return Day(self.n + other.n) + return other + np.timedelta64(self.n, "D") + + def _apply_array(self, dtarr): + return dtarr + np.timedelta64(self.n, "D") + + @cache_readonly + def freqstr(self) -> str: + """ + Return a string representing the frequency. + + Examples + -------- + >>> pd.Day(5).freqstr + '5D' + + >>> pd.offsets.Day(1).freqstr + 'D' + """ + if self.n != 1: + return str(self.n) + "D" + return "D" + cdef class Hour(Tick): """ @@ -1431,16 +1474,13 @@ cdef class Nano(Tick): def delta_to_tick(delta: timedelta) -> Tick: if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0: # nanoseconds only for pd.Timedelta - if delta.seconds == 0: - return Day(delta.days) + seconds = delta.days * 86400 + delta.seconds + if seconds % 3600 == 0: + return Hour(seconds / 3600) + elif seconds % 60 == 0: + return Minute(seconds / 60) else: - seconds = delta.days * 86400 + delta.seconds - if seconds % 3600 == 0: - return Hour(seconds / 3600) - elif seconds % 60 == 0: - return Minute(seconds / 60) - else: - return Second(seconds) + return Second(seconds) else: nanos = delta_to_nanoseconds(delta) if nanos % 1_000_000 == 0: @@ -5131,8 +5171,8 @@ def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str: warnings.warn( f"\'{name}\' is deprecated and will be removed " f"in a future version, please use " - f"\'{c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)}\'" - f" instead.", + f"\'{c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)}\' " + f"instead.", FutureWarning, stacklevel=find_stack_level(), ) @@ -5145,8 +5185,8 @@ def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str: warnings.warn( f"\'{name}\' is deprecated and will be removed " f"in a future version, please use " - f"\'{_name}\'" - f" instead.", + f"\'{_name}\' " + f"instead.", FutureWarning, stacklevel=find_stack_level(), ) @@ -5247,7 +5287,7 @@ cpdef to_offset(freq, bint is_period=False): <2 * BusinessDays> >>> to_offset(pd.Timedelta(days=1)) - + <24 * Hours> >>> to_offset(pd.offsets.Hour()) @@ -5308,7 +5348,7 @@ cpdef to_offset(freq, bint is_period=False): if not stride: stride = 1 - if name in {"D", "h", "min", "s", "ms", "us", "ns"}: + if name in {"h", "min", "s", "ms", "us", "ns"}: # For these prefixes, we have something like "3h" or # "2.5min", so we can construct a Timedelta with the # matching unit and get our offset from delta_to_tick @@ -5326,6 +5366,12 @@ cpdef to_offset(freq, bint is_period=False): if result is None: result = offset + elif isinstance(result, Day) and isinstance(offset, Tick): + # e.g. "1D1H" is treated like "25H" + result = Hour(result.n * 24) + offset + elif isinstance(offset, Day) and isinstance(result, Tick): + # e.g. "1H1D" is treated like "25H" + result = result + Hour(offset.n * 24) else: result = result + offset except (ValueError, TypeError) as err: diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index df5c17745b8a4..22c1b98bc54bb 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -113,6 +113,7 @@ from pandas._libs.tslibs.offsets cimport ( from pandas._libs.tslibs.offsets import ( INVALID_FREQ_ERR_MSG, BDay, + Day, ) from pandas.util._decorators import set_module @@ -1825,6 +1826,10 @@ cdef class _Period(PeriodMixin): # i.e. np.timedelta64("nat") return NaT + if isinstance(other, Day): + # Periods are timezone-naive, so we treat Day as Tick-like + other = np.timedelta64(other.n, "D") + try: inc = delta_to_nanoseconds(other, reso=self._dtype._creso, round_ok=False) except ValueError as err: @@ -1846,7 +1851,7 @@ cdef class _Period(PeriodMixin): @cython.overflowcheck(True) def __add__(self, other): - if is_any_td_scalar(other): + if is_any_td_scalar(other) or isinstance(other, Day): return self._add_timedeltalike_scalar(other) elif is_offset_object(other): return self._add_offset(other) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 6c76e05471577..11ef39fdf60bd 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -2575,6 +2575,7 @@ cdef bint _should_cast_to_timedelta(object obj): cpdef int64_t get_unit_for_round(freq, NPY_DATETIMEUNIT creso) except? -1: from pandas._libs.tslibs.offsets import to_offset - freq = to_offset(freq) + # In this context it is unambiguous that "D" represents 24 hours + freq = to_offset(freq)._maybe_to_hours() freq.nanos # raises on non-fixed freq return delta_to_nanoseconds(freq, creso) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index d5e654c95577e..3c98d81240c54 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -29,6 +29,7 @@ ) from pandas._libs.tslibs import ( BaseOffset, + Day, IncompatibleFrequency, NaT, NaTType, @@ -920,14 +921,21 @@ def inferred_freq(self) -> str | None: TimedeltaIndex(['0 days', '10 days', '20 days'], dtype='timedelta64[ns]', freq=None) >>> tdelta_idx.inferred_freq - '10D' + '240h' """ if self.ndim != 1: return None try: - return frequencies.infer_freq(self) + res = frequencies.infer_freq(self) except ValueError: return None + if self.dtype.kind == "m" and res is not None and res.endswith("D"): + # TimedeltaArray freq must be a Tick, so we convert the inferred + # daily freq to hourly. + if res == "D": + return "24h" + res = str(int(res[:-1]) * 24) + "h" + return res @property # NB: override with cache_readonly in immutable subclasses def _resolution_obj(self) -> Resolution | None: @@ -1068,6 +1076,10 @@ def _get_arithmetic_result_freq(self, other) -> BaseOffset | None: elif isinstance(self.freq, Tick): # In these cases return self.freq + elif isinstance(self.freq, Day) and getattr(self, "tz", None) is None: + return self.freq + # TODO: are there tzaware cases when we can reliably preserve freq? + # We have a bunch of tests that seem to think so return None @final @@ -1163,6 +1175,10 @@ def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray: res_m8 = res_values.view(f"timedelta64[{self.unit}]") new_freq = self._get_arithmetic_result_freq(other) + if new_freq is not None: + # TODO: are we sure this is right? + new_freq = new_freq._maybe_to_hours() + new_freq = cast("Tick | None", new_freq) return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype, freq=new_freq) @@ -2015,6 +2031,8 @@ def _maybe_pin_freq(self, freq, validate_kwds: dict) -> None: # We cannot inherit a freq from the data, so we need to validate # the user-passed freq freq = to_offset(freq) + if self.dtype.kind == "m": + freq = freq._maybe_to_hours() type(self)._validate_frequency(self, freq, **validate_kwds) self._freq = freq else: @@ -2286,6 +2304,9 @@ def _with_freq(self, freq) -> Self: assert freq == "infer" freq = to_offset(self.inferred_freq) + if self.dtype.kind == "m" and freq is not None: + assert isinstance(freq, Tick) + arr = self.view() arr._freq = freq return arr diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 57c138d9828bd..710e3ec733cc1 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -474,8 +474,10 @@ def _generate_range( if end is not None: end = end.tz_localize(None) - if isinstance(freq, Tick): - i8values = generate_regular_range(start, end, periods, freq, unit=unit) + if isinstance(freq, Tick) or (tz is None and isinstance(freq, Day)): + i8values = generate_regular_range( + start, end, periods, freq._maybe_to_hours(), unit=unit + ) else: xdr = _generate_range( start=start, end=end, periods=periods, offset=freq, unit=unit @@ -928,7 +930,14 @@ def tz_convert(self, tz) -> Self: # No conversion since timestamps are all UTC to begin with dtype = tz_to_dtype(tz, unit=self.unit) - return self._simple_new(self._ndarray, dtype=dtype, freq=self.freq) + new_freq = self.freq + if self.freq is not None and self.freq._adjust_dst: + # TODO: in some cases we may be able to retain, e.g. if old and new + # tz are both fixed offsets, or if no DST-crossings occur. + # The latter is value-dependent behavior that we may want to avoid. + # Or could convert e.g. "D" to "24h", see GH#51716 + new_freq = None + return self._simple_new(self._ndarray, dtype=dtype, freq=new_freq) @dtl.ravel_compat def tz_localize( diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index ae92e17332c76..732159d6f10bd 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -21,6 +21,7 @@ from pandas._libs.arrays import NDArrayBacked from pandas._libs.tslibs import ( BaseOffset, + Day, NaT, NaTType, Timedelta, @@ -1014,6 +1015,9 @@ def _addsub_int_array_or_scalar( def _add_offset(self, other: BaseOffset): assert not isinstance(other, Tick) + if isinstance(other, Day): + return self + np.timedelta64(other.n, "D") + self._require_matching_freq(other, base=True) return self._addsub_int_array_or_scalar(other.n, operator.add) @@ -1028,7 +1032,7 @@ def _add_timedeltalike_scalar(self, other): ------- PeriodArray """ - if not isinstance(self.freq, Tick): + if not isinstance(self.freq, (Tick, Day)): # We cannot add timedelta-like to non-tick PeriodArray raise raise_on_incompatible(self, other) @@ -1036,7 +1040,10 @@ def _add_timedeltalike_scalar(self, other): # i.e. np.timedelta64("NaT") return super()._add_timedeltalike_scalar(other) - td = np.asarray(Timedelta(other).asm8) + if isinstance(other, Day): + td = np.asarray(Timedelta(days=other.n).asm8) + else: + td = np.asarray(Timedelta(other).asm8) return self._add_timedelta_arraylike(td) def _add_timedelta_arraylike( diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 9012b9f36348a..da48102d4df09 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -14,6 +14,7 @@ tslibs, ) from pandas._libs.tslibs import ( + Day, NaT, NaTType, Tick, @@ -261,6 +262,12 @@ def _from_sequence_not_strict( assert unit not in ["Y", "y", "M"] # caller is responsible for checking + if isinstance(freq, Day): + raise ValueError( + "Day offset object is not valid for TimedeltaIndex, " + "pass e.g. 24H instead." + ) + data, inferred_freq = sequence_to_td64ns(data, copy=copy, unit=unit) if dtype is not None: @@ -279,6 +286,9 @@ def _generate_range( if freq is None and any(x is None for x in [periods, start, end]): raise ValueError("Must provide freq argument if no data is supplied") + if isinstance(freq, Day): + raise TypeError("TimedeltaArray/Index freq must be a Tick or None") + if com.count_not_none(start, end, periods, freq) != 3: raise ValueError( "Of the four parameters: start, end, periods, " diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 8b316de30662c..fd79849fd8a36 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -24,6 +24,7 @@ ) from pandas._libs.tslibs import ( BaseOffset, + Day, Resolution, Tick, parsing, @@ -90,6 +91,7 @@ class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC): _can_hold_strings = False _data: DatetimeArray | TimedeltaArray | PeriodArray + _freq: BaseOffset | None @doc(DatetimeLikeArrayMixin.mean) def mean(self, *, skipna: bool = True, axis: int | None = 0): @@ -599,8 +601,9 @@ def _intersection(self, other: Index, sort: bool = False) -> Index: # At this point we should have result.dtype == self.dtype # and type(result) is type(self._data) result = self._wrap_setop_result(other, result) - return result._with_freq(None)._with_freq("infer") - + result = result._with_freq(None)._with_freq("infer") + result = self._maybe_restore_day(result._data) + return result else: return self._fast_intersect(other, sort) @@ -724,7 +727,18 @@ def _union(self, other, sort): # that result.freq == self.freq return result else: - return super()._union(other, sort)._with_freq("infer") + result = super()._union(other, sort)._with_freq("infer") + return self._maybe_restore_day(result) + + def _maybe_restore_day(self, result: Self) -> Self: + if isinstance(self.freq, Day) and isinstance(result.freq, Tick): + # If we infer a 24H-like freq but are D, restore "D" + td = Timedelta(result.freq) + div, mod = divmod(td.value, 24 * 3600 * 10**9) + if mod == 0: + freq = to_offset("D") * div + result._freq = freq + return result # -------------------------------------------------------------------- # Join Methods diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 8c40b630e8cfd..465a71ca2ac9a 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -1285,6 +1285,8 @@ def interval_range( if isinstance(endpoint, Timestamp): breaks = date_range(start=start, end=end, periods=periods, freq=freq) else: + if freq is not None: + freq = freq._maybe_to_hours() breaks = timedelta_range(start=start, end=end, periods=periods, freq=freq) return IntervalIndex.from_breaks( diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 0a7a0319bed3a..d9f5d2e59eb4f 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -11,6 +11,7 @@ from pandas._libs import index as libindex from pandas._libs.tslibs import ( BaseOffset, + Day, NaT, Period, Resolution, @@ -367,7 +368,7 @@ def _maybe_convert_timedelta(self, other) -> int | npt.NDArray[np.int64]: of self.freq. Note IncompatibleFrequency subclasses ValueError. """ if isinstance(other, (timedelta, np.timedelta64, Tick, np.ndarray)): - if isinstance(self.freq, Tick): + if isinstance(self.freq, (Tick, Day)): # _check_timedeltalike_freq_compat will raise if incompatible delta = self._data._check_timedeltalike_freq_compat(other) return delta diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index fa3de46621643..3d2542582ef2b 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -9,6 +9,7 @@ lib, ) from pandas._libs.tslibs import ( + Day, Resolution, Timedelta, to_offset, @@ -116,7 +117,7 @@ class TimedeltaIndex(DatetimeTimedeltaMixin): >>> pd.TimedeltaIndex(np.arange(5) * 24 * 3600 * 1e9, freq="infer") TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'], - dtype='timedelta64[ns]', freq='D') + dtype='timedelta64[ns]', freq='24h') """ _typ = "timedeltaindex" @@ -295,14 +296,14 @@ def timedelta_range( -------- >>> pd.timedelta_range(start="1 day", periods=4) TimedeltaIndex(['1 days', '2 days', '3 days', '4 days'], - dtype='timedelta64[ns]', freq='D') + dtype='timedelta64[ns]', freq='24h') The ``closed`` parameter specifies which endpoint is included. The default behavior is to include both endpoints. >>> pd.timedelta_range(start="1 day", periods=4, closed="right") TimedeltaIndex(['2 days', '3 days', '4 days'], - dtype='timedelta64[ns]', freq='D') + dtype='timedelta64[ns]', freq='24h') The ``freq`` parameter specifies the frequency of the TimedeltaIndex. Only fixed frequencies can be passed, non-fixed frequencies such as @@ -325,12 +326,22 @@ def timedelta_range( >>> pd.timedelta_range("1 Day", periods=3, freq="100000D", unit="s") TimedeltaIndex(['1 days', '100001 days', '200001 days'], - dtype='timedelta64[s]', freq='100000D') + dtype='timedelta64[s]', freq='2400000h') """ if freq is None and com.any_none(periods, start, end): - freq = "D" + freq = "24h" + + if isinstance(freq, Day): + # If a user specifically passes a Day *object* we disallow it, + # but if they pass a Day-like string we'll convert it to hourly below. + raise ValueError( + "Passing a Day offset to timedelta_range is not allowed, " + "pass an hourly offset instead" + ) freq = to_offset(freq) + if freq is not None: + freq = freq._maybe_to_hours() tdarr = TimedeltaArray._generate_range( start, end, periods, freq, closed=closed, unit=unit ) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 08e3beef99e60..98d88c2b4d72c 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -2099,6 +2099,18 @@ def get_resampler(obj: Series | DataFrame, **kwds) -> Resampler: """ Create a TimeGrouper and return our resampler. """ + freq = kwds.get("freq", None) + if freq is not None: + # TODO: same thing in get_resampler_for_grouping? + axis = kwds.get("axis", 0) + axis = obj._get_axis_number(axis) + ax = obj.axes[axis] + if isinstance(ax, TimedeltaIndex): + # TODO: could disallow/deprecate Day _object_ while still + # allowing "D" string? + freq = to_offset(freq)._maybe_to_hours() + kwds["freq"] = freq + tg = TimeGrouper(obj, **kwds) # type: ignore[arg-type] return tg._get_resampler(obj) @@ -2421,29 +2433,28 @@ def _get_time_delta_bins(self, ax: TimedeltaIndex): f"an instance of {type(ax).__name__}" ) + freq = self.freq._maybe_to_hours() if not isinstance(self.freq, Tick): # GH#51896 raise ValueError( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - f"e.g. '24h' or '3D', not {self.freq}" + f"e.g. '24h' or '72h', not {freq}" ) if not len(ax): - binner = labels = TimedeltaIndex(data=[], freq=self.freq, name=ax.name) + binner = labels = TimedeltaIndex(data=[], freq=freq, name=ax.name) return binner, [], labels start, end = ax.min(), ax.max() if self.closed == "right": - end += self.freq + end += freq - labels = binner = timedelta_range( - start=start, end=end, freq=self.freq, name=ax.name - ) + labels = binner = timedelta_range(start=start, end=end, freq=freq, name=ax.name) end_stamps = labels if self.closed == "left": - end_stamps += self.freq + end_stamps += freq bins = ax.searchsorted(end_stamps, side=self.closed) @@ -2632,7 +2643,7 @@ def _get_timestamp_range_edges( ------- A tuple of length 2, containing the adjusted pd.Timestamp objects. """ - if isinstance(freq, Tick): + if isinstance(freq, (Tick, Day)): index_tz = first.tz if isinstance(origin, Timestamp) and (origin.tz is None) != (index_tz is None): raise ValueError("The origin must have the same timezone as the index.") @@ -2642,6 +2653,8 @@ def _get_timestamp_range_edges( origin = Timestamp("1970-01-01", tz=index_tz) if isinstance(freq, Day): + # TODO: should we change behavior for next comment now that Day + # respects DST? # _adjust_dates_anchored assumes 'D' means 24h, but first/last # might contain a DST transition (23h, 24h, or 25h). # So "pretend" the dates are naive when adjusting the endpoints @@ -2651,7 +2664,15 @@ def _get_timestamp_range_edges( origin = origin.tz_localize(None) first, last = _adjust_dates_anchored( - first, last, freq, closed=closed, origin=origin, offset=offset, unit=unit + first, + last, + # error: Argument 3 to "_adjust_dates_anchored" has incompatible + # type "BaseOffset"; expected "Tick" + freq._maybe_to_hours(), # type: ignore[arg-type] + closed=closed, + origin=origin, + offset=offset, + unit=unit, ) if isinstance(freq, Day): first = first.tz_localize(index_tz) diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 03534bbee4c58..bd5e393b27090 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -1945,6 +1945,7 @@ def _validate(self) -> None: f"passed window {self.window} is not " "compatible with a datetimelike index" ) from err + if isinstance(self._on, PeriodIndex): # error: Incompatible types in assignment (expression has type # "float", variable has type "Optional[int]") @@ -1952,6 +1953,9 @@ def _validate(self) -> None: self._on.freq.nanos / self._on.freq.n ) else: + # In this context we treat Day as 24H + # TODO: will this cause trouble with tzaware cases? + freq = freq._maybe_to_hours() try: unit = dtype_to_unit(self._on.dtype) # type: ignore[arg-type] except TypeError: diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 9251841bdb82f..52943f4e10148 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -835,6 +835,8 @@ def test_dt64arr_add_timedeltalike_scalar( rng = date_range("2000-01-01", "2000-02-01", tz=tz) expected = date_range("2000-01-01 02:00", "2000-02-01 02:00", tz=tz) + if tz is not None: + expected = expected._with_freq(None) rng = tm.box_expected(rng, box_with_array) expected = tm.box_expected(expected, box_with_array) @@ -855,6 +857,8 @@ def test_dt64arr_sub_timedeltalike_scalar( rng = date_range("2000-01-01", "2000-02-01", tz=tz) expected = date_range("1999-12-31 22:00", "2000-01-31 22:00", tz=tz) + if tz is not None: + expected = expected._with_freq(None) rng = tm.box_expected(rng, box_with_array) expected = tm.box_expected(expected, box_with_array) diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index d205569270705..9fc148f492e48 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -290,10 +290,16 @@ def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box_with_array index = tm.box_expected(index, box) expected = tm.box_expected(expected, box) - result = three_days / index - tm.assert_equal(result, expected) + if isinstance(three_days, pd.offsets.Day): + # GH#41943 Day is no longer timedelta-like + msg = "unsupported operand type" + with pytest.raises(TypeError, match=msg): + three_days / index + else: + result = three_days / index + tm.assert_equal(result, expected) + msg = "cannot use operands with types dtype" - msg = "cannot use operands with types dtype" with pytest.raises(TypeError, match=msg): index / three_days diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 642420713aeba..80eecf96591e4 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -628,32 +628,32 @@ def test_tdi_isub_timedeltalike(self, two_hours, box_with_array): # ------------------------------------------------------------- def test_tdi_ops_attributes(self): - rng = timedelta_range("2 days", periods=5, freq="2D", name="x") + rng = timedelta_range("2 days", periods=5, freq="48h", name="x") result = rng + 1 * rng.freq - exp = timedelta_range("4 days", periods=5, freq="2D", name="x") + exp = timedelta_range("4 days", periods=5, freq="48h", name="x") tm.assert_index_equal(result, exp) - assert result.freq == "2D" + assert result.freq == "48h" result = rng - 2 * rng.freq - exp = timedelta_range("-2 days", periods=5, freq="2D", name="x") + exp = timedelta_range("-2 days", periods=5, freq="48h", name="x") tm.assert_index_equal(result, exp) - assert result.freq == "2D" + assert result.freq == "48h" result = rng * 2 exp = timedelta_range("4 days", periods=5, freq="4D", name="x") tm.assert_index_equal(result, exp) - assert result.freq == "4D" + assert result.freq == "96h" result = rng / 2 exp = timedelta_range("1 days", periods=5, freq="D", name="x") tm.assert_index_equal(result, exp) - assert result.freq == "D" + assert result.freq == "24h" result = -rng exp = timedelta_range("-2 days", periods=5, freq="-2D", name="x") tm.assert_index_equal(result, exp) - assert result.freq == "-2D" + assert result.freq == "-48h" rng = timedelta_range("-2 days", periods=5, freq="D", name="x") @@ -1007,7 +1007,7 @@ def test_td64arr_add_sub_datetimelike_scalar( ts = dt_scalar tdi = timedelta_range("1 day", periods=3) - expected = pd.date_range("2012-01-02", periods=3, tz=tz) + expected = pd.date_range("2012-01-02", periods=3, tz=tz, freq="24h") tdarr = tm.box_expected(tdi, box_with_array) expected = tm.box_expected(expected, box_with_array) @@ -1015,7 +1015,7 @@ def test_td64arr_add_sub_datetimelike_scalar( tm.assert_equal(ts + tdarr, expected) tm.assert_equal(tdarr + ts, expected) - expected2 = pd.date_range("2011-12-31", periods=3, freq="-1D", tz=tz) + expected2 = pd.date_range("2011-12-31", periods=3, freq="-24h", tz=tz) expected2 = tm.box_expected(expected2, box_with_array) tm.assert_equal(ts - tdarr, expected2) @@ -1828,6 +1828,16 @@ def test_td64arr_mod_tdscalar( expected = TimedeltaIndex(["1 Day", "2 Days", "0 Days"] * 3) expected = tm.box_expected(expected, box_with_array) + if isinstance(three_days, offsets.Day): + msg = "unsupported operand type" + with pytest.raises(TypeError, match=msg): + tdarr % three_days + with pytest.raises(TypeError, match=msg): + divmod(tdarr, three_days) + with pytest.raises(TypeError, match=msg): + tdarr // three_days + return + result = tdarr % three_days tm.assert_equal(result, expected) @@ -1871,6 +1881,12 @@ def test_td64arr_rmod_tdscalar(self, box_with_array, three_days): expected = TimedeltaIndex(expected) expected = tm.box_expected(expected, box_with_array) + if isinstance(three_days, offsets.Day): + msg = "Cannot divide Day by TimedeltaArray" + with pytest.raises(TypeError, match=msg): + three_days % tdarr + return + result = three_days % tdarr tm.assert_equal(result, expected) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index d1ef29b0bf8a0..44c1498c3eade 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -93,9 +93,9 @@ def arr1d(self): """Fixture returning DatetimeArray with daily frequency.""" data = np.arange(10, dtype="i8") * 24 * 3600 * 10**9 if self.array_cls is PeriodArray: - arr = self.array_cls(data, freq="D") + arr = self.array_cls(data, freq="24h") else: - arr = self.index_cls(data, freq="D")._data + arr = self.index_cls(data, freq="24h")._data return arr def test_compare_len1_raises(self, arr1d): diff --git a/pandas/tests/indexes/datetimes/methods/test_round.py b/pandas/tests/indexes/datetimes/methods/test_round.py index b023542ba0a4c..c7aca5793e5a7 100644 --- a/pandas/tests/indexes/datetimes/methods/test_round.py +++ b/pandas/tests/indexes/datetimes/methods/test_round.py @@ -193,7 +193,7 @@ def test_ceil_floor_edge(self, test_input, rounder, freq, expected): ) def test_round_int64(self, start, index_freq, periods, round_freq): dt = date_range(start=start, freq=index_freq, periods=periods) - unit = to_offset(round_freq).nanos + unit = to_offset(round_freq)._maybe_to_hours().nanos # test floor result = dt.floor(round_freq) diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index bac849301d1f7..fd7ac92ae4e34 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -1,7 +1,6 @@ # Arithmetic tests specific to DatetimeIndex are generally about `freq` # retention or inference. Other arithmetic tests belong in # tests/arithmetic/test_datetime64.py -import pytest from pandas import ( Timedelta, @@ -16,28 +15,30 @@ class TestDatetimeIndexArithmetic: def test_add_timedelta_preserves_freq(self): # GH#37295 should hold for any DTI with freq=None or Tick freq + # In pandas3 "D" preserves time-of-day across DST transitions, so + # is not preserved by subtraction. Ticks offsets like "24h" + # are still preserved tz = "Canada/Eastern" dti = date_range( start=Timestamp("2019-03-26 00:00:00-0400", tz=tz), end=Timestamp("2020-10-17 00:00:00-0400", tz=tz), - freq="D", + freq="24h", ) result = dti + Timedelta(days=1) assert result.freq == dti.freq def test_sub_datetime_preserves_freq(self, tz_naive_fixture): # GH#48818 - dti = date_range("2016-01-01", periods=12, tz=tz_naive_fixture) + # In pandas3 "D" preserves time-of-day across DST transitions, so + # is not preserved by subtraction. Ticks offsets like "24h" + # are still preserved + dti = date_range("2016-01-01", periods=12, tz=tz_naive_fixture, freq="24h") res = dti - dti[0] expected = timedelta_range("0 Days", "11 Days") tm.assert_index_equal(res, expected) assert res.freq == expected.freq - @pytest.mark.xfail( - reason="The inherited freq is incorrect bc dti.freq is incorrect " - "https://github.com/pandas-dev/pandas/pull/48818/files#r982793461" - ) def test_sub_datetime_preserves_freq_across_dst(self): # GH#48818 ts = Timestamp("2016-03-11", tz="US/Pacific") diff --git a/pandas/tests/indexes/timedeltas/methods/test_insert.py b/pandas/tests/indexes/timedeltas/methods/test_insert.py index f8164102815f6..cfac475c1f47d 100644 --- a/pandas/tests/indexes/timedeltas/methods/test_insert.py +++ b/pandas/tests/indexes/timedeltas/methods/test_insert.py @@ -136,7 +136,7 @@ def test_insert_empty(self): td = idx[0] result = idx[:0].insert(0, td) - assert result.freq == "D" + assert result.freq == "24h" with pytest.raises(IndexError, match="loc must be an integer between"): result = idx[:0].insert(1, td) diff --git a/pandas/tests/indexes/timedeltas/test_constructors.py b/pandas/tests/indexes/timedeltas/test_constructors.py index ace0ab7990138..5c699e3c17bed 100644 --- a/pandas/tests/indexes/timedeltas/test_constructors.py +++ b/pandas/tests/indexes/timedeltas/test_constructors.py @@ -177,7 +177,7 @@ def test_constructor_coverage(self): # non-conforming freq msg = ( "Inferred frequency None from passed values does not conform to " - "passed frequency D" + "passed frequency 24h" ) with pytest.raises(ValueError, match=msg): TimedeltaIndex(["1 days", "2 days", "4 days"], freq="D") diff --git a/pandas/tests/indexes/timedeltas/test_formats.py b/pandas/tests/indexes/timedeltas/test_formats.py index 607336060cbbc..ebd9757a26af5 100644 --- a/pandas/tests/indexes/timedeltas/test_formats.py +++ b/pandas/tests/indexes/timedeltas/test_formats.py @@ -13,7 +13,7 @@ def test_repr_round_days_non_nano(self): # we should get "1 days", not "1 days 00:00:00" with non-nano tdi = TimedeltaIndex(["1 days"], freq="D").as_unit("s") result = repr(tdi) - expected = "TimedeltaIndex(['1 days'], dtype='timedelta64[s]', freq='D')" + expected = "TimedeltaIndex(['1 days'], dtype='timedelta64[s]', freq='24h')" assert result == expected result2 = repr(Series(tdi)) @@ -28,15 +28,17 @@ def test_representation(self, method): idx4 = TimedeltaIndex(["1 days", "2 days", "3 days"], freq="D") idx5 = TimedeltaIndex(["1 days 00:00:01", "2 days", "3 days"]) - exp1 = "TimedeltaIndex([], dtype='timedelta64[ns]', freq='D')" + exp1 = "TimedeltaIndex([], dtype='timedelta64[ns]', freq='24h')" - exp2 = "TimedeltaIndex(['1 days'], dtype='timedelta64[ns]', freq='D')" + exp2 = "TimedeltaIndex(['1 days'], dtype='timedelta64[ns]', freq='24h')" - exp3 = "TimedeltaIndex(['1 days', '2 days'], dtype='timedelta64[ns]', freq='D')" + exp3 = ( + "TimedeltaIndex(['1 days', '2 days'], dtype='timedelta64[ns]', freq='24h')" + ) exp4 = ( "TimedeltaIndex(['1 days', '2 days', '3 days'], " - "dtype='timedelta64[ns]', freq='D')" + "dtype='timedelta64[ns]', freq='24h')" ) exp5 = ( @@ -89,13 +91,13 @@ def test_summary(self): idx4 = TimedeltaIndex(["1 days", "2 days", "3 days"], freq="D") idx5 = TimedeltaIndex(["1 days 00:00:01", "2 days", "3 days"]) - exp1 = "TimedeltaIndex: 0 entries\nFreq: D" + exp1 = "TimedeltaIndex: 0 entries\nFreq: 24h" - exp2 = "TimedeltaIndex: 1 entries, 1 days to 1 days\nFreq: D" + exp2 = "TimedeltaIndex: 1 entries, 1 days to 1 days\nFreq: 24h" - exp3 = "TimedeltaIndex: 2 entries, 1 days to 2 days\nFreq: D" + exp3 = "TimedeltaIndex: 2 entries, 1 days to 2 days\nFreq: 24h" - exp4 = "TimedeltaIndex: 3 entries, 1 days to 3 days\nFreq: D" + exp4 = "TimedeltaIndex: 3 entries, 1 days to 3 days\nFreq: 24h" exp5 = "TimedeltaIndex: 3 entries, 1 days 00:00:01 to 3 days 00:00:00" diff --git a/pandas/tests/indexes/timedeltas/test_freq_attr.py b/pandas/tests/indexes/timedeltas/test_freq_attr.py index 1912c49d3000f..f497595e584cb 100644 --- a/pandas/tests/indexes/timedeltas/test_freq_attr.py +++ b/pandas/tests/indexes/timedeltas/test_freq_attr.py @@ -4,7 +4,6 @@ from pandas.tseries.offsets import ( DateOffset, - Day, Hour, MonthEnd, ) @@ -12,7 +11,7 @@ class TestFreq: @pytest.mark.parametrize("values", [["0 days", "2 days", "4 days"], []]) - @pytest.mark.parametrize("freq", ["2D", Day(2), "48h", Hour(48)]) + @pytest.mark.parametrize("freq", ["48h", Hour(48)]) def test_freq_setter(self, values, freq): # GH#20678 idx = TimedeltaIndex(values) @@ -42,11 +41,11 @@ def test_freq_setter_errors(self): # setting with an incompatible freq msg = ( - "Inferred frequency 2D from passed values does not conform to " - "passed frequency 5D" + "Inferred frequency 48h from passed values does not conform to " + "passed frequency 120h" ) with pytest.raises(ValueError, match=msg): - idx._data.freq = "5D" + idx._data.freq = "120h" # setting with a non-fixed frequency msg = r"<2 \* BusinessDays> is a non-fixed frequency" @@ -61,12 +60,12 @@ def test_freq_view_safe(self): # Setting the freq for one TimedeltaIndex shouldn't alter the freq # for another that views the same data - tdi = TimedeltaIndex(["0 days", "2 days", "4 days"], freq="2D") + tdi = TimedeltaIndex(["0 days", "2 days", "4 days"], freq="48h") tda = tdi._data tdi2 = TimedeltaIndex(tda)._with_freq(None) assert tdi2.freq is None # Original was not altered - assert tdi.freq == "2D" - assert tda.freq == "2D" + assert tdi.freq == "48h" + assert tda.freq == "48h" diff --git a/pandas/tests/indexes/timedeltas/test_ops.py b/pandas/tests/indexes/timedeltas/test_ops.py index f6013baf86edc..ed433e42a5e0f 100644 --- a/pandas/tests/indexes/timedeltas/test_ops.py +++ b/pandas/tests/indexes/timedeltas/test_ops.py @@ -11,4 +11,12 @@ def test_infer_freq(self, freq_sample): idx = timedelta_range("1", freq=freq_sample, periods=10) result = TimedeltaIndex(idx.asi8, freq="infer") tm.assert_index_equal(idx, result) - assert result.freq == freq_sample + + if freq_sample == "D": + assert result.freq == "24h" + elif freq_sample == "3D": + assert result.freq == "72h" + elif freq_sample == "-3D": + assert result.freq == "-72h" + else: + assert result.freq == freq_sample diff --git a/pandas/tests/indexes/timedeltas/test_setops.py b/pandas/tests/indexes/timedeltas/test_setops.py index ae88caf18fdae..021c000fdea42 100644 --- a/pandas/tests/indexes/timedeltas/test_setops.py +++ b/pandas/tests/indexes/timedeltas/test_setops.py @@ -93,7 +93,7 @@ def test_union_freq_infer(self): result = left.union(right) tm.assert_index_equal(result, tdi) - assert result.freq == "D" + assert result.freq == "24h" def test_intersection_bug_1708(self): index_1 = timedelta_range("1 day", periods=4, freq="h") diff --git a/pandas/tests/indexes/timedeltas/test_timedelta_range.py b/pandas/tests/indexes/timedeltas/test_timedelta_range.py index 6f3d29fb4240a..72090436333a6 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -30,7 +30,9 @@ def test_timedelta_range(self): result = timedelta_range("0 days", "10 days", freq="D") tm.assert_index_equal(result, expected) - expected = to_timedelta(np.arange(5), unit="D") + Second(2) + Day() + expected = ( + to_timedelta(np.arange(5), unit="D") + Second(2) + Day()._maybe_to_hours() + ) result = timedelta_range("1 days, 00:00:02", "5 days, 00:00:02", freq="D") tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexing/test_partial.py b/pandas/tests/indexing/test_partial.py index 6f20d0e4e7cbf..a28a7dde57a2b 100644 --- a/pandas/tests/indexing/test_partial.py +++ b/pandas/tests/indexing/test_partial.py @@ -344,9 +344,9 @@ def test_partial_setting2(self): columns=["A", "B", "C", "D"], ) - expected = pd.concat( - [df_orig, DataFrame({"A": 7}, index=dates[-1:] + dates.freq)], sort=True - ) + exp_index = dates[-1:] + dates.freq + exp_index.freq = dates.freq + expected = pd.concat([df_orig, DataFrame({"A": 7}, index=exp_index)], sort=True) df = df_orig.copy() df.loc[dates[-1] + dates.freq, "A"] = 7 tm.assert_frame_equal(df, expected) diff --git a/pandas/tests/resample/test_base.py b/pandas/tests/resample/test_base.py index d9bd89af61aaf..760e2fcaf9808 100644 --- a/pandas/tests/resample/test_base.py +++ b/pandas/tests/resample/test_base.py @@ -191,7 +191,7 @@ def test_resample_empty_series(freq, index, resample_method): if freq == "ME" and isinstance(ser.index, TimedeltaIndex): msg = ( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - "e.g. '24h' or '3D', not " + "e.g. '24h' or '72h', not " ) with pytest.raises(ValueError, match=msg): ser.resample(freq) @@ -294,7 +294,7 @@ def test_resample_count_empty_series(freq, index, resample_method): if freq == "ME" and isinstance(ser.index, TimedeltaIndex): msg = ( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - "e.g. '24h' or '3D', not " + "e.g. '24h' or '72h', not " ) with pytest.raises(ValueError, match=msg): ser.resample(freq) @@ -330,7 +330,7 @@ def test_resample_empty_dataframe(index, freq, resample_method): if freq == "ME" and isinstance(df.index, TimedeltaIndex): msg = ( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - "e.g. '24h' or '3D', not " + "e.g. '24h' or '72h', not " ) with pytest.raises(ValueError, match=msg): df.resample(freq, group_keys=False) @@ -378,7 +378,7 @@ def test_resample_count_empty_dataframe(freq, index): if freq == "ME" and isinstance(empty_frame_dti.index, TimedeltaIndex): msg = ( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - "e.g. '24h' or '3D', not " + "e.g. '24h' or '72h', not " ) with pytest.raises(ValueError, match=msg): empty_frame_dti.resample(freq) @@ -414,7 +414,7 @@ def test_resample_size_empty_dataframe(freq, index): if freq == "ME" and isinstance(empty_frame_dti.index, TimedeltaIndex): msg = ( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - "e.g. '24h' or '3D', not " + "e.g. '24h' or '72h', not " ) with pytest.raises(ValueError, match=msg): empty_frame_dti.resample(freq) @@ -504,7 +504,7 @@ def test_apply_to_empty_series(index, freq): if freq == "ME" and isinstance(ser.index, TimedeltaIndex): msg = ( "Resampling on a TimedeltaIndex requires fixed-duration `freq`, " - "e.g. '24h' or '3D', not " + "e.g. '24h' or '72h', not " ) with pytest.raises(ValueError, match=msg): ser.resample(freq) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index f871c0bf0218c..3050dab8046c8 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -884,7 +884,7 @@ def test_resample_origin_epoch_with_tz_day_vs_24h(unit): result_1 = ts_1.resample("D", origin="epoch").mean() result_2 = ts_1.resample("24h", origin="epoch").mean() - tm.assert_series_equal(result_1, result_2) + tm.assert_series_equal(result_1, result_2, check_freq=False) # check that we have the same behavior with epoch even if we are not timezone aware ts_no_tz = ts_1.tz_localize(None) @@ -1843,9 +1843,17 @@ def test_resample_equivalent_offsets(n1, freq1, n2, freq2, k, unit): dti = date_range("1991-09-05", "1991-09-12", freq=freq1).as_unit(unit) ser = Series(range(len(dti)), index=dti) + if freq2 == "D" and n2 % 1 != 0: + msg = "Invalid frequency: (0.25|0.5|0.75|1.0|1.5)D" + with pytest.raises(ValueError, match=msg): + ser.resample(str(n2_) + freq2) + return + result1 = ser.resample(str(n1_) + freq1).mean() result2 = ser.resample(str(n2_) + freq2).mean() - tm.assert_series_equal(result1, result2) + assert result1.index.freq == str(n1_) + freq1 + assert result2.index.freq == str(n2_) + freq2 + tm.assert_series_equal(result1, result2, check_freq=False) @pytest.mark.parametrize( diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index e17529dfab00c..78c02e0265def 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -912,6 +912,7 @@ def test_resample_with_offset(self, start, end, start_freq, end_freq, offset): result = result.to_timestamp(end_freq) expected = ser.to_timestamp().resample(end_freq, offset=offset).mean() + result.index._data._freq = result.index.freq._maybe_to_hours() tm.assert_series_equal(result, expected) def test_resample_with_offset_month(self): diff --git a/pandas/tests/resample/test_resample_api.py b/pandas/tests/resample/test_resample_api.py index da1774cf22587..065c38a5765a8 100644 --- a/pandas/tests/resample/test_resample_api.py +++ b/pandas/tests/resample/test_resample_api.py @@ -748,7 +748,7 @@ def test_resample_agg_readonly(): arr.setflags(write=False) ser = Series(arr, index=index) - rs = ser.resample("1D") + rs = ser.resample("24h") expected = Series([pd.Timestamp(0), pd.Timestamp(0)], index=index[::24]) diff --git a/pandas/tests/resample/test_timedelta.py b/pandas/tests/resample/test_timedelta.py index 309810b656ed3..f400fddcd33df 100644 --- a/pandas/tests/resample/test_timedelta.py +++ b/pandas/tests/resample/test_timedelta.py @@ -215,5 +215,5 @@ def test_arrow_duration_resample(): # GH 56371 idx = pd.Index(timedelta_range("1 day", periods=5), dtype="duration[ns][pyarrow]") expected = Series(np.arange(5, dtype=np.float64), index=idx) - result = expected.resample("1D").mean() + result = expected.resample("24h").mean() tm.assert_series_equal(result, expected) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index baaedaa853565..00e2773a98730 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -573,7 +573,7 @@ def test_period_cons_combined(self): with pytest.raises(ValueError, match=msg): Period(ordinal=1, freq="-1h1D") - msg = "Frequency must be positive, because it represents span: 0D" + msg = "Frequency must be positive, because it represents span: 0h" with pytest.raises(ValueError, match=msg): Period("2011-01", freq="0D0h") with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 45caeb1733590..93a8542276ec4 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -253,7 +253,13 @@ def test_from_tick_reso(): assert Timedelta(tick)._creso == NpyDatetimeUnit.NPY_FR_s.value tick = offsets.Day() - assert Timedelta(tick)._creso == NpyDatetimeUnit.NPY_FR_s.value + msg = ( + "Value must be Timedelta, string, integer, float, timedelta or " + "convertible, not Day" + ) + with pytest.raises(ValueError, match=msg): + # TODO: should be TypeError? + Timedelta(tick) def test_construction(): diff --git a/pandas/tests/scalar/timestamp/methods/test_round.py b/pandas/tests/scalar/timestamp/methods/test_round.py index 6b27e5e6c5554..27738e95f2e34 100644 --- a/pandas/tests/scalar/timestamp/methods/test_round.py +++ b/pandas/tests/scalar/timestamp/methods/test_round.py @@ -244,7 +244,7 @@ def test_round_int64(self, timestamp, freq): # check that all rounding modes are accurate to int64 precision # see GH#22591 dt = Timestamp(timestamp).as_unit("ns") - unit = to_offset(freq).nanos + unit = to_offset(freq)._maybe_to_hours().nanos # test floor result = dt.floor(freq) diff --git a/pandas/tests/tseries/frequencies/test_freq_code.py b/pandas/tests/tseries/frequencies/test_freq_code.py index 16b7190753ee2..e854ba28fa285 100644 --- a/pandas/tests/tseries/frequencies/test_freq_code.py +++ b/pandas/tests/tseries/frequencies/test_freq_code.py @@ -28,7 +28,6 @@ def test_get_to_timestamp_base(freqstr, exp_freqstr): ((1.04, "h"), (3744, "s")), ((1, "D"), (1, "D")), ((0.342931, "h"), (1234551600, "us")), - ((1.2345, "D"), (106660800, "ms")), ], ) def test_resolution_bumping(args, expected): diff --git a/pandas/tests/tseries/offsets/test_dst.py b/pandas/tests/tseries/offsets/test_dst.py index e75958843040d..c96dbddbc0b06 100644 --- a/pandas/tests/tseries/offsets/test_dst.py +++ b/pandas/tests/tseries/offsets/test_dst.py @@ -214,7 +214,7 @@ def test_springforward_singular(self, performance_warning): QuarterEnd: ["11/2/2012", "12/31/2012"], BQuarterBegin: ["11/2/2012", "12/3/2012"], BQuarterEnd: ["11/2/2012", "12/31/2012"], - Day: ["11/4/2012", "11/4/2012 23:00"], + Day: ["11/4/2012", "11/5/2012"], }.items() @pytest.mark.parametrize("tup", offset_classes) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 0b2e66a2b3a0d..26b182fb4e9b1 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -47,6 +47,7 @@ CustomBusinessMonthBegin, CustomBusinessMonthEnd, DateOffset, + Day, Easter, FY5253Quarter, LastWeekOfMonth, @@ -244,7 +245,7 @@ def test_offset_freqstr(self, offset_types): assert offset.rule_code == code def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=False): - if normalize and issubclass(offset, Tick): + if normalize and issubclass(offset, (Tick, Day)): # normalize=True disallowed for Tick subclasses GH#21427 return @@ -464,7 +465,7 @@ def test_is_on_offset(self, offset_types, expecteds): assert offset_s.is_on_offset(dt) # when normalize=True, is_on_offset checks time is 00:00:00 - if issubclass(offset_types, Tick): + if issubclass(offset_types, (Tick, Day)): # normalize=True disallowed for Tick subclasses GH#21427 return offset_n = _create_offset(offset_types, normalize=True) @@ -496,7 +497,7 @@ def test_add(self, offset_types, tz_naive_fixture, expecteds): assert result == expected_localize # normalize=True, disallowed for Tick subclasses GH#21427 - if issubclass(offset_types, Tick): + if issubclass(offset_types, (Tick, Day)): return offset_s = _create_offset(offset_types, normalize=True) expected = Timestamp(expected.date()) diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 46b6846ad1ec2..f1065690233af 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -54,7 +54,7 @@ def test_delta_to_tick(): delta = timedelta(3) tick = delta_to_tick(delta) - assert tick == offsets.Day(3) + assert tick == offsets.Hour(72) td = Timedelta(nanoseconds=5) tick = delta_to_tick(td) diff --git a/pandas/tests/tslibs/test_api.py b/pandas/tests/tslibs/test_api.py index 42d055326c2a5..89835ff4b7694 100644 --- a/pandas/tests/tslibs/test_api.py +++ b/pandas/tests/tslibs/test_api.py @@ -29,6 +29,7 @@ def test_namespace(): "NaTType", "iNaT", "nat_strings", + "Day", "OutOfBoundsDatetime", "OutOfBoundsTimedelta", "Period", diff --git a/pandas/tests/tslibs/test_to_offset.py b/pandas/tests/tslibs/test_to_offset.py index add9213ae59fb..f3d323f315002 100644 --- a/pandas/tests/tslibs/test_to_offset.py +++ b/pandas/tests/tslibs/test_to_offset.py @@ -147,7 +147,7 @@ def test_to_offset_leading_plus(freqstr, expected, wrn): ({"days": -1, "seconds": 1}, offsets.Second(-86399)), ({"hours": 1, "minutes": 10}, offsets.Minute(70)), ({"hours": 1, "minutes": -10}, offsets.Minute(50)), - ({"weeks": 1}, offsets.Day(7)), + ({"weeks": 1}, offsets.Hour(168)), ({"hours": 1}, offsets.Hour(1)), ({"hours": 1}, to_offset("60min")), ({"microseconds": 1}, offsets.Micro(1)),