15

Here is the example which is bothering me:

>>> x = decimal.Decimal('0.0001')
>>> print x.normalize()
>>> print x.normalize().to_eng_string()
0.0001
0.0001

Is there a way to have engineering notation for representing mili (10e-3) and micro (10e-6)?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
TheMeaningfulEngineer
  • 15,679
  • 27
  • 85
  • 143
  • Is this what you are looking for? http://stackoverflow.com/questions/6913532/python-how-to-convert-decimal-to-scientific-notation – Hans Then Jul 31 '13 at 14:46
  • 2
    Nope. Engineering notation is the floating point representation in which exponents are only multiples of 3, and the mantissa never has more than 3 digits. [Reference](http://en.wikipedia.org/wiki/Engineering_notation) – TheMeaningfulEngineer Jul 31 '13 at 14:54
  • Then, would the engineering notation of this be 100E-6 – sihrc Jul 31 '13 at 15:15
  • @sihrc Yes, that is correct. – TheMeaningfulEngineer Jul 31 '13 at 15:25
  • 1
    Looks like zero has your answer. you can probably implement your own code to take in the exceptions if it bothers you that much. – sihrc Jul 31 '13 at 15:31
  • I like engineering notation as well. I created a package to deal with it `pip install engineering_notation`, some explanation at [forembed.com](http://forembed.com/engineering-notation-in-python.html) – slightlynybbled Jun 06 '17 at 15:16
  • Related: https://stackoverflow.com/questions/12311148/print-number-in-engineering-format – handle Jun 26 '17 at 13:02

7 Answers7

16

Here's a function that does things explicitly, and also has support for using SI suffixes for the exponent:

def eng_string( x, format='%s', si=False):
    '''
    Returns float/int value <x> formatted in a simplified engineering format -
    using an exponent that is a multiple of 3.

    format: printf-style string used to format the value before the exponent.

    si: if true, use SI suffix for exponent, e.g. k instead of e3, n instead of
    e-9 etc.

    E.g. with format='%.2f':
        1.23e-08 => 12.30e-9
             123 => 123.00
          1230.0 => 1.23e3
      -1230000.0 => -1.23e6

    and with si=True:
          1230.0 => 1.23k
      -1230000.0 => -1.23M
    '''
    sign = ''
    if x < 0:
        x = -x
        sign = '-'
    exp = int( math.floor( math.log10( x)))
    exp3 = exp - ( exp % 3)
    x3 = x / ( 10 ** exp3)

    if si and exp3 >= -24 and exp3 <= 24 and exp3 != 0:
        exp3_text = 'yzafpnum kMGTPEZY'[ ( exp3 - (-24)) / 3]
    elif exp3 == 0:
        exp3_text = ''
    else:
        exp3_text = 'e%s' % exp3

    return ( '%s'+format+'%s') % ( sign, x3, exp3_text)
Julian Smith
  • 196
  • 1
  • 2
  • 1
    This is a great function! it works well. I would suggest one improvement, something like x = numpy.float64(x) as it doesn't quite handle integer numbers – Paul May 16 '14 at 17:03
  • 1
    This great, but as Paul suggested, it doesn't work for integers. Suggest adding `import math` and `x=float(x)` (no need to bring numpy into it..?) – beroe Apr 09 '15 at 01:09
  • 3
    Two more suggestions: For python3, we need to explicitly do integer division, so 'yzafpnum kMGTPEZY'[ exp3 // 3 + 8] And for x=0, we need a case so something like if x==0: exp=0, exp3=0 – poppie Nov 19 '16 at 08:55
12

EDIT: Matplotlib implemented the engineering formatter, so one option is to directly use Matplotlibs formatter, e.g.:

import matplotlib as mpl
formatter = mpl.ticker.EngFormatter()
formatter(10000)

result: '10 k'

Original answer:

Based on Julian Smith's excellent answer (and this answer), I changed the function to improve on the following points:

  • Python3 compatible (integer division)
  • Compatible for 0 input
  • Rounding to significant number of digits, by default 3, no trailing zeros printed

so here's the updated function:

import math
def eng_string( x, sig_figs=3, si=True):
    """
    Returns float/int value <x> formatted in a simplified engineering format -
    using an exponent that is a multiple of 3.

    sig_figs: number of significant figures

    si: if true, use SI suffix for exponent, e.g. k instead of e3, n instead of
    e-9 etc.
    """
    x = float(x)
    sign = ''
    if x < 0:
        x = -x
        sign = '-'
    if x == 0:
        exp = 0
        exp3 = 0
        x3 = 0
    else:
        exp = int(math.floor(math.log10( x )))
        exp3 = exp - ( exp % 3)
        x3 = x / ( 10 ** exp3)
        x3 = round( x3, -int( math.floor(math.log10( x3 )) - (sig_figs-1)) )
        if x3 == int(x3): # prevent from displaying .0
            x3 = int(x3)

    if si and exp3 >= -24 and exp3 <= 24 and exp3 != 0:
        exp3_text = 'yzafpnum kMGTPEZY'[ exp3 // 3 + 8]
    elif exp3 == 0:
        exp3_text = ''
    else:
        exp3_text = 'e%s' % exp3

    return ( '%s%s%s') % ( sign, x3, exp3_text)
poppie
  • 549
  • 5
  • 14
  • 1
    Why are you importing FuncFormatter and not using it? But you are using np (numpy?) – Brian Apr 02 '17 at 17:37
  • You're right, that import is unnecessary. I updated the answer, thanks! – poppie Apr 04 '17 at 09:43
  • This answer is incorrect for negative numbers. It looks like the first `elif` should be an `if` otherwise all the computation in the corresponding `else` block is skipped. – Rob Smallshire Dec 06 '17 at 16:08
  • The use of numpy is optional here. The function works fine with the equivalent functions from the Python Standard Library math module. – Rob Smallshire Dec 07 '17 at 19:44
  • i agree to both your comments. this function will always have scalars as inputs so the math module seems the better choice. i updated the answer, thanks. – poppie Dec 07 '17 at 20:53
7

The decimal module is following the Decimal Arithmetic Specification, which states:

This is outdated - see below

to-scientific-string – conversion to numeric string

[...]

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.

[...]

to-engineering-string – conversion to numeric string

This operation converts a number to a string, using engineering notation if an exponent is needed.

The conversion exactly follows the rules for conversion to scientific numeric string except in the case of finite numbers where exponential notation is used. In this case, the converted exponent is adjusted to be a multiple of three (engineering notation) by positioning the decimal point with one, two, or three characters preceding it (that is, the part before the decimal point will range from 1 through 999). This may require the addition of either one or two trailing zeros.

If after the adjustment the decimal point would not be followed by a digit then it is not added. If the final exponent is zero then no indicator letter and exponent is suffixed.

Examples:

For each abstract representation [sign, coefficient, exponent] on the left, the resulting string is shown on the right.

Representation String
[0,123,1] "1.23E+3"
[0,123,3] "123E+3"
[0,123,-10] "12.3E-9"
[1,123,-12] "-123E-12"
[0,7,-7] "700E-9"
[0,7,1] "70"

Or, in other words:

>>> for n in (10 ** e for e in range(-1, -8, -1)):
...     d = Decimal(str(n))
...     print d.to_eng_string()
... 
0.1
0.01
0.001
0.0001
0.00001
0.000001
100E-9
Solomon Ucko
  • 5,724
  • 3
  • 24
  • 45
Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
  • I'm looking for a workaround to that. So that `to_eng_string()` works for smaller numbers. In the standard way, `mili` and `micro` prefixes are completely ignored and they are quite often. – TheMeaningfulEngineer Jul 31 '13 at 15:33
  • @Alan that's a slightly different question - you asked for "engineering notation for all cases", and you're getting that, per spec. What you're after is "my variation on engineering notation", which is understandably missing from the standard library. – Zero Piraeus Jul 31 '13 at 15:35
  • 1
    I see where the confusion is coming from. By "all cases" i meant that it wouldn't be discriminating towards `mili` and `micro`. Will change the subject to make it more clear. – TheMeaningfulEngineer Jul 31 '13 at 15:43
  • @ZeroPiraeus This proprietary (IBM) specification actually **admits that it is not applying engineering notation** for infinite numbers! As an engineer, I share Alan's opinion that Python chose poorly in adopting this proprietary specification. – Serge Stroobandt Jan 27 '16 at 18:00
  • @Alan you are right in noting that the [engineering notation](https://en.wikipedia.org/wiki/Engineering_notation) is not applied in all cases when implementing this proprietary specification. This becomes obvious when quoting this specification in its entirety. See also my other comment. – Serge Stroobandt Jan 27 '16 at 18:08
  • I created a [Python bug report](https://bugs.python.org/issue26223#msg259047) for `decimal.to_eng_string()`. – Serge Stroobandt Jan 27 '16 at 19:17
6

I realize that this is an old thread, but it does come near the top of a search for python engineering notation and it seems prudent to have this information located here.

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

slightlynybbled
  • 2,408
  • 2
  • 20
  • 38
4

The «full» quote shows what is wrong!

The decimal module is indeed following the proprietary (IBM) Decimal Arithmetic Specification. Quoting this IBM specification in its entirety clearly shows what is wrong with decimal.to_eng_string() (emphasis added):

to-engineering-string – conversion to numeric string

This operation converts a number to a string, using engineering notation if an exponent is needed.

The conversion exactly follows the rules for conversion to scientific numeric string except in the case of finite numbers where exponential notation is used. In this case, the converted exponent is adjusted to be a multiple of three (engineering notation) by positioning the decimal point with one, two, or three characters preceding it (that is, the part before the decimal point will range from 1 through 999). This may require the addition of either one or two trailing zeros.

If after the adjustment the decimal point would not be followed by a digit then it is not added. If the final exponent is zero then no indicator letter and exponent is suffixed.

This proprietary IBM specification actually admits to not applying the engineering notation for numbers with an infinite decimal representation, for which ordinary scientific notation is used instead! This is obviously incorrect behaviour for which a Python bug report was opened.

Solution

from math import floor, log10

def powerise10(x):
    """ Returns x as a*10**b with 0 <= a < 10
    """
    if x == 0: return 0,0
    Neg = x < 0
    if Neg: x = -x
    a = 1.0 * x / 10**(floor(log10(x)))
    b = int(floor(log10(x)))
    if Neg: a = -a
    return a,b

def eng(x):
    """Return a string representing x in an engineer friendly notation"""
    a,b = powerise10(x)
    if -3 < b < 3: return "%.4g" % x
    a = a * 10**(b % 3)
    b = b - b % 3
    return "%.4gE%s" % (a,b)

Source: https://code.activestate.com/recipes/578238-engineering-notation/

Test result

>>> eng(0.0001)
100E-6
Serge Stroobandt
  • 28,495
  • 9
  • 107
  • 102
  • You don't *really* mean "infinite numbers", do you? What do you propose that the output of `Decimal('inf').to_eng_string()` should be? – Mark Dickinson Jan 28 '16 at 08:18
  • BTW, the quoted sentence is dangerously close to ambiguous. It should be read as "except in the case of (those finite numbers for which exponential notation is used)", rather than "except in the case of finite numbers, where exponential notation is used". – Mark Dickinson Jan 28 '16 at 08:20
  • @MarkDickinson I agree, the quoted specification _is_ ambiguous. I edited my text to leave clear that what is meant is [infinite decimal representation](https://en.wikipedia.org/wiki/Decimal_representation). – Serge Stroobandt Jan 28 '16 at 09:43
  • 1
    I think you're misinterpreting that sentence in the specification. It's got nothing to do with infinite numbers, and nothing to do with numbers that don't have a finite-length decimal representation either. (The whole point of the decimal module is that *all* representable numbers have a finite-length decimal representation.) It's simply saying that the engineering format and scientific format are the same for *finite* numbers that aren't large or small enough in magnitude to require an exponent. Infinity doesn't come into it anywhere. – Mark Dickinson Jan 28 '16 at 15:52
  • Note that `eng(0.01234)` will return `'0.01234'`. I believe most people would expect `'12.3e-3'`. – AXO Aug 20 '22 at 03:46
0

Like the answers above, but a bit more compact:

from math import log10, floor

def eng_format(x,precision=3):
    """Returns string in engineering format, i.e. 100.1e-3"""
    x = float(x)  # inplace copy
    if x == 0:
        a,b = 0,0
    else: 
        sgn = 1.0 if x > 0 else -1.0
        x = abs(x) 
        a = sgn * x / 10**(floor(log10(x)))
        b = int(floor(log10(x)))

    if -3 < b < 3: 
        return ("%." + str(precision) + "g") % x
    else:
        a = a * 10**(b % 3)
        b = b - b % 3
        return ("%." + str(precision) + "gE%s") % (a,b)

Trial:

In [10]: eng_format(-1.2345e-4,precision=5)
Out[10]: '-123.45E-6'
RexBarker
  • 1,456
  • 16
  • 14
0

I've written a package called sciform to cover this and other formatting use cases that are not well-covered in stdlib. Both engineering notation (mantissa m has 1 <= m < 1000) and shifted engineering notation (mantissa m has 0.1 <= m < 100) are supported.

sciform has a custom format specification mini-language which can be used to format SciNum objects. Here r flags engineering notation and # alternate flag flags shifted engineering notation.

print(f'{SciNum(0.0001):r}')
# '100e-06'
print(f'{SciNum(0.0001):#r}')
# '0.1e-03'

Formatting can also be done in an object-oriented way using FormatOptions and Formatter objects.

from sciform import FormatOptions as Fo
from sciform import Formatter, ExpMode

sform = Formatter(Fo(exp_mode=ExpMode.ENGINEERING))
print(sform(0.0001))
# 100e-06
Jagerber48
  • 488
  • 4
  • 13