Decimal

PyCon 2005

Facundo Batista

facundo@taniquetil.com.ar

www.taniquetil.com.ar

Graphic art: Diana Batista

Index

Decimal structure

Decimal methods

Using Context

Context methods

Signals

Rounding

Usage cases

Decimal Structure

Finite numbers

Special values

Finite Numbers

Decimal can be

Finite numbers

Special values

Finite numbers are defined by three parameters:

Sign

Coefficient

Exponent

The numerical value of a finite number is given by:

(-1)**sign * coefficient * 10**exponent

Special Values

Special values:

Infinity: a very large number, positive or negative.

Quiet NaN: undefined results (Not a Number), that will flow through other computations always resulting in another NaN.

Signaling NaN: also Not a Number, signals Invalid operation rather than remaining quiet after every operation.

Decimal Methods

as_tuple

quantize

to_eng_strings

to_integral

adjusted

same_quantum

normalize

compare

max and min

sqrt

Decimal Methods: as_tuple

as_tuple(): Returns a tuple representation of the number, showing its internals:

(sign, digittuple, exponent)

>>> Decimal("321e+5").as_tuple()

(0, (3, 2, 1), 5)

>>> Decimal("-.336").as_tuple()

(1, (3, 3, 6), -3)

>>> Decimal("NaN").as_tuple()

(0, (), 'n')

>>> Decimal("-Inf").as_tuple()

(1, (0,), 'F')

Decimal Methods: quantize

quantize(exp [, rounding[, context[, watchexp]]]): Quantize makes the exponent the same as exp.

>>> d = Decimal(".01")

>>> Decimal("7.328").quantize(d)

Decimal("7.33")

>>> Decimal("7.328").quantize(d,

... rounding=ROUND_DOWN)

Decimal("7.32")

Decimal Methods: to_eng_string

to_eng_string([context]): Convert to an engineering-type (exponent multiple of 3) string, only if an exponent is needed.

>>> Decimal("1230").to_eng_string()

'1230'

>>> Decimal("123e1").to_eng_string()

'1.23E+3'

Decimal Methods: to_integral

to_integral([rounding[, context]]): Rounds to the nearest integer without signaling Inexact or Rounded.

>>> Decimal("2.33").to_integral()

Decimal("2")

>>> getcontext() # check the flags

Context(..., flags=[], ...)

>>> Decimal("2.33").quantize(Decimal(0))

Decimal("2")

>>> getcontext()

Context(..., flags=[Rounded,

Inexact], ...)

Decimal Methods: adjusted

adjusted(): Return the position of the most significant digit with respect to the decimal point.

>>> Decimal("321").adjusted()

2

>>> Decimal("-3.45").adjusted()

0

>>> Decimal("0.00533").adjusted()

-3

Decimal Methods: same_quantum

same_quantum(other[, context]): Test whether self and other have the same exponent.

>>> d = Decimal(".0001")

>>> Decimal("3.62736").same_quantum(d)

False

>>> Decimal("3.6273").same_quantum(d)

True

>>> Decimal("20").same_quantum(

... Decimal("2e1"))

False

Decimal Methods: normalize

normalize([context]): Normalize the number by stripping the rightmost trailing zeroes.

>>> Decimal("32.100").normalize()

Decimal("32.1")

>>> Decimal("0.321000e+2").normalize()

Decimal("32.1")

Decimal Methods: compare

compare(other[, context]): Compares like __cmp__() but returns a decimal instance.

>>> Decimal(".333").compare(Decimal(".444"))

Decimal("-1")

>>> Decimal(".444").compare(Decimal(".333"))

Decimal("1")

>>> Decimal(".333").compare(Decimal(".333"))

Decimal("0")

...and supports non-numbers...

>>> Decimal("NaN").compare(Decimal(".333"))

Decimal("NaN")

Decimal Methods: max and min

max(other[, context]): Like built-in max().

min(other[, context]): Like built-in min().

Return NaN if both objects are NaNs and apply the context rounding rule before returning.

>>> Decimal(8).max(Decimal(3))

Decimal("8")

>>> Decimal("NaN").min(Decimal("NaN"))

Decimal("NaN")

>>> Decimal(8).min(Decimal("Nan"))

Decimal("8")

>>> Decimal(8).max(Decimal("Nan"))

Decimal("8")

Decimal Methods: sqrt

sqrt([context]): Return the square root to full precision.

>>> Decimal(144).sqrt()

Decimal("12")

>>> Decimal("3.6273").sqrt()

Decimal("1.904547190279096234922408326")

>>> math.sqrt(Decimal("3.6273"))

1.9045471902790962

