The issue observed in this question is caused largely by Microsoft’s choice of formatting, notably that Microsoft software fails to show the exact values because it limits the number of digits used to convert to decimal even when the format string requests more digits. Furthermore, it uses fewer digits when converting float than when converting double. Thus, if a float and double with the same value are formatted, the results may be different because the float formatting will use fewer significant digits.
Below, I go through the code statements in the question one by one. In summary, the crux of the matter is that the value 61.0099983215332 is formatted as “61.0100000000000” when it is a float and “61.0099983215332” when it is a double. This is purely Microsoft’s choice of formatting and is not caused by the nature of floating-point arithmetic.
The statement double temp3 = 61.01 initializes temp3 to exactly 61.00999999999999801048033987171947956085205078125. This change from 61.01 is necessary due to the nature of a binary floating-point format—it cannot represent exactly 61.01, so the nearest value representable in double is used.
The statement dynamic temp = 61.01f initializes temp to exactly 61.009998321533203125. As with double, the nearest representable value has been used, but, since float has less precision, the nearest value is not as close as in the double case.
The statement double temp2 = (double)Convert.ChangeType(temp, typeof(double)); converts temp to a double that has the same value as temp, so it has the value 61.009998321533203125.
The statement double newValue = temp2 - temp3; correctly subtracts the two values, producing the exact result 0.00000167846679488548033987171947956085205078125, with no error.
The statement Console.WriteLine(String.Format("  {0:F20}", temp)); formats the float named temp. Formatting a float involves callling Single.ToString. Microsoft‘s documentation is a bit vague. It says that, by default, only seven (decimal) digits of precision are returned. It says to use G or R formats to get up to nine, and F20 uses neither G nor R. So I believe only seven digits are used. When 61.009998321533203125 is rounded to seven significant decimal digits, the result is “61.01000”. The ToString method then pads this to twenty digits after the decimal point, producing “61.01000000000000000000”.
I will address your third WriteLine statement next and come back to the second one afterward.
The statement Console.WriteLine(String.Format("  {0:F20}", temp3)); formats the double named temp3. Since temp3 is a double, Double.ToString is called. This method uses 15 digits of precision (unless G orR are used). When 61.00999999999999801048033987171947956085205078125 is rounded to 15 significant decimal digits, the result is “61.0100000000000”. The ToString method then pads this to twenty digits after the decimal point, producing “61.01000000000000000000”.
The statement Console.WriteLine(String.Format("  {0:F20}", temp2)); formats the double named temp2. temp2 is a double that contains the value from the float temp, so it contains 61.009998321533203125. When this is converted to 15 significant decimal digits, the result is “61.0099983215332”. The ToString method then pads this to twenty digits after the decimal point, producing “61.00999832153320000000”.
Finally, the statement Console.WriteLine(String.Format("  {0:F20}", newValue)); formats newValue. Formatting .00000167846679488548033987171947956085205078125 to 15 significant digits produces “0.00000167846679488548”.