-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
round function in Math library sometimes doesn't work #9082
Comments
Yep, |
Surprise, round with |
These are the wonders of the surprising world of floating-point arithmetic. Numbers as large as your 786541300837.5601 are no longer precise to the 4th decimal place. In fact 786541300837.56 has exactly the same binary representation as 786541300837.5601. If you You will also get surprising results from much smaller numbers: import math, strformat
for i in 1..9:
let j = float(i) + 0.015
echo fmt"{j:4g} {j:.16f} {round(j, 2):.2f} {j:.2f}" produces:
Most people expect that all numbers x in the first column would be rounded to x.02. But thats not the case and this is not due to an error in the rounding procedure, but a consequence of the binary floating-point implementation (IEEE-754) used by most current computers and programming languages. As an excercise you might try to explain the difference in the last two columns, or eg. why 7.015 is rounded to 7.02 although the binary representation of 7.015 is just as slightly smaller as in the case of 4.015 which is rounded to 4.01. This also shows you one reason why the PR from @krux02 isn't really a good idea. It only adds some more surprises. There is really no need to deprecate proc round*[T: float32|float64](x: T, places: int = 0): T. It works as expected and mostly like similar procedures in e.g. Python (uses round to even) and Ruby. |
@skilchen I only deprecate the version with two arguments, because that is the one that can't be implemented precisely. The reason for that is rounding is normally done only for printing values. And for printing the rounding to decimal digits can be done precisely. If people still really really want rounding for non printing, well it's not that hard to implement. |
No @krux02 the round to decimal digits can't be done precisely for printing as shown in my little example above. Only very few people expect that |
@skilchen well the important part is that |
@krux02 this is more a problem of Nim's default stringification of floats than of the 2-argument rounding procedure. And i repeat for the last time that most people are surprised when you tell them that 7.015 "correctly rounded" to 2 decimal digits gives 7.01 and not 7.02 as they learned in school. This is just an artifact of IEEE-754 and has nothing to do with correctness in general. I just found out, that Python indeed does a roundtrip import strutils, math
when defined(js):
proc parseFloat(str: string): float =
let cstr = cstring(str)
{.emit: "`result` = parseFloat(`cstr`);".}
proc parseInt(str: string): int =
let cstr = cstring(str)
{.emit: "`result` = parseInt(`cstr`);".}
proc round(f:float, ndigits: int): float =
case classify(f)
of fcZero, fcNegZero, fcInf, fcNegInf, fcNan:
return f
else:
discard
{.emit: "`result` = parseFloat(`f`.toFixed(`ndigits`));".}
if result == 0.0:
{.emit: "`result` = parseFloat(`f`.toPrecision(`ndigits`));".}
proc dtoa(f: float): string =
case classify(f)
of fcZero: return "0.0"
of fcNegZero: return "-0.0"
of fcInf: return "inf"
of fcNegInf: return "-inf"
of fcNan: return "nan"
else:
discard
var r: cstring
{.emit: """
`r` = `f`.toString();
"""
.}
result = $r
else:
import dtoa
proc fcvt(f: float, ndigits: int, decpt: ptr cint, sig: ptr cint): cstring
{.importc: "fcvt", header: "<stdlib.h>".}
proc ecvt(f: float, ndigits: int, decpt: ptr cint, sig: ptr cint): cstring
{.importc: "ecvt", header: "<stdlib.h>".}
proc round(f: float, ndigits: int): float =
case classify(f)
of fcZero, fcNegZero, fcInf, fcNegInf, fcNan:
return f
else:
var decpt: cint
var sig: cint
var s: string
s = $fcvt(f, ndigits, addr decpt, addr sig)
if s == "":
s = $ecvt(f, ndigits, addr decpt, addr sig)
if decpt <= 0:
for i in decpt .. 0:
s.insert("0", 0)
s.insert(".", 1)
else:
s.insert(".", decpt)
if sig != 0:
s.insert("-", 0)
result = parseFloat(s)
when defined(test):
import times
proc `$`(dt: DateTime): string =
dt.format("uuuu-MM-dd HH:mm:ss'.'fff")
proc `$`(dur: Duration): string =
$dur.seconds & "." & intToStr(dur.milliseconds, 3)
proc test() =
var t0 = now()
var t1 = t0
for i in 1..1_000_000_000:
if i mod 1_000_000 == 0:
let t2 = now()
echo t1, " ", align($i, 10), " ", t2 - t1, " ", t2 - t0
t1 = t2
let j = float(i) + 0.015
let s1 = dtoa(round(j, 2))
let s2 = formatFloat(j, ffDecimal, 2)
doAssert s1 == s2, $s1 & " != " & $s2
when isMainModule:
when defined(js):
proc paramStr(n: int): string =
var arg: cstring
{.emit: "`arg` = process.argv[`n` + 1];".}
return $arg
else:
import os
let number = parseFloat(paramStr(1))
let ndigits = parseInt(paramStr(2))
echo "number: ", number, " ndigits: ", ndigits
let rounded = round(number, ndigits)
echo rounded, " ", dtoa(rounded), " ", formatFloat(rounded, ffDecimal, min(32, ndigits))
when defined(test):
import times, strformat
test() One interesting anecdotal fact is that the @LemonBoy's dtoa needs some better handling of the special float values, i added this to the top of his case classify(value)
of fcZero: return "0.0"
of fcNegZero: return "-0.0"
of fcInf: return "inf"
of fcNegInf: return "-inf"
of fcNan: return "nan"
else:
discard |
It doesn't matter if people are surprised if they see |
@skilchen People will also be surprised if they see that the following code and that it outputs "false":
We can't put people on a cloud where they can spare themself to learn about how floating point numbers are stored in the computer. |
That exact equality comparisons of floats are a thing to avoid is one of the first things people learn about floating-point and it is not that surprising that you get better approximations of numbers that are not exactly representable as binary fractions if you have more bits available. My goals were:
In reality printing is affected by the exact same surprising properties of binary floating-point arithmetic as is rounding to zero or more decimal digits. Python and my Here "precisely" has to be understood in terms of IEEE-754. Its not me who wants to deprecate things just because one has to know something about binary floating-point arithmetic when using them. Just for fun, one of the most inaccurate results you can get from floating-point arithmetic: import math, strutils
var x = 0.3 mod 0.1
echo formatFloat(x, ffDefault, 1) prints a "precisely" rounded result of 0.1 which is rather far away from the true value of 0.0. I don't think that we therefore should deprecate |
If you want decimal rounding in the floating point math module, it should be called In your example the rounding works flawlessly. The modulo division works flawlessly as well. The problem is that there is no 0.3 and no 0.1 in floating point numbers in the first place, so
Nobody is surprised that And of course you can do exact equality comparison on floating point numbers, but only if you know what you are doing. Generally it is a good advice to avoid them, but if you know what you are doing, it is possible. here are some examples that do work precisely: here are some example that do not work precisely: And for the second claim: To sum it up: No I am not convinced to close anything, and I still hold to my claims. |
for example
The text was updated successfully, but these errors were encountered: