/rounders

Primary LanguagePythonApache License 2.0Apache-2.0

Rounders

The rounders package extends the functionality provided by Python's built-in round function. It aims to provide a more complete and consistent collection of decimal rounding functionality than is provided by the Python core and standard library. Specifically, it provides:

  • Drop-in replacements for round that use rounding modes other than Python's default Banker's rounding mode. Thirteen different rounding modes are available.
  • Functionality for rounding to a given number of significant figures, rather than to a set number of places after or before the decimal point.

Package contents

General-purpose rounding functions

There are four general-purpose rounding functions. These all accept an optional mode argument for specifying the rounding mode, and default to using TIES_TO_EVEN (Banker's rounding) if no mode is specified.

  • The round function has the same signature as the built-in round. It supports rounding to the nearest integer in the direction of the given rounding mode, and rounding to a given number of places while preserving the type of the input.

    >>> from rounders import round, TIES_TO_AWAY, TO_MINUS
    >>> round(2.5)  # The default rounding mode is TIES_TO_EVEN
    2
    >>> round(2.5, mode=TIES_TO_AWAY)  # round halfway cases away from zero
    3
    >>> round(2.97, 1, mode=TO_MINUS)  # round towards negative infinity (like floor)
    2.9
    >>> round(Decimal(-1628), -2, mode=TO_MINUS)  # Decimal and Fraction types supported
    Decimal('-1.7E+3')
  • The round_to_figures function rounds to a given number of significant figures, rather than to a given number of places before or after the decimal point.

    >>> from rounders import round_to_figures, TO_AWAY
    >>> round_to_figures(1.234567, 3)
    1.23
    >>> round_to_figures(1234567., 3)
    1230000.0
    >>> round_to_figures(0.0001234567, 3)
    0.000123
    >>> round_to_figures(0.0001234567, 3, mode=TO_AWAY)  # round away from zero
    0.000124
  • The round_to_int and round_to_places functions provide separately the two pieces of functionality that round combines: round_to_int rounds to a nearby integer using the given rounding mode, while round_to_places always expects an ndigits argument and rounds to the given number of places. The round function is currently a simple wrapper around round_to_int and round_to_places.

    >>> from rounders import round_to_int, round_to_places, TO_PLUS
    >>> round_to_int(3.1415, mode=TO_PLUS)
    4
    >>> round_to_places(3.1415, 2, mode=TO_PLUS)
    3.15

Rounding-mode-specific rounding functions

There are thirteen functions that act as drop-in replacements for round, but that use a specific rounding mode. For example, if you always want to round ties away from zero instead of to the nearest even number, you can do this:

>>> from rounders import round_ties_to_away as round
>>> round(4.5)
5
>>> round(1.25, 1)
1.3

Or if you want a version of math.ceil that accepts a number of places after the point, you can do:

>>> from rounders import ceil
>>> ceil(1.78)
2
>>> ceil(1.782, 2)
1.79
>>> ceil(-1.782, 2)
-1.78

The complete list of functions is below

Rounding modes

These are the currently supported rounding modes, along with their corresponding mode-specific rounding functions.

To-nearest rounding modes

There are six to-nearest rounding modes: these all round to the closest target value (e.g., to the closest integer in the case of round_to_int), and differ only in their handling of ties.

Rounding mode Function Description
TIES_TO_EVEN round_ties_to_even Ties rounded to the nearest even value
TIES_TO_ODD round_ties_to_odd Ties rounded to the nearest odd value
TIES_TO_AWAY round_ties_to_away Ties rounded away from zero
TIES_TO_ZERO round_ties_to_zero Ties rounded towards zero
TIES_TO_MINUS round_ties_to_minus Ties rounded towards negative infinity
TIES_TO_PLUS round_ties_to_plus Ties rounded towards positive infinity

Directed rounding modes

There are six matching directed rounding modes: for these, all values between any two representable output values will be rounded in the same direction.

Rounding mode Function Description
TO_EVEN round_to_even Round to the nearest even value
TO_ODD round_to_odd Round to the nearest odd value
TO_AWAY round_to_away Round away from zero
TO_ZERO round_to_zero Round towards zero
TO_MINUS round_to_minus Round towards negative infinity
TO_PLUS round_to_plus Round towards positive infinity

Miscellaneous rounding modes

There's one miscellaneous rounding mode TO_ZERO_05_AWAY, with corresponding function round_to_zero_05_away.

Rounding mode Function Description
TO_ZERO_05_AWAY round_to_zero_05_away See below

This rounding mode matches the behaviour of TO_ZERO, except in the case where rounding towards zero would produce a final significant digit of 0 or 5. In that case, it matches the behaviour of TO_AWAY instead. Note that in the case where the value is already rounded to the required number of digits, neither TO_ZERO nor TO_AWAY would change its value, and similarly TO_ZERO_05_AWAY does not change the value in this case.

>>> from rounders import round_to_zero_05_away
>>> round_to_zero_05_away(1.234, 1)  # behaves like `TO_ZERO`
1.2
>>> round_to_zero_05_away(-1.294, 1)  # also behaves like `TO_ZERO`
-1.2
>>> round_to_zero_05_away(1.534, 1)  # `TO_ZERO` would give 1.5, so round away
1.6
>>> round_to_zero_05_away(-2.088, 1)  # `TO_ZERO` would give -2.0, so round away
-2.1
>>> round_to_zero_05_away(3.5, 1)  # `TO_ZERO` wouldn't change the value; leave as-is
3.5

Aliases

The functions trunc, floor and ceil are aliases for round_to_zero, round_to_minus and round_to_plus, respectively.

Notes on rounding modes

Some notes on particular rounding modes:

  • TIES_TO_EVEN goes by a variety of names, including "Banker's rounding", "statisticians' rounding", and "Dutch rounding". It matches Python's default rounding mode and the IEEE 754 default rounding mode, roundTiesToEven. Many other languages also use this rounding mode by default.

  • TIES_TO_AWAY appears to be the rounding mode most commonly taught in schools, and is the mode that users often mistakenly expect round to use. Python 2's round function used this rounding mode.

  • TIES_TO_PLUS matches the rounding mode used by JavaScript's Math.round, and also appears to be commonly taught. (See ECMA-262, 13th edn., §21.3.2.28.)

  • TIES_TO_ZERO is used in IEEE 754's "Augmented arithmetic operations".

  • TO_ZERO matches the behaviour of math.trunc

  • TO_PLUS matches the behaviour of math.ceil

  • TO_MINUS matches the behaviour of math.floor

  • TO_ODD is interesting as a form of "round for reround", providing a way to avoid the phenomenon of double rounding. Suppose we're given a real number x and a number of places p. Let y be the result of rounding x to p + 2 places using the TO_ODD rounding mode. Then y can act as a proxy for x when rounding to p places, in the sense that y and x will round the same way under any of the rounding modes defined in this module. (The binary analog of TO_ODD is a little more useful here - it works in the same way, but requires only two extra bits for the intermediate value instead of two extra digits.)

  • TO_ZERO_05_AWAY also provides a form of "round for reround", but is more efficient in that it only requires one extra decimal digit instead of two. Given a value x and a number of places p, if y = round(x, p + 1, mode=TO_ZERO_05_AWAY), then round(x, p, mode=mode) == round(y, p, mode=mode) for any of the thirteen rounding modes defined in this package.

    >>> from rounders import *
    >>> import random
    >>> x = random.uniform(-1.0, 1.0)
    >>> y = round(x, 5, mode=TO_ZERO_05_AWAY)
    >>> round(x, 4, mode=TO_ZERO) == round(y, 4, mode=TO_ZERO)
    True
    >>> round(x, 4, mode=TIES_TO_ODD) == round(y, 4, mode=TIES_TO_ODD)
    True
    >>> round(x, 4, mode=TO_ZERO_05_AWAY) == round(y, 4, mode=TO_ZERO_05_AWAY)
    True

On relationships between the rounding modes in this package and rounding modes elsewhere:

  • IEEE 754 defines five "rounding-direction" attributes: roundTiesToEven, roundTiesToAway, roundTowardPositive, roundTowardNegative and roundTowardZero. These match TIES_TO_EVEN, TIES_TO_AWAY, TO_PLUS, TO_MINUS and TO_ZERO, respectively. The "Augmented arithmetic operations" section of IEEE 754-2019 also defines an attribute roundTiesToZero, corresponding to TIES_TO_ZERO in this module.

    IEEE 754 rounding direction rounders rounding mode
    roundTiesToEven TIES_TO_EVEN
    roundTiesToAway TIES_TO_AWAY
    roundTiesToZero TIES_TO_ZERO
    roundTowardPositive TO_PLUS
    roundTowardNegative TO_MINUS
    roundTowardZero TO_ZERO
  • As of Python 3.11, Python's decimal module defines eight rounding options, corresponding to the rounding modes in this module as follows:

    decimal rounding option rounders rounding mode
    ROUND_CEILING TO_PLUS
    ROUND_DOWN TO_ZERO
    ROUND_FLOOR TO_MINUS
    ROUND_HALF_DOWN TIES_TO_ZERO
    ROUND_HALF_EVEN TIES_TO_EVEN
    ROUND_HALF_UP TIES_TO_AWAY
    ROUND_UP TO_AWAY
    ROUND_05UP TO_ZERO_05_AWAY

Supported numeric types

Out of the box, rounders supports Python's built-in numeric types: int, float, decimal.Decimal and fractions.Fraction. Under the hood, it uses functools.singledispatch for all type-specific operations. This should allow easy extension to new numeric types in the future. The extension mechanism has not yet stabilised.

Future directions

Major goals for future releases:

  • Add formatting support, including the ability to specify rounding direction in a format specification.
  • Finalise and document mechanisms for adding support for custom types.
  • Improve performance of round, especially for the float type, with the aid of a C extension if necessary.
  • Better document the pitfalls of round applied to binary floats (especially for directed rounding modes, where round is not idempotent).