Permalink
Browse files

OrderedComparer - simple comparer chaining

When creating derived collection you need to provide an ordering function.
Writing these comparators is easy enough when you only need to deal with a
single property, ie (```(x,y) => x.Show.CompareTo(y.Show))```).  But they
can get complex in a hurry if you need to take multiple properties into
account. The OrderedComparer helps with that by adding some syntactic
sugar for chaining multiple comparisons together.

Say you have a list of employees (yup, I have zero imagination) and you
whish to sort these by descingin Salary, descending Age and ascending
Name (ignoring case). Today you'd probably write something like this:

```int CompareEmployees(Employee x, Employee y) {
  int ret = 0;

  ret = -(x.Salary.CompareTo(y.Salary));

  if(ret != 0) {
    return ret;
  }

  ret = -(x.Age.CompareTo(y.Age));

  if(ret != 0) {
    return ret;
  }

  return StringComparer.OrdinalIgnoreCase.Compare(x.Name, y.Name);
}```

With OrderedComparer you'd write that like this:

```OrderedComparer<Employee>
  .OrderByDescending(x => x.Salary)
  .ThenByDescending(x => x.Age)
  .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase);
```

LINQ-style chaining. The end result is a reusable generic comparer, an
IComparer<Employee> instance which you can use for sorting not only
derived collections but also generic lists or any other type which relies
on ICompare<T>.

The OrderedComparer<T> class is only syntactic sugar in itself since the
ThenBy and ThenByDescending methods are implemented as extension methods
on the IComparer<T> interface. The OrderedComparer<T> only exists to
provide a nice starting point but you could also start from an existing
comparer.

```// This is a stupid comparer
StringComparer.OrdinalIgnoreCase
  .ThenBy(x => x, StringComparer.Ordinal);
```
  • Loading branch information...
1 parent dfcc95f commit 42f8576ce5e8be35e9ac5ab6500bca562bb235b5 @niik niik committed Mar 20, 2013
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace ReactiveUI.Tests
+{
+ public class OrderedComparerTests
+ {
+ [DebuggerDisplay("{Name}")]
+ public class Employee
+ {
+ public string Name;
+ public int Age;
+ public int Salary;
+ }
+
+ [Fact]
+ public void SmokeTest()
+ {
+ var adam = new Employee { Name = "Adam", Age = 50, Salary = 125 };
+ var alice = new Employee { Name = "Alice", Age = 25, Salary = 100 };
+ var bob = new Employee { Name = "Bob", Age = 30, Salary = 75 };
+ var carol = new Employee { Name = "Carol", Age = 35, Salary = 100 };
+ var xavier = new Employee { Name = "Xavier", Age = 35, Salary = 100 };
+
+ var employees = new List<Employee> { adam, alice, bob, carol, xavier };
+
+ employees.Sort(OrderedComparer<Employee>.OrderBy(x => x.Name));
+ Assert.True(employees.SequenceEqual(new[] { adam, alice, bob, carol, xavier }));
+
+ employees.Sort(OrderedComparer<Employee>
+ .OrderByDescending(x => x.Age)
+ .ThenBy(x => x.Name)
+ );
+ Assert.True(employees.SequenceEqual(new[] { adam, carol, xavier, bob, alice }));
+
+ employees.Sort(OrderedComparer<Employee>
+ .OrderByDescending(x => x.Salary)
+ .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
+ );
+ Assert.True(employees.SequenceEqual(new[] { adam, alice, carol, xavier, bob }));
+
+ employees.Sort(OrderedComparer<Employee>
+ .OrderByDescending(x => x.Age)
+ .ThenByDescending(x => x.Salary)
+ .ThenBy(x => x.Name)
+ );
+ Assert.True(employees.SequenceEqual(new[] { adam, carol, xavier, bob, alice }));
+ }
+
+ [Fact]
+ public void CustomComparerTest()
+ {
+ var items = new List<string> { "aaa", "AAA", "abb", "aaaa" };
+
+ items.Sort(OrderedComparer<string>.OrderBy(x => x, StringComparer.Ordinal));
+ Assert.True(items.SequenceEqual(new[] { "AAA", "aaa", "aaaa", "abb" }));
+
+ items.Sort(OrderedComparer<string>.OrderByDescending(x => x.Length).ThenBy(x => x, StringComparer.Ordinal));
+ Assert.True(items.SequenceEqual(new[] { "aaaa", "AAA", "aaa", "abb" }));
+
+ items.Sort(OrderedComparer<string>.OrderBy(x => x.Length).ThenBy(x => x, StringComparer.Ordinal));
+ Assert.True(items.SequenceEqual(new[] { "AAA", "aaa", "abb", "aaaa" }));
+
+ items.Sort(OrderedComparer<string>.OrderBy(x => x.Length).ThenBy(x => x, StringComparer.OrdinalIgnoreCase));
+ Assert.True(items.SequenceEqual(new[] { "AAA", "AAA", "abb", "aaaa" }, StringComparer.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void ChainOntoRegularIComparables()
+ {
+ var items = new List<string> { "aaa", "AAA", "abb", "aaaa" };
+ var comparer = StringComparer.OrdinalIgnoreCase;
+
+ items.Sort(comparer);
+ Assert.True(items.SequenceEqual(new[] { "AAA", "aaa", "aaaa", "abb" }, StringComparer.OrdinalIgnoreCase));
+
+ items.Sort(comparer.ThenByDescending(x => x, StringComparer.Ordinal));
+ Assert.True(items.SequenceEqual(new[] { "aaa", "AAA", "aaaa", "abb" }, StringComparer.Ordinal));
+ }
+ }
+}
@@ -119,6 +119,7 @@
<Compile Include="MessageBusTest.cs" />
<Compile Include="ObservableAsPropertyHelperTest.cs" />
<Compile Include="ObservedChangedMixinTest.cs" />
+ <Compile Include="OrderedComparerTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PropertyBindingTest.cs" />
<Compile Include="ReactiveCollectionTest.cs" />
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+
+namespace ReactiveUI
+{
+ /// <summary>
+ /// Convienience class providing a starting point for chaining comparers.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public static class OrderedComparer<T>
+ {
+ /// <summary>
+ /// Creates a comparer that will sort elements in ascending order based on the values returned by the provided
+ /// selector. The values will be compared using the default comparer for the return type of the selector.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ public static IComparer<T> OrderBy<TValue>(Func<T,TValue> selector)
+ {
+ return ComparerChainingExtensions.ThenBy<T, TValue>(null, selector);
+ }
+
+ /// <summary>
+ /// Creates a comparer that will sort elements in ascending order based on the values returned by the provided
+ /// selector. The selector values will be compared using the provided comparer or the default comparer for the
+ /// return type of the selector if no comparer is specified.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ /// <param name="comparer">
+ /// The comparer to use when comparing the values returned by the selector.
+ /// The default comparer for that type will be used if this parameter is null.
+ /// </param>
+ public static IComparer<T> OrderBy<TValue>(Func<T, TValue> selector, IComparer<TValue> comparer)
+ {
+ return ComparerChainingExtensions.ThenBy<T, TValue>(null, selector, comparer);
+ }
+
+ /// <summary>
+ /// Creates a comparer that will sort elements in descending order based on the values returned by the provided
+ /// selector. The values will be compared using the default comparer for the return type of the selector.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ public static IComparer<T> OrderByDescending<TValue>(Func<T, TValue> selector)
+ {
+ return ComparerChainingExtensions.ThenByDescending<T, TValue>(null, selector);
+ }
+
+ /// <summary>
+ /// Creates a comparer that will sort elements in descending order based on the values returned by the provided
+ /// selector. The selector values will be compared using the provided comparer or the default comparer for the
+ /// return type of the selector if no comparer is specified.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ /// <param name="comparer">
+ /// The comparer to use when comparing the values returned by the selector.
+ /// The default comparer for that type will be used if this parameter is null.
+ /// </param>
+ public static IComparer<T> OrderByDescending<TValue>(Func<T, TValue> selector, IComparer<TValue> comparer)
+ {
+ return ComparerChainingExtensions.ThenByDescending<T, TValue>(null, selector, comparer);
+ }
+ }
+
+ public static class ComparerChainingExtensions
+ {
+ /// <summary>
+ /// Creates a derived comparer based on the given parent comparer. The returned comparer will sort elements
+ /// using the parent comparer first. If the parent considers the values equal elements will be sorted
+ /// in ascending order based on the values returned by the provided selector. The selector values will be
+ /// compared using the default comparer for the return type of the selector.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ public static IComparer<T> ThenBy<T, TValue>(this IComparer<T> parent, Func<T, TValue> selector)
+ {
+ return ThenBy(parent, selector, Comparer<TValue>.Default);
+ }
+
+ /// <summary>
+ /// Creates a derived comparer based on the given parent comparer. The returned comparer will sort elements
+ /// using the parent comparer first. If the parent considers the values equal elements will be sorted
+ /// in ascending order based on the values returned by the provided selector. The selector values will be
+ /// compared using the provided comparer or the default comparer for the return type of the selector if no
+ /// comparer is specified.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ public static IComparer<T> ThenBy<T, TValue>(this IComparer<T> parent, Func<T, TValue> selector, IComparer<TValue> comparer)
+ {
+ return new ChainedComparer<T>(parent, (x, y) => comparer.Compare(selector(x), selector(y)));
+ }
+
+ /// <summary>
+ /// Creates a derived comparer based on the given parent comparer. The returned comparer will sort elements
+ /// using the parent comparer first. If the parent considers the values equal elements will be sorted
+ /// in descending order based on the values returned by the provided selector. The selector values will be
+ /// compared using the default comparer for the return type of the selector.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ public static IComparer<T> ThenByDescending<T, TValue>(this IComparer<T> parent, Func<T, TValue> selector)
+ {
+ return ThenByDescending(parent, selector, Comparer<TValue>.Default);
+ }
+
+ /// <summary>
+ /// Creates a derived comparer based on the given parent comparer. The returned comparer will sort elements
+ /// using the parent comparer first. If the parent considers the values equal elements will be sorted
+ /// in descending order based on the values returned by the provided selector. The selector values will be
+ /// compared using the provided comparer or the default comparer for the return type of the selector if no
+ /// comparer is specified.
+ /// </summary>
+ /// <param name="selector">A function supplying the values for the comparator.</param>
+ public static IComparer<T> ThenByDescending<T, TValue>(this IComparer<T> parent, Func<T, TValue> selector, IComparer<TValue> comparer)
+ {
+ return new ChainedComparer<T>(parent, (x, y) => -comparer.Compare(selector(x), selector(y)));
+ }
+ }
+
+ internal sealed class ChainedComparer<T> : IComparer<T>
+ {
+ private IComparer<T> parent;
+ private Comparison<T> inner;
+
+ public ChainedComparer(IComparer<T> parent, Comparison<T> comparison)
+ {
+ if (comparison == null)
+ throw new ArgumentNullException("comparison");
+
+ this.parent = parent;
+ this.inner = comparison;
+ }
+
+ public int Compare(T x, T y)
+ {
+ int parentResult = parent == null ? 0 : parent.Compare(x, y);
+
+ return parentResult != 0 ? parentResult : inner(x, y);
+ }
+ }
+}
@@ -140,6 +140,7 @@
<Compile Include="ObservableAsPropertyHelper.cs" />
<Compile Include="ObservableAsyncMRUCache.cs" />
<Compile Include="ObservedChangedMixin.cs" />
+ <Compile Include="OrderedComparer.cs" />
<Compile Include="POCOObservableForProperty.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PropertyBinding.cs" />

0 comments on commit 42f8576

Please sign in to comment.