Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this a valid float comparison that accounts for a set number of decimal places?

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.

like image 849
Kim Avatar asked Feb 07 '12 16:02

Kim


People also ask

Can floats have decimal values?

Integers and floats are two different kinds of numerical data. An integer (more commonly called an int) is a number without a decimal point. A float is a floating-point number, which means it is a number that has a decimal place. Floats are used when more precision is needed.

How do you compare floating point numbers?

Relative Comparison of Floating-point ValuesIf a and b differ in sign then returns the largest representable value for T. If both a and b are both infinities (of the same sign), then returns zero. If just one of a and b is an infinity, then returns the largest representable value for T.

Is 1.2 float or double?

1.2 is a double (8 bytes). 1.2f is a float (4 bytes). Save this answer.


2 Answers

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.

like image 27
Thorsten S. Avatar answered Oct 04 '22 03:10

Thorsten S.


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.

like image 172
EtherDragon Avatar answered Oct 04 '22 03:10

EtherDragon