Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Allow Timestamp to accept Nanosecond argument #19889

Merged
merged 2 commits into from
Mar 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ Datetimelike API Changes
- Subtraction of :class:`Series` with timezone-aware ``dtype='datetime64[ns]'`` with mis-matched timezones will raise ``TypeError`` instead of ``ValueError`` (:issue:`18817`)
- :func:`pandas.merge` provides a more informative error message when trying to merge on timezone-aware and timezone-naive columns (:issue:`15800`)
- For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with ``freq=None``, addition or subtraction of integer-dtyped array or ``Index`` will raise ``NullFrequencyError`` instead of ``TypeError`` (:issue:`19895`)
- :class:`Timestamp` constructor now accepts a `nanosecond` keyword or positional argument (:issue:`18898`)

.. _whatsnew_0230.api.other:

Expand Down
3 changes: 2 additions & 1 deletion pandas/_libs/tslibs/conversion.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ cdef class _TSObject:


cdef convert_to_tsobject(object ts, object tz, object unit,
bint dayfirst, bint yearfirst)
bint dayfirst, bint yearfirst,
int32_t nanos=*)

cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz,
int32_t nanos=*)
Expand Down
4 changes: 2 additions & 2 deletions pandas/_libs/tslibs/conversion.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ cpdef int64_t pydt_to_i8(object pydt) except? -1:


cdef convert_to_tsobject(object ts, object tz, object unit,
bint dayfirst, bint yearfirst):
bint dayfirst, bint yearfirst, int32_t nanos=0):
"""
Extract datetime and int64 from any of:
- np.int64 (with unit providing a possible modifier)
Expand Down Expand Up @@ -297,7 +297,7 @@ cdef convert_to_tsobject(object ts, object tz, object unit,
obj.value = ts
dt64_to_dtstruct(ts, &obj.dts)
elif PyDateTime_Check(ts):
return convert_datetime_to_tsobject(ts, tz)
return convert_datetime_to_tsobject(ts, tz, nanos)
elif PyDate_Check(ts):
# Keep the converter same as PyDateTime's
ts = datetime.combine(ts, datetime_time())
Expand Down
25 changes: 19 additions & 6 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ cdef class _Timestamp(datetime):
cdef readonly:
int64_t value, nanosecond
object freq # frequency reference
list _date_attributes

def __hash__(_Timestamp self):
if self.nanosecond:
Expand Down Expand Up @@ -425,6 +426,8 @@ class Timestamp(_Timestamp):
.. versionadded:: 0.19.0
hour, minute, second, microsecond : int, optional, default 0
.. versionadded:: 0.19.0
nanosecond : int, optional, default 0
.. versionadded:: 0.23.0
tzinfo : datetime.tzinfo, optional, default None
.. versionadded:: 0.19.0
Expand Down Expand Up @@ -556,7 +559,7 @@ class Timestamp(_Timestamp):
object freq=None, tz=None, unit=None,
year=None, month=None, day=None,
hour=None, minute=None, second=None, microsecond=None,
tzinfo=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

nanosecond=None, tzinfo=None):
# The parameter list folds together legacy parameter names (the first
# four) and positional and keyword parameter names from pydatetime.
#
Expand All @@ -580,6 +583,9 @@ class Timestamp(_Timestamp):

cdef _TSObject ts

_date_attributes = [year, month, day, hour, minute, second,
microsecond, nanosecond]

if tzinfo is not None:
if not PyTZInfo_Check(tzinfo):
# tzinfo must be a datetime.tzinfo object, GH#17690
Expand All @@ -588,28 +594,35 @@ class Timestamp(_Timestamp):
elif tz is not None:
raise ValueError('Can provide at most one of tz, tzinfo')

if ts_input is _no_input:
if is_string_object(ts_input):
# User passed a date string to parse.
# Check that the user didn't also pass a date attribute kwarg.
if any(arg is not None for arg in _date_attributes):
raise ValueError('Cannot pass a date attribute keyword '
'argument when passing a date string')

elif ts_input is _no_input:
# User passed keyword arguments.
if tz is None:
# Handle the case where the user passes `tz` and not `tzinfo`
tz = tzinfo
return Timestamp(datetime(year, month, day, hour or 0,
minute or 0, second or 0,
microsecond or 0, tzinfo),
tz=tz)
nanosecond=nanosecond, tz=tz)
elif is_integer_object(freq):
# User passed positional arguments:
# Timestamp(year, month, day[, hour[, minute[, second[,
# microsecond[, tzinfo]]]]])
# microsecond[, nanosecond[, tzinfo]]]]]])
return Timestamp(datetime(ts_input, freq, tz, unit or 0,
year or 0, month or 0, day or 0,
hour), tz=hour)
minute), nanosecond=hour, tz=minute)

if tzinfo is not None:
# User passed tzinfo instead of tz; avoid silently ignoring
tz, tzinfo = tzinfo, None

ts = convert_to_tsobject(ts_input, tz, unit, 0, 0)
ts = convert_to_tsobject(ts_input, tz, unit, 0, 0, nanosecond or 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we get here with a string ts_input with conflicting nanos? I think it might be better to accept nanos only in combination with other integer args/kwargs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently (and in this PR), the Timestamp constructor will just parse the string and ignore all additional passed kwargs related to the values of each datetime element:

In [7]: Timestamp('2010-10-10 12:59:59.999999999', hour=0, nanosecond=16)
Out[7]: Timestamp('2010-10-10 12:59:59.999999999')

The nanosecond kwarg will only be used if a datetime.datetime is passed or other datetime element kwargs are passed. As mentioned in my other comment, we might also want to raise in the situation above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree here, the nanoscecond kw must only be used when kwargs are passed


if ts.value == NPY_NAT:
return NaT
Expand Down
21 changes: 21 additions & 0 deletions pandas/tests/scalar/timestamp/test_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,27 @@ def test_constructor_fromordinal(self):
ts = Timestamp.fromordinal(dt_tz.toordinal(), tz='US/Eastern')
assert ts.to_pydatetime() == dt_tz

@pytest.mark.parametrize('result', [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test the string case as you have a above, which should raise a ValueError

Timestamp(datetime(2000, 1, 2, 3, 4, 5, 6), nanosecond=1),
Timestamp(year=2000, month=1, day=2, hour=3, minute=4, second=5,
microsecond=6, nanosecond=1),
Timestamp(year=2000, month=1, day=2, hour=3, minute=4, second=5,
microsecond=6, nanosecond=1, tz='UTC'),
Timestamp(2000, 1, 2, 3, 4, 5, 6, 1, None),
Timestamp(2000, 1, 2, 3, 4, 5, 6, 1, pytz.UTC)])
def test_constructor_nanosecond(self, result):
# GH 18898
expected = Timestamp(datetime(2000, 1, 2, 3, 4, 5, 6), tz=result.tz)
expected = expected + Timedelta(nanoseconds=1)
assert result == expected

@pytest.mark.parametrize('arg', ['year', 'month', 'day', 'hour', 'minute',
'second', 'microsecond', 'nanosecond'])
def test_invalid_date_kwarg_with_string_input(self, arg):
kwarg = {arg: 1}
with pytest.raises(ValueError):
Timestamp('2010-10-10 12:59:59.999999999', **kwarg)

def test_out_of_bounds_value(self):
one_us = np.timedelta64(1).astype('timedelta64[us]')

Expand Down