The Python Decimal class is a handy arbitrary precision numeric type for dealing with extraordinarily large or small numbers, or numbers requiring exact precision. The strength of this class is in it’s use of overridden methods that allow it to be used in standard mathematical constructs:
>>> Decimal(2) ** 16 - 1
Decimal("65535")
Because of these overridden methods, the class can in fact be passed to many of the math library’s methods:
>>> from math import *
>>> floor(Decimal("1.21"))
1.0
>>> fmod(Decimal("10.25"), Decimal("10"))
0.25
>>> modf(Decimal("1.234"))
(0.23399999999999999, 1.0)
Of course the short coming of this is what is returned: a built in float type. When using the math library methods we lose the precision of the Decimal type. We could attempt to use the math library log method to calculate the logarithm of a Decimal type:
>>> log(Decimal(100), 10) 2.0 >>> log(Decimal(100), 3.14) 4.0247145803329714
For a log that returns an integer this works well, but for anything requiring decimal precision we lose the accuracy and precision of the Decimal type. We can instead use an adaptation of the common logarithm, reconstructed to work as a class method for the Decimal type. The Python code:
def dec_log(self, base=10):
cur_prec = getcontext().prec
getcontext().prec += 2
baseDec = Decimal(10)
retValue = self
if isinstance(base, Decimal):
baseDec = base
elif isinstance(base, float):
baseDec = Decimal("%f" % (base))
else:
baseDec = Decimal(base)
integer_part = Decimal(0)
while retValue < 1:
integer_part = integer_part - 1
retValue = retValue * baseDec
while retValue >= baseDec:
integer_part = integer_part + 1
retValue = retValue / baseDec
retValue = retValue ** 10
decimal_frac = Decimal(0)
partial_part = Decimal(1)
while cur_prec > 0:
partial_part = partial_part / Decimal(10)
digit = Decimal(0)
while retValue >= baseDec:
digit += 1
retValue = retValue / baseDec
decimal_frac = decimal_frac + digit * partial_part
retValue = retValue ** 10
cur_prec -= 1
getcontext().prec -= 2
return integer_part + decimal_frac
This algorithm calculates the integer and fractional portions of the logarithm in separate passes, composing the two when returning the value. Taking a cue from a comment by Jake Voytko on adding nth roots to Python, this algorithm adjusts the working precision of the Decimal types by adding 2 extra digits of precision during the calculation, and trimming two digits from the precision when the calculation is complete, to ensure that the trailing digits of the result are as accurate as possible (a technique I should have used in previous examples, had I consulted the Decimal numeric recipes).
The logarithm can be added to the Decimal type and used with no parameters, a float, an integer or a Decimal parameter. Without a parameter the logarithm defaults to log10:
>>> Decimal.log = dec_log
>>> Decimal(100).log()
Decimal("2.000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000")
>>> Decimal(100).log(3.14)
Decimal("4.024714580332970585367941961553706972817670933108784336660631220853887
020695465320530995672957013298")
>>> Decimal(1234).log(Decimal("1.1234"))
Decimal("61.17246795014182187657829372589717334008147684454521224410597688872638
875290237619255864354194950937")
favorited this one, dude