Skip to content

Commit b72558c

Browse files
authored
Merge branch 'main' into patch-2
2 parents 3cafdf1 + 259a15c commit b72558c

20 files changed

+257
-88
lines changed

pandas/_libs/tslibs/dtypes.pyi

+16
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,19 @@ class Resolution(Enum):
5656
def get_reso_from_freqstr(cls, freq: str) -> Resolution: ...
5757
@property
5858
def attr_abbrev(self) -> str: ...
59+
60+
class NpyDatetimeUnit(Enum):
61+
NPY_FR_Y: int
62+
NPY_FR_M: int
63+
NPY_FR_W: int
64+
NPY_FR_D: int
65+
NPY_FR_h: int
66+
NPY_FR_m: int
67+
NPY_FR_s: int
68+
NPY_FR_ms: int
69+
NPY_FR_us: int
70+
NPY_FR_ns: int
71+
NPY_FR_ps: int
72+
NPY_FR_fs: int
73+
NPY_FR_as: int
74+
NPY_FR_GENERIC: int

pandas/_libs/tslibs/dtypes.pyx

+20
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,26 @@ class Resolution(Enum):
257257
return cls.from_attrname(attr_name)
258258

259259

260+
class NpyDatetimeUnit(Enum):
261+
"""
262+
Python-space analogue to NPY_DATETIMEUNIT.
263+
"""
264+
NPY_FR_Y = NPY_DATETIMEUNIT.NPY_FR_Y
265+
NPY_FR_M = NPY_DATETIMEUNIT.NPY_FR_M
266+
NPY_FR_W = NPY_DATETIMEUNIT.NPY_FR_W
267+
NPY_FR_D = NPY_DATETIMEUNIT.NPY_FR_D
268+
NPY_FR_h = NPY_DATETIMEUNIT.NPY_FR_h
269+
NPY_FR_m = NPY_DATETIMEUNIT.NPY_FR_m
270+
NPY_FR_s = NPY_DATETIMEUNIT.NPY_FR_s
271+
NPY_FR_ms = NPY_DATETIMEUNIT.NPY_FR_ms
272+
NPY_FR_us = NPY_DATETIMEUNIT.NPY_FR_us
273+
NPY_FR_ns = NPY_DATETIMEUNIT.NPY_FR_ns
274+
NPY_FR_ps = NPY_DATETIMEUNIT.NPY_FR_ps
275+
NPY_FR_fs = NPY_DATETIMEUNIT.NPY_FR_fs
276+
NPY_FR_as = NPY_DATETIMEUNIT.NPY_FR_as
277+
NPY_FR_GENERIC = NPY_DATETIMEUNIT.NPY_FR_GENERIC
278+
279+
260280
cdef str npy_unit_to_abbrev(NPY_DATETIMEUNIT unit):
261281
if unit == NPY_DATETIMEUNIT.NPY_FR_ns or unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
262282
# generic -> default to nanoseconds

pandas/_libs/tslibs/offsets.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def to_offset(freq: None) -> None: ...
108108
def to_offset(freq: timedelta | BaseOffset | str) -> BaseOffset: ...
109109

110110
class Tick(SingleConstructorOffset):
111+
_reso: int
111112
def __init__(self, n: int = ..., normalize: bool = ...) -> None: ...
112113
@property
113114
def delta(self) -> Timedelta: ...

pandas/_libs/tslibs/offsets.pyx

+7
Original file line numberDiff line numberDiff line change
@@ -968,42 +968,49 @@ cdef class Day(Tick):
968968
_nanos_inc = 24 * 3600 * 1_000_000_000
969969
_prefix = "D"
970970
_period_dtype_code = PeriodDtypeCode.D
971+
_reso = NPY_DATETIMEUNIT.NPY_FR_D
971972

972973

973974
cdef class Hour(Tick):
974975
_nanos_inc = 3600 * 1_000_000_000
975976
_prefix = "H"
976977
_period_dtype_code = PeriodDtypeCode.H
978+
_reso = NPY_DATETIMEUNIT.NPY_FR_h
977979

978980

979981
cdef class Minute(Tick):
980982
_nanos_inc = 60 * 1_000_000_000
981983
_prefix = "T"
982984
_period_dtype_code = PeriodDtypeCode.T
985+
_reso = NPY_DATETIMEUNIT.NPY_FR_m
983986

984987

985988
cdef class Second(Tick):
986989
_nanos_inc = 1_000_000_000
987990
_prefix = "S"
988991
_period_dtype_code = PeriodDtypeCode.S
992+
_reso = NPY_DATETIMEUNIT.NPY_FR_s
989993

990994

