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 --> //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 //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, # //www.calculateme.com/time/years/to-seconds/
# //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 --> //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