diff --git a/doc/source/reference/offset_frequency.rst b/doc/source/reference/offset_frequency.rst index ab89fe74e7337..da88b8d42dd6c 100644 --- a/doc/source/reference/offset_frequency.rst +++ b/doc/source/reference/offset_frequency.rst @@ -1107,7 +1107,6 @@ Properties .. autosummary:: :toctree: api/ - Day.delta Day.freqstr Day.kwds Day.name diff --git a/doc/source/user_guide/timedeltas.rst b/doc/source/user_guide/timedeltas.rst index a6eb96f91a4bf..8c0cd3c756074 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 2cabbe3ff07da..703724ec9cf48 100644 --- a/pandas/_libs/tslibs/__init__.py +++ b/pandas/_libs/tslibs/__init__.py @@ -33,6 +33,7 @@ "is_supported_unit", "npy_unit_to_abbrev", "get_supported_reso", + "Day", ] from pandas._libs.tslibs import dtypes # pylint: disable=import-self @@ -60,6 +61,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 f65933cf740b2..b4cfeb46e3111 100644 --- a/pandas/_libs/tslibs/offsets.pyi +++ b/pandas/_libs/tslibs/offsets.pyi @@ -92,6 +92,7 @@ class BaseOffset: @property def nanos(self) -> int: ... def is_anchored(self) -> bool: ... + def _maybe_to_hours(self) -> BaseOffset: ... def _get_offset(name: str) -> BaseOffset: ... @@ -115,10 +116,13 @@ class Tick(SingleConstructorOffset): def delta(self) -> Timedelta: ... @property def nanos(self) -> int: ... + def _maybe_to_hours(self) -> Tick: ... def delta_to_tick(delta: timedelta) -> Tick: ... -class Day(Tick): ... +class Day(Tick): + 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 84b102bd4a262..cbbc31bfe67d2 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -790,6 +790,11 @@ cdef class BaseOffset: def nanos(self): 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_anchored(self) -> bool: # TODO: Does this make sense for the general case? It would help # if there were a canonical docstring for what is_anchored means. @@ -928,8 +933,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: @@ -1088,12 +1091,41 @@ cdef class Tick(SingleConstructorOffset): self.normalize = False -cdef class Day(Tick): - _nanos_inc = 24 * 3600 * 1_000_000_000 +cdef class Day(SingleConstructorOffset): + _adjust_dst = True + _attributes = tuple(["n", "normalize"]) _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") + + @apply_array_wraps + def _apply_array(self, dtarr): + return dtarr + np.timedelta64(self.n, "D") + + @cache_readonly + def freqstr(self) -> str: + if self.n != 1: + return str(self.n) + "D" + return "D" + cdef class Hour(Tick): _nanos_inc = 3600 * 1_000_000_000 @@ -1140,16 +1172,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: @@ -4378,7 +4407,7 @@ cpdef to_offset(freq): <2 * BusinessDays> >>> to_offset(pd.Timedelta(days=1)) - + <24 * Hours> >>> to_offset(pd.offsets.Hour()) @@ -4417,7 +4446,7 @@ cpdef to_offset(freq): if not stride: stride = 1 - if prefix in {"D", "H", "T", "S", "L", "U", "N"}: + if prefix in {"H", "T", "S", "L", "U", "N"}: # For these prefixes, we have something like "3H" or # "2.5T", so we can construct a Timedelta with the # matching unit and get our offset from delta_to_tick @@ -4435,6 +4464,12 @@ cpdef to_offset(freq): if delta is None: delta = offset + elif isinstance(delta, Day) and isinstance(offset, Tick): + # e.g. "1D1H" is treated like "25H" + delta = Hour(delta.n * 24) + offset + elif isinstance(offset, Day) and isinstance(delta, Tick): + # e.g. "1H1D" is treated like "25H" + delta = delta + Hour(offset.n * 24) else: delta = delta + offset except (ValueError, TypeError) as err: diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index c37e9cd7ef1f3..da3d2a96e354a 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -110,6 +110,7 @@ from pandas._libs.tslibs.offsets cimport ( from pandas._libs.tslibs.offsets import ( INVALID_FREQ_ERR_MSG, BDay, + Day, ) cdef: @@ -1817,6 +1818,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: @@ -1844,7 +1849,7 @@ cdef class _Period(PeriodMixin): return NaT return other.__add__(self) - 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 ffa9a67542e21..1a40fed9dfb92 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1784,6 +1784,7 @@ class Timedelta(_Timedelta): ) * 1_000_000_000 ) + # TODO: catch OverflowError and re-raise as OutOfBoundsTimedelta value = np.timedelta64( int(kwargs.get("nanoseconds", 0)) + int(kwargs.get("microseconds", 0) * 1_000) @@ -1909,7 +1910,10 @@ class Timedelta(_Timedelta): from pandas._libs.tslibs.offsets import to_offset - to_offset(freq).nanos # raises on non-fixed freq + orig = freq + # In this context it is sufficiently clear that "D" this means 24H + freq = to_offset(freq)._maybe_to_hours() + freq.nanos # raises on non-fixed freq unit = delta_to_nanoseconds(to_offset(freq), self._creso) arr = np.array([self._value], dtype="i8") @@ -1917,7 +1921,7 @@ class Timedelta(_Timedelta): result = round_nsint64(arr, mode, unit)[0] except OverflowError as err: raise OutOfBoundsTimedelta( - f"Cannot round {self} to freq={freq} without overflow" + f"Cannot round {self} to freq={orig} without overflow" ) from err return Timedelta._from_value_and_reso(result, self._creso) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 844fc8f0ed187..16d05830e6a82 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1907,7 +1907,8 @@ class Timestamp(_Timestamp): cdef: int64_t nanos - freq = to_offset(freq) + # In this context it is sufficiently clear that "D" this means 24H + freq = to_offset(freq)._maybe_to_hours() freq.nanos # raises on non-fixed freq nanos = delta_to_nanoseconds(freq, self._creso) if nanos == 0: diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 55eea72262170..8592cdb88dc28 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -27,6 +27,7 @@ from pandas._libs.arrays import NDArrayBacked from pandas._libs.tslibs import ( BaseOffset, + Day, IncompatibleFrequency, NaT, NaTType, @@ -886,9 +887,16 @@ def inferred_freq(self) -> str | None: 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: @@ -1025,6 +1033,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 @@ -1124,6 +1136,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) @@ -1964,9 +1980,13 @@ def __init__( if copy: values = values.copy() if freq: + if values.dtype.kind == "m" and isinstance(freq, Day): + raise TypeError("TimedeltaArray freq must be a Tick or None") freq = to_offset(freq) - if values.dtype.kind == "m" and not isinstance(freq, Tick): - raise TypeError("TimedeltaArray/Index freq must be a Tick") + if values.dtype.kind == "m": + freq = freq._maybe_to_hours() + if not isinstance(freq, Tick): + raise TypeError("TimedeltaArray/Index freq must be a Tick") NDArrayBacked.__init__(self, values=values, dtype=dtype) self._freq = freq @@ -1999,7 +2019,7 @@ def freq(self, value) -> None: self._freq = value @classmethod - def _validate_frequency(cls, index, freq, **kwargs): + def _validate_frequency(cls, index, freq: BaseOffset, **kwargs): """ Validate that a frequency is compatible with the values of a given Datetime Array/Index or Timedelta Array/Index @@ -2115,6 +2135,10 @@ def _round(self, freq, mode, ambiguous, nonexistent): values = self.view("i8") values = cast(np.ndarray, values) offset = to_offset(freq) + + # In this context it is clear "D" means "24H" + offset = offset._maybe_to_hours() + offset.nanos # raises on non-fixed frequencies nanos = delta_to_nanoseconds(offset, self._creso) if nanos == 0: @@ -2195,6 +2219,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 8ad51e4a90027..b5a964534ab97 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -448,8 +448,10 @@ def _generate_range( # type: ignore[override] 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 diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index af6402b9964e5..54ead1b417f67 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -36,6 +36,7 @@ from pandas._libs.tslibs.dtypes import FreqGroup from pandas._libs.tslibs.fields import isleapyear_arr from pandas._libs.tslibs.offsets import ( + Day, Tick, delta_to_tick, ) @@ -830,6 +831,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) @@ -844,7 +848,7 @@ def _add_timedeltalike_scalar(self, other): ------- PeriodArray """ - if not isinstance(self.freq, Tick): + if not isinstance(self.freq, Tick) and not isinstance(self.freq, Day): # We cannot add timedelta-like to non-tick PeriodArray raise raise_on_incompatible(self, other) @@ -852,7 +856,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 a81609e1bb618..0ce85e5059d54 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -15,6 +15,7 @@ tslibs, ) from pandas._libs.tslibs import ( + Day, NaT, NaTType, Tick, @@ -263,7 +264,14 @@ def _from_sequence_not_strict( explicit_none = freq is None freq = freq if freq is not lib.no_default else None + if isinstance(freq, Day): + raise ValueError( + "Day offset object is not valid for TimedeltaIndex, " + "pass e.g. 24H instead." + ) freq, freq_infer = dtl.maybe_infer_freq(freq) + if freq is not None: + freq = freq._maybe_to_hours() data, inferred_freq = sequence_to_td64ns(data, copy=copy, unit=unit) freq, freq_infer = dtl.validate_inferred_freq(freq, inferred_freq, freq_infer) @@ -297,6 +305,9 @@ def _generate_range( # type: ignore[override] 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/generic.py b/pandas/core/generic.py index 9d09665b15be9..5def7fc382061 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -34,6 +34,7 @@ from pandas._libs import lib from pandas._libs.lib import is_range_indexer from pandas._libs.tslibs import ( + Day, Period, Tick, Timestamp, @@ -9392,7 +9393,7 @@ def first(self, offset) -> Self: return self.copy(deep=False) offset = to_offset(offset) - if not isinstance(offset, Tick) and offset.is_on_offset(self.index[0]): + if not isinstance(offset, (Tick, Day)) and offset.is_on_offset(self.index[0]): # GH#29623 if first value is end of period, remove offset with n = 1 # before adding the real offset end_date = end = self.index[0] - offset.base + offset @@ -9400,7 +9401,7 @@ def first(self, offset) -> Self: end_date = end = self.index[0] + offset # Tick-like, e.g. 3 weeks - if isinstance(offset, Tick) and end_date in self.index: + if isinstance(offset, (Tick, Day)) and end_date in self.index: end = self.index.searchsorted(end_date, side="left") return self.iloc[:end] diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index d5a292335a5f6..cf817d717099b 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -26,6 +26,7 @@ ) from pandas._libs.tslibs import ( BaseOffset, + Day, Resolution, Tick, parsing, @@ -411,6 +412,7 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC): """ _data: DatetimeArray | TimedeltaArray + _freq: BaseOffset | None _comparables = ["name", "freq"] _attributes = ["name", "freq"] @@ -574,7 +576,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) @@ -696,7 +700,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 1f6c275934070..0c44fd9b9f302 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -1103,6 +1103,8 @@ def interval_range( raise ValueError( f"freq must be numeric or convertible to DateOffset, got {freq}" ) from err + if isinstance(start, Timedelta) or isinstance(end, Timedelta): + freq = freq._maybe_to_hours() # verify type compatibility if not all( diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index a8bd220d14613..c8c19e280d427 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, @@ -298,7 +299,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 cd6a4883946d2..8e295c381850e 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, @@ -307,14 +308,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 @@ -338,12 +339,23 @@ def timedelta_range( >>> pd.timedelta_range("1 Day", periods=3, freq="100000D", unit="s") TimedeltaIndex(['1 days 00:00:00', '100001 days 00:00:00', '200001 days 00:00:00'], - 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, _ = dtl.maybe_infer_freq(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 53d587cdde182..3288f9b488f5a 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1706,9 +1706,10 @@ def _downsample(self, how, **kwargs): if not len(ax): # reset to the new freq + freq = self.freq obj = obj.copy() - obj.index = obj.index._with_freq(self.freq) - assert obj.index.freq == self.freq, (obj.index.freq, self.freq) + obj.index = obj.index._with_freq(freq) + assert obj.index.freq == freq, (obj.index.freq, freq) return obj # do we have a regular frequency @@ -1966,6 +1967,19 @@ def get_resampler(obj: Series | DataFrame, kind=None, **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? + freq = to_offset(freq) + 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 = freq._maybe_to_hours() + kwds["freq"] = freq + tg = TimeGrouper(**kwds) return tg._get_resampler(obj, kind=kind) @@ -2270,29 +2284,28 @@ def _get_time_delta_bins(self, ax: TimedeltaIndex): f"an instance of {type(ax).__name__}" ) - if not isinstance(self.freq, Tick): + freq = self.freq._maybe_to_hours() + if not isinstance(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 '3D', 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) @@ -2461,7 +2474,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 != index_tz: @@ -2479,6 +2492,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 @@ -2488,7 +2503,13 @@ 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, + freq._maybe_to_hours(), + 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 ec48e9c9eb46a..564f117ae35ba 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -1877,7 +1877,9 @@ def _validate(self): self._on.freq.nanos / self._on.freq.n ) else: - self._win_freq_i8 = freq.nanos + # In this context we treat Day as 24H + # TODO: will this cause trouble with tzaware cases? + self._win_freq_i8 = freq._maybe_to_hours().nanos # min_periods must be an integer if self.min_periods is None: diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index e6c743c76a2c1..785f38a885363 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -816,6 +816,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) @@ -836,6 +838,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 455cae084b7c6..3097b8a6eab0e 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -267,10 +267,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 0ffe1ddc3dfb7..3c09e1a098a90 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -624,27 +624,27 @@ def test_tdi_ops_attributes(self): result = rng + 1 * rng.freq exp = timedelta_range("4 days", periods=5, freq="2D", 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") 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") @@ -1008,7 +1008,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) @@ -1016,7 +1016,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) @@ -1821,6 +1821,16 @@ def test_td64arr_mod_tdscalar(self, box_with_array, three_days): 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) @@ -1864,6 +1874,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 8f87749a4ed6e..27094e4f16064 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -89,7 +89,7 @@ class SharedTests: def arr1d(self): """Fixture returning DatetimeArray with daily frequency.""" data = np.arange(10, dtype="i8") * 24 * 3600 * 10**9 - arr = self.array_cls(data, freq="D") + arr = self.array_cls(data, freq="24H") return arr def test_compare_len1_raises(self, arr1d): diff --git a/pandas/tests/arrays/test_timedeltas.py b/pandas/tests/arrays/test_timedeltas.py index 1043c2ee6c9b6..1509516838f55 100644 --- a/pandas/tests/arrays/test_timedeltas.py +++ b/pandas/tests/arrays/test_timedeltas.py @@ -245,7 +245,7 @@ def test_setitem_objects(self, obj): @pytest.mark.parametrize("index", [True, False]) def test_searchsorted_invalid_types(self, other, index): data = np.arange(10, dtype="i8") * 24 * 3600 * 10**9 - arr = TimedeltaArray(data, freq="D") + arr = TimedeltaArray(data, freq="24H") if index: arr = pd.Index(arr) diff --git a/pandas/tests/arrays/timedeltas/test_constructors.py b/pandas/tests/arrays/timedeltas/test_constructors.py index 3a076a6828a98..76e9ec25e066d 100644 --- a/pandas/tests/arrays/timedeltas/test_constructors.py +++ b/pandas/tests/arrays/timedeltas/test_constructors.py @@ -23,7 +23,7 @@ def test_freq_validation(self): msg = ( "Inferred frequency None from passed values does not " - "conform to passed frequency D" + "conform to passed frequency 24H" ) with pytest.raises(ValueError, match=msg): TimedeltaArray(arr.view("timedelta64[ns]"), freq="D") diff --git a/pandas/tests/indexes/datetimes/test_datetime.py b/pandas/tests/indexes/datetimes/test_datetime.py index af1a94391a353..a8c2104a95206 100644 --- a/pandas/tests/indexes/datetimes/test_datetime.py +++ b/pandas/tests/indexes/datetimes/test_datetime.py @@ -19,17 +19,19 @@ class TestDatetimeIndex: def test_sub_datetime_preserves_freq(self, tz_naive_fixture): # GH#48818 + # GH#41943 we cannot reliably preserve non-tick freq when crossing + # DS dti = date_range("2016-01-01", periods=12, tz=tz_naive_fixture) res = dti - dti[0] expected = pd.timedelta_range("0 Days", "11 Days") tm.assert_index_equal(res, expected) - assert res.freq == expected.freq + if tz_naive_fixture is None: + assert res.freq == expected.freq + else: + # we _could_ preserve for UTC and fixed-offsets + assert res.freq is None - @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/datetimes/test_misc.py b/pandas/tests/indexes/datetimes/test_misc.py index 139190962895d..81c381e652804 100644 --- a/pandas/tests/indexes/datetimes/test_misc.py +++ b/pandas/tests/indexes/datetimes/test_misc.py @@ -295,6 +295,7 @@ def test_iter_readonly(): def test_add_timedelta_preserves_freq(): # GH#37295 should hold for any DTI with freq=None or Tick freq + # GH#51874 changed this, with tzaware we can no longer retain "D" in addition tz = "Canada/Eastern" dti = date_range( start=Timestamp("2019-03-26 00:00:00-0400", tz=tz), @@ -302,4 +303,4 @@ def test_add_timedelta_preserves_freq(): freq="D", ) result = dti + Timedelta(days=1) - assert result.freq == dti.freq + assert result.freq is None diff --git a/pandas/tests/indexes/datetimes/test_scalar_compat.py b/pandas/tests/indexes/datetimes/test_scalar_compat.py index f07a9dce5f6ae..314c2e8f7761d 100644 --- a/pandas/tests/indexes/datetimes/test_scalar_compat.py +++ b/pandas/tests/indexes/datetimes/test_scalar_compat.py @@ -251,7 +251,10 @@ 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 + if round_freq == "1D": + unit = 24 * 3600 * 10**9 + else: + unit = to_offset(round_freq).nanos # test floor result = dt.floor(round_freq) diff --git a/pandas/tests/indexes/timedeltas/methods/test_insert.py b/pandas/tests/indexes/timedeltas/methods/test_insert.py index f8164102815f6..547eaaec0e5bf 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 a3de699a9f58d..793a79532b8a3 100644 --- a/pandas/tests/indexes/timedeltas/test_constructors.py +++ b/pandas/tests/indexes/timedeltas/test_constructors.py @@ -214,7 +214,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 751f9e4cc9eee..48c2b4e3db469 100644 --- a/pandas/tests/indexes/timedeltas/test_formats.py +++ b/pandas/tests/indexes/timedeltas/test_formats.py @@ -16,15 +16,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 = ( @@ -76,13 +78,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 868da4329dccf..09764314ea5c4 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) @@ -41,12 +40,13 @@ def test_freq_setter_errors(self): idx = TimedeltaIndex(["0 days", "2 days", "4 days"]) # setting with an incompatible freq + # FIXME: should probably say "48H" rather than "2D"? 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" @@ -68,5 +68,5 @@ def test_freq_view_safe(self): 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..6c3e9d0605d2a 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 cb6dce1e7ad80..650fb68cb2e07 100644 --- a/pandas/tests/indexes/timedeltas/test_setops.py +++ b/pandas/tests/indexes/timedeltas/test_setops.py @@ -90,7 +90,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 72bdc6da47d94..0bcedc256c6d6 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -8,10 +8,7 @@ ) import pandas._testing as tm -from pandas.tseries.offsets import ( - Day, - Second, -) +from pandas.tseries.offsets import Second class TestTimedeltas: @@ -30,7 +27,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) + Timedelta(hours=24) + ) 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 bc6e8aed449f3..bb97a83a550e4 100644 --- a/pandas/tests/indexing/test_partial.py +++ b/pandas/tests/indexing/test_partial.py @@ -335,9 +335,9 @@ def test_partial_setting2(self): np.random.randn(8, 4), index=dates, 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_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index 12e15eab3aa64..2202358e55a31 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -949,7 +949,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) @@ -1838,9 +1838,17 @@ def test_resample_equivalent_offsets(n1, freq1, n2, freq2, k, unit): dti = date_range("19910905 13:00", "19911005 07:00", 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 9246ed4647e63..d7777cb31b8eb 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -848,6 +848,8 @@ def test_resample_with_offset(self, start, end, start_freq, end_freq, offset): if end_freq == "M": # TODO: is non-tick the relevant characteristic? (GH 33815) expected.index = expected.index._with_freq(None) + else: + result.index._data._freq = result.index.freq._maybe_to_hours() tm.assert_series_equal(result, expected) @pytest.mark.parametrize( diff --git a/pandas/tests/resample/test_resample_api.py b/pandas/tests/resample/test_resample_api.py index 6aa59d8b3d164..03a9d8a8ec033 100644 --- a/pandas/tests/resample/test_resample_api.py +++ b/pandas/tests/resample/test_resample_api.py @@ -756,7 +756,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/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index b1fb657bb2051..c779518ea55e9 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -548,7 +548,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 7bd9e5fc5e293..5a4dd7a7959f2 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -90,7 +90,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/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py index f5c9c576abc24..9c24d364841d1 100644 --- a/pandas/tests/scalar/timestamp/test_arithmetic.py +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -40,17 +40,12 @@ def test_overflow_offset_raises(self): stamp = Timestamp("2017-01-13 00:00:00").as_unit("ns") offset_overflow = 20169940 * offsets.Day(1) - msg = ( - "the add operation between " - r"\<-?\d+ \* Days\> and \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} " - "will overflow" - ) lmsg2 = r"Cannot cast -?20169940 days \+?00:00:00 to unit='ns' without overflow" with pytest.raises(OutOfBoundsTimedelta, match=lmsg2): stamp + offset_overflow - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=lmsg2): offset_overflow + stamp with pytest.raises(OutOfBoundsTimedelta, match=lmsg2): @@ -68,7 +63,7 @@ def test_overflow_offset_raises(self): with pytest.raises(OutOfBoundsTimedelta, match=lmsg3): stamp + offset_overflow - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=lmsg3): offset_overflow + stamp with pytest.raises(OutOfBoundsTimedelta, match=lmsg3): diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index 0a43db87674af..9ec0e0e2c214f 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -264,7 +264,10 @@ 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 + if freq == "1D": + unit = 24 * 3600 * 10**9 + else: + unit = to_offset(freq).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 e961fdc295c96..6e08cbf6abb51 100644 --- a/pandas/tests/tseries/frequencies/test_freq_code.py +++ b/pandas/tests/tseries/frequencies/test_freq_code.py @@ -56,7 +56,6 @@ def test_get_freq_roundtrip2(freq): ((1.04, "H"), (3744, "S")), ((1, "D"), (1, "D")), ((0.342931, "H"), (1234551600, "U")), - ((1.2345, "D"), (106660800, "L")), ], ) 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 ea4855baa87e1..39965ac7ed7f2 100644 --- a/pandas/tests/tseries/offsets/test_dst.py +++ b/pandas/tests/tseries/offsets/test_dst.py @@ -173,7 +173,7 @@ def test_springforward_singular(self): 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 5139331bebaf7..915d159bd8a4b 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -46,6 +46,7 @@ CustomBusinessMonthBegin, CustomBusinessMonthEnd, DateOffset, + Day, Easter, FY5253Quarter, LastWeekOfMonth, @@ -212,7 +213,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 @@ -442,7 +443,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) @@ -474,7 +475,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 69953955ebbce..22a5127078dd3 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -53,7 +53,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 a596d4a85074e..0b95f26237698 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 27ddbb82f49a9..2ebd86b0e14ca 100644 --- a/pandas/tests/tslibs/test_to_offset.py +++ b/pandas/tests/tslibs/test_to_offset.py @@ -139,7 +139,7 @@ def test_to_offset_leading_plus(freqstr, expected): ({"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)),