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 @@ -247,6 +249,7 @@ Properties
Timedelta.nanoseconds
Timedelta.resolution
Timedelta.seconds
Timedelta.unit
Timedelta.value
Timedelta.view

Expand All @@ -255,6 +258,7 @@ Methods
.. autosummary::
:toctree: api/

Timedelta.as_unit
Timedelta.ceil
Timedelta.floor
Timedelta.isoformat
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, round_ok: bool = ...) -> NaTType: ...
17 changes: 17 additions & 0 deletions pandas/_libs/tslibs/nattype.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,7 @@ default 'raise'
NaT
""",
)

@property
def tz(self) -> None:
return None
Expand All @@ -1210,6 +1211,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: 3 additions & 1 deletion pandas/_libs/tslibs/timedeltas.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,6 @@ class Timedelta(timedelta):
def freq(self) -> None: ...
@property
def is_populated(self) -> bool: ...
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timedelta: ...
def as_unit(self, unit: str, round_ok: bool = ...) -> Timedelta: ...
@property
def unit(self) -> str: ...
19 changes: 18 additions & 1 deletion pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,10 @@ cdef class _Timedelta(timedelta):
)
return self._is_populated

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

def __hash__(_Timedelta self):
if self._has_ns():
# Note: this does *not* satisfy the invariance
Expand Down Expand Up @@ -1534,7 +1538,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)
try:
Expand Down
4 changes: 3 additions & 1 deletion pandas/_libs/tslibs/timestamps.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,6 @@ class Timestamp(datetime):
def days_in_month(self) -> int: ...
@property
def daysinmonth(self) -> int: ...
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...
def as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...
@property
def unit(self) -> str: ...
19 changes: 18 additions & 1 deletion pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ cdef class _Timestamp(ABCTimestamp):
)
return self._freq

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

# -----------------------------------------------------------------
# Constructors

Expand Down Expand Up @@ -1113,7 +1117,20 @@ cdef class _Timestamp(ABCTimestamp):
value = convert_reso(self.value, self._reso, 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
2 changes: 1 addition & 1 deletion pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:
# Just as with Timestamp/Timedelta, we cast to the lower resolution
# so long as doing so is lossless.
if self._reso < other._reso:
other = other._as_unit(self._unit, round_ok=False)
other = other.as_unit(self._unit, round_ok=False)
else:
unit = npy_unit_to_abbrev(other._reso)
self = self._as_unit(unit)
Expand Down
4 changes: 2 additions & 2 deletions 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 + ts._as_unit(tda._unit)
expected = tda + ts.as_unit(tda._unit)
res = tda + ts
tm.assert_extension_array_equal(res, expected)
res = ts + tda
Expand All @@ -119,7 +119,7 @@ def test_add_datetimelike_scalar(self, tda, tz_naive_fixture):
# mismatched reso -> check that we don't give an incorrect result
ts + tda

ts = ts._as_unit(tda._unit)
ts = ts.as_unit(tda._unit)

exp_values = tda._ndarray + ts.asm8
expected = (
Expand Down
4 changes: 3 additions & 1 deletion pandas/tests/scalar/test_nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def test_nat_iso_format(get_nat):
@pytest.mark.parametrize(
"klass,expected",
[
(Timestamp, ["freqstr", "normalize", "to_julian_date", "to_period"]),
(Timestamp, ["freqstr", "normalize", "to_julian_date", "to_period", "unit"]),
(
Timedelta,
[
Expand All @@ -200,6 +200,7 @@ def test_nat_iso_format(get_nat):
"resolution_string",
"to_pytimedelta",
"to_timedelta64",
"unit",
"view",
],
),
Expand Down Expand Up @@ -262,6 +263,7 @@ def _get_overlap_public_nat_methods(klass, as_tuple=False):
(
Timestamp,
[
"as_unit",
"astimezone",
"ceil",
"combine",
Expand Down
28 changes: 14 additions & 14 deletions pandas/tests/scalar/timedelta/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,29 @@ class TestAsUnit:
def test_as_unit(self):
td = Timedelta(days=1)

assert td._as_unit("ns") is td
assert td.as_unit("ns") is td

res = td._as_unit("us")
res = td.as_unit("us")
assert res.value == td.value // 1000
assert res._reso == NpyDatetimeUnit.NPY_FR_us.value

rt = res._as_unit("ns")
rt = res.as_unit("ns")
assert rt.value == td.value
assert rt._reso == td._reso

res = td._as_unit("ms")
res = td.as_unit("ms")
assert res.value == td.value // 1_000_000
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value

rt = res._as_unit("ns")
rt = res.as_unit("ns")
assert rt.value == td.value
assert rt._reso == td._reso

res = td._as_unit("s")
res = td.as_unit("s")
assert res.value == td.value // 1_000_000_000
assert res._reso == NpyDatetimeUnit.NPY_FR_s.value

rt = res._as_unit("ns")
rt = res.as_unit("ns")
assert rt.value == td.value
assert rt._reso == td._reso

Expand All @@ -63,15 +63,15 @@ def test_as_unit_overflows(self):

msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow"
with pytest.raises(OutOfBoundsTimedelta, match=msg):
td._as_unit("ns")
td.as_unit("ns")

res = td._as_unit("ms")
res = td.as_unit("ms")
assert res.value == us // 1000
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value

def test_as_unit_rounding(self):
td = Timedelta(microseconds=1500)
res = td._as_unit("ms")
res = td.as_unit("ms")

expected = Timedelta(milliseconds=1)
assert res == expected
Expand All @@ -80,18 +80,18 @@ def test_as_unit_rounding(self):
assert res.value == 1

with pytest.raises(ValueError, match="Cannot losslessly convert units"):
td._as_unit("ms", round_ok=False)
td.as_unit("ms", round_ok=False)

def test_as_unit_non_nano(self):
# case where we are going neither to nor from nano
td = Timedelta(days=1)._as_unit("ms")
td = Timedelta(days=1).as_unit("ms")
assert td.days == 1
assert td.value == 86_400_000
assert td.components.days == 1
assert td._d == 1
assert td.total_seconds() == 86400

res = td._as_unit("us")
res = td.as_unit("us")
assert res.value == 86_400_000_000
assert res.components.days == 1
assert res.components.hours == 0
Expand Down Expand Up @@ -733,7 +733,7 @@ def test_round_sanity(self, val, method):

@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
def test_round_non_nano(self, unit):
td = Timedelta("1 days 02:34:57")._as_unit(unit)
td = Timedelta("1 days 02:34:57").as_unit(unit)

res = td.round("min")
assert res == Timedelta("1 days 02:35:00")
Expand Down
30 changes: 15 additions & 15 deletions pandas/tests/scalar/timestamp/test_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ def test_sub_datetimelike_mismatched_reso(self, ts_tz):
NpyDatetimeUnit.NPY_FR_ms.value: "s",
NpyDatetimeUnit.NPY_FR_s.value: "us",
}[ts._reso]
other = ts._as_unit(unit)
other = ts.as_unit(unit)
assert other._reso != ts._reso

result = ts - other
Expand Down Expand Up @@ -978,7 +978,7 @@ def test_sub_timedeltalike_mismatched_reso(self, ts_tz):
NpyDatetimeUnit.NPY_FR_ms.value: "s",
NpyDatetimeUnit.NPY_FR_s.value: "us",
}[ts._reso]
other = Timedelta(0)._as_unit(unit)
other = Timedelta(0).as_unit(unit)
assert other._reso != ts._reso

result = ts + other
Expand Down Expand Up @@ -1045,29 +1045,29 @@ class TestAsUnit:
def test_as_unit(self):
ts = Timestamp("1970-01-01")

assert ts._as_unit("ns") is ts
assert ts.as_unit("ns") is ts

res = ts._as_unit("us")
res = ts.as_unit("us")
assert res.value == ts.value // 1000
assert res._reso == NpyDatetimeUnit.NPY_FR_us.value

rt = res._as_unit("ns")
rt = res.as_unit("ns")
assert rt.value == ts.value
assert rt._reso == ts._reso

res = ts._as_unit("ms")
res = ts.as_unit("ms")
assert res.value == ts.value // 1_000_000
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value

rt = res._as_unit("ns")
rt = res.as_unit("ns")
assert rt.value == ts.value
assert rt._reso == ts._reso

res = ts._as_unit("s")
res = ts.as_unit("s")
assert res.value == ts.value // 1_000_000_000
assert res._reso == NpyDatetimeUnit.NPY_FR_s.value

rt = res._as_unit("ns")
rt = res.as_unit("ns")
assert rt.value == ts.value
assert rt._reso == ts._reso

Expand All @@ -1078,15 +1078,15 @@ def test_as_unit_overflows(self):

msg = "Cannot cast 2262-04-12 00:00:00 to unit='ns' without overflow"
with pytest.raises(OutOfBoundsDatetime, match=msg):
ts._as_unit("ns")
ts.as_unit("ns")

res = ts._as_unit("ms")
res = ts.as_unit("ms")
assert res.value == us // 1000
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value

def test_as_unit_rounding(self):
ts = Timestamp(1_500_000) # i.e. 1500 microseconds
res = ts._as_unit("ms")
res = ts.as_unit("ms")

expected = Timestamp(1_000_000) # i.e. 1 millisecond
assert res == expected
Expand All @@ -1095,17 +1095,17 @@ def test_as_unit_rounding(self):
assert res.value == 1

with pytest.raises(ValueError, match="Cannot losslessly convert units"):
ts._as_unit("ms", round_ok=False)
ts.as_unit("ms", round_ok=False)

def test_as_unit_non_nano(self):
# case where we are going neither to nor from nano
ts = Timestamp("1970-01-02")._as_unit("ms")
ts = Timestamp("1970-01-02").as_unit("ms")
assert ts.year == 1970
assert ts.month == 1
assert ts.day == 2
assert ts.hour == ts.minute == ts.second == ts.microsecond == ts.nanosecond == 0

res = ts._as_unit("s")
res = ts.as_unit("s")
assert res.value == 24 * 3600
assert res.year == 1970
assert res.month == 1
Expand Down
Loading