Skip to content

Commit

Permalink
Fixed rendering of full precision numbers in Quantity.fixed()
Browse files Browse the repository at this point in the history
  • Loading branch information
Ken Kundert authored and Ken Kundert committed Dec 27, 2022
1 parent bedbb58 commit 1bc07c9
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 65 deletions.
1 change: 1 addition & 0 deletions doc/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Latest development release
- Added scale factor conversion.
- Added quantity functions: :func:`as_real`, :func:`as_tuple`, :func:`render`,
:func:`fixed`, and :func:`binary`.
- Fixed rendering of full precision numbers in :meth:`Quantity.fixed()`.
- Added “cover” option to *strip_radix* preference.
- Added type hints.

Expand Down
5 changes: 5 additions & 0 deletions doc/user.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2729,6 +2729,11 @@ corresponding Python error for compatibility with existing code. It is
recommended that new code catch the *QuantiPhy* specific exceptions rather than
the generic Python exceptions as their use will be deprecated in the future.

.. note::

It is expected that in release 2.20, expected in the first half of 2023, the
exceptions will no longer inherit from the generic Python exceptions.

*QuantiPhy* employs the following exceptions:

:class:`ExpectedQuantity`:
Expand Down
141 changes: 87 additions & 54 deletions quantiphy/quantiphy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,8 @@ def _label(self, value, show_label):
except KeyError as e:
raise UnknownFormatKey(e.args[0])

# _map_leading_sign {{{2
# private utility functions {{{2
# _map_leading_sign {{{3
def _map_leading_sign(self, value, leading_units=''):
# maps a leading sign, but only if given
if math.isnan(self):
Expand All @@ -1178,7 +1179,7 @@ def _map_leading_sign(self, value, leading_units=''):
return self.plus + leading_units + value[1:]
return leading_units + value

# _map_sign {{{2
# _map_sign {{{3
def _map_sign(self, value):
# maps + and - anywhere in the value
if self.minus != '-':
Expand All @@ -1187,7 +1188,7 @@ def _map_sign(self, value):
value = value.replace('+', self.plus)
return value

# _fix_punct {{{2
# _fix_punct {{{3
def _fix_punct(self, mantissa):
def replace_char(c):
if c == '.':
Expand All @@ -1197,7 +1198,45 @@ def replace_char(c):
return c
return ''.join((map(replace_char, mantissa)))

# _combine {{{2
# _split_original_number {{{3
def _split_original_number(self):
mantissa = self._mantissa
if mantissa[0] in '+-':
sign = '-' if mantissa[0] == '-' else ''
mantissa = mantissa[1:]
else:
sign = ''
sf = self._scale_factor

# convert scale factor to integer exponent
try:
exp = int(sf)
except ValueError:
if sf:
exp = int(MAPPINGS.get(sf, sf).lstrip('e'))
else:
exp = 0

# add decimal point to mantissa if missing
mantissa += '' if '.' in mantissa else '.'
# strip off leading zeros and break into components
whole, frac = mantissa.lstrip('0').split('.')
if whole == '':
# no whole part
# normalize by removing leading zeros from fractional part
orig_len = len(frac)
frac_stripped = frac.lstrip('0')
if frac_stripped:
whole = frac_stripped[:1]
frac = frac_stripped[1:]
exp -= orig_len - len(frac)
else:
# stripping off zeros left us with nothing, this must be 0
whole = '0'
exp = 0
return sign, whole, frac, exp

