Print number in engineering format

Question:

I am trying to print a number into engineering format with python, but I cannot seem to get it to work. The syntax SEEMS simple enough, but it just doesn’t work.

>>> import decimal 
>>> x = decimal.Decimal(1000000)
>>> print x
1000000
>>>> print x.to_eng_string() 
1000000

I cannot figure out why this is. The two values are not equal (one is a string, the other is an int). Setting various contexts in decimal doesn’t seem to help either. Any clues or ideas?

Asked By: jmurrayufo

||

Answers:

It seems to me that you’re going to have to roll your own:

from math import log10
def eng_str(x):
    y = abs(x)
    exponent = int(log10(y))
    engr_exponent = exponent - exponent%3
    z = y/10**engr_exponent
    sign = '-' if x < 0 else ''
    return sign+str(z)+'e'+str(engr_exponent)

Although you may want to take a little more care in the formatting of the z portion…

not well tested. Feel free to edit if you find bugs

Answered By: mgilson

To get this to work, you have to normalize the decimal first:

>>> x = decimal.Decimal ('10000000')

>>> x.normalize()
Decimal('1E+7')

>>> x.normalize().to_eng_string()
'10E+6'

The reason for this can be discovered by delving in to the source code.

If you examine to_eng_string() in the Python 2.7.3 source tree (Lib/decimal.py from the gzipped source tar ball here), it simply calls __str__ with eng set to true.

You can then see that it decides on how many digits go to the left of the decimal initially with:

leftdigits = self._exp + len(self._int)

The following table shows what the values are for those two things:

                         ._exp       ._int         len   leftdigits
                         -----       ---------     ---   ----------
Decimal (1000000)            0       '1000000'       7            7
Decimal ('1E+6')             6       '1'             1            7

The code that continues after that is:

if self._exp <= 0 and leftdigits > -6:
    # no exponent required
    dotplace = leftdigits
elif not eng:
    # usual scientific notation: 1 digit on left of the point
    dotplace = 1
elif self._int == '0':
    # engineering notation, zero
    dotplace = (leftdigits + 1) % 3 - 1
else:
    # engineering notation, nonzero
    dotplace = (leftdigits - 1) % 3 + 1

and you can see that, unless it already has an exponent in a certain range (self._exp > 0 or leftdigits <= -6), none will be given to it in the string representation.


Further investigation shows the reason for this behaviour. Looking at the code itself, you’ll see it’s based on the General Decimal Arithmetic Specification (PDF here).

If you search that document for to-scientific-string (on which to-engineering-string is heavily based), it states in part (paraphrased, and with my bold bits):

The "to-scientific-string" operation converts a number to a string, using scientific notation if an exponent is needed. The operation is not affected by the context.

If the number is a finite number then:

The coefficient is first converted to a string in base ten using the characters 0 through 9 with no leading zeros (except if its value is zero, in which case a single 0 character is used).

Next, the adjusted exponent is calculated; this is the exponent, plus the number of characters in the converted coefficient, less one. That is, exponent+(clength-1), where clength is the length of the coefficient in decimal digits.

If the exponent is less than or equal to zero and the adjusted exponent is greater than or equal to -6, the number will be converted to a character form without using exponential notation. In this case, if the exponent is zero then no decimal point is added. Otherwise (the exponent will be negative), a decimal point will be inserted with the absolute value of the exponent specifying the number of characters to the right of the decimal point. “0” characters are added to the left of the converted coefficient as necessary. If no character precedes the decimal point after this insertion then a conventional “0” character is prefixed.

In other words, it’s doing what it’s doing because that’s what the standard tells it to do.

Answered By: paxdiablo

I realize that this is an old thread, but it does come near the top of a search for python engineering notation.

I am an engineer who likes the “engineering 101” engineering units. I don’t even like designations such as 0.1uF, I want that to read 100nF. I played with the Decimal class and didn’t really like its behavior over the range of possible values, so I rolled a package called engineering_notation that is pip-installable.

pip install engineering_notation

From within Python:

>>> from engineering_notation import EngNumber
>>> EngNumber('1000000')
1M
>>> EngNumber(1000000)
1M
>>> EngNumber(1000000.0)
1M
>>> EngNumber('0.1u')
100n
>>> EngNumber('1000m')
1

This package also supports comparisons and other simple numerical operations.

https://github.com/slightlynybbled/engineering_notation

Answered By: slightlynybbled

It is 2020 now folks! and there is an COVID-19 pandemic. You got to use f-string literals for this type of work:

With python3.6.4 and above, you can do

a = 3e-4
print(f"{a:.0E}")
Answered By: episodeyang

QuantiPhy is another package that can help you with this. QuantiPhy provides the Quantity class that is used to combine a value and units into one object. It can format the quantity in a variety of ways. Generally people use SI scale factors, but engineering form is also supported. Install with:

pip3 install quantiphy

You can create a Quantity object starting from a number or a string:

>>> from quantiphy import Quantity
>>> q = Quantity(10000000)
>>> q.render(form='eng')
'10e6'

By default QuantiPhy uses SI notation, but you can set the default form to ‘eng’:

>>> print(q)
10M
>>> Quantity.set_prefs(form='eng')
>>> print(q)
10e6

Normally you would specify units to a quantity:

>>> f = Quantity(10_000_000, 'Hz')
>>> print(f)
10e6 Hz

Quantity subclasses float, so you can use a quantity anywhere you would use a float.

QuantiPhy is a powerful package that is well supported and well documented. I encourage you to give it a try.

Answered By: August West

matplotlib can be used also:

>>> from matplotlib.ticker import EngFormatter
>>> engFormat = EngFormatter()
>>> print(engFormat(1000))
1 k

I let you take a look for further details and options at https://matplotlib.org/stable/api/ticker_api.html
Another more complex example:

>>> from matplotlib.ticker import EngFormatter
>>> engFormat = EngFormatter(unit='Hz',places=2,sep='')
>>> print(engFormat(1000))
1.00kHz
Answered By: EricF

There is a solution using the built-in printf-style String Formatting

I will assume that (forward in time) you now use the built-in python 3, as all things formatting are greatly simplifed and normalized (I recommend to bookmark this cheat sheet.

In [1]: myfloat = 0.000000001
In [2]: print(f'{myfloat:g}')
1e-09

the older syntax (which would work with python 2) is:

In [17]: '%g' % myfloat
Out[17]: '1e-09'

Another nice format is given by:

In [3]: print(f'{myfloat:E}')
1.000000E-09

for which you ccan have some control over the output:

In [4]: print(f'{myfloat:.3E}')
1.000E-09
Answered By: meduz

My solution (I had the same problem)
It’s shallow thought, no fancy programming,
quick and dirty, but fine for me :).
It has a quick test included.

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
    converts float number to string in engineering form
    xxx.xxx [G M k _ m u n p]
    
"""

def F(f):

    if f < 0.0:
        sgn = "-"# = True
        f = -f
    else:
        sgn = "+"
    
    if   f > 999999999999.999999999999:
        return sgn+"max.---"+">" # "overflow error"
    elif f > 999999999.999999999999:
        v = f/1000000000, "G"
    elif f > 999999.999999999999:
        v = f/1000000.0, "M"
    elif f > 999.999999999999:
        v = f/1000.0, "k"
    elif f > 0.999999999999:
        v = f , " "
    elif f > 0.000999999999:
        v = f*1000 , "m"
    elif f > 0.000000999999:
        v = f*1000000.0 , "u"
    elif f > 0.000000000999:
        v = f*1000000000.0 , "n"
    elif f > 0.000000000000999:
        v = f*1000000000000.0 , "p"
    elif f > 0.000000000000000999:
        v = f*1000000000000.0 , "p"
    else:
        return sgn+"min.---"+"<" # "underflow error"
        
    n = v[0]
    if sgn == "-":
        n = - n
        
    # to sIII.FFF s = sign, I = integer digit, F = fractional digit
    
    n = "{:+08.3f}".format(n)
    return n + v[1]

if __name__ == "__main__":
    # test
    s = ""
    f = 0.0000000000000001111111111
    for a in range(55):
        f = f * 3.333
        v = F(f)
        print("{:03d} {:-37.18f}".format(a,f),v)
        v = F(-f)
        print("{:03d} {:-37.18f}".format(a,-f),v)
    
Answered By: Oliver Richter

Like Decimal(s).normalize().to_eng_string() but the latter does not use engineering notation for e-3 and e-6.

from decimal import Decimal

def to_eng(v: float | str | Decimal) -> str:
    """
    Convert a floating point value to engineering notation.
    """
    dec = Decimal(v).normalize().as_tuple()
    sign = '-' if dec.sign else ''
    digits = ''.join(str(digit) for digit in dec.digits)
    exp = dec.exponent

    # number of digits to left of decimal point
    leftdigits = exp + len(digits)

    dotplace = (leftdigits - 1) % 3 + 1

    if dotplace <= 0:
        intpart = '0'
        fracpart = '.' + '0' * -dotplace + digits
    elif dotplace >= len(digits):
        intpart = digits + '0' * (dotplace - len(digits))
        fracpart = ''
    else:
        intpart = digits[:dotplace]
        fracpart = '.' + digits[dotplace:]
    if leftdigits == dotplace:
        exps = ''
    else:
        exps = f'e{leftdigits - dotplace}'

    return sign + intpart + fracpart + exps
Answered By: Waxrat
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.