Method to find significant digits for any given number. Accounts for scientific notation. Returns a numeric string.
A unit test is included.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | import unittest
def makeSigDigs(num, digits, debug=False):
"""Return a numeric string with significant digits of a given number.
Arguments:
num -- a numeric value
digits -- how many significant digits (int)
debug -- boolean; set to True for verbose mode
"""
notsig = ['0', '.', '-', '+', 'e'] # not significant
pad_zeros_left = '' # zeros to pad immed. left of the decimal
pad_zeros_right = '' # zeros to pad immed. right of the decimal
pad_zeros_last = '' # zeros to pad after last number after decimal
str_left = '' # string to prepend to left of zeros and decimal
str_right = '' # string to append to right of decimal and zeros
dec = '.' # the decimal
e_idx = None
e_str = ''
num = float(num)
if debug: print "%s at %s digits:" % (repr(num), digits)
for n in repr(num):
if n not in notsig: # ignore zeros and punctuation
first_idx = repr(num).find(n) # index of first digit we care about
if debug: print "\tfirst digit at %s" % (first_idx)
break
try: first_idx # If it doesn't exist, then we're looking at 0.0
except UnboundLocalError:
return '0.0'
try: e_idx = repr(num).index('e') # get index of e if in scientific notation
except: pass
if debug: print "\te at: %s" % (e_idx)
dec_idx = repr(num).find('.') # index of the decimal
if debug: print "\tdecimal at %s" % (dec_idx)
if dec_idx < first_idx:
"""All sigdigs to right of decimal '0.033'
"""
if debug: print "\tdigits are right of decimal."
last_idx = first_idx + digits -1
if last_idx+1 > len(repr(num)[0:e_idx]): # in case we need extra zeros at the end
pad_zeros_last = '0'*(last_idx+1 - len(repr(num)[0:e_idx]))
if e_idx and last_idx >= e_idx: # fix last_idx if it picks up the 'e'
last_idx = e_idx-1
pad_zeros_left = '0'*1
pad_zeros_right = '0'*(first_idx - dec_idx - 1)
str_right = repr(num)[first_idx:last_idx+1]
elif dec_idx > first_idx + digits - 1:
"""All sigdigs to left of decimal. '3300.0'
"""
if debug: print "\tdigits are left of decimal."
last_idx = first_idx + digits - 1
if e_idx and last_idx >= e_idx: # fix last_idx if it picks up the 'e'
last_idx = e_idx-1
str_left = repr(num)[first_idx]
str_right = repr(num)[first_idx+1:last_idx+1]+'e+'+str(dec_idx-1-first_idx)
else:
"""Sigdigs straddle the decimal '3.300'
"""
if debug: print "\tnumber straddles decimal."
last_idx = first_idx + digits # an extra place for the decimal
if last_idx+1 > len(repr(num)[0:e_idx]): # in case we need extra zeros at the end
pad_zeros_last = '0'*(last_idx+1 - len(repr(num)[0:e_idx]))
if e_idx and last_idx >= e_idx: # fix last_idx if it picks up the 'e'
last_idx = e_idx-1
str_left = repr(num)[first_idx:dec_idx]
str_right = repr(num)[dec_idx+1:last_idx + 1]
if e_idx:
e_str = repr(num)[e_idx:]
if debug: print "\tlast digit at %s" % (last_idx)
if debug: print "\t%s %s %s %s %s %s %s" % (str_left or '_',
pad_zeros_left or '_',
dec or '_',
pad_zeros_right or '_',
str_right or '_',
pad_zeros_last or '_',
e_str or '_')
sig_string = str_left+pad_zeros_left+dec+pad_zeros_right+str_right+pad_zeros_last+e_str
if debug: print "\tsignificant: %s\n" % (sig_string)
return sig_string
class utMakeSigDigs(unittest.TestCase):
knownValues = [[333.333, 4, '333.3'],
[33.0, 2, '3.3e+1'],
[333.33, 2, '3.3e+2'],
[33300.00, 4, '3.330e+4'],
[0.0033333, 3, '0.00333'],
[3.3e-10, 2, '3.3e-10'],
[0.0001, 2, '0.00010'],
[3.3e-10, 3, '3.30e-10'],
[1.0000000, 6, '1.00000'],
[1.00000001591, 6, '1.00000'],
[33330000000000000000.0, 6, '3.33300e+19'],
[33330000000000000000.03, 6, '3.33300e+19']
]
def testKnownValues(self):
"""MakeSigDigs should return known values for known inputs.
"""
for el in self.knownValues:
self.assertEqual(makeSigDigs(el[0], el[1], debug=True), el[2])
if __name__ == "__main__":
unittest.main()
|
Significant digits are important in many scientific and engineering applications, and anything involving measurement and calculation.
For an introduction to significant digits: http://www.physics.uoguelph.ca/tutorials/sig_fig/SIG_dig.htm
Known Issues:
If all the significant digits are to the left of the decimal, then it always returns in scientific notation. That is, 3300 becomes 3.3e+3 and 33 becames 3.3e+1. This is a feature, not a bug. ;) Actually, I discovered this rule late in the process, and I really didn't feel like writing another rule to make 33.0 become 33. :P It is ugly, but technically correct.
I suppose some people might actually prefer that all cases returned scientific notation. However, I'm not one of those people.
The unit test could use more test cases. Bug reports and refactoring suggestions are welcome--the implementation is rather byzantine.
Disclaimer:
I wrote this mainly by memory from what I learned in high school physics. I might have missed something, because Mr. Rast was always docking me for getting my sigdigs wrong. :P
Updates:
Fixed (or rather, caught) an UnboundLocalError that raised if the given number was zero. It now returns the string '0.0', which seems correct. Any dissenters?
5-Sep-2007: So, it's kind of embarrassing still having this recipe out here. I will clean it up one of these days. Until then, see the comments below for something actually useful.
Your code is monstrous, if I will need this I'll write that.
This code passes your unit test. If you work only with floats, and don't want add to tests
code may be even one line shorter (see comments).
Just for fun. when not using longs calculating order and digits can be written with following one-liner: digs, order = [[lambda x, d=digits: (x.replace('.', '') + '0'*d)[:d], int][func](arg) for func, arg in enumerate(('%.20e'%num).split('e'))] I didn't saw such trick before. It is a map() which applies different functions to record fields.
minor bug in your code. When I ran this recipe i got:
Thanks for the tips. When I have a moment, I'll see if I can refactor my code based on your ideas. I'm fairly new with python, hence the monstrosity. ;) Thanks.
Neither of the above recipes actually works. When asked for two significant digits of 0.9999, both of the above recipes return the wrong result of 0.99, rather than the correct result of 1.0.
Better for some. Try Tim Peters' solution http://mail.python.org/pipermail/tutor/2004-July/030324.html
Love the test case, what about (0.0009, 2)? Alexander,
I decided to try your improved version before trying the original version (which looks more like something I wrote).
However: myMakeSigDigs(0.0009, 2) -> 0.00089, which I submit will not do. I would have expected to see 0.00090 the reason this matters to me is that I work with reports from chemical analysis, and folks will sometimes switch from mg/L to ug/L or vice versa, in the latter case they would expect 0.00090 to become 0.90 and 0.89 is not a good match.