Skip to content

make it simpler to round fractions (support different rounding modes for division) #76159

Open

Description

@wm75
mannequin
BPO 31978
Nosy @rhettinger, @facundobatista, @mdickinson, @abalkin, @skrah, @serhiy-storchaka, @wm75
Files
  • fractions_divround.patch
  • 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:

    assignee = None
    closed_at = None
    created_at = <Date 2017-11-08.10:13:02.851>
    labels = ['3.7', 'type-feature', 'library']
    title = 'make it simpler to round fractions'
    updated_at = <Date 2017-11-09.08:45:06.327>
    user = 'https://github.com/wm75'

    bugs.python.org fields:

    activity = <Date 2017-11-09.08:45:06.327>
    actor = 'wolma'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2017-11-08.10:13:02.851>
    creator = 'wolma'
    dependencies = []
    files = ['47254']
    hgrepos = []
    issue_num = 31978
    keywords = ['patch']
    message_count = 11.0
    messages = ['305817', '305818', '305843', '305844', '305846', '305851', '305879', '305941', '305942', '305943', '305944']
    nosy_count = 8.0
    nosy_names = ['rhettinger', 'facundobatista', 'mark.dickinson', 'belopolsky', 'stutzbach', 'skrah', 'serhiy.storchaka', 'wolma']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = None
    status = 'open'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue31978'
    versions = ['Python 3.7']

    Activity

    1. wm75 commented on Nov 8, 2017

      wm75mannequin
      MannequinAuthor

      Hi,

      because of floating point inaccuracies it is suboptimal to use round(int1/int2) for rounding of a fraction.
      fractions.Fraction, OTOH, offers exact rounding through its implementation of __round__, but using it requires users to create a fractions.Fraction instance from two ints first.
      The algorithm used by Fraction.__round__ is, essentially, the same as the one used in the pure-Python version of the datetime._divide_and_round module (which, in turn, is taken from a comment by Mark Dickinson in Objects/longobject.c).

      My suggestion is to promote this algorithm to an exposed function in the fractions module (actually, it may make sense to have it in the math module instead - compare the case of the gcd function, which started out in fractions, but is now in transition to math) so that it can be used by Fraction.__round__, but also any other code.

      Attached is a patch demonstrating the idea. In addition, to the above benefit, it turns out to actually speed up Fraction.__round__ substantially, when ndigits is not None because it then avoids the generation of temporary Fraction instances, and, in my hands at least, it even makes the general (ndigits=None) case slightly faster (apparently the copied datetime._divide_and_round code is more efficient than the original in Fraction.__round__).

      There is one slight additional tweak in the patch: in the case of ndigits < 0, it returns an int, not a Fraction (see test_fractions modification to make it pass).
      I think this is actually a mistake in the current Fraction.__round__, which already promotes the result to int in the general case. This change speeds up round to the next ndigits power of ten by ~ a factor of 5 in my hands because no new Fraction needs to be instantiated anymore.

      A full PR could include having pure-Python datetime import the function from fractions instead of rolling its own, but I'd first like to hear whether you think this should go into math instead.

    2. added
      stdlibStandard Library Python modules in the Lib/ directory
      type-featureA feature request or enhancement
      on Nov 8, 2017
    3. serhiy-storchaka commented on Nov 8, 2017

      @serhiy-storchaka
      Member

      See also _divide_and_round() in Lib/datetime.py, _div_nearest() in Lib/_pydecimal.py and _PyLong_DivmodNear() in Objects/longobject.c.

    4. mdickinson commented on Nov 8, 2017

      @mdickinson
      Member

      There is one slight additional tweak in the patch: in the case of ndigits < 0, it returns an int, not a Fraction (see test_fractions modification to make it pass).

      This would be a backwards incompatible change, potentially breaking existing code, so we'd need to think carefully before making it. It's definitely not a "slight tweak" :-).

      I'd prefer that this change is not made at all: in general, it's not a great idea to have a return type that changes depending on the values of an argument (though as always, there are exceptions). The general rule is that two-argument round returns something of the same type as its first argument: I don't think there's a really compelling reason to change this for the Fraction type. So -1 on this change from me.

    5. mdickinson commented on Nov 8, 2017

      @mdickinson
      Member

      On the main proposal: rounding an integer division to the nearest integer does seem to be a somewhat common need, and one that's not entirely trivial to code up and get right first time. (Unlike floor or ceiling of a quotient, which can be simply spelled as n // d or -(-n // d) respectively.) As Serhiy points out, it already turns up in multiple guises in the C source. The questions for me would be:

      1. Is it actually a common enough need that we should add it?
      2. If answer to (1) is yes, where should we add it? A method on the int type is one possible option, beyond the ones already mentioned.
      3. There are actually three related operations here: (a) round a quotient to the nearest integer; (b) get the remainder of that rounding (the integer version of math.remainder), and (c) both (a) and (b) together. Which of those three should be implemented? (i.e., do we want the round-to-nearest analogs of div, mod, divmod, all three, or some nontrivial subset)?
    6. wm75 commented on Nov 8, 2017

      wm75mannequin
      MannequinAuthor

      ok, I agree with you that the returned type should not change with the value of an argument. I simply didn't think one vs two argument versions here, but in terms of three different code branches where one returns int already.
      Maybe 'slight' was the wrong wording - think of it as 'easy to revert' then :)

    7. serhiy-storchaka commented on Nov 8, 2017

      @serhiy-storchaka
      Member

      I thought about adding a public function in the math module when worked on one of that functions. But there are not many cases for it in the stdlib (datetime, fractions, decimal, and maybe it's all). I don't know whether there is a common enough need in third-party code. But there is a parallel with math.remainder().

      If add this function, I would implement it in C and add in the math module. It already contains integer specific functions factorial() and gcd(). There was a proposition to add as_integer_ratio() (which will work with arbitrary rationals). All together they will create a small integer mathematics domain in the math module. Or can be extracted in a special module. Later it can be extended by adding functions for binomial coefficients and the number of permutations and generators of prime and Fibonacci numbers.

    8. mdickinson commented on Nov 8, 2017

      @mdickinson
      Member

      I don't know whether there is a common enough need in third-party code.

      Me neither. But one thing to look for would be people doing round(a / b), which is almost certainly giving the wrong result in corner cases. OTOH, even those uses may well be in code that doesn't actually care about getting the wrong result in those corner cases, or that doesn't exercise the corner cases. (E.g., if both a and b are not-too-large integers, round(a / b) is still "safe" in that it will give the same result as if a non-lossy integer division is used.)

    9. wm75 commented on Nov 9, 2017

      wm75mannequin
      MannequinAuthor

      (E.g., if both a and b are not-too-large integers, round(a / b) is still "safe" in that it will give the same result as if a non-lossy integer division is used.)

      Well, it does not take particularly large a and b to break round's tie-breaking through rounding-to-even though:

      >>> for x in range(1,501):
              for y in range(1,501):
                  if round(x/y, 1) != float(round(F(x,y), 1)):
                      print(x,y)

      1 20
      2 40
      3 20
      3 60
      4 80
      5 100
      6 40
      6 120
      7 20
      7 140
      8 160
      9 20
      9 60
      9 180
      10 200
      11 220
      12 80
      12 240
      13 20
      13 260
      14 40
      14 280
      15 100
      15 300
      16 320
      17 340
      18 40
      18 120
      18 360
      19 20
      19 380
      20 400
      21 20
      21 60
      21 140
      21 420
      22 440
      23 20
      23 460
      24 160
      24 480
      25 500

      ...

    10. wm75 commented on Nov 9, 2017

      wm75mannequin
      MannequinAuthor
      >>> for x in range(1,501):
              for y in range(1,501):
                  if round(x/y, 1) != float(round(F(x,y), 1)):
                      print(x,y)

      where F is fractions.Fraction
      Sorry!

    11. serhiy-storchaka commented on Nov 9, 2017

      @serhiy-storchaka
      Member

      Mark said about round(x/y), not round(x/y, 1). Do you propose to add a three argument divide_and_round(x, y, ndigits)?

    12. wm75 commented on Nov 9, 2017

      wm75mannequin
      MannequinAuthor

      That, of course, wasn't my original suggestion, but since Mark started discussing other possible forms this could take, like round-to-nearest analogs of mod and divmod, I thought maybe it's worth pointing out this aspect and, yes, maybe the three-argument form would be nice. Essentially, this would be fractions.Fraction.__round__ then.

    13. transferred this issue fromon Apr 10, 2022
    14. self-assigned this
      on May 8, 2026
    15. changed the title [-]make it simpler to round fractions[/-] [+]make it simpler to round fractions (support different rounding modes for division)[/+] on May 31, 2026
    16. skirpichev commented on May 31, 2026

      @skirpichev
      Member

      With #90797, ceil division was requested.

    17. skirpichev commented on May 31, 2026

      @skirpichev
      Member

      I opened d.p.o thread to discuss this (and related) issue: https://discuss.python.org/t/107587

    18. removed their assignment
      on May 31, 2026
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      3.7 (EOL)end of lifestdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions