Wednesday, June 22, 2016

FuncComparer

Often we need a means to compare collections of complex structures. Consider



public class Contact
{
    public Guid Id { get; private set; }
    public string Name { get; set; }
    public Contact() { Id = Guid.NewGuid(); }
}

[TestMethod]
public void Test()
{
    Contact[] expected = new[] 
    {
        new Contact { Name = "Bob", },
        new Contact { Name = "Abel", },
    };
    Contact[] actual = new[] 
    {
        new Contact { Name = "Abel", }, 
        new Contact { Name = "Charlie", },
    };
    // TODO: test for equivalence?
}


I often run into situations like these when testing functions on collections, as presented above. I also see similar situations when comparing collections from a client (that may have modified or net-new entities) to collections from a datastore.


There are many ways to resolve these sorts of scenarios, like in-lining comparison logic, overriding Equals method, or even declaring a custom equivalence class that implements IEqualityComparer<Contact>. The first method is brittle and completely not portable. Overriding Equals is a little more portable, but still fairly brittle - for instance, if in one context I require equality based on Name, in another context equality of Id, and yet another based on both. The third method is portable and is externalized from the class so that it may be clearly swapped depending on context (consider ContactNameEqualityComparer versus ContactIdEqualityComparer).


My only gripe with this last method is that it is always so tedious to implement the boiler-plate comparisons in every instance of a comparer (for instance, writing trivial accept and reject criteria for ContactNameEqualityComparer, and then re-writing that criteria again for ContactIdEqualityComparer, and then again for any other comparers that exist in our system for other types).


FuncComparer



Really I want something that encapsulates basic comparison functionality while being able to supply comparison criteria as lambdas.


public class FuncComparer<T> : IComparer, IComparer<T>
{

    private readonly Comparison<T>[] comparisons = null;
    private readonly int xIsNotNullComparison = 0;
    private readonly int yIsNotNullComparison = 0;

    public FuncComparer(
        Comparison<T> comparison, 
        bool isNullLessThan = true) :
        this (new[] { comparison, }, isNullLessThan)
    {
    }

    public FuncComparer(
        Comparison<T>[] comparisons, 
        bool isNullLessThan = true)
    {
        this.comparisons = comparisons;
        this.xIsNotNullComparison = isNullLessThan ? 1 : -1;
        this.yIsNotNullComparison = isNullLessThan ? -1 : 1;
    }

    // interfaces

    #region IComparer Members

    public int Compare(object x, object y)
    {
        int comparison = 0;
        if (!ReferenceEquals(x, y))
        {
            bool xIsNull = true;
            bool yIsNull = true;
            T a = default(T);
            T b = default(T);
            if (x is T)
            {
                xIsNull = false;
                a = (T)(x);
            }
            if (y is T)
            {
                yIsNull = false;
                b = (T)(y);
            }
            comparison = Compare(a, xIsNull, b, yIsNull);
        }
        return comparison;
    }

    #endregion

    #region IComparer<T> Members

    public int Compare(T x, T y)
    {
        int comparison = 0;
        if (!ReferenceEquals(x, y))
        {
            comparison = Compare(x, x == null, y, y == null);
        }
        return comparison;
    }

    #endregion

    // private methods

    /// <summary>
    /// Compare two strong-typed values. Explicit 'is null' 
    /// parameters are required for scenarios where 
    /// <typeparamref name="T"/> is a valuetype and type-less 
    /// compare is used. In such cases, type-less method uses 
    /// default-type value which will telescope nulls to default
    /// values. For example, when comparing a series of integers,
    /// any nulls in our inputs will be interpreted as '0'.
    /// </summary>
    /// <param name="x"></param>
    /// <param name="isXNull"></param>
    /// <param name="y"></param>
    /// <param name="isYNull"></param>
    /// <returns></returns>
    private int Compare(T x, bool isXNull, T y, bool isYNull)
    {
        int comparison = 0;
        if (!isXNull && !isYNull)
        {
            for (
                int i = 0; 
                i < comparisons.Length && comparison == 0; 
                i++)
            {
                comparison = comparisons[i](x, y);
            }
        }
        else if (!isXNull)
        {
            comparison = xIsNotNullComparison;
        }
        else if (!isYNull)
        {
            comparison = yIsNotNullComparison;
        }
        // both x and y are null, implies equality, no-op
        //else
        //{
        //}
        return comparison;
    }

}

What we may now do is
public class Contact
{
    public Guid Id { get; private set; }
    public string Name { get; set; }
    public Contact() { Id = Guid.NewGuid(); }
}

[TestMethod]
public void Test()
{
    FuncComparer<Contact> contactNameComparer = 
        new FuncComparer<Contact>(
            (a,b) => string.Compare(
                a.Name,
                b.Name,
                StringComparison.InvariantCultureIgnoreCase));
    Contact[] expected = new[] 
    {
        new Contact { Name = "Bob", },
        new Contact { Name = "Abel", },
    };
    Contact[] actual = new[] 
    {
        new Contact { Name = "Abel", }, 
        new Contact { Name = "Charlie", },
    };
    // NOTE: successfully demonstrates ease-of-use, esp
    // when tests are repeated with minor variances in
    // expectation
    CollectionAssert.AreNotEqual(
        expected, 
        actual, 
        contactNameComparer);
    CollectionAssert.AreEqual(
        new[] 
        {
            new Contact { Name = "Abel", },
            new Contact { Name = "Charlie", },
        },
        actual,
        contactNameComparer);
}


Why 'isXNull' and 'isYNull'?



The oddest thing about FuncComparer is how it handles null checks. We perform null checks on the way in, before the private core-implementation executes. This is because the private core-implementation is strongly typed, co-ercing any null inputs into base-type defaults. For reference types, such as any class, this is ok, since default(T) is still null. However, for valuetypes like int or struct, we end up telescoping nulls into default values, which is a loss-full transformation, a reduction in fidelity.


We overcome this through explicit null tests on the way in, and pass that on as state to our core-implementation.



[TestMethod]
public void Test()
{
    IComparer typelessComparer = 
        new FuncComparer<int>(
            (a,b) => a.CompareTo(b),
            true);
    // we expect 'null is less-than', so x as null should
    // return less-than
    int expectedComparison = -1;
    int actualComparison = 
        typelessComparer.Compare(null, 0);
    Assert.AreEqual(expectedComparison, actualComparison);
}

One last minor detail, is the ability to configure an ordinal interpretation of null in relation to all other values. Our constructor accepts a boolean that indicates any null values should be considered less-than non-null values, or vice versa. This allows us to tweak things like list ordering.
[TestMethod]
public void Test()
{
    IComparer typelessComparer = 
        new FuncComparer<int>(
            (a,b) => a.CompareTo(b),
            false);
    object[] expected = new object[] { 0, 1, 2, 3, 4, null, };
    ArrayList actual = new ArrayList { 1, null, 4, 2, 3, 0, };
    actual.Sort(typelessComparer);
    CollectionAssert.AreEqual(expected, actual);
}

FuncEqualityComparer

Upon implementing a FuncComparer, the very next thing that I do is implement a FuncEqualityComparer; so it follows that I should post this next. An implementation of IEqualityComparer on top of an IComparer is relatively trivial and straightforward,


public class FuncEqualityComparer<T> : 
    FuncComparer<T>, 
    IEqualityComparer, 
    IEqualityComparer<T>
{

    private readonly Func<T, int> getHashCode = null;

    public FuncEqualityComparer(
        Comparison<T> comparison, 
        Func<T, int> getHashCode, 
        bool isNullLessThan = true) :
        this(new[] { comparison, }, getHashCode, isNullLessThan)
    {
    }

    public FuncEqualityComparer(
        Comparison<T>[] comparisons, 
        Func<T, int> getHashCode, 
        bool isNullLessThan = true) :
        base(comparisons, isNullLessThan)
    {
        this.getHashCode = getHashCode;
    }

    // interfaces

    #region IEqualityComparer Members

    public new bool Equals(object x, object y)
    {
        int comparison = Compare(x, y);
        bool isEqual = comparison == 0;
        return isEqual;
    }

    public int GetHashCode(object obj)
    {
        int hashCode = 0;
        T o = default(T);
        if (obj is T)
        {
            o = (T)(obj);
        }
        hashCode = getHashCode(o);
        return hashCode;
    }

    #endregion

    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        int comparison = Compare(x, y);
        bool isEqual = comparison == 0;
        return isEqual;
    }

    public int GetHashCode(T obj)
    {
        int hashCode = getHashCode(obj);
        return hashCode;
    }

    #endregion

}



No real secret sauce here; merely account for GetHashCode and interpret an underlying comparison.