Skip to content

Commit

Permalink
create the number_to_si and si_to_number functions
Browse files Browse the repository at this point in the history
  • Loading branch information
jborbely committed Mar 5, 2020
1 parent c473d8b commit e34a59c
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 0 deletions.
3 changes: 3 additions & 0 deletions msl/qt/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@

HOME_DIR = os.path.join(os.path.expanduser('~'), '.msl')
""":class:`str`: The default ``$HOME`` directory where all files are to be located."""

SI_PREFIX_MAP = {i: prefix for i, prefix in enumerate('yzafpn\u00b5m kMGTPEZY', start=-8)}
""":class:`dict`: The SI prefixes used to form multiples of 10**(3*n), n=-8..8"""
91 changes: 91 additions & 0 deletions msl/qt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
General helper functions.
"""
import logging
from math import (
isinf,
isnan,
floor,
log10,
fabs,
)

from .constants import SI_PREFIX_MAP
from . import (
QtGui,
Qt,
Expand Down Expand Up @@ -172,3 +180,86 @@ def screen_geometry(widget=None):

# the Qt docs say that this function is deprecated
return QtWidgets.QDesktopWidget().availableGeometry(widget)


def number_to_si(number):
"""Convert a number to be represented with an SI prefix.
The hecto (h), deka (da), deci (d) and centi (c) prefixes are not used.
Parameters
----------
number : :class:`int` or :class:`float`
The number to convert.
Returns
-------
:class:`float`
The number rescaled.
:class:`str`
The SI prefix.
Examples
--------
>>> number_to_si(0.0123)
(12.3, 'm')
>>> number_to_si(123456.789)
(123.456789, 'k')
>>> number_to_si(712.123e14)
(71.2123, 'P')
>>> number_to_si(1.23e-13)
(123.0, 'f')
"""
if isnan(number) or isinf(number) or number == 0:
return number, ''
n = int(floor(log10(fabs(number)) / 3))
if n == 0:
return number, ''
if n > 8 or n < -8:
raise ValueError('The number {} cannot be expressed with an SI prefix'.format(number))
return number * 10 ** (-3 * n), SI_PREFIX_MAP[n]


def si_to_number(string):
"""Convert an SI string to a number.
Parameters
----------
string : :class:`str`
The string to convert.
Returns
-------
:class:`float`
The number.
Examples
--------
>>> si_to_number('12.3m')
0.0123
>>> si_to_number('123.456789k')
123456.789
>>> si_to_number('71.2123P')
7.12123e+16
>>> si_to_number('123f')
1.23e-13
"""
string_ = string.strip()
if not string_:
# mimic the builtin error message if one did float('')
raise ValueError("could not convert string to float: ''")

if string_ in ['nan', 'inf', '+inf', '-inf']:
return float(string_)

prefix = string_[-1]
if prefix.isdigit():
return float(string_)
if prefix == 'u':
prefix = '\u00b5'
for n, value in SI_PREFIX_MAP.items():
if prefix == value:
return float(string_[:-1]) * 10 ** (3 * n)

# mimic the builtin error message
raise ValueError('could not convert string to float: {!r}'.format(string))
196 changes: 196 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import math

import pytest

from msl.qt import Qt, QtGui, QtCore, QtWidgets, utils
Expand Down Expand Up @@ -163,3 +165,197 @@ def test_screen_geometry():
assert isinstance(utils.screen_geometry(), QtCore.QRect)
assert isinstance(utils.screen_geometry(QtWidgets.QLabel()), QtCore.QRect)
assert isinstance(utils.screen_geometry(QtWidgets.QLabel(parent=QtWidgets.QLabel())), QtCore.QRect)


def test_number_to_si():

def check(args, number, string):
value, prefix = args
if math.isnan(number):
assert math.isnan(value)
assert prefix == ''
elif math.isinf(number):
assert math.isinf(value)
assert prefix == ''
else:
assert number == pytest.approx(value, abs=1e-12)
assert prefix == string

check(utils.number_to_si(math.nan), math.nan, '')
check(utils.number_to_si(math.inf), math.inf, '')
check(utils.number_to_si(-math.inf), -math.inf, '')

with pytest.raises(ValueError) as e:
utils.number_to_si(0.0012e-24)
assert 'cannot be expressed' in str(e.value)

check(utils.number_to_si(-12.34e-25), -1.234, 'y')
check(utils.number_to_si(0.123e-20), 1.23, 'z')
check(utils.number_to_si(-123456e-23), -1.23456, 'a')
check(utils.number_to_si(1.23e-13), 123.0, 'f')
check(utils.number_to_si(-1.23e-12), -1.23, 'p')
check(utils.number_to_si(123.e-12), 123., 'p')
check(utils.number_to_si(-12.3e-10), -1.23, 'n')
check(utils.number_to_si(1.23e-8), 12.3, 'n')
check(utils.number_to_si(-0.123e-7), -12.3, 'n')
check(utils.number_to_si(0.123e-5), 1.23, '\u00b5')
check(utils.number_to_si(-0.0123), -12.3, 'm')
check(utils.number_to_si(123.4), 123.4, '')
check(utils.number_to_si(0), 0, '')
check(utils.number_to_si(-123456.789), -123.456789, 'k')
check(utils.number_to_si(1.23e8), 123., 'M')
check(utils.number_to_si(-0.123e10), -1.23, 'G')
check(utils.number_to_si(1.23e14), 123., 'T')
check(utils.number_to_si(-1.23e12), -1.23, 'T')
check(utils.number_to_si(12.3e15), 12.3, 'P')
check(utils.number_to_si(-712.123e14), -71.2123, 'P')
check(utils.number_to_si(1234.56e16), 12.3456, 'E')
check(utils.number_to_si(-12.3e18), -12.3, 'E')
check(utils.number_to_si(0.123e20), 12.3, 'E')
check(utils.number_to_si(-12.3e20), -1.23, 'Z')
check(utils.number_to_si(-1.754e21), -1.754, 'Z')
check(utils.number_to_si(123.456e20), 12.3456, 'Z')
check(utils.number_to_si(123.456e21), 123.456, 'Z')
check(utils.number_to_si(-0.42e24), -420., 'Z')
check(utils.number_to_si(1.234e24), 1.234, 'Y')
check(utils.number_to_si(12.678e24), 12.678, 'Y')
check(utils.number_to_si(12345.678e22), 123.45678, 'Y')

with pytest.raises(ValueError) as e:
utils.number_to_si(12345.678e24)
assert 'cannot be expressed' in str(e.value)

check(utils.number_to_si(-0), 0., '')
check(utils.number_to_si(0), 0., '')
check(utils.number_to_si(1), 1., '')
check(utils.number_to_si(-1), -1., '')
check(utils.number_to_si(12), 12., '')
check(utils.number_to_si(123), 123., '')
check(utils.number_to_si(1234), 1.234, 'k')
check(utils.number_to_si(12345), 12.345, 'k')
check(utils.number_to_si(123456), 123.456, 'k')
check(utils.number_to_si(1234567), 1.234567, 'M')
check(utils.number_to_si(12345678), 12.345678, 'M')
check(utils.number_to_si(123456789), 123.456789, 'M')
check(utils.number_to_si(-123456789), -123.456789, 'M')
check(utils.number_to_si(1234567890), 1.234567890, 'G')
check(utils.number_to_si(12345678901), 12.345678901, 'G')
check(utils.number_to_si(123456789012), 123.456789012, 'G')
check(utils.number_to_si(1234567890123), 1.234567890123, 'T')
check(utils.number_to_si(12345678901234), 12.345678901234, 'T')
check(utils.number_to_si(123456789012345), 123.456789012345, 'T')
check(utils.number_to_si(1234567890123456), 1.234567890123456, 'P')
check(utils.number_to_si(-1234567890123456), -1.234567890123456, 'P')
check(utils.number_to_si(12345678901234567), 12.345678901234567, 'P')
check(utils.number_to_si(123456789012345678), 123.456789012345678, 'P')
check(utils.number_to_si(1234567890123456789), 1.234567890123456789, 'E')
check(utils.number_to_si(12345678901234567890), 12.345678901234567890, 'E')
check(utils.number_to_si(123456789012345678901), 123.456789012345678901, 'E')
check(utils.number_to_si(1234567890123456789012), 1.234567890123456789012, 'Z')
check(utils.number_to_si(12345678901234567890123), 12.345678901234567890123, 'Z')
check(utils.number_to_si(123456789012345678901234), 123.456789012345678901234, 'Z')
check(utils.number_to_si(1234567890123456789012345), 1.234567890123456789012345, 'Y')
check(utils.number_to_si(12345678901234567890123456), 12.345678901234567890123456, 'Y')
check(utils.number_to_si(123456789012345678901234567), 123.456789012345678901234567, 'Y')
check(utils.number_to_si(-123456789012345678901234567), -123.456789012345678901234567, 'Y')

with pytest.raises(ValueError) as e:
utils.number_to_si(1234567890123456789012345678)
assert 'cannot be expressed' in str(e.value)


def test_si_to_number():

def check(string, value):
assert utils.si_to_number(string) == pytest.approx(value)

check('12.3m', 0.0123)
check('123.456789k', 123456.789)
check('71.2123P', 712.123e14)
check('123f', 1.23e-13)

check('0.12Y', 1.2e23)
check('1.2Y', 1.2e24)
check('-123.4Y', -1.234e26)
check('0.12Z', 1.2e20)
check('-1.2Z', -1.2e21)
check('-123.4Z', -1.234e23)
check('-0.12E', -1.2e17)
check('1.2E', 1.2e18)
check('123.4E', 1.234e20)
check('-0.12P', -1.2e14)
check('1.2P', 1.2e15)
check('123.4P', 1.234e17)
check('0.12T', 1.2e11)
check('-1.2T', -1.2e12)
check('123.4T', 1.234e14)
check('-0.12G', -1.2e8)
check('1.2G', 1.2e9)
check('123.4G', 1.234e11)
check('0.12M', 1.2e5)
check('1.2M', 1.2e6)
check('-123.4M', -1.234e8)
check('-0.12k', -1.2e2)
check('1.2k', 1.2e3)
check('123.4k', 1.234e5)
check('-0.12', -0.12)
check('0', 0)
check('123456789', 123456789.0)
check('1.2', 1.2)
check('123.4', 123.4)
check('0.12m', 1.2e-4)
check('-1.2m', -1.2e-3)
check('123.4m', 1.234e-1)
check('-0.12\u00b5', -1.2e-7)
check('1.2\u00b5', 1.2e-6)
check('123.4\u00b5', 1.234e-4)
check('-0.12u', -1.2e-7)
check('1.2u', 1.2e-6)
check('123.4u', 1.234e-4)
check('0.12n', 1.2e-10)
check('1.2n', 1.2e-9)
check('-123.4n', -1.234e-7)
check('0.12p', 1.2e-13)
check('1.2p', 1.2e-12)
check('-123.4p', -1.234e-10)
check('0.12f', 1.2e-16)
check('-1.2f', -1.2e-15)
check('123.4f', 1.234e-13)
check('0.12a', 1.2e-19)
check('-1.2a', -1.2e-18)
check('123.4a', 1.234e-16)
check('-0.12z', -1.2e-22)
check('-1.2z', -1.2e-21)
check('123.4z', 1.234e-19)
check('0.12y', 1.2e-25)
check('1.2y', 1.2e-24)
check('-123.4y', -1.234e-22)

# spaces
check('123.4 m', 1.234e-1)
check('123.4 m ', 1.234e-1)
check(' 123.4 m ', 1.234e-1)
for item in ['', ' ', '\t', ' \t ']:
with pytest.raises(ValueError) as err:
utils.si_to_number(item)
assert str(err.value).startswith("could not convert string to float: ''")

# nan, +/-inf
assert math.isnan(utils.si_to_number('nan'))
value = utils.si_to_number('inf')
assert math.isinf(value) and value > 0
value = utils.si_to_number('+inf')
assert math.isinf(value) and value > 0
value = utils.si_to_number('-inf')
assert math.isinf(value) and value < 0

for c in 'bcdeghijloqrstvwxABCDFHIJKLNOQRSUVWX':
with pytest.raises(ValueError) as err:
utils.si_to_number('1.2'+c)
assert str(err.value).startswith('could not convert string to float: {!r}'.format('1.2'+c))

for prefix in 'yzafpnu\u00b5mkMGTPEZY':
with pytest.raises(ValueError) as err:
utils.si_to_number('1.2'+c+prefix)
# the prefix is not at the end of the error message
assert str(err.value).startswith('could not convert string to float: {!r}'.format('1.2'+c))

0 comments on commit e34a59c

Please sign in to comment.