Welcome, guest | Sign In | My Account | Store | Cart

What is it about?

  • I need to say someting like 1 day ago, 5 days ago, 2 weeks ago, ...
  • I can control to have it with/without milliseconds and microseconds.
  • I can use it automatically with current date and time or with a provide one.

Why?

  • I need it for next revision of my recipe 578111.
  • I found recipes here and there but often it is always assumed that a month has 30 days and that a year has 365 days; this is not true. That's why I've left away months and years.
Python, 97 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""
    @author   Thomas Lehmann
    @file     dateBack.py
    @brief    provides a human readable format for a time delta
"""
from datetime import datetime , timedelta

def dateBack(theDateAndTime, precise=False, fromDate=None):
    """ provides a human readable format for a time delta
        @param theDateAndTime this is time equal or older than now or the date in 'fromDate'
        @param precise        when true then milliseconds and microseconds are included
        @param fromDate       when None the 'now' is used otherwise a concrete date is expected
        @return the time delta as text

        @note I don't calculate months and years because those varies (28,29,30 or 31 days a month
              and 365 or 366 days the year depending on leap years). In addition please refer
              to the documentation for timedelta limitations.
    """
    if not fromDate:
        fromDate = datetime.now()

    if theDateAndTime > fromDate:    return None
    elif theDateAndTime == fromDate: return "now"

    delta = fromDate - theDateAndTime

    # the timedelta structure does not have all units; bigger units are converted
    # into given smaller ones (hours -> seconds, minutes -> seconds, weeks > days, ...)
    # but we need all units:
    deltaMinutes      = delta.seconds // 60
    deltaHours        = delta.seconds // 3600
    deltaMinutes     -= deltaHours * 60
    deltaWeeks        = delta.days    // 7
    deltaSeconds      = delta.seconds - deltaMinutes * 60 - deltaHours * 3600
    deltaDays         = delta.days    - deltaWeeks * 7
    deltaMilliSeconds = delta.microseconds // 1000
    deltaMicroSeconds = delta.microseconds - deltaMilliSeconds * 1000

    valuesAndNames =[ (deltaWeeks  ,"week"  ), (deltaDays   ,"day"   ),
                      (deltaHours  ,"hour"  ), (deltaMinutes,"minute"),
                      (deltaSeconds,"second") ]
    if precise:
        valuesAndNames.append((deltaMilliSeconds, "millisecond"))
        valuesAndNames.append((deltaMicroSeconds, "microsecond"))

    text =""
    for value, name in valuesAndNames:
        if value > 0:
            text += len(text)   and ", " or ""
            text += "%d %s" % (value, name)
            text += (value > 1) and "s" or ""

    # replacing last occurrence of a comma by an 'and'
    if text.find(",") > 0:
        text = " and ".join(text.rsplit(", ",1))

    return text

def test():
    """ testing function "dateBack" """
    # we need a date to rely on for testing concrete deltas
    fromDate  = datetime(year=2012, month=4, day=26, hour=8, minute=40, second=45)
    testCases = [
          ("1 second"                             , fromDate-timedelta(seconds=1), False),
          ("5 seconds"                            , fromDate-timedelta(seconds=5), False),
          ("1 minute"                             , fromDate-timedelta(minutes=1), False),
          ("5 minutes"                            , fromDate-timedelta(minutes=5), False),
          ("1 minute and 10 seconds"              , fromDate-timedelta(minutes=1, seconds=10), False),
          ("1 hour"                               , fromDate-timedelta(hours= 1), False),
          ("1 hour and 1 second"                  , fromDate-timedelta(hours=1, seconds=1), False),
          ("1 hour and 1 minute"                  , fromDate-timedelta(hours=1, minutes=1), False),
          ("1 hour, 1 minute and 1 second"        , fromDate-timedelta(hours=1, minutes=1, seconds=1), False),
          ("1 week"                               , fromDate-timedelta(weeks=1), False),
          ("2 weeks"                              , fromDate-timedelta(weeks=2), False),
          ("1 week and 1 second"                  , fromDate-timedelta(weeks=1, seconds=1), False),
          ("1 week and 1 minute"                  , fromDate-timedelta(weeks=1, minutes=1), False),
          ("1 week and 1 hour"                    , fromDate-timedelta(weeks=1, hours=1), False),
          ("1 week, 1 hour, 1 minute and 1 second", fromDate-timedelta(weeks=1, hours=1, minutes=1, seconds=1), False),
          ("1 millisecond"                        , fromDate-timedelta(milliseconds=1),True),
          ("2 milliseconds"                       , fromDate-timedelta(milliseconds=2),True),
          ("1 microsecond"                        , fromDate-timedelta(microseconds=1),True),
          ("2 microseconds"                       , fromDate-timedelta(microseconds=2),True),
          ("1 millisecond and 1 microsecond"      , fromDate-timedelta(milliseconds=1, microseconds=1),True) ]

    for expectedResult, testDate, precise in testCases:
        print("test case for '%s'" % expectedResult)
        calculatedResult = dateBack(testDate, precise=precise, fromDate=fromDate)
        try:    assert expectedResult == calculatedResult
        except: print(" -> error: wrong value: '%s'" % calculatedResult)

    # future date in relation to 'fromDate' (1 hour)
    futureDate     = fromDate + timedelta(hours=1)
    expectedResult = None
    assert expectedResult == dateBack(futureDate, precise=False, fromDate=fromDate)

if __name__ == "__main__":
    test()

A few notes

There's this code and some of you might wonder because it's not really pythonic:

text += len(text)   and ", " or ""

The point is I tried this variant...

text += ", " if len(text)

...but this variant was not accepted by the Python interpeter. Somwhere I've been reading that this is since 2.5 available; I tried Jyton 2.5.3 and Python 3.1.2 but without success

4 comments

Mauricio Dada Fonseca de Freitas 9 years, 9 months ago  # | flag

And why not this?:

if len(text): text += ', '
Daniel Lepage 9 years, 9 months ago  # | flag

The syntax you want is

text += ", " if len(text) else ""

i.e. you need the else clause.

Sharoon Thomas 9 years, 7 months ago  # | flag

Why not use relative delta ??

from dateutil.relativedelta import relativedelta

attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
human_readable = lambda delta: [
    '%d %s' % (getattr(delta, attr), getattr(delta, attr) > 1 and attr or attr[:-1]) 
        for attr in attrs if getattr(delta, attr)
]

Example usage:

>>> human_readable(relativedelta(minutes=125))
['2 hours', '5 minutes']
>>> human_readable(relativedelta(hours=(24 * 365) + 1))
['365 days', '1 hour']
Carlos Hanson 6 years, 6 months ago  # | flag

Since I really like Sharoon Thomas' solution, I want to share a couple modifications:

human_readable = lambda delta: [
    '%d %s ago' % (getattr(delta, attr), getattr(delta, attr) != 1 and attr or attr[:-1])
        for attr in attrs if getattr(delta, attr) or attr == attrs[-1]
]

I use this to find how long ago something was modified. If there was no difference in time, the original function returned an empty list. By adding "attr == attrs[-1]", I ensure that I return a value of ['0 seconds'].

>>> from datetime import datetime
>>> now = datetime.now()
>>> then = now
>>> human_readable(relativedelta(now, then))
['0 seconds ago']

I also added 'ago' to the string, since that is how I'm using it.

The last item I added was the test for whether the time is plural or not:

original: getattr(delta, attr) > 1 and attr or attr[:-1]
modified: getattr(delta, attr) != 1 and attr or attr[:-1]

We say '0 seconds' not '0 second'. I also accidentally tested a negative value and found '-2 minute' should be '-2 minutes'. The modified version says 1 is singular, everything else is plural.

Thanks for this function.