StephenCleary / Comparers

The last comparison library you'll ever need!
MIT License
427 stars 33 forks source link

Float/double equality comparers #28

Open ljani opened 4 years ago

ljani commented 4 years ago

How about adding special equality comparers for float and double types, which allow you to specify the allowed tolerance for difference?

As far as I know there are none in .NET or this library.

StephenCleary commented 4 years ago

I like it! The next update is adding string-specific comparers, so this will go well as a part of that update.

StephenCleary commented 4 years ago

Looks like it won't be very straightforward: https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ https://www.boost.org/doc/libs/1_72_0/libs/test/doc/html/boost_test/testing_tools/extended_comparison/floating_point/floating_points_comparison_impl.html

In particular, "tolerance" equality isn't transitive, so I'm not sure if these can be made into equality operators or not.

ljani commented 4 years ago

Yeah, it might not make sense to add it to the core library.

I was alternatively thinking having an overload accepting a lambda (Func<T, T, bool>) for eg. ThenEquateBy to test the difference. Similarly, you'd need to throw NotSupportedException from GetHashCode.

My use case is mainly testing. I usually add my own IEqualityComparer<float> for each project I have, but it'd be nice to have it in a library with other similar functionality.

Here's an example what I'd like to do:

var comparer = EqualityComparerBuilder
    .For<DataSample>()
    .EquateBy(sample => sample.Timestamp)
    .ThenEquateBy(sample => sample.Value, (a, b) => Math.Abs(a - b) < 0.1f);
Assert.Equal(expected, actual, comparer);

EDIT: DataSample.Value is a float.

StephenCleary commented 4 years ago

The problem with doing that to ThenEquateBy is that it does return an IEqualityComparer<T> which has unexpected behavior (not transitive, throwing for GetHashCode). Major version 4 of this library did have "anonymous comparers" which allowed passing delegates as you suggest, but this was removed since it is too easy to create comparers that don't work correctly (e.g., a tolerance equality comparer).

I think it does make sense for this library to implement a kind of "best known practice" for floating-point comparisons (tolerance and relative tolerance) but they would be in their own utility class and not ever exposed as actual comparers (and by extension, not be used in ThenEquateBy, etc). For your use case, I'd recommend writing an AssertEx.Equal that used those utility methods, and if code wanted to do floating-point comparisons at runtime, it could invoke the utility methods directly.