Timeago for C#

By Rob on Thursday, September 10, 2009

5 Comments

Filed Under: .Net, Dot

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 for this post

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

Posted onSeptember 10th, 2009 at 8:27 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]);
}

Posted onFebruary 18th, 2010 at 1:31 pm
Posted onApril 20th, 2010 at 11:40 am
Rob
Posted onApril 20th, 2010 at 11:52 am

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.

Posted onApril 20th, 2010 at 12:27 pm

Leave a comment

Name (required) Comment
Mail (required)
Website