Human readable duration format python

Efficient human readable timedelta

I wanted to write a human readable datetime.timedelta that can be used in log files. Eg, «Report issued 1 hour, 44 minutes, 20 seconds ago» I noticed that casting a timedelta to str() generates something almost like what I want, but not quite. To this end I wrote this:

def verbose_timedelta(delta): hours, remainder = divmod(delta.seconds, 3600) minutes, seconds = divmod(remainder, 60) dstr = "%s day%s" % (delta.days, "s"[delta.days==1:]) hstr = "%s hour%s" % (hours, "s"[hours==1:]) mstr = "%s minute%s" % (minutes, "s"[minutes==1:]) sstr = "%s second%s" % (seconds, "s"[seconds==1:]) dhms = [dstr, hstr, mstr, sstr] for x in range(len(dhms)): if not dhms[x].startswith('0'): dhms = dhms[x:] break dhms.reverse() for x in range(len(dhms)): if not dhms[x].startswith('0'): dhms = dhms[x:] break dhms.reverse() return ', '.join(dhms) 

Essentially, it’s shaving off both ends of a list to make the results more meaningful. The code above feels clunky though. Is there a more «Pythonic» way to do it? I’m using Python 2.7.3.

\$\begingroup\$ Use xrange instead of range and str.format instead of the % operator. But these are just side notes 😉 \$\endgroup\$

\$\begingroup\$ You can also use «s» if hours==1 else «» instead of «s»[hours==1:] . This goes with the «Explicit is better than implicit.» Python idiom. \$\endgroup\$

\$\begingroup\$ Thanks. The main part I wanted to clear up was the two for loops and the double reversing. This seems like something python could do in a single line. Maybe. Hmm. \$\endgroup\$

3 Answers 3

  • Use a ternary operator in «s»[seconds==1:] .
  • Use a generator expression to replace the xstr http://docs.python.org/2/library/functions.html#enumerate» rel=»nofollow»> enumerate() , i.e. they could be for s, i in range(. ) .
  • The two for loops should be moved into a for _ in range(2): .
  • i is preferred over x when using an i​ncrementing i​ndex counter.
  • The filtering of the redundant strings which the for-loop does could be done earlier so that the number to string code can be modified but the filtering code will not require adjustments.

PS: I have implemented a similar function here:

 days, rem = divmod(seconds, 86400) hours, rem = divmod(rem, 3600) minutes, seconds = divmod(rem, 60) if seconds < 1:seconds = 1 locals_ = locals() magnitudes_str = ("".format(n=int(locals_[magnitude]), magnitude=magnitude) for magnitude in ("days", "hours", "minutes", "seconds") if locals_[magnitude]) eta_str = ", ".join(magnitudes_str) 

\$\begingroup\$ I really like this because it's teaching me how to get under the belly of Python. However, it doesn't quite do what I would like. Or what my function above does. Your code could return something like this: "7 days, 5 seconds" Ie because hours and minutes aren't set they're not shown. I don't like this, but purely on aesthetic grounds. For me, intervening zero time intervals should be shown, eg "7 days, 0 hours, 6 minutes" not "7 days, 6 minutes". \$\endgroup\$

\$\begingroup\$ @Paul Yes, it doesn't do exactly the same thing as yours. I should update it to shave off both the ends like yours does. \$\endgroup\$

\$\begingroup\$ I'm not sure if using locals() is considered best practice. I probably rather give it a dict instead. Makes it easier to adapt it for other languages, too. \$\endgroup\$

\$\begingroup\$ I can't understand this line: if seconds < 1:seconds = 1 . If seconds < 1 , then that's that. I don't understand why it needs to be assigned with 1. \$\endgroup\$

Читайте также:  Сколько зарабатывают java разработчики

Pythonic is in the eye of the beholder, but here's my stab at it:

def verbose_timedelta(delta): d = delta.days h, s = divmod(delta.seconds, 3600) m, s = divmod(s, 60) labels = ['day', 'hour', 'minute', 'second'] dhms = ['%s %s%s' % (i, lbl, 's' if i != 1 else '') for i, lbl in zip([d, h, m, s], labels)] for start in range(len(dhms)): if not dhms[start].startswith('0'): break for end in range(len(dhms)-1, -1, -1): if not dhms[end].startswith('0'): break return ', '.join(dhms[start:end+1]) 

\$\begingroup\$ Cool, looks like it might fall foul to "0 hour ago". Plural exists on zero quantities. \$\endgroup\$

  1. From the 1st look, I see quite a lot of duplication. When you see repeated code try to somehow unify it. But not at whatever cost. Everything has a limit 😉 But the s suffix handling can be definitely unified somehow.
  2. There is useful timedelta.total_seconds() method. It's better to use it than handling the attributes separately.
  3. I think also the handling of zeros should be done in reverse. You should filter them out by comparing hours > 0 etc. and only then format the data into a string. Numbers are much better data for processing in your case and the converting to the UI (in this case "string") should be the last piece of the code.

I would like to add my readable version of the readable timedelta in Python 3 🙂 Hope it helps you and other people how to make it a little more readable. It's not the exact version as yours but still, there are some useful hints about data structures and the program flow.

def readable_timedelta(duration: timedelta): data = <> data['days'], remaining = divmod(duration.total_seconds(), 86_400) data['hours'], remaining = divmod(remaining, 3_600) data['minutes'], data['seconds'] = divmod(remaining, 60) time_parts = [f' ' for name, value in data.items() if value > 0] if time_parts: return ' '.join(time_parts) else: return 'below 1 second' 

It does not handle the s suffixes so it will produce 1 minutes and similar, but that is no big deal for me. It should be easy to add also suffix handling. For example like this:

def readable_timedelta(duration: timedelta): data = <> data['days'], remaining = divmod(duration.total_seconds(), 86_400) data['hours'], remaining = divmod(remaining, 3_600) data['minutes'], data['seconds'] = divmod(remaining, 60) time_parts = ((name, round(value)) for name, value in data.items()) time_parts = [f' ' for name, value in time_parts if value > 0] if time_parts: return ' '.join(time_parts) else: return 'below 1 second' 
@pytest.mark.parametrize('duration, readable_duration', [ (timedelta(), 'below 1 second'), (timedelta(seconds=29), '29 seconds'), (timedelta(seconds=61), '1 minutes 1 seconds'), (timedelta(minutes=5, seconds=66), '6 minutes 6 seconds'), (timedelta(hours=4, minutes=0, seconds=5), '4 hours 5 seconds'), (timedelta(hours=48, minutes=5), '2 days 5 minutes'), ]) def test_readable_timedelta(duration, readable_duration): assert readable_timedelta(duration) == readable_duration 

Источник

Human readable duration format python

Your task in order to complete this Kata is to write a function which formats a duration, given as a number of seconds, in a human-friendly way.
The function must accept a non-negative integer. If it is zero, it just returns “now”. Otherwise, the duration is expressed as a combination of years, days, hours, minutes and seconds.
It is much easier to understand with an example:
format_duration(62) # returns “1 minute and 2 seconds”
format_duration(3662) # returns “1 hour, 1 minute and 2 seconds”
For the purpose of this Kata, a year is 365 days and a day is 24 hours.
Note that spaces are important.
Detailed rules
The resulting expression is made of components like 4 seconds, 1 year, etc. In general, a positive integer and one of the valid units of time, separated by a space. The unit of time is used in plural if the integer is greater than 1.
The components are separated by a comma and a space (", "). Except the last component, which is separated by " and ", just like it would be written in English.
A more significant units of time will occur before than a least significant one. Therefore, 1 second and 1 year is not correct, but 1 year and 1 second is.
Different components have different unit of times. So there is not repeated units like in 5 seconds and 1 second.
A component will not appear at all if its value happens to be zero. Hence, 1 minute and 0 seconds is not valid, but it should be just 1 minute.
A unit of time must be used “as much as possible”. It means that the function should not return 61 seconds, but 1 minute and 1 second instead. Formally, the duration specified by of a component must not be greater than any valid more significant unit of time.

My thoughts:

This question is very simple to calculate the numbers, mainly for format adjustments.

