Patrick Rutkowski rutski89 at gmail.com
Sat Jul 2 15:25:42 CEST 2005

```On 7/2/05, Brian van den Broek <bvande at po-box.mcgill.ca> wrote:
> Patrick Rutkowski said unto the world upon 02/07/2005 00:12:
> > That's... annoying, to say the least. And my number 4/5 was a rational
> > number too; I can understand how when doing 1/3 things can get funky.
> > Really though... 4/5 = 0.8 right on the nose, whats up?
> >
> > I read that http://www.python.org/doc/faq/general.html#why-are-floating-point-calculations-so-inaccurate
> > I'd like to find out more, is there anyway for this issue to be
> > circumvented? That "is within a reasonable limit check" seams like way
> > too much just to get a proper result from something simple like 4/5.
> >
> >
>
> Hi Patrick,
>
> my quick piece of code became something I thought might someday be
> useful. So, I expanded it with tests, and sanity checks, etc. It
> doesnt' yet have the full functionality I'd like (see docstrings), but
> I won't get to fixing it anytime real soon.
>
> So, in case you are interested, it is attached. It allows you to
> compute the value of a rational[*] as expressed in any base, 1 < base
> < 37, to arbitrary precision.
>
> [*] Any n/m, -3 < n/m < 3. Subject to some restrictions, a greater
> range. (These are the limits on functionality mentioned above.)
>
> It is in need of some re-organization, and I'm no skilled programmer,
> so certainly don't take it as an ideal model :-)
>
> It also got me to look into decimal for the first time :-)
>
> Anyway, attached.
>
> Best,
>
> Brian vdB
>
>
>
>
> #! /usr/bin/env python
> #
> # rationalconverter.py
> # Copyright 2005 Brian van den Broek
> # vanden at gmail.com
> #
> # Version 0.1
> # 02-Jul-2005  05:29:20
> #
> # Released under the GPL
> # (If you would prefer some other licence, feel free to write and ask: we
> # will most probably be able to work something out -- I picked the GPL
> # because safe and well-known, not out of a deep ideological commitment.)
>
> ##############################################################################
> #
> #     rationalconverter.py -- functions to convert rationals to other bases
> #     Copyright (C) 2005 Brian van den Broek
> #
> #     This program is free software; you can redistribute it and/or modify
> #     the Free Software Foundation; either version 2 of the License, or
> #     (at your option) any later version.
> #
> #     This program is distributed in the hope that it will be useful,
> #     but WITHOUT ANY WARRANTY; without even the implied warranty of
> #     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> #     GNU General Public License for more details.
> #
> #     You can view the GNU General Public License at:
> #
> #     Alternatively, you can obtain a copy by writing to:
> #     The Free Software Foundation, Inc.
> #     59 Temple Place - Suite 330
> #     Boston, MA 02111-1307
> #     USA
> #
> ##############################################################################
>
>
> import math, string
> from decimal import Decimal
>
> # values is the set of 36 'digits' for use in representing the converted value.
> values = string.digits + string.uppercase
>
> def float_to_base_n(numerator, denominator, base, precision):
>     '''_> string expressing target in base to desired precision
>
>     numerator and denominator must be integral values, denominator must also be
>     non-zero.
>     base must be an integral value greater than 1 and less than 37.
>     precision must be an integeral value greater than 0.
>
>     The function returns a string representing numerator/denominator in base
>     to the desired level of precision.
>
>     NB Temporary limitations:
>         Presently, it can handle neither numerator/denominator values greater
>         than 10, nor combinations of numerator, denominator, and base values
>         such that numerator/denominator > base. (The original code was written
>         just to deal with decimal fractions in [0, 1] and thus cannot cope with
>         these cases.) Future plan. Patches welcomed.
>
>     >>> float_to_base_n(4, 5, 3, 20)
>     '0.210121012101210121012'
>     '''
>
>     error, msg = _sanity_check_for_float_to_base_n(numerator, denominator,
>                                                    base, precision)
>     if error:
>         raise error, msg
>
>     base = Decimal(base)
>     precision = Decimal(precision)
>     target = Decimal(numerator) / Decimal(denominator)
>
>     start = target.to_integral()
>
>     # Must be a better way, but this ensures that start is the integral portion
>     # of numerator/denominator. FIXME
>     if target > 0:
>         if start > target:
>             start = start - 1
>         residue = target - start
>     else:
>         if start < target:
>             start = start + 1
>         residue = target + start
>
>     start = int(start)
>     expression = [str(start), '.']
>
>     place = Decimal(0)
>
>     while place < precision + 1:
>         place += 1
>         for value in range(0, base+1):
>             value = Decimal(value)
>             #print place, value, value / base ** place, residue, expression
>             if (value / base ** place) > residue:
>                 #print value / base ** place, 9999999999999, residue
>                 #print type(value / base ** place), type(residue)
>                 #print 6666666666666666666
>                 # then we've gone too far. This is why base+1 in range. That
>                 # way, we will also have gone too far by the last value
>                 break
>         # relying on value leaking out of the loop,
>         # and decrimenting as it went too far.
>         #print value, residue, type(value)
>         expression.append(values[int(value - 1)])
>         #print expression
>         #raw_input()
>         residue = residue - ((value - 1) / (base ** place))
>
>     return ''.join([str(i) for i in expression])
>
> def _sanity_check_for_float_to_base_n(numerator, denominator, base, precision):
>     '''Helper function to perform sanity check for float_to_base_n()
>
>     It is here merely because with this in the body of the main function, the
>     code became too long. It simply performs the various type and value tests
>     to ensure that numerator, denominator, base, and precision are all sane in
>     float_to_base_n(target, base, precision).
>
>     If all values are sane, it returns None, None
>     If an exception should be raised, it returns the exception class and an
>     informative error message.
>     '''
>
>     # null starting msg to support if tests below, giving default msg values
>     msg = None
>
>     try:
>         int(numerator)
>
>         if math.floor(numerator) != numerator:
>             msg = "numerator must be exact integer"
>             raise ValueError
>
>     except ValueError:
>         if not msg:
>             msg = "Invalid numerator value for float_to_base_n: %s" %numerator
>         return ValueError, msg
>
>     try:
>         target = numerator * 1.0 / denominator
>
>     except ZeroDivisionError:
>         msg = "Denominator must be a non-zero integral value"
>         raise ValueError, msg
>
>     try:
>         int(denominator)
>
>         if math.floor(denominator) != denominator:
>             msg = "denominator must be exact integer"
>             raise ValueError
>
>     except ValueError:
>         if not msg:
>             msg = "Invalid denominator value for float_to_base_n: %s" %(
>                                                                 denominator)
>         return ValueError, msg
>
>     try:
>         int(base)
>
>         if math.floor(base) != base:
>             msg = "base must be exact integer"
>             raise ValueError
>
>         if not 37 > base > 1:
>             msg = "base must be greater than 1 and less than 37"
>             raise ValueError
>
>     except ValueError:
>         if not msg:
>             msg = "Invalid base value for float_to_base_n: %s" %base
>         return ValueError, msg
>
>
>     try:
>         int(precision)
>
>         if math.floor(precision) != precision:
>             msg = "precision must be exact integer"
>             raise ValueError
>
>         if not precision > 0:
>             msg = "precision must be greater than 0"
>             raise ValueError
>
>     except ValueError:
>         if not msg:
>             msg = "Invalid precision value for float_to_base_n: %s" %precision
>         raise ValueError, msg
>
>     if target > base:
>         msg = "Cannot yet handle numerator/denominator values greater than base"
>         return NotImplementedError, msg
>     if target > 10:
>         msg = "Cannot yet handle numerator/denominator values greater than 10"
>         return NotImplementedError, msg
>
>     return None, None
>
>
> def _test():
>     '''Function to run doctest on the module'''
>     global __test__
>     __test__ = {
>     "Test of float_to_base_n for values between 0 and 1": '''
>
>     >>> float_to_base_n(4, 5, 3, 20)
>     '0.210121012101210121012'
>     >>> float_to_base_n(4, 5, 3, 10)
>     '0.21012101210'
>     >>> float_to_base_n(4, 5, 10, 10)
>     '0.80000000000'
>     >>> float_to_base_n(1, 8, 2, 10)
>     '0.00100000000'
>     >>> float_to_base_n(1, 3, 3, 10)
>     '0.10000000000'
>     ''',
>
>     "tests for rejecting invalid input": '''
>
>     >>> float_to_base_n('one point three', 4, 3, 20)
>     Traceback (most recent call last):
>         ...
>     ValueError: Invalid numerator value for float_to_base_n: one point three
>     >>> float_to_base_n(4, 5, 1, 4)
>     Traceback (most recent call last):
>         ...
>     ValueError: base must be greater than 1 and less than 37
>     >>> float_to_base_n(3, 4, 1.2, 4)
>     Traceback (most recent call last):
>         ...
>     ValueError: base must be exact integer
>     >>> float_to_base_n(4, 6, 'AA', 4)
>     Traceback (most recent call last):
>         ...
>     ValueError: Invalid base value for float_to_base_n: AA
>     >>> float_to_base_n(4, 9, 12, 1.7)
>     Traceback (most recent call last):
>         ...
>     ValueError: precision must be exact integer
>     >>> float_to_base_n(4, 9, 12, -7)
>     Traceback (most recent call last):
>         ...
>     ValueError: precision must be greater than 0
>     >>> float_to_base_n(9, 13, 14, 'gg')
>     Traceback (most recent call last):
>         ...
>     ValueError: Invalid precision value for float_to_base_n: gg
>     >>> float_to_base_n(120, 10, 16, 8)
>     Traceback (most recent call last):
>         ...
>     NotImplementedError: Cannot yet handle numerator/denominator values greater than 10
>     >>> float_to_base_n(90, 3, 4, 8)
>     Traceback (most recent call last):
>         ...
>     NotImplementedError: Cannot yet handle numerator/denominator values greater than base
>     >>>
>     '''}
>     import doctest, sys
>     test = doctest.testmod(sys.modules[__name__], report = True,
>                            optionflags = doctest.ELLIPSIS)
>
>     if test[0] == 0:
>         print "Congratulations!\n%s tests ran, %s failed" %(test[1], test[0])
>
>
> if __name__ == '__main__':
>     _test()
>     pass
>
>
>

I just got up and read your mail. However, I'm going away for the day
so I didn't have time to look over your code. Thanks for the advice
and explanations, they really helped. I understand the problem now...
/me waves fist a base 2 fractions. If your code can fix it, then
that's pretty great! What was that "import decimal" idea you proposed?
Does it contain some functions that can sanely work with floating
points? I'm not a new programmer, but my main arena is the simple side
of Java, so I'm not really too far along yet.

Hoping Python will be fun,
Patrick Rutkowski

P.S. I responded to the list this time.

```