-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
Implement __format__ for Fraction #67790
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
Comments
Since Decimal supports __format__, it would be nice that Fraction did too. |
Here's a patch that adds Fraction.__format__ implementation, test cases and documentation. |
>>> from fractions import Fraction as F
>>> format(F(1, 3), '.30f')
'0.333333333333333333333333333300' |
Serhiy Storchaka wrote:
>>>> from fractions import Fraction as F
>>>> format(F(1, 3), '.30f')
> '0.333333333333333333333333333300' Good catch! I'll try to fix this and add some more test cases. |
I'm not sure it needs fixing: it follows from the definition of using Decimal(num) / Decimal(denom). Plus, it's controllable with a decimal context: >>> from decimal import localcontext
>>> with localcontext() as ctx:
... ctx.prec = 100
... format(F(1, 3), '.30f')
...
'0.333333333333333333333333333333'
>>> For all of the tests, I suggest using format(value, str) instead of ''.format(value). It more directly tests Fraction.__format__. In general I think adding Fraction.__format__ is a good idea, and I think converting to Decimal is reasonable for the specified codes. My only question is what to do when "natively" formatting Fractions themselves. We might want to support field widths, padding, etc. |
I’ve never actually used the Fraction class, but I doubt its behaviour should depend on whatever settings are in the current decimal context. Maybe you can extract the precision out of the format string, and base the internal decimal object on that. |
Eric V. Smith wrote:
Hmm... Even though it's tempting to agree with you and just ignore the
I agree. Will change those.
Thanks! Actually I already tried to support field widths, padding and |
Here's the next round of the patch. For formatting fractions with any given precision I had to parse the precision from format specifier and at this point it seemed easier to just create a general parser for the Format Specification Mini-Language. In this patch it is implemented in fractions._parse_format_specifier function, but maybe this kind of general function should be moved to better place and be documented and exported. What do you think? |
Regarding sharing fractions._parse_format_specifier(), perhaps have a look at _pydecimal._parse_format_specifier() |
Martin Panter wrote:
I did find that, but since it was a private function in private |
Version 3 of the patch. Changes to v2:
|
>>> from fractions import Fraction as F
>>> format(F(4, 27), 'f')
'0.1481481'
>>> format(F(4, 27), '.1f')
'0.2' |
[Eric]
Hmm. We've gone to some lengths to make sure that we get correctly-rounded results for formatting of Decimal and float types, as well as to make sure that operations like converting a Fraction to a float are correctly rounded. It would be disappointing if the result of formatting a Fraction wasn't correctly rounded, and I'd personally consider it a bug. |
Absolutely. Fractions are all about exact calculations, much more so than Decimals. So the formatting output should be as accurate as requested or possible (well, excluding infinity). |
actually, I'm not sure whether formatting Decimals gives correct output under all conditions (correctly rounded yes, but maybe not formatted correctly?). compare: >>> format(float('1.481e-6'),'.3g')
'1.48e-06'
>>> format(Decimal('1.481e-6'),'.3g')
'0.00000148'
>>> format(float('1.481e-7'),'.3g')
'1.48e-07'
>>> format(Decimal('1.481e-7'),'.3g')
'1.48e-7' So with the 'g' specifier the switch between floating point and scientific notation seems to be broken. Are these bugs ? |
Decimal formatting intentionally differs from float formatting, see bpo-23460. |
I see. Thanks for the pointer. What about the missing zero in the exponent ? |
The zero isn't missing. :) We are following http://speleotrove.com/decimal/decarith.html, with thousands of test cases. We could decide to do something special for "g", but there are good reasons not to do that. |
Thanks for the comments again! I fixed the "format(F(4, 27), '.1f') -> 0.2" issue (a) I don't want "f" specifier to mean "infinite" precision, but instead some predefined value. I chose 6. The rounding thing made things harder, since there was no way to pass decimal context for Decimal.__format__ without changing the local context -- at least with the C implementation; the Python implementation (pydecimal) provided nicer API with optional context keyword argument. So I decided to unify the C and Py API's of Decimal.__format_ and add the keyword argument support to the C API too. This is done in this v4 of the patch. There's no docs for the added Decimal.__format__ kwargs, since I want some comments on that change first. |
I think that Decimal is not needed for Fraction.__format__ (and I'm not sure that issue23602v4.patch is correct). The correct way to format Fraction as fixed-precision decimal is to use Fraction.__round__() or similar algorithm. The implementation can look like: f = self.__round__(prec)
i = int(f)
return '%d.%0*d' % (i, prec, abs(f - i) * 10**prec) |
On 29 March 2015 at 19:54, Serhiy Storchaka wrote:
Of course it's not needed. I'm using it to avoid re-implementing all
Why this would be more correct than delegating the rounding (and |
But these parameters could also be partly delegated to normal string (not number) formatting, right? One of the advantages of not depending on Decimal is, well, to not depend on Decimal, which is a rather uncommon dependency when using Fractions in an application. I think it could avoid some more calculations to first multiply the nominator by 10**prec, then round(), int() and str() the result, and then split the string at "-prec". BTW, if "division with remainder" wasn't (sadly) linear time, that would definitely be the most beautiful algorithm here. :) |
Meaning, something like this should work: x = (nom * 10**(prec+1)) // den
if x % 10 < 5:
x = x // 10
else:
x = x // 10 + 1
print('%s.%s' % (x[:-prec], x[-prec:])) |
Or, speaking of "division with remainder":
... minus the usual off-by-one that the tests would quickly find :) |
Initially, I also thought that this should be addressable with Fraction.__round__ or an optimized variation of it, but Tuomas is right that it gets complicated by the fact that you need to cope with the different format specifiers and not all of them fit the algorithm. >>> float(round(Fraction(4, 27000), 6))
0.000148
but
>>> format(4/27000, 'e')
'1.481481e-04' Trying to deal with this in pure Python quickly slows your code (at least a few naive attempts of mine) unacceptably compared to Tuomas' patch. |
Regarding Decimal:
|
On 30 March 2015 at 13:49, Stefan Krah wrote:
Yes, context precision isn't, but context rounding mode is. That's why But yes, I also thought that maybe the Decimal.__format__ changes
I don't understand what do you mean with this. Is this something that
What double rounding issues you're referring to? |
I understand double rounding to mean incorrectly rounding something like 0.149999 up to 0.2. It should be rounded once to 1 decimal place (0.1). If you temporarily round it to a higher number of places before rounding to 1 place, you’re doing it wrong. So you might have to ensure that any rounding done before formatting step exactly matches the rounding specified in the formatting. |
Let's discuss that in a separate issue. [DefaultContext]
Decimal.DefaultContext has global scope and currently affects |
) This PR adds support for float-style formatting for `Fraction` objects: it supports the `"e"`, `"E"`, `"f"`, `"F"`, `"g"`, `"G"` and `"%"` presentation types, and all the various bells and whistles of the formatting mini-language for those presentation types. The behaviour almost exactly matches that of `float`, but the implementation works with the exact `Fraction` value and does not do an intermediate conversion to `float`, and so avoids loss of precision or issues with numbers that are outside the dynamic range of the `float` type. Note that the `"n"` presentation type is _not_ supported. That support could be added later if people have a need for it. There's one corner-case where the behaviour differs from that of float: for the `float` type, if explicit alignment is specified with a fill character of `'0'` and alignment type `'='`, then thousands separators (if specified) are inserted into the padding string: ```python >>> format(3.14, '0=11,.2f') '0,000,003.14' ``` The exact same effect can be achieved by using the `'0'` flag: ```python >>> format(3.14, '011,.2f') '0,000,003.14' ``` For `Fraction`, only the `'0'` flag has the above behaviour with respect to thousands separators: there's no special-casing of the particular `'0='` fill-character/alignment combination. Instead, we treat the fill character `'0'` just like any other: ```python >>> format(Fraction('3.14'), '0=11,.2f') '00000003.14' >>> format(Fraction('3.14'), '011,.2f') '0,000,003.14' ``` The `Fraction` formatter is also stricter about combining these two things: it's not permitted to use both the `'0'` flag _and_ explicit alignment, on the basis that we should refuse the temptation to guess in the face of ambiguity. `float` is less picky: ```python >>> format(3.14, '0<011,.2f') '3.140000000' >>> format(Fraction('3.14'), '0<011,.2f') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/Users/mdickinson/Repositories/python/cpython/Lib/fractions.py", line 414, in __format__ raise ValueError( ValueError: Invalid format specifier '0<011,.2f' for object of type 'Fraction'; can't use explicit alignment when zero-padding ```
Done for float-style formatting in #100161. I'm planning to make a new PR that adds support for the |
See #111320. |
PR #100161 added fancy float-style formatting for the Fraction type, but left us in a state where basic formatting for fractions (alignment, fill, minimum width, thousands separators) still wasn't supported. This PR adds that support. --------- Co-authored-by: Serhiy Storchaka <[email protected]>
PR python#100161 added fancy float-style formatting for the Fraction type, but left us in a state where basic formatting for fractions (alignment, fill, minimum width, thousands separators) still wasn't supported. This PR adds that support. --------- Co-authored-by: Serhiy Storchaka <[email protected]>
@mdickinson, #111320 was merged. From the issue thread it seems one could be closed. |
@skirpichev It could indeed! Thanks. |
PR python#100161 added fancy float-style formatting for the Fraction type, but left us in a state where basic formatting for fractions (alignment, fill, minimum width, thousands separators) still wasn't supported. This PR adds that support. --------- Co-authored-by: Serhiy Storchaka <[email protected]>
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs
The text was updated successfully, but these errors were encountered: