views:

11628

answers:

34

Given a specific DateTime value, how do I display relative time, like

  • 2 hours ago
  • 3 days ago
  • a month ago

etc, etc...?

+48  A: 

Well, here's how we do it on Stack Overflow.

var ts = new TimeSpan(DateTime.UtcNow.Ticks - dt.Ticks);
double delta = ts.TotalSeconds;

if (delta < 60)
{
  return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";
}
if (delta < 120)
{
  return "a minute ago";
}
if (delta < 2700) // 45 * 60
{
  return ts.Minutes + " minutes ago";
}
if (delta < 5400) // 90 * 60
{
  return "an hour ago";
}
if (delta < 86400) // 24 * 60 * 60
{
  return ts.Hours + " hours ago";
}
if (delta < 172800) // 48 * 60 * 60
{
  return "yesterday";
}
if (delta < 2592000) // 30 * 24 * 60 * 60
{
  return ts.Days + " days ago";
}
if (delta < 31104000) // 12 * 30 * 24 * 60 * 60
{
  int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  return months <= 1 ? "one month ago" : months + " months ago";
}
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
return years <= 1 ? "one year ago" : years + " years ago";

Suggestions? Comments? Ways to improve this algorithm?

Jeff Atwood
"one" second ago, "one" month ago, "one" year ago.But, "an" hour ago, "a" minute ago.Did you pick up that way of talking in Des Moines?
Corey Trager
"< 48*60*60s" is a rather unconventional definition for "yesterday". If it's 9am on Wednesday, would you really think of 9:01am on Monday as "yesterday". I'd have thought an algorithm for yesterday or "n days ago" should consider before/after midnight.
Joe
Compilers are usually pretty good at pre-calculating constant expressions, like 24 * 60 * 60, so you can directly use those instead of calculating it yourself to be 86400 and putting the original expression in comments
zvolkov
noticed that this function excludes weeks
jray
@jray So go implement them. :)
bzlm
+3  A: 

@jeff

IMHO yours seems a little long. However it does seem a little more robust with support for "yesterday" and "years". But in my experience when this is used the person is most likely to view the content in the first 30 days. It is only the really hardcore people that come after that. So that is why I usually elect to keep this short and simple.

This is the method I am currently using on one of my websites. This only returns a relative day, hour, time. And then the user has to slap on "ago" in the output.

public static string ToLongString(this TimeSpan time)
{
string output = String.Empty;

if (time.Days > 0)
output += time.Days + " days ";

if ((time.Days == 0 || time.Days == 1) && time.Hours > 0)
output += time.Hours + " hr ";

if (time.Days == 0 && time.Minutes > 0)
output += time.Minutes + " min ";

if (output.Length == 0)
output += time.Seconds + " sec";

return output.Trim();
}
Nick Berardi
Currently looking at this page - asked2 years ago :)
tim
+6  A: 

Stack Overflow will write both "answered an hour ago" and "answered 1 hours ago".

Zack Peterson
Not both. But yes, 91 minutes, for example, will return "1 hours ago".
dreeves
+150  A: 

Jeff, your code is nice but could be clearer with constants (as suggested in Code Complete).

const int SECOND = 1;
const int MINUTE = 60 * SECOND;
const int HOUR = 60 * MINUTE;
const int DAY = 24 * HOUR;
const int MONTH = 30 * DAY;

if (delta < 0)
{
  return "not yet";
}
if (delta < 1 * MINUTE)
{
  return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";
}
if (delta < 2 * MINUTE)
{
  return "a minute ago";
}
if (delta < 45 * MINUTE)
{
  return ts.Minutes + " minutes ago";
}
if (delta < 90 * MINUTE)
{
  return "an hour ago";
}
if (delta < 24 * HOUR)
{
  return ts.Hours + " hours ago";
}
if (delta < 48 * HOUR)
{
  return "yesterday";
}
if (delta < 30 * DAY)
{
  return ts.Days + " days ago";
}
if (delta < 12 * MONTH)
{
  int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  return months <= 1 ? "one month ago" : months + " months ago";
}
else
{
  int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
  return years <= 1 ? "one year ago" : years + " years ago";
}
Vincent Robert
in some languages you may even specify any type of comparing operator for the switch()-statement, like >, >=, <= etc... isn't this somehow possible in C#?
Atmocreations
@Atmo: no it's not possible
Ciwee
I hate such constants with a passion. Does this look wrong to anyone? `Thread.Sleep(1 * MINUTE)`? Because it's wrong by a factor of 1000.
romkyns
A: 

I thought I'd give this a shot using classes and polymorphism. I had a previous iteration which used sub-classing which ended up having way too much overhead. I've switched to a more flexible delegate / public property object model which is significantly better. My code is very slightly more accurate, I wish I could come up with a better way to generate "months ago" that didn't seem too over-engineered.

I think I'd still stick with Jeff's if-then cascade because it's less code and it's simpler (it's definitely easier to ensure it'll work as expected).

For the below code PrintRelativeTime.GetRelativeTimeMessage(TimeSpan ago) returns the relative time message (e.g. "yesterday").

public class RelativeTimeRange : IComparable
{
public TimeSpan UpperBound { get; set; }

public delegate string RelativeTimeTextDelegate(TimeSpan timeDelta);

public RelativeTimeTextDelegate MessageCreator { get; set; }

public int CompareTo(object obj)
{
if (!(obj is RelativeTimeRange))
{
return 1;
}
// note that this sorts in reverse order to the way you'd expect,
// this saves having to reverse a list later
return (obj as RelativeTimeRange).UpperBound.CompareTo(UpperBound);
}
}

public class PrintRelativeTime
{
private static List<RelativeTimeRange> timeRanges;

static PrintRelativeTime()
{
timeRanges = new List<RelativeTimeRange>{
new RelativeTimeRange
{
UpperBound = TimeSpan.FromSeconds(1),
MessageCreator = (delta) =>
{ return "one second ago"; }
},
new RelativeTimeRange
{
UpperBound = TimeSpan.FromSeconds(60),
MessageCreator = (delta) =>
{ return delta.Seconds + " seconds ago"; }

},
new RelativeTimeRange
{
UpperBound = TimeSpan.FromMinutes(2),
MessageCreator = (delta) =>
{ return "one minute ago"; }
},
new RelativeTimeRange
{
UpperBound = TimeSpan.FromMinutes(60),
MessageCreator = (delta) =>
{ return delta.Minutes + " minutes ago"; }
},
new RelativeTimeRange
{
UpperBound = TimeSpan.FromHours(2),
MessageCreator = (delta) =>
{ return "one hour ago"; }
},
new RelativeTimeRange
{
UpperBound = TimeSpan.FromHours(24),
MessageCreator = (delta) =>
{ return delta.Hours + " hours ago"; }
},
new RelativeTimeRange
{
UpperBound = TimeSpan.FromDays(2),
MessageCreator = (delta) =>
{ return "yesterday"; }
},
new RelativeTimeRange
{
UpperBound = DateTime.Now.Subtract(DateTime.Now.AddMonths(-1)),
MessageCreator = (delta) =>
{ return delta.Days + " days ago"; }
},
new RelativeTimeRange
{
UpperBound = DateTime.Now.Subtract(DateTime.Now.AddMonths(-2)),
MessageCreator = (delta) =>
{ return "one month ago"; }
},
new RelativeTimeRange
{
UpperBound = DateTime.Now.Subtract(DateTime.Now.AddYears(-1)),
MessageCreator = (delta) =>
{ return (int)Math.Floor(delta.TotalDays / 30) + " months ago"; }
},
new RelativeTimeRange
{
UpperBound = DateTime.Now.Subtract(DateTime.Now.AddYears(-2)),
MessageCreator = (delta) =>
{ return "one year ago"; }
},
new RelativeTimeRange
{
UpperBound = TimeSpan.MaxValue,
MessageCreator = (delta) =>
{ return (int)Math.Floor(delta.TotalDays / 365.24D) + " years ago"; }
}
};

timeRanges.Sort();
}

public static string GetRelativeTimeMessage(TimeSpan ago)
{
RelativeTimeRange postRelativeDateRange = timeRanges[0];

foreach (var timeRange in timeRanges)
{
if (ago.CompareTo(timeRange.UpperBound) <= 0)
{
postRelativeDateRange = timeRange;
}
}

return postRelativeDateRange.MessageCreator(ago);
}
}
Wedge
+1  A: 

Jeff,

Your years algorithm doesn't take into account Leap Years and such, so it would be a little off in about 40 years. :) However the "months" algorithm might be off much faster considering that you didn't take into account February and all the months with 31.

I know it complicates things but, just might be a concern. :)

Jon Limjap
+3  A: 

Have +1 to Zack, but to re-iterate there is a condition between 90 and 120 minutes where it displays '1 hours ago' that needs a bit of tweaking - extra clause perhaps?

Brendan
+13  A: 
   public static string RelativeDate(DateTime theDate) {
        Dictionary<long, string> thresholds = new Dictionary<long, string>();
        int minute = 60;
        int hour = 60 * minute;
        int day = 24 * hour;
        thresholds.Add(60, "{0} seconds ago");
        thresholds.Add(minute * 2, "a minute ago");
        thresholds.Add(45 * minute, "{0} minutes ago");
        thresholds.Add(120 * minute, "an hour ago");
        thresholds.Add(day, "{0} hours ago");
        thresholds.Add(day * 2, "yesterday");
        thresholds.Add(day * 30, "{0} days ago");
        thresholds.Add(day * 365, "{0} months ago");
        thresholds.Add(long.MaxValue, "{0} years ago");

        long since = (DateTime.Now.Ticks - theDate.Ticks) / 1000000;
        foreach (long threshold in thresholds.Keys) {
            if (since < threshold) {
                TimeSpan t = new TimeSpan((DateTime.Now.Ticks - theDate.Ticks));
                return string.Format(thresholds[threshold], (t.Days > 365 ? t.Days / 365 : (t.Days > 0 ? t.Days : (t.Hours > 0 ? t.Hours : (t.Minutes > 0 ? t.Minutes : (t.Seconds > 0 ? t.Seconds : 0))))).ToString());
            }
        }
        return "";
    }

I prefer this version for its conciseness, and ability to add in new tick points. This could be encapsulated with a Latest() extension to Timespan instead of that long 1 liner, but for the sake of brevity in posting, this will do. This fixes the an hour ago, 1 hours ago, by providing an hour until 2 hours have elapsed

DevelopingChris
I am getting all sorts of problems using this function, for instance if you mock 'theDate = DateTime.Now.AddMinutes(-40);' I am getting '40 hours ago', but with Michael's refactormycode response, it returns correct at '40 minutes ago' ?
GONeale
i think you are missing a zero, try:long since = (DateTime.Now.Ticks - theDate.Ticks) / 10000000;
robnardo
+1  A: 

So the basic answer is a Big-Ass switch/if/else statement :)

Andrew Grant
A: 

I created a Rails plugin that does pretty date and time formatting: http://github.com/gcnovus/ruby-tidbits/tree/master

It, too, uses a "big-ass switch/if/else statement," though, being Ruby, it's of course a pretty, big-ass switch/if/else statement :)

James A. Rosen
A: 

@Jeff,

I've been going a bit mad trying to figure out why my code was returning negative values for times in the past and now I've finally cracked it... I'm in the middle of BST!

Line 1 now becomes:

var ts = new TimeSpan(DateTime.Now.Ticks - dt.Ticks);
GateKiller
A: 

@Wedge : wouldn't it be smoother to be able to break the loop when you've found the right on to use?

Peteter
A: 

@Jeff

var ts = new TimeSpan(DateTime.UtcNow.Ticks - dt.Ticks);

Doing a subtraction on DateTime returns a TimeSpan anyway.

So you can just do

(DateTime.UtcNow - dt).TotalSeconds

I'm also surprised to see the constants multiplied-out by hand and then comments added with the multiplications in. Was that some misguided optimisation?

Will Dean
+1  A: 

When you know the viewer's time zone, it might be clearer to use calendar days at the day scale. I'm not familiar with the .NET libraries so I don't know how you'd do that in C#, unfortunately.

On consumer sites, you could also be hand-wavier under a minute. "Less than a minute ago" or "just now" could be good enough.

markpasc
+1  A: 

You can reduce the server-side load by performing this logic client-side. View source on some Digg pages for reference. They have the server emit an epoch time value that gets processed by Javascript. This way you don't need to manage the end user's time zone. The new server-side code would be something like:

  public string GetRelativeTime(DateTime timeStamp)
  {
     return string.Format("<script>printdate({0});</script>", timeStamp.ToFileTimeUtc());
  }

You could even add a NOSCRIPT block there and just perform a ToString().

+1  A: 

I went to the UserVoice site to add a bug report, and found that issues with the relative time labels were already mentioned -- sort of. And I just searched for "relative yesterday". But nobody seems to have mentioned that the "yesterday" algorithm is technically wrong. Today is August 20th, around noon. I was just looking at an answer I posted the day before yesterday, on August 18th, in the 6 PM hour (local time), and it was displayed as "yesterday".

"the XX [dateparts] ago" patterns all work fine because they're straightforward relative measures. To get roughly the correct time, you take "now" and substract XX [dateparts]. But at 12:00:01 AM, "yesterday" is any time from 1+ seconds ago to 24 hours and 1+ seconds ago. At 11:59:59 PM, "yesterday" is any time from 23:59:59+ ago to 47:59:59+ ago.

In short, the relative time "yesterday" can't be determined in a sensible way by using a delta at the seconds' level, only by subtracting the value of the "day" datepart. Note that "one day ago" would be a correct rendering of the same time threshold, even up to 1.999 days, but "yesterday" isn't.

EDIT: I didn't notice at first that the times that appeared on the tooltips were in UTC. Considering that the timestamps aren't localized to the user's time zone, I think that even using "yesterday" is probably ill-advised. If using relative times is intended to make the time strings easier to understand without thinking about them, any "yesterday" that would be based on UTC would be counterproductive.

kcrumley
+1  A: 

In PHP, I do it this way:

<?php
function timesince($original) {
    // array of time period chunks
    $chunks = array(
        array(60 * 60 * 24 * 365 , 'year'),
        array(60 * 60 * 24 * 30 , 'month'),
        array(60 * 60 * 24 * 7, 'week'),
        array(60 * 60 * 24 , 'day'),
        array(60 * 60 , 'hour'),
        array(60 , 'minute'),
    );

    $today = time(); /* Current unix time  */
    $since = $today - $original;

    if($since > 604800) {
 $print = date("M jS", $original);

 if($since > 31536000) {
  $print .= ", " . date("Y", $original);
 }

 return $print;
}

// $j saves performing the count function each time around the loop
for ($i = 0, $j = count($chunks); $i < $j; $i++) {

    $seconds = $chunks[$i][0];
    $name = $chunks[$i][1];

    // finding the biggest chunk (if the chunk fits, break)
    if (($count = floor($since / $seconds)) != 0) {
        break;
    }
}

$print = ($count == 1) ? '1 '.$name : "$count {$name}s";

return $print . " ago";

} ?>
icco
A: 

Surely an easy fix to get rid of the '1 hours ago' problem would be to increase the window that 'an hour ago' is valid for. Change

if (delta < 5400) // 90 * 60
{
  return "an hour ago";
}

into

if (delta < 7200) // 120 * 60
{
  return "an hour ago";
}

This means that something that occurred 110 minutes ago will read as 'an hour ago' - this may not be perfect, but I'd say it is better than the current situation of '1 hours ago'.

Cebjyre
A: 

Here's a post that discusses this, and has code samples in C#

Juan Manuel
+12  A: 
public static string ToRelativeDate(DateTime input)
{
    TimeSpan oSpan = DateTime.Now.Subtract(input);
    double TotalMinutes = oSpan.TotalMinutes;
    string Suffix = " ago";

    if (TotalMinutes < 0.0)
    {
        TotalMinutes = Math.Abs(TotalMinutes);
        Suffix = " from now";
    }

    Dictionary<double, Func<string>> aValue = new Dictionary<double, Func<string>>();
    aValue.Add(0.75, () => "less than a minute");
    aValue.Add(1.5, () => "about a minute");
    aValue.Add(45, () => string.Format("{0} minutes", Math.Round(TotalMinutes)));
    aValue.Add(90, () => "about an hour");
    aValue.Add(1440, () => string.Format("about {0} hours", Math.Round(Math.Abs(oSpan.TotalHours)))); // 60 * 24
    aValue.Add(2880, () => "a day"); // 60 * 48
    aValue.Add(43200, () => string.Format("{0} days", Math.Floor(Math.Abs(oSpan.TotalDays)))); // 60 * 24 * 30
    aValue.Add(86400, () => "about a month"); // 60 * 24 * 60
    aValue.Add(525600, () => string.Format("{0} months", Math.Floor(Math.Abs(oSpan.TotalDays / 30)))); // 60 * 24 * 365 
    aValue.Add(1051200, () => "about a year"); // 60 * 24 * 365 * 2
    aValue.Add(double.MaxValue, () => string.Format("{0} years", Math.Floor(Math.Abs(oSpan.TotalDays / 365))));

    return aValue.First(n => TotalMinutes < n.Key).Value.Invoke() + Suffix;
}

http://refactormycode.com/codes/493-twitter-esque-relative-dates

Michael Wolfenden
i saw this on refactormycode too. elegant.
MikeJ
this is very nice IMO :) This could also be refactored as an extension method? could the dictionary become static so it's only created once and referenced from then after?
Pure.Krome
Pure.Krome: http://stackoverflow.com/questions/11/how-do-i-calculate-relative-time/1141237#1141237
Chris Charabaruk
You'd probably want to pull that dictionary out into a field so that you reduce instantiation and GC churn. You'd have to change `Func<string>` to `Func<double>`.
Drew Noakes
+71  A: 

jquery.timeago plugin

Jeff, Because stackoverflow uses jquery extensively, I recommend this jquery.timeago plugin.

Benefits:

  • Avoid timestamps dated "1 minute ago" even though the page was opened 10 minutes ago; timeago refreshes automatically.
  • You can take full advantage of page and/or fragment caching in your web applications, because the timestamps aren't calculated on the server.
  • You get to use microformats like the cool kids.

Just attach it to your timestamps on DOM ready:

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

This will turn all abbr elements with a class of timeago and an ISO 8601 timestamp in the title:

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

