Welcome, guest | Sign In | My Account | Store | Cart

Method to find significant digits for any given number. Accounts for scientific notation. Returns a numeric string.

A unit test is included.

Python, 104 lines
 ``` 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

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.

Alexander Semenov 16 years, 8 months ago

Your code is monstrous, if I will need this I'll write that.

``````def myMakeSigDigs(num, digits, debug=False):
digs, order = ('%.20e'%num).split('e')
order = int(order)
if type(num) is long: digs = str(num) # Not needed for current tests
digs = (digs.replace('.', '') + '0'*digits)[:digits]

if debug: print 'num=%r, order=%d, digits=%s'%(num, order, digs)

if 0&lt;=order&lt;5 and order&lt;len(digs)-1:
return '%s.%s'%(digs[:order+1], digs[order+1:])
elif -5&lt;=order&lt;0:
return '0.%s%s'%('0'*(-order-1), digs)
else:
return '%s.%se%+d'%(digs[0], digs[1:], order)
``````

This code passes your unit test. If you work only with floats, and don't want add to tests

``````[1234567890123456789012L, 19, '1.234567890123456789e+21']
``````

code may be even one line shorter (see comments).

Alexander Semenov 16 years, 8 months ago

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.

Alexander Semenov 16 years, 8 months ago

minor bug in your code. When I ran this recipe i got:

``````======================================================================
FAIL: MakeSigDigs should return known values for known inputs.
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Usr\Sav\wrk\prj\digs\digicalc.py", line 118, in testKnownValues
self.assertEqual(makeSigDigs(el[0], el[1], debug=True), el[2])
AssertionError: '3.3e-010' != '3.3e-10'

----------------------------------------------------------------------
``````
David Eyk (author) 16 years, 8 months ago

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.

Brian Boonstra 15 years, 4 months ago

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.

Brian Boonstra 15 years, 2 months ago

Better for some. Try Tim Peters' solution http://mail.python.org/pipermail/tutor/2004-July/030324.html

John Hill 13 years, 9 months ago

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.

 Created by David Eyk on Thu, 17 Mar 2005 (PSF)