5
\$\begingroup\$

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.

\$\endgroup\$

migrated from stackoverflow.com Dec 13 '13 at 5:37

This question came from our site for professional and enthusiast programmers.

  • \$\begingroup\$ Use xrange instead of range and str.format instead of the % operator. But these are just side notes ;) \$\endgroup\$ – jazzpi Nov 26 '13 at 16:12
  • 2
    \$\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\$ – jorispilot Nov 26 '13 at 16:16
  • \$\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\$ – Paul Nov 26 '13 at 16:44
3
\$\begingroup\$
  • Use a ternary operator in "s"[seconds==1:].
  • Use a generator expression to replace the xstr = "%s... lines.
  • The two for loops should use 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 = ("{n} {magnitude}".format(n=int(locals_[magnitude]), magnitude=magnitude)
                      for magnitude in ("days", "hours", "minutes", "seconds") if locals_[magnitude])
    eta_str = ", ".join(magnitudes_str)
\$\endgroup\$
  • \$\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\$ – Paul Nov 26 '13 at 16:42
  • \$\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\$ – Ramchandra Apte Nov 27 '13 at 5:03
3
\$\begingroup\$

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])
\$\endgroup\$
  • \$\begingroup\$ Cool, looks like it might fall foul to "0 hour ago". Plural exists on zero quantities. \$\endgroup\$ – Paul Nov 27 '13 at 18:39
  • \$\begingroup\$ @Paul: Changed i > 1 to i != 1 to address zero quantities. \$\endgroup\$ – Steven Rumbalski Nov 27 '13 at 18:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy