Skip to content

Commit

Permalink
Feature/nan zero sign (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
jagerber48 authored Feb 16, 2024
2 parents 1217526 + 525eceb commit fa2383f
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 14 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ Fixed
Now all ``Decimal`` inputs are immediately normalized before any
formatting.
[`#148 <https://github.com/jagerber48/sciform/issues/148>`_]
* Fixed the behavior around the sign symbols for zero and non-finite
inputs.
Previously ``0`` was treated as positive for the sake of resolving
its sign symbol, the sign of infinite numbers was preserved but
``+inf`` did not respect the ``"+"`` and ``" "`` sign modes, and
``nan`` never had a sign but also never had an extra character added
for ``"+"`` or ``" "`` sign modes.
Now both ``0`` and ``nan`` are treated as having no sign.
In both ``"+"`` and ``" "`` sign modes ``0`` and ``nan`` are prepended
by a space.
The sign of infinite numbers is retained as before, but now formatting
of these numbers respects the sign mode.
[`#147 <https://github.com/jagerber48/sciform/issues/147>`_]

----

Expand Down
32 changes: 31 additions & 1 deletion docs/source/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,6 @@ extra whitespace in place of a sign symbol.
This mode may be useful to match string lengths when positive and
negatives numbers are being presented together, but without explicitly
including a ``'+'`` symbol.
Note that ``0`` is always considered positive.

>>> formatter = Formatter(sign_mode="-")
>>> print(formatter(42))
Expand All @@ -514,6 +513,37 @@ Note that ``0`` is always considered positive.
>>> print(formatter(42))
42

Note that both :class:`float` ``nan`` and :class:`float` ``0`` have sign
bits which may be positive or negative.
:mod:`sciform` always ignores these sign bits and never puts a ``+`` or
``-`` symbol in front of either ``nan`` or ``0``.
In ``"+"`` or ``" "`` sign modes ``nan`` and ``0`` are always preceded
by a space.
The sign symbol for ``±inf`` is resolved the same as for
finite numbers.

>>> formatter = Formatter(sign_mode="-")
>>> print(formatter(float("-0")))
0
>>> print(formatter(float("-nan")))
nan
>>> print(formatter(float("+inf")))
inf
>>> formatter = Formatter(sign_mode="+")
>>> print(formatter(float("+0")))
0
>>> print(formatter(float("+nan")))
nan
>>> print(formatter(float("+inf")))
+inf
>>> formatter = Formatter(sign_mode=" ")
>>> print(formatter(float("-0")))
0
>>> print(formatter(float("-nan")))
nan
>>> print(formatter(float("-inf")))
-inf

Capitalization
==============

Expand Down
2 changes: 1 addition & 1 deletion docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ types for the uncertainty.

These example strings can be natively cast to numeric types like
:class:`int`, :class:`float`, or :class:`Decimal`.
However, the :class:`Formatter` and :class:`SciNum` also accept
However, :class:`Formatter` and :class:`SciNum` also accept
formatted strings which contain the numeric information, but with more
sophisticated formatting.

Expand Down
24 changes: 17 additions & 7 deletions src/sciform/format_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,26 @@ def get_exp_str( # noqa: PLR0913
def get_sign_str(num: Decimal, sign_mode: SignModeEnum) -> str:
"""Get the format sign string."""
if num < 0:
# Always return "-" for negative numbers.
sign_str = "-"
elif sign_mode is SignModeEnum.ALWAYS:
sign_str = "+"
elif sign_mode is SignModeEnum.SPACE:
elif num > 0:
# Return "+", " ", or "" for positive numbers.
if sign_mode is SignModeEnum.ALWAYS:
sign_str = "+"
elif sign_mode is SignModeEnum.SPACE:
sign_str = " "
elif sign_mode is SignModeEnum.NEGATIVE:
sign_str = ""
else:
msg = f"Invalid sign mode {sign_mode}."
raise ValueError(msg)
elif sign_mode is SignModeEnum.ALWAYS or sign_mode is SignModeEnum.SPACE:
# For anything else (typically 0, possibly nan) return " " in "+" and " " modes
sign_str = " "
elif sign_mode is SignModeEnum.NEGATIVE:
sign_str = ""
else:
msg = f"Invalid sign mode {sign_mode}."
raise ValueError(msg)
# Otherwise return the empty string.
sign_str = ""

return sign_str


Expand Down
9 changes: 6 additions & 3 deletions src/sciform/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
get_exp_str,
get_mantissa_exp_base,
get_round_digit,
get_sign_str,
get_val_unc_exp,
get_val_unc_mantissa_strs,
get_val_unc_top_digit,
Expand Down Expand Up @@ -66,10 +67,12 @@ def format_non_finite(num: Decimal, options: FinalizedOptions) -> str:
"""Format non-finite numbers."""
if num.is_nan():
num_str = "nan"
elif num == Decimal("inf"):
if options.sign_mode in [SignModeEnum.ALWAYS, SignModeEnum.SPACE]:
num_str = f" {num_str}"
elif num.is_infinite():
num_str = "inf"
elif num == Decimal("-inf"):
num_str = "-inf"
sign_str = get_sign_str(num, options.sign_mode)
num_str = f"{sign_str}{num_str}"
else:
msg = f"format_non_finite() cannot format {num}."
raise ValueError(msg)
Expand Down
61 changes: 61 additions & 0 deletions tests/test_float_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,73 @@ def test_nan(self):
(
float("nan"),
[
(Formatter(sign_mode="-"), "nan"),
(Formatter(sign_mode="+"), " nan"),
(Formatter(sign_mode=" "), " nan"),
(Formatter(exp_mode="percent"), "nan"),
(Formatter(exp_mode="percent", nan_inf_exp=True), "(nan)%"),
],
),
(
float("-nan"),
[
(Formatter(sign_mode="-"), "nan"),
(Formatter(sign_mode="+"), " nan"),
(Formatter(sign_mode=" "), " nan"),
(Formatter(exp_mode="percent"), "nan"),
(Formatter(exp_mode="percent", nan_inf_exp=True), "(nan)%"),
],
),
]

self.run_float_formatter_cases(cases_list)

def test_inf(self):
cases_list = [
(
float("inf"),
[
(Formatter(sign_mode="-"), "inf"),
(Formatter(sign_mode="+"), "+inf"),
(Formatter(sign_mode=" "), " inf"),
(Formatter(exp_mode="percent", nan_inf_exp=False), "inf"),
(Formatter(exp_mode="percent", nan_inf_exp=True), "(inf)%"),
],
),
(
float("-inf"),
[
(Formatter(sign_mode="-"), "-inf"),
(Formatter(sign_mode="+"), "-inf"),
(Formatter(sign_mode=" "), "-inf"),
(Formatter(exp_mode="percent", nan_inf_exp=False), "-inf"),
(Formatter(exp_mode="percent", nan_inf_exp=True), "(-inf)%"),
],
),
]
self.run_float_formatter_cases(cases_list)

def test_zero(self):
cases_list = [
(
float("+0"),
[
(Formatter(sign_mode="-"), "0"),
(Formatter(sign_mode="+"), " 0"),
(Formatter(sign_mode=" "), " 0"),
(Formatter(exp_mode="percent", nan_inf_exp=False), "0%"),
],
),
(
float("-0"),
[
(Formatter(sign_mode="-"), "0"),
(Formatter(sign_mode="+"), " 0"),
(Formatter(sign_mode=" "), " 0"),
(Formatter(exp_mode="percent", nan_inf_exp=False), "0%"),
],
),
]
self.run_float_formatter_cases(cases_list)

def test_parts_per_exp(self):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_float_fsml.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ def test_signs(self):
[
("", "0"),
("-", "0"),
("+", "+0"),
("+", " 0"),
(" ", " 0"),
],
),
Expand All @@ -538,7 +538,7 @@ def test_signs(self):
[
("", "0"),
("-", "0"),
("+", "+0"),
("+", " 0"),
(" ", " 0"),
],
),
Expand Down

0 comments on commit fa2383f

Please sign in to comment.