Skip to content

Commit

Permalink
API: make Timestamp/Timedelta _as_unit public as_unit (#48819)
Browse files Browse the repository at this point in the history
* API: make Timestamp/Timedelta _as_unit public as_unit

* update test

* update test

* update tests

* fix pyi typo

* fixup

* fixup
  • Loading branch information
jbrockmendel authored Nov 10, 2022
1 parent 72e923e commit f3c46cd
Show file tree
Hide file tree
Showing 27 changed files with 156 additions and 103 deletions.
4 changes: 4 additions & 0 deletions doc/source/reference/arrays.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Properties
Timestamp.second
Timestamp.tz
Timestamp.tzinfo
Timestamp.unit
Timestamp.value
Timestamp.week
Timestamp.weekofyear
Expand All @@ -149,6 +150,7 @@ Methods
.. autosummary::
:toctree: api/

Timestamp.as_unit
Timestamp.astimezone
Timestamp.ceil
Timestamp.combine
Expand Down Expand Up @@ -242,6 +244,7 @@ Properties
Timedelta.nanoseconds
Timedelta.resolution
Timedelta.seconds
Timedelta.unit
Timedelta.value
Timedelta.view

Expand All @@ -250,6 +253,7 @@ Methods
.. autosummary::
:toctree: api/

Timedelta.as_unit
Timedelta.ceil
Timedelta.floor
Timedelta.isoformat
Expand Down
4 changes: 2 additions & 2 deletions pandas/_libs/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ cpdef array_to_datetime(
raise ValueError('Cannot mix tz-aware with '
'tz-naive values')
if isinstance(val, _Timestamp):
iresult[i] = val._as_unit("ns").value
iresult[i] = val.as_unit("ns").value
else:
iresult[i] = pydatetime_to_dt64(val, &dts)
check_dts_bounds(&dts)
Expand Down Expand Up @@ -906,7 +906,7 @@ def array_to_datetime_with_tz(ndarray values, tzinfo tz):
else:
# datetime64, tznaive pydatetime, int, float
ts = ts.tz_localize(tz)
ts = ts._as_unit("ns")
ts = ts.as_unit("ns")
ival = ts.value

# Analogous to: result[i] = ival
Expand Down
1 change: 1 addition & 0 deletions pandas/_libs/tslibs/nattype.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ class NaTType:
__le__: _NatComparison
__gt__: _NatComparison
__ge__: _NatComparison
def as_unit(self, unit: str, round_ok: bool = ...) -> NaTType: ...
16 changes: 16 additions & 0 deletions pandas/_libs/tslibs/nattype.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,22 @@ default 'raise'
def tzinfo(self) -> None:
return None

def as_unit(self, str unit, bint round_ok=True) -> "NaTType":
"""
Convert the underlying int64 representaton to the given unit.

Parameters
----------
unit : {"ns", "us", "ms", "s"}
round_ok : bool, default True
If False and the conversion requires rounding, raise.

Returns
-------
Timestamp
"""
return c_NaT


c_NaT = NaTType() # C-visible
NaT = c_NaT # Python-visible
Expand Down
4 changes: 2 additions & 2 deletions pandas/_libs/tslibs/timedeltas.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,5 @@ class Timedelta(timedelta):
def to_numpy(self) -> np.timedelta64: ...
def view(self, dtype: npt.DTypeLike = ...) -> object: ...
@property
def _unit(self) -> str: ...
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timedelta: ...
def unit(self) -> str: ...
def as_unit(self, unit: str, round_ok: bool = ...) -> Timedelta: ...
21 changes: 19 additions & 2 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ cdef convert_to_timedelta64(object ts, str unit):
elif isinstance(ts, _Timedelta):
# already in the proper format
if ts._creso != NPY_FR_ns:
ts = ts._as_unit("ns").asm8
ts = ts.as_unit("ns").asm8
else:
ts = np.timedelta64(ts.value, "ns")
elif is_timedelta64_object(ts):
Expand Down Expand Up @@ -1081,6 +1081,10 @@ cdef class _Timedelta(timedelta):
# TODO: add nanos/1e9?
return self.days * 24 * 3600 + self.seconds + self.microseconds / 1_000_000

@property
def unit(self) -> str:
return npy_unit_to_abbrev(self._creso)

def __hash__(_Timedelta self):
if self._has_ns():
# Note: this does *not* satisfy the invariance
Expand Down Expand Up @@ -1500,7 +1504,20 @@ cdef class _Timedelta(timedelta):
# exposing as classmethod for testing
return _timedelta_from_value_and_reso(value, reso)

def _as_unit(self, str unit, bint round_ok=True):
def as_unit(self, str unit, bint round_ok=True):
"""
Convert the underlying int64 representaton to the given unit.
Parameters
----------
unit : {"ns", "us", "ms", "s"}
round_ok : bool, default True
If False and the conversion requires rounding, raise.
Returns
-------
Timedelta
"""
dtype = np.dtype(f"m8[{unit}]")
reso = get_unit_from_dtype(dtype)
return self._as_creso(reso, round_ok=round_ok)
Expand Down
4 changes: 2 additions & 2 deletions pandas/_libs/tslibs/timestamps.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,5 @@ class Timestamp(datetime):
@property
def daysinmonth(self) -> int: ...
@property
def _unit(self) -> str: ...
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...
def unit(self) -> str: ...
def as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...
17 changes: 15 additions & 2 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ cdef class _Timestamp(ABCTimestamp):
resolution = MinMaxReso("resolution") # GH#21336, GH#21365

@property
def _unit(self) -> str:
def unit(self) -> str:
"""
The abbreviation associated with self._creso.
"""
Expand Down Expand Up @@ -993,7 +993,20 @@ cdef class _Timestamp(ABCTimestamp):
value = convert_reso(self.value, self._creso, reso, round_ok=round_ok)
return type(self)._from_value_and_reso(value, reso=reso, tz=self.tzinfo)

def _as_unit(self, str unit, bint round_ok=True):
def as_unit(self, str unit, bint round_ok=True):
"""
Convert the underlying int64 representaton to the given unit.
Parameters
----------
unit : {"ns", "us", "ms", "s"}
round_ok : bool, default True
If False and the conversion requires rounding, raise.
Returns
-------
Timestamp
"""
dtype = np.dtype(f"M8[{unit}]")
reso = get_unit_from_dtype(dtype)
try:
Expand Down
20 changes: 10 additions & 10 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ def isin(self, values) -> npt.NDArray[np.bool_]:

if self.dtype.kind in ["m", "M"]:
self = cast("DatetimeArray | TimedeltaArray", self)
values = values._as_unit(self._unit)
values = values.as_unit(self.unit)

try:
self._check_compatible_with(values)
Expand Down Expand Up @@ -1116,7 +1116,7 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:
# i.e. np.datetime64("NaT")
# In this case we specifically interpret NaT as a datetime, not
# the timedelta interpretation we would get by returning self + NaT
result = self._ndarray + NaT.to_datetime64().astype(f"M8[{self._unit}]")
result = self._ndarray + NaT.to_datetime64().astype(f"M8[{self.unit}]")
# Preserve our resolution
return DatetimeArray._simple_new(result, dtype=result.dtype)

Expand All @@ -1128,10 +1128,10 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:
result = checked_add_with_arr(
self.asi8, other_i8, arr_mask=self._isnan, b_mask=o_mask
)
res_values = result.view(f"M8[{self._unit}]")
res_values = result.view(f"M8[{self.unit}]")

dtype = tz_to_dtype(tz=other.tz, unit=self._unit)
res_values = result.view(f"M8[{self._unit}]")
dtype = tz_to_dtype(tz=other.tz, unit=self.unit)
res_values = result.view(f"M8[{self.unit}]")
new_freq = self._get_arithmetic_result_freq(other)
return DatetimeArray._simple_new(res_values, dtype=dtype, freq=new_freq)

Expand Down Expand Up @@ -1191,7 +1191,7 @@ def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray:
res_values = checked_add_with_arr(
self.asi8, -other_i8, arr_mask=self._isnan, b_mask=o_mask
)
res_m8 = res_values.view(f"timedelta64[{self._unit}]")
res_m8 = res_values.view(f"timedelta64[{self.unit}]")

new_freq = self._get_arithmetic_result_freq(other)
return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype, freq=new_freq)
Expand Down Expand Up @@ -1989,13 +1989,13 @@ def _creso(self) -> int:
return get_unit_from_dtype(self._ndarray.dtype)

@cache_readonly
def _unit(self) -> str:
def unit(self) -> str:
# e.g. "ns", "us", "ms"
# error: Argument 1 to "dtype_to_unit" has incompatible type
# "ExtensionDtype"; expected "Union[DatetimeTZDtype, dtype[Any]]"
return dtype_to_unit(self.dtype) # type: ignore[arg-type]

def _as_unit(self: TimelikeOpsT, unit: str) -> TimelikeOpsT:
def as_unit(self: TimelikeOpsT, unit: str) -> TimelikeOpsT:
dtype = np.dtype(f"{self.dtype.kind}8[{unit}]")
new_values = astype_overflowsafe(self._ndarray, dtype, round_ok=True)

Expand All @@ -2017,9 +2017,9 @@ def _ensure_matching_resos(self, other):
if self._creso != other._creso:
# Just as with Timestamp/Timedelta, we cast to the higher resolution
if self._creso < other._creso:
self = self._as_unit(other._unit)
self = self.as_unit(other.unit)
else:
other = other._as_unit(self._unit)
other = other.as_unit(self.unit)
return self, other

# --------------------------------------------------------------
Expand Down
10 changes: 5 additions & 5 deletions pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,9 @@ def _from_sequence_not_strict(
data_unit = np.datetime_data(subarr.dtype)[0]
data_dtype = tz_to_dtype(tz, data_unit)
result = cls._simple_new(subarr, freq=freq, dtype=data_dtype)
if unit is not None and unit != result._unit:
if unit is not None and unit != result.unit:
# If unit was specified in user-passed dtype, cast to it here
result = result._as_unit(unit)
result = result.as_unit(unit)

if inferred_freq is None and freq is not None:
# this condition precludes `freq_infer`
Expand Down Expand Up @@ -843,7 +843,7 @@ def tz_convert(self, tz) -> DatetimeArray:
)

# No conversion since timestamps are all UTC to begin with
dtype = tz_to_dtype(tz, unit=self._unit)
dtype = tz_to_dtype(tz, unit=self.unit)
return self._simple_new(self._ndarray, dtype=dtype, freq=self.freq)

@dtl.ravel_compat
Expand Down Expand Up @@ -1018,8 +1018,8 @@ def tz_localize(
nonexistent=nonexistent,
creso=self._creso,
)
new_dates = new_dates.view(f"M8[{self._unit}]")
dtype = tz_to_dtype(tz, unit=self._unit)
new_dates = new_dates.view(f"M8[{self.unit}]")
dtype = tz_to_dtype(tz, unit=self.unit)

freq = None
if timezones.is_utc(tz) or (len(self) == 1 and not isna(new_dates[0])):
Expand Down
6 changes: 3 additions & 3 deletions pandas/core/arrays/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,10 @@ def _generate_range(cls, start, end, periods, freq, closed=None):
)

if start is not None:
start = Timedelta(start)._as_unit("ns")
start = Timedelta(start).as_unit("ns")

if end is not None:
end = Timedelta(end)._as_unit("ns")
end = Timedelta(end).as_unit("ns")

left_closed, right_closed = validate_endpoints(closed)

Expand All @@ -298,7 +298,7 @@ def _unbox_scalar(self, value) -> np.timedelta64:
if value is NaT:
return np.timedelta64(value.value, "ns")
else:
return value._as_unit(self._unit).asm8
return value.as_unit(self.unit).asm8

def _scalar_from_string(self, value) -> Timedelta | NaTType:
return Timedelta(value)
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/dtypes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ def is_datetime64_ns_dtype(arr_or_dtype) -> bool:
else:
return False
return tipo == DT64NS_DTYPE or (
isinstance(tipo, DatetimeTZDtype) and tipo._unit == "ns"
isinstance(tipo, DatetimeTZDtype) and tipo.unit == "ns"
)


Expand Down
4 changes: 2 additions & 2 deletions pandas/core/dtypes/dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ def na_value(self) -> NaTType:
# error: Signature of "str" incompatible with supertype "PandasExtensionDtype"
@cache_readonly
def str(self) -> str: # type: ignore[override]
return f"|M8[{self._unit}]"
return f"|M8[{self.unit}]"

def __init__(self, unit: str_type | DatetimeTZDtype = "ns", tz=None) -> None:
if isinstance(unit, DatetimeTZDtype):
Expand Down Expand Up @@ -720,7 +720,7 @@ def _creso(self) -> int:
"ms": dtypes.NpyDatetimeUnit.NPY_FR_ms,
"us": dtypes.NpyDatetimeUnit.NPY_FR_us,
"ns": dtypes.NpyDatetimeUnit.NPY_FR_ns,
}[self._unit]
}[self.unit]
return reso.value

@property
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/window/ewm.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def _calculate_deltas(
"""
_times = np.asarray(times.view(np.int64), dtype=np.float64)
# TODO: generalize to non-nano?
_halflife = float(Timedelta(halflife)._as_unit("ns").value)
_halflife = float(Timedelta(halflife).as_unit("ns").value)
return np.diff(_times) / _halflife


Expand Down
20 changes: 10 additions & 10 deletions pandas/tests/arrays/test_datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ def test_add_mismatched_reso_doesnt_downcast(self):
# https://github.com/pandas-dev/pandas/pull/48748#issuecomment-1260181008
td = pd.Timedelta(microseconds=1)
dti = pd.date_range("2016-01-01", periods=3) - td
dta = dti._data._as_unit("us")
dta = dti._data.as_unit("us")

res = dta + td._as_unit("us")
res = dta + td.as_unit("us")
# even though the result is an even number of days
# (so we _could_ downcast to unit="s"), we do not.
assert res._unit == "us"
assert res.unit == "us"

@pytest.mark.parametrize(
"scalar",
Expand All @@ -240,32 +240,32 @@ def test_add_timedeltalike_scalar_mismatched_reso(self, dta_dti, scalar):
exp_reso = max(dta._creso, td._creso)
exp_unit = npy_unit_to_abbrev(exp_reso)

expected = (dti + td)._data._as_unit(exp_unit)
expected = (dti + td)._data.as_unit(exp_unit)
result = dta + scalar
tm.assert_extension_array_equal(result, expected)

result = scalar + dta
tm.assert_extension_array_equal(result, expected)

expected = (dti - td)._data._as_unit(exp_unit)
expected = (dti - td)._data.as_unit(exp_unit)
result = dta - scalar
tm.assert_extension_array_equal(result, expected)

def test_sub_datetimelike_scalar_mismatch(self):
dti = pd.date_range("2016-01-01", periods=3)
dta = dti._data._as_unit("us")
dta = dti._data.as_unit("us")

ts = dta[0]._as_unit("s")
ts = dta[0].as_unit("s")

result = dta - ts
expected = (dti - dti[0])._data._as_unit("us")
expected = (dti - dti[0])._data.as_unit("us")
assert result.dtype == "m8[us]"
tm.assert_extension_array_equal(result, expected)

def test_sub_datetime64_reso_mismatch(self):
dti = pd.date_range("2016-01-01", periods=3)
left = dti._data._as_unit("s")
right = left._as_unit("ms")
left = dti._data.as_unit("s")
right = left.as_unit("ms")

result = left - right
exp_values = np.array([0, 0, 0], dtype="m8[ms]")
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/arrays/test_timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def test_add_pdnat(self, tda):
def test_add_datetimelike_scalar(self, tda, tz_naive_fixture):
ts = pd.Timestamp("2016-01-01", tz=tz_naive_fixture)

expected = tda._as_unit("ns") + ts
expected = tda.as_unit("ns") + ts
res = tda + ts
tm.assert_extension_array_equal(res, expected)
res = ts + tda
Expand Down
Loading

0 comments on commit f3c46cd

Please sign in to comment.