9

I'm writing an extension method to compare two floats using a set number of decimal points (significant figures) to determine if they are equal instead of a tolerance or percentage difference. Looking through the other questions regarding float comparison I see complex implementations. Have I oversimplified or is this valid?

/// <summary>
/// Determines if the float value is equal to (==) the float parameter according to the defined precision.
/// </summary>
/// <param name="float1">The float1.</param>
/// <param name="float2">The float2.</param>
/// <param name="precision">The precision.  The number of digits after the decimal that will be considered when comparing.</param>
/// <returns></returns>
public static bool AlmostEquals(this float float1, float float2, int precision = 2)
{
    return (Math.Round(float1 - float2, precision) == 0);
}

Note: I'm looking for a comparison on decimal places, not tolerance. I don't want 1,000,000 to equal 1,000,001.

Kim
  • 1,068
  • 13
  • 25
  • 7
    I would write a set of Unit Tests to check if your algorithm is ok for your needs. – Wouter de Kort Feb 07 '12 at 16:55
  • 7
    I love this method name: **AlmostEquals**... – gdoron Feb 07 '12 at 16:55
  • I've written some unit tests and they pass with the values I've provided, but I'd like the advice of an audience with a deeper understanding of float implementation/behavior. – Kim Feb 07 '12 at 17:00
  • `Math.Round()` throws an exception if precision is less than zero or greater than 15. – Igor Korkhov Feb 07 '12 at 17:07
  • possible duplicate of [Floating point comparison functions for C#](http://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp) – user7116 Feb 07 '12 at 17:11
  • 2
    So how would you say "I have two floats that are both in the range 1 million to 2 million; I want them to be equal to the nearest thousand"? – Eric Lippert Feb 07 '12 at 17:30
  • @EricLippert Updated the title. In my case I actually don't care about significant figures, it's really decimal places that are important. But an interesting question! – Kim Feb 07 '12 at 18:15
  • What are you after then? It's unclear now. You want `123.45` to be equal to `678.45`? – Mr Lister Feb 07 '12 at 18:28
  • @MrLister My original problem was that I wanted (0.1F + 0.2F) == 0.3F to be true (which returns false in C#). But the implementation can be used to say that 123.123 almost equals 123.124 with 2 decimal places. I'm not concerned that 12,300 would equal 12,400 if you took 2 significant figures into account. That's what I meant about decimal places. – Kim Feb 07 '12 at 18:38
  • OK... then you mean for `123.123` to be almost equal to `123.120`, but `123.124` to _not_ be almost equal to `123.126` (because the latter rounds to `123.13`)? – Mr Lister Feb 07 '12 at 18:53
  • Not possible, see answer below – Thorsten S. Feb 09 '12 at 13:47
  • What result do you want for `AlmostEquals(0.149f, 0.151f, 1)`? – CodesInChaos Mar 18 '12 at 20:31

2 Answers2

2

Based on @infact's answer and some comments in both the question and the answer I came up with

public static bool AlmostEquals(this float float1, float float2, int precision = 2) 
{ 
    float epsilon = Math.Pow(10.0, -precision) 
    return (Math.Abs(float1-float2) <= epsilon);   
} 

This has the benfit of accepting any integer, you could check for > 1.0 precision by using precision = -x, where x is the power of 10 to check against.

I would also recommend making the default precision = 3, which would give you accuracy down to a tenth of a penny, by default, if this method were used for financials.

EtherDragon
  • 2,679
  • 1
  • 18
  • 24
  • Picked this as the answer because it addresses the idea of being concerned with the number of decimals places and expands the method to allow negative values for the precision. – Kim Feb 07 '12 at 19:40
  • Props to @infact for `return (Math.Abs(float1-float2) <= precision);` – EtherDragon Feb 07 '12 at 22:13
  • 3
    It is *NOT* working, the C# section even cites an example: http://msdn.microsoft.com/en-us/library/75ks3aby.aspx Because of the loss of precision [...] the Round(Double, Int32) method may not appear to round midpoint values to the nearest even value in the digits decimal position. This is illustrated in the following example, where 2.135 is rounded to 2.13 instead of 2.14. This occurs because internally the method multiplies value by 10digits, and the multiplication operation in this case suffers from a loss of precision. – Thorsten S. Feb 09 '12 at 20:39
2

If the user wants to "compare two floats using a set number of decimal points (significant figures)" and this actually means that we have a function

AlmostEquals(14.3XXXXXXXX, 14.3YYYYYYY, 1) == true for all possible XXX and YYY and the last parameter is the decimal place after the decimal point.

there is a simple but unfortunate answer:

It is NOT possible to program this function which will fulfill this contract. It may be possible to program something which will often give the correct result, but you cannot foresee when this will be the case, so the function is effectively worthless.

The given solutions here break already with AlmostEquals(0.06f, 0.14f, 1) = true but 0 != 1.

Why ? The first reason is the extreme sensitivity. For example: 0.0999999999.... and 0.100000...1 have different digits in the first place, but they are almost indistinguishable in the difference, they are almost exactly equal. Whatever the mythical function does, it cannot allow even small differences in calculation.

The second reason is that we want actually calculate with the numbers. I used VC 2008 with C# to print out the correct values of the Math.pow function. The first is the precision parameter, the second the hex value of the resulting float and the third is the exact decimal value.

1 3dcccccd 0.100000001490116119384765625

2 3c23d70a 0.00999999977648258209228515625

3 3a83126f 0.001000000047497451305389404296875

4 38d1b717 0.0000999999974737875163555145263671875

5 3727c5ac 0.00000999999974737875163555145263671875

6 358637bd 9.999999974752427078783512115478515625E-7

As you can see, the sequence 0.1, 0.01, 0.001 etc. produces numbers which are excellent approximations, but are either slightly too small or too big.

What if we enforce that the given place must have the correct digit ? Lets enumerate the 16 binary values for 4 bits

0.0
0.0625
0.125
0.1875
0.25
0.3125
0.375
0.4375
0.5
0.5625
0.625
0.6875
0.75
0.8125
0.875
0.9375

16 different binary numbers should be able to suffice for 10 decimal numbers if we want to calculate only with one place after the decimal point. While 0.5 is exactly equal, enforcing the same decimal digit means that 0.4 needs 0.4375 and 0.9 needs 0.9375, introducing severe errors.

Violating the first condition of extreme sensitivity means that you cannot do anything reasonable with such numbers. If you would know that the decimal place of a number has a certain value, you would not need to calculate in the first place.

The C# documentation even cites an example: http://msdn.microsoft.com/en-us/library/75ks3aby.aspx

Notes to Callers

Because of the loss of precision that can result from representing decimal values as floating-point numbers or performing arithmetic operations on floating-point values, in some cases the Round(Double, Int32) method may not appear to round midpoint values to the nearest even value in the digits decimal position. This is illustrated in the following example, where 2.135 is rounded to 2.13 instead of 2.14. This occurs because internally the method multiplies value by 10digits, and the multiplication operation in this case suffers from a loss of precision.

Thorsten S.
  • 4,144
  • 27
  • 41
  • Your premise is incorrect. While floating point numbers *are* approximations, it's certainly possible to predict the degree of error in that approximation, and as long as you know that, you also know whether or not your comparison is valid. – Robert Harvey Mar 18 '12 at 21:56
  • It is possible to predict the degree of error for a decimal-binary conversion and back, but due to the problem the necessary resolution can be as small as the smallest floating point number, breaking the code. The OP wants that AlmostEquals(10.0,10.0XXXXXX,1) == true, allowing the numbers 10.0000000..1 and 10.0999999999999.... as valid input. The floating point conversion routines choose the next floating point, but that may 9.999999999 or 11.10000001. Now with extreme care you can program constructors which give the smallest fitting decimal number, but you cannot add etc. because it breaks. – Thorsten S. Mar 18 '12 at 22:19
  • +1 @ThorstenS. for a very refined answer and elaboration on this matter. – XAMlMAX Feb 19 '14 at 15:13