diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34996a2c..0b309767 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -106,6 +106,19 @@ Fixed Now all ``Decimal`` inputs are immediately normalized before any formatting. [`#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 `_] ---- diff --git a/docs/source/options.rst b/docs/source/options.rst index efed02d7..f1cf094a 100644 --- a/docs/source/options.rst +++ b/docs/source/options.rst @@ -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)) @@ -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 ============== diff --git a/docs/source/usage.rst b/docs/source/usage.rst index d26a66b7..4de9c8b6 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -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. diff --git a/src/sciform/format_utils.py b/src/sciform/format_utils.py index c8f39fc8..a86ad8dd 100644 --- a/src/sciform/format_utils.py +++ b/src/sciform/format_utils.py @@ -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 diff --git a/src/sciform/formatting.py b/src/sciform/formatting.py index f5b03c47..b6423822 100644 --- a/src/sciform/formatting.py +++ b/src/sciform/formatting.py @@ -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, @@ -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) diff --git a/tests/test_float_formatter.py b/tests/test_float_formatter.py index 5869f7a8..1a85ed6b 100644 --- a/tests/test_float_formatter.py +++ b/tests/test_float_formatter.py @@ -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): diff --git a/tests/test_float_fsml.py b/tests/test_float_fsml.py index f8d4444d..f6da115f 100644 --- a/tests/test_float_fsml.py +++ b/tests/test_float_fsml.py @@ -529,7 +529,7 @@ def test_signs(self): [ ("", "0"), ("-", "0"), - ("+", "+0"), + ("+", " 0"), (" ", " 0"), ], ), @@ -538,7 +538,7 @@ def test_signs(self): [ ("", "0"), ("-", "0"), - ("+", "+0"), + ("+", " 0"), (" ", " 0"), ], ),