-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
BUG/ENH: cleanup for Timedelta arithmetic #8884
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,11 @@ Enhancements | |
- Added support for ``utcfromtimestamp()``, ``fromtimestamp()``, and ``combine()`` on `Timestamp` class (:issue:`5351`). | ||
- Added Google Analytics (`pandas.io.ga`) basic documentation (:issue:`8835`). See :ref:`here<remote_data.ga>`. | ||
- Added flag ``order_categoricals`` to ``StataReader`` and ``read_stata`` to select whether to order imported categorical data (:issue:`8836`). See :ref:`here <io.stata-categorical>` for more information on importing categorical variables from Stata data files. | ||
- ``Timedelta`` arithmetic returns ``NotImplemented`` in unknown cases, allowing extensions | ||
by custom classes (:issue:`8813`). | ||
- ``Timedelta`` now supports arithemtic with ``numpy.ndarray`` objects of the appropriate | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add release notes for all issues fixed (or if 2 apply to the same one, pls list them there) |
||
dtype (numpy 1.8 or newer only) (:issue:`8884`). | ||
- Added ``Timedelta.to_timedelta64`` method to the public API (:issue:`8884`). | ||
|
||
.. _whatsnew_0152.performance: | ||
|
||
|
@@ -89,6 +94,8 @@ Bug Fixes | |
- Bug in slicing a multi-index with an empty list and at least one boolean indexer (:issue:`8781`) | ||
- ``io.data.Options`` now raises ``RemoteDataError`` when no expiry dates are available from Yahoo (:issue:`8761`). | ||
- ``Timedelta`` kwargs may now be numpy ints and floats (:issue:`8757`). | ||
- Fixed several outstanding bugs for ``Timedelta`` arithmetic and comparisons | ||
(:issue:`8813`, :issue:`5963`, :issue:`5436`). | ||
- ``sql_schema`` now generates dialect appropriate ``CREATE TABLE`` statements (:issue:`8697`) | ||
- ``slice`` string method now takes step into account (:issue:`8754`) | ||
- Bug in ``BlockManager`` where setting values with different type would break block integrity (:issue:`8850`) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -311,7 +311,7 @@ def _evaluate_with_timedelta_like(self, other, op, opstr): | |
result = self._maybe_mask_results(result,convert='float64') | ||
return Index(result,name=self.name,copy=False) | ||
|
||
raise TypeError("can only perform ops with timedelta like values") | ||
return NotImplemented | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we usually have message with NotImplemented? (Ideally would like to, not sure if its convention) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, Python automatically fills in the error message, e.g., |
||
|
||
def _add_datelike(self, other): | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
from datetime import datetime, timedelta, time | ||
import nose | ||
|
||
from distutils.version import LooseVersion | ||
import numpy as np | ||
import pandas as pd | ||
|
||
|
@@ -45,12 +46,12 @@ def test_construction(self): | |
self.assertEqual(Timedelta(days=10,seconds=10).value, expected) | ||
self.assertEqual(Timedelta(days=10,milliseconds=10*1000).value, expected) | ||
self.assertEqual(Timedelta(days=10,microseconds=10*1000*1000).value, expected) | ||
|
||
# test construction with np dtypes | ||
# GH 8757 | ||
timedelta_kwargs = {'days':'D', 'seconds':'s', 'microseconds':'us', | ||
timedelta_kwargs = {'days':'D', 'seconds':'s', 'microseconds':'us', | ||
'milliseconds':'ms', 'minutes':'m', 'hours':'h', 'weeks':'W'} | ||
npdtypes = [np.int64, np.int32, np.int16, | ||
npdtypes = [np.int64, np.int32, np.int16, | ||
np.float64, np.float32, np.float16] | ||
for npdtype in npdtypes: | ||
for pykwarg, npkwarg in timedelta_kwargs.items(): | ||
|
@@ -163,9 +164,17 @@ def test_identity(self): | |
def test_conversion(self): | ||
|
||
for td in [ Timedelta(10,unit='d'), Timedelta('1 days, 10:11:12.012345') ]: | ||
self.assertTrue(td == Timedelta(td.to_pytimedelta())) | ||
self.assertEqual(td,td.to_pytimedelta()) | ||
self.assertEqual(td,np.timedelta64(td.value,'ns')) | ||
pydt = td.to_pytimedelta() | ||
self.assertTrue(td == Timedelta(pydt)) | ||
self.assertEqual(td, pydt) | ||
self.assertTrue(isinstance(pydt, timedelta) | ||
and not isinstance(pydt, Timedelta)) | ||
|
||
self.assertEqual(td, np.timedelta64(td.value, 'ns')) | ||
td64 = td.to_timedelta64() | ||
self.assertEqual(td64, np.timedelta64(td.value, 'ns')) | ||
self.assertEqual(td, td64) | ||
self.assertTrue(isinstance(td64, np.timedelta64)) | ||
|
||
# this is NOT equal and cannot be roundtriped (because of the nanos) | ||
td = Timedelta('1 days, 10:11:12.012345678') | ||
|
@@ -204,6 +213,15 @@ def test_ops(self): | |
self.assertRaises(TypeError, lambda : td + 2) | ||
self.assertRaises(TypeError, lambda : td - 2) | ||
|
||
def test_ops_offsets(self): | ||
td = Timedelta(10, unit='d') | ||
self.assertEqual(Timedelta(241, unit='h'), td + pd.offsets.Hour(1)) | ||
self.assertEqual(Timedelta(241, unit='h'), pd.offsets.Hour(1) + td) | ||
self.assertEqual(240, td / pd.offsets.Hour(1)) | ||
self.assertEqual(1 / 240.0, pd.offsets.Hour(1) / td) | ||
self.assertEqual(Timedelta(239, unit='h'), td - pd.offsets.Hour(1)) | ||
self.assertEqual(Timedelta(-239, unit='h'), pd.offsets.Hour(1) - td) | ||
|
||
def test_freq_conversion(self): | ||
|
||
td = Timedelta('1 days 2 hours 3 ns') | ||
|
@@ -214,6 +232,74 @@ def test_freq_conversion(self): | |
result = td / np.timedelta64(1,'ns') | ||
self.assertEquals(result, td.value) | ||
|
||
def test_ops_ndarray(self): | ||
td = Timedelta('1 day') | ||
|
||
# timedelta, timedelta | ||
other = pd.to_timedelta(['1 day']).values | ||
expected = pd.to_timedelta(['2 days']).values | ||
self.assert_numpy_array_equal(td + other, expected) | ||
if LooseVersion(np.__version__) >= '1.8': | ||
self.assert_numpy_array_equal(other + td, expected) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this really changed in numpy? ugh.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well, it was broken in numpy 1.7 but fixed in 1.8 |
||
self.assertRaises(TypeError, lambda: td + np.array([1])) | ||
self.assertRaises(TypeError, lambda: np.array([1]) + td) | ||
|
||
expected = pd.to_timedelta(['0 days']).values | ||
self.assert_numpy_array_equal(td - other, expected) | ||
if LooseVersion(np.__version__) >= '1.8': | ||
self.assert_numpy_array_equal(-other + td, expected) | ||
self.assertRaises(TypeError, lambda: td - np.array([1])) | ||
self.assertRaises(TypeError, lambda: np.array([1]) - td) | ||
|
||
expected = pd.to_timedelta(['2 days']).values | ||
self.assert_numpy_array_equal(td * np.array([2]), expected) | ||
self.assert_numpy_array_equal(np.array([2]) * td, expected) | ||
self.assertRaises(TypeError, lambda: td * other) | ||
self.assertRaises(TypeError, lambda: other * td) | ||
|
||
self.assert_numpy_array_equal(td / other, np.array([1])) | ||
if LooseVersion(np.__version__) >= '1.8': | ||
self.assert_numpy_array_equal(other / td, np.array([1])) | ||
|
||
# timedelta, datetime | ||
other = pd.to_datetime(['2000-01-01']).values | ||
expected = pd.to_datetime(['2000-01-02']).values | ||
self.assert_numpy_array_equal(td + other, expected) | ||
if LooseVersion(np.__version__) >= '1.8': | ||
self.assert_numpy_array_equal(other + td, expected) | ||
|
||
expected = pd.to_datetime(['1999-12-31']).values | ||
self.assert_numpy_array_equal(-td + other, expected) | ||
if LooseVersion(np.__version__) >= '1.8': | ||
self.assert_numpy_array_equal(other - td, expected) | ||
|
||
def test_ops_series(self): | ||
# regression test for GH8813 | ||
td = Timedelta('1 day') | ||
other = pd.Series([1, 2]) | ||
expected = pd.Series(pd.to_timedelta(['1 day', '2 days'])) | ||
tm.assert_series_equal(expected, td * other) | ||
tm.assert_series_equal(expected, other * td) | ||
|
||
def test_compare_timedelta_series(self): | ||
# regresssion test for GH5963 | ||
s = pd.Series([timedelta(days=1), timedelta(days=2)]) | ||
actual = s > timedelta(days=1) | ||
expected = pd.Series([False, True]) | ||
tm.assert_series_equal(actual, expected) | ||
|
||
def test_ops_notimplemented(self): | ||
class Other: | ||
pass | ||
other = Other() | ||
|
||
td = Timedelta('1 day') | ||
self.assertTrue(td.__add__(other) is NotImplemented) | ||
self.assertTrue(td.__sub__(other) is NotImplemented) | ||
self.assertTrue(td.__truediv__(other) is NotImplemented) | ||
self.assertTrue(td.__mul__(other) is NotImplemented) | ||
self.assertTrue(td.__floordiv__(td) is NotImplemented) | ||
|
||
def test_fields(self): | ||
rng = to_timedelta('1 days, 10:11:12') | ||
self.assertEqual(rng.days,1) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two spaces in the beginning of the line are missing (to align it with ```Time..`). But can be fixed later when doing a clean-up of the whatsnew file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixups here: ff0756f