Skip to content

Commit

Permalink
Merge 4043116 into 05d02d4
Browse files Browse the repository at this point in the history
  • Loading branch information
pomidoroshev committed May 18, 2020
2 parents 05d02d4 + 4043116 commit 0b5937b
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 14 deletions.
42 changes: 32 additions & 10 deletions humanfriendly/__init__.py
Expand Up @@ -464,8 +464,9 @@ def parse_timespan(timespan):
"""
Parse a "human friendly" timespan into the number of seconds.
:param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or
``42s`` (42 seconds).
:param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes),
``42s`` (42 seconds), ``5h2d`` (5 hours 2 days) or
``10m30s`` (10 minutes 30 sesonds).
:returns: The number of seconds as a floating point number.
:raises: :exc:`InvalidTimespan` when the input can't be parsed.
Expand Down Expand Up @@ -499,19 +500,40 @@ def parse_timespan(timespan):
>>> parse_timespan('1d')
86400.0
"""
def _find_divider(unit):
normalized_unit = unit.lower()
for time_unit in time_units:
if (normalized_unit == time_unit['singular'] or
normalized_unit == time_unit['plural'] or
normalized_unit in time_unit['abbreviations']):
return time_unit['divider']
return None

def _to_seconds(pair):
"""
Convert pair of tokens to seconds.
"""
if not is_string(pair[1]):
raise ValueError

divider = _find_divider(pair[1])
if divider is None:
raise ValueError

return float(pair[0]) * divider

tokens = tokenize(timespan)
if tokens and isinstance(tokens[0], numbers.Number):
# If the input contains only a number, it's assumed to be the number of seconds.
if len(tokens) == 1:
return float(tokens[0])
# Otherwise we expect to find two tokens: A number and a unit.
if len(tokens) == 2 and is_string(tokens[1]):
normalized_unit = tokens[1].lower()
for unit in time_units:
if (normalized_unit == unit['singular'] or
normalized_unit == unit['plural'] or
normalized_unit in unit['abbreviations']):
return float(tokens[0]) * unit['divider']
# Otherwise we expect to find an even number of pairs of numbers and units.
if len(tokens) % 2 == 0:
try:
return round(sum(map(_to_seconds, zip(tokens[::2], tokens[1::2]))), 9)
except ValueError:
pass

# We failed to parse the timespan specification.
msg = "Failed to parse timespan! (input %r was tokenized as %r)"
raise InvalidTimespan(format(msg, timespan, tokens))
Expand Down
27 changes: 24 additions & 3 deletions humanfriendly/tests.py
Expand Up @@ -479,6 +479,27 @@ def test_parse_timespan(self):
self.assertEqual(60 * 60 * 24 * 4, parse_timespan('4 days'))
self.assertEqual(60 * 60 * 24 * 7 * 5, parse_timespan('5 w'))
self.assertEqual(60 * 60 * 24 * 7 * 5, parse_timespan('5 weeks'))

self.assertEqual(0, parse_timespan('0m0s'))
self.assertEqual(0, parse_timespan('0h0m0s'))
self.assertEqual(0.000001001, parse_timespan('1us1ns'))
self.assertEqual(0.001001, parse_timespan('1ms1us'))
self.assertEqual(0.001001001, parse_timespan('1ms1us1ns'))
self.assertEqual(0.5, parse_timespan('0 seconds 500 milliseconds'))
self.assertEqual(60 * 2 + 5, parse_timespan('2m5s'))
self.assertEqual(60 * 2 + 5, parse_timespan('2 minutes 5 seconds'))
self.assertEqual(60 * 2 + 5, parse_timespan('2 min 5 sec'))
self.assertEqual(60 * 2 + 5, parse_timespan('2 mins 5 secs'))
self.assertEqual(60 * 60 * 3 + 5 * 60, parse_timespan('3h5m'))
self.assertEqual(60 * 60 * 3 + 5 * 60, parse_timespan('3 h 5 m'))
self.assertEqual(60 * 60 * 3 + 5 * 60, parse_timespan('3 hours 5 minutes'))
self.assertEqual(60 * 60 * 24 * 4 + 60 * 60, parse_timespan('4d1h'))
self.assertEqual(60 * 60 * 24 * 4 + 60 * 60, parse_timespan('4 days 1 hour'))
self.assertEqual(60 * 60 * 24 * 4 + 60 * 60 * 2, parse_timespan('4 days 2 hours'))
self.assertEqual(60 * 60 * 24 * 7 * 5 + 60 * 60 * 24 * 2, parse_timespan('5w2d'))
self.assertEqual(60 * 60 * 24 * 7 * 5 + 60 * 60 * 24 * 2, parse_timespan('5 w 2 d'))
self.assertEqual(60 * 60 * 24 * 7 * 5 + 60 * 60 * 24 * 2, parse_timespan('5 weeks 2 days'))

with self.assertRaises(InvalidTimespan):
parse_timespan('1z')

Expand Down Expand Up @@ -787,8 +808,8 @@ def test_spinner(self):
.replace(ANSI_HIDE_CURSOR, ''))
lines = [line for line in output.split(ANSI_ERASE_LINE) if line]
self.assertTrue(len(lines) > 0)
self.assertTrue(all('test spinner' in l for l in lines))
self.assertTrue(all('%' in l for l in lines))
self.assertTrue(all('test spinner' in x for x in lines))
self.assertTrue(all('%' in x for x in lines))
self.assertEqual(sorted(set(lines)), sorted(lines))

def test_automatic_spinner(self):
Expand Down Expand Up @@ -952,7 +973,7 @@ def test_cli(self):
# https://github.com/xolox/python-humanfriendly/issues/28
returncode, output = run_cli(main, '--demo')
assert returncode == 0
lines = [ansi_strip(l) for l in output.splitlines()]
lines = [ansi_strip(x) for x in output.splitlines()]
assert "Text styles:" in lines
assert "Foreground colors:" in lines
assert "Background colors:" in lines
Expand Down
2 changes: 1 addition & 1 deletion humanfriendly/usage.py
Expand Up @@ -258,7 +258,7 @@ def render_usage(text):
('\n\n'.join(render_paragraph(p, meta_variables) for p in split_paragraphs(description))).rstrip(),
])
csv_lines = csv_buffer.getvalue().splitlines()
output.append('\n'.join(' %s' % l for l in csv_lines))
output.append('\n'.join(' %s' % x for x in csv_lines))
logger.debug("Rendered output: %s", output)
return '\n\n'.join(trim_empty_lines(o) for o in output)

Expand Down

0 comments on commit 0b5937b

Please sign in to comment.