-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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
math/big: Rat: add FloatPrec() (int, bool) #50489
Comments
I wrote a very long and complicated answer to this, but perhaps it's better just to state that rather than change FloatString in such a peculiar and possibly incompatible way, a better answer is to add a function that tells you how many bits of precision you'd need. The problem with that is that there is no simple answer in general, as you may have a repeating decimal, but the function could tell you that: func (r *Rat) FloatSize() (digitsLeftOfDecimal, digitsRightOfDecimal int, repeats bool) That solves the problem but I am far from convinced the problem is significant enough to require such a messy fix, either mine or yours. If you want to be clever, you can compute (10 gcd denominator) and get some of the answer yourself, since infinite-length representation only happens when the factors of the denominator are not all factors of the printing base. |
That is a good point and I like the idea of
With messy fix you mean the negative precision proposal or |
Both. As I said, "mine or yours". |
I see. The problem I have is that when marshaling and unmarshaling JSON with large decimal numbers as strings it is useful to have a way to parse them and then marshal them back into the same precision as they came in. Because they are parsed from a fixed length strings I know that I can format them back to one as well (so that there will be no repeats or infinite digits). The issue is only that there is no easy way to do that back conversion. On the other hand I do not want to lose precision if it is not necessary. So having |
I made an implementation with signature |
This seems like an awkward use of big.Rat. Why use big.Rat in this case at all? Why not use json.Number as the round-trippable representation? |
Oh, sorry if that was unclear from the proposal. The roundtrip is the sanity check motivation, if I have a data structure and I read it from the JSON into numbers with |
Do we care about the repeat size or the digits on the left? The finite digits on the right is guaranteed to be 10 log D, which will fit in an int whenever D fits in memory.
that returns 0, false for rats with non-finite float representations? |
Yes, I agree that digits on the left are not important. The implementation I made here has the following signature:
So But maybe my implementation is not as elegant as it could be. But I would be OK with In practice, using the signature from my implementation, I generally do |
@mitar the problem is that there can be >2^64 repeating digits. |
This proposal has been added to the active column of the proposals project |
Sure, the caller of this function would then decide what limits to apply before passing digits further. Or are you saying that |
Yes, the problem is the result does not fit in int, or even int64.
|
I am fine with that. |
For clarity:
|
Does anyone object to FloatPrec as described in #50489 (comment)? |
Few more examples:
|
I disagree about the above examples. If the bool is false, the count is always zero. The number of digits before the repetition begins may not fit in an int64 (I'm not sure) If there is some trivial computation of the non-repeated prefix and it is guaranteed to fit in int64 and it's useful, then maybe we could think about adding it. But none of those three seem to be true. |
With that clarification, does anyone object to adding FloatPrec as in #50489 (comment)? |
I am still not sure this problem is worth solving at all due to its rarity in practice. |
The original message said this was the use case:
I am sympathetic to that and can see it arising in a variety of programs. In fact, it is a bit annoying to me in Ivy that 0.1 + 0.1 = 1/5 instead of 0.2. |
Based on the discussion above, this proposal seems like a likely accept. |
No change in consensus, so accepted. 🎉 |
Change https://go.dev/cl/521235 mentions this issue: |
Change https://go.dev/cl/539299 mentions this issue: |
To be clear about what we accepted here, I believe the boolean result should state whether the rational number can be represented exactly as a decimal number with a fractional part of the returned length, or not. If the boolean is false, a decimal representation of the number will be rounded. // FloatPrec returns the number n of non-repeating digits immediately
// following the decimal point of the decimal representation of x.
// The boolean result indicates whether a decimal representation of x
// with that many fractional digits is exact or rounded.
//
// Examples:
//
// x n exact decimal representation n fractional digits
// 0 0 true 0
// 1 0 true 1
// 1/2 1 true 0.5
// 1/3 0 false 0 (0.333... rounded)
// 1/4 2 true 0.25
// 1/6 1 false 0.2 (0.166... rounded)
func (x *Rat) FloatPrec() (n int, exact bool) This is subtly different from what has been discussed so far. Per this earlier comment:
For instance, the boolean result for 1/6 would need to be true (if it were false, the count n would have to be 0 which is incorrect for 1/6). By changing the boolean result as proposed, it always has a meaning (true means exact decimal representation is possible), instead of only being meaningful to identify integers. |
Hmm. Above I had written:
Do you have an algorithm for computing this value efficiently? And does it always fit in an int64? |
https://math.stackexchange.com/questions/726969/how-do-you-calculate-how-many-decimal-places-there-are-before-the-repeating-digi has a lot to say about this problem but I haven't figured out where the answer is. |
So the function I implemented returns both the number of digits in the non-repeated prefix and the number of repeating digits which cyclicly follow. I am not sure if that is efficient implementation for you though. |
@rsc A better link might be https://en.wikipedia.org/wiki/Repeating_decimal (section on "Reciprocals of integers not coprime to 10"). The length returned by FloatPrec is the length of the "initial transient" of the decimal representation of the fraction. This length is surprisingly simple to compute and only depends on the denominator: it is the max of the the number of factors 2 and 5 appearing in the denominator. That number cannot be longer than the denominator length in bits. The method Here's an implementation: CL 539299. Determining the number of 2 factors is very fast (counting the trailing zero bits), determining the number of 5 factors can be sped up if need be by dividing by higher powers of 5 as needed. Regarding your comment here: I think the number of digits before the repetition is the most useful number: if a fraction can be represented exactly, this is the precision needed; if the fraction cannot be represented exactly, this is the number of digits we may want to print before rounding. |
Interesting. As long as it works and isn't too slow, I'm fine with the new definition of FloatPrec. For the 5s, we know the answer is less than the number of bits B consumed by D, and we divide by 5 that many times, and each division is O(B), so the overall complexity of FloatPrec is O(B^2)? The interesting benchmark would be something like (1/5^1e6).FloatPrec(). I wondered if there is some repeated-squaring kind of thing where you divide by 5, and if that works move on to 5^2, and then 5^4, and then come back down once you run out. But that probably ends up being O(B^2) too. There's at least an easy factor of 10+ by dividing out 5^13 (<2^32) or 5^27 (<2^64) first. |
Since multiplication (squaring) is much faster then division, a simpler approach is to repeatedly square an initial factor (5^13) until it is larger than the denominator (after the factors of 2 were removed), keep all the intermediate results in a table, and then repeatedly divide using the table entries. For small numbers this doesn't cost extra because no table is built, but for large numbers it can reduce the cost a lot if there's many multiples of 5. On the other hand, if there are no multiples of 5 at all, building the table is where all the cost goes. On the other hand, a decent-size table could be pre-computed once, or perhaps even be statically allocated if so desired. Lots of options here. Using this approach, for 1/(5 ^ 1e6), using just factors of 5 takes about ~120s (3.2 GHz 6-Core Intel Core i7), using the factor 5^13 reduces this to about ~9s, and using the full table we end up at ~0.1s (!). This is an improvement of ~1200x from slowest to fastest. But for 1/(5 ^ 1e6 + 1), which doesn't have any factors of 5, the corresponding numbers are ~0.3ms, ~0.6ms, and ~0.15s. This is a slowdown of ~500x from fastest to slowest. Great for special cases, but possibly horrible in other cases. Using a repeated squaring and divide approach looks more promising. For 1/(5 ^ 1e6), using the full table takes ~3s which is 3x better than the ~9s for the easy gain (just using factor 5^13) approach. |
This feels wrong to me. 5/9 = 0.555... has zero non-repeating digits, but rounding after zero digits (whether to 1 or 0.5, whichever is the interpretation) seems misleading. Maybe I'm misunderstanding? |
You're free to round to n+1 if the number cannot be represented exactly. For 5/9, n = 0, exact = false, and if you round to n+1 digits you get 0.6 (or 0.5 dep. on rounding mode). But I think the way FloatPrec is defined, you have the information needed to make the rounding decisions (whichever way that decision goes) - or use the info for something else not related to rounding. |
Best issue thread ever. |
Change https://go.dev/cl/542115 mentions this issue: |
Awesome! Really fast implementation! |
Follow-up on CL 539299: missed to incorporate the updated comment per feedback on that CL. For #50489. Change-Id: Ib035400038b1d11532f62055b5cdb382ab75654c Reviewed-on: https://go-review.googlesource.com/c/go/+/542115 Run-TryBot: Robert Griesemer <gri@google.com> Auto-Submit: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Robert Griesemer <gri@google.com> Reviewed-by: Cherry Mui <cherryyz@google.com>
Change https://go.dev/cl/542756 mentions this issue: |
Based on observations by Cherry Mui (see comments in CL 539299). Add new benchmark FloatPrecMixed. For #50489. name old time/op new time/op delta FloatPrecExact/1-12 129ns ± 0% 105ns ±11% -18.51% (p=0.008 n=5+5) FloatPrecExact/10-12 317ns ± 2% 283ns ± 1% -10.65% (p=0.008 n=5+5) FloatPrecExact/100-12 1.80µs ±15% 1.35µs ± 0% -25.09% (p=0.008 n=5+5) FloatPrecExact/1000-12 9.48µs ±14% 8.32µs ± 1% -12.25% (p=0.008 n=5+5) FloatPrecExact/10000-12 195µs ± 1% 191µs ± 0% -1.73% (p=0.008 n=5+5) FloatPrecExact/100000-12 7.31ms ± 1% 7.24ms ± 1% -0.99% (p=0.032 n=5+5) FloatPrecExact/1000000-12 301ms ± 3% 302ms ± 2% ~ (p=0.841 n=5+5) FloatPrecMixed/1-12 141ns ± 0% 110ns ± 3% -21.88% (p=0.008 n=5+5) FloatPrecMixed/10-12 767ns ± 0% 739ns ± 5% ~ (p=0.151 n=5+5) FloatPrecMixed/100-12 4.93µs ± 2% 3.73µs ± 1% -24.33% (p=0.008 n=5+5) FloatPrecMixed/1000-12 90.9µs ±11% 70.3µs ± 2% -22.66% (p=0.008 n=5+5) FloatPrecMixed/10000-12 2.30ms ± 0% 1.92ms ± 1% -16.41% (p=0.008 n=5+5) FloatPrecMixed/100000-12 87.1ms ± 1% 68.5ms ± 1% -21.42% (p=0.008 n=5+5) FloatPrecMixed/1000000-12 4.09s ± 1% 3.58s ± 1% -12.35% (p=0.008 n=5+5) FloatPrecInexact/1-12 92.4ns ± 0% 66.1ns ± 5% -28.41% (p=0.008 n=5+5) FloatPrecInexact/10-12 118ns ± 0% 91ns ± 1% -23.14% (p=0.016 n=5+4) FloatPrecInexact/100-12 310ns ±10% 244ns ± 1% -21.32% (p=0.008 n=5+5) FloatPrecInexact/1000-12 952ns ± 1% 828ns ± 1% -12.96% (p=0.016 n=4+5) FloatPrecInexact/10000-12 6.71µs ± 1% 6.25µs ± 3% -6.83% (p=0.008 n=5+5) FloatPrecInexact/100000-12 66.1µs ± 1% 61.2µs ± 1% -7.45% (p=0.008 n=5+5) FloatPrecInexact/1000000-12 635µs ± 2% 584µs ± 1% -7.97% (p=0.008 n=5+5) Change-Id: I3aa67b49a042814a3286ee8306fbed36709cbb6e Reviewed-on: https://go-review.googlesource.com/c/go/+/542756 Reviewed-by: Cherry Mui <cherryyz@google.com> Run-TryBot: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Robert Griesemer <gri@google.com> Auto-Submit: Robert Griesemer <gri@google.com>
Change https://go.dev/cl/546357 mentions this issue: |
For #50489. Change-Id: I4544a24327196eb3ed62af64ae5ddb1f60441d12 Reviewed-on: https://go-review.googlesource.com/c/go/+/546357 Reviewed-by: Alan Donovan <adonovan@google.com> Auto-Submit: Robert Griesemer <gri@google.com> Reviewed-by: Robert Griesemer <gri@google.com> TryBot-Bypass: Robert Griesemer <gri@google.com>
For golang#50489. Change-Id: I4544a24327196eb3ed62af64ae5ddb1f60441d12 Reviewed-on: https://go-review.googlesource.com/c/go/+/546357 Reviewed-by: Alan Donovan <adonovan@google.com> Auto-Submit: Robert Griesemer <gri@google.com> Reviewed-by: Robert Griesemer <gri@google.com> TryBot-Bypass: Robert Griesemer <gri@google.com>
Update, Nov 13, 2023: Current API is at #50489 (comment). -gri
Currently if I parse string like
123.34
withSetString()
and want to print it back to full precision, one has to manually pick the right precision forFloatString()
function to get that. I propose that insteadFloatString()
should accept negativeprec
parameter. The full semantics ofprec
parameter would then be:prec
, it formats withprec
digits after the radix point, padding with 0 on the right if necessary. The last digit is rounded to nearest, with halves rounded away from zero, if rounding is necessary.prec
is 0, there are no digits after the radix point.prec
is less than 0, then:abs(prec)
number of digits is used after the radix point, rounded the last digit to nearest, with halves rounded away from zero.So
FloatString(3)
for the number above returns123.340
, butFloatString(-3)
return123.34
.big.NewRat(1, 3).FloatString(3)
returns0.333
andbig.NewRat(1, 3).FloatString(-3)
would return0.333
.The text was updated successfully, but these errors were encountered: