Skip to content

Rounding 0.00015 to precision 4 is not giving 0.0002 #11

@oderwat

Description

@oderwat

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions