@@ -36,13 +36,15 @@ from pandas._libs.tslibs.np_datetime cimport (
36
36
NPY_DATETIMEUNIT,
37
37
npy_datetimestruct,
38
38
pandas_datetime_to_datetimestruct,
39
+ pydatetime_to_dt64,
39
40
)
40
41
from pandas._libs.tslibs.timezones cimport (
41
42
get_dst_info,
42
43
is_fixed_offset,
43
44
is_tzlocal,
44
45
is_utc,
45
46
is_zoneinfo,
47
+ utc_stdlib,
46
48
)
47
49
48
50
@@ -154,7 +156,7 @@ cdef int64_t tz_localize_to_utc_single(
154
156
# TODO: test with non-nano
155
157
return val
156
158
157
- elif is_tzlocal(tz) or is_zoneinfo(tz) :
159
+ elif is_tzlocal(tz):
158
160
return val - _tz_localize_using_tzinfo_api(val, tz, to_utc = True , creso = creso)
159
161
160
162
elif is_fixed_offset(tz):
@@ -242,29 +244,6 @@ timedelta-like}
242
244
if info.use_utc:
243
245
return vals.copy()
244
246
245
- result = cnp.PyArray_EMPTY(vals.ndim, vals.shape, cnp.NPY_INT64, 0 )
246
-
247
- if info.use_tzlocal:
248
- for i in range (n):
249
- v = vals[i]
250
- if v == NPY_NAT:
251
- result[i] = NPY_NAT
252
- else :
253
- result[i] = v - _tz_localize_using_tzinfo_api(
254
- v, tz, to_utc = True , creso = creso
255
- )
256
- return result.base # to return underlying ndarray
257
-
258
- elif info.use_fixed:
259
- delta = info.delta
260
- for i in range (n):
261
- v = vals[i]
262
- if v == NPY_NAT:
263
- result[i] = NPY_NAT
264
- else :
265
- result[i] = v - delta
266
- return result.base # to return underlying ndarray
267
-
268
247
# silence false-positive compiler warning
269
248
ambiguous_array = np.empty(0 , dtype = bool )
270
249
if isinstance (ambiguous, str ):
@@ -299,11 +278,39 @@ timedelta-like}
299
278
" shift_backwards} or a timedelta object" )
300
279
raise ValueError (msg)
301
280
281
+ result = cnp.PyArray_EMPTY(vals.ndim, vals.shape, cnp.NPY_INT64, 0 )
282
+
283
+ if info.use_tzlocal and not is_zoneinfo(tz):
284
+ for i in range (n):
285
+ v = vals[i]
286
+ if v == NPY_NAT:
287
+ result[i] = NPY_NAT
288
+ else :
289
+ result[i] = v - _tz_localize_using_tzinfo_api(
290
+ v, tz, to_utc = True , creso = creso
291
+ )
292
+ return result.base # to return underlying ndarray
293
+
294
+ elif info.use_fixed:
295
+ delta = info.delta
296
+ for i in range (n):
297
+ v = vals[i]
298
+ if v == NPY_NAT:
299
+ result[i] = NPY_NAT
300
+ else :
301
+ result[i] = v - delta
302
+ return result.base # to return underlying ndarray
303
+
302
304
# Determine whether each date lies left of the DST transition (store in
303
305
# result_a) or right of the DST transition (store in result_b)
304
- result_a, result_b = _get_utc_bounds(
305
- vals, info.tdata, info.ntrans, info.deltas, creso = creso
306
- )
306
+ if is_zoneinfo(tz):
307
+ result_a, result_b = _get_utc_bounds_zoneinfo(
308
+ vals, tz, creso = creso
309
+ )
310
+ else :
311
+ result_a, result_b = _get_utc_bounds(
312
+ vals, info.tdata, info.ntrans, info.deltas, creso = creso
313
+ )
307
314
308
315
# silence false-positive compiler warning
309
316
dst_hours = np.empty(0 , dtype = np.int64)
@@ -391,8 +398,7 @@ timedelta-like}
391
398
return result.base # .base to get underlying ndarray
392
399
393
400
394
- cdef inline Py_ssize_t bisect_right_i8(int64_t * data,
395
- int64_t val, Py_ssize_t n):
401
+ cdef inline Py_ssize_t bisect_right_i8(int64_t * data, int64_t val, Py_ssize_t n):
396
402
# Caller is responsible for checking n > 0
397
403
# This looks very similar to local_search_right in the ndarray.searchsorted
398
404
# implementation.
@@ -483,6 +489,72 @@ cdef _get_utc_bounds(
483
489
return result_a, result_b
484
490
485
491
492
+ cdef _get_utc_bounds_zoneinfo(ndarray vals, tz, NPY_DATETIMEUNIT creso):
493
+ """
494
+ For each point in 'vals', find the UTC time that it corresponds to if
495
+ with fold=0 and fold=1. In non-ambiguous cases, these will match.
496
+
497
+ Parameters
498
+ ----------
499
+ vals : ndarray[int64_t]
500
+ tz : ZoneInfo
501
+ creso : NPY_DATETIMEUNIT
502
+
503
+ Returns
504
+ -------
505
+ ndarray[int64_t]
506
+ ndarray[int64_t]
507
+ """
508
+ cdef:
509
+ Py_ssize_t i, n = vals.size
510
+ npy_datetimestruct dts
511
+ datetime dt, rt, left, right, aware, as_utc
512
+ int64_t val, pps = periods_per_second(creso)
513
+ ndarray result_a, result_b
514
+
515
+ result_a = cnp.PyArray_EMPTY(vals.ndim, vals.shape, cnp.NPY_INT64, 0 )
516
+ result_b = cnp.PyArray_EMPTY(vals.ndim, vals.shape, cnp.NPY_INT64, 0 )
517
+
518
+ for i in range (n):
519
+ val = vals[i]
520
+ if val == NPY_NAT:
521
+ result_a[i] = NPY_NAT
522
+ result_b[i] = NPY_NAT
523
+ continue
524
+
525
+ pandas_datetime_to_datetimestruct(val, creso, & dts)
526
+ # casting to pydatetime drops nanoseconds etc, which we will
527
+ # need to re-add later as 'extra''
528
+ extra = (dts.ps // 1000 ) * (pps // 1 _000_000_000)
529
+
530
+ dt = datetime_new(dts.year, dts.month, dts.day, dts.hour,
531
+ dts.min, dts.sec, dts.us, None )
532
+
533
+ aware = dt.replace(tzinfo = tz)
534
+ as_utc = aware.astimezone(utc_stdlib)
535
+ rt = as_utc.astimezone(tz)
536
+ if aware != rt:
537
+ # AFAICT this means that 'aware' is non-existent
538
+ # TODO: better way to check this?
539
+ # mail.python.org/archives/list/[email protected] /
540
+ # thread/57Y3IQAASJOKHX4D27W463XTZIS2NR3M/
541
+ result_a[i] = NPY_NAT
542
+ else :
543
+ left = as_utc.replace(tzinfo = None )
544
+ result_a[i] = pydatetime_to_dt64(left, & dts, creso) + extra
545
+
546
+ aware = dt.replace(fold = 1 , tzinfo = tz)
547
+ as_utc = aware.astimezone(utc_stdlib)
548
+ rt = as_utc.astimezone(tz)
549
+ if aware != rt:
550
+ result_b[i] = NPY_NAT
551
+ else :
552
+ right = as_utc.replace(tzinfo = None )
553
+ result_b[i] = pydatetime_to_dt64(right, & dts, creso) + extra
554
+
555
+ return result_a, result_b
556
+
557
+
486
558
@ cython.boundscheck (False )
487
559
cdef ndarray[int64_t] _get_dst_hours(
488
560
# vals, creso only needed here to potential render an exception message
0 commit comments