9
function timeSince(date) {

    var seconds = Math.floor((new Date() - date) / 1000);

    var interval = Math.floor(seconds / 31536000);

    if (interval > 1) {
        return interval + " years";
    }
    interval = Math.floor(seconds / 2592000);
    if (interval > 1) {
        return interval + " months";
    }
    interval = Math.floor(seconds / 86400);
    if (interval > 1) {
        return interval + " days";
    }
    interval = Math.floor(seconds / 3600);
    if (interval > 1) {
        return interval + " hours";
    }
    interval = Math.floor(seconds / 60);
    if (interval > 1) {
        return interval + " minutes";
    }
    return Math.floor(seconds) + " seconds";
}

Post an implementation in your language of choice.

7
  • Is there a question? This is more suitable for Stack Overflow. Jul 5, 2010 at 3:34
  • It's a dev-tip... maybe we should play Jepoardy here as well? Jul 5, 2010 at 3:48
  • 1
    no, there is no question. it is a tip on how to present dates in a friendly format, similar to that used by the stack exchange sites, whose data we are consuming via the api. If you wanted to format a list of items that have dates, as most do, this might be useful information. I know I had to spend some time figuring it out, no need for others. I am not sure why or how I would post this on stack overflow and am sure that it would not be discoverable. Jul 5, 2010 at 3:48
  • 1
    @code poet: I'd still post on Stack Overflow and try to give it a good title. This is something lots of people will find useful at some point.
    – Edan Maor
    Jul 5, 2010 at 7:31
  • @Edan - you are probably right. I will, although I want this post to remain in the relative quiet and more contextually appropriate stackapps as well, so hopefully it won't suffer 'exact duplicate' fate. dupe cops miss the point sometimes. Jul 5, 2010 at 7:52
  • Follow-up question: There are some slight errors on the elapsed times displayed on the site due to treating all months uniformly as 30 days and ignoring leap years. Do you think it's a good idea to keep these errors to match the site (see my answer) or should we try to fix them? Jul 7, 2010 at 4:19
  • 1
    @bill - month is pretty low resolution and the effort doesn't seem worth it. In any case, as george has pointed out, anything past 2 days is a formatted date. I just have not gotten around to porting his php to javascript, which I plan to do. Jul 7, 2010 at 4:29

7 Answers 7

5

For all the jQuery developers, there is - as always - a decent plugin available: timeago...

Bonus: comes with auto-refresh, supports a semantic approach and is kind of progressive enhancement-ish, since it will turn something like

<abbr class="timeago" title="2008-07-17T09:24:17Z">July 17, 2008</abbr>

into timestamps like

about a year ago

As ususal, one line of jQuery does the job...

jQuery(document).ready(function() {
    jQuery("abbr.timeago").timeago();
});

Having that said, I want to emphasize that I don't know the author of this plugin in any way and don't get paid for making propaganda for it =)

1
  • I use this as the primary method, and I use my method below for server side calculations... for users without JS enabled. Nov 15, 2010 at 19:33
2

Here's a Java implementation that was heavily influenced by the answers on the SO question How do I calculate relative time?, particularly @Jeff's and the accepted answer by @Vincent Robert.

/**
 * Formats the time elapsed between fromDate and now.
 * See https://stackoverflow.com/questions/11/how-do-i-calculate-relative-time/
 * 
 * @param fromDate the start time, a unix epoch datetime stamp.
 * @return the formatted time elapsed.
 */
public static String formatElapsedTime(long fromDate) {
    int SECOND = 1;
    int MINUTE = 60 * SECOND;
    int HOUR = 60 * MINUTE;
    int DAY = 24 * HOUR;
    int MONTH = 30 * DAY;

    long now = (new Date()).getTime();
    long delta = (now / 1000) - fromDate;

    if (delta < 1 * MINUTE) {
        return delta == 1 ? "one second ago" : delta + " seconds ago";
    }
    if (delta < 2 * MINUTE) {
        return "a minute ago";
    }
    if (delta < 45 * MINUTE) {
        return (delta / MINUTE) + " minutes ago";
    }
    if (delta < 90 * MINUTE) {
        return "an hour ago";
    }
    if (delta < 24 * HOUR) {
        return (delta / HOUR) + " hours ago";
    }
    if (delta < 48 * HOUR) {
        return "yesterday";
    }
    if (delta < 30 * DAY) {
        return (delta / DAY) + " days ago";
    }
    if (delta < 12 * MONTH) {
        int months = (int)(Math.floor((double) ((delta / DAY) / 30)));
        return months <= 1 ? "one month ago" : months + " months ago";
    } else {
        int years = (int)(Math.floor((double) (delta / DAY) / 365));
        return years <= 1 ? "one year ago" : years + " years ago";
    }
}

Here are some passing JUnit tests that show what kind of results you can expect.

@Test
public void testFormatElapsedTime() {
    long now = (new Date()).getTime() / 1000;

    assertEquals( "one second ago", formatElapsedTime(now - 1) );
    assertEquals( "2 seconds ago", formatElapsedTime(now - 2) );
    assertEquals( "30 seconds ago", formatElapsedTime(now - 30) );
    assertEquals( "59 seconds ago", formatElapsedTime(now - 59) );

    assertEquals( "a minute ago", formatElapsedTime(now - 60) );
    assertEquals( "a minute ago", formatElapsedTime(now - 61) );
    assertEquals( "a minute ago", formatElapsedTime(now - 119) );

    assertEquals( "2 minutes ago", formatElapsedTime(now - 120) );
    assertEquals( "2 minutes ago", formatElapsedTime(now - 121) );
    assertEquals( "2 minutes ago", formatElapsedTime(now - 179) );
    assertEquals( "3 minutes ago", formatElapsedTime(now - 180) );
    assertEquals( "44 minutes ago", formatElapsedTime(now - (44 * 60)) );

    assertEquals( "an hour ago", formatElapsedTime(now - (45 * 60)) );
    assertEquals( "an hour ago", formatElapsedTime(now - (89 * 60)) );
    assertEquals( "1 hours ago", formatElapsedTime(now - (90 * 60)) );
    assertEquals( "1 hours ago", formatElapsedTime(now - (119 * 60)) );
    assertEquals( "2 hours ago", formatElapsedTime(now - (120 * 60)) );
    assertEquals( "2 hours ago", formatElapsedTime(now - (179 * 60)) );
    assertEquals( "3 hours ago", formatElapsedTime(now - (180 * 60)) );
    assertEquals( "23 hours ago", formatElapsedTime(now - (23 * 60 * 60)) );

    assertEquals( "yesterday", formatElapsedTime(now - (24 * 60 * 60)) );
    assertEquals( "yesterday", formatElapsedTime(now - (47 * 60 * 60)) );
    assertEquals( "2 days ago", formatElapsedTime(now - (48 * 60 * 60)) );
    assertEquals( "3 days ago", formatElapsedTime(now - (3 * 24 * 60 * 60)) );
    assertEquals( "14 days ago", formatElapsedTime(now - (14 * 24 * 60 * 60)) );
    assertEquals( "29 days ago", formatElapsedTime(now - (29 * 24 * 60 * 60)) );

    assertEquals( "one month ago", formatElapsedTime(now - (30 * 24 * 60 * 60)) );
    assertEquals( "one month ago", formatElapsedTime(now - (59 * 24 * 60 * 60)) );
    assertEquals( "2 months ago", formatElapsedTime(now - (60 * 24 * 60 * 60)) );
    assertEquals( "2 months ago", formatElapsedTime(now - (89 * 24 * 60 * 60)));
    assertEquals( "3 months ago", formatElapsedTime(now - (3 * 30 * 24 * 60 * 60)) );
    assertEquals( "11 months ago", formatElapsedTime(now - (11 * 30 * 24 * 60 * 60)) );

    assertEquals( "one year ago", formatElapsedTime(now - (12 * 30 * 24 * 60 * 60)) );
    assertEquals( "one year ago", formatElapsedTime(now - (18 * 30 * 24 * 60 * 60)) );
    assertEquals( "one year ago", formatElapsedTime(now - (23 * 30 * 24 * 60 * 60)) );

    // a little bit of error in the calculation shows up here.
    assertEquals( "one year ago", formatElapsedTime(now - (24 * 30 * 24 * 60 * 60)) );
    assertEquals( "2 years ago", formatElapsedTime(now - (25 * 30 * 24 * 60 * 60)) );
}
4
  • your name in lights: stackapps.com/questions/1009/… Aug 10, 2010 at 7:44
  • "an hour ago" vs. "1 hours ago" ?? Aug 13, 2010 at 20:10
  • @Juha: Yeah, I'm not crazy about that either. It's an artifact of the way that "an hour ago" is defined as 45 - 90 minutes ago and anything more than that (within the last 24 hours) is displayed as an integer number of hours. I wanted to stay true to the original implementation though. Aug 13, 2010 at 20:28
  • Actually SO does not currently use this algorithm. It says "1 hour ago" for 60-120 minutes ago. Aug 13, 2010 at 20:51
2

C# and Javascript port of Bill's answer

Works for me - adding it to both Soapi libraries. See the JS version in action here