into something like this:

<abbr class="timeago" title="July 17, 2008">4 months ago</abbr>

which yields: 4 months ago. As time passes, the timestamps will automatically update.

Disclaimer: I wrote this plugin, so I'm biased.

Ryan McGeary
And what if the user has JavaScript disabled? jQuery (and JavaScritp generally speaking) should only be used to improve client-side experience, but your application features shouldn't depend on client technologies. You can accomplish this without using JavaScript.
Seb
Seb, If you have Javascript disabled, then the string you originally put between the abbr tags is displayed. Typically, this is just a date or time in any format you wish. Timeago degrades gracefully. It doesn't get much simpler.
Ryan McGeary
Shouldn't it be jQuery('abbr.timeago')?
Will Morgan
Will, absolutely! I don't know what I was thinking. Fixed.
Ryan McGeary
+1 Wow, this is exactly what I need for a site I'm working on. Thanks.
Splash
Ryan, I suggested that SO use timeago a while ago. Jeff's response made me cry, i suggest you sit down: http://stackoverflow.uservoice.com/pages/1722-general/suggestions/96770-auto-update-the-fuzzy-timestamps-with-jquery-timeago-
Rob Fonseca-Ensor
Heh, Thanks Rob. That's okay. It's barely noticeable, especially when only one number changes during the transition, though SO pages have a lot of timestamps. I would have thought he would have at least appreciated the benefits of page caching though, even if he chooses to avoid auto-updates. I'm sure Jeff could have provided feedback to improve the plugin too. I take solace knowing sites like http://arstechnica.com/ use it.
Ryan McGeary
@Rob Fonseca-Ensor - now it's making me cry too. How is an update once per minute, to show accurate information, *in any way* related to text blinking once a second?
Daniel Earwicker
An option to disable the auto-updating shouldn't be that hard, would it? If it doesn't already exist, that is... Just update the timestamps once on pageload and be done with it.
Dean Harding
A: 

If you expect equal distribution of the different cases then rearranging the conditional tests into a binary tree should be beneficial.

Tommy
A: 

Here's the algorithm stackoverflow uses but rewritten more concisely in perlish pseudocode with a bug fix (no "one hours ago"). The function takes a (positive) number of seconds ago and returns a human-friendly string like "3 hours ago" or "yesterday".

agoify($delta)
  local($y, $mo, $d, $h, $m, $s);
  $s = floor($delta);
  if($s<=1)            return "a second ago";
  if($s<60)            return "$s seconds ago";
  $m = floor($s/60);
  if($m==1)            return "a minute ago";
  if($m<45)            return "$m minutes ago";
  $h = floor($m/60);
  if($h==1)            return "an hour ago";
  if($h<24)            return "$h hours ago";
  $d = floor($h/24);
  if($d<2)             return "yesterday";
  if($d<30)            return "$d days ago";
  $mo = floor($d/30);
  if($mo<=1)           return "a month ago";
  $y = floor($mo/12);
  if($y<1)             return "$mo months ago";
  if($y==1)            return "a year ago";
  return "$y years ago";
dreeves
+2  A: 

Perhaps if the delta is less than 5 seconds ago, you could return "just now". I've seen that on a few "web2.0!!" sites and I think it's a nice touch. Realistically, for the end user, the difference between "0 seconds ago" and "4 seconds ago" is negligible.

aaaidan
I do like the whole "That just happened!"
d1k_is
+3  A: 

I would recommend computing this on the client side too. Less work for the server.

The following is the version that I use (from Zach Leatherman)

/*
 * Javascript Humane Dates
 * Copyright (c) 2008 Dean Landolt (deanlandolt.com)
 * Re-write by Zach Leatherman (zachleat.com)
 * 
 * Adopted from the John Resig's pretty.js
 * at http://ejohn.org/blog/javascript-pretty-date
 * and henrah's proposed modification 
 * at http://ejohn.org/blog/javascript-pretty-date/#comment-297458
 * 
 * Licensed under the MIT license.
 */

