diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 114607f117756..62601821488d3 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -98,6 +98,7 @@ time. pd.Timestamp(datetime(2012, 5, 1)) pd.Timestamp('2012-05-01') + pd.Timestamp(2012, 5, 1) However, in many cases it is more natural to associate things like change variables with a time span instead. The span represented by ``Period`` can be diff --git a/doc/source/whatsnew/v0.18.2.txt b/doc/source/whatsnew/v0.18.2.txt index b86a7a81625e2..29a177f2bcceb 100644 --- a/doc/source/whatsnew/v0.18.2.txt +++ b/doc/source/whatsnew/v0.18.2.txt @@ -38,6 +38,14 @@ Other enhancements idx = pd.Index(["a1a2", "b1", "c1"]) idx.str.extractall("[ab](?P\d)") +- ``Timestamp``s can now accept positional and keyword parameters like :func:`datetime.datetime` (:issue:`10758`, :issue:`11630`) + + .. ipython:: python + + Timestamp(2012, 1, 1) + + Timestamp(2012, 1, 1, 8, 30) + .. _whatsnew_0182.api: API changes diff --git a/pandas/tseries/tests/test_tslib.py b/pandas/tseries/tests/test_tslib.py index 4543047a8a72a..6ae1354730987 100644 --- a/pandas/tseries/tests/test_tslib.py +++ b/pandas/tseries/tests/test_tslib.py @@ -180,6 +180,52 @@ def test_constructor_invalid(self): with tm.assertRaisesRegexp(ValueError, 'Cannot convert Period'): Timestamp(Period('1000-01-01')) + def test_constructor_positional(self): + # GH 10758 + with tm.assertRaises(TypeError): + Timestamp(2000, 1) + with tm.assertRaises(ValueError): + Timestamp(2000, 0, 1) + with tm.assertRaises(ValueError): + Timestamp(2000, 13, 1) + with tm.assertRaises(ValueError): + Timestamp(2000, 1, 0) + with tm.assertRaises(ValueError): + Timestamp(2000, 1, 32) + + # GH 11630 + self.assertEqual( + repr(Timestamp(2015, 11, 12)), + repr(Timestamp('20151112'))) + + self.assertEqual( + repr(Timestamp(2015, 11, 12, 1, 2, 3, 999999)), + repr(Timestamp('2015-11-12 01:02:03.999999'))) + + self.assertIs(Timestamp(None), pd.NaT) + + def test_constructor_keyword(self): + # GH 10758 + with tm.assertRaises(TypeError): + Timestamp(year=2000, month=1) + with tm.assertRaises(ValueError): + Timestamp(year=2000, month=0, day=1) + with tm.assertRaises(ValueError): + Timestamp(year=2000, month=13, day=1) + with tm.assertRaises(ValueError): + Timestamp(year=2000, month=1, day=0) + with tm.assertRaises(ValueError): + Timestamp(year=2000, month=1, day=32) + + self.assertEqual( + repr(Timestamp(year=2015, month=11, day=12)), + repr(Timestamp('20151112'))) + + self.assertEqual( + repr(Timestamp(year=2015, month=11, day=12, + hour=1, minute=2, second=3, microsecond=999999)), + repr(Timestamp('2015-11-12 01:02:03.999999'))) + def test_conversion(self): # GH 9255 ts = Timestamp('2000-01-01') diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index a240558025090..9588a6c420422 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -214,8 +214,8 @@ cdef inline bint _is_fixed_offset(object tz): return 0 return 1 - _zero_time = datetime_time(0, 0) +_no_input = object() # Python front end to C extension type _Timestamp # This serves as the box for datetime64 @@ -225,6 +225,10 @@ class Timestamp(_Timestamp): for the entries that make up a DatetimeIndex, and other timeseries oriented data structures in pandas. + There are essentially three calling conventions for the constructor. The + primary form accepts four parameters. They can be passed by position or + keyword. + Parameters ---------- ts_input : datetime-like, str, int, float @@ -235,6 +239,23 @@ class Timestamp(_Timestamp): Time zone for time which Timestamp will have. unit : string numpy unit used for conversion, if ts_input is int or float + + The other two forms mimic the parameters from ``datetime.datetime``. They + can be passed by either position or keyword, but not both mixed together. + + :func:`datetime.datetime` Parameters + ------------------------------------ + + .. versionadded:: 0.18.2 + + year : int + month : int + day : int + hour : int, optional, default is 0 + minute : int, optional, default is 0 + second : int, optional, default is 0 + microsecond : int, optional, default is 0 + tzinfo : datetime.tzinfo, optional, default is None """ @classmethod @@ -288,10 +309,46 @@ class Timestamp(_Timestamp): def combine(cls, date, time): return cls(datetime.combine(date, time)) - def __new__(cls, object ts_input, object offset=None, tz=None, unit=None): + def __new__(cls, + object ts_input=_no_input, object offset=None, tz=None, unit=None, + year=None, month=None, day=None, + hour=None, minute=None, second=None, microsecond=None, + tzinfo=None): + # The parameter list folds together legacy parameter names (the first + # four) and positional and keyword parameter names from pydatetime. + # + # There are three calling forms: + # + # - In the legacy form, the first parameter, ts_input, is required + # and may be datetime-like, str, int, or float. The second + # parameter, offset, is optional and may be str or DateOffset. + # + # - ints in the first, second, and third arguments indicate + # pydatetime positional arguments. Only the first 8 arguments + # (standing in for year, month, day, hour, minute, second, + # microsecond, tzinfo) may be non-None. As a shortcut, we just + # check that the second argument is an int. + # + # - Nones for the first four (legacy) arguments indicate pydatetime + # keyword arguments. year, month, and day are required. As a + # shortcut, we just check that the first argument was not passed. + # + # Mixing pydatetime positional and keyword arguments is forbidden! + cdef _TSObject ts cdef _Timestamp ts_base + if ts_input is _no_input: + # User passed keyword arguments. + return Timestamp(datetime(year, month, day, hour or 0, + minute or 0, second or 0, microsecond or 0, tzinfo), + tz=tzinfo) + elif is_integer_object(offset): + # User passed positional arguments: + # Timestamp(year, month, day[, hour[, minute[, second[, microsecond[, tzinfo]]]]]) + return Timestamp(datetime(ts_input, offset, tz, unit or 0, + year or 0, month or 0, day or 0, hour), tz=hour) + ts = convert_to_tsobject(ts_input, tz, unit, 0, 0) if ts.value == NPY_NAT: