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.
TimeSpan or DateTime to Friendly Duration Text (e.g. “3 days ago”)
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.