One thing to keep in mind is that dates coming from the API are UTC. In order to get a viable diff, you need to convert your Now to UTC.

c#

    /// <summary>
    /// Formats the time elapsed between fromDate and now.
    /// See http://stackoverflow.com/questions/11/how-do-i-calculate-relative-time/
    /// and http://stackapps.com/questions/1009/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site/1034#1034
    /// </summary>
    /// <param name="fromDate">the start time</param>
    /// <returns>the formatted time elapsed.</returns>
    /// <remarks>A direct port of Bill the Lizards answer</remarks>
    public static string FormatElapsedTime(DateTime fromDate)
    {
        const int second = 1;
        const int minute = 60 * second;
        const int hour = 60 * minute;
        const int day = 24 * hour;
        const int month = 30 * day;


        int delta = (int) (DateTime.Now.ToUniversalTime() - fromDate).TotalSeconds;

        if (delta < 1 * minute)
        {
            return delta == 1 ? "one second ago" : delta + " seconds ago";
        }
        if (delta < 2 * minute)
        {
            return "a minute ago";
        }
        if (delta < 45 * minute)
        {
            return (delta / minute) + " minutes ago";
        }
        if (delta < 90 * minute)
        {
            return "an hour ago";
        }
        if (delta < 24 * hour)
        {
            return (delta / hour) + " hours ago";
        }
        if (delta < 48 * hour)
        {
            return "yesterday";
        }
        if (delta < 30 * day)
        {
            return (delta / day) + " days ago";
        }
        if (delta < 12 * month)
        {
            int months = (int) (Math.Floor(((delta / day) / 30)));
            return months <= 1 ? "one month ago" : months + " months ago";
        }

        int years = (int)(Math.Floor((delta / day) / 365));
        return years <= 1 ? "one year ago" : years + " years ago";
    }
}

JavaScript

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title></title>
    <script type="text/javascript">

        function formatElapsedTime(fromDate)
        {
            var second = 1;
            var minute = 60 * second;
            var hour = 60 * minute;
            var day = 24 * hour;
            var month = 30 * day;

            var delta = (new Date() - fromDate) / 1000;

            if (delta < 1 * minute)
            {
                return delta == 1 ? "one second ago" : Math.floor(delta) + " seconds ago";
            }
            if (delta < 2 * minute)
            {
                return "a minute ago";
            }
            if (delta < 45 * minute)
            {
                return Math.floor(delta / minute) + " minutes ago";
            }
            if (delta < 90 * minute)
            {
                return "an hour ago";
            }
            if (delta < 24 * hour)
            {
                return Math.floor(delta / hour) + " hours ago";
            }
            if (delta < 48 * hour)
            {
                return "yesterday";
            }
            if (delta < 30 * day)
            {
                return Math.floor(delta / day) + " days ago";
            }
            if (delta < 12 * month)
            {
                var months = Math.floor((delta / day) / 30);
                return months <= 1 ? "one month ago" : months + " months ago";
            }
            var years = Math.floor((delta / day) / 365);
            return years <= 1 ? "one year ago" : years + " years ago";
        }


        function tests()
        {

            function assertEquals(expected, actual)
            {
                var equals = expected == actual;
                var output = document.getElementById("output");
                var div = document.createElement("div");
                div.innerHTML = (equals ? "passed" : "FAILED") + " : expected " + expected + " and got " + actual;
                output.appendChild(div)
            }

            var now = (new Date()).getTime();

            assertEquals("one second ago", formatElapsedTime(now - 1000));
            assertEquals("2 seconds ago", formatElapsedTime(now - 2000));
            assertEquals("30 seconds ago", formatElapsedTime(now - 30000));
            assertEquals("59 seconds ago", formatElapsedTime(now - 59000));

            assertEquals("a minute ago", formatElapsedTime(now - 60000));
            assertEquals("a minute ago", formatElapsedTime(now - 61000));
            assertEquals("a minute ago", formatElapsedTime(now - 119000));

            assertEquals("2 minutes ago", formatElapsedTime(now - 120000));
            assertEquals("2 minutes ago", formatElapsedTime(now - 121000));
            assertEquals("2 minutes ago", formatElapsedTime(now - 179000));
            assertEquals("3 minutes ago", formatElapsedTime(now - 180000));
            assertEquals("44 minutes ago", formatElapsedTime(now - (44 * 60000)));

            assertEquals("an hour ago", formatElapsedTime(now - (45 * 60000)));
            assertEquals("an hour ago", formatElapsedTime(now - (89 * 60000)));
            assertEquals("1 hours ago", formatElapsedTime(now - (90 * 60000)));
            assertEquals("1 hours ago", formatElapsedTime(now - (119 * 60000)));
            assertEquals("2 hours ago", formatElapsedTime(now - (120 * 60000)));
            assertEquals("2 hours ago", formatElapsedTime(now - (179 * 60000)));
            assertEquals("3 hours ago", formatElapsedTime(now - (180 * 60000)));
            assertEquals("23 hours ago", formatElapsedTime(now - (23 * 60 * 60000)));

            assertEquals("yesterday", formatElapsedTime(now - (24 * 60 * 60000)));
            assertEquals("yesterday", formatElapsedTime(now - (47 * 60 * 60000)));
            assertEquals("2 days ago", formatElapsedTime(now - (48 * 60 * 60000)));
            assertEquals("3 days ago", formatElapsedTime(now - (3 * 24 * 60 * 60000)));
            assertEquals("14 days ago", formatElapsedTime(now - (14 * 24 * 60 * 60000)));
            assertEquals("29 days ago", formatElapsedTime(now - (29 * 24 * 60 * 60000)));

            assertEquals("one month ago", formatElapsedTime(now - (30 * 24 * 60 * 60000)));
            assertEquals("one month ago", formatElapsedTime(now - (59 * 24 * 60 * 60000)));
            assertEquals("2 months ago", formatElapsedTime(now - (60 * 24 * 60 * 60000)));
            assertEquals("2 months ago", formatElapsedTime(now - (89 * 24 * 60 * 60000)));
            assertEquals("3 months ago", formatElapsedTime(now - (3 * 30 * 24 * 60 * 60000)));
            assertEquals("11 months ago", formatElapsedTime(now - (11 * 30 * 24 * 60 * 60000)));

            assertEquals("one year ago", formatElapsedTime(now - (12 * 30 * 24 * 60 * 60000)));
            assertEquals("one year ago", formatElapsedTime(now - (18 * 30 * 24 * 60 * 60000)));
            assertEquals("one year ago", formatElapsedTime(now - (23 * 30 * 24 * 60 * 60000)));

            // a little bit of error in the calculation shows up here.
            assertEquals("one year ago", formatElapsedTime(now - (24 * 30 * 24 * 60 * 60000)));
            assertEquals("2 years ago", formatElapsedTime(now - (25 * 30 * 24 * 60 * 60000)));
        }
    </script>
