Wednesday 23 May 2012

How To properly implement "Any value except X" in C#?

This time it's going to be dead simple. I promise.

Prior to reading this post, take a look at this post by Mark Seemann, to understant the concept of anonymous values.

Sometimes, when writing specifications/unit tests, we want to say that we can use any enumeration value except some value "x". Let's take an example of reporting feature access denied for non-admin user:

var reportingFeature = new ReportingFeature();
var nonAdmin = Any.Except(Users.Admin);

Assert.IsFalse(reportingFeature.IsAvailableFor(nonAdmin);

As you can see, here, we say "Any user except Admin". So, how to implement such a facility? The first, naive implementation (do not use it!) goes would be something along these lines:

public static T Besides<T>(T excludedValue)
{
  Random random = new Random();
  var values = Enum.GetValues(typeof(T));
  T val = default(T);
  do
  {
    var index = random.Next(0, values.Length);
    val = (T) values.GetValue(index);
  } while(val.Equals(excludedValue));
  return val;
}

In plain English, we take random value until we get a value that is different than the passed one. However, this solution usually performs at least two iterations before reaching the target, especially when the enumeration consists of only two possible values. Theoretically, it may do much more. This is a better solution:

public static T Besides<T>(T excludedValue)
{
  var genericValues = Enum.GetValues(typeof(T));
  var values = new List<T>();
  
  foreach(var v in genericValues)
  {
    if(!v.Equals(excludedValue))
    {
      values.Add((T)v);
    }
  }
  var index = random.Next(0, values.Count);
  return values[index];
}

Here, we put all the possible values into a list and remove the one we don't want. Then we just take the random value from the list - any is fine. The foreach loop can be easily changed to Linq expression (from...select) and the method can be easily extended to support many parameters (via params keyword), so you could write something like this:

var privilegedUser = Any.Except(Users.Intern, Users.ProbationUser);

See? I told you it was going to be dead simple this time :-)

No comments: