Introduction
This tip reveals number problems that exist in .NET. The first part describes common limitations of floating numbers. In the second part specific to .NET number problems are shown.
This will be very helpful for junior and mid level developers, though still in my career I have been meeting senior developers that are not aware of such basic things.
Part 1 - Floating Numbers
There are 3 types of floating numbers in .NET:
float or System.Single // 32 bit floating number with base 2
double or System.Double // 64 bit floating number with base 2
decimal or System.Decimal // 128 bit floating number with base 10
(float
, double
and decimal
are the keywords available in C# and not in VB.)
Decimal
floating number is mostly convenient for people.
decimal m = 102.45m
Dim m As Decimal = 102.45
In the computer, it will be represented approximately as we expected:
10^2 10^1 10^0 . 10^-1 10^-2
100 10 1 . 1/10 1/100
1 0 2 . 4 5
The same strategy is used in Single
and Double
(base 2 floating numbers)
double d = 12.75
Dim d as Double = 12.75
2^3 2^2 2^1 2^0 . 2^-1 2^-2
8 4 2 1 . 1/2 1/4
----------------------------------------
1 1 0 0 . 1 1 ----------------------------------------
8 + 4 + 0 + 0 . 1/2 + 1/4 = 12.75
The first restriction is that sum of 2^(-N) cannot map all fractional numbers with limitation in bits. For example, decimal 0.2 is represented in endless double number.
Using DoubleConverter created by Jon Skeet, we can look at the representation of the number 0.2 in double format:
0.200000000000000011102230246251565404236316680908203125
So do not be surprised in equality checking for base-2 floating numbers (it is always approximation if exact conversion is not possible).
[TestMethod]
public void DoubleNumberEqualityCheck()
{
double d1 = 0.6 - 0.2;
double d2 = 0.4;
Assert.AreNotEqual(d1, d2); }
<TestMethod()> Public Sub DoubleNumberEqualityCheck()
Dim d1 As Double = 0.6 - 0.2
Dim d2 As Double = 0.4
Assert.AreNotEqual(d1, d2) ' d1 <> d2
End Sub
Use number type that works for the task.
Decimal
type fits well for money. Double
and Single
are good for scientific calculations, because processor works faster with them (than with Decimal
) and approximation can be smartly taken.
Because of its nature, this limitation exists for any program language (Java, .NET, SQL).
Part 2 Floating Numbers in .NET
Conversion Problem
The next conversion problem is less known. Casting and converting in C# have distinct implementation.
[TestMethod]
public void ConvertAndCastFromDecimalToInt32ShouldBeTheSame()
{
decimal m1 = 2.500001m;
int i1 = (int)m1; int i2 = Convert.ToInt32(m1); Assert.AreEqual(i1, i2); }
In C# integer i1 = 2
after casting from decimal and conversion for the same decimal produces i2 = 3
.
It is interesting that the same code in VB.NET with CType casting does not produce this error.
<TestMethod()> Public Sub ConvertAndCastFromDecimalToInt32ShouldBeTheSame()
Dim m1 As Decimal = 2.500001D
Dim i1 As Integer = CType(m1, Integer)
Dim i2 As Integer = Convert.ToInt32(m1)
Assert.AreEqual(i1, i2) End Sub
Only in C#, the same problem exists for double
and int
, float
and int (Int32/Int64)
.
Not a Number
For Double
and Single
type, there are several special values reserved in .NET (and not for Decimal
).
Double.NaN;
Double.NegativeInfinity;
Double.PositiveInfinity;
Single.NaN;
Single.NegativeInfinity;
Single.PositiveInfinity;
These numbers can appear in special operations:
- square root from negative number. e.g.
Math.Sqrt(-2)
- 0 / 0
- 0 * 8
- 8 / 8
Idea behind NaN
numbers is that it should not be equal to another NaN
number. Otherwise, how we should compare Math.Sqrt(-2)
and Math.Sqrt(-100)
?
So, Double.Nan != Double.Nan
, however if we compare on equality with Equals()
method, it will produce very messy result for NaN
and Infinity
.
Why Double.NaN != Double.NaN
, but Double.NaN.Equals(Double.NaN)
and Double.PositiveInfinity == Double.PositiveInfinity
?
[TestMethod]
public void CheckNaNForInequality() {
double d = Double.NaN;
Assert.IsFalse(d == Double.NaN);
Assert.IsTrue(d.Equals(Double.NaN));
double d2 = Double.PositiveInfinity;
Assert.IsTrue(d2 == Double.PositiveInfinity);
Assert.IsTrue(d2.Equals(Double.PositiveInfinity));
double d3 = Double.NegativeInfinity;
Assert.IsTrue(d3 == Double.NegativeInfinity);
Assert.IsTrue(d3.Equals(Double.NegativeInfinity));
}
<TestMethod()> Public Sub CheckNaNForInequality() Dim d As Double = Double.NaN
Assert.IsFalse(d = Double.NaN)
Assert.IsTrue(d.Equals(Double.NaN))
Dim d2 As Double = Double.PositiveInfinity
Assert.IsTrue(d2 = Double.PositiveInfinity)
Assert.IsTrue(d2.Equals(Double.PositiveInfinity))
Dim d3 As Double = Double.NegativeInfinity
Assert.IsTrue(d3 = Double.NegativeInfinity)
Assert.IsTrue(d3.Equals(Double.NegativeInfinity))
End Sub
To avoid errors, it is better to use next methods for special number checking:
Double.IsNaN(value)
Double.IsInfinity(value)
Double.IsNegativeInfinity(value)
Double.IsPositiveInfinity(value)
(similar for Single
)
There is a difference in representation between C# and VB in debug value inspection:
NaN
is presented as NaN
in C# and -1.#IND
in VB (IND for indeterminate) PositiveInfinity
is presented as +Infinity
in C# and 1.#INF
in VBNegativeInfinity
is presented as -Infinity
in C# and -1.#INF
in VB
Related links