Firstly, a function for those who just want to get the copy and paste code:
def truncate(f, n): '''Truncates/pads a float f to n decimal places without rounding''' s = '{}'.format(f) if 'e' in s or 'E' in s: return '{0:.{1}f}'.format(f, n) i, p, d = s.partition('.') return '.'.join([i, (d+'0'*n)[:n]])
This is true in Python 2.7 and 3.1+. For older versions, it is impossible to get the same “smart rounding” effect (at least not without a lot of complex code), but rounding to 12 decimal places before truncation will work most of the time:
def truncate(f, n): '''Truncates/pads a float f to n decimal places without rounding''' s = '%.12f' % f i, p, d = s.partition('.') return '.'.join([i, (d+'0'*n)[:n]])
Explanation
The core of the basic method is to convert the value to a string with complete precision, and then simply chop off everything except the desired number of characters. The last step is simple; this can be done either using string manipulation
i, p, d = s.partition('.') '.'.join([i, (d+'0'*n)[:n]])
or decimal module
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
The first step, converting to a string, is quite difficult because there are several pairs of floating point literals (i.e. what you write in the source code) that both produce the same binary representation and still have to be truncated by in different ways. For example, consider 0.3 and 0.29999999999999998. If you write 0.3 in a Python program, the compiler encodes it using the IEEE floating point format into a sequence of bits (assuming a 64-bit float)
0011111111010011001100110011001100110011001100110011001100110011
This is the closest value to 0.3, which can be accurately represented as an IEEE float. But if you write 0.29999999999999998 in a Python program, the compiler translates it to exactly the same value. In one case, you meant that it was truncated (by one digit) as 0.3 , while in the other case, you meant that it was truncated as 0.2 , but Python can only give one answer. This is a fundamental limitation of Python or even any programming language without lazy evaluation. The truncation function has access only to the binary value stored in the computer's memory, and not to the line that you actually entered in the source code. one
If you decode the sequence of bits back to decimal, again using the IEEE 64-bit floating point format, you will get
0.2999999999999999888977697537484345957637...
so a naive implementation would come about with 0.2 , although this is probably not what you want. For more information about floating point errors, see the Python Tutorial .
It is very rare to work with a floating point value that is so close to a round number and yet intentionally not equal to that round number. Therefore, when truncating, it probably makes sense to choose the “most enjoyable” decimal representation of everything that can match the value in memory. Python 2.7 and later (but not 3.0) includes a sophisticated algorithm that does just that , which we can access through the default string formatting operation.
'{}'.format(f)
The only caveat is that it acts like a g format specification in the sense that it uses exponential notation ( 1.23e+4 ) if the number is large or small enough. Therefore, the method should catch this case and handle it differently. There are several cases where using the f format specification instead causes a problem, for example, trying to trim 3e-10 to 28 precision digits (it produces 0.0000000002999999999999999980 ), and I'm still not sure how to best handle those.
If you really work with float that are very close to round numbers but intentionally not equal to them (for example, 0.29999999999999998 or 99.959999999999994), this will lead to some false positives, i.e. a number that you did not want to round up will be rounded. In this case, the solution should indicate a fixed accuracy.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
The number of precision digits used here is not significant, it should be large enough to ensure that any rounding performed in the string conversion does not "replace" the value with its good decimal representation. I think that sys.float_info.dig + n + 2 may be sufficient in all cases, but if it is not necessary that 2 could be increased, and this will not hinder doing this.
In earlier versions of Python (prior to 2.6 or 3.0), formatting floating-point numbers was much rougher and regularly produced things like
>>> 1.1 1.1000000000000001
If this is your situation, if you want to use “beautiful” decimal notations for truncation, all you can do (as far as I know) is to select a certain number of digits, less than the full precision represented by the float , and round the number to this number, before you crop it. A typical choice is 12,
'%.12f' % f
but you can customize it according to the numbers you use.
1 Well ... I lied. Technically, you can instruct Python to reanalyze your own source code and extract the part corresponding to the first argument that you pass to the truncation function. If this argument is a floating point literal, you can simply cut it off from a certain number of places after the decimal point and return it. However, this strategy does not work if the argument is a variable, which makes it useless. For entertainment value, only the following is presented:
def trunc_introspect(f, n): '''Truncates/pads the float f to n decimal places by looking at the caller source code''' current_frame = None caller_frame = None s = inspect.stack() try: current_frame = s[0] caller_frame = s[1] gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline) for token_type, token_string, _, _, _ in gen: if token_type == tokenize.NAME and token_string == current_frame[3]: next(gen)
Generalizing this to handle the case when you pass a variable seems like a lost cause, since you have to trace back through the program execution until you find the floating point literal that gave the variable a value. If there is even one. Most variables will be initialized from user input or mathematical expressions, in which case the binary representation is all there is.