Wednesday, May 23, 2012

Calculating relative time


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



  • 2 hours ago

  • 3 days ago

  • a month ago



etc...?


Source: Tips4all

14 comments:

  1. 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";
    }

    ReplyDelete
  2. jquery.timeago plugin

    Jeff, because Stack Overflow uses jQuery extensively, I recommend the 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.

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

    var ts = new TimeSpan(DateTime.UtcNow.Ticks - dt.Ticks);
    double delta = Math.Abs(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?

    ReplyDelete
  4. 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) / 10000000;
    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

    ReplyDelete
  5. 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";
    }
    }

    ReplyDelete
  6. 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";
    }

    var aValue = new SortedList<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

    ReplyDelete
  7. 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);
    });
    };
    }

    ReplyDelete
  8. 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();
    }

    ReplyDelete
  9. 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];
    }


    }

    ReplyDelete
  10. @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();}

    ReplyDelete
  11. 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?

    ReplyDelete
  12. 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.

    ReplyDelete
  13. 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;
    }

    ReplyDelete
  14. A couple of years late to the party, but I had a requirement to do this for both past and future dates, so I combined Jeff's and Vincent's into this. It's a ternarytastic extravaganza! :)

    public static class DateTimeHelper
    {
    private const int SECOND = 1;
    private const int MINUTE = 60 * SECOND;
    private const int HOUR = 60 * MINUTE;
    private const int DAY = 24 * HOUR;
    private const int MONTH = 30 * DAY;

    /// <summary>
    /// Returns a friendly version of the provided DateTime, relative to now. E.g.: "2 days ago", or "in 6 months".
    /// </summary>
    /// <param name="dateTime">The DateTime to compare to Now</param>
    /// <returns>A friendly string</returns>
    public static string GetFriendlyRelativeTime(DateTime dateTime)
    {
    if (DateTime.UtcNow.Ticks == dateTime.Ticks)
    {
    return "Right now!";
    }

    bool isFuture = (DateTime.UtcNow.Ticks < dateTime.Ticks);
    var ts = DateTime.UtcNow.Ticks < dateTime.Ticks ? new TimeSpan(dateTime.Ticks - DateTime.UtcNow.Ticks) : new TimeSpan(DateTime.UtcNow.Ticks - dateTime.Ticks);

    double delta = ts.TotalSeconds;

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

    ReplyDelete