Question: Write a program that asks the user to enter a number of seconds, and works as follows:
There are 60 seconds in a minute. If the number of seconds
These functions are fairly compact and only use standard Python 2.6 and later.
def ddhhmmss(seconds):
"""Convert seconds to a time string "[[[DD:]HH:]MM:]SS".
"""
dhms = ''
for scale in 86400, 3600, 60:
result, seconds = divmod(seconds, scale)
if dhms != '' or result > 0:
dhms += '{0:02d}:'.format(result)
dhms += '{0:02d}'.format(seconds)
return dhms
def seconds(dhms):
"""Convert a time string "[[[DD:]HH:]MM:]SS" to seconds.
"""
components = [int(i) for i in dhms.split(':')]
pad = 4 - len(components)
if pad < 0:
raise ValueError('Too many components to match [[[DD:]HH:]MM:]SS')
components = [0] * pad + components
return sum(i * j for i, j in zip((86400, 3600, 60, 1), components))
And here are tests to go with them. I'm using the pytest package as a simple way to test exceptions.
import ddhhmmss
import pytest
def test_ddhhmmss():
assert ddhhmmss.ddhhmmss(0) == '00'
assert ddhhmmss.ddhhmmss(2) == '02'
assert ddhhmmss.ddhhmmss(12 * 60) == '12:00'
assert ddhhmmss.ddhhmmss(3600) == '01:00:00'
assert ddhhmmss.ddhhmmss(10 * 86400) == '10:00:00:00'
assert ddhhmmss.ddhhmmss(86400 + 5 * 3600 + 30 * 60 + 1) == '01:05:30:01'
assert ddhhmmss.ddhhmmss(365 * 86400) == '365:00:00:00'
def test_seconds():
assert ddhhmmss.seconds('00') == 0
assert ddhhmmss.seconds('02') == 2
assert ddhhmmss.seconds('12:00') == 12 * 60
assert ddhhmmss.seconds('01:00:00') == 3600
assert ddhhmmss.seconds('1:0:0') == 3600
assert ddhhmmss.seconds('3600') == 3600
assert ddhhmmss.seconds('60:0') == 3600
assert ddhhmmss.seconds('10:00:00:00') == 10 * 86400
assert ddhhmmss.seconds('1:05:30:01') == 86400 + 5 * 3600 + 30 * 60 + 1
assert ddhhmmss.seconds('365:00:00:00') == 365 * 86400
def test_seconds_raises():
with pytest.raises(ValueError):
ddhhmmss.seconds('')
with pytest.raises(ValueError):
ddhhmmss.seconds('foo')
with pytest.raises(ValueError):
ddhhmmss.seconds('1:00:00:00:00')
Patching as well Ralph Bolton's answer. Moving to a class and moving tulp of tulp (intervals) to dictionary. Adding an optional rounded function depending of granularity (enable by default). Ready to translation using gettext (default is disable). This is intend to be load from an module. This is for python3 (tested 3.6 - 3.8)
import gettext
import locale
from itertools import chain
mylocale = locale.getdefaultlocale()
# see --> https://stackoverflow.com/a/10174657/11869956 thx
#localedir = os.path.join(os.path.dirname(__file__), 'locales')
# or python > 3.4:
try:
localedir = pathlib.Path(__file__).parent/'locales'
lang_translations = gettext.translation('utils', localedir,
languages=[mylocale[0]])
lang_translations.install()
_ = lang_translations.gettext
except Exception as exc:
print('Error: unexcept error while initializing translation:', file=sys.stderr)
print(f'Error: {exc}', file=sys.stderr)
print(f'Error: localedir={localedir}, languages={mylocale[0]}', file=sys.stderr)
print('Error: translation has been disabled.', file=sys.stderr)
_ = gettext.gettext
Here is the class:
class FormatTimestamp:
"""Convert seconds to, optional rounded, time depending of granularity's degrees.
inspired by https://stackoverflow.com/a/24542445/11869956"""
def __init__(self):
# For now i haven't found a way to do it better
# TODO: optimize ?!? ;)
self.intervals = {
# 'years' : 31556952, # https://www.calculateme.com/time/years/to-seconds/
# https://www.calculateme.com/time/months/to-seconds/ -> 2629746 seconds
# But it's outputing some strange result :
# So 3 seconds less (2629743) : 4 weeks, 2 days, 10 hours, 29 minutes and 3 seconds
# than after 3 more seconds : 1 month ?!?
# Google give me 2628000 seconds
# So 3 seconds less (2627997): 4 weeks, 2 days, 9 hours, 59 minutes and 57 seconds
# Strange as well
# So for the moment latest is week ...
#'months' : 2419200, # 60 * 60 * 24 * 7 * 4
'weeks' : 604800, # 60 * 60 * 24 * 7
'days' : 86400, # 60 * 60 * 24
'hours' : 3600, # 60 * 60
'minutes' : 60,
'seconds' : 1
}
self.nextkey = {
'seconds' : 'minutes',
'minutes' : 'hours',
'hours' : 'days',
'days' : 'weeks',
'weeks' : 'weeks',
#'months' : 'months',
#'years' : 'years' # stop here
}
self.translate = {
'weeks' : _('weeks'),
'days' : _('days'),
'hours' : _('hours'),
'minutes' : _('minutes'),
'seconds' : _('seconds'),
## Single
'week' : _('week'),
'day' : _('day'),
'hour' : _('hour'),
'minute' : _('minute'),
'second' : _('second'),
' and' : _('and'),
',' : _(','), # This is for compatibility
'' : '\0' # same here BUT we CANNOT pass empty string to gettext
# or we get : warning: Empty msgid. It is reserved by GNU gettext:
# gettext("") returns the header entry with
# meta information, not the empty string.
# Thx to --> https://stackoverflow.com/a/30852705/11869956 - saved my day
}
def convert(self, seconds, granularity=2, rounded=True, translate=False):
"""Proceed the conversion"""
def _format(result):
"""Return the formatted result
TODO : numpy / google docstrings"""
start = 1
length = len(result)
none = 0
next_item = False
for item in reversed(result[:]):
if item['value']:
# if we have more than one item
if length - none > 1:
# This is the first 'real' item
if start == 1:
item['punctuation'] = ''
next_item = True
elif next_item:
# This is the second 'real' item
# Happened 'and' to key name
item['punctuation'] = ' and'
next_item = False
# If there is more than two 'real' item
# than happened ','
elif 2 < start:
item['punctuation'] = ','
else:
item['punctuation'] = ''
else:
item['punctuation'] = ''
start += 1
else:
none += 1
return [ { 'value' : mydict['value'],
'name' : mydict['name_strip'],
'punctuation' : mydict['punctuation'] } for mydict in result \
if mydict['value'] is not None ]
def _rstrip(value, name):
"""Rstrip 's' name depending of value"""
if value == 1:
name = name.rstrip('s')
return name
# Make sure granularity is an integer
if not isinstance(granularity, int):
raise ValueError(f'Granularity should be an integer: {granularity}')
# For seconds only don't need to compute
if seconds < 0:
return 'any time now.'
elif seconds < 60:
return 'less than a minute.'
result = []
for name, count in self.intervals.items():
value = seconds // count
if value:
seconds -= value * count
name_strip = _rstrip(value, name)
# save as dict: value, name_strip (eventually strip), name (for reference), value in seconds
# and count (for reference)
result.append({
'value' : value,
'name_strip' : name_strip,
'name' : name,
'seconds' : value * count,
'count' : count
})
else:
if len(result) > 0:
# We strip the name as second == 0
name_strip = name.rstrip('s')
# adding None to key 'value' but keep other value
# in case when need to add seconds when we will
# recompute every thing
result.append({
'value' : None,
'name_strip' : name_strip,
'name' : name,
'seconds' : 0,
'count' : count
})
# Get the length of the list
length = len(result)
# Don't need to compute everything / every time
if length < granularity or not rounded:
if translate:
return ' '.join('{0} {1}{2}'.format(item['value'], _(self.translate[item['name']]),
_(self.translate[item['punctuation']])) \
for item in _format(result))
else:
return ' '.join('{0} {1}{2}'.format(item['value'], item['name'], item['punctuation']) \
for item in _format(result))
start = length - 1
# Reverse list so the firsts elements
# could be not selected depending on granularity.
# And we can delete item after we had his seconds to next
# item in the current list (result)
for item in reversed(result[:]):
if granularity <= start <= length - 1:
# So we have to round
current_index = result.index(item)
next_index = current_index - 1
# skip item value == None
# if the seconds of current item is superior
# to the half seconds of the next item: round
if item['value'] and item['seconds'] > result[next_index]['count'] // 2:
# +1 to the next item (in seconds: depending on item count)
result[next_index]['seconds'] += result[next_index]['count']
# Remove item which is not selected
del result[current_index]
start -= 1
# Ok now recalculate everything
# Reverse as well
for item in reversed(result[:]):
# Check if seconds is superior or equal to the next item
# but not from 'result' list but from 'self.intervals' dict
# Make sure it's not None
if item['value']:
next_item_name = self.nextkey[item['name']]
# This mean we are at weeks
if item['name'] == next_item_name:
# Just recalcul
item['value'] = item['seconds'] // item['count']
item['name_strip'] = _rstrip(item['value'], item['name'])
# Stop to weeks to stay 'right'
elif item['seconds'] >= self.intervals[next_item_name]:
# First make sure we have the 'next item'
# found via --> https://stackoverflow.com/q/26447309/11869956
# maybe there is a faster way to do it ? - TODO
if any(search_item['name'] == next_item_name for search_item in result):
next_item_index = result.index(item) - 1
# Append to
result[next_item_index]['seconds'] += item['seconds']
# recalculate value
result[next_item_index]['value'] = result[next_item_index]['seconds'] // \
result[next_item_index]['count']
# strip or not
result[next_item_index]['name_strip'] = _rstrip(result[next_item_index]['value'],
result[next_item_index]['name'])
else:
# Creating
next_item_index = result.index(item) - 1
# get count
next_item_count = self.intervals[next_item_name]
# convert seconds
next_item_value = item['seconds'] // next_item_count
# strip 's' or not
next_item_name_strip = _rstrip(next_item_value, next_item_name)
# added to dict
next_item = {
'value' : next_item_value,
'name_strip' : next_item_name_strip,
'name' : next_item_name,
'seconds' : item['seconds'],
'count' : next_item_count
}
# insert to the list
result.insert(next_item_index, next_item)
# Remove current item
del result[result.index(item)]
else:
# for current item recalculate
# keys 'value' and 'name_strip'
item['value'] = item['seconds'] // item['count']
item['name_strip'] = _rstrip(item['value'], item['name'])
if translate:
return ' '.join('{0} {1}{2}'.format(item['value'],
_(self.translate[item['name']]),
_(self.translate[item['punctuation']])) \
for item in _format(result))
else:
return ' '.join('{0} {1}{2}'.format(item['value'], item['name'], item['punctuation']) \
for item in _format(result))
To use it:
myformater = FormatTimestamp()
myconverter = myformater.convert(seconds)
granularity = 1 - 5, rounded = True / False, translate = True / False
Some test to show difference:
myformater = FormatTimestamp()
for firstrange in [131440, 563440, 604780, 2419180, 113478160]:
print(f'#### Seconds : {firstrange} ####')
print('\tFull - function: {0}'.format(display_time(firstrange, granularity=5)))
print('\tFull - class: {0}'.format(myformater.convert(firstrange, granularity=5)))
for secondrange in range(1, 6, 1):
print('\tGranularity this answer ({0}): {1}'.format(secondrange,
myformater.convert(firstrange,
granularity=secondrange, translate=False)))
print('\tGranularity Bolton\'s answer ({0}): {1}'.format(secondrange, display_time(firstrange,
granularity=secondrange)))
print()
Seconds : 131440Seconds : 563440Full - function: 1 day, 12 hours, 30 minutes, 40 seconds Full - class: 1 day, 12 hours, 30 minutes and 40 seconds Granularity this answer (1): 2 days Granularity Bolton's answer (1): 1 day Granularity this answer (2): 1 day and 13 hours Granularity Bolton's answer (2): 1 day, 12 hours Granularity this answer (3): 1 day, 12 hours and 31 minutes Granularity Bolton's answer (3): 1 day, 12 hours, 30 minutes Granularity this answer (4): 1 day, 12 hours, 30 minutes and 40 seconds Granularity Bolton's answer (4): 1 day, 12 hours, 30 minutes, 40 seconds Granularity this answer (5): 1 day, 12 hours, 30 minutes and 40 seconds Granularity Bolton's answer (5): 1 day, 12 hours, 30 minutes, 40 seconds
Full - function: 6 days, 12 hours, 30 minutes, 40 seconds
Full - class: 6 days, 12 hours, 30 minutes and 40 seconds
Granularity this answer (1): 1 week
Granularity Bolton's answer (1): 6 days
Granularity this answer (2): 6 days and 13 hours
Granularity Bolton's answer (2): 6 days, 12 hours
Granularity this answer (3): 6 days, 12 hours and 31 minutes
Granularity Bolton's answer (3): 6 days, 12 hours, 30 minutes
Granularity this answer (4): 6 days, 12 hours, 30 minutes and 40 seconds
Granularity Bolton's answer (4): 6 days, 12 hours, 30 minutes, 40 seconds
Granularity this answer (5): 6 days, 12 hours, 30 minutes and 40 seconds
Granularity Bolton's answer (5): 6 days, 12 hours, 30 minutes, 40 seconds
Seconds : 604780
Full - function: 6 days, 23 hours, 59 minutes, 40 seconds
Full - class: 6 days, 23 hours, 59 minutes and 40 seconds
Granularity this answer (1): 1 week
Granularity Bolton's answer (1): 6 days
Granularity this answer (2): 1 week
Granularity Bolton's answer (2): 6 days, 23 hours
Granularity this answer (3): 1 week
Granularity Bolton's answer (3): 6 days, 23 hours, 59 minutes
Granularity this answer (4): 6 days, 23 hours, 59 minutes and 40 seconds
Granularity Bolton's answer (4): 6 days, 23 hours, 59 minutes, 40 seconds
Granularity this answer (5): 6 days, 23 hours, 59 minutes and 40 seconds
Granularity Bolton's answer (5): 6 days, 23 hours, 59 minutes, 40 seconds
Seconds : 2419180
Full - function: 3 weeks, 6 days, 23 hours, 59 minutes, 40 seconds
Full - class: 3 weeks, 6 days, 23 hours, 59 minutes and 40 seconds
Granularity this answer (1): 4 weeks
Granularity Bolton's answer (1): 3 weeks
Granularity this answer (2): 4 weeks
Granularity Bolton's answer (2): 3 weeks, 6 days
Granularity this answer (3): 4 weeks
Granularity Bolton's answer (3): 3 weeks, 6 days, 23 hours
Granularity this answer (4): 4 weeks
Granularity Bolton's answer (4): 3 weeks, 6 days, 23 hours, 59 minutes
Granularity this answer (5): 3 weeks, 6 days, 23 hours, 59 minutes and 40 seconds
Granularity Bolton's answer (5): 3 weeks, 6 days, 23 hours, 59 minutes, 40 seconds
Seconds : 113478160
Full - function: 187 weeks, 4 days, 9 hours, 42 minutes, 40 seconds
Full - class: 187 weeks, 4 days, 9 hours, 42 minutes and 40 seconds
Granularity this answer (1): 188 weeks
Granularity Bolton's answer (1): 187 weeks
Granularity this answer (2): 187 weeks and 4 days
Granularity Bolton's answer (2): 187 weeks, 4 days
Granularity this answer (3): 187 weeks, 4 days and 10 hours
Granularity Bolton's answer (3): 187 weeks, 4 days, 9 hours
Granularity this answer (4): 187 weeks, 4 days, 9 hours and 43 minutes
Granularity Bolton's answer (4): 187 weeks, 4 days, 9 hours, 42 minutes
Granularity this answer (5): 187 weeks, 4 days, 9 hours, 42 minutes and 40 seconds
Granularity Bolton's answer (5): 187 weeks, 4 days, 9 hours, 42 minutes, 40 seconds
I have a french translation ready. But it's fast to do the translation ... just few words. Hope this could help as the other answer help me a lot.
Patching Mr.B's answer (sorry, not enough rep. to comment), we can return variable granularity based on the amount of time. For example, we don't say "1 week, 5 seconds", we just say "1 week":
def display_time(seconds, granularity=2):
result = []
for name, count in intervals:
value = seconds // count
if value:
seconds -= value * count
if value == 1:
name = name.rstrip('s')
result.append("{} {}".format(value, name))
else:
# Add a blank if we're in the middle of other values
if len(result) > 0:
result.append(None)
return ', '.join([x for x in result[:granularity] if x is not None])
Some sample input:
for diff in [5, 67, 3600, 3605, 3667, 24*60*60, 24*60*60+5, 24*60*60+57, 24*60*60+3600, 24*60*60+3667, 2*24*60*60, 2*24*60*60+5*60*60, 7*24*60*60, 7*24*60*60 + 24*60*60]:
print "For %d seconds: %s" % (diff, display_time(diff, 2))
...returns this output:
For 5 seconds: 5 seconds
For 67 seconds: 1 minute, 7 seconds
For 3600 seconds: 1 hour
For 3605 seconds: 1 hour
For 3667 seconds: 1 hour, 1 minute
For 86400 seconds: 1 day
For 86405 seconds: 1 day
For 86457 seconds: 1 day
For 90000 seconds: 1 day, 1 hour
For 90067 seconds: 1 day, 1 hour
For 172800 seconds: 2 days
For 190800 seconds: 2 days, 5 hours
For 604800 seconds: 1 week
For 691200 seconds: 1 week, 1 day
At first glance, I figured divmod would be faster since it's a single statement and a built-in function, but timeit seems to show otherwise. Consider this little example I came up with when I was trying to figure out the fastest method for use in a loop that continuously runs in a gobject idle_add splitting a seconds counter into a human readable time for updating a progress bar label.
import timeit
def test1(x,y, dropy):
while x > 0:
y -= dropy
x -= 1
# the test
minutes = (y-x) / 60
seconds = (y-x) % 60.0
def test2(x,y, dropy):
while x > 0:
y -= dropy
x -= 1
# the test
minutes, seconds = divmod((y-x), 60)
x = 55 # litte number, also number of tests
y = 10000 # make y > x by factor of drop
dropy = 7 # y is reduced this much each iteration, for variation
print "division and modulus:", timeit.timeit( lambda: test1(x,y,dropy) )
print "divmod function:", timeit.timeit( lambda: test2(x,y,dropy) )
The built-in divmod function seems incredibly slower compared to using the simple division and modulus.
division and modulus: 12.5737669468
divmod function: 17.2861430645