>>> d = Decimal("1.21")

>>> d == (d.sqrt() * d.sqrt())

True

>>> d == (math.sqrt(d) * math.sqrt(d))

False

Using Context

Structure of a Context

Getting the current context

Setting a new context

Standard contexts

DefaultContext

Structure of a Context

>>> getcontext()

Context(

prec=28,

rounding=ROUND_HALF_EVEN,

Emin=-999999999,

Emax=999999999,

capitals=1,

flags=[],

traps=[DivisionByZero,

InvalidOperation,

Overflow]

)

Getting the Current Context

Each thread has its own current context which is accessed using...

getcontext(): Return the current context for the active thread.

>>> c = getcontext()

>>> c

Context(prec=28, rounding=ROUND_HALF_EVEN, ...)

>>> c.prec = 6

>>> c.rounding = ROUND_DOWN

>>> c

Context(prec=6, rounding=ROUND_DOWN, ...)

Setting a New Context

For more advanced work, it may be useful to create alternate contexts using the Context() constructor, and set it using...

setcontext(c): Set the current context for the active thread to c.

>>> othercontext = Context(prec=5)

>>> setcontext(othercontext)

>>> getcontext()

Context(prec=5, ...)

>>> Decimal(1) / Decimal(7)

Decimal("0.14286")

Standard Contexts

Decimal provides two ready to use standard contexts:

>>> ExtendedContext

Context(prec=9, rounding=ROUND_HALF_EVEN,

Emin=-999999999, Emax=999999999, capitals=1, flags=[], traps=[])

>>> BasicContext

Context(prec=9, rounding=ROUND_HALF_UP, Emin=-999999999, Emax=999999999, capitals=1, flags=[], traps=[Underflow, Clamped, InvalidOperation, Overflow, DivisionBy Zero])

DefaultContext

Used by Context() as a prototype for new contexts.

Useful in multi-threaded environments.

Not necessary in single threaded environments.

>>> DefaultContext

Context(prec=28, rounding=ROUND_HALF_EVEN,

Emin=-999999999, Emax=999999999, capitals=1, flags=[], traps=[Overflow, InvalidOperation, DivisionByZero])

Context Methods

Standard arithmetic

Specific I

Specific II

Context Methods: Standard Arithmetic

>>> d = Decimal("1234.567890")

>>> getcontext().prec = 5

>>> othercontext = Context(prec=9)

>>> d + 3

Decimal("1237.6")

>>> othercontext.add(d, 3)

Decimal("1237.56789")

>>> d.sqrt()

Decimal("35.136")

>>> othercontext.sqrt(d)

Decimal("35.1364183")

Context Methods: Specific I

create_decimal(num): Creates a new Decimal instance from num but using self as context. Unlike the Decimal constructor, the context precision, rounding method, flags, and traps are applied to the conversion.

>>> c = Context(prec=5)

>>> setcontext(c)

>>> Decimal(1234567890)

Decimal("1234567890")

>>> c.create_decimal(1234567890)

Decimal("1.2346E+9")

Context Methods: Specific II

clear_flags(): Sets all of the flags to 0.

copy(): Returns a duplicate of the context.

Etiny(): Returns the minimum exponent value for subnormal results. When underflow occurs, the exponent is set to Etiny.

Etop(): Returns the maximum exponent value.

Signals

What are Signals?

Dealing with Flags

Dealing with Traps

Clamped and DivisionByZero

Inexact and Rounded

InvalidOperation and Overflow

Subnormal and Underflow

What are Signals?

Signals are groups of exceptional conditions arising during the course of computation.

May be ignored

Considered as informational

Treated as exceptions

For each signal there is a flag and a trap enabler.

When the signal is raised:

The flag is incremented.

If the trap is enabled, an exception is raised.

Dealing with Flags

>>> setcontext(ExtendedContext)

>>> getcontext()

Context(..., flags=[], ...)

>>> Decimal(355) / Decimal(113)

Decimal("3.141592920353982300884955752")

>>> getcontext()

Context(..., flags=[Inexact, Rounded], ...)

Flags are sticky: the user needs to reset them before monitoring a calculation, using the clear_flags() method.

>>> getcontext().clear_flags()

>>> getcontext()

Context(..., flags=[], ...)

Dealing with Traps

To change the behaviour of raising or not an exception for each signal, modify the individual traps using the context's trap dictionary:

>>> setcontext(ExtendedContext)

>>> Decimal(2) / Decimal(3)

Decimal("0.666666667")

>>> getcontext().traps[Inexact] = True

>>> Decimal(2) / Decimal(3)

Traceback (most recent call last):

...

decimal.Inexact: Changed in rounding

