Skip to content

Commit be14333

Browse files
jbrockmendeljreback
authored andcommitted
move and de-privatize _localize_pydatetime, move shift_day to liboffsets (#21691)
1 parent 1af2f6c commit be14333

File tree

11 files changed

+94
-81
lines changed

11 files changed

+94
-81
lines changed

pandas/_libs/tslib.pyx

-17
Original file line numberDiff line numberDiff line change
@@ -235,23 +235,6 @@ def _test_parse_iso8601(object ts):
235235
return Timestamp(obj.value)
236236

237237

238-
cpdef inline object _localize_pydatetime(object dt, object tz):
239-
"""
240-
Take a datetime/Timestamp in UTC and localizes to timezone tz.
241-
"""
242-
if tz is None:
243-
return dt
244-
elif isinstance(dt, Timestamp):
245-
return dt.tz_localize(tz)
246-
elif tz == 'UTC' or tz is UTC:
247-
return UTC.localize(dt)
248-
try:
249-
# datetime.replace with pytz may be incorrect result
250-
return tz.localize(dt)
251-
except AttributeError:
252-
return dt.replace(tzinfo=tz)
253-
254-
255238
def format_array_from_datetime(ndarray[int64_t] values, object tz=None,
256239
object format=None, object na_rep=None):
257240
"""

pandas/_libs/tslibs/conversion.pxd

+2
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ cpdef int64_t pydt_to_i8(object pydt) except? -1
3131
cdef maybe_datetimelike_to_i8(object val)
3232

3333
cdef int64_t tz_convert_utc_to_tzlocal(int64_t utc_val, tzinfo tz)
34+
35+
cpdef datetime localize_pydatetime(datetime dt, object tz)

pandas/_libs/tslibs/conversion.pyx

+28-1
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ cdef inline datetime _localize_pydatetime(datetime dt, tzinfo tz):
565565
"""
566566
Take a datetime/Timestamp in UTC and localizes to timezone tz.
567567
568-
NB: Unlike the version in tslib, this treats datetime and Timestamp objects
568+
NB: Unlike the public version, this treats datetime and Timestamp objects
569569
identically, i.e. discards nanos from Timestamps.
570570
It also assumes that the `tz` input is not None.
571571
"""
@@ -580,6 +580,33 @@ cdef inline datetime _localize_pydatetime(datetime dt, tzinfo tz):
580580
# ----------------------------------------------------------------------
581581
# Timezone Conversion
582582

583+
cpdef inline datetime localize_pydatetime(datetime dt, object tz):
584+
"""
585+
Take a datetime/Timestamp in UTC and localizes to timezone tz.
586+
587+
Parameters
588+
----------
589+
dt : datetime or Timestamp
590+
tz : tzinfo, "UTC", or None
591+
592+
Returns
593+
-------
594+
localized : datetime or Timestamp
595+
"""
596+
if tz is None:
597+
return dt
598+
elif not PyDateTime_CheckExact(dt):
599+
# i.e. is a Timestamp
600+
return dt.tz_localize(tz)
601+
elif tz == 'UTC' or tz is UTC:
602+
return UTC.localize(dt)
603+
try:
604+
# datetime.replace with pytz may be incorrect result
605+
return tz.localize(dt)
606+
except AttributeError:
607+
return dt.replace(tzinfo=tz)
608+
609+
583610
cdef inline int64_t tz_convert_tzlocal_to_utc(int64_t val, tzinfo tz):
584611
"""
585612
Parameters

pandas/_libs/tslibs/offsets.pyx

+31-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ cimport cython
55
from cython cimport Py_ssize_t
66

77
import time
8-
from cpython.datetime cimport datetime, timedelta, time as dt_time
8+
from cpython.datetime cimport (PyDateTime_IMPORT, PyDateTime_CheckExact,
9+
datetime, timedelta,
10+
time as dt_time)
11+
PyDateTime_IMPORT
912

1013
from dateutil.relativedelta import relativedelta
14+
from pytz import UTC
1115

1216
import numpy as np
1317
cimport numpy as cnp
@@ -19,7 +23,7 @@ from util cimport is_string_object, is_integer_object
1923

2024
from ccalendar import MONTHS, DAYS
2125
from ccalendar cimport get_days_in_month, dayofweek
22-
from conversion cimport tz_convert_single, pydt_to_i8
26+
from conversion cimport tz_convert_single, pydt_to_i8, localize_pydatetime
2327
from frequencies cimport get_freq_code
2428
from nattype cimport NPY_NAT
2529
from np_datetime cimport (pandas_datetimestruct,
@@ -494,6 +498,31 @@ class BaseOffset(_BaseOffset):
494498
# ----------------------------------------------------------------------
495499
# RelativeDelta Arithmetic
496500

501+
cpdef datetime shift_day(datetime other, int days):
502+
"""
503+
Increment the datetime `other` by the given number of days, retaining
504+
the time-portion of the datetime. For tz-naive datetimes this is
505+
equivalent to adding a timedelta. For tz-aware datetimes it is similar to
506+
dateutil's relativedelta.__add__, but handles pytz tzinfo objects.
507+
508+
Parameters
509+
----------
510+
other : datetime or Timestamp
511+
days : int
512+
513+
Returns
514+
-------
515+
shifted: datetime or Timestamp
516+
"""
517+
if other.tzinfo is None:
518+
return other + timedelta(days=days)
519+
520+
tz = other.tzinfo
521+
naive = other.replace(tzinfo=None)
522+
shifted = naive + timedelta(days=days)
523+
return localize_pydatetime(shifted, tz)
524+
525+
497526
cdef inline int year_add_months(pandas_datetimestruct dts, int months) nogil:
498527
"""new year number after shifting pandas_datetimestruct number of months"""
499528
return dts.year + (dts.month + months - 1) / 12

pandas/tests/indexes/datetimes/test_arithmetic.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
DatetimeIndex, TimedeltaIndex,
1616
date_range)
1717
from pandas.core import ops
18-
from pandas._libs import tslib
18+
from pandas._libs.tslibs.conversion import localize_pydatetime
1919
from pandas._libs.tslibs.offsets import shift_months
2020

2121

@@ -56,10 +56,7 @@ def test_dti_cmp_datetimelike(self, other, tz):
5656
if isinstance(other, np.datetime64):
5757
# no tzaware version available
5858
return
59-
elif isinstance(other, Timestamp):
60-
other = other.tz_localize(dti.tzinfo)
61-
else:
62-
other = tslib._localize_pydatetime(other, dti.tzinfo)
59+
other = localize_pydatetime(other, dti.tzinfo)
6360

6461
result = dti == other
6562
expected = np.array([True, False])

pandas/tests/indexes/datetimes/test_timezones.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
import pandas.util._test_decorators as td
1616

1717
import pandas as pd
18-
from pandas._libs import tslib
19-
from pandas._libs.tslibs import timezones
18+
from pandas._libs.tslibs import timezones, conversion
2019
from pandas.compat import lrange, zip, PY3
2120
from pandas import (DatetimeIndex, date_range, bdate_range,
2221
Timestamp, isna, to_datetime, Index)
@@ -911,12 +910,12 @@ def test_with_tz(self, tz):
911910
central = dr.tz_convert(tz)
912911
assert central.tz is tz
913912
naive = central[0].to_pydatetime().replace(tzinfo=None)
914-
comp = tslib._localize_pydatetime(naive, tz).tzinfo
913+
comp = conversion.localize_pydatetime(naive, tz).tzinfo
915914
assert central[0].tz is comp
916915

917916
# compare vs a localized tz
918917
naive = dr[0].to_pydatetime().replace(tzinfo=None)
919-
comp = tslib._localize_pydatetime(naive, tz).tzinfo
918+
comp = conversion.localize_pydatetime(naive, tz).tzinfo
920919
assert central[0].tz is comp
921920

922921
# datetimes with tzinfo set
@@ -946,7 +945,7 @@ def test_dti_convert_tz_aware_datetime_datetime(self, tz):
946945
dates = [datetime(2000, 1, 1), datetime(2000, 1, 2),
947946
datetime(2000, 1, 3)]
948947

949-
dates_aware = [tslib._localize_pydatetime(x, tz) for x in dates]
948+
dates_aware = [conversion.localize_pydatetime(x, tz) for x in dates]
950949
result = DatetimeIndex(dates_aware)
951950
assert timezones.tz_compare(result.tz, tz)
952951

pandas/tests/scalar/timestamp/test_unary_ops.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import pandas.util._test_decorators as td
1111

1212
from pandas.compat import PY3
13-
from pandas._libs import tslib
13+
from pandas._libs.tslibs import conversion
1414
from pandas._libs.tslibs.frequencies import _INVALID_FREQ_ERROR
1515
from pandas import Timestamp, NaT
1616

@@ -242,7 +242,7 @@ def test_replace_across_dst(self, tz, normalize):
242242
# GH#18319 check that 1) timezone is correctly normalized and
243243
# 2) that hour is not incorrectly changed by this normalization
244244
ts_naive = Timestamp('2017-12-03 16:03:30')
245-
ts_aware = tslib._localize_pydatetime(ts_naive, tz)
245+
ts_aware = conversion.localize_pydatetime(ts_naive, tz)
246246

247247
# Preliminary sanity-check
248248
assert ts_aware == normalize(ts_aware)

pandas/tests/series/test_timezones.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
from dateutil.tz import tzoffset
1111

1212
import pandas.util.testing as tm
13-
from pandas._libs import tslib
14-
from pandas._libs.tslibs import timezones
13+
from pandas._libs.tslibs import timezones, conversion
1514
from pandas.compat import lrange
1615
from pandas.core.indexes.datetimes import date_range
1716
from pandas import Series, Timestamp, DatetimeIndex, Index
@@ -298,7 +297,7 @@ def test_getitem_pydatetime_tz(self, tzstr):
298297
time_pandas = Timestamp('2012-12-24 17:00', tz=tzstr)
299298

300299
dt = datetime(2012, 12, 24, 17, 0)
301-
time_datetime = tslib._localize_pydatetime(dt, tz)
300+
time_datetime = conversion.localize_pydatetime(dt, tz)
302301
assert ts[time_pandas] == ts[time_datetime]
303302

304303
def test_series_truncate_datetimeindex_tz(self):

pandas/tests/tseries/offsets/test_offsets.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pandas.compat.numpy import np_datetime64_compat
1111

1212
from pandas.core.series import Series
13+
from pandas._libs.tslibs import conversion
1314
from pandas._libs.tslibs.frequencies import (get_freq_code, get_freq_str,
1415
_INVALID_FREQ_ERROR)
1516
from pandas.tseries.frequencies import _offset_map, get_offset
@@ -319,7 +320,7 @@ def _check_offsetfunc_works(self, offset, funcname, dt, expected,
319320
for tz in self.timezones:
320321
expected_localize = expected.tz_localize(tz)
321322
tz_obj = timezones.maybe_get_tz(tz)
322-
dt_tz = tslib._localize_pydatetime(dt, tz_obj)
323+
dt_tz = conversion.localize_pydatetime(dt, tz_obj)
323324

324325
result = func(dt_tz)
325326
assert isinstance(result, Timestamp)

pandas/tests/tslibs/test_timezones.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import pytz
66
import dateutil.tz
77

8-
from pandas._libs import tslib
9-
from pandas._libs.tslibs import timezones
8+
from pandas._libs.tslibs import timezones, conversion
109
from pandas import Timestamp
1110

1211

@@ -51,17 +50,17 @@ def test_infer_tz(eastern, localize):
5150
end = localize(eastern, end_naive)
5251

5352
assert (timezones.infer_tzinfo(start, end) is
54-
tslib._localize_pydatetime(start_naive, eastern).tzinfo)
53+
conversion.localize_pydatetime(start_naive, eastern).tzinfo)
5554
assert (timezones.infer_tzinfo(start, None) is
56-
tslib._localize_pydatetime(start_naive, eastern).tzinfo)
55+
conversion.localize_pydatetime(start_naive, eastern).tzinfo)
5756
assert (timezones.infer_tzinfo(None, end) is
58-
tslib._localize_pydatetime(end_naive, eastern).tzinfo)
57+
conversion.localize_pydatetime(end_naive, eastern).tzinfo)
5958

6059
start = utc.localize(start_naive)
6160
end = utc.localize(end_naive)
6261
assert timezones.infer_tzinfo(start, end) is utc
6362

64-
end = tslib._localize_pydatetime(end_naive, eastern)
63+
end = conversion.localize_pydatetime(end_naive, eastern)
6564
with pytest.raises(Exception):
6665
timezones.infer_tzinfo(start, end)
6766
with pytest.raises(Exception):

0 commit comments

Comments
 (0)