Skip to content

API/COMPAT: add pydatetime-style positional args to Timestamp constructor #12482

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

Closed
wants to merge 7 commits into from
Closed
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/timeseries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions doc/source/whatsnew/v0.18.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ Other enhancements
idx = pd.Index(["a1a2", "b1", "c1"])
idx.str.extractall("[ab](?P<digit>\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
Expand Down
46 changes: 46 additions & 0 deletions pandas/tseries/tests/test_tslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

can you use assertRaisesRegexp here to check for things

 In [1]: datetime.datetime(2000,1)
TypeError: Required argument 'day' (pos 3) not found

doesn't have to be an exact checking, but need to make sure that it is raising the datetime.datetime constructor error message and not something else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the cases where not enough parameters are passed to Timestamp, the error will be an integer is required (got NoneType) instead of Required argument not found. Matching the datetime exception exactly requires 3 additional checks and calls:

if year is None:
    return datetime.datetime()
if month is None:
    return datetime.datetime(year)
if day is None:
    return datetime.datetime(year, month)

which seems ugly. Do we really want that?

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

add some tests where None is passed for various places Timestamp(2000, 1, 1, None, None)
Timestamp(2000, 1, None, ...)

# GH 11630
self.assertEqual(
repr(Timestamp(2015, 11, 12)),
repr(Timestamp('20151112')))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here is the test for #11630, Github didn't do the best job linking it to your earlier comment.


self.assertEqual(
repr(Timestamp(2015, 11, 12, 1, 2, 3, 999999)),
repr(Timestamp('2015-11-12 01:02:03.999999')))

Copy link
Contributor

Choose a reason for hiding this comment

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

can you add the assertion for Timestamp(None) as well

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')
Expand Down
61 changes: 59 additions & 2 deletions pandas/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down