Human-readable time intervals in Python
Expressing time intervals in a format that is readable by humans is an old problem that every frontend developer will eventually face. While there are plenty of solutions available (this one is my favourite), I required a function that could provide different levels of granularity in it’s “human-readable” description of time.
Take, for example, a component that tells the user how long in aggregate they have been using a service. If they have been using it for a few hours, it would be appropriate to display ## hours, ## minutes
, ignoring the seconds and milliseconds. If they have been using the service for several years, it would probably not make sense to provide minute-level-accuracy – simply ## years, ## days
would suffice. So for this situation, granularity=2
seems appropriate.
If you are creating a countdown timer however, it may be more useful to have a higher level of granularity, displaying days, hours, minutes and seconds (granularity=4
).
The below script contains two functions: time_breakdown
, which handles the unit conversions, and time_breakdown_string
, which handles the “human readability” logic:
semantic_time.py
from math import floor
def time_breakdown(ms):
"""Converts an integer representing number of milliseconds into a dictionary
representing days, hours, minutes, seconds and milliseconds. Output is
rounded down to the nearest whole number.
Parameters:
ms (integer): number of milliseconds.
Returns:
(dictionary): breakdown of total days, hours, minutes, seconds and
milliseconds.
Example usage:
>>> time_breakdown = time_breakdown(123456789)
{'day': 1, 'hr': 10, 'min': 17, 'sec': 36, 'ms': 789}
"""
ms_out = ms % 1000;
sec = floor((ms % (1000 * 60)) / 1000)
min = floor((ms % (1000 * 60 * 60)) / 1000 / 60)
hr = floor((ms % (1000 * 60 * 60 * 24)) / 1000 / 60 / 60)
day = floor(
(ms % (1000 * 60 * 60 * 24 * 365)) / 1000 / 60 / 60 / 24
)
return {
"day": day,
"hr": hr,
"min": min,
"sec": sec,
"ms": round(ms_out)
}
def time_breakdown_string(ms, granularity=5):
"""Converts an integer representing number of milliseconds into a string that
uses natural language to represent the time quantity.
Parameters:
ms (integer): number of milliseconds.
granularity (integer): the level of detail required.
Returns:
(string): string that uses natural language to represent the time quantity.
Example usage:
>>> time_breakdown_string(123456789)
'1 day 10 hours 17 minutes 36 seconds 789 milliseconds'
>>>time_breakdown_string(
123456789,
granularity=3)
'1 day 10 hours 17 minutes'
"""
time = time_breakdown(ms)
time_string = ""
if time["day"] != 0:
time_string += str(time["day"]) + " day"
if time["day"] != 1:
time_string += "s"
if time["hr"] != 0:
time_string += " " + str(time["hr"]) + " hour"
if time["hr"] != 1:
time_string += "s"
if time["min"] != 0:
time_string += " " + str(time["min"]) + " minute"
if time["min"] != 1:
time_string += "s"
if time["sec"] != 0:
time_string += " " + str(time["sec"]) + " second"
if time["sec"] != 1:
time_string += "s"
if time["ms"] != 0:
time_string += " " + str(time["ms"]) + " millisecond"
if time["ms"] != 1:
time_string += "s"
#filter to specified granularity
time_string = time_string.strip().split(" ")[:2*granularity]
return " ".join(time_string)
Example implementation
To test it out, add the below code at the bottom of semantic_time.py
:
def main():
from datetime import datetime
mercury_orbit_period = 7600608000
print(f"""
To orbit the sun, it takes Mercury:
{time_breakdown(mercury_orbit_period)}
{time_breakdown_string(mercury_orbit_period, 1)}
{time_breakdown_string(mercury_orbit_period, 2)}
{time_breakdown_string(mercury_orbit_period, 3)}
{time_breakdown_string(mercury_orbit_period, 4)}
""")
one_day_minus_one_second = 1*24*60*60*1000 - 1000
one_day_exactly = 1*24*60*60*1000
one_day_plus_one_second = 1*24*60*60*1000 + 1000
one_day_plus_one_minute = 1*24*60*60*1000 + 1000*60
print(f"""
Ignores 'zero' units:
{time_breakdown(one_day_minus_one_second)}
{time_breakdown_string(one_day_minus_one_second)}
{time_breakdown(one_day_exactly)}
{time_breakdown_string(one_day_exactly)}
{time_breakdown(one_day_plus_one_second)}
{time_breakdown_string(one_day_plus_one_second)}
{time_breakdown(one_day_plus_one_minute)}
{time_breakdown_string(one_day_plus_one_minute)}
""")
one_second = 1000
two_seconds = 2000
one_minute_one_second = 361000
two_minute_one_second = 721000
two_minute_two_second = 722000
print(f"""
Plurality is considered:
{time_breakdown_string(one_second)}
{time_breakdown_string(two_seconds)}
{time_breakdown_string(one_minute_one_second)}
{time_breakdown_string(two_minute_one_second)}
{time_breakdown_string(two_minute_two_second)}
""")
if __name__ == "__main__":
main()
Example output
To orbit the sun, it takes Mercury:
{'day': 87, 'hr': 23, 'min': 16, 'sec': 48, 'ms': 0}
87 days
87 days 23 hours
87 days 23 hours 16 minutes
87 days 23 hours 16 minutes 48 seconds
Ignores 'zero' units:
{'day': 0, 'hr': 23, 'min': 59, 'sec': 59, 'ms': 0}
23 hours 59 minutes 59 seconds
{'day': 1, 'hr': 0, 'min': 0, 'sec': 0, 'ms': 0}
1 day
{'day': 1, 'hr': 0, 'min': 0, 'sec': 1, 'ms': 0}
1 day 1 second
{'day': 1, 'hr': 0, 'min': 1, 'sec': 0, 'ms': 0}
1 day 1 minute
Plurality is considered:
1 second
2 seconds
6 minutes 1 second
12 minutes 1 second
12 minutes 2 seconds