Skip to content

Commit 7c68ecc

Browse files
authored
ENH: support non-nano in Localizer (#47246)
* ENH: support non-nano in Localizer * update setup.py
1 parent 6317d81 commit 7c68ecc

File tree

7 files changed

+82
-30
lines changed

7 files changed

+82
-30
lines changed

pandas/_libs/tslibs/conversion.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ cdef _TSObject _create_tsobject_tz_using_offset(npy_datetimestruct dts,
401401
return obj
402402

403403
cdef:
404-
Localizer info = Localizer(tz)
404+
Localizer info = Localizer(tz, NPY_FR_ns)
405405

406406
# Infer fold from offset-adjusted obj.value
407407
# see PEP 495 https://www.python.org/dev/peps/pep-0495/#the-fold-attribute
@@ -569,7 +569,7 @@ cdef inline void _localize_tso(_TSObject obj, tzinfo tz):
569569
cdef:
570570
int64_t local_val
571571
Py_ssize_t outpos = -1
572-
Localizer info = Localizer(tz)
572+
Localizer info = Localizer(tz, NPY_FR_ns)
573573

574574
assert obj.tzinfo is None
575575

pandas/_libs/tslibs/timestamps.pyx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,10 +2001,11 @@ default 'raise'
20012001
ambiguous = [ambiguous]
20022002
value = tz_localize_to_utc_single(self.value, tz,
20032003
ambiguous=ambiguous,
2004-
nonexistent=nonexistent)
2004+
nonexistent=nonexistent,
2005+
reso=self._reso)
20052006
elif tz is None:
20062007
# reset tz
2007-
value = tz_convert_from_utc_single(self.value, self.tz)
2008+
value = tz_convert_from_utc_single(self.value, self.tz, reso=self._reso)
20082009

20092010
else:
20102011
raise TypeError(
@@ -2152,7 +2153,7 @@ default 'raise'
21522153
fold = self.fold
21532154

21542155
if tzobj is not None:
2155-
value = tz_convert_from_utc_single(value, tzobj)
2156+
value = tz_convert_from_utc_single(value, tzobj, reso=self._reso)
21562157

21572158
# setup components
21582159
dt64_to_dtstruct(value, &dts)

pandas/_libs/tslibs/tzconversion.pxd

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ from numpy cimport (
55
ndarray,
66
)
77

8+
from pandas._libs.tslibs.np_datetime cimport NPY_DATETIMEUNIT
9+
810

911
cpdef int64_t tz_convert_from_utc_single(
10-
int64_t utc_val, tzinfo tz
12+
int64_t utc_val, tzinfo tz, NPY_DATETIMEUNIT reso=*
1113
) except? -1
1214
cdef int64_t tz_localize_to_utc_single(
13-
int64_t val, tzinfo tz, object ambiguous=*, object nonexistent=*
15+
int64_t val,
16+
tzinfo tz,
17+
object ambiguous=*,
18+
object nonexistent=*,
19+
NPY_DATETIMEUNIT reso=*,
1420
) except? -1
1521

1622

1723
cdef class Localizer:
1824
cdef:
1925
tzinfo tz
26+
NPY_DATETIMEUNIT _reso
2027
bint use_utc, use_fixed, use_tzlocal, use_dst, use_pytz
2128
ndarray trans
2229
Py_ssize_t ntrans

pandas/_libs/tslibs/tzconversion.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import numpy as np
99
from pandas._typing import npt
1010

1111
# tz_convert_from_utc_single exposed for testing
12-
def tz_convert_from_utc_single(val: np.int64, tz: tzinfo) -> np.int64: ...
12+
def tz_convert_from_utc_single(
13+
val: np.int64, tz: tzinfo, reso: int = ...
14+
) -> np.int64: ...
1315
def tz_localize_to_utc(
1416
vals: npt.NDArray[np.int64],
1517
tz: tzinfo | None,
1618
ambiguous: str | bool | Iterable[bool] | None = ...,
1719
nonexistent: str | timedelta | np.timedelta64 | None = ...,
20+
reso: int = ..., # NPY_DATETIMEUNIT
1821
) -> npt.NDArray[np.int64]: ...

pandas/_libs/tslibs/tzconversion.pyx

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ from pandas._libs.tslibs.ccalendar cimport (
3131
DAY_NANOS,
3232
HOUR_NANOS,
3333
)
34+
from pandas._libs.tslibs.dtypes cimport periods_per_second
3435
from pandas._libs.tslibs.nattype cimport NPY_NAT
3536
from pandas._libs.tslibs.np_datetime cimport (
36-
dt64_to_dtstruct,
37+
NPY_DATETIMEUNIT,
3738
npy_datetimestruct,
39+
pandas_datetime_to_datetimestruct,
3840
)
3941
from pandas._libs.tslibs.timezones cimport (
4042
get_dst_info,
@@ -54,6 +56,7 @@ cdef const int64_t[::1] _deltas_placeholder = np.array([], dtype=np.int64)
5456
cdef class Localizer:
5557
# cdef:
5658
# tzinfo tz
59+
# NPY_DATETIMEUNIT _reso
5760
# bint use_utc, use_fixed, use_tzlocal, use_dst, use_pytz
5861
# ndarray trans
5962
# Py_ssize_t ntrans
@@ -63,8 +66,9 @@ cdef class Localizer:
6366

6467
@cython.initializedcheck(False)
6568
@cython.boundscheck(False)
66-
def __cinit__(self, tzinfo tz):
69+
def __cinit__(self, tzinfo tz, NPY_DATETIMEUNIT reso):
6770
self.tz = tz
71+
self._reso = reso
6872
self.use_utc = self.use_tzlocal = self.use_fixed = False
6973
self.use_dst = self.use_pytz = False
7074
self.ntrans = -1 # placeholder
@@ -80,19 +84,35 @@ cdef class Localizer:
8084

8185
else:
8286
trans, deltas, typ = get_dst_info(tz)
87+
if reso != NPY_DATETIMEUNIT.NPY_FR_ns:
88+
# NB: using floordiv here is implicitly assuming we will
89+
# never see trans or deltas that are not an integer number
90+
# of seconds.
91+
if reso == NPY_DATETIMEUNIT.NPY_FR_us:
92+
trans = trans // 1_000
93+
deltas = deltas // 1_000
94+
elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
95+
trans = trans // 1_000_000
96+
deltas = deltas // 1_000_000
97+
elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
98+
trans = trans // 1_000_000_000
99+
deltas = deltas // 1_000_000_000
100+
else:
101+
raise NotImplementedError(reso)
102+
83103
self.trans = trans
84104
self.ntrans = self.trans.shape[0]
85105
self.deltas = deltas
86106

87107
if typ != "pytz" and typ != "dateutil":
88108
# static/fixed; in this case we know that len(delta) == 1
89109
self.use_fixed = True
90-
self.delta = self.deltas[0]
110+
self.delta = deltas[0]
91111
else:
92112
self.use_dst = True
93113
if typ == "pytz":
94114
self.use_pytz = True
95-
self.tdata = <int64_t*>cnp.PyArray_DATA(self.trans)
115+
self.tdata = <int64_t*>cnp.PyArray_DATA(trans)
96116

97117
@cython.boundscheck(False)
98118
cdef inline int64_t utc_val_to_local_val(
@@ -102,7 +122,7 @@ cdef class Localizer:
102122
return utc_val
103123
elif self.use_tzlocal:
104124
return utc_val + _tz_localize_using_tzinfo_api(
105-
utc_val, self.tz, to_utc=False, fold=fold
125+
utc_val, self.tz, to_utc=False, reso=self._reso, fold=fold
106126
)
107127
elif self.use_fixed:
108128
return utc_val + self.delta
@@ -117,7 +137,11 @@ cdef class Localizer:
117137

118138

119139
cdef int64_t tz_localize_to_utc_single(
120-
int64_t val, tzinfo tz, object ambiguous=None, object nonexistent=None,
140+
int64_t val,
141+
tzinfo tz,
142+
object ambiguous=None,
143+
object nonexistent=None,
144+
NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns,
121145
) except? -1:
122146
"""See tz_localize_to_utc.__doc__"""
123147
cdef:
@@ -131,7 +155,7 @@ cdef int64_t tz_localize_to_utc_single(
131155
return val
132156

133157
elif is_tzlocal(tz) or is_zoneinfo(tz):
134-
return val - _tz_localize_using_tzinfo_api(val, tz, to_utc=True)
158+
return val - _tz_localize_using_tzinfo_api(val, tz, to_utc=True, reso=reso)
135159

136160
elif is_fixed_offset(tz):
137161
_, deltas, _ = get_dst_info(tz)
@@ -144,13 +168,19 @@ cdef int64_t tz_localize_to_utc_single(
144168
tz,
145169
ambiguous=ambiguous,
146170
nonexistent=nonexistent,
171+
reso=reso,
147172
)[0]
148173

149174

150175
@cython.boundscheck(False)
151176
@cython.wraparound(False)
152-
def tz_localize_to_utc(ndarray[int64_t] vals, tzinfo tz, object ambiguous=None,
153-
object nonexistent=None):
177+
def tz_localize_to_utc(
178+
ndarray[int64_t] vals,
179+
tzinfo tz,
180+
object ambiguous=None,
181+
object nonexistent=None,
182+
NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns,
183+
):
154184
"""
155185
Localize tzinfo-naive i8 to given time zone (using pytz). If
156186
there are ambiguities in the values, raise AmbiguousTimeError.
@@ -177,6 +207,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, tzinfo tz, object ambiguous=None,
177207
nonexistent : {None, "NaT", "shift_forward", "shift_backward", "raise", \
178208
timedelta-like}
179209
How to handle non-existent times when converting wall times to UTC
210+
reso : NPY_DATETIMEUNIT, default NPY_FR_ns
180211
181212
Returns
182213
-------
@@ -196,7 +227,7 @@ timedelta-like}
196227
bint shift_forward = False, shift_backward = False
197228
bint fill_nonexist = False
198229
str stamp
199-
Localizer info = Localizer(tz)
230+
Localizer info = Localizer(tz, reso=reso)
200231

201232
# Vectorized version of DstTzInfo.localize
202233
if info.use_utc:
@@ -210,7 +241,7 @@ timedelta-like}
210241
if v == NPY_NAT:
211242
result[i] = NPY_NAT
212243
else:
213-
result[i] = v - _tz_localize_using_tzinfo_api(v, tz, to_utc=True)
244+
result[i] = v - _tz_localize_using_tzinfo_api(v, tz, to_utc=True, reso=reso)
214245
return result.base # to return underlying ndarray
215246

216247
elif info.use_fixed:
@@ -512,7 +543,9 @@ cdef ndarray[int64_t] _get_dst_hours(
512543
# ----------------------------------------------------------------------
513544
# Timezone Conversion
514545

515-
cpdef int64_t tz_convert_from_utc_single(int64_t utc_val, tzinfo tz) except? -1:
546+
cpdef int64_t tz_convert_from_utc_single(
547+
int64_t utc_val, tzinfo tz, NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns
548+
) except? -1:
516549
"""
517550
Convert the val (in i8) from UTC to tz
518551
@@ -522,13 +555,14 @@ cpdef int64_t tz_convert_from_utc_single(int64_t utc_val, tzinfo tz) except? -1:
522555
----------
523556
utc_val : int64
524557
tz : tzinfo
558+
reso : NPY_DATETIMEUNIT, default NPY_FR_ns
525559
526560
Returns
527561
-------
528562
converted: int64
529563
"""
530564
cdef:
531-
Localizer info = Localizer(tz)
565+
Localizer info = Localizer(tz, reso=reso)
532566
Py_ssize_t pos
533567

534568
# Note: caller is responsible for ensuring utc_val != NPY_NAT
@@ -538,7 +572,11 @@ cpdef int64_t tz_convert_from_utc_single(int64_t utc_val, tzinfo tz) except? -1:
538572
# OSError may be thrown by tzlocal on windows at or close to 1970-01-01
539573
# see https://github.com/pandas-dev/pandas/pull/37591#issuecomment-720628241
540574
cdef int64_t _tz_localize_using_tzinfo_api(
541-
int64_t val, tzinfo tz, bint to_utc=True, bint* fold=NULL
575+
int64_t val,
576+
tzinfo tz,
577+
bint to_utc=True,
578+
NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns,
579+
bint* fold=NULL,
542580
) except? -1:
543581
"""
544582
Convert the i8 representation of a datetime from a general-case timezone to
@@ -552,6 +590,7 @@ cdef int64_t _tz_localize_using_tzinfo_api(
552590
tz : tzinfo
553591
to_utc : bint
554592
True if converting _to_ UTC, False if going the other direction.
593+
reso : NPY_DATETIMEUNIT
555594
fold : bint*, default NULL
556595
pointer to fold: whether datetime ends up in a fold or not
557596
after adjustment.
@@ -571,8 +610,9 @@ cdef int64_t _tz_localize_using_tzinfo_api(
571610
datetime dt
572611
int64_t delta
573612
timedelta td
613+
int64_t pps = periods_per_second(reso)
574614

575-
dt64_to_dtstruct(val, &dts)
615+
pandas_datetime_to_datetimestruct(val, reso, &dts)
576616

577617
# datetime_new is cython-optimized constructor
578618
if not to_utc:
@@ -590,7 +630,7 @@ cdef int64_t _tz_localize_using_tzinfo_api(
590630
dts.min, dts.sec, dts.us, None)
591631

592632
td = tz.utcoffset(dt)
593-
delta = int(td.total_seconds() * 1_000_000_000)
633+
delta = int(td.total_seconds() * pps)
594634
return delta
595635

596636

pandas/_libs/tslibs/vectorized.pyx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def tz_convert_from_utc(ndarray stamps, tzinfo tz):
5858
ndarray[int64]
5959
"""
6060
cdef:
61-
Localizer info = Localizer(tz)
61+
Localizer info = Localizer(tz, reso=NPY_FR_ns)
6262
int64_t utc_val, local_val
6363
Py_ssize_t pos, i, n = stamps.size
6464

@@ -130,7 +130,7 @@ def ints_to_pydatetime(
130130
ndarray[object] of type specified by box
131131
"""
132132
cdef:
133-
Localizer info = Localizer(tz)
133+
Localizer info = Localizer(tz, reso=NPY_FR_ns)
134134
int64_t utc_val, local_val
135135
Py_ssize_t i, n = stamps.size
136136
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
@@ -229,7 +229,7 @@ cdef inline c_Resolution _reso_stamp(npy_datetimestruct *dts):
229229
def get_resolution(ndarray stamps, tzinfo tz=None) -> Resolution:
230230
# stamps is int64_t, any ndim
231231
cdef:
232-
Localizer info = Localizer(tz)
232+
Localizer info = Localizer(tz, reso=NPY_FR_ns)
233233
int64_t utc_val, local_val
234234
Py_ssize_t i, n = stamps.size
235235
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
@@ -281,7 +281,7 @@ cpdef ndarray normalize_i8_timestamps(ndarray stamps, tzinfo tz, NPY_DATETIMEUNI
281281
result : int64 ndarray of converted of normalized nanosecond timestamps
282282
"""
283283
cdef:
284-
Localizer info = Localizer(tz)
284+
Localizer info = Localizer(tz, reso=reso)
285285
int64_t utc_val, local_val, res_val
286286
Py_ssize_t i, n = stamps.size
287287
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
@@ -328,7 +328,7 @@ def is_date_array_normalized(ndarray stamps, tzinfo tz, NPY_DATETIMEUNIT reso) -
328328
is_normalized : bool True if all stamps are normalized
329329
"""
330330
cdef:
331-
Localizer info = Localizer(tz)
331+
Localizer info = Localizer(tz, reso=reso)
332332
int64_t utc_val, local_val
333333
Py_ssize_t i, n = stamps.size
334334
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
@@ -357,7 +357,7 @@ def is_date_array_normalized(ndarray stamps, tzinfo tz, NPY_DATETIMEUNIT reso) -
357357
def dt64arr_to_periodarr(ndarray stamps, int freq, tzinfo tz):
358358
# stamps is int64_t, arbitrary ndim
359359
cdef:
360-
Localizer info = Localizer(tz)
360+
Localizer info = Localizer(tz, reso=NPY_FR_ns)
361361
Py_ssize_t i, n = stamps.size
362362
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
363363
int64_t utc_val, local_val, res_val

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
549549
"_libs.tslibs.tzconversion": {
550550
"pyxfile": "_libs/tslibs/tzconversion",
551551
"depends": tseries_depends,
552+
"sources": ["pandas/_libs/tslibs/src/datetime/np_datetime.c"],
552553
},
553554
"_libs.tslibs.vectorized": {"pyxfile": "_libs/tslibs/vectorized"},
554555
"_libs.testing": {"pyxfile": "_libs/testing"},

0 commit comments

Comments
 (0)