-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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