[Python-Dev] Adding decimal (aka FixedPoint) numbers to Python

Michael McLay mmclay@comcast.net
Sat, 14 Dec 2002 21:06:17 -0500

```sf/653938I is a patch to integrate the fixedpoint module, that was created by
Tim Peters, into Python as a first class object. That is, the patch adds this
new number type so that it has direct syntax support like float, int, long,
str., etc. within the interpreter.

I use Tim's module to implement the type. This limits the patch to a small bit
of code that adds the a syntax interface to this module. Syntax recognition
for the new number format required a change to Parser/tokenizer.c and to
Python/compile.c.  This patch allows the new decimal type (I renamed the
FixedPoint type to decimal in the fixedpoint.py file because the name is
shorter and it is sufficient to distinguish the type from a binary float.) to
be entered as a literal that is similar to a long, int, or float. The new
syntax works as follows:

>>> 12.00d
12.00d
>>> .012d
0.012d
>>> 1d
1.d
>>> str(1.003d)
'1.003'

As you can see from the example, the default precision for decimal literals
are determined by the precision entered expressed in the literal.

The patch also adds glue to Python/bltinmodule.c to create a builtin decimal()
function. This builtin decimal (Formally FixedPoint) function invokes the
constructor of the Python implementation of the decimal class defined in the
fixedpoint module.  The implementation follows the familiar pattern for
adding special builtin functions, much like the implementation of  apply and
abs. It does not follow the typical method for adding built in types. That
would require a more invasive patch. The builtin decimal function works as
follows:

>>> decimal(1.333)
1.33d
>>> decimal(1.333,4)
1.3330d
>>> decimal(400,2)
400.00d

The semantics of the precision parameter may be incorrect because it is only
addressing the precision of the fraction part of the number. Shouldn't it
reflect the number of significant digits? If that were the case then the
following would have been the result:

>>> decimal(400,2)
4.0e2d

This problem is more noticable when exponents are large relative to the number
of significant digits. For instance:

>>> 40.e3d
40000.d

The result should have been 40.e3d, not 40000. This implies more precision
than is declared by the constant.

As it currently is implemented the type pads with zeros on the integer side of
the decimal point, whic implies more accuracy in the number than is true. I
ran into this problem when I tried to implement the automatic conversion of
string number values to decimal representation for numbers in 'e' format.

>>> 3.03e5d
303000.00d

The representation is incorrect. It should have returned 3.03e5d but it padded
the result with bogus zeros.

For this first cut at a new decimal type I am primarily interested in
investigating the semantics of the new native decimal number type in Python.
This type will be useful to bankers, accountants, and newbies. The backend
implementation could be replaced with a C implementation without affecting
the Python language semantics.

The approach used in this patch makes it very easy to experiment with changes
to the semantics because all of the experimentation can be done by changing
the code in Lib/fixedpoint.py. The compiled wrapper doesn't have to be
modified and recompiled.

Unit testing so far just reuses the _test function in the fixedpoint.py
module. Since I am not sure which way to go with the semantic interpretation
of precision I decided to post the patch before making a significant change
to fixedpoint.py. Some feedback on the interpretation of precision would be
appreciated.

Documentation:

The following needs to be added to section 2.1 of the library reference
manual:

decimal(value, precision)
Convert a string or a number to fixed point decimal number. If the
argument is a string, it must contain a possibly signed decimal or floating
point number, possibly embedded in whitespace. The precision parameter will
be ignored on string values. The precision will be set based on the number of
significant digits. Otherwise, the argument may be a plain or long integer or
a floating point number, and a decimal number with the same value ( the
precision attribute will set the precision of the fraction roundoff) is
returned.

Section 3.1.1 of the tutorial will add a brief description of decimal number
usage after the description of floating point:

There is full support for floating point; operators with mixed type operands
convert the integer operand to floating point:

>>> 3 * 3.75 / 1.5
7.5
>>> 7.0 / 2
3.5

Python also supports fixed point decimal numbers. These numbers do not suffer
from the somewhat random roundoff errors that can occur with binary floating
point numbers. A decimal number is created by adding a 'd' or 'D' suffix to a
number:

>>> 3.3d + 3d
6.3d
>>> 3.3d + 3.03
6.3d
>>> 3.3d + decimal(3.03,3)
6.330d
>>> decimal(1.1, 16)
1.1000000000000001d
>>>
The builtin decimal constructor provides a means for converting float and int
types to decimal types. Since floats are approximations of decimal floating
point numbers there are often roundoff errors introduced by using floats.
(The 1 in the last place of the conversion of 1.1 with 16 digits is a binary
round off error.)  For this reason the decimal function requires the
specification of precision when converting a float to a decimal. This allows
the significant digits of the number to be specified. (Accountants and
bankers love this because it allows them to balance the books without pennies
being lost due to the use of binary numbers.)

>>> 3.3d/2
1.6d

Note that in the example above the  expression 3.3d/2 returned 1.6d. The
rounding scheme for Python decimals uses "banking" rounding rules. With
floating point numbers the result would have been as follows:

>>> 3.3/2
1.6499999999999999

So as you can see the banking rules round off the .04999999999 portion of the
number and calls it a an even 1.6.

```