Skip to content

Commit b869a30

Browse files
committed
Convert float freqstrs to ints at finer resolution
Passing `'0.5min'` as a frequency string should generate 30 second intervals, rather than five minute intervals. By recursively increasing resolution until one is found for which the frequency is an integer, this commit ensures that that's the case for resolutions from days to microseconds. Fixes pandas-dev#8419
1 parent daba8e5 commit b869a30

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

pandas/tseries/frequencies.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,28 @@ class Resolution(object):
6363
RESO_SEC: 'second',
6464
RESO_MIN: 'minute',
6565
RESO_HR: 'hour',
66-
RESO_DAY: 'day'}
66+
RESO_DAY: 'day'
67+
}
68+
69+
# factor to multiply a value by to convert it to the next finer grained
70+
# resolution
71+
_reso_mult_map = {
72+
RESO_US: None,
73+
RESO_MS: 1000,
74+
RESO_SEC: 1000,
75+
RESO_MIN: 60,
76+
RESO_HR: 60,
77+
RESO_DAY: 24
78+
}
79+
80+
_reso_str_bump_map = {
81+
'D': 'H',
82+
'H': 'T',
83+
'T': 'S',
84+
'S': 'L',
85+
'L': 'U',
86+
'U': None
87+
}
6788

6889
_str_reso_map = dict([(v, k) for k, v in compat.iteritems(_reso_str_map)])
6990

@@ -160,6 +181,34 @@ def get_reso_from_freq(cls, freq):
160181
"""
161182
return cls.get_reso(cls.get_str_from_freq(freq))
162183

184+
@classmethod
185+
def bump_to_int(cls, value, freq):
186+
"""
187+
Increase resolution an integer value is available to create an
188+
offset.
189+
190+
Example
191+
-------
192+
>>> Resolution.bump_to_int(1.5, 'T')
193+
<90 * Seconds>
194+
195+
>>> Resolution.bump_to_int(1.04, 'H')
196+
<3744 * Seconds>
197+
198+
>>> Resolution.bump_to_int(1, 'D')
199+
<Day>
200+
"""
201+
202+
if np.isclose(value % 1, 0):
203+
return int(value), freq
204+
else:
205+
start_reso = cls.get_reso_from_freq(freq)
206+
if start_reso == 0:
207+
raise ValueError(_CANNOT_CONVERT_TO_INT)
208+
next_value = cls._reso_mult_map[start_reso] * value
209+
next_name = cls._reso_str_bump_map[freq]
210+
return cls.bump_to_int(next_value, next_name)
211+
163212

164213
def get_to_timestamp_base(base):
165214
"""
@@ -384,6 +433,9 @@ def get_period_alias(offset_str):
384433

385434

386435
_INVALID_FREQ_ERROR = "Invalid frequency: {0}"
436+
_CANNOT_CONVERT_TO_INT = (
437+
"Could not convert to integer offset at any resolution"
438+
)
387439

388440

389441
@deprecate_kwarg(old_arg_name='freqstr', new_arg_name='freq')
@@ -472,12 +524,17 @@ def to_offset(freq):
472524
splitted[2::4]):
473525
if sep != '' and not sep.isspace():
474526
raise ValueError('separator must be spaces')
475-
offset = get_offset(name)
527+
prefix = _lite_rule_alias.get(name)
476528
if stride_sign is None:
477529
stride_sign = -1 if stride.startswith('-') else 1
478530
if not stride:
479531
stride = 1
532+
if prefix in Resolution._reso_str_bump_map.keys():
533+
stride, name = Resolution.bump_to_int(
534+
float(stride), prefix
535+
)
480536
stride = int(stride)
537+
offset = get_offset(name)
481538
offset = offset * int(np.fabs(stride) * stride_sign)
482539
if delta is None:
483540
delta = offset
@@ -493,7 +550,9 @@ def to_offset(freq):
493550

494551

495552
# hack to handle WOM-1MON
496-
opattern = re.compile(r'([\-]?\d*)\s*([A-Za-z]+([\-][\dA-Za-z\-]+)?)')
553+
opattern = re.compile(
554+
r'([\-]?\d*|[\-]?\d*\.\d*)\s*([A-Za-z]+([\-][\dA-Za-z\-]+)?)'
555+
)
497556

498557

499558
def _base_and_stride(freqstr):

pandas/tseries/tests/test_frequencies.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def test_to_offset_multiple(self):
3939
expected = offsets.Hour(3)
4040
assert (result == expected)
4141

42+
freqstr = '1.5min'
43+
result = frequencies.to_offset(freqstr)
44+
expected = offsets.Second(90)
45+
assert (result == expected)
46+
4247
freqstr = '15l500u'
4348
result = frequencies.to_offset(freqstr)
4449
expected = offsets.Micro(15500)
@@ -108,10 +113,6 @@ def test_to_offset_invalid(self):
108113
with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: -2D:3H'):
109114
frequencies.to_offset('-2D:3H')
110115

111-
# ToDo: Must be fixed in #8419
112-
with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: .5S'):
113-
frequencies.to_offset('.5S')
114-
115116
# split offsets with spaces are valid
116117
assert frequencies.to_offset('2D 3H') == offsets.Hour(51)
117118
assert frequencies.to_offset('2 D3 H') == offsets.Hour(51)
@@ -379,6 +380,17 @@ def test_freq_to_reso(self):
379380
result = Reso.get_freq(Reso.get_str(Reso.get_reso_from_freq(freq)))
380381
self.assertEqual(freq, result)
381382

383+
def test_resolution_bumping(self):
384+
Reso = frequencies.Resolution
385+
386+
self.assertEqual(Reso.bump_to_int(1.5, 'T'), (90, 'S'))
387+
self.assertEqual(Reso.bump_to_int(62.4, 'T'), (3744, 'S'))
388+
self.assertEqual(Reso.bump_to_int(1.04, 'H'), (3744, 'S'))
389+
self.assertEqual(Reso.bump_to_int(1, 'D'), (1, 'D'))
390+
391+
with self.assertRaises(ValueError):
392+
Reso.bump_to_int(0.5, 'U')
393+
382394
def test_get_freq_code(self):
383395
# freqstr
384396
self.assertEqual(frequencies.get_freq_code('A'),

0 commit comments

Comments
 (0)