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

Utf8JsonWriter.WriteNumberValue(double / float) doesn't allow to control decimal precision #435

Closed
alexch-m opened this issue Dec 2, 2019 · 8 comments

Comments

@alexch-m
Copy link

alexch-m commented Dec 2, 2019

I'd like to be able to write to JSON double and single numbers with specified precision, e.g. 554.1667 instead of 554.166666666667. Could not achieve this with help of Math.Round(x, <precision>): the results (the strings written to JSON) are unexpected sometimes, e.g. 554.16669999999999 instead of 554.1667 or 789.14290000000005 instead of 789.1429.

Maybe add optional "precision" parameter to Utf8JsonWriter.WriteNumberValue(double) and Utf8JsonWriter.WriteNumberValue(float) ?

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Text.Json untriaged New issue has not been triaged by the area owner labels Dec 2, 2019
@Gnbrkm41
Copy link
Contributor

Gnbrkm41 commented Dec 2, 2019

It isn't possible to represent certain numbers precisely with float and doubles, and 554.1667 and 789.1249 are one of them. Instead, it will store the closest representable
number (or at least it's supposed to; the rounding logic isn't exactly precise now and it does have some quirks). The same happens when you round the number; the rounded number may not be exactly representable and the closest representation will be stored instead. This is the limitation of the data type itself.

I believe the serialiser default to generate shortest round-trippable (i.e. when you ToString then Parse the number again you get the exact same number, without a slightest difference) string, which is why those longer strings are printed when you think shorter strings could be printed. This is useful because you usually want the serialised output to round-trip as well when deserialised.

Is there a reason you want to write truncated string instead? The only reason I can think of is human readability... but JSON isn't exactly built with readability being the prime objective.

Perhaps you might be able to work around it by obtaining a UTF-8 formatted number of your desired precision with System.Buffers.Text.Utf8Formatter.TryFormat with StandardFormat of F4 then writing the said string with Utf8JsonWriter.WriteStringValue? Actually that will write quoted numbers, so probably not...

@alexch-m
Copy link
Author

alexch-m commented Dec 2, 2019

Thank you for the answer. Both sample numbers from my question are round-trippable, i.e. the expression (double.Parse("554.1667", CultureInfo.InvariantCulture) == 554.1667) is true.

I checked the sources, currently the implementation uses "G17" format specifier. I understand the idea to minimize precision losses after serializing/deserializing floating point numbers but I'd prefer to have control on this. The reasons are simple: get more compact JSON taking into account the data (in my case) are coming from sensors which actual precision is probably 1-2 digits after decimal point. JSON is used as an interchange format when sending data to other system.

@Gnbrkm41
Copy link
Contributor

Gnbrkm41 commented Dec 2, 2019

currently the implementation uses "G17" format specifier

Ah, right. it's not generating the shortest roundtrippable string, just a round-trippable string 🙂

@svick
Copy link
Contributor

svick commented Dec 2, 2019

What framework are you using? When I call writer.WriteNumberValue(554.1667) on .Net Core 3.0, I get the expect result: 554.1667. But when I do the same thing on .Net Framework, I get 554.16669999999999 instead.

Since floating point handling on .Net Framework is problematic and will almost certainly stay that way forever and it works well on recent .Net Core, I'm not sure it makes sense to do anything here. (In other words, I don't think it would be worth it to change the API just to make .Net Framework work better.)

@tannergooding
Copy link
Member

.NET Framework and .NET Core have different behaviors here.

In .NET Core, the floating-point parsing/formatting APIs were updated to be IEEE compliant and to produce the shortest roundtrippable string by default: https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/.

In .NET Framework, the APIs need to remain backwards compatible and so they do not produce roundtrippable strings by default and may not parse to the nearest representable result in all scenarios. The closest we can get is to always use "G17" and so that is what is done.

Not using "G17" would mean that two different numbers would frequently be serialized to the same string and result in a loss of precision. For example, 554.1667 is represented internally by the bits: 0x4081515566CF41F2. .NET Framework, by default, formats everything from 0x4081515566CF41EE to 0x4081515566CF41F6 (inclusive) as "554.1667". So that is 8 values that would be serialized to the same value by default, which is generally undesirable, especially for a serialization format.

@alexch-m
Copy link
Author

alexch-m commented Dec 2, 2019

2 svick: yes, I tried on .Net Framework and .NET Core 2.1, not on 3.0

What framework are you using? When I call writer.WriteNumberValue(554.1667) on .Net Core 3.0, I get the expect result: 554.1667. But when I do the same thing on .Net Framework, I get 554.16669999999999 instead.

2 tannergooding: thank you for explanations.

Regarding my original wish, it's not something urgent of cause but at the same time will not cause any harm I think.

@svick
Copy link
Contributor

svick commented Dec 2, 2019

@alexch-m

Regarding my original wish, it's not something urgent of cause but at the same time will not cause any harm I think.

Adding new API to .Net has a very high cost, so the bar for that is much higher than just "will not cause any harm".

@ahsonkhan
Copy link
Member

ahsonkhan commented Feb 20, 2020

I don't think this is a common enough scenario to warrant built-in API capabilities. What would you do in the case of Newtonsoft.Json if you wanted to do this?

One workaround here could be to provide a WriteRaw API on the Utf8JsonWriter and let the caller write whatever they like, unvalidated. Then, you could format your float/double to a string of any precision and then exclude the quotes when writing it.

As it stands, this issue is not actionable.

@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Feb 20, 2020
@layomia layomia added this to the 5.0 milestone Feb 20, 2020
@layomia layomia closed this as completed Feb 20, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
radical pushed a commit to radical/runtime that referenced this issue Jul 7, 2022
- Adds support for the `apple test -t=maccatalyst` command (resolves dotnet#435)
- Getting system logs for the process is not working at the moment (dotnet#459)
- The `apple run` command cannot detect exit code yet (dotnet#462)
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

7 participants