Skip to content

API: make Timestamp/Timedelta _as_unit public as_unit #48819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Nov 10, 2022
Merged
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 @@ -1210,6 +1210,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add an Examples section too? (Hoping to have all public APIs have examples in the near future)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would make sense, but first i think we should determine if we want unit to somehow show up in the repr

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, frequency didn't end up in the repr before right? (just as a comparison note)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 1.5.x freq is in the repr when it is non-None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be helpful then to have the unit resolution show up in the repr then (guess it would be an API change)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yah that's on my todo list, just not a priority ATM

-------
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 @@ -342,7 +342,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 @@ -1058,6 +1058,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 @@ -1477,7 +1481,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 @@ -827,7 +827,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 @@ -1127,7 +1127,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 @@ -1139,10 +1139,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 @@ -1202,7 +1202,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 @@ -2003,13 +2003,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 @@ -2031,9 +2031,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 @@ -844,7 +844,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 @@ -1019,8 +1019,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