Signals: Clamped and DivisionByZero

Clamped: An exponent was altered to fit representation constraints (i.e. when an exponent falls outside Emin and Emax limits).

>>> Decimal(2) / Decimal("Inf")

Decimal("0E-1000000007")

DivisionByZero: Signals the division of a non-zero number by zero.

>>> Decimal(2) / Decimal(0)

Decimal("Infinity")

Signals: Inexact and Rounded

Inexact: Indicates that rounding occurred and the result is not exact (when non-zero digits were discarded during rounding).

>>> Decimal("2.03").quantize(Decimal(".1"))

Decimal("2.0")

Rounded: Rounding occurred though possibly no information was lost. Signaled whenever rounding discards digits; even if those digits are zero.

>>> Decimal("2.50").quantize(Decimal(".1"))

Decimal("2.5")

Signals: InvalidOperation and Overflow

InvalidOperation: An operation was requested that does not make sense.

>>> Decimal("Inf") - Decimal("Inf")

Decimal("NaN")

Overflow: Numerical overflow. Indicates the exponent is larger than Emax after rounding.

>>> Decimal("1e100000") ** 200000

Decimal("Infinity")

Signals: Subnormal and Underflow

Subnormal: Exponent was lower than Emin prior to rounding.

>>> Decimal("1e-999999999") / 100

Decimal("1E-1000000001")

Underflow: Numerical underflow with result rounded to zero (when a subnormal result is pushed to zero by rounding).

>>> Decimal("1e-100000") ** 200000

Decimal("0E-1000000007")

Rounding

When rounding happens

Up & down

Ceiling & floor

Half up, down & even

When Rounding Happens

When asked for it (quantize(), to_integral()):

>>> d = Decimal("5.063")

>>> d.quantize(Decimal(".01"))

Decimal("5.06")

>>> getcontext()

Context(..., flags=[Rounded, Inexact], ...)

When the precision is not enough:

>>> getcontext().prec = 5

>>> +Decimal(123456789)

Decimal("1.2346E+8")

>>> getcontext()

Context(..., flags=[Rounded, Inexact], ...)

Rounding: up & down

>>> getcontext().prec = 2

>>> getcontext().rounding = ROUND_UP

>>> +Decimal("1.11")

Decimal("1.2")

>>> +Decimal("-1.11")

Decimal("-1.2")

>>> getcontext().rounding = ROUND_DOWN

>>> +Decimal("1.18")

Decimal("1.1")

>>> +Decimal("-1.18")

Decimal("-1.1")

Rounding: ceiling & floor

>>> getcontext().rounding = ROUND_CEILING

>>> +Decimal("1.11")

Decimal("1.2")

>>> +Decimal("-1.11")

Decimal("-1.1")

>>> getcontext().rounding = ROUND_FLOOR

>>> +Decimal("1.18")

Decimal("1.1")

>>> +Decimal("-1.18")

Decimal("-1.2")

Rounding: half up, down & even

>>> getcontext().rounding = ROUND_HALF_UP

>>> +Decimal("1.15")

Decimal("1.2")

>>> getcontext().rounding = ROUND_HALF_DOWN

>>> +Decimal("1.15")

Decimal("1.1")

>>> getcontext().rounding = ROUND_HALF_EVEN

>>> getcontext().prec = 5

>>> +Decimal("1.23465")

Decimal("1.2346")

>>> +Decimal("1.23475")

Decimal("1.2348")

Usage Cases

Do use Decimal: Financial

Do use Decimal: Easier life

Don't use Decimal: Speed / Accuracy

Don't use Decimal: Directly as Money

Do Use Decimal: Financial

Currency calculations are often defined in terms of a given precision. All the digits must be present, even right zeroes (e.g.: 1 Euro = 3.930 Pesos)

When dealing with money you'll always want to have exact quantities. For example if you have to add a 5% to a 0.70 bill:

>>> Decimal("1.05")*Decimal("0.70")

Decimal("0.7350")

>>> 1.05*.7

0.73499999999999999

Do Use Decimal: Easier life

>>> f = .1

>>> f+f+f+f+f+f+f+f

0.79999999999999993

>>> f*8

0.80000000000000004

>>> f+f+f+f+f+f+f+f == f*8

False

>>> d = Decimal(".1")

>>> d+d+d+d+d+d+d+d

Decimal("0.8")

>>> d*8

Decimal("0.8")

>>> d+d+d+d+d+d+d+d == d*8

True

Don't Use Decimal: Speed / Accuracy

Sometimes, speed is more important than accuracy.

>>> dpi

Decimal("3.141592653589793238462643383")

