I often find myself implementing IComparable{T}
in different classes for sorting in a very similar fashion:
- Values meeting criteria X and Y should be sorted to the front
- Values meeting criteria Z and W should be sorted to the back
- Values not meeting any special criteria can stay where they are
So, I've created a class that abstracts this comparison style. It works okay, but I feel like there may be a code smell here.
Is there anything conceptually wrong with this class? Is there anything that could be simplified, or made more robust?
/// <summary>
/// Implementation of IComparer that uses a mutable collection of
/// delegates to compare instances.
/// </summary>
/// <remarks>Each condition is added with a boolean value indicating
/// whether an instance meeting the condition should be considered
/// "less than" or "greater than" instances not meeting the condition.
/// Conditions are evaluated in the order they are added to the collection.
/// If both instances have the same result for a condition, the condition has no effect.
/// If both instances have the same result for all conditions, the comparison result is 0.</remarks>
public class DynamicComparer<T> :
IComparer<T>,
IEnumerable<KeyValuePair<Func<T, bool>, bool>> {
private readonly List<KeyValuePair<Func<T, bool>, bool>> conditions;
private bool isReadOnly;
public DynamicComparer() {
this.conditions = new List<KeyValuePair<Func<T, bool>, bool>>();
}
#region Condition collection
public bool IsReadOnly {
get { return isReadOnly; }
set {
if (isReadOnly == true)
throw new InvalidOperationException("Readonly");
isReadOnly = value;
}
}
public void Clear() {
if (isReadOnly == true)
throw new InvalidOperationException("Readonly");
conditions.Clear();
}
public void Add(Func<T, bool> condition, bool prefer) {
if (condition == null) throw new ArgumentNullException("condition");
if (isReadOnly == true)
throw new InvalidOperationException("Readonly");
conditions.Add(new KeyValuePair<Func<T, bool>, bool>(condition, prefer));
}
public IEnumerator<KeyValuePair<Func<T, bool>, bool>> GetEnumerator() { return conditions.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
#endregion
#region Comparison
public int Compare(T x, T y) {
foreach (var condition in conditions) {
var result = condition.Value
? Prefer(condition.Key, x, y)
: Defer(condition.Key, x, y);
if (result != 0)
return result;
}
return 0;
}
/// <summary>
/// Tests both items.
/// If both tests have same result, returns 0;
/// otherwise, sorts passing item first.
/// </summary>
private int Prefer(Func<T, bool> condition, T x, T y) {
var testX = condition(x);
var testY = condition(y);
return testX
? (testY ? 0 : -1)
: (testY ? 1 : 0);
}
/// <summary>
/// Tests both items.
/// If both tests have same result, returns 0;
/// otherwise, sorts failing item first.
/// </summary>
private int Defer(Func<T, bool> condition, T x, T y) {
var testX = condition(x);
var testY = condition(y);
return testX
? (testY ? 0 : 1)
: (testY ? -1 : 0);
}
#endregion
}
Here is a derived class:
public class SubReportComparer : DynamicComparer<ISubReport> {
public SubReportComparer() : base() {
Add(sub => sub == null, true);
Add(sub => sub.Name == "FirstSubReport", true);
Add(sub => sub.IsIssue, true);
Add(sub => sub.Name == "LastSubReport", false);
Add(sub => sub.Name == "2ndToLast", false);
IsReadOnly = true;
}
}
And here is what client code will look like:
List<ISubReport> subs = GetSubReports();
subs.Sort(new SubReportComparer());