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

fmt: floating numbers are rounded inconsistently for "%.Nf" where N > 1 #21091

Closed
iafan opened this issue Jul 19, 2017 · 8 comments
Closed

fmt: floating numbers are rounded inconsistently for "%.Nf" where N > 1 #21091

iafan opened this issue Jul 19, 2017 · 8 comments

Comments

@iafan
Copy link

iafan commented Jul 19, 2017

Please answer these questions before submitting your issue. Thanks!

What version of Go are you using (go version)?

go1.8.3 darwin/amd64 (but also reproducible in the official playground)

What did you do?

Consider the following example that fully explains the issue:

package main

import (
	"fmt"
)

func main() {
	fmt.Printf("%.1f\n", 0.05) // => 0.1
	fmt.Printf("%.1f\n", 1.05) // => 1.1 (correct)

	fmt.Printf("%.2f\n", 0.005) // => 0.01
	fmt.Printf("%.2f\n", 1.005) // => 1.00 (why not 1.01?)

	fmt.Printf("%.3f\n", 0.0005) // => 0.001
	fmt.Printf("%.3f\n", 1.0005) // => 1.000 (why not 1.001?)
}

The same code in the playground

What did you expect to see?

I would expect proper rounding of the fractional part regardless of the integer part value.

What did you see instead?

Inconsistent rounding as depicted in the sample code.

@ALTree
Copy link
Member

ALTree commented Jul 19, 2017

fmt.Printf("%.2f\n", 1.005) // => 1.00 (why not 1.01?)

because 1.00 is closer to the float64 representation of 1.005 than 1.01 is. The rounding is correct. Try to port your code to c, you'll notice it prints the same numbers as go.

Closing this, since it's not a bug.

@ALTree ALTree closed this as completed Jul 19, 2017
@iafan
Copy link
Author

iafan commented Jul 19, 2017

Thanks for the explanation. I, however, would still argue that this is a bug, even if the current behavior aligns with internal implementation quirks. The problem is that the final output is inconsistent from the end user's perspective, and this is definitely not a behavior one would deliberately want to have when formatting numbers.

https://golang.org/pkg/fmt/ doesn't provide any documentation on this behavior, either (and if this is considered a non-issue, it deserves to be at least documented).

@ianlancetaylor
Copy link
Contributor

Go uses IEEE floating point, and this is how IEEE floating point works. What you are suggesting is something more like decimal floating point.

See https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html for detail on IEEE floats.

@iafan
Copy link
Author

iafan commented Jul 19, 2017

I guess the 'decimal floating point' is what people would expect from a formatting function, no?

I can totally understand the logic from the developer's point of view ("we stick to IEEE spec with all its side effects related to rounding"), but what is good for the developer is not necessarily good for the consumer, and formatting is a user-facing aspect. I can't imagine myself saying something like "this is not a bug, this is a feature of IEEE floats; deal with it" to the user of my app. So if I want to implement the formatting the way users would expect it, I, as a developer, would like to at least be warned about these quirks in the documentation of the fmt package, and make my decision on whether to implement workarounds.

It would be good to get this issue a better exposure to see what others think about this. Maybe I'm just a special snowflake and everybody else expects the formatting to work the way it works now.

@ianlancetaylor
Copy link
Contributor

You can argue it both ways. If Go behaves differently than C, a different set of people will be surprised.

It would certainly be reasonable to solicit more opinions. On the other hand, this clearly can not change before Go 2.

@ALTree
Copy link
Member

ALTree commented Jul 20, 2017

@iafan I guess I don't actually understand what you are suggesting. You are proposing that we keep the standard IEEE754 representation, but change the way we print floats to align more with that the end user expectations. How are we supposed to do that?

Consider:

package main

import "fmt"

func main() {
	fmt.Printf("%.2f\n", 1.005)
	fmt.Printf("%.2f\n", 1.00499999999999998)
}

Right now, we print:

1.00
1.00

If we were to follow your suggestion, we would print:

1.01
1.00

This is not a reasonable expectation, because the two numbers have the exact same IEEE754 representation.

How are we supposed to distinguish between the case where the user wanted 1.005, but got a slightly smaller float64, because 1.005 is not representable, and the case where the user actually typed a number slightly smaller than 1.005? The user expectations are opposite: in the first case, she wants us to print 1.01, in the second case she'll want us to print 1.00 (because she typed a number smaller than 1.005). But the two numbers are exactly the same in memory. We can't differentiate between them. And for this reason we can't format them in different ways.

If the current behaviour is problematic for you, the best solution, as Ian suggested, is to use a decimal library not built on IEEE754, one that can handle and format decimal numbers the way humans expect them.

@iafan
Copy link
Author

iafan commented Jul 20, 2017

@ALTree yes, I was suggesting to adjust the formatting only without changing any internals. And I believe it is possible to do so. Here's my (probably naive) ParseFloat implementation that tries to fix this (with tests that show the 'mathematically correct' rounding for the edge case I was reporting initially).

Having said that, @ianlancetaylor has a good point: some existing users may rely on the current formatting behavior even if it's not mathematically correct. So probably two formatting implementations could co-exist for backward compatibility and default to the old behavior (and differences between them should be documented to avoid surprises).

@iafan
Copy link
Author

iafan commented Jul 21, 2017

@ALTree note that I'm not talking about cases with the precision overflow: 1.00499999999999998 is actually a 1.005 and there's nothing one can do about this. So your example:

fmt.Printf("%.2f\n", 1.005)
fmt.Printf("%.2f\n", 1.00499999999999998)

is essentially the same as

fmt.Printf("%.2f\n", 1.005)
fmt.Printf("%.2f\n", 1.005)

and I would expect it to produce:

1.01
1.01

@golang golang locked and limited conversation to collaborators Jul 21, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants