Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enclosing round bracket flags r and R to the format_num method #116

Closed
wants to merge 9 commits into from
18 changes: 18 additions & 0 deletions doc/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,24 @@ formatting options. They can be added at the end of the format string:
>>> print '{:L}'.format(x*1e7) # Automatic exponent form, LaTeX
\left(2.00 \pm 0.10\right) \times 10^{6}

- ``b`` for surrounding **brackets** (common exponent outside)

>>> print '{:b}'.format(x)
(2.00+/-0.10)e-01

- ``B`` for surrounding **brackets** (common exponent inside)

>>> print '{:B}'.format(x)
((2.0+/-0.10)e-01)

The ``b`` and ``B`` options can be useful when dealing with units:

>>> print '{:Pb} m'.format(x)
(2.00±0.10)×10⁻¹ m

>>> print '{:Pb} dm'.format(10 * x)
(2.00±0.10) dm

These custom formatting options **can be combined** (when meaningful).

Details
Expand Down
63 changes: 46 additions & 17 deletions uncertainties/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,13 +976,25 @@ def robust_align(orig_str, fill_char, align_option, width):
0x37: '⁷',
0x38: '⁸',
0x39: '⁹'
}
}
if sys.version_info < (3, 0):
TO_SUPERSCRIPT = {normal: ord(unicode(sup, 'utf-8'))
for (normal, sup) in TO_SUPERSCRIPT.items()}

# Inverted TO_SUPERSCRIPT table, for use with unicode.translate():
#
#! Python 2.7+ can use a dictionary comprehension instead:
FROM_SUPERSCRIPT = {
ord(sup): normal for (normal, sup) in TO_SUPERSCRIPT.items()}
# ! Python 2.7+ can use a dictionary comprehension instead:
if sys.version_info > (3, 0):
# all strings are unicode strings.
FROM_SUPERSCRIPT = {ord(sup): normal
for (normal, sup) in TO_SUPERSCRIPT.items()}
else:
# we need to decode the utf-8 strings first, before ord can find
# the code point for us.
FROM_SUPERSCRIPT = {sup: normal
for (normal, sup) in TO_SUPERSCRIPT.items()}



def to_superscript(value):
'''
Expand All @@ -992,8 +1004,7 @@ def to_superscript(value):

value -- integer.
'''

return ('%d' % value).translate(TO_SUPERSCRIPT)
return (u'%d' % value).translate(TO_SUPERSCRIPT)

def from_superscript(number_str):
'''
Expand All @@ -1003,12 +1014,16 @@ def from_superscript(number_str):

number_str -- basestring object.
'''
return int(str(number_str).translate(FROM_SUPERSCRIPT))
# return int(str(number_str).translate(FROM_SUPERSCRIPT))
if sys.version_info > (3, 0):
return int(str(number_str).translate(FROM_SUPERSCRIPT))
else:
return int(unicode(str(number_str), 'utf-8').translate(FROM_SUPERSCRIPT))

# Function that transforms an exponent produced by format_num() into
# the corresponding string notation (for non-default modes):
EXP_PRINT = {
'pretty-print': lambda common_exp: '×10%s' % to_superscript(common_exp),
'pretty-print': lambda common_exp: u'×10%s' % to_superscript(common_exp),
'latex': lambda common_exp: r' \times 10^{%d}' % common_exp}

# Symbols used for grouping (typically between parentheses) in format_num():
Expand Down Expand Up @@ -1069,7 +1084,10 @@ def format_num(nom_val_main, error_main, common_exp,
and the error, superscript exponents, etc.). "L" is for a LaTeX
output. Options can be combined. "%" adds a final percent sign,
and parentheses if the shorthand notation is not used. The P
option has priority over the L option (if both are given).
option has priority over the L option (if both are given). The "r"
option adds enclosing parenthesis with a common factor outside. "R"
forces the parenthesis to also enclose the factor, which means that
you might get two pairs of parenthesis.
'''

# print (nom_val_main, error_main, common_exp,
Expand Down Expand Up @@ -1403,7 +1421,7 @@ def format_num(nom_val_main, error_main, common_exp,
if 'P' in options:
# Unicode has priority over LaTeX, so that users with a
# Unicode-compatible LaTeX source can use ±:
pm_symbol = '±'
pm_symbol = u'±'
elif 'L' in options:
pm_symbol = r' \pm '
else:
Expand All @@ -1422,16 +1440,20 @@ def format_num(nom_val_main, error_main, common_exp,
# percent sign handling because this sign may too need
# parentheses.
if any_exp_factored and common_exp is not None:
value_str = ''.join((
value_str = u''.join((
LEFT_GROUPING,
nom_val_str, pm_symbol, error_str,
RIGHT_GROUPING,
exp_str, percent_str))
if 'R' in options:
value_str = LEFT_GROUPING + value_str + RIGHT_GROUPING
else:
value_str = ''.join([nom_val_str, pm_symbol, error_str])
value_str = u''.join([nom_val_str, pm_symbol, error_str])
if percent_str:
value_str = ''.join((
value_str = u''.join((
LEFT_GROUPING, value_str, RIGHT_GROUPING, percent_str))
if 'r' in options or 'R' in options:
value_str = u''.join((LEFT_GROUPING, value_str, RIGHT_GROUPING))

return value_str

Expand Down Expand Up @@ -1712,6 +1734,9 @@ def __bool__(self):
# vector of that space):
return self != 0. # Uses the AffineScalarFunc.__ne__ function

if sys.version_info < (3, 0):
__nonzero__ = __bool__

########################################

## Logical operators: warning: the resulting value cannot always
Expand Down Expand Up @@ -1926,6 +1951,9 @@ def __format__(self, format_spec):
mode is activated: "±" separates the nominal value from the
standard deviation, exponents use superscript characters,
etc. When "L" is present, the output is formatted with LaTeX.
The option "r" enforces surrounding brackets, but a common
factor will still be outside of the parenthesis. To have
a pair of parenthesis enclose everything, use "R".

An uncertainty which is exactly zero is represented as the
integer 0 (i.e. with no decimal point).
Expand Down Expand Up @@ -1964,7 +1992,7 @@ def __format__(self, format_spec):
(?P<uncert_prec>u?) # Precision for the uncertainty?
# The type can be omitted. Options must not go here:
(?P<type>[eEfFgG%]??) # n not supported
(?P<options>[LSP]*)$''',
(?P<options>[LSPrR]*)$''',
format_spec,
re.VERBOSE)

Expand Down Expand Up @@ -2944,7 +2972,7 @@ def correlation_matrix(nums_with_uncert):
\(
(?P<simple_num_with_uncert>.*)
\)
(?:[eE]|\s*×\s*10) (?P<exp_value>.*)
(?:[eE]|\s*×\s*10)(?P<exp_value>.*)
$''', re.VERBOSE).match

class NotParenUncert(ValueError):
Expand Down Expand Up @@ -3012,7 +3040,7 @@ def parse_error_in_parentheses(representation):
return (value, uncert_value)

# Regexp for catching the two variable parts of -1.2×10⁻¹²:
PRETTY_PRINT_MATCH = re.compile(r'(.*?)\s*×\s*10(.*)').match
PRETTY_PRINT_MATCH = re.compile('(.*?)\s*×\s*10(.*)').match

def to_float(value_str):
'''
Expand Down Expand Up @@ -3091,7 +3119,6 @@ def str_to_number_with_uncert(representation):

match = re.match(r'(.*)(?:\+/-|±)(.*)', representation)
if match:

(nom_value, uncert) = match.groups()

try:
Expand Down Expand Up @@ -3168,6 +3195,8 @@ def ufloat_fromstr(representation, tag=None):
the uncertainty signals an absolute uncertainty (instead of an
uncertainty on the last digits of the nominal value).
"""
if representation.startswith('(') and representation.endswith(')'):
representation = representation[1:-1]

(nominal_value, std_dev) = str_to_number_with_uncert(
representation.strip())
Expand Down
56 changes: 38 additions & 18 deletions uncertainties/test_uncertainties.py
Original file line number Diff line number Diff line change
Expand Up @@ -1643,7 +1643,7 @@ def test_format():
# found in Python 2.7: '{:.1%}'.format(0.0055) is '0.5%'.
'.1u%': '(42.0+/-0.5)%',
'.1u%S': '42.0(5)%',
'%P': '(42.0±0.5)%'
'%P': u'(42.0±0.5)%'
},

# Particle Data Group automatic convention, including limit cases:
Expand Down Expand Up @@ -1714,17 +1714,17 @@ def test_format():
# instead of 1.4 for Python 3.1. The problem does not appear
# with 1.2, so 1.2 is used.
(-1.2e-12, 0): {
'12.2gPL': r' -1.2×10⁻¹²± 0',
'12.2gPL': u' -1.2×10⁻¹²± 0',
# Pure "width" formats are not accepted by the % operator,
# and only %-compatible formats are accepted, for Python <
# 2.6:
'13S': ' -1.2(0)e-12',
'10P': '-1.2×10⁻¹²± 0',
'10P': u'-1.2×10⁻¹²± 0',
'L': r'\left(-1.2 \pm 0\right) \times 10^{-12}',
# No factored exponent, LaTeX
'1L': r'-1.2 \times 10^{-12} \pm 0',
'SL': r'-1.2(0) \times 10^{-12}',
'SP': r'-1.2(0)×10⁻¹²'
'SP': u'-1.2(0)×10⁻¹²'
},

# Python 3.2 and 3.3 give 1.4e-12*1e+12 = 1.4000000000000001
Expand All @@ -1735,7 +1735,7 @@ def test_format():
'15GS': ' -1.2(%s)E-12' % NaN_EFG,
'SL': r'-1.2(\mathrm{nan}) \times 10^{-12}', # LaTeX NaN
# Pretty-print priority, but not for NaN:
'PSL': '-1.2(\mathrm{nan})×10⁻¹²',
'PSL': u'-1.2(\mathrm{nan})×10⁻¹²',
'L': r'\left(-1.2 \pm \mathrm{nan}\right) \times 10^{-12}',
# Uppercase NaN and LaTeX:
'.1EL': (r'\left(-1.2 \pm \mathrm{%s}\right) \times 10^{-12}'
Expand All @@ -1746,8 +1746,8 @@ def test_format():

(3.14e-10, 0.01e-10): {
# Character (Unicode) strings:
'P': '(3.140±0.010)×10⁻¹⁰', # PDG rules: 2 digits
'PL': r'(3.140±0.010)×10⁻¹⁰', # Pretty-print has higher priority
'P': u'(3.140±0.010)×10⁻¹⁰', # PDG rules: 2 digits
'PL': u'(3.140±0.010)×10⁻¹⁰', # Pretty-print has higher priority
# Truncated non-zero uncertainty:
'.1e': '(3.1+/-0.0)e-10',
'.1eS': '3.1(0.0)e-10'
Expand Down Expand Up @@ -1855,7 +1855,9 @@ def test_format():
'.6g': '(1.20000+/-0.00000)e-34',
'13.6g': ' 1.20000e-34+/- 0.00000e-34',
'13.6G': ' 1.20000E-34+/- 0.00000E-34',
'.6GL': r'\left(1.20000 \pm 0.00000\right) \times 10^{-34}'
'.6GL': r'\left(1.20000 \pm 0.00000\right) \times 10^{-34}',
'.6GLr': r'\left(1.20000 \pm 0.00000\right) \times 10^{-34}',
'.6GLR': r'\left(\left(1.20000 \pm 0.00000\right) \times 10^{-34}\right)',
},

(float('nan'), 100): { # NaN *nominal value*
Expand Down Expand Up @@ -1912,9 +1914,13 @@ def test_format():
'': 'inf+/-123456789.0', # Similar to '{}'.format(123456789.)
'g': '(inf+/-1.23457)e+08', # Similar to '{:g}'.format(123456789.)
'.1e': '(inf+/-1.2)e+08',
'.1er': '(inf+/-1.2)e+08',
'.1eR': '((inf+/-1.2)e+08)',
'.1E': '(%s+/-1.2)E+08' % Inf_EFG,
'.1ue': '(inf+/-1)e+08',
'.1ueL': r'\left(\infty \pm 1\right) \times 10^{8}',
'.1ueLr': r'\left(\infty \pm 1\right) \times 10^{8}',
'.1ueLR': r'\left(\left(\infty \pm 1\right) \times 10^{8}\right)',
'10.1e': ' inf+/- 1.2e+08',
'10.1eL': r' \infty \pm 1.2 \times 10^{8}'
},
Expand All @@ -1923,7 +1929,9 @@ def test_format():
'.1e': 'inf+/-inf',
'.1E': '%s+/-%s' % (Inf_EFG, Inf_EFG),
'.1ue': 'inf+/-inf',
'EL': r'\infty \pm \infty'
'EL': r'\infty \pm \infty',
'ELR': r'\left(\infty \pm \infty\right)',
'ELr': r'\left(\infty \pm \infty\right)',
},

# Like the tests for +infinity, but for -infinity:
Expand Down Expand Up @@ -1965,7 +1973,9 @@ def test_format():
# digit past the decimal point" for Python floats, but only
# with a non-zero uncertainty:
(724.2, 26.4): {
'': '724+/-26'
'': '724+/-26',
'r': '(724+/-26)',
'R': '(724+/-26)',
},
(724, 0): {
'': '724.0+/-0'
Expand All @@ -1977,7 +1987,7 @@ def test_format():
'S': '-inf(inf)',
'LS': '-\infty(\infty)',
'L': '-\infty \pm \infty',
'LP': '-\infty±\infty',
'LP': u'-\infty±\infty',
# The following is consistent with Python's own
# formatting, which depends on the version of Python:
# formatting float("-inf") with format(..., "020") gives
Expand All @@ -2003,7 +2013,7 @@ def test_format():
'S': 'nan(inf)',
'LS': '\mathrm{nan}(\infty)',
'L': '\mathrm{nan} \pm \infty',
'LP': '\mathrm{nan}±\infty'
'LP': u'\mathrm{nan}±\infty'
},

# Leading zeroes in the shorthand notation:
Expand All @@ -2018,7 +2028,7 @@ def test_format():

tests.update({
(1234.56789, 0.012): {
',.1uf': '1,234.57+/-0.01'
',.1uf': u'1,234.57+/-0.01'
},

(123456.789123, 1234.5678): {
Expand Down Expand Up @@ -2054,9 +2064,9 @@ def test_format():
assert representation == result, (
# The representation is used, for terminal that do not
# support some characters like ±, and superscripts:
'Incorrect representation %r for format %r of %r:'
'Incorrect representation %r for format %r of %r (%r):'
' %r expected.'
% (representation, format_spec, value, result))
% (representation, format_spec, value, values, result))

# An empty format string is like calling str()
# (http://docs.python.org/2/library/string.html#formatspec):
Expand All @@ -2076,10 +2086,16 @@ def test_format():
# Specific case:
and '=====' not in representation):

value_back = ufloat_fromstr(representation)
try:
value_back = ufloat_fromstr(representation)
except ValueError:
print(representation, type(representation))
continue

# The original number and the new one should be consistent
# with each other:
# print(' Checking for consistency: %r ~> %r ~> %r'
# % (value, representation, value_back))
try:

# The nominal value can be rounded to 0 when the
Expand Down Expand Up @@ -2111,8 +2127,12 @@ def test_unicode_format():

x = ufloat(3.14159265358979, 0.25)

assert isinstance('Résultat = %s' % x.format(''), str)
assert isinstance('Résultat = %s' % x.format('P'), str)
if sys.version_info < (3, 0):
assert isinstance(u'Résultat = %s' % x.format(''), unicode)
assert isinstance(u'Résultat = %s' % x.format('P'), unicode)
else:
assert isinstance(u'Résultat = %s' % x.format(''), str)
assert isinstance(u'Résultat = %s' % x.format('P'), str)

###############################################################################

Expand Down
2 changes: 1 addition & 1 deletion uncertainties/unumpy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ def wrapped_func(array_like, *args, **kwargs):
for element in array_version.flat:
# floats, etc. might be present
if isinstance(element, uncert_core.AffineScalarFunc):
variables |= element.derivatives.keys()
variables |= set(element.derivatives.keys())

array_nominal = nominal_values(array_version)
# Function value, then derivatives at array_nominal (the
Expand Down