I would be happy to see this functionality available now in simplefractions
and/or any other similar PyPI library. If at a later date we somehow reached consensus to add it to stdlib too, we could do that then.
Yes, you understand my position correctly, Iâd view 0.3, 0.7, 5.3 and 1000.3 differently, because they are an exact number of tenths, and as a denominator 10 is small enough that Iâd expect a user to naturally think in terms of tenths (whereas Iâd be less convinced that someone would intuitively feel the difference between 33/100 and 34/100, so viewing both of them as 1/3 seems natural to me (ironically, 0.34 is interpreted as 17/50, so I guess thatâs another discrepancy).
Your definition is entirely reasonable on its own terms (the specification is clear and understandable). My expectations, on the other hand, have too of a âdo what I meanâ flavour to be implementable. So Iâm not trying to say your implementation is wrong, just that it violates my expectations in a number of cases which feel common enough to me that I wouldnât use it in practice. Whereas simplefractions.simplest_from_float()
gives me the answer Iâd expect in those cases (the trade-off being, it gives more complex fractions for things like 0.3333333).
In case it is not futile to look at this yet another way âŚ
I am imagining that we would be modifying the fraction.Fraction
constructor so that instead of
def __new__(cls, numerator=0, denominator=None, *,
_normalize=True):
it would be something like
def __new__(cls, numerator=0, denominator=None, *,
_normalize=True, assume_rounded=False):
The functionality I propose would come into effect only if the user constructed the fraction via something like
a = fractions.Fraction("0.3", assume_rounded=True)
In particular, if you are thinking â0.3â is exact, then maybe you wonât be setting assume_rounded=True
in the first place. And if you do happen to think it might be rounded in some peculiar case then would returning a value of 1/3 be what you would want in that case?
(Apologies if the name assume_rounded
is not according to convention or is otherwise inappropriate. In that case, letâs change it.)
One way of looking at the differences between 0.3, 0.7, 5.3 and
1000.3 is that they have 1, 1, 2 and 5 significant digits
respectively, so have different levels of precision on their own.
Lee said:
"I would expect "0.15" -> 2/13
and "0.150" -> 3/20
and "0.3" -> 1/3
and "0.7" -> 2/3
. In particular, in a situation where something is reported to the nearest tenth, my first assumption for â0.7â would be that it comes from 2/3.
These are very odd expectations. Under what circumstances are you expecting somebody who wanted 2/13 to write 0.15 instead of 2/13?
I think the problem is not in your description of what is getting computed, but why it is being computed.
To me, this requested feature seems horribly like DWIM:
http://www.catb.org/jargon/html/D/DWIM.html
It feels to me like you are coming from the pespective of somebody who knows that they want 2/13 as an exact fraction, but for some mysterious reason is forced to write it as 0.15 rather than â2/13â or 0.15384615384615385 or even 0.153846 like you might see on a cheap $2 calculator, and wants the Fraction constructor to Do What I Mean and return that 2/13 fraction.
But imagine that you were somebody who actually wanted 6/41, or 9/59. These are no weirder fractions to desire than 2/13, they also round to 0.15 if forced to use only 2 decimal places, and they are closer to 0.15 than 2/13. Your DWIM function fails to return them. What a disappointment you would feel.
There are rather a lot of possible fractions which might also have been written as 0.15, starting with the fraction 3/20 which is exactly 0.15, and it really isnât clear why 2/13 should be considered âbetterâ or âmore likelyâ, rather than (say) 19/130 (equally close to 0.15, but from the opposite direction).
I donât think that you are trying to just minimise the denominator, without caring about being the closest rational approximation. But perhaps Iâm wrong?
There are 50 rational pairs where both the numerator and denominator are no greater than 100 which are rounded to 0.15 to two decimal places. After normalising by cancelling common factors, there are 31 such fractions.
One of those is exactly 0.15 (3/20). Another 22 of them are closer to 0.15 than 2/13.
If the true value might be any fraction that rounds to 0.15, why is 2/13 a better guess than 6/41 or 9/59?
I saw a talk recently where it was reported that the approach did the right thing in 90.3%
of their test cases. I knew that they didnât have that many test cases and was quickly able to estimate that they were achieving 28/31
by using best_fraction("90.3e-2")
.
I think that this is typical. When the denominator is relatively low, there are many situations where people will instinctively provide enough digits for it to be reconstructed. I would like to provide a tool that does this computation. That people might apply the tool in situations where it is not applicable ⌠is a problem that pretty much all tools can suffer from. I suppose we could put child restraints on â e.g., so that the algorithm would refuse to run unless there were at least N significant digits in the input â but my inclination is to let users decide for themselves.
Sure. But that sounds like a case where whatâs clearly wanted is the simplest fraction in the interval
(0.9025, 0.9035)
. With the current description of best_fraction
, as a user I couldnât be sure whether it was going to give me the simplest fraction, or whether it was going to give me something else because that something else is closer to the value 0.903
than the simplest fraction.
To give one example along the same lines: suppose itâs reported that 28.3% of voters responded âyesâ in some poll, and you want to figure out the smallest possible number of people involved in that poll. The answer is 46 (13/46 = 0.282608âŚ). But best_fraction("0.283")
gives 15/53
instead, and I have no idea why 15/53
should be considered the âbestâ approximation to 0.283
.
Iâm opposed to this being in the stdlib. It should be on PyPI. If itâs clearly a success there, then we could talk about including it in Fractions.
You make a good point. The result from simplefractions
is not my first choice but it is easier to explain than my approach, and I would accept that instead of the implementation above for best_fraction
. In such a case I would like to have an assume_rounded
parameter (or by another name) in the constructor fractions.Fraction
default to False
, but when set to True
would infer the interval as is currently done by best_fraction
and then find the simplefractions
answer from that interval.
If there is support for the extra bells and whistles, we could support both the simplefractions
and best_fraction
approaches by allowing assume_rounded
to support multiple values, or by introducing additional defaulted parameters.
Whether or not this is incorporated into fractions
, I would be happy to see this kind of functionality in simplefractions
.
Why not just publish it yourself? It doesnât need to be incorporated into another package to be usefulâŚ
Agreed. I think the main issue is that, for example, with this function, 0.7 gives back 2/3. While that may be the case sometimes, I donât really see why anyone would use 0.7 to represent 2/3 unless specifically needing one decimal place. Yes the math works but then fractions that are exact like 7/10 are ignored in situations like this. I think itâs only useful if you give 3 or more decimal places. Or, if thereâs an exact match, you give that instead. Or even better, multiple options are given. Otherwise, itâs pointless and inaccurate.
I am confused by this response. If you want â0.7â to be 7/10 then you donât set assume_rounded=True
in the fractions.Fraction
constructor. You set assume_rounded=True
when you want â0.7â to be the simpler fraction 2/3.
So basically what youâre asking to be added is just an extra parameter to a command? Because if thatâs the case then that would probably be fine.
Lee said:
âIf you want â0.7â to be 7/10 then you donât set assume_rounded=True
in the fractions.Fraction
constructor. You set assume_rounded=True
when you want â0.7â to be the simpler fraction 2/3.â
What if I want 0.7 to be 5/7, or 8/11, or 9/13, or 11/16, or 12/17, or one of the many dozens of other simple fractions that round to 0.7?
More to the point, if the only information I have is that when rounded as a decimal, my fraction is 0.7, how can I know which of the many fractions that round to 0.7 is the one I want?
how can I know which of the many fractions that round to 0.7 is the one I want?
Indeed there are infinitely many. This function (as amended by the suggestion of @mdickinson) chooses the simplefractions
answer, which is the fraction with the lowest numerator and denominator. If that is not what the user wants then the user would not request the functionality. Would it help if the name of the flag that selects this behavior (and which is False
by default) were assume_rounded_from_simplest_fraction
?
I think the point that youâre missing here is that youâre proposing that one of a multitude of possible alternative behaviours is worth adding to the Fraction
constructor. But you donât provide any sort of justification for why that specific behaviour is sufficiently important to add, when the others are not.
We get that the new behaviour is optional. We get that people shouldnât enable it if itâs not what they want. What you havenât explained is why people who want a different behaviour arenât also entitled to having an option to enable their preferred behaviour.
Instead of a boolean âassume itâs roundedâ option, what about an âallowed errorâ option that, if non-zero, specifies the absolute value the returned Fraction is allowed to deviate from the exact value after simplification? If you know how much rounding youâre dealing with, you can specify a constant value; otherwise you can calculate a value based on your input in whatever way makes sense for your application.
Itâs not always the case that the user choosing which functionality to use (the software developer) is the same as the user entering the input (the end user, or a client application).
âassume_rounded_from_simplest_fractionâ
I want to say âyou cannot possibly be seriousâ but I fear that you actually are.
Not every functionality needs to be crammed into the default constructor. Boolean flag arguments are a code smell (if not outright anti-pattern):
So your proposal to add this to the constructor is already a bit wiffy. But at the point that we are proposing a parameter name which is more than double the length of the fully qualified class (fractions.Fraction
), it positively reeks.
So letâs think a bit harder about the API:
-
What is it that this thing actually does? Give a short but descriptive name for it.
-
What arguments does it accept? Just strings, or floats and Fractions, etc?)
-
Obviously it returns a Fraction. So it could be an alternative constructor, like Fraction.from_decimal and from_float, or a method like limit_denominator.
-
If this is based on the continued fraction algorithm for Best Rational Within An Interval, the obvious API is for a method that allows the user to provide the interval.
Consider the design principles expressed in the Zen of Python (import this
). While they arenât necessarily intended to be taken entirely seriously, they do offer some good guidelines to think about. In this case, I argue that the koan âExplicit is better than implicitâ applies.
If your aim is to have a function that takes a decimal written as a string, and returns the best rational approximation to that assuming that the string was rounded to N digits, then your method should take two arguments: the decimal string, and the number of digits.
Donât rely on trailing zeroes being significant:
"%.2f" % (19/27) # 0.70
"%.2g" % (19/27) # 0.7
although I guess it wouldnât be too bad if the user could explicitly opt in to âguess the number of significant figures from the stringâ, e.g. if you pass -1 as the number of digits.