# _combine {{{3
def _combine(self, mantissa, sf, units, spacer, sf_is_exp=False):
if units in self.tight_units:
spacer = ''
Expand Down Expand Up @@ -1938,42 +1977,8 @@ def render(

# convert into scientific notation with proper precision {{{3
if prec == 'full' and hasattr(self, '_mantissa') and not scale:
mantissa = self._mantissa
if mantissa[0] in '+-':
sign = '-' if mantissa[0] == '-' else ''
mantissa = mantissa[1:]
else:
sign = ''
sf = self._scale_factor

# convert scale factor to integer exponent
try:
exp = int(sf)
except ValueError:
if sf:
exp = int(MAPPINGS.get(sf, sf).lstrip('e'))
else:
exp = 0

# add decimal point to mantissa if missing
mantissa += '' if '.' in mantissa else '.'
# strip off leading zeros and break into components
whole, frac = mantissa.lstrip('0').split('.')
if whole == '':
# no whole part
# normalize by removing leading zeros from fractional part
orig_len = len(frac)
frac_stripped = frac.lstrip('0')
if frac_stripped:
whole = frac_stripped[:1]
frac = frac_stripped[1:]
exp -= orig_len - len(frac)
else:
# stripping off zeros left us with nothing, this must be 0
whole = '0'
exp = 0
# normalize the mantissa
mantissa = whole[0] + '.' + whole[1:] + frac
sign, whole, frac, exp = self._split_original_number()
mantissa = f"{whole[0]}.{whole[1:]}{frac}"
exp += len(whole) - 1
else:
# determine precision
Expand Down Expand Up @@ -2077,9 +2082,7 @@ def fixed(
:arg prec:
The desired precision (one plus this value is the desired number of
digits). If specified as 'full', *full_prec* is used as the number
of digits (and not the originally specified precision as with
*render()*).
digits). If specified as 'full', the full original precision is used.
:type prec: integer or 'full'
:arg show_label:
Expand Down Expand Up @@ -2132,7 +2135,7 @@ def fixed(
Example::
>>> t = Quantity('Total = $1000000 — the total')
>>> t = Quantity('Total = $1000000.00 — the total')
>>> print(
... t.fixed(),
... t.fixed(show_commas=True),
Expand All @@ -2153,7 +2156,7 @@ def fixed(
... t.fixed(strip_zeros=False, prec='full'),
... t.fixed(show_label=True),
... t.fixed(show_label='f'), sep=newline)
$1000000.000000000000
$1000000.00
Total = $1000000
Total = $1000000 — the total
Expand Down Expand Up @@ -2181,16 +2184,46 @@ def fixed(
value = self._combine(value, '', units, ' ')
return self._label(value, show_label)

# format and return the result {{{3
if prec == 'full':
prec = self.full_prec
if scale or isinstance(scale, numbers.Number):
number, units = _scale(scale, self)
units = units if show_units else ''
# split into and process components {{{3
if prec == 'full' and hasattr(self, '_mantissa') and not scale:
sign, whole, frac, exp = self._split_original_number()

# eliminate exponent by moving radix
if exp < 0: # move radix to left
if -exp < len(whole):
# partition whole and move trailing digits to frac
frac = whole[exp:] + frac
whole = whole[:exp]
else:
# move all of whole to frac and add zeros to left-hand side
frac = (-exp - len(whole))*'0' + whole + frac
whole = '0'
else: # move radix to right
if len(frac) > exp:
# partition frac and move leading digits to frac
whole = whole + frac[:exp]
frac = frac[exp:]
else:
# move all of frac to whole and add zeros to right-hand side
whole = whole + frac + (exp-len(frac))*'0'
frac = ''
if show_commas:
whole = f"{int(whole):,}"
mantissa = f"{sign}{whole}.{frac}"
else:
number = float(self)
comma = ',' if show_commas else ''
mantissa = '{0:{1}.{2}f}'.format(number, comma, prec)
if prec == 'full':
prec = self.full_prec
assert prec >= 0

if scale or isinstance(scale, numbers.Number):
number, units = _scale(scale, self)
units = units if show_units else ''
else:
number = float(self)
comma = ',' if show_commas else ''
mantissa = '{0:{1}.{2}f}'.format(number, comma, prec)

# strip zeros and radix if requested
if '.' in mantissa:
if strip_zeros:
mantissa = mantissa.rstrip('0')
Expand Down
23 changes: 16 additions & 7 deletions tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ def test_full_format():
assert '{:G}'.format(q) == 'f = 1420405751.786'
assert '{:n}'.format(q) == 'f'
assert '{:d}'.format(q) == 'frequency of hydrogen line'
assert '{:.2p}'.format(q) == '1420405751.79 Hz'
assert '{:,.2p}'.format(q) == '1,420,405,751.79 Hz'
assert '{:p}'.format(q) == '1420405751.786 Hz'
assert '{:,p}'.format(q) == '1,420,405,751.786 Hz'
assert '{:,.2pHz}'.format(q) == '1,420,405,751.79 Hz'
assert '{:,.2pkHz}'.format(q) == '1,420,405.75 kHz'
assert '{:,.2pMHz}'.format(q) == '1,420.41 MHz'
assert '{:,.2pGHz}'.format(q) == '1.42 GHz'
assert '{:,.2pTHz}'.format(q) == '0 THz'
assert '{:.2P}'.format(q) == 'f = 1420405751.79 Hz'
assert '{:,.2P}'.format(q) == 'f = 1,420,405,751.79 Hz'
assert '{:P}'.format(q) == 'f = 1420405751.786 Hz'
assert '{:,P}'.format(q) == 'f = 1,420,405,751.786 Hz'
assert '{:#.3q}'.format(q) == '1.420 GHz'
assert '{:#.6p}'.format(q) == '1420405751.786000 Hz'
assert '{:.0q}'.format(q) == '1 GHz'
Expand All @@ -98,10 +98,19 @@ def test_full_format():
q = Quantity(given)
if q == 0 and expected[0] == '-':
expected = expected[1:]
assert q.render(form='si', strip_zeros=False) == expected, given
assert q.render(form='si', prec='full', strip_zeros=False) == expected, given

q=Quantity('2ns')
assert float(q) == 2e-9
# check fixed()
base = '000654321.123456000'
for d in range(-12, 12):
num = f"{base}e{d}"
q = Quantity(num, '$')

prec = max(6-d, 0)
fmtd = f"{float(num):25.{prec}f}".strip()
assert f"${fmtd}" == q.fixed()


def test_width():
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
Expand Down Expand Up @@ -437,7 +446,7 @@ def test_render():
assert q.fixed(strip_zeros=False) == '$1000000.0000'
assert q.fixed(strip_zeros=True, strip_radix=False) == '$1000000.'
assert q.fixed(prec='full') == '$1000000'
assert q.fixed(prec='full', strip_zeros=False) == '$1000000.000000000000'
assert q.fixed(prec='full', strip_zeros=False) == '$1000000'
assert q.render(form='fixed') == '$1000000'

q=Quantity('$100')
Expand Down
19 changes: 15 additions & 4 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,23 @@ class Foo(Quantity):
assert q.render(prec='full') == '1.8 V'

with Quantity.prefs(keep_components=False, strip_zeros=False):
q = Quantity('1.2345 V')
assert q.render(prec='full') == '1.234500000000 V'
q = Quantity('1.2345 MV')
assert q.render(prec='full') == '1.234500000000 MV'
with pytest.raises(AttributeError):
q._mantissa
with pytest.raises(AttributeError):
q._scale_factor

with Quantity.prefs(keep_components=True, strip_zeros=False):
q = Quantity('1.2345000 V')
assert q.render(prec='full') == '1.2345000 V'
q = Quantity('1.2345000 MV')
assert q.render(prec='full') == '1.2345000 MV'
assert q._mantissa == '1.2345000'
assert q._scale_factor == 'M'

q = Quantity('1.2345000e6 V')
assert q.render(prec='full') == '1.2345000 MV'
assert q._mantissa == '1.2345000'
assert q._scale_factor == 'e6'

with pytest.raises(ValueError) as exception:
q = Quantity('x*y = z')
Expand Down

0 comments on commit 1bc07c9

Please sign in to comment.