My pair and I were looking at performing some unusual string formatting today. We kept finding that the extension methods in System.Linq.Enumerable
were pretty helpful, but they often seemed to fall just short of what we needed to make the code really readable. Once I got home I thought I’d see how far I’d get by dumping some functionality into extension methods with blatant disregard for the potential consequences. (Unfortunately I had to miss the Sydney ALT.NET meeting tonight, so I had a bit of time to play around.)
Formatting arrays for acceptance tests
Here is the basic behaviour we’re after. Given an array or other enumerable of integers (or of any type with a sensible ToString()
method really), we want to return the items as a single, comma separated string. The strange part of it is that if every value in the enumeration is the same, we just want to return that one value as a single string. The reason for this unusual behaviour is to help get some easily usable output for the acceptance test framework we are using.
As this is just a helper for acceptance tests (i.e. we won’t be polluting namespaces in production code) I’ll dump this functionality onto any IEnumerable<T>
using an extension method.
Starting test first
Let’s start with an easy case: what should happen when we have an empty enumerable?
[TestFixture] public class FixtureFormatterTests { [Test] public void Empty_array_should_format_as_empty_string() { var emptyArray = new int[0]; Assert.That(emptyArray.ToFixtureString(), Is.EqualTo(string.Empty)); } } public static class HelperExtensions { public static string ToFixtureString<T>(this IEnumerable<T> enumerable) { return string.Empty; } }
After that monumentally brilliant piece of code, let’s add the comma-separated string part of the requirement.
[Test] public void Array_with_different_values_should_give_comma_separated_string() { var ints = new[] {1, 2, 3, 4}; Assert.That(ints.ToFixtureString(), Is.EqualTo("1,2,3,4")); }
Now we’ll get it to pass. We’ll lean heavily on the built-in String.Join(String, String[])
method to do the work for us.
public static string ToFixtureString<T>(this IEnumerable<T> enumerable) { if (enumerable.Count() == 0) return string.Empty; return string.Join(",", enumerable.Select(item => item.ToString()).ToArray()); }
This passes, but it’s a bit ugly. Let’s look at refactoring.
First refactoring
First, I’ve got a feeling that if our enumerable is empty, String.Join(...)
won’t concatenate anything, and so will just return an empty string. This would render our first line redundant.
public static string ToFixtureString<T>(this IEnumerable<T> enumerable) { return string.Join(",", enumerable.Select(item => item.ToString()).ToArray()); }
It still passes both our tests so we are safe (I love unit tests :)). We also have that ugly bit of code where we are translating our IEnumerable<T>
into an array of strings, using the Select()
extension method. As I’m keen to start over using extensions methods, let’s hide all that away in a Python-like join()
method. Python’s join()
works like this:
>>> ints = [1,2,3,4] >>> ",".join(str(i) for i in ints) '1,2,3,4'
I’d like to do that, but abstract away the sequence/enumerable to string conversion. Let’s do this using an extension method to char
:
public static string Join(this char separator, IEnumerable enumerable) { return string.Join(separator.ToString(), enumerable.Select(item => item.ToString()).ToArray()); } public static string ToFixtureString (this IEnumerable enumerable) { return ','.Join(enumerable); }
Assuming you know the whole "Join" concept, our ToFixtureString()
method is now pretty darned clean :). The original ugliness is now moved to the Join()
method, but at least it is all directly related to the purpose of that method. In its original spot I think it obscured the intention behind the ToFixtureString()
method.
Completing our ToFixtureString()
requirements
The last requirement we have for this is to only show one value if all the items in the enumerable are the same.
[Test] public void Array_with_the_same_values_should_return_that_value_as_a_single_string() { const int value = 2; var ints = new[] {value, value, value}; Assert.That(ints.ToFixtureString(), Is.EqualTo(value.ToString())); }
Here’s an attempt at get this to pass.
public static string ToFixtureString<T>(this IEnumerable<T> enumerable) { var firstItem = enumerable.First(); if (enumerable.All(item => item.Equals(firstItem))) { return firstItem.ToString(); } return ','.Join(enumerable); }
This fails our Empty_array_should_format_as_empty_string
test because the enumerable.First()
call throws with InvalidOperationException: Sequence contains no elements
. So we’re back to that enumerable.Count() == 0
line, which gets all our tests passing again.
public static string ToFixtureString<T>(this IEnumerable<T> enumerable) { if (enumerable.Count() == 0) return string.Empty; var firstItem = enumerable.First(); if (enumerable.All(item => item.Equals(firstItem))) { return firstItem.ToString(); } return ','.Join(enumerable); }
Refactoring out the empty enumerable check
I don’t like enumerable.Count()
. It needs to go through the entire enumerator to get the count, when we really only care if the enumerable is empty. Sounds like time for some more extension method abuse. Here’s some tests that require adding an IsEmpty()
extension method to IEnumerable<T>
:
[TestFixture] public class IsEmptyEnumerableTests { [Test] public void Empty_enumerable() { Assert.That(new int[0].IsEmpty()); } [Test] public void Non_empty_enumerable() { Assert.That(new[]{1,2,3}.IsEmpty(), Is.False); } } public static class HelperExtensions { //... public static bool IsEmpty<T>(this IEnumerable<T> enumerable) { return !enumerable.GetEnumerator().MoveNext(); } }
This is a bit hacky, but means we only need to to see if our enumerator has a single item to determine whether it is empty, and we can make our ToFixtureString()
method a bit more expressive as a result:
public static string ToFixtureString<T>(this IEnumerable<T> enumerable) { if (enumerable.IsEmpty()) return string.Empty; var firstItem = enumerable.First(); if (enumerable.All(item => item.Equals(firstItem))) { return firstItem.ToString(); } return ','.Join(enumerable); }
Vague semblance of a conclusion
We now have our unusual formatting covered, and IsEmpty()
and Join()
extension methods to help make our code a bit cleaner. I’m not advocating this kind of thing for every day use, but I think it shows how useful extension methods can be to make your code more expressive. It comes at the cost of changing classes that most .NET developers are familiar with, so it’s definitely something to be careful with.