Search code examples
c#wpfsortingicomparer

C# Sorting/comparing items


I have a class (Patch) that I want to have sorted so I implemented IComparer.

However, it needs to be sorted depending on how the user wants it, e.g.: - key1, key2, key3 - key1, key3, key2

For each key compare I have written a IComparer class, however, I was wondering how to implement its connection. i.e. when sorting I only can pass one IComparer instance.

Or should I make an IComparer class for each kind of full sorting, i.e. IComparerKey1Key2Key3, IComparerKey1Key3Key2 etc?


Solution

  • You could make a generic comparer that takes a delegate to select the key:

    class ByKeyComparer<T, TKey> : IComparer<T>
    {
        private readonly Func<T, TKey> _keySelector;
        private readonly IComparer<TKey> _keyComparer;
    
        public ByKeyComparer(Func<T, TKey> keySelector, IComparer<TKey> keyComparer = null)
        {
            if (keySelector == null) throw new ArgumentNullException("keySelector");
            _keySelector = keySelector;
            _keyComparer = keyComparer ?? Comparer<TKey>.Default;
        }
    
        public int Compare(T x, T y)
        {
            return _keyComparer.Compare(_keySelector(x), _keySelector(y));
        }
    }
    

    With a helper class to take advantage of type inference (so you don't need to specify the type of the key):

    static class ByKeyComparer<T>
    {
        public static IComparer<T> Create<TKey>(Func<T, TKey> keySelector, IComparer<TKey> keyComparer = null)
        {
            return new ByKeyComparer<T, TKey>(keySelector, keyComparer);
        }
    }
    

    You can use it like this:

    var patchVersionComparer = ByKeyComparer<Patch>.Create(p => p.Version);
    patches.Sort(patchVersionComparer);
    

    If you need to combine several compare keys, you can create a comparer that uses other comparers:

    class CompositeComparer<T> : IComparer<T>
    {
        private readonly IEnumerable<IComparer<T>> _comparers;
    
        public CompositeComparer(IEnumerable<IComparer<T>> comparers)
        {
            if (comparers == null) throw new ArgumentNullException("comparers");
            _comparers = comparers;
        }
    
        public CompositeComparer(params IComparer<T>[] comparers)
            : this((IEnumerable<IComparer<T>>)comparers)
        {
        }
    
        public int Compare(T x, T y)
        {
            foreach (var comparer in _comparers)
            {
                int result = comparer.Compare(x, y);
                if (result != 0)
                    return result;
            }
            return 0;
        }
    }
    

    Example usage:

    var comparer = new CompositeComparer<Patch>(
                           ByKeyComparer<Patch>.Create(p => p.Key1),
                           ByKeyComparer<Patch>.Create(p => p.Key2),
                           ByKeyComparer<Patch>.Create(p => p.Key3));
    patches.Sort(comparer);
    

    EDIT: here's a more fluent API:

    static class ByKeyComparer<T>
    {
        public static IComparer<T> CompareBy<TKey>(Func<T, TKey> keySelector, IComparer<TKey> keyComparer = null)
        {
            return new ByKeyComparer<T, TKey>(keySelector, keyComparer);
        }
    }
    
    static class ComparerExtensions
    {
        public static IComparer<T> ThenBy<T, TKey>(this IComparer<T> comparer, Func<T, TKey> keySelector, IComparer<TKey> keyComparer = null)
        {
            var newComparer = ByKeyComparer<T>.CompareBy(keySelector, keyComparer);
    
            var composite = comparer as CompositeComparer<T>;
            if (composite != null)
                return new CompositeComparer<T>(composite.Comparers.Concat(new[] { newComparer }));
            return new CompositeComparer<T>(comparer, newComparer);
        }
    }
    

    Example:

    var comparer = ByKeyComparer<Patch>.CompareBy(p => p.Key1)
                                       .ThenBy(p => p.Key2)
                                       .ThenBy(p => p.Key3);
    patches.Sort(comparer);
    

    (obviously you might want to add *Descending versions of the CompareBy and ThenBy methods to allow ordering in descending order)