991995
cdef class Milli(Tick):
992996
_nanos_inc = 1_000_000
993997
_prefix = "L"
994998
_period_dtype_code = PeriodDtypeCode.L
999+
_reso = NPY_DATETIMEUNIT.NPY_FR_ms
9951000

9961001

9971002
cdef class Micro(Tick):
9981003
_nanos_inc = 1000
9991004
_prefix = "U"
10001005
_period_dtype_code = PeriodDtypeCode.U
1006+
_reso = NPY_DATETIMEUNIT.NPY_FR_us
10011007

10021008

10031009
cdef class Nano(Tick):
10041010
_nanos_inc = 1
10051011
_prefix = "N"
10061012
_period_dtype_code = PeriodDtypeCode.N
1013+
_reso = NPY_DATETIMEUNIT.NPY_FR_ns
10071014

10081015

10091016
def delta_to_tick(delta: timedelta) -> Tick:

pandas/_libs/tslibs/period.pyx

+10-7
Original file line numberDiff line numberDiff line change
@@ -1680,14 +1680,17 @@ cdef class _Period(PeriodMixin):
16801680

16811681
def _add_timedeltalike_scalar(self, other) -> "Period":
16821682
cdef:
1683-
int64_t nanos, base_nanos
1683+
int64_t inc
16841684

