diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 2e65fc9a44e29..90ce6b47728fb 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -618,6 +618,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: diff --git a/pandas/_libs/tslibs/conversion.pxd b/pandas/_libs/tslibs/conversion.pxd index 868c2641b34db..8f887dc3af203 100644 --- a/pandas/_libs/tslibs/conversion.pxd +++ b/pandas/_libs/tslibs/conversion.pxd @@ -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=*) diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 4726bd7ea3629..f4841e6abb7e8 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -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) @@ -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()) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index ed77916a1d887..421f781483290 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -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: @@ -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 @@ -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): + nanosecond=None, tzinfo=None): # The parameter list folds together legacy parameter names (the first # four) and positional and keyword parameter names from pydatetime. # @@ -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 @@ -588,7 +594,14 @@ 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` @@ -596,20 +609,20 @@ class Timestamp(_Timestamp): 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) if ts.value == NPY_NAT: return NaT diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index 7695c94409232..504a76f259e55 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -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', [ + 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]')