@@ -27,11 +27,10 @@ from numpy cimport (
27
27
28
28
cnp.import_array()
29
29
30
- from pandas._libs.tslibs.ccalendar cimport (
31
- DAY_NANOS ,
32
- HOUR_NANOS ,
30
+ from pandas._libs.tslibs.dtypes cimport (
31
+ periods_per_day ,
32
+ periods_per_second ,
33
33
)
34
- from pandas._libs.tslibs.dtypes cimport periods_per_second
35
34
from pandas._libs.tslibs.nattype cimport NPY_NAT
36
35
from pandas._libs.tslibs.np_datetime cimport (
37
36
NPY_DATETIMEUNIT,
@@ -153,6 +152,7 @@ cdef int64_t tz_localize_to_utc_single(
153
152
return val
154
153
155
154
elif is_utc(tz) or tz is None :
155
+ # TODO: test with non-nano
156
156
return val
157
157
158
158
elif is_tzlocal(tz) or is_zoneinfo(tz):
@@ -161,6 +161,15 @@ cdef int64_t tz_localize_to_utc_single(
161
161
elif is_fixed_offset(tz):
162
162
_, deltas, _ = get_dst_info(tz)
163
163
delta = deltas[0 ]
164
+ # TODO: de-duplicate with Localizer.__init__
165
+ if reso != NPY_DATETIMEUNIT.NPY_FR_ns:
166
+ if reso == NPY_DATETIMEUNIT.NPY_FR_us:
167
+ delta = delta // 1000
168
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
169
+ delta = delta // 1 _000_000
170
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
171
+ delta = delta // 1 _000_000_000
172
+
164
173
return val - delta
165
174
166
175
else :
@@ -229,6 +238,7 @@ timedelta-like}
229
238
bint fill_nonexist = False
230
239
str stamp
231
240
Localizer info = Localizer(tz, reso = reso)
241
+ int64_t pph = periods_per_day(reso) // 24
232
242
233
243
# Vectorized version of DstTzInfo.localize
234
244
if info.use_utc:
@@ -242,7 +252,9 @@ timedelta-like}
242
252
if v == NPY_NAT:
243
253
result[i] = NPY_NAT
244
254
else :
245
- result[i] = v - _tz_localize_using_tzinfo_api(v, tz, to_utc = True , reso = reso)
255
+ result[i] = v - _tz_localize_using_tzinfo_api(
256
+ v, tz, to_utc = True , reso = reso
257
+ )
246
258
return result.base # to return underlying ndarray
247
259
248
260
elif info.use_fixed:
@@ -283,20 +295,22 @@ timedelta-like}
283
295
shift_backward = True
284
296
elif PyDelta_Check(nonexistent):
285
297
from .timedeltas import delta_to_nanoseconds
286
- shift_delta = delta_to_nanoseconds(nonexistent)
298
+ shift_delta = delta_to_nanoseconds(nonexistent, reso = reso )
287
299
elif nonexistent not in (' raise' , None ):
288
300
msg = (" nonexistent must be one of {'NaT', 'raise', 'shift_forward', "
289
301
" shift_backwards} or a timedelta object" )
290
302
raise ValueError (msg)
291
303
292
304
# Determine whether each date lies left of the DST transition (store in
293
305
# result_a) or right of the DST transition (store in result_b)
294
- result_a, result_b = _get_utc_bounds(vals, info.tdata, info.ntrans, info.deltas)
306
+ result_a, result_b = _get_utc_bounds(
307
+ vals, info.tdata, info.ntrans, info.deltas, reso = reso
308
+ )
295
309
296
310
# silence false-positive compiler warning
297
311
dst_hours = np.empty(0 , dtype = np.int64)
298
312
if infer_dst:
299
- dst_hours = _get_dst_hours(vals, result_a, result_b)
313
+ dst_hours = _get_dst_hours(vals, result_a, result_b, reso = reso )
300
314
301
315
# Pre-compute delta_idx_offset that will be used if we go down non-existent
302
316
# paths.
@@ -316,46 +330,51 @@ timedelta-like}
316
330
left = result_a[i]
317
331
right = result_b[i]
318
332
if val == NPY_NAT:
333
+ # TODO: test with non-nano
319
334
result[i] = val
320
335
elif left != NPY_NAT and right != NPY_NAT:
321
336
if left == right:
337
+ # TODO: test with non-nano
322
338
result[i] = left
323
339
else :
324
340
if infer_dst and dst_hours[i] != NPY_NAT:
341
+ # TODO: test with non-nano
325
342
result[i] = dst_hours[i]
326
343
elif is_dst:
327
344
if ambiguous_array[i]:
328
345
result[i] = left
329
346
else :
330
347
result[i] = right
331
348
elif fill:
349
+ # TODO: test with non-nano; parametrize test_dt_round_tz_ambiguous
332
350
result[i] = NPY_NAT
333
351
else :
334
- stamp = _render_tstamp(val)
352
+ stamp = _render_tstamp(val, reso = reso )
335
353
raise pytz.AmbiguousTimeError(
336
354
f" Cannot infer dst time from {stamp}, try using the "
337
355
" 'ambiguous' argument"
338
356
)
339
357
elif left != NPY_NAT:
340
358
result[i] = left
341
359
elif right != NPY_NAT:
360
+ # TODO: test with non-nano
342
361
result[i] = right
343
362
else :
344
363
# Handle nonexistent times
345
364
if shift_forward or shift_backward or shift_delta != 0 :
346
365
# Shift the nonexistent time to the closest existing time
347
- remaining_mins = val % HOUR_NANOS
366
+ remaining_mins = val % pph
348
367
if shift_delta != 0 :
349
368
# Validate that we don't relocalize on another nonexistent
350
369
# time
351
- if - 1 < shift_delta + remaining_mins < HOUR_NANOS :
370
+ if - 1 < shift_delta + remaining_mins < pph :
352
371
raise ValueError (
353
372
" The provided timedelta will relocalize on a "
354
373
f" nonexistent time: {nonexistent}"
355
374
)
356
375
new_local = val + shift_delta
357
376
elif shift_forward:
358
- new_local = val + (HOUR_NANOS - remaining_mins)
377
+ new_local = val + (pph - remaining_mins)
359
378
else :
360
379
# Subtract 1 since the beginning hour is _inclusive_ of
361
380
# nonexistent times
@@ -368,7 +387,7 @@ timedelta-like}
368
387
elif fill_nonexist:
369
388
result[i] = NPY_NAT
370
389
else :
371
- stamp = _render_tstamp(val)
390
+ stamp = _render_tstamp(val, reso = reso )
372
391
raise pytz.NonExistentTimeError(stamp)
373
392
374
393
return result.base # .base to get underlying ndarray
@@ -404,17 +423,19 @@ cdef inline Py_ssize_t bisect_right_i8(int64_t *data,
404
423
return left
405
424
406
425
407
- cdef inline str _render_tstamp(int64_t val):
426
+ cdef inline str _render_tstamp(int64_t val, NPY_DATETIMEUNIT reso ):
408
427
""" Helper function to render exception messages"""
409
428
from pandas._libs.tslibs.timestamps import Timestamp
410
- return str (Timestamp(val))
429
+ ts = Timestamp._from_value_and_reso(val, reso, None )
430
+ return str (ts)
411
431
412
432
413
433
cdef _get_utc_bounds(
414
434
ndarray vals,
415
435
int64_t* tdata,
416
436
Py_ssize_t ntrans,
417
437
const int64_t[::1 ] deltas,
438
+ NPY_DATETIMEUNIT reso,
418
439
):
419
440
# Determine whether each date lies left of the DST transition (store in
420
441
# result_a) or right of the DST transition (store in result_b)
@@ -424,6 +445,7 @@ cdef _get_utc_bounds(
424
445
Py_ssize_t i, n = vals.size
425
446
int64_t val, v_left, v_right
426
447
Py_ssize_t isl, isr, pos_left, pos_right
448
+ int64_t ppd = periods_per_day(reso)
427
449
428
450
result_a = cnp.PyArray_EMPTY(vals.ndim, vals.shape, cnp.NPY_INT64, 0 )
429
451
result_b = cnp.PyArray_EMPTY(vals.ndim, vals.shape, cnp.NPY_INT64, 0 )
@@ -438,8 +460,8 @@ cdef _get_utc_bounds(
438
460
if val == NPY_NAT:
439
461
continue
440
462
441
- # TODO: be careful of overflow in val-DAY_NANOS
442
- isl = bisect_right_i8(tdata, val - DAY_NANOS , ntrans) - 1
463
+ # TODO: be careful of overflow in val-ppd
464
+ isl = bisect_right_i8(tdata, val - ppd , ntrans) - 1
443
465
if isl < 0 :
444
466
isl = 0
445
467
@@ -449,8 +471,8 @@ cdef _get_utc_bounds(
449
471
if v_left + deltas[pos_left] == val:
450
472
result_a[i] = v_left
451
473
452
- # TODO: be careful of overflow in val+DAY_NANOS
453
- isr = bisect_right_i8(tdata, val + DAY_NANOS , ntrans) - 1
474
+ # TODO: be careful of overflow in val+ppd
475
+ isr = bisect_right_i8(tdata, val + ppd , ntrans) - 1
454
476
if isr < 0 :
455
477
isr = 0
456
478
@@ -465,10 +487,11 @@ cdef _get_utc_bounds(
465
487
466
488
@ cython.boundscheck (False )
467
489
cdef ndarray[int64_t] _get_dst_hours(
468
- # vals only needed here to potential render an exception message
490
+ # vals, reso only needed here to potential render an exception message
469
491
const int64_t[:] vals,
470
492
ndarray[int64_t] result_a,
471
493
ndarray[int64_t] result_b,
494
+ NPY_DATETIMEUNIT reso,
472
495
):
473
496
cdef:
474
497
Py_ssize_t i, n = vals.shape[0 ]
@@ -497,7 +520,7 @@ cdef ndarray[int64_t] _get_dst_hours(
497
520
498
521
if trans_idx.size == 1 :
499
522
# TODO: not reached in tests 2022-05-02; possible?
500
- stamp = _render_tstamp(vals[trans_idx[0 ]])
523
+ stamp = _render_tstamp(vals[trans_idx[0 ]], reso = reso )
501
524
raise pytz.AmbiguousTimeError(
502
525
f" Cannot infer dst time from {stamp} as there "
503
526
" are no repeated times"
@@ -519,7 +542,7 @@ cdef ndarray[int64_t] _get_dst_hours(
519
542
delta = np.diff(result_a[grp])
520
543
if grp.size == 1 or np.all(delta > 0 ):
521
544
# TODO: not reached in tests 2022-05-02; possible?
522
- stamp = _render_tstamp(vals[grp[0 ]])
545
+ stamp = _render_tstamp(vals[grp[0 ]], reso = reso )
523
546
raise pytz.AmbiguousTimeError(stamp)
524
547
525
548
# Find the index for the switch and pull from a for dst and b
0 commit comments