RSS
 

Timeago for C#

10 Sep

Sometimes you don’t want to make your users think. There’s the odd situation where you want to represent time in natural language: “about 4 hours ago” instead of just printing out a full timestamp. If you’re building a website, then the jQuery plugin Timeago is a pretty sweet way to do it (as long as you can stand webpages that auto update text).

Sucks for me, I’m working with WPF! (Not really sucks at all). So I needed a C# implementation of the same thing. Surely someone’s done this, right? Well my Google-fu failed me, and even when I Googled on bing I came up with nothing, so I built it myself. And I’m posting it here for you (and for me, later). If you’ve found a good one, please let me know.

First, the test cases, so you can see if the format I want is the format you want:

[TestClass]
public class FriendlyTimeDescriptionTest
{
    private static string Run(TimeSpan span)
    {
        return FriendlyTimeDescription.Describe(span);
    }

    [TestMethod]
    public void TestNow()
    {
        Assert.AreEqual("now", Run(new TimeSpan(0, 0, 0, 0)));
    }

    [TestMethod]
    public void TestSeconds()
    {
        Assert.AreEqual("1 second ago", Run(new TimeSpan(0, 0, 0, 1)));
        Assert.AreEqual("2 seconds ago", Run(new TimeSpan(0, 0, 0, 2)));
        Assert.AreEqual("59 seconds ago", Run(new TimeSpan(0, 0, 0, 59)));
    }

    [TestMethod]
    public void TestMinutesAndSeconds()
    {
        Assert.AreEqual("about 1 minute ago", Run(new TimeSpan(0, 0, 1, 1)));
        Assert.AreEqual("about 3 minutes ago", Run(new TimeSpan(0, 0, 3, 1)));
    }

    [TestMethod]
    public void TestMinutesAndSecondsRounding()
    {
        Assert.AreEqual("about 4 minutes ago", Run(new TimeSpan(0, 0, 3, 31)));
    }

    [TestMethod]
    public void TestDaysHours()
    {
        Assert.AreEqual("about 3 hours ago", Run(new TimeSpan(0, 3, 3, 1)));
        Assert.AreEqual("about 2 days ago", Run(new TimeSpan(2, 0, 1, 1)));
    }
}

I’ve conveniently wrapped all this up into an IValueConverter implementation, but if you’re not using WPF you can rip out the necessary methods. Please excuse the newline-enthused formatting – this blog theme has limited column width!

[ValueConversion(typeof(DateTime), typeof(string))]
public class FriendlyTimeDescription : IValueConverter
{
    public object Convert(
        object value,
        Type targetType,
        object parameter,
        CultureInfo culture)
    {
        var time = System.Convert.ToDateTime(value);
        return Describe(DateTime.Now - time);
    }

    static readonly string[] NAMES = {
                                         "day",
                                         "hour",
                                         "minute",
                                         "second"
                                     };

    public static string Describe(TimeSpan t)
    {
        int[] ints = {
                         t.Days,
                         t.Hours,
                         t.Minutes,
                         t.Seconds
                     };

        double[] doubles = {
                               t.TotalDays,
                               t.TotalHours,
                               t.TotalMinutes,
                               t.TotalSeconds
                           };

        var firstNonZero = ints
            .Select((value, index) => new { value, index })
            .FirstOrDefault(x => x.value != 0);
        if (firstNonZero == null)
        {
            return "now";
        }
        int i = firstNonZero.index;
        string prefix = (i >= 3) ? "" : "about ";
        int quantity = (int)Math.Round(doubles[i]);
        return prefix + Tense(quantity, NAMES[i]) + " ago";
    }

    public static string Tense(int quantity, string noun)
    {
        return quantity == 1
            ? "1 " + noun
            : string.Format("{0} {1}s", quantity, noun);
    }

    public object ConvertBack(
        object value,
        Type targetType,
        object parameter,
        CultureInfo culture)
    {
        return Binding.DoNothing;
    }
}

Enjoy!

 
5 Comments

Posted in .Net, Dot

 

Tags: ,

Leave a Reply

 

 
  1. IanR

    10 September 2009 at 8:00 pm

    Nice!
    I did something similar a while ago, but without the elegance of this solution, obviously :)

     
  2. Luke Venediger

    18 February 2010 at 1:58 pm

    Hi, thanks for this! I’ve modified it to be an extension method off TimeSpan:

    public static string PrettyPrint(this TimeSpan t)
    {
    string[] NAMES = {
    "day",
    "hour",
    "minute",
    "second"
    };

    int[] ints = {
    t.Days,
    t.Hours,
    t.Minutes,
    t.Seconds
    };

    double[] doubles = {
    t.TotalDays,
    t.TotalHours,
    t.TotalMinutes,
    t.TotalSeconds
    };

    var firstNonZero = ints
    .Select((value, index) => new { value, index })
    .FirstOrDefault(x => x.value != 0);
    if (firstNonZero == null)
    {
    return "now";
    }
    int i = firstNonZero.index;
    string prefix = (i >= 3) ? "" : "about ";
    int quantity = (int)Math.Round(doubles[i]);

    Func Tense = (q, n) => q == 1
    ? "1 " + n
    : string.Format("{0} {1}s", q, n);

    return prefix + Tense(quantity, NAMES[i]);
    }

     
  3. Rob

    20 April 2010 at 11:57 am

     
  4. Richard O'Donnell

    20 April 2010 at 12:02 pm

    Rob,

    Nope- I just thought the SO link would be useful for you, I didn’t realise you’d already seen it!

    Oh well, it might be useful for someone finding your site via Google and wanting to look at other solutions to the problem.