I ran across this rather strange failure in one of my tests and am at a loss as to what's causing it. Can someone please enlighten me?
For the purposes of this post the filename is roundxxx.nim. I left in a bunch of extra assertions just to show it otherwise works as expected.
import math
proc round*(n: float, d: int = 0): float =
floor(n * pow(10, (d.float * -1)) + 0.5) * pow(10, d.float)
when isMainModule:
block:
doAssert round(0.1) == 0.0
doAssert round(0.1, 0) == 0.0
doAssert round(0.1, -1) == 0.1
doAssert round(1.49) == 1.0
doAssert round(1.49, 0) == 1.0
doAssert round(1.49, -1) == 1.5
doAssert round(1.49, -2) == 1.49
doAssert round(1.5) == 2.0
doAssert round(1.9) == 2.0
doAssert round(1.79, -1) == 1.8
doAssert round(1.95, -1) == 2.0
doAssert round(1.95, -2) == 1.95
doAssert round(1.955, -2) == 1.96
doAssert round(1, 0) == 1.0
doAssert round(1, 1) == 0.0
doAssert round(9, 1) == 10.0
doAssert round(149) == 149.0
doAssert round(149, 0) == 149.0
doAssert round(149, 1) == 150.0
doAssert round(149, 2) == 100.0
doAssert round(150, 2) == 200.0
doAssert round(499, 2) == 500.0
doAssert round(499, 3) == 0.0
doAssert round(15) == 15.0
doAssert round(19) == 19.0
doAssert round(19, 1) == 20.0
doAssert round(195, 1) == 200.0
doAssert round(195, 2) == 200.0
doAssert round(195.4) == 195.0
doAssert round(195.5) == 196.0
# XXX this is some strangeness going on here
doAssert round(1.89, -1) == 1.9 # XXX I've no clue why this fails
let n = round(1.89, -1) # XXX when the exact same values if
doAssert $n == "1.9" # XXX casted to strings pass just fine
The result of nim c -r roundxxx.nim is as follows
roundxxx.nim(39) roundxxx
system.nim(3790) failedAssertImpl
system.nim(3783) raiseAssert
system.nim(2830) sysFatal
Error: unhandled exception:
[redacted]/roundxxx.nim(39, 14)
`round(1.89, -1) == 1.9` [AssertionError]
Error: execution of an external program failed:
'[redacted]/roundxxx '
FYI: nim --version
Nim Compiler Version 0.19.0 [Linux: amd64]
Compiled at 2018-09-26
Copyright (c) 2006-2018 by Andreas Rumpf
git hash: f6c5c636bb1a1f4e1301ae0ba5a8afecef439132
active boot switches: -d:release
I wonder that you are surprised by your results.
I assume that you know that float arithmetic is generally not absolutely exact in computers. One reason is, that not every fractional decimal number can be exactly represented in binary floating point format. So when we have three floating point numbers a, b, b then generally (a + b) + c != a + (b + c). See
https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems
So it can not surprise that the rounding proc does not always give the exactly same result as the value which is obtained from the string representation.
I don't know what your concrete problem is in real life application. When you do rounding, and print the result with a limited number of digits I guess it should be fine, as the error is in the very tiny places which generally are not printed at all. Always converting the rounded result to string and back to float may help when the same proc is used as Nim compiler use internally for processing float literals, but that is slow. Maybe you can tell us where exactly you got problems in your real life application.
I don't think the problem is with Nim. It's highly likely with floating point. Unless you want to play with arbitrary precision FP you'll have to live with the fact that while FP can represent mildly large integers it doesn't offer precision with fractions.
Btw, that you use only up to 2 decimal places in your code suggests that your project might be about finance/trading or similar, i.e. about currency values. Should that be the case I strongly advise you to use integer arithmetic and to work with cents (or, should it be required as in some financial fields, with tenths of cents).
Unless you expect exact result because all you are doing is copying, as soon as computation is involved you need to check either the relative error or the absolute error or both.
For instance, extracted from my code:
proc relative_error*[T: SomeFloat](y, y_true: T): T {.inline.} =
## Relative error, |y_true - y|/max(|y_true|, |y|)
## Normally the relative error is defined as |y_true - y| / |y_true|,
## but here max is used to make it symmetric and to prevent dividing by zero,
## guaranteed to return zero in the case when both values are zero.
let denom = max(abs(y_true), abs(y))
if denom == 0.T:
return 0.T
result = abs(y_true - y) / denom
proc absolute_error*[T: SomeFloat](y, y_true: T): T {.inline.} =
## Absolute error for a single value, |y_true - y|
result = abs(y_true - y)
In general you should prefer the relative_error especially with numbers not in the [-1, 1] range, but when the target is near 0, the relative change will get quite high due to the division and here you need to use the absolute error instead.