</head>
<body onload="tests()">
    <div id="output">
    </div>
</body>
</html>

Test Results

passed : expected one second ago and got one second ago
passed : expected 2 seconds ago and got 2 seconds ago
passed : expected 30 seconds ago and got 30 seconds ago
passed : expected 59 seconds ago and got 59 seconds ago
passed : expected a minute ago and got a minute ago
passed : expected a minute ago and got a minute ago
passed : expected a minute ago and got a minute ago
passed : expected 2 minutes ago and got 2 minutes ago
passed : expected 2 minutes ago and got 2 minutes ago
passed : expected 2 minutes ago and got 2 minutes ago
passed : expected 3 minutes ago and got 3 minutes ago
passed : expected 44 minutes ago and got 44 minutes ago
passed : expected an hour ago and got an hour ago
passed : expected an hour ago and got an hour ago
passed : expected 1 hours ago and got 1 hours ago
passed : expected 1 hours ago and got 1 hours ago
passed : expected 2 hours ago and got 2 hours ago
passed : expected 2 hours ago and got 2 hours ago
passed : expected 3 hours ago and got 3 hours ago
passed : expected 23 hours ago and got 23 hours ago
passed : expected yesterday and got yesterday
passed : expected yesterday and got yesterday
passed : expected 2 days ago and got 2 days ago
passed : expected 3 days ago and got 3 days ago
passed : expected 14 days ago and got 14 days ago
passed : expected 29 days ago and got 29 days ago
passed : expected one month ago and got one month ago
passed : expected one month ago and got one month ago
passed : expected 2 months ago and got 2 months ago
passed : expected 2 months ago and got 2 months ago
passed : expected 3 months ago and got 3 months ago
passed : expected 11 months ago and got 11 months ago
passed : expected one year ago and got one year ago
passed : expected one year ago and got one year ago
passed : expected one year ago and got one year ago
passed : expected one year ago and got one year ago
passed : expected 2 years ago and got 2 years ago
2

Here's my code from Py-StackExchange, in Python:

def format_relative_date(date):
    """Takes a datetime object and returns the date formatted as a string e.g. "3 minutes ago", like the real site.
    This is based roughly on George Edison's code from StackApps:
    https://stackapps.com/questions/1009/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site/1018#1018"""

    now = datetime.now()
    diff = (now - date).seconds

    # Anti-repetition! These simplify the code somewhat.
    plural = lambda d: 's' if d != 1 else ''
    frmt   = lambda d: (diff / float(d), plural(diff / float(d)))

    if diff < 60:
        return '%d second%s ago' % frmt(1)
    elif diff < 3600:
        return '%d minute%s ago' % frmt(60)
    elif diff < 86400:
        return '%d hour%s ago' % frmt(3600)
    elif diff < 172800:
        return 'yesterday'
    else:
        return date.strftime('M j / y - H:i')
0
2

Here's a quick and dirty way of how I did mine in .NET

Visual Basic

    ''# <summary>
    ''# Return how long in the past something happend
    ''# </summary>
    ''# <param name="input">The date of an event in UTC time</param>
    ''# <param name="append">append characters to the end of the string 
    ''# example: "suffixAgo"</param>
    ''# <returns>a logical string "10 mins ago"</returns>
    ''# <remarks>This function is primarily used in Views to format Model data.</remarks>
    Public Shared Function GetDateRange(ByVal input As DateTime, Optional ByVal suffixAgo As String = Nothing) As String
        Dim output As String
        Dim theDate As DateTime = dt
        Dim DateInterval As TimeSpan = DateTime.UtcNow - theDate
        Dim SecondsAppart As Integer = Math.Floor(DateInterval.TotalSeconds)
        If (SecondsAppart < 86400) Then
            If SecondsAppart < 59 Then
                output = ("less than a minute " + suffixAgo)
            ElseIf SecondsAppart < 119 Then
                output = ("about a minute " + suffixAgo)
            ElseIf SecondsAppart < 3599 Then
                output = String.Format("{0} minutes {1}", Math.Floor(DateInterval.TotalMinutes), suffixAgo)
            ElseIf SecondsAppart < 7199 Then
                output = "about an hour " + suffixAgo
            Else
                output = String.Format("{0} hours {1}", Math.Floor(DateInterval.TotalHours), suffixAgo)
            End If
        Else
            Dim DaysAppart As Integer = Math.Floor(DateInterval.TotalDays)
            If DaysAppart <= 1 Then
                output = "yesterday"
            ElseIf DaysAppart < 30 Then
                output = String.Format("{0} days {1}", Math.Floor(DateInterval.TotalDays), suffixAgo)
            ElseIf DaysAppart < 60 Then
                output = "about a month " + suffixAgo
            ElseIf DaysAppart < 365 Then
                output = String.Format("{0} months {1}", Math.Floor(DateInterval.TotalDays / 30), suffixAgo)
            ElseIf DaysAppart < 730 Then
                output = ("about a year " + suffixAgo)
            Else
                output = String.Format("{0} years {1}", Math.Floor(DateInterval.TotalDays / 365), suffixAgo)
            End If
        End If 

            Return output
    End Function
0
2

Here is a similar function in PHP:

function Pluralize($amount, $unit)
{
    if($amount == 1)
        return "$amount $unit ago";
    else
        return "$amount {$unit}s ago";
}

function RelativeTime($timestamp)
{
    // Calculate the difference
    $current_time = time();
    $difference = $current_time - $timestamp;

    if($difference == 0)
        return 'just now';
    elseif($difference < 60)
        return Pluralize($difference, 'second');
    elseif($difference < self::Hour)
        return Pluralize(floor($difference / 60), 'minute');
    elseif($difference < self::Day)
        return Pluralize(floor($difference / self::Hour), 'hour');
    elseif($difference < (self::Day * 2))
        return 'yesterday';
    elseif($difference < self::Month)
        return Pluralize(floor($difference / self::Day), 'day');
    elseif($difference < self::Year)
        return Pluralize(floor($difference / self::Month), 'month');
    else
        return Pluralize(floor($difference / self::Year), 'year');
}

Note: this is borrowed from my Stack.PHP wrapper.

2
0

Looks good. I did a very similar thing for stack.PHP, but it wasn't as elagant.

Is this code public domain?

P.S. On SO, it would look something like this:

  • 0 sec. ago - 59 sec. ago
  • 1 min. ago - 59 min. ago
  • 1 hr. ago - 23 hr. ago
  • yesterday
  • 2 days ago
  • MM DD
  • MM DD, YY
4
  • 1
    as per my profile: All code posted is tested and works on my machine. Unless otherwise noted, it is public domain and you are free to do with as you please. Jul 5, 2010 at 4:49
  • post your implementation, that is what this post is about. Jul 5, 2010 at 16:03
  • yup, let's see the PHP. Jul 15, 2010 at 19:22
  • @rock: See my answer above. Jul 15, 2010 at 20:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .