Rounding 0.00015 to precision 4 is not giving 0.0002
I am actually looking into this for some hours, I need "Bankers Rounding" on place 4 and originally started with a float variant:
func BankerRound4(num float64) float64 {
// fails for 0.00015 because 0.00015*10000 becomes 1.4999999999999998
return math.RoundToEven(num*10000) / 10000
}
I didn't notice that problem before I tried to implement a big.Rat version myself. In the end, I came up with:
func RatBankerRound4(num *big.Rat) *big.Rat {
floatStr5 := num.FloatString(5)
var result = &big.Rat{}
fifth := floatStr5[len(floatStr5)-1:]
addOne := false
result, _ = result.SetString(floatStr5[:len(floatStr5)-1])
if fifth == "5" {
fourth := floatStr5[len(floatStr5)-2 : len(floatStr5)-1]
switch fourth {
case "1", "3", "5", "7", "9":
addOne = true
}
} else if fifth > "5" {
addOne = true
}
if addOne {
if num.Sign() < 0 {
result = result.Sub(result, big.NewRat(1, 10000))
} else {
result = result.Add(result, big.NewRat(1, 10000))
}
}
return result
}
This does solve all my tests, which I verified with multiple online calculators (of which one had the problem I described above):
// See: https://www.calculatestuff.com/math/rounding-numbers-calculator
{"Test 0.00014", args{0.00014}, 0.0001},
// half to even. 1 to even = 2
{"Test 0.00015", args{0.00015}, 0.0002},
{"Test 0.00016", args{0.00016}, 0.0002},
{"Test 0.00024", args{0.00024}, 0.0002},
// half to even. 2 to even = 2
{"Test 0.00025", args{0.00025}, 0.0002},
{"Test 0.00026", args{0.00026}, 0.0003},
{"Test 0.00034", args{0.00034}, 0.0003},
// half to even. 3 to even = 4
{"Test 0.00035", args{0.00035}, 0.0004},
{"Test 0.00036", args{0.00036}, 0.0004},
{"Test 0.00044", args{0.00044}, 0.0004},
// half to even. 4 to even = 4
{"Test 0.00045", args{0.00045}, 0.0004},
{"Test 0.00046", args{0.00046}, 0.0005},
// here the negatives
{"Test -0.00014", args{-0.00014}, -0.0001},
// half to even. -1 to even = -2
{"Test -0.00015", args{-0.00015}, -0.0002},
{"Test -0.00016", args{-0.00016}, -0.0002},
{"Test -0.00024", args{-0.00024}, -0.0002},
// half to even. -2 to even = -2
{"Test -0.00025", args{-0.00025}, -0.0002},
{"Test -0.00026", args{-0.00026}, -0.0003},
{"Test -0.00034", args{-0.00034}, -0.0003},
// half to even. -3 to even = -4
{"Test -0.00035", args{-0.00035}, -0.0004},
{"Test -0.00036", args{-0.00036}, -0.0004},
{"Test -0.00044", args{-0.00044}, -0.0004},
// half to even. -4 to even = -4
{"Test -0.00045", args{-0.00045}, -0.0004},
{"Test -0.00046", args{-0.00046}, -0.0005},
While implementing stuff, I found that I needed to truncate big.Rat values to 4 decimals. This is when I found your package.
While looking through it, I saw your rounder and tried rounding.Round(num, 4, rounding.HalfEven), which I thought, it would be maybe more elegant than my quick straightforward implementation, that uses strings to detect the different cases. But the results are similar to the Float64 version I posted above. In fact, nearly every implementation I find does that "wrong". This includes a big Go database. There is a lot of code that uses multiply/divide using math.Pow() or some variant. It seems that all of them suffer from the floating-point problematic, that I think causes the problem.
See also: https://go.dev/play/p/bduKZF3AfQC
This may help to resolve the problem: https://go.dev/play/p/HP82b8ZX_Zp
trunc() is defective:
trunc(new(big.Rat).SetFloat64(0.00015), 5) returns "14/100000" which is not right, is it?
OK. Forget it :) ... I just found that the real problem here is how I initialize the variable!
fmt.Println(trunc(big.NewRat(15, 100000), 5)) does return "15/100000".
I guess my Round routine actually works "correct" in an unexpected way because it introduces a similar rounding as all the other float64 "outputs" do.
I hope I did not waste your time. Thanks for this great package!
Actually. Maybe it is a bug because it should also round 5534023222112865/36893488147419103232 correctly?
Thanks for checking out the module!
Actually. Maybe it is a bug because it should also round
5534023222112865/36893488147419103232correctly?
How are you testing that? It seems to be correct to me:
x, _ := new(big.Int).SetString("5534023222112865", 10)
y, _ := new(big.Int).SetString("36893488147419103232", 10)
fmt.Println(rounding.Round(new(big.Rat).SetFrac(x, y), 3, rounding.HalfEven).FloatString(3))
fmt.Println(rounding.Round(new(big.Rat).SetFrac(x, y), 4, rounding.HalfEven).FloatString(4))
fmt.Println(rounding.Round(new(big.Rat).SetFrac(x, y), 5, rounding.HalfEven).FloatString(5))
fmt.Println(rounding.Round(new(big.Rat).SetFrac(x, y), 32, rounding.HalfEven).FloatString(32))
Gives:
0.000
0.0001
0.00015
0.00014999999999999998685946966948
Which looks correct based on:
- https://www.wolframalpha.com/input?i=5534023222112865%2F36893488147419103232
- https://www.wolframalpha.com/input?i=round+5534023222112865%2F36893488147419103232+to+4+decimal+places
- https://www.wolframalpha.com/input?i=round+5534023222112865%2F36893488147419103232+to+32+decimal+places
Yes. Your package works correct but that is the problem with float64 origins. Because we all expect it to be not correct. So Rounding new(big.Int).SetString("0.00015") probably should be aware of the 0.000149999999^ number and when I say trunc it to 4 it may (optionally?) roung in the 5 or "last" place first.
What I mean is that if you have a periodic '9'er non-rational input number, you may want to round it before truncating:
0,00015*10000=1,4999999999999999 = trunc(1) = 1,4 ... but is that really what we want?