function humane_date(date_str){
        var time_formats = [
                [60, 'just now'],
                [90, '1 minute'], // 60*1.5
                [3600, 'minutes', 60], // 60*60, 60
                [5400, '1 hour'], // 60*60*1.5
                [86400, 'hours', 3600], // 60*60*24, 60*60
                [129600, '1 day'], // 60*60*24*1.5
                [604800, 'days', 86400], // 60*60*24*7, 60*60*24
                [907200, '1 week'], // 60*60*24*7*1.5
                [2628000, 'weeks', 604800], // 60*60*24*(365/12), 60*60*24*7
                [3942000, '1 month'], // 60*60*24*(365/12)*1.5
                [31536000, 'months', 2628000], // 60*60*24*365, 60*60*24*(365/12)
                [47304000, '1 year'], // 60*60*24*365*1.5
                [3153600000, 'years', 31536000], // 60*60*24*365*100, 60*60*24*365
                [4730400000, '1 century'] // 60*60*24*365*100*1.5
        ];

        var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," "),
                dt = new Date,
                seconds = ((dt - new Date(time) + (dt.getTimezoneOffset() * 60000)) / 1000),
                token = ' ago',
                i = 0,
                format;

        if (seconds < 0) {
                seconds = Math.abs(seconds);
                token = '';
        }

        while (format = time_formats[i++]) {
                if (seconds < format[0]) {
                        if (format.length == 2) {
                                return format[1] + (i > 1 ? token : ''); // Conditional so we don't return Just Now Ago
                        } else {
                                return Math.round(seconds / format[2]) + ' ' + format[1] + (i > 1 ? token : '');
                        }
                }
        }

        // overflow for centuries
        if(seconds > 4730400000)
                return Math.round(seconds / 4730400000) + ' centuries' + token;

        return date_str;
};

if(typeof jQuery != 'undefined') {
        jQuery.fn.humane_dates = function(){
                return this.each(function(){
                        var date = humane_date(this.title);
                        if(date && jQuery(this).text() != date) // don't modify the dom if we don't have to
                                jQuery(this).text(date);
                });
        };
}
Jauder Ho
+8  A: 

Here a rewrite from Jeffs Script for PHP:

define("SECOND", 1);
define("MINUTE", 60 * SECOND);
define("HOUR", 60 * MINUTE);
define("DAY", 24 * HOUR);
define("MONTH", 30 * DAY);
function relativeTime($time)
{   
    $delta = time() - $time;

    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 floor($delta / MINUTE) . " minutes ago";
    }
    if ($delta < 90 * MINUTE)
    {
      return "an hour ago";
    }
    if ($delta < 24 * HOUR)
    {
      return floor($delta / HOUR) . " hours ago";
    }
    if ($delta < 48 * HOUR)
    {
      return "yesterday";
    }
    if ($delta < 30 * DAY)
    {
     return floor($delta / DAY) . " days ago";
    }
    if ($delta < 12 * MONTH)
    {
      $months = floor($delta / DAY / 30);
      return $months <= 1 ? "one month ago" : $months . " months ago";
    }
    else
    {
     $years = floor($delta / DAY / 365);
     return $years <= 1 ? "one year ago" : $years . " years ago";
    }
}
Thomaschaaf
This works a treat...
Patrick Beardmore
Thanks :) +1 :)
Prashant
+1  A: 

Is there an easy way to do this in Java? The java.util.Date class seems rather limited.

Here is my quick and dirty Java solution:

import java.util.Date;
import javax.management.timer.Timer;

String getRelativeDate(Date date) {  
  long delta = new Date().getTime() - date.getTime();
  if (delta < 1L * Timer.ONE_MINUTE) {
    return toSeconds(delta) == 1 ? "one second ago" : toSeconds(delta) + " seconds ago";
  }
  if (delta < 2L * Timer.ONE_MINUTE) {
    return "a minute ago";
  }
  if (delta < 45L * Timer.ONE_MINUTE) {
    return toMinutes(delta) + " minutes ago";
  }
  if (delta < 90L * Timer.ONE_MINUTE) {
    return "an hour ago";
  }
  if (delta < 24L * Timer.ONE_HOUR) {
    return toHours(delta) + " hours ago";
  }
  if (delta < 48L * Timer.ONE_HOUR) {
    return "yesterday";
  }
  if (delta < 30L * Timer.ONE_DAY) {
    return toDays(delta) + " days ago";
  }
  if (delta < 12L * 4L * Timer.ONE_WEEK) // a month {
    long months = toMonths(delta); 
    return months <= 1 ? "one month ago" : months + " months ago";
  }
  else {
    long years = toYears(delta);
    return years <= 1 ? "one year ago" : years + " years ago";
  }
}

private long toSeconds(long date) {
  return date / 1000L;
}

private long toMinutes(long date) {
  return toSeconds(date) / 60L;
}

private long toHours(long date) {
  return toMinutes(date) / 60L;
}

private long toDays(long date) {
  return toHours(date) / 24L;
}

private long toMonths(long date) {
  return toDays(date) / 30L;
}

private long toYears(long date) {
  return toMonths(date) / 365L;
}
+4  A: 

Here's an implementation I added as an extension method to the DateTime class that handles both future and past dates and provides an approximation option that allows you to specify the level of detail you're looking for ("3 hour ago" vs "3 hours, 23 minutes, 12 seconds ago"):

using System.Text;

/// <summary>
/// Compares a supplied date to the current date and generates a friendly English 
/// comparison ("5 days ago", "5 days from now")
/// </summary>
/// <param name="date">The date to convert</param>
/// <param name="approximate">When off, calculate timespan down to the second.
/// When on, approximate to the largest round unit of time.</param>
/// <returns></returns>
public static string ToRelativeDateString(this DateTime value, bool approximate)
{
    StringBuilder sb = new StringBuilder();

    string suffix = (value > DateTime.Now) ? " from now" : " ago";

    TimeSpan timeSpan = new TimeSpan(Math.Abs(DateTime.Now.Subtract(value).Ticks));

    if (timeSpan.Days > 0)
    {
        sb.AppendFormat("{0} {1}", timeSpan.Days,
          (timeSpan.Days > 1) ? "days" : "day");
        if (approximate) return sb.ToString() + suffix;
    }
    if (timeSpan.Hours > 0)
    {
        sb.AppendFormat("{0}{1} {2}", (sb.Length > 0) ? ", " : string.Empty,
          timeSpan.Hours, (timeSpan.Hours > 1) ? "hours" : "hour");
        if (approximate) return sb.ToString() + suffix;
    }
    if (timeSpan.Minutes > 0)
    {
        sb.AppendFormat("{0}{1} {2}", (sb.Length > 0) ? ", " : string.Empty, 
          timeSpan.Minutes, (timeSpan.Minutes > 1) ? "minutes" : "minute");
        if (approximate) return sb.ToString() + suffix;
    }
    if (timeSpan.Seconds > 0)
    {
        sb.AppendFormat("{0}{1} {2}", (sb.Length > 0) ? ", " : string.Empty, 
          timeSpan.Seconds, (timeSpan.Seconds > 1) ? "seconds" : "second");
        if (approximate) return sb.ToString() + suffix;
    }
    if (sb.Length == 0) return "right now";

    sb.Append(suffix);
    return sb.ToString();
}
neuracnu
A: 
using System;
using System.Collections.Generic;
using System.Linq;

public static class RelativeDateHelper
{
    private static Dictionary<double, Func<double, string>> sm_Dict = null;

    private static Dictionary<double, Func<double, string>> DictionarySetup()
    {
        var dict = new Dictionary<double, Func<double, string>>();
        dict.Add(0.75, (mins) => "less than a minute");
        dict.Add(1.5, (mins) => "about a minute");
        dict.Add(45, (mins) => string.Format("{0} minutes", Math.Round(mins)));
        dict.Add(90, (mins) => "about an hour");
        dict.Add(1440, (mins) => string.Format("about {0} hours", Math.Round(Math.Abs(mins / 60)))); // 60 * 24
        dict.Add(2880, (mins) => "a day"); // 60 * 48
        dict.Add(43200, (mins) => string.Format("{0} days", Math.Floor(Math.Abs(mins / 1440)))); // 60 * 24 * 30
        dict.Add(86400, (mins) => "about a month"); // 60 * 24 * 60
        dict.Add(525600, (mins) => string.Format("{0} months", Math.Floor(Math.Abs(mins / 43200)))); // 60 * 24 * 365 
        dict.Add(1051200, (mins) => "about a year"); // 60 * 24 * 365 * 2
        dict.Add(double.MaxValue, (mins) => string.Format("{0} years", Math.Floor(Math.Abs(mins / 525600))));

        return dict;
    }

    public static string ToRelativeDate(this DateTime input)
    {
        TimeSpan oSpan = DateTime.Now.Subtract(input);
        double TotalMinutes = oSpan.TotalMinutes;
        string Suffix = " ago";

        if (TotalMinutes < 0.0)
        {
            TotalMinutes = Math.Abs(TotalMinutes);
            Suffix = " from now";
        }

        if (null == sm_Dict)
            sm_Dict = DictionarySetup();

        return sm_Dict.First(n => TotalMinutes < n.Key).Value.Invoke(TotalMinutes) + Suffix;
    }
}

The same as another answer to this question but as an extension method with a static dictionary.

Chris Charabaruk
+1  A: 

using Fluent DateTime http://fluentdatetime.codeplex.com/

        var dateTime1 = 2.Hours().Ago();
        var dateTime2 = 3.Days().Ago();
        var dateTime3 = 1.Months().Ago();
        var dateTime4 = 5.Hours().FromNow();
        var dateTime5 = 2.Weeks().FromNow();
        var dateTime6 = 40.Seconds().FromNow();
Simon
Looks interesting .... thanks for sharing
jalchr
+1  A: 

One minor addition to the top answer is that it will incorrectly calculate "Yesterday". This code will look at the delta and then figure out if the date is the same (meaning today) or off by 1 (meaning yesterday). The problem with the code in the top answer is that a date difference of 8 hours ago should say yesterday if it was posted at night and it is now the next morning.

