Engineering Notation Screenshot

Engineering Notation

Anyone reading this will likely already have a background that covers engineering notation, so I won't go into it deeply. Computer scientists might also call engineering notation a 'human-readable' numbers. Instead of 4096B you might see 4kB or 4.096kB, depending on your precision.

There are more expansive explanations all over the web if you want more information.

Motivation

Explanation of Decimal Class

The Decimal class is used when you want to do math the way that you learned to do it in school. Numbers are not represented in the processor as binary equivalents, they are represented as the exact value. Slower, but more precise.

The python Decimal class actually has a built-in method to_eng_string() appears to be written for this purpose, but actually doesn't work strictly for all numbers, meaning that:

>>> Decimal('1000000').to_eng_string()
1000000

This is not engineering notation! But it is in line with the standard that is being implemented by Decimal. You have to actually give the string value in scientific notation OR call the normalize method:

>>> Decimal('1000000').normalize().to_eng_string()
100E+3

Unfortunately, it doesn't work as well for fractional numbers:

>>> Decimal('0.01').to_eng_string()
0.01
>>> Decimal('0.01').normalize().to_eng_string()
0.01

This behavior isn't a fault in the library. As some have explained, the library is doing exactly what it is written to do. It just isn't exactly what I want.

What Do I Want?

Ideally, I want any number to always resolve into a human-readable form presented in every ENG 101 class:

>>> EngNumber('1000000')
1M
>>> EngNumber('0.1u')
100n
>>> EngNumber('1000m')
1

To get this behavior, consistently, it took writing a bit of code...

Desired Features

As explained above, the most fundamental features is the ability to resolve any number into engineering notation the same way that you might on paper. Some additional features that would add some usefulness:

  • easy creation from floats/ints/strings
  • basic arithmetic
  • comparisons
  • proper parsing using int() and float()
  • practical rounding strategy

It would be value-added to be able to have units involved as well.

Easy Variable Creation

It should be crazy easy to create an EngNumber:

>>> EngNumber('0.0102')
10.2m
>>> EngNumber('102m')
10.2m
>>> EngNumber('10.2e-2')
10.2m
>>> EngNumber(0.01)
10.2m
>>> EngNumber('10.2k')
10.2k
>>> EngNumber(10200)
10.2k

Basic Arithmetic

We should be able to do some easy math with the numbers as well:

>>> EngNumber(10200) + EngNumber('0.4k')
10.6k
>>> EngNumber('10.2e3') - EngNumber(400)
9.8k

Comparisons

Basic comparisons should work out of the box, including comparisons with floats, ints, and Decimals:

>>> EngNumber(10200) == EngNumber('0.4k')
False
>>> EngNumber(10200) >= EngNumber('0.4k')
True
>>> EngNumber(10200) == 10200
True

Proper Parsing using int() and float()

Does this need explanation?

>>> n = EngNumber(10200)
>>> int(n)
10200
>>> float(n)
10200.0

Practical Rounding Strategy

Sometimes, adding numbers is just impractical:

>>> EngNumber('10.2k') + EngNumber(1)
10.2k

Source

Source code may be found on github, including 100% test coverage.

Actually, using the Decimal class as a starting point works very well since it implements 90% of the desired operations. All we have to do is parse input strings for proper suffixes (m, k, etc.) and replace __repl__ and __str__ with proper parsing to turn a Decimal into a human-readable format.

_suffix_lookup = {
    'p': 'e-12',
    'n': 'e-9',
    'u': 'e-6',
    'm': 'e-3',
    '': 'e0',
    'k': 'e3',
    'M': 'e6',
    'G': 'e9',
    'T': 'e12'
}

_exponent_lookup_scaled = {
    '-36': 'p',
    '-33': 'n',
    '-30': 'u',
    '-27': 'm',
    '-24': '',
    '-21': 'k',
    '-18': 'M',
    '-15': 'G',
    '-12': 'T'
}

class EngNumber:
    """
    Used for easy manipulation of numbers which use engineering notation
    """

    def __init__(self, value: (str, int, float), precision: int=2):
        """
        Initialize the class
        :param value: string, integer, or float representing the numeric value of the number
        :param precision: the precision past the decimal - default to 2
        """
        self.precision = precision

        if isinstance(value, str):
            suffix_keys = [key for key in _suffix_lookup.keys() if key != '']

            for suffix in suffix_keys:
                if suffix in value:
                    value = value[:-1] + _suffix_lookup[suffix]
                    break

            self.number = Decimal(value)

        elif isinstance(value, int) or isinstance(value, float):
            self.number = Decimal(str(value))

    def __repr__(self):
        """
        Returns the string representation
        :return: a string representing the engineering number
        """
        # since Decimal class only really converts number that are very small
        # into engineering notation, then we will simply make all number a
        # small number and take advantage of Decimal class
        num_str = self.number * Decimal('10e-25')
        num_str = num_str.to_eng_string().lower()

        base, exponent = num_str.split('e')

        base = str(round(Decimal(base), self.precision))
        if '.00' in base:
            base = base[:-3]

        return base + _exponent_lookup_scaled[exponent]

        ...
        ...

One trick that I used in the __repr__ is to take advantage of the Decimal class. Since it works well for very small numbers, then simply multiplying the number by a very small number and looking up the proper suffix based on the scaled exponent works quite well.

Adding Units

I also created an EngUnit class which builds on the EngNumber class. It simply allows one to specify units that get parsed correctly. It also does basic unit checking for addition, subtraction, and comparison:

>>> EngUnit('10000ohm')
10kohm
>>> EngUnit('10ohm') + EngUnit(0.02)
AttributeError: units do not match
>>> EngUnit('10ohm') + EngUnit('20ohm')
30ohm

When a unit is not present, then EngUnit works basically like EngNumber except the unit is treated as None.

Enjoy!



© by Jason R. Jones 2016
My thanks to the Pelican and Python Communities.