# Pretty print with unit and physical suffix

I have a solution for pretty printing physical values with the correct suffix (kilo, mega etc.).

I'd like to discuss my solution with you.

# -*- coding: utf-8 -*-

import math
def eng_format(x, unit=''):
e =(math.floor(math.log10(x))//3)*3
sL = ['f', 'p', 'n', 'u', 'm', '', 'k', 'M', 'G']

idx = int(e/3) + sL.index('')

if idx<0 or idx>=len(sL):
s = '*10^%d '%e
else:
s = sL[idx]

nx = x * 10**(-e)
return '%.2f%s%s'%(nx, s, unit)

if __name__ == '__main__':
print eng_format(0.002, 'V')
print eng_format(0.000003221, 'A')
print eng_format(30589, 'A')
print eng_format(60000000, 'W')


Screen output:

2.00mV
3.22uA
30.59kA
60.00MW

-
In this day and age, you should really use str.format e.g. '*10^{0} '.format(e) rather than '*10^%d '%e –  jonrsharpe May 16 at 22:23

• Use better variable naming, with proper vocabulary. In particular, "significand" is the term to use instead of nx.
• You multiplied some intermediate result by 3 to obtain e. Then you immediately do idx = int(e/3) + …, which represents wasted work. More importantly, clarity suffers. I suggest replacing e with power_of_1000 = int(math.floor(math.log10(x) // 3)), whose intent is much clearer.
• You can take advantage of the fact that negative indexes count backwards from the end of a list.
• There should be a Space in front of each prefix for consistency with the '*10^%d ' format.
import math

def eng_format(x, unit=''):
# U+03BC is Greek lowercase mu
UNITS = [' ', ' k', ' M', ' G'] + \
([None] * 10) + \
[' f', ' p', ' n', u' \u03bc', ' m']

power_of_1000 = int(math.floor(math.log10(x) // 3))
exponent = 3 * power_of_1000
prefix = UNITS[power_of_1000]
if prefix is None:
prefix = '*10^%d ' % exponent

significand = x * 10**(-exponent)
return '%.2f%s%s' % (significand, prefix, unit)

-

### Unit testing

When developing a utility function like this it's good to create unit tests so you can refactor safely, knowing when you break something. Let's say your code above is in a file called prettyprint.py, you could create a file with unit tests in prettyprint_tests.py like this:

import unittest
from test.test_support import run_unittest

from prettyprint import eng_format

class FormatTestCase(unittest.TestCase):
def test_examples(self):
self.assertEquals('2.00mV', eng_format(0.002, 'V'))
self.assertEquals('3.22uA', eng_format(0.000003221, 'A'))
self.assertEquals('30.59kA', eng_format(30589, 'A'))
self.assertEquals('60.00MW', eng_format(60000000, 'W'))
self.assertEquals('6.00*10^15 W', eng_format(6000000000000000, 'W'))
self.assertEquals('600.00*10^-18 W', eng_format(.0000000000000006, 'W'))

if __name__ == '__main__':
run_unittest(FormatTestCase)


### Coding style

PEP8 is the official style guide for Python. It's good to follow it, it makes your code more readable by everyone following the standard.

Even more importantly, it would be better to use more meaningful variable names, and to remove unnecessary brackets, for example:

def eng_format(x, unit):
magnitude = math.floor(math.log10(x)) // 3 * 3
suffixes = ['f', 'p', 'n', 'u', 'm', '', 'k', 'M', 'G']

idx = int(magnitude / 3) + suffixes.index('')

if idx < 0 or idx >= len(suffixes):
suffix = '*10^%d ' % magnitude
else:
suffix = suffixes[idx]

nx = x / 10 ** magnitude
return '%.2f%s%s' % (nx, suffix, unit)


Other than these minor issues, your code seems fine to me.

-
+1 for tests. You can never have too many tests. –  BeetDemGuise May 16 at 19:34

Output:

• A space after the number is conventional, except when there's no unit: 0.12 nm, but 34.5M.
• Why not use µ instead of u?
• More prefixes: ['y', 'z', 'a' ... 'T', 'P', 'E', 'Z', 'Y']
• Trailing zeroes are mildly annoying to read, and may give a false impression of significant digits.
• Ideally, the number of decimal places would vary to keep the same number of significant digits: 0.123 km, 1.23 km, 12.3 km, 123 km. The number of significant digits could be a parameter.

Names:

• e = scale
• sL = prefixes
• nx = scaled

Simplification:

• You divide e by 3, then multiply by 3, then divide by 3 again. They can all be avoided by scale = math.floor(math.log(x, 1000)), changing the uses of scale accordingly.
-