16851685
if is_tick_object(self.freq):
1686-
nanos = delta_to_nanoseconds(other)
1687-
base_nanos = self.freq.base.nanos
1688-
if nanos % base_nanos == 0:
1689-
ordinal = self.ordinal + (nanos // base_nanos)
1690-
return Period(ordinal=ordinal, freq=self.freq)
1686+
try:
1687+
inc = delta_to_nanoseconds(other, reso=self.freq._reso, round_ok=False)
1688+
except ValueError as err:
1689+
raise IncompatibleFrequency("Input cannot be converted to "
1690+
f"Period(freq={self.freqstr})") from err
1691+
# TODO: overflow-check here
1692+
ordinal = self.ordinal + inc
1693+
return Period(ordinal=ordinal, freq=self.freq)
16911694
raise IncompatibleFrequency("Input cannot be converted to "
16921695
f"Period(freq={self.freqstr})")
16931696

@@ -2253,7 +2256,7 @@ cdef class _Period(PeriodMixin):
22532256
@property
22542257
def daysinmonth(self) -> int:
22552258
"""
2256-
Get the total number of days of the month that the Period falls in.
2259+
Get the total number of days of the month that this period falls on.
22572260

22582261
Returns
22592262
-------

pandas/_libs/tslibs/timedeltas.pxd

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ from .np_datetime cimport NPY_DATETIMEUNIT
55

66

77
# Exposed for tslib, not intended for outside use.
8-
cpdef int64_t delta_to_nanoseconds(delta) except? -1
8+
cpdef int64_t delta_to_nanoseconds(
9+
delta, NPY_DATETIMEUNIT reso=*, bint round_ok=*, bint allow_year_month=*
10+
) except? -1
911
cdef convert_to_timedelta64(object ts, str unit)
1012
cdef bint is_any_td_scalar(object obj)
1113

pandas/_libs/tslibs/timedeltas.pyi

+6-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ def array_to_timedelta64(
7272
errors: str = ...,
7373
) -> np.ndarray: ... # np.ndarray[m8ns]
7474
def parse_timedelta_unit(unit: str | None) -> UnitChoices: ...
75-
def delta_to_nanoseconds(delta: np.timedelta64 | timedelta | Tick) -> int: ...
75+
def delta_to_nanoseconds(
76+
delta: np.timedelta64 | timedelta | Tick,
77+
reso: int = ..., # NPY_DATETIMEUNIT
78+
round_ok: bool = ...,
79+
allow_year_month: bool = ...,
80+
) -> int: ...
7681

7782
class Timedelta(timedelta):
7883
min: ClassVar[Timedelta]

pandas/_libs/tslibs/timedeltas.pyx

+62-13
Original file line numberDiff line numberDiff line change
@@ -201,28 +201,76 @@ def ints_to_pytimedelta(ndarray m8values, box=False):
201201

202202
# ----------------------------------------------------------------------
203203

204-
cpdef int64_t delta_to_nanoseconds(delta) except? -1:
205-
if is_tick_object(delta):
206-
return delta.nanos
207-
if isinstance(delta, _Timedelta):
208-
if delta._reso == NPY_FR_ns:
209-
return delta.value
210-
raise NotImplementedError(delta._reso)
211204

212-
if is_timedelta64_object(delta):
213-
return get_timedelta64_value(ensure_td64ns(delta))
205+
cpdef int64_t delta_to_nanoseconds(
206+
delta,
207+
NPY_DATETIMEUNIT reso=NPY_FR_ns,
208+
bint round_ok=True,
209+
bint allow_year_month=False,
210+
) except? -1:
211+
cdef:
212+
_Timedelta td
213+
NPY_DATETIMEUNIT in_reso
214+
int64_t n
215+
216+
if is_tick_object(delta):
217+
n = delta.n
218+
in_reso = delta._reso
219+
if in_reso == reso:
220+
return n
221+
else:
222+
td = Timedelta._from_value_and_reso(delta.n, reso=in_reso)
223+
224+
elif isinstance(delta, _Timedelta):
225+
td = delta
226+
n = delta.value
227+
in_reso = delta._reso
228+
if in_reso == reso:
229+
return n
230+
231+
elif is_timedelta64_object(delta):
232+
in_reso = get_datetime64_unit(delta)
233+
n = get_timedelta64_value(delta)
234+
if in_reso == reso:
235+
return n
236+
else:
237+
# _from_value_and_reso does not support Year, Month, or unit-less,
238+
# so we have special handling if speciifed
239+
try:
240+
td = Timedelta._from_value_and_reso(n, reso=in_reso)
241+
except NotImplementedError:
242+
if allow_year_month:
243+
td64 = ensure_td64ns(delta)
244+
return delta_to_nanoseconds(td64, reso=reso)
245+
else:
246+
raise
214247

215-
if PyDelta_Check(delta):
248+
elif PyDelta_Check(delta):
249+
in_reso = NPY_DATETIMEUNIT.NPY_FR_us
216250
try:
217-
return (
251+
n = (
218252
delta.days * 24 * 3600 * 1_000_000
219253
+ delta.seconds * 1_000_000
220254
+ delta.microseconds
221-
) * 1000
255+
)
222256
except OverflowError as err:
223257
raise OutOfBoundsTimedelta(*err.args) from err
224258

225-
raise TypeError(type(delta))
259+
if in_reso == reso:
260+
return n
261+
else:
262+
td = Timedelta._from_value_and_reso(n, reso=in_reso)
263+
264+
else:
265+
raise TypeError(type(delta))
266+
267+
try:
268+
return td._as_reso(reso, round_ok=round_ok).value
269+
except OverflowError as err:
270+
unit_str = npy_unit_to_abbrev(reso)
271+
raise OutOfBoundsTimedelta(
272+
f"Cannot cast {str(delta)} to unit={unit_str} without overflow."
273+
) from err
226274

227275

228276
@cython.overflowcheck(True)
@@ -1411,6 +1459,7 @@ cdef class _Timedelta(timedelta):
14111459
else:
14121460
mult = get_conversion_factor(self._reso, reso)
14131461
with cython.overflowcheck(True):
1462+
# Note: caller is responsible for re-raising as OutOfBoundsTimedelta
14141463
value = self.value * mult
14151464
return type(self)._from_value_and_reso(value, reso=reso)
14161465

pandas/_libs/tslibs/timestamps.pyx

+11-1
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,17 @@ cdef class _Timestamp(ABCTimestamp):
353353
raise NotImplementedError(self._reso)
354354

355355
if is_any_td_scalar(other):
356-
nanos = delta_to_nanoseconds(other)
356+
if (
357+
is_timedelta64_object(other)
358+
and get_datetime64_unit(other) == NPY_DATETIMEUNIT.NPY_FR_GENERIC
359+
):
360+
# TODO: deprecate allowing this? We only get here
361+
# with test_timedelta_add_timestamp_interval
362+
other = np.timedelta64(other.view("i8"), "ns")
363+
# TODO: disallow round_ok, allow_year_month?
364+
nanos = delta_to_nanoseconds(
365+
other, reso=self._reso, round_ok=True, allow_year_month=True
366+
)
357367
try:
358368
result = type(self)(self.value + nanos, tz=self.tzinfo)
359369
except OverflowError:

pandas/core/arrays/datetimelike.py

+3-22
Original file line numberDiff line numberDiff line change
@@ -1120,28 +1120,9 @@ def _add_timedeltalike_scalar(self, other):
11201120
new_values.fill(iNaT)
11211121
return type(self)(new_values, dtype=self.dtype)
11221122

1123-
# FIXME: this may overflow with non-nano
1124-
inc = delta_to_nanoseconds(other)
1125-
1126-
if not is_period_dtype(self.dtype):
1127-
# FIXME: don't hardcode 7, 8, 9, 10 here
1128-
# TODO: maybe patch delta_to_nanoseconds to take reso?
1129-
1130-
# error: "DatetimeLikeArrayMixin" has no attribute "_reso"
1131-
reso = self._reso # type: ignore[attr-defined]
1132-
if reso == 10:
1133-
pass
1134-
elif reso == 9:
1135-
# microsecond
1136-
inc = inc // 1000
1137-
elif reso == 8:
1138-
# millisecond
1139-
inc = inc // 1_000_000
1140-
elif reso == 7:
1141-
# second
1142-
inc = inc // 1_000_000_000
1143-
else:
1144-
raise NotImplementedError(reso)
1123+
# PeriodArray overrides, so we only get here with DTA/TDA
1124+
# error: "DatetimeLikeArrayMixin" has no attribute "_reso"
1125+
inc = delta_to_nanoseconds(other, reso=self._reso) # type: ignore[attr-defined]
11451126

11461127
new_values = checked_add_with_arr(self.asi8, inc, arr_mask=self._isnan)
11471128
new_values = new_values.view("i8")

pandas/core/arrays/period.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ def _add_offset(self, other: BaseOffset):
782782
self._require_matching_freq(other, base=True)
783783
return self._addsub_int_array_or_scalar(other.n, operator.add)
784784

785+
# TODO: can we de-duplicate with Period._add_timedeltalike_scalar?
785786
def _add_timedeltalike_scalar(self, other):
786787
"""
787788
Parameters
@@ -797,10 +798,15 @@ def _add_timedeltalike_scalar(self, other):
797798
raise raise_on_incompatible(self, other)
798799

799800
if notna(other):
800-
# special handling for np.timedelta64("NaT"), avoid calling
801-
# _check_timedeltalike_freq_compat as that would raise TypeError
802-
other = self._check_timedeltalike_freq_compat(other)
803-
other = np.timedelta64(other, "ns")
801+
# Convert to an integer increment of our own freq, disallowing
802+
# e.g. 30seconds if our freq is minutes.
803+
try:
804+
inc = delta_to_nanoseconds(other, reso=self.freq._reso, round_ok=False)
805+
except ValueError as err:
806+
# "Cannot losslessly convert units"
807+
raise raise_on_incompatible(self, other) from err
808+
809+
return self._addsub_int_array_or_scalar(inc, operator.add)
804810

805811
return super()._add_timedeltalike_scalar(other)
806812

pandas/core/dtypes/dtypes.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -670,14 +670,17 @@ class DatetimeTZDtype(PandasExtensionDtype):
670670

671671
type: type[Timestamp] = Timestamp
672672
kind: str_type = "M"
673-
str = "|M8[ns]"
674673
num = 101
675-
base = np.dtype("M8[ns]")
674+
base = np.dtype("M8[ns]") # TODO: depend on reso?
676675
na_value = NaT
677676
_metadata = ("unit", "tz")
678677
_match = re.compile(r"(datetime64|M8)\[(?P<unit>.+), (?P<tz>.+)\]")
679678
_cache_dtypes: dict[str_type, PandasExtensionDtype] = {}
680679

680+
@cache_readonly
681+
def str(self):
682+
return f"|M8[{self._unit}]"
683+
681684
def __init__(self, unit: str_type | DatetimeTZDtype = "ns", tz=None) -> None:
682685
if isinstance(unit, DatetimeTZDtype):
683686
# error: "str" has no attribute "tz"
@@ -696,8 +699,8 @@ def __init__(self, unit: str_type | DatetimeTZDtype = "ns", tz=None) -> None:
696699
"'DatetimeTZDtype.construct_from_string()' instead."
697700
)
698701
raise ValueError(msg)
699-
else:
700-
raise ValueError("DatetimeTZDtype only supports ns units")
702+
if unit not in ["s", "ms", "us", "ns"]:
703+
raise ValueError("DatetimeTZDtype only supports s, ms, us, ns units")
701704

702705
if tz:
703706
tz = timezones.maybe_get_tz(tz)
@@ -710,6 +713,19 @@ def __init__(self, unit: str_type | DatetimeTZDtype = "ns", tz=None) -> None:
710713
self._unit = unit
711714
self._tz = tz
712715

716+
@cache_readonly
717+
def _reso(self) -> int:
718+
"""
719+
The NPY_DATETIMEUNIT corresponding to this dtype's resolution.
720+
"""
721+
reso = {
722+
"s": dtypes.NpyDatetimeUnit.NPY_FR_s,
723+
"ms": dtypes.NpyDatetimeUnit.NPY_FR_ms,
724+
"us": dtypes.NpyDatetimeUnit.NPY_FR_us,
725+
"ns": dtypes.NpyDatetimeUnit.NPY_FR_ns,
726+
}[self._unit]
727+
return reso.value
728+
713729
@property
714730
def unit(self) -> str_type:
715731
"""

0 commit comments

Comments
 (0)