Search code examples
pythonbinarynumbersradix

How can I convert a recurring binary number into decimal and vice-versa without approximating it?


I want to convert a number like 10.1(010) (which I'm storing as a triple ('10','1','010')) into decimal, getting 2.6(428571) (or ('2','6','42857')).

I want to get the exact number, repeating decimals and all, not a float that approximates it.

I can easily convert it to a float, by simply reading each item as an integer and multiplying it by the appropriate place value:

bin_to_float(('10','1','010')) = 2 + 1 * 2-1 + 2 * 2-1/(23-1) = 2.642857142857143

Which I wrote in python

def read_numeral(self, numeral, base=2) -> float:
    if ',' not in numeral: numeral = numeral.replace('r',',r')

    integer, finite, period = numeral
    offset = len(finite)

    n = read_int(integer, base)
    if finite: n += read_int(finite, base) * (base ** -offset)
    if period: n += read_int(period, base) * base ** -offset / (base**len(period)-1)

    return n

I couldn't figure out a way to get the exact decimal representation, though.


Solution

  • Please clarify what you mean by "exact decimal representation". I'm sure you're aware of that many possible inputs cannot be represented by a finite decimal string. Python, alas, does not support infinite strings ;-)

    If there's some limit on how many decimal digits you want to get, the decimal module can be used easily, and configured to use as many digits as you have RAM to store them.

    >>> import decimal
    >>> from decimal import Decimal as D
    >>> decimal.getcontext().prec = 200  # use 200 digits
    >>> D2 = D(2)
    >>> result = D2 + 1 * D2**-1 + D2 * D2**-1 / (D2**3 - 1)
    >>> result
    Decimal('2.6428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571')
    

    BTW, while Python doesn't support infinite strings, unbounded sequences are a different story. You could, e.g., write a generator to produce decimal digits one at a time.

    EDIT

    Maybe you'll find these building blocks useful. It's best to keep "the math part" separate from "the pretty part". That is, code will quickly become an incomprehensible mess if you try to do everything in one function. So these just do "the math part", and make no assumptions about how, e.g., you want to input or display numbers in base 493921. To this code, they're just lists of integers with elements in range(493921).

    # Convert int n >= 0 to base. Return list of base `base` digits.
    def tobase(n, base):
        bs = []
        while True:
            n, digit = divmod(n, base)
            bs.append(digit)
            if not n:
                break
        bs.reverse()
        return bs
    
    # Convert rational n/d to base as a list of digits in the base.
    # The list contains '.' after the integer part, and if it's a
    # repeating fraction the list contains '('. and ')' at the end,
    # to delimit the repeating digits.
    def rep(n, d, base):
        xs = []
        n, rem = divmod(n, d)
        xs = tobase(n, base)
        xs.append('.')
        rem2pos = {}
        while rem:
            if rem in rem2pos:
                i = rem2pos[rem]
                xs.insert(i, '(')
                xs.append(')')
                break
            rem2pos[rem] = len(xs)
            digit, rem = divmod(rem * base, d)
            xs.append(digit)
        return xs
    

    And then, e.g.,

    >>> rep(100, 4, 10) # plain integer
    [2, 5, '.']
    >>> rep(100, 3, 10) # simple repeat, 33 1/3
    [3, 3, '.', '(', 3, ')']
    >>> rep(2, 7, 10)  # and pure fraction
    [0, '.', '(', 2, 8,  5, 7, 1, 4, ')']
    >>> rep(1, 37, 10) @ subtler repeat
    [0, '.', '(', 0, 2, 7, ')']
    >>> 1/37  # yup! looks about right ;-)
    0.02702702702702703
    >>> rep(1, 64, 10) # terminating fraction
    [0, '.', 0, 1, 5, 6, 2, 5]
    >>> rep(21, 10, 2)  # and use base 2 for a change
    [1, 0, '.', 0, '(', 0, 0, 1, 1, ')']
    >>> rep(37, 14, 10) # your example
    [2, '.', 6, '(', 4, 2, 8, 5, 7, 1, ')']
    

    That last may, or may not, match what you may mean by "exact decimal representation".