Skip to content

Commit d86f26b

Browse files
committed
BUG: Avoid rounding when specifying unit and integer
Add tests Add whatsnew flake8 Add aditional formatting remove tabs address review
1 parent 52559f5 commit d86f26b

File tree

3 files changed

+178
-9
lines changed

3 files changed

+178
-9
lines changed

doc/source/whatsnew/v0.23.0.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,8 @@ Timedelta
817817
- Bug in :func:`Timedelta.total_seconds()` causing precision errors i.e. ``Timedelta('30S').total_seconds()==30.000000000000004`` (:issue:`19458`)
818818
- Bug in :func: `Timedelta.__rmod__` where operating with a ``numpy.timedelta64`` returned a ``timedelta64`` object instead of a ``Timedelta`` (:issue:`19820`)
819819
- Multiplication of :class:`TimedeltaIndex` by ``TimedeltaIndex`` will now raise ``TypeError`` instead of raising ``ValueError`` in cases of length mis-match (:issue`19333`)
820-
-
820+
- Bug in :class:`Timedelta`: where a numerical value with a unit would round values (:issue: `12690`)
821+
821822

822823
Timezones
823824
^^^^^^^^^

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -200,22 +200,22 @@ cpdef inline int64_t cast_from_unit(object ts, object unit) except? -1:
200200

201201
if unit == 'D' or unit == 'd':
202202
m = 1000000000L * 86400
203-
p = 6
203+
p = 9
204204
elif unit == 'h':
205205
m = 1000000000L * 3600
206-
p = 6
206+
p = 9
207207
elif unit == 'm':
208208
m = 1000000000L * 60
209-
p = 6
209+
p = 9
210210
elif unit == 's':
211211
m = 1000000000L
212-
p = 6
212+
p = 9
213213
elif unit == 'ms':
214214
m = 1000000L
215-
p = 3
215+
p = 6
216216
elif unit == 'us':
217217
m = 1000L
218-
p = 0
218+
p = 3
219219
elif unit == 'ns' or unit is None:
220220
m = 1L
221221
p = 0
@@ -229,10 +229,10 @@ cpdef inline int64_t cast_from_unit(object ts, object unit) except? -1:
229229
# cast the unit, multiply base/frace separately
230230
# to avoid precision issues from float -> int
231231
base = <int64_t> ts
232-
frac = ts -base
232+
frac = ts - base
233233
if p:
234234
frac = round(frac, p)
235-
return <int64_t> (base *m) + <int64_t> (frac *m)
235+
return <int64_t> (base * m) + <int64_t> (frac * m)
236236

237237

238238
cdef inline _decode_if_necessary(object ts):

pandas/tests/scalar/timedelta/test_timedelta.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,174 @@ def test_compare_timedelta_ndarray(self):
9494

9595
class TestTimedeltas(object):
9696

97+
def setup_method(self, method):
98+
pass
99+
100+
def test_construction(self):
101+
102+
expected = np.timedelta64(10, 'D').astype('m8[ns]').view('i8')
103+
assert Timedelta(10, unit='d').value == expected
104+
assert Timedelta(10.0, unit='d').value == expected
105+
assert Timedelta('10 days').value == expected
106+
assert Timedelta(days=10).value == expected
107+
assert Timedelta(days=10.0).value == expected
108+
109+
expected += np.timedelta64(10, 's').astype('m8[ns]').view('i8')
110+
assert Timedelta('10 days 00:00:10').value == expected
111+
assert Timedelta(days=10, seconds=10).value == expected
112+
assert Timedelta(days=10, milliseconds=10 * 1000).value == expected
113+
assert (Timedelta(days=10, microseconds=10 * 1000 * 1000)
114+
.value == expected)
115+
116+
# gh-8757: test construction with np dtypes
117+
timedelta_kwargs = {'days': 'D',
118+
'seconds': 's',
119+
'microseconds': 'us',
120+
'milliseconds': 'ms',
121+
'minutes': 'm',
122+
'hours': 'h',
123+
'weeks': 'W'}
124+
npdtypes = [np.int64, np.int32, np.int16, np.float64, np.float32,
125+
np.float16]
126+
for npdtype in npdtypes:
127+
for pykwarg, npkwarg in timedelta_kwargs.items():
128+
expected = np.timedelta64(1, npkwarg).astype(
129+
'm8[ns]').view('i8')
130+
assert Timedelta(**{pykwarg: npdtype(1)}).value == expected
131+
132+
# rounding cases
133+
assert Timedelta(82739999850000).value == 82739999850000
134+
assert ('0 days 22:58:59.999850' in str(Timedelta(82739999850000)))
135+
assert Timedelta(123072001000000).value == 123072001000000
136+
assert ('1 days 10:11:12.001' in str(Timedelta(123072001000000)))
137+
138+
# string conversion with/without leading zero
139+
# GH 9570
140+
assert Timedelta('0:00:00') == timedelta(hours=0)
141+
assert Timedelta('00:00:00') == timedelta(hours=0)
142+
assert Timedelta('-1:00:00') == -timedelta(hours=1)
143+
assert Timedelta('-01:00:00') == -timedelta(hours=1)
144+
145+
# more strings & abbrevs
146+
# GH 8190
147+
assert Timedelta('1 h') == timedelta(hours=1)
148+
assert Timedelta('1 hour') == timedelta(hours=1)
149+
assert Timedelta('1 hr') == timedelta(hours=1)
150+
assert Timedelta('1 hours') == timedelta(hours=1)
151+
assert Timedelta('-1 hours') == -timedelta(hours=1)
152+
assert Timedelta('1 m') == timedelta(minutes=1)
153+
assert Timedelta('1.5 m') == timedelta(seconds=90)
154+
assert Timedelta('1 minute') == timedelta(minutes=1)
155+
assert Timedelta('1 minutes') == timedelta(minutes=1)
156+
assert Timedelta('1 s') == timedelta(seconds=1)
157+
assert Timedelta('1 second') == timedelta(seconds=1)
158+
assert Timedelta('1 seconds') == timedelta(seconds=1)
159+
assert Timedelta('1 ms') == timedelta(milliseconds=1)
160+
assert Timedelta('1 milli') == timedelta(milliseconds=1)
161+
assert Timedelta('1 millisecond') == timedelta(milliseconds=1)
162+
assert Timedelta('1 us') == timedelta(microseconds=1)
163+
assert Timedelta('1 micros') == timedelta(microseconds=1)
164+
assert Timedelta('1 microsecond') == timedelta(microseconds=1)
165+
assert Timedelta('1.5 microsecond') == Timedelta('00:00:00.000001500')
166+
assert Timedelta('1 ns') == Timedelta('00:00:00.000000001')
167+
assert Timedelta('1 nano') == Timedelta('00:00:00.000000001')
168+
assert Timedelta('1 nanosecond') == Timedelta('00:00:00.000000001')
169+
170+
# combos
171+
assert Timedelta('10 days 1 hour') == timedelta(days=10, hours=1)
172+
assert Timedelta('10 days 1 h') == timedelta(days=10, hours=1)
173+
assert Timedelta('10 days 1 h 1m 1s') == timedelta(
174+
days=10, hours=1, minutes=1, seconds=1)
175+
assert Timedelta('-10 days 1 h 1m 1s') == -timedelta(
176+
days=10, hours=1, minutes=1, seconds=1)
177+
assert Timedelta('-10 days 1 h 1m 1s') == -timedelta(
178+
days=10, hours=1, minutes=1, seconds=1)
179+
assert Timedelta('-10 days 1 h 1m 1s 3us') == -timedelta(
180+
days=10, hours=1, minutes=1, seconds=1, microseconds=3)
181+
assert Timedelta('-10 days 1 h 1.5m 1s 3us'), -timedelta(
182+
days=10, hours=1, minutes=1, seconds=31, microseconds=3)
183+
184+
# Currently invalid as it has a - on the hh:mm:dd part
185+
# (only allowed on the days)
186+
pytest.raises(ValueError,
187+
lambda: Timedelta('-10 days -1 h 1.5m 1s 3us'))
188+
189+
# only leading neg signs are allowed
190+
pytest.raises(ValueError,
191+
lambda: Timedelta('10 days -1 h 1.5m 1s 3us'))
192+
193+
# no units specified
194+
pytest.raises(ValueError, lambda: Timedelta('3.1415'))
195+
196+
# invalid construction
197+
tm.assert_raises_regex(ValueError, "cannot construct a Timedelta",
198+
lambda: Timedelta())
199+
tm.assert_raises_regex(ValueError,
200+
"unit abbreviation w/o a number",
201+
lambda: Timedelta('foo'))
202+
tm.assert_raises_regex(ValueError,
203+
"cannot construct a Timedelta from the "
204+
"passed arguments, allowed keywords are ",
205+
lambda: Timedelta(day=10))
206+
207+
# round-trip both for string and value
208+
for v in ['1s', '-1s', '1us', '-1us', '1 day', '-1 day',
209+
'-23:59:59.999999', '-1 days +23:59:59.999999', '-1ns',
210+
'1ns', '-23:59:59.999999999']:
211+
212+
td = Timedelta(v)
213+
assert Timedelta(td.value) == td
214+
215+
# str does not normally display nanos
216+
if not td.nanoseconds:
217+
assert Timedelta(str(td)) == td
218+
assert Timedelta(td._repr_base(format='all')) == td
219+
220+
# floats
221+
expected = np.timedelta64(
222+
10, 's').astype('m8[ns]').view('i8') + np.timedelta64(
223+
500, 'ms').astype('m8[ns]').view('i8')
224+
assert Timedelta(10.5, unit='s').value == expected
225+
226+
# offset
227+
assert (to_timedelta(pd.offsets.Hour(2)) ==
228+
Timedelta('0 days, 02:00:00'))
229+
assert (Timedelta(pd.offsets.Hour(2)) ==
230+
Timedelta('0 days, 02:00:00'))
231+
assert (Timedelta(pd.offsets.Second(2)) ==
232+
Timedelta('0 days, 00:00:02'))
233+
234+
# gh-11995: unicode
235+
expected = Timedelta('1H')
236+
result = pd.Timedelta(u'1H')
237+
assert result == expected
238+
assert (to_timedelta(pd.offsets.Hour(2)) ==
239+
Timedelta(u'0 days, 02:00:00'))
240+
241+
pytest.raises(ValueError, lambda: Timedelta(u'foo bar'))
242+
243+
@pytest.mark.parametrize("unit, value, expected", [
244+
('us', 9.999, 9999), ('ms', 9.999999, 9999999),
245+
('s', 9.999999999, 9999999999)])
246+
def test_rounding_on_int_unit_construction(self, unit, value, expected):
247+
# GH 12690
248+
result = Timedelta(value, unit=unit)
249+
assert result.value == expected
250+
result = Timedelta(str(value) + unit)
251+
assert result.value == expected
252+
253+
def test_overflow_on_construction(self):
254+
# xref https://github.com/statsmodels/statsmodels/issues/3374
255+
value = pd.Timedelta('1day').value * 20169940
256+
pytest.raises(OverflowError, pd.Timedelta, value)
257+
258+
# xref gh-17637
259+
with pytest.raises(OverflowError):
260+
pd.Timedelta(7 * 19999, unit='D')
261+
262+
with pytest.raises(OverflowError):
263+
pd.Timedelta(timedelta(days=13 * 19999))
264+
97265
def test_total_seconds_scalar(self):
98266
# see gh-10939
99267
rng = Timedelta('1 days, 10:11:12.100123456')

0 commit comments

Comments
 (0)