Rails helper distance_of_time_in_words ported to C#
While learning rails I came across a very cool helper method called distance_of_time_in_words. You’ve probably seen this method in action if you’ve used Twitter – no guessing about time zones and as a user I think it’s a nice touch. So on to the port itself…
The Good
C# is automatically faster because everyone knows Ruby is slow and that rails can’t scale.
Seriously though, the port to C# uses resource files so internationalization is handled by simply adding a translated resource file to your project.
The Bad
C# doesn’t have the concept of static extension methods. To me DateTime.DistanceOfTimeInWords would be much nicer than DateHelper.DistanceOfTimeInWords.
Range support in the language is hokey at best. No ranges in switch statements. bleh.
The Ugly
The code! If you compare the C# version to the Ruby version it’s a little ahhhhhhhhhhh… inelegant? The Ruby version weighs in at 28 lines of readable code. The C# version is a b-e-a-s-t with 55 lines of code including 2 helper methods. I’m open to any suggestions in making the C# code shorter or more readable.
The Rest
The method time_ago_in_words has also been ported. Altogether the helper code is 74 lines – also included is over 250 lines of test code. All examples listed on the Rails page were also included in the test.
You can grab the code here or at the subversion repository http://svn.gaskell.org/helpers
October 8th, 2008 at 1:11 pm
using System;
using System.Collections.Generic;
using System.Linq;
using DistanceOfTimeInWordsDemo.Properties;
namespace DistanceOfTimeInWordsDemo
{
public static class DateTimeExtension
{
private class RangePair { public int Lower { get; set; } public int Upper { get; set; } }
private class KeyArgPair { public string ResourceKey { get; set; } public int DivideBy { get; set; } }
private static Dictionary<RangePair, KeyArgPair> WordsRange;
static DateTimeExtension()
{
WordsRange = new Dictionary<RangePair, KeyArgPair>();
WordsRange.Add(new RangePair { Lower = 0, Upper = 4 }, new KeyArgPair { ResourceKey = “LessThanFiveSeconds”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = 5, Upper = 9 }, new KeyArgPair { ResourceKey = “LessThanTenSeconds”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = 10, Upper = 19 }, new KeyArgPair { ResourceKey = “LessThanTwentySeconds”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = 20, Upper = 39 }, new KeyArgPair { ResourceKey = “HalfAMinute”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = 40, Upper = 59 }, new KeyArgPair { ResourceKey = “LessThanAMinute”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(2).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(44).TotalSeconds }, new KeyArgPair { ResourceKey = “MultipleMinutes”, DivideBy = 1 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(45).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(89).TotalSeconds }, new KeyArgPair { ResourceKey = “AboutOneHour”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(90).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(1439).TotalSeconds }, new KeyArgPair { ResourceKey = “AboutMultipleHours”, DivideBy = 60 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(1440).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(2879).TotalSeconds }, new KeyArgPair { ResourceKey = “OneDay”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(2880).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(43199).TotalSeconds }, new KeyArgPair { ResourceKey = “MultipleDays”, DivideBy = 1440 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(43200).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(86399).TotalSeconds }, new KeyArgPair { ResourceKey = “AboutOneMonth”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(86400).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(525599).TotalSeconds }, new KeyArgPair { ResourceKey = “MultipleMonths”, DivideBy = 43200 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(525600).TotalSeconds, Upper = (int)TimeSpan.FromMinutes(1051199).TotalSeconds }, new KeyArgPair { ResourceKey = “AboutOneYear”, DivideBy = 0 });
WordsRange.Add(new RangePair { Lower = (int)TimeSpan.FromMinutes(1051200).TotalSeconds, Upper = Int32.MaxValue }, new KeyArgPair { ResourceKey = “MultipleYears”, DivideBy = 525600 });
}
public static string TimeAgoInWords(this DateTime fromTime)
{
return DistanceOfTimeInWords(fromTime, DateTime.Now);
}
public static string TimeAgoInWords(this DateTime fromTime, bool includeSeconds)
{
return DistanceOfTimeInWords(fromTime, DateTime.Now, includeSeconds);
}
public static string DistanceOfTimeInWords(this DateTime fromTime, DateTime toTime)
{
return DistanceOfTimeInWords(fromTime, toTime, false);
}
public static string DistanceOfTimeInWords(this DateTime fromTime, DateTime toTime, bool includeSeconds)
{
Func<int, double, int> RoundedDistance = (value, dividedBy) => (int)Decimal.Round(Convert.ToDecimal(value / dividedBy), MidpointRounding.AwayFromZero);
Func<int, int, int, bool> InRange = (value, low, high) => (value >= low && value <= high);
Func<string, int?, string> InWords = (name, args) => (!String.IsNullOrEmpty(name)) ? (args.HasValue) ? String.Format(Resources.ResourceManager.GetString(name), args.Value) : Resources.ResourceManager.GetString(name) : String.Empty;
TimeSpan ts = (toTime – fromTime).Duration();
int distanceInMinutes = (int)ts.TotalMinutes;
int distanceInSeconds = (int)ts.TotalSeconds;
if (distanceInMinutes <= 1)
{
var query = (from s in WordsRange
where InRange(distanceInSeconds, s.Key.Lower, s.Key.Upper)
select s.Value.ResourceKey).FirstOrDefault();
if (includeSeconds)
{
if (!String.IsNullOrEmpty(query)) { return InWords(query, null); }
else { return InWords(“OneMinute”, null); }
}
else { return InWords((distanceInMinutes.Equals(0)) ? “LessThanAMinute” : “OneMinute”, null); }
}
else
{
var query = (from s in WordsRange
where InRange(distanceInSeconds, s.Key.Lower, s.Key.Upper)
select s.Value).FirstOrDefault();
if (query != null) { return InWords(query.ResourceKey, (query.DivideBy > 0) ? new Nullable<int>(RoundedDistance(distanceInMinutes, query.DivideBy)) : null); }
}
return String.Empty;
}
}
}
October 8th, 2008 at 1:16 pm
Hmmm, my brackets were stripped. Actually, *alot* was stripped. I’ll send you the code via email and you can post it.
July 28th, 2009 at 5:19 pm
Actually, .net 3.0 does have extension methods. I’ll have a go at tweaking your code.
July 30th, 2009 at 7:27 pm
@Ben – .net has extension methods, but you can’t create “static” extension methods as far as I know.
For example you could have:
DateTime.Now.CoolExtension()
but not
DateTime.CoolExtension()