Fulfilling the ‘Freshness’ pattern with extension methods
Share
Code: Freshness.cs
I posted last week on how extension methods can be broken down into three levels: extension methods that wrap .NET Framework methods, extension methods that provide “missing” features, and extension methods that solve business problems.
Here’s another common web development scenario that can be written cleanly with an application of these three layers of extension methods. In this example we want to provide the user with a description of “freshness”, perhaps for a blog post or comment. Instead of a boring timestamp, the user knows that his comment was posted “10 minutes ago”. There’s a lot more you could do with this example (for instance, you could display numbers by name, “ten minutes ago”, or show that the user posted their comment on a Tuesday), but this should get you started.
First, we wrap some .NET Framework static methods to give us some approximate methods for handling times on integers. These are very similar to Ruby, which Derek Slager first explored:
1: public static TimeSpan Days(this int value)
2: {
3: return TimeSpan.FromDays(value);
4: }
5:
6: public static TimeSpan Months(this int value)
7: {
8: return TimeSpan.FromDays(value * 30);
9: }
10:
11: public static TimeSpan Weeks(this int value)
12: {
13: return TimeSpan.FromDays(value * 7);
14: }
15:
16: public static TimeSpan Hours(this int value)
17: {
18: return TimeSpan.FromHours(value);
19: }
20:
21: public static TimeSpan Minutes(this int value)
22: {
23: return TimeSpan.FromMinutes(value);
24: }
25:
26: public static TimeSpan Seconds(this int value)
27: {
28: return TimeSpan.FromSeconds(value);
29: }
Next we create a few extension methods that can be used to wrap commonly used code snippets in a more natural wrapper:
1: public static bool IsEmpty(this StringBuilder instance)
2: {
3: return instance.Length < 1;
4: }
5:
6: // This is a personal favorite: it returns the real time passed since the given time.
7: public static TimeSpan Passed(this DateTime time)
8: {
9: return DateTime.Now.Subtract(time).Duration();
10: }
11:
12: public static bool AtLeast<T>(this T instance, T value) where T : IComparable<T>
13: {
14: var result = instance.CompareTo(value);
15: return result == 0 || result == 1;
16: }
17:
18: public static bool Before<T>(this T instance, T value) where T : IComparable<T>
19: {
20: return instance.CompareTo(value) == -1;
21: }
1: public static bool MoreThan<T>(this T instance, T value) where T : IComparable<T>
2: {
3: return instance.CompareTo(value) == 1;
4: }
5:
6: public static bool After<T>(this T instance, T value) where T : IComparable<T>
7: {
8: return instance.MoreThan(value);
9: }
It makes sense to use one or the other depending on the IComparable instances we’re comparing, but you can see how situations like this can steer your code towards unnecessary duplication, confusion, and bloat.
Finally, here’s our code that uses all of these extension methods and returns the “freshness” on a DateTime instance:
public static string Freshness(this DateTime time)
{
var sb = new StringBuilder();
var now = DateTime.Now;
var isComplete = false;
if (time.Before(now))
{
var timePassed = time.Passed();
if (timePassed.AtLeast(1.Days()))
{
if (timePassed.AtLeast(3.Months()) && sb.IsEmpty())
{
sb.Append("awhile");
}
if (timePassed.AtLeast(2.Months()) && sb.IsEmpty())
{
sb.Append("a few months");
}
if (timePassed.AtLeast(1.Months()) && sb.IsEmpty())
{
sb.Append("about a month");
}
if (timePassed.AtLeast(2.Weeks()) && sb.IsEmpty())
{
sb.Append("a few weeks");
}
if (timePassed.MoreThan(1.Weeks()) && sb.IsEmpty())
{
sb.Append("about a week");
}
if (timePassed == 1.Weeks() && sb.IsEmpty())
{
sb.Append("a week");
}
if (timePassed == 1.Days() && sb.IsEmpty())
{
sb.Append("yesterday");
isComplete = true;
}
if (sb.IsEmpty())
{
sb.AppendFormat("{0} days", timePassed.Days);
}
}
if (timePassed.AtLeast(1.Hours()) && sb.IsEmpty())
{
sb.AppendFormat("{0} hour", timePassed.Hours);
if (timePassed.AtLeast(2.Hours()))
{
sb.Append("s");
}
}
if (timePassed.AtLeast(1.Minutes()) && sb.IsEmpty())
{
sb.AppendFormat("{0} minute", timePassed.Minutes);
if (timePassed.AtLeast(2.Minutes()))
{
sb.Append("s");
}
}
if (timePassed.AtLeast(1.Seconds()) && sb.IsEmpty())
{
sb.AppendFormat("{0} second", timePassed.Seconds);
if (timePassed.AtLeast(2.Seconds()))
{
sb.Append("s");
}
}
if(sb.IsEmpty())
{
sb.Append("just moments");
}
if (!isComplete)
{
sb.Append(" ago");
}
}
else
{
sb.Append("from the future");
}
return sb.ToString();
}
Socialized