>>> fpi

3.1415926535897931

>>> radius = 6378 # earth radius in km

>>> f_perim = 2 * mpi * radius; f_perim

40074.155889191403

>>> d_perim = 2 * dpi * radius; d_perim

Decimal("40074.15588919140254982947899")

>>> diff = d_perim - Decimal(repr(f_perim))

>>> diff * 10**12

Decimal("-0.45017052101000000000000")

Don't Use Decimal: Directly as Money

Decimal can't be used as Money without further effort.

>>> loan = Decimal("10.85")

>>> interest = loan * Decimal(".3") / 100

>>> interest

Decimal("0.03255")

>>> interest.quantize(Decimal(".01"))

Decimal("0.03")

>>> interest.quantize(Decimal(".01")) * 10

Decimal("0.30")

>>> interest * 10

Decimal("0.32550")

>>> (interest * 10).quantize(Decimal(".01"))

Decimal("0.33")

Questions?

Suggestions?

Thank you!

Facundo Batista

facundo@taniquetil.com.ar

www.taniquetil.com.ar

Licencia: Creative Commons
Atribución-NoComercial-CompartirDerivadasIgual 2.5 Argentina
http://creativecommons.org/licenses/by-nc-sa/2.5/deed.es_AR

Decimal Structure: Finite numbers, Special values

Decimal Methods: as_tuple, quantize, to_eng_strings, to_integral, adjusted, same_quantum, normalize, compare, max and min, sqrt

Using Context: Structure of a Context, Getting the current context, Setting a new context, Standard contexts, DefaultContext

Context Methods: Standard arithmetic, Specific I, Specific II

Signals: What are Signals?, Dealing with Flags, Dealing with Traps, Clamped and DivisionByZero, Inexact and Rounded, InvalidOperation and Overflow, Subnormal and Underflow

Rounding: When rounding happens, Up & down, Ceiling & floor, Half up, down & even

Usage Cases: Do use Decimal: Financial, Do use Decimal: Easier life, Don't use Decimal: Speed / Accuracy, Don't use Decimal: Directly as Money

Decimal can be finite numbers or special values. Finite numbers are defined by three parameters:

- ``Sign``: 0 (positive) or 1 (negative).

- ``Coefficient``: a non-negative integer.

- ``Exponent``: a signed integer, the power of ten of the coefficient multiplier.

The numerical value of a finite number is given by:

(-1)**sign * coefficient * 10**exponent

The infinities are signed and can be used in arithmetic operations where they get treated as very large, indeterminate numbers (for instance, adding a constant to infinity gives another infinite result). The sign in a NaN has no meaning.

Some operations are indeterminate and return NaN. This variety of NaN is quiet and, once created, will flow through other computations always resulting in another NaN.

A variant is sNaN which signals rather than remaining quiet after every operation. This is a useful return value when an invalid result needs to interrupt a calculation for special handling.

In addition to the standard numeric properties, decimal floating point objects also have a number of specialized methods:

>>> Decimal("1230").as_tuple()

(0, (1, 2, 3, 0), 0)

>>> Decimal("123e1").as_tuple()

(0, (1, 2, 3), 1)

Searches for a rounding method in ``rounding``, then in ``context``, and then in the current context.

If ``watchexp`` is True (default), then an error is returned whenever the resulting exponent is greater than Emax or less than Etiny.

This method is useful for monetary applications that often round results to a fixed number of places.

Engineering notation has an exponent which is a multiple of 3, so there are up to 3 digits left of the decimal place.

If given, applies rounding; otherwise, uses the rounding method in either the supplied context or the current context.

Return the adjusted exponent after shifting out the coefficient's rightmost digits until only the lead digit remains.

Decimal("20").same_quantum(Decimal("2e1")) is False, even being Decimal("20") equal to Decimal("2e1"), because...

>>> Decimal("2e1").as_tuple()

(0, (2,), 1)

>>> Decimal("20").as_tuple()

(0, (2, 0), 0)

Used for producing canonical values for members of an equivalence class.

Built-in cmp() returns -1, 0 or 1.

Don't use built-in max() nor min(), they don't support NaNs:

>>> max(Decimal("NaN"), Decimal(8))

Decimal("8")

>>> max(Decimal(8), Decimal("NaN"))

Decimal("NaN")

Note: Between a number and NaN, the number is the minimum *and* the maximum.

Needed if want to extend Decimal to trigonometry.

Default values are copied from the DefaultContext.

- The prec field is a positive integer.

- The rounding option.

- The traps and flags fields list any signals to be set. Generally, new contexts should only set traps and leave the flags clear.

- The Emin and Emax fields are integers specifying the outer limits allowable for exponents.

- The capitals field is either 0 or 1 (the default). If set to 1, exponents are printed with a capital E; otherwise, a lowercase e is used: ``Decimal('6.02e+23')``.

Each thread has its own current context which is accessed or changed using the getcontext() and setcontext() functions:

- ``getcontext()``: Return the current context for the active thread. Accesses a different Context object for each thread. Having separate thread contexts means that threads may make changes without interfering with other threads.

- ``setcontext(c)``: Set the current context for the active thread to ``c``. Automatically assigns its target to the current thread. If setcontext() has not been called before getcontext(), the latter will automatically create a new context for use in the current thread.

>>> setcontext(ExtendedContext)

>>> Decimal(1) / Decimal(7)

Decimal("0.142857143")

>>> Decimal(42) / Decimal(0)

Decimal("Infinity")

>>> setcontext(BasicContext)

>>> Decimal(42) / Decimal(0)

Traceback (most recent call last):

...

DivisionByZero: x / 0

Changing one of the fields before threads are started has the effect of setting system-wide defaults (changing the fields after threads have started is not recommended as it would require thread synchronization to prevent race conditions).

Most programs adjust the current context only once, at the beginning of the program.

It's up to the application to work with one or several contexts, but definitely the idea is not to get a context per Decimal number.

The Context class defines several general purpose methods as well as a large number of methods for doing arithmetic directly in a given context.

These methods are similar to those for the Decimal class.

Useful because constants are often given to a greater precision than is needed by the application. Another benefit is that rounding immediately eliminates unintended effects from digits beyond the current precision.

The flags entry shows that the result was rounded (digits beyond the context precision were thrown away) and that it's inexact (some of the discarded digits were non-zero).

Clamped:

>>> getcontext()

Context(..., flags=[Clamped], ...)

DivisionByZero: It can occur with division, modulo division, or when raising a number to a negative power.

>>> getcontext()

Context(..., flags=[DivisionByZero], ...)

Inexact:

>>> getcontext()

Context(..., flags=[Rounded, Inexact], ...)

Rounded: This signal is used to detect loss of significant digits.

>>> getcontext()

Context(..., flags=[Rounded], ...)

InvalidOperation:

>>> getcontext()

Context(..., flags=[InvalidOperation], ...)

Overflow: ``Inexact`` and ``Rounded`` are also signaled.

>>> getcontext()

Context(..., flags=[Rounded, Overflow, Inexact], ...)

Subnormal: Occurs when an operation result is subnormal (the exponent is too small).

>>> getcontext()

Context(..., flags=[Subnormal], ...)

Underflow:

>>> getcontext()

Context(..., flags=[Rounded, Underflow, Subnormal, Clamped, Inexact], ...)

Rounding (implying rounding methods, signals rules, etc.) happens mainly in two situations:

round-up: If all of the discarded digits are zero the result is unchanged, otherwise the result is incremented by 1 (round away from 0).

round-down: The discarded digits are ignored; the result is unchanged (round toward 0, truncate).

round-ceiling: If all of the discarded digits are zero or if the sign is negative the result is unchanged; otherwise, the result is incremented by 1 (round toward positive infinity).

round-floor: If all of the discarded digits are zero or if the sign is positive the result is unchanged; otherwise, the absolute value of the result is incremented by 1 (round toward negative infinity).

round-half-up: If the discarded digits represent greater than or equal to half (0.5) then the result should be incremented by 1; otherwise the discarded digits are ignored.

round-half-down: If the discarded digits represent greater than half (0.5) then the result is incremented by 1; otherwise the discarded digits are ignored.

round-half-even: If the discarded digits represent equal to half (0.5) the result is unaltered if its rightmost digit is even, or incremented by 1 if its rightmost digit is odd (to make an even digit).

Currency calculations are often defined in terms of a given precision (for instance, regulations dictate that Euro exchange rates must be quoted to 6 digits).

Note that in the float calculus you get something that is less than 0.735, so how sure are you that it'll round up to 0.74?

Perils of binary floating point comparison.

This happens a lot in standard scientific applications, when generally some error is allowed and other errors are bigger than the originated by binary floating point.

Here, the error introduced because of working with ``float`` is a lot smaller than other errors (measuring the Earth radius). Not having the binary floating point error is not enough reason to switch to Decimal and have a slower math.

If you'll work with two digits after the decimal point (up to the cents), it's not the same to round after or between the calculations.

It's my purpose to build some day a Money data type. If you're also interested in it, stay tuned to python lists (or just mail me).

For Money you must only use Decimal, though.

Facundo Batista

2005-03-06T16:13:33