Uses an arbitrary cutoff of 6 hours for the "n hours ago" display. And uses a variable of inputDate for the date to compare:

...
if (delta < 7 * HOUR)
{
    return ts.Hours + " hours ago";
}
if (delta < 24 * HOUR && inputDate.Date == DateTime.Now.Date)
{
    return "Today";
}
if (delta < 48 * HOUR && inputDate.AddDays(1).Date == DateTime.Now.Date)
{
    return "Yesterday";
}
...
Peter Jacoby
+1  A: 

Java for client-side gwt usage:

import java.util.Date;

public class RelativeDateFormat {

 private static final long ONE_MINUTE = 60000L;
 private static final long ONE_HOUR = 3600000L;
 private static final long ONE_DAY = 86400000L;
 private static final long ONE_WEEK = 604800000L;

 public static String format(Date date) {

  long delta = new Date().getTime() - date.getTime();
  if (delta < 1L * ONE_MINUTE) {
   return toSeconds(delta) == 1 ? "one second ago" : toSeconds(delta)
     + " seconds ago";
  }
  if (delta < 2L * ONE_MINUTE) {
   return "one minute ago";
  }
  if (delta < 45L * ONE_MINUTE) {
   return toMinutes(delta) + " minutes ago";
  }
  if (delta < 90L * ONE_MINUTE) {
   return "one hour ago";
  }
  if (delta < 24L * ONE_HOUR) {
   return toHours(delta) + " hours ago";
  }
  if (delta < 48L * ONE_HOUR) {
   return "yesterday";
  }
  if (delta < 30L * ONE_DAY) {
   return toDays(delta) + " days ago";
  }
  if (delta < 12L * 4L * ONE_WEEK) {
   long months = toMonths(delta);
   return months <= 1 ? "one month ago" : months + " months ago";
  } else {
   long years = toYears(delta);
   return years <= 1 ? "one year ago" : years + " years ago";
  }
 }

 private static long toSeconds(long date) {
  return date / 1000L;
 }

 private static long toMinutes(long date) {
  return toSeconds(date) / 60L;
 }

 private static long toHours(long date) {
  return toMinutes(date) / 60L;
 }

 private static long toDays(long date) {
  return toHours(date) / 24L;
 }

 private static long toMonths(long date) {
  return toDays(date) / 30L;
 }

 private static long toYears(long date) {
  return toMonths(date) / 365L;
 }

}
antony.trupe
A: 

This, I got from one of Bill Gates' blog (no lie). I need to find it on my browser history and I'll give you the link.

The Javascript code to do the same thing (as requested):

function posted(t) {
    var now = new Date();
    var diff = parseInt((now.getTime() - Date.parse(t)) / 1000);
    if (diff < 60) { return 'less than a minute ago'; }
    else if (diff < 120) { return 'about a minute ago'; }
    else if (diff < (2700)) { return (parseInt(diff / 60)).toString() + ' minutes ago'; }
    else if (diff < (5400)) { return 'about an hour ago'; }
    else if (diff < (86400)) { return 'about ' + (parseInt(diff / 3600)).toString() + ' hours ago'; }
    else if (diff < (172800)) { return '1 day ago'; } 
    else {return (parseInt(diff / 86400)).toString() + ' days ago'; }
}

Basically, you work in terms of seconds...

The Elite Gentleman
+1  A: 

iPhone obj-c Version

+ (NSString *)timeAgoString:(NSDate *)date {
int delta = -(int)[date timeIntervalSinceNow];

if (delta < 60)
{
    return delta == 1 ? @"one second ago" : [NSString stringWithFormat:@"%i seconds ago", delta];
}
if (delta < 120)
{
    return @"a minute ago";
}
if (delta < 2700)
{
    return [NSString stringWithFormat:@"%i minutes ago", delta/60];
}
if (delta < 5400)
{
    return @"an hour ago";
}
if (delta < 24 * 3600)
{
    return [NSString stringWithFormat:@"%i hours ago", delta/3600];
}
if (delta < 48 * 3600)
{
    return @"yesterday";
}
if (delta < 30 * 24 * 3600)
{
    return [NSString stringWithFormat:@"%i days ago", delta/(24*3600)];
}
if (delta < 12 * 30 * 24 * 3600)
{
    int months = delta/(30*24*3600);
    return months <= 1 ? @"one month ago" : [NSString stringWithFormat:@"%i months ago", months];
}
else
{
    int years = delta/(12*30*24*3600);
    return years <= 1 ? @"one year ago" : [NSString stringWithFormat:@"%i years ago", years];
}

}

ollie