If the number is 0, it does not need to appear; if the number is 1, the unit is in singular form; if the number is greater than 1, the unit is in plural form.

In the overall format, if there is more than one part, the format is: (XX,) XX and XX; if there is only one part, it is: XX.

I constructed two functions,form() with readable(), Solve these two problems separately.

def format_duration(seconds): if seconds == 0: return "now" y = seconds // (3600*24*365) d = (seconds % (3600*24*365))//(3600*24) h = (seconds % (3600*24)) // 3600 m = (seconds % 3600) // 60 s = seconds % 60 return readable([form(y,"year"),form(d,"day"),form(h,"hour"),form(m,"minute"),form(s,"second")]) def form(n,unit): if n == 0: return "" if n == 1: return "1 "+ unit return str(n) + " " + unit + "s" def readable(l): l = list(filter(None, l)) result = "" if len(l) == 1: return l[0] for i in range(len(l)-1): result = result + ", " + l[i] result = result + " and " + l[len(l)-1] return result[2:] 

Most clever:

After finishing the question, look at the answer with the most clever votes:

times = [("year", 365 * 24 * 60 * 60), ("day", 24 * 60 * 60), ("hour", 60 * 60), ("minute", 60), ("second", 1)] def format_duration(seconds): if not seconds: return "now" chunks = [] for name, secs in times: qty = seconds // secs if qty: if qty > 1: name += "s" chunks.append(str(qty) + " " + name) seconds = seconds % secs return ', '.join(chunks[:-1]) + ' and ' + chunks[-1] if len(chunks) > 1 else chunks[0] 

He puts the numerical calculation in the main loop, which is more convenient. Regarding the singular and plural forms of units and the adjustment of the entire sentence format, although the presentation methods are different, the ideas are the same. In order to realize my own thinking, I split the function into several pieces to realize separately.

Источник

kyu_4.human_readable_duration_format package¶

kyu_4.human_readable_duration_format.format_duration module¶

A function which formats a duration, given as a number of seconds, in a human-friendly way.

kyu_4.human_readable_duration_format.format_duration. calc_days ( seconds : int ) → int [source] ¶

kyu_4.human_readable_duration_format.format_duration. calc_hours ( seconds : int ) → int [source] ¶

kyu_4.human_readable_duration_format.format_duration. calc_minutes ( seconds : int ) → int [source] ¶

kyu_4.human_readable_duration_format.format_duration. calc_seconds ( seconds : int ) → int [source] ¶

kyu_4.human_readable_duration_format.format_duration. calc_years ( seconds : int ) → int [source] ¶

kyu_4.human_readable_duration_format.format_duration. format_duration ( seconds : int ) → str [source] ¶

A function which formats a duration, given as a number of seconds, in a human-friendly way.

The resulting expression is made of components like 4 seconds, 1 year, etc. In general, a positive integer and one of the valid units of time, separated by a space. The unit of time is used in plural if the integer is greater than 1.

The components are separated by a comma and a space (“, “). Except the last component, which is separated by ” and “, just like it would be written in English.

A more significant units of time will occur before than a least significant one. Therefore, 1 second and 1 year is not correct, but 1 year and 1 second is.

Different components have different unit of times. So there is not repeated units like in 5 seconds and 1 second.

A component will not appear at all if its value happens to be zero. Hence, 1 minute and 0 seconds is not valid, but it should be just 1 minute.

A unit of time must be used “as much as possible”. It means that the function should not return 61 seconds, but 1 minute and 1 second instead. Formally, the duration specified by of a component must not be greater than any valid more significant unit of time.

kyu_4.human_readable_duration_format.format_duration. get_string ( number : int , string : str ) → str [source] ¶

Concatenate string result

kyu_4.human_readable_duration_format.test_format_duration module¶

class kyu_4.human_readable_duration_format.test_format_duration. FormatDurationTestCase ( methodName = 'runTest' ) [source] ¶

Test a function which formats a duration, given as a number of seconds, in a human-friendly way.

The function must accept a non-negative integer. If it is zero, it just returns “now”. Otherwise, the duration is expressed as a combination of years, days, hours, minutes and seconds. :return:

Источник

Оцените статью