Skip to content
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

Rounding 0.00015 to precision 4 is not giving 0.0002 #11

Open
oderwat opened this issue May 9, 2023 · 6 comments
Open

Rounding 0.00015 to precision 4 is not giving 0.0002 #11

oderwat opened this issue May 9, 2023 · 6 comments

Comments

@oderwat
Copy link

oderwat commented May 9, 2023

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

@oderwat
Copy link
Author

oderwat commented May 9, 2023

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?

@oderwat
Copy link
Author

oderwat commented May 9, 2023

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!

@oderwat oderwat closed this as completed May 9, 2023
@oderwat oderwat reopened this May 9, 2023
@oderwat
Copy link
Author

oderwat commented May 9, 2023

Actually. Maybe it is a bug because it should also round 5534023222112865/36893488147419103232 correctly?

@wadey
Copy link
Owner

wadey commented May 10, 2023

Thanks for checking out the module!

Actually. Maybe it is a bug because it should also round 5534023222112865/36893488147419103232 correctly?

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:

@oderwat
Copy link
Author

oderwat commented May 10, 2023

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.

@oderwat
Copy link
Author

oderwat commented May 10, 2023

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants