diff --git a/doc/source/functions.rst b/doc/source/functions.rst index 8ea67a69..fdaa4c78 100644 --- a/doc/source/functions.rst +++ b/doc/source/functions.rst @@ -65,6 +65,8 @@ SI Unit Conversion Functions .. autofunction:: pyqtgraph.siEval +.. autofunction:: pyqtgraph.siParse + Image Preparation Functions --------------------------- @@ -100,5 +102,3 @@ Miscellaneous Functions .. autofunction:: pyqtgraph.systemInfo .. autofunction:: pyqtgraph.exit - - diff --git a/examples/SpinBox.py b/examples/SpinBox.py index f7023037..88366cdf 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -31,6 +31,8 @@ spins = [ pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)), + ("Float with SI prefix but no suffix", + pg.SpinBox(value=1e9, siPrefix=True)), ("Float with custom formatting", pg.SpinBox(value=23.07, format='${value:0.02f}', regex='\$?(?P(-?\d+(\.\d+)?)|(-?\.\d+))$')), diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 037f0f51..994520ba 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -119,10 +119,28 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al def siParse(s, regex=FLOAT_REGEX, suffix=None): """Convert a value written in SI notation to a tuple (number, si_prefix, suffix). - + Example:: - - siParse('100 μV") # returns ('100', 'μ', 'V') + + siParse('100 µV") # returns ('100', 'µ', 'V') + + Note that in the above example, the µ symbol is the "micro sign" (UTF-8 + 0xC2B5), as opposed to the Greek letter mu (UTF-8 0xCEBC). + + Parameters + ---------- + s : str + The string to parse. + regex : re.Pattern, optional + Compiled regular expression object for parsing. The default is a + general-purpose regex for parsing floating point expressions, + potentially containing an SI prefix and a suffix. + suffix : str, optional + Suffix to check for in ``s``. The default (None) indicates there may or + may not be a suffix contained in the string and it is returned if + found. An empty string ``""`` is handled differently: if the string + contains a suffix, it is discarded. This enables interpreting + characters following the numerical value as an SI prefix. """ s = asUnicode(s) s = s.strip() @@ -130,15 +148,20 @@ def siParse(s, regex=FLOAT_REGEX, suffix=None): if s[-len(suffix):] != suffix: raise ValueError("String '%s' does not have the expected suffix '%s'" % (s, suffix)) s = s[:-len(suffix)] + 'X' # add a fake suffix so the regex still picks up the si prefix - + + # special case: discard any extra characters if suffix is explicitly empty + if suffix == "": + s += 'X' + m = regex.match(s) if m is None: raise ValueError('Cannot parse number "%s"' % s) + try: sip = m.group('siPrefix') except IndexError: sip = '' - + if suffix is None: try: suf = m.group('suffix') @@ -146,8 +169,8 @@ def siParse(s, regex=FLOAT_REGEX, suffix=None): suf = '' else: suf = suffix - - return m.group('number'), '' if sip is None else sip, '' if suf is None else suf + + return m.group('number'), '' if sip is None else sip, '' if suf is None else suf def siEval(s, typ=float, regex=FLOAT_REGEX, suffix=None): diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index e7849809..cc7f5a89 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -423,6 +423,30 @@ def test_eq(): assert eq(set(range(10)), set(range(10))) assert not eq(set(range(10)), set(range(9))) - -if __name__ == '__main__': - test_interpolateArray() + +@pytest.mark.parametrize("s,suffix,expected", [ + # usual cases + ("100 uV", "V", ("100", "u", "V")), + ("100 µV", "V", ("100", "µ", "V")), + ("4.2 nV", None, ("4.2", "n", "V")), + ("1.2 m", "m", ("1.2", "", "m")), + ("1.2 m", None, ("1.2", "", "m")), + ("5.0e9", None, ("5.0e9", "", "")), + ("2 units", "units", ("2", "", "units")), + # siPrefix with explicit empty suffix + ("1.2 m", "", ("1.2", "m", "")), + ("5.0e-9 M", "", ("5.0e-9", "M", "")), + # weirder cases that should return the reasonable thing + ("4.2 nV", "nV", ("4.2", "", "nV")), + ("4.2 nV", "", ("4.2", "n", "")), + ("1.2 j", "", ("1.2", "", "")), + ("1.2 j", None, ("1.2", "", "j")), + # expected error cases + ("100 uV", "v", ValueError), +]) +def test_siParse(s, suffix, expected): + if isinstance(expected, tuple): + assert pg.siParse(s, suffix=suffix) == expected + else: + with pytest.raises(expected): + pg.siParse(s, suffix=suffix) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index b6acc8d7..108a32c1 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -132,8 +132,10 @@ class SpinBox(QtGui.QAbstractSpinBox): siPrefix (bool) If True, then an SI prefix is automatically prepended to the units and the value is scaled accordingly. For example, if value=0.003 and suffix='V', then the SpinBox will display - "300 mV" (but a call to SpinBox.value will still return 0.003). Default - is False. + "300 mV" (but a call to SpinBox.value will still return 0.003). In case + the value represents a dimensionless quantity that might span many + orders of magnitude, such as a Reynold's number, an SI + prefix is allowed with no suffix. Default is False. step (float) The size of a single step. This is used when clicking the up/ down arrows, when rolling the mouse wheel, or when pressing keyboard arrows while the widget has keyboard focus. Note that @@ -466,7 +468,7 @@ class SpinBox(QtGui.QAbstractSpinBox): # format the string val = self.value() - if self.opts['siPrefix'] is True and len(self.opts['suffix']) > 0: + if self.opts['siPrefix'] is True: # SI prefix was requested, so scale the value accordingly if self.val == 0 and prev is not None: @@ -545,7 +547,7 @@ class SpinBox(QtGui.QAbstractSpinBox): return False # check suffix - if suffix != self.opts['suffix'] or (suffix == '' and siprefix != ''): + if suffix != self.opts['suffix']: return False # generate value diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index bbef5eaa..19a7bbfa 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -23,6 +23,7 @@ def test_SpinBox_defaults(): (1.45, '1.45 PSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)), (1.45e-3, '1.45 mPSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)), (-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')), + (1000, '1 k', dict(siPrefix=True, suffix="")), ]) def test_SpinBox_formatting(value, expected_text, opts): sb = pg.SpinBox(**opts) @@ -35,10 +36,11 @@ def test_SpinBox_formatting(value, expected_text, opts): @pytest.mark.parametrize("suffix", ["", "V"]) def test_SpinBox_gui_set_value(suffix): sb = pg.SpinBox(suffix=suffix) + sb.lineEdit().setText('0.1' + suffix) sb.editingFinishedEvent() assert sb.value() == 0.1 - if suffix != '': - sb.lineEdit().setText('0.1 m' + suffix) - sb.editingFinishedEvent() - assert sb.value() == 0.1e-3 + + sb.lineEdit().setText('0.1 m' + suffix) + sb.editingFinishedEvent() + assert sb.value() == 0.1e-3