TimeSpan or DateTime to Friendly Duration Text (e.g. “3 days ago”)

Data should be displayed to end users in a way that requires the least amount of effort on their part. With dates, do the math for your users ahead of time. Explicit dates may be exact, but relative “duration text” is easier to understand, and it’s usually good enough.
Makes users think hard Easy to understand at a glance
12/9/2009 7:41 PM 2 hours ago
12/2/2009 10:13 AM 1 week ago
12/9/2009 7:26 PM 15 minutes ago
6/22/2009 2:31 PM 6 months ago
11/13/2006 1:22 AM 3 years and 1 month ago (Nov 2006)
I Googled “timespan human readable“, and found a few useful resources. A stackoverflow question was particularly interesting, but I’m not overly familiar with F#, so I whipped up this helper method:
public static string TimeAgoString(DateTime dateTime)
{
	StringBuilder sb = new StringBuilder();
	TimeSpan timespan = DateTime.Now - dateTime;

	// A year or more?  Do "[Y] years and [M] months ago"
	if ((int)timespan.TotalDays >= 365)
	{
		// Years
		int nYears = (int)timespan.TotalDays / 365;
		sb.Append(nYears);
		if (nYears > 1)
			sb.Append(" years");
		else
			sb.Append(" year");

		// Months
		int remainingDays = (int)timespan.TotalDays - (nYears * 365);
		int nMonths = remainingDays / 30;
		if (nMonths == 1)
			sb.Append(" and ").Append(nMonths).Append(" month");
		else if (nMonths > 1)
			sb.Append(" and ").Append(nMonths).Append(" months");
	}
	// More than 60 days? (appx 2 months or 8 weeks)
	else if ((int)timespan.TotalDays >= 60)
	{
		// Do months
		int nMonths = (int)timespan.TotalDays / 30;
		sb.Append(nMonths).Append(" months");
	}
	// Weeks? (7 days or more)
	else if ((int)timespan.TotalDays >= 7)
	{
		int nWeeks = (int)timespan.TotalDays / 7;
		sb.Append(nWeeks);
		if (nWeeks == 1)
			sb.Append(" week");
		else
			sb.Append(" weeks");
	}
	// Days? (1 or more)
	else if ((int)timespan.TotalDays >= 1)
	{
		int nDays = (int)timespan.TotalDays;
		sb.Append(nDays);
		if (nDays == 1)
			sb.Append(" day");
		else
			sb.Append(" days");
	}
	// Hours?
	else if ((int)timespan.TotalHours >= 1)
	{
		int nHours = (int)timespan.TotalHours;
		sb.Append(nHours);
		if (nHours == 1)
			sb.Append(" hour");
		else
			sb.Append(" hours");
	}
	// Minutes?
	else if ((int)timespan.TotalMinutes >= 1)
	{
		int nMinutes = (int)timespan.TotalMinutes;
		sb.Append(nMinutes);
		if (nMinutes == 1)
			sb.Append(" minute");
		else
			sb.Append(" minutes");
	}
	// Seconds?
	else if ((int)timespan.TotalSeconds >= 1)
	{
		int nSeconds = (int)timespan.TotalSeconds;
		sb.Append(nSeconds);
		if (nSeconds == 1)
			sb.Append(" second");
		else
			sb.Append(" seconds");
	}
	// Just say "1 second" as the smallest unit of time
	else
	{
		sb.Append("1 second");
	}

	sb.Append(" ago");

	// For anything more than 6 months back, put " ([Month] [Year])" at the end, for better reference
	if ((int)timespan.TotalDays >= 30 * 6)
	{
		sb.Append(" (" + dateTime.ToString("MMMM") + " " + dateTime.Year + ")");
	}

	return sb.ToString();
}
Obviously this is a custom solution, with some specific formatting rules. If required, this could be easily wrapped up into a class with configurable options. You could pass in a TimeSpan instead of a DateTime if you didn’t want to base everything off of DateTime.Now. Anyway, it’s a useful start. Have at it.
Posted in Code | Tagged , , , , | 3 Comments