planetp
planetp

Reputation: 16115

How to determine if a decimal fraction can be represented exactly as Python float?

From the Python tutorial:

Unfortunately, most decimal fractions cannot be represented exactly as binary fractions. A consequence is that, in general, the decimal floating-point numbers you enter are only approximated by the binary floating-point numbers actually stored in the machine.

I wonder how can I check if a given decimal fraction will be represented exactly as a Python float. For instance, 0.25 can be represented exactly while 0.1 can't:

>>> 0.1 + 0.1 + 0.1 == 0.3
False
>>> 0.25 + 0.25 + 0.25 == 0.75
True

Upvotes: 3

Views: 1381

Answers (3)

Martijn Pieters
Martijn Pieters

Reputation: 1123490

You could use the fractions module to check if a given fraction can be represented:

from fractions import Fraction

def can_be_represented(num, den):
    f = Fraction(num, den)
    return Fraction.from_float(float(f)) == f

Because floating point numbers use binary fractions, you'll soon find that this can be simplified to checking for a denominator that is a power of two:

def can_be_represented(num, den):
    f = Fraction(num, den)
    return f.denominator & (f.denominator - 1) == 0

However, this doesn't make any bounds checks on the numerator, add a bounds check by comparing with the information from sys.float_info:

import sys

def can_be_represented(num, den):
    f = Fraction(num, den)
    return (
        # denominator is a power of 2
        f.denominator & (f.denominator - 1) == 0 and
        # numerator exponent can be represented
        f.numerator.bit_length() <= sys.float_info.max_exp and
        # numerator significant bits can be represented without loss
        len(format(f.numerator, 'b').rstrip('0')) <= sys.float_info.mant_dig
    )

The above version tests:

  • That the denominator is a power of 2
  • That the numerator binary exponent can be represented
  • That the portion of the numerator that contains significant information can be shifted to fit in the mantissa of a float.

An optimised but less readable version of the above is:

def can_be_represented(num, den,
                      _mexp=sys.float_info.max_exp,
                      _mdig=sys.float_info.mant_dig):
    f = Fraction(num, den)
    num, den = f.numerator, f.denominator
    numbl = num.bit_length()
    return (
        # denominator is a power of 2
        den & (den - 1) == 0 and
        # numerator exponent can be represented
        numbl <= _mexp and
        # numerator significant bits can be represented without loss
        (numbl <= _mdig or num << numbl - _mdig >> numbl - _mdig == num)
    )

Upvotes: 4

aka.nice
aka.nice

Reputation: 9412

In Squeak Smalltalk, you would find this method:

Fraction>>isAnExactFloat
    "Answer true if this Fraction can be converted exactly to a Float"
    ^ denominator isPowerOfTwo
        and: ["I have a reasonable significand: not too big"
            numerator highBitOfMagnitude <= Float precision
                and: ["I have a reasonable exponent: not too small"
                    Float emin + denominator highBitOfMagnitude <= Float precision]]

and

Integer>>isAnExactFloat
    "Answer true if this Integer can be converted exactly to a Float"
    | h |
    (h := self highBitOfMagnitude) <= Float precision
        ifTrue: [^ true].
    ^ h - 1 <= Float emax
        and: [h - self abs lowBit < Float precision]

Of course it's not Python, but it's the same underlying floating point, so that shouldn't be that hard to translate...

Upvotes: 1

Eugene Yarmash
Eugene Yarmash

Reputation: 149991

On that very page:

On most machines today, floats are approximated using a binary fraction with the numerator using the first 53 bits starting with the most significant bit and with the denominator as a power of two.

Thus for a decimal fraction to be exactly representable as float it must be a fraction with a denominator which is a power of 2:

>>> 1/10 + 1/10 + 1/10 == 3/10  # 0.1 + 0.1 + 0.1
False
>>> 1/4 + 1/4 + 1/4 == 3/4      # 0.25 + 0.25 + 0.25
True

Upvotes: 3

Related Questions