Take a custom IComparer, that treats two doubles as equal if their difference is less than a given epsilon.
What would happen if this IComparer is used in a OrderBy().ThenBy() clause?
Specifically I am thinking of the following implementation:
public class EpsilonComparer : IComparer<double>
{
private readonly double epsilon;
public EpsilonComparer(double epsilon)
{
this.epsilon = epsilon;
}
public int Compare(double d1, double d2)
{
if (Math.Abs(d1-d2)<=epsilon) return 0;
return d1.CompareTo(d2);
}
}
Now this IComparer relationship is clearly not transitive. (if a ~ b and b ~ c then a ~ c
)
With epsilon== 0.6 :
- Compare(1, 1.5) == 0
- Compare(1.5, 2) == 0
yet - Compare(1, 2 ) == -1
What would happen if this IComparer was used in an OrderBy query, like this:
List<Item> itemlist;
itemList = itemlist.OrderBy(item=>item.X, new EpsilonComparer(0.352))
.ThenBy (item=>item.Y, new EpsilonComparer(1.743)).ToList();
Would the sort behave as one would expect, sorting the list first by X, then by Y, while treating roughly equal values as exactly equal?
Would it blow up under certain circumstances?
Or is this whole sort ill-defined?
What exactly are the consequences of using an IComparer without transitivity?
(I know that this is most likely undefined behavior of the c# language. I am still very much interested in an answer.)
And is there an alternative way to get this sorting behaviour?
(besides rounding the values, which would introduce artifacts when for two close doubles one is rounded up and the other down)
An online fidle of the code in this question is available here:
The problem is that the first sorting level (on
X
) can result in different orders already. Imagine that all items are within one epsilon of each other. Then all sort orders are consistent with your comparer because it will always return 0. The sort algorithm could flip coins and still provide a "right" answer. Not useful.If the first level is arbitrarily sorted, you cannot expect the 2nd sorting level to work.
Of course, all of this discussion is moot because you are violating the precondition of the sorting API. Even if it happened to work, you couldn't be sure that it would work on a) all data b) on all future releases of .NET.
How can you still achieve your goal? Your problem is just ill-defined because many solutions are possible. I get what you want to achieve but it is not possible with your current definition of the problem.
I propose this: sort all items by
X
(without epsilon). Then, traverse the sorted items left-to-right and merge the items into groups that one epsilon wide at most. That gives you groups of items whoseX
-value is at most epsilon apart.You can then use the group number as the first sorting level. It is just a simple integer, so no trouble sorting on it. For the
Y
field, you can then use a normal comparer without epsilon (or repeat the same trick).