Repository: jagerber48/sciform

Issue:
`0` and sig fig rounding
#180 ensured that when sig fig rounding falls back onto the value, due to the uncertainty being invalid (0, `nan`, or `inf`) then if the value is also 0 it will be displayed as `"0"` instead of, e.g. `"0.00"`. However, direct value rounding still has the issue. Presently we have
```
print(format(SciNum(0), "!6f"))
# 0.00000
```
But we would prefer
```
print(format(SciNum(0), "!6f"))
# 0
```

The justification is that `0` does not have any significant figures, similar to `inf` or `nan`, so it is unreasonable to pad with extra zeros to try to fake the presence of significant figures.

Discussion:

Solution patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7d458325..fe7a1574 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -33,12 +33,20 @@ Changed
   accepts integers.
   [`#178 <https://github.com/jagerber48/sciform/issues/178>`_,
   `#185 <https://github.com/jagerber48/sciform/issues/185>`_]
-* Previously during value/uncertainty formatting, if the uncertainty was invalid
-  (0, ``nan`` or ``inf``) and the value was 0, the number would be formatted
-  with as many digits as significant figures requested, e.g. ``0.00 ± nan``.
-  Now a zero value is always shown with a single digit, e.g. ``0 ± nan``.
-  The justification for this is that ``0`` has no significant figures so it
-  doesn't make sense to fake significant figures by showing additional digits.
+* Previously, significant figure rounding with a value of 0 would result in trailing
+  zeros being added in the formatted output.
+  For example, ``format(SciNum(0), "!3f"`` would give ``"0.00"`` or
+  ``format(SciNum(0, float("nan")), "!2f")`` would give ``"0.0 ± nan"``.
+  This is not reasonable because zero doesn't have any significant figures, so it
+  doesn't make sense to add more trailing zeros to indicate additional "fake"
+  significant figures.
+  Now if zero is used for significant figure rounding it always appears directly as 0.
+  E.g. ``format(SciNum(0), "!3f"`` gives ``"0"`` and
+  ``format(SciNum(0, float("nan")), "!2f")`` gives ``"0 ± nan"``.
+  Note that, as before, trailing zeros may still be added to a zero value if the
+  uncertainty is less than one, e.g. ``format(SciNum(0, 0.0012), "!2f")`` gives
+  ``"0.0000 ± 0.0012"``.
+  [`#189 <https://github.com/jagerber48/sciform/issues/189>`_]
 * Previously the backend ``FinalizedOptions`` class ran a validation check on
   itself after initialization.
   This check has been removed in favor of not complicating the validation code
diff --git a/src/sciform/format_utils/numbers.py b/src/sciform/format_utils/numbers.py
index 2480595d..9cd6d0e0 100644
--- a/src/sciform/format_utils/numbers.py
+++ b/src/sciform/format_utils/numbers.py
@@ -20,7 +20,7 @@
 
 def get_top_dec_place(num: Decimal) -> int:
     """Get the decimal place of a decimal's most significant digit."""
-    if not num.is_finite() or num == 0:
+    if not num.is_finite():
         return 0
     _, digits, exp = num.normalize().as_tuple()
     return len(digits) + exp - 1
diff --git a/src/sciform/format_utils/rounding.py b/src/sciform/format_utils/rounding.py
index 0b52041f..3b9d0267 100644
--- a/src/sciform/format_utils/rounding.py
+++ b/src/sciform/format_utils/rounding.py
@@ -25,6 +25,8 @@ def get_pdg_round_digit(num: Decimal) -> int:
     if not num.is_finite():
         msg = f"num must be finite, not {num}."
         raise ValueError(msg)
+    if num == 0:
+        return 0
 
     top_dec_place = get_top_dec_place(num)
 
@@ -65,7 +67,10 @@ def get_round_dec_place(
     elif round_mode is RoundModeEnum.PDG:
         round_digit = get_pdg_round_digit(num)
     elif round_mode is RoundModeEnum.SIG_FIG:
-        round_digit = get_top_dec_place(num) - (ndigits - 1)
+        if num == 0:
+            round_digit = 0
+        else:
+            round_digit = get_top_dec_place(num) - (ndigits - 1)
     elif round_mode is RoundModeEnum.DEC_PLACE:
         round_digit = -ndigits
     else:
@@ -92,7 +97,7 @@ def round_val_unc(
             val_rounded = round(val, -round_digit)
         else:
             val_rounded = val
-    elif val.is_finite() and val != 0:
+    elif val.is_finite():
         round_digit = get_round_dec_place(
             val,
             round_mode,


Test patch:
diff --git a/tests/feature/test_val_fsml.py b/tests/feature/test_val_fsml.py
index 0af4d425..a8613e80 100644
--- a/tests/feature/test_val_fsml.py
+++ b/tests/feature/test_val_fsml.py
@@ -414,21 +414,21 @@ def test_zero(self):
                     (".3e", "0.000e+00"),
                     (".3r", "0.000e+00"),
                     ("#.3r", "0.000e+00"),
-                    ("!3", "0.00"),
-                    ("!3f", "0.00"),
-                    ("!3e", "0.00e+00"),
-                    ("!3r", "0.00e+00"),
-                    ("#!3r", "0.00e+00"),
+                    ("!3", "0"),
+                    ("!3f", "0"),
+                    ("!3e", "0e+00"),
+                    ("!3r", "0e+00"),
+                    ("#!3r", "0e+00"),
                     ("0=2.3", "000.000"),
                     ("0=2.3f", "000.000"),
                     ("0=2.3e", "000.000e+00"),
                     ("0=2.3r", "000.000e+00"),
                     ("0=#2.3r", "000.000e+00"),
-                    ("0=2!3", "000.00"),
-                    ("0=2!3f", "000.00"),
-                    ("0=2!3e", "000.00e+00"),
-                    ("0=2!3r", "000.00e+00"),
-                    ("0=#2!3r", "000.00e+00"),
+                    ("0=2!3", "000"),
+                    ("0=2!3f", "000"),
+                    ("0=2!3e", "000e+00"),
+                    ("0=2!3r", "000e+00"),
+                    ("0=#2!3r", "000e+00"),
                 ],
             ),
         ]
diff --git a/tests/feature/test_val_unc_fsml.py b/tests/feature/test_val_unc_fsml.py
index b824c80c..3f3d4f20 100644
--- a/tests/feature/test_val_unc_fsml.py
+++ b/tests/feature/test_val_unc_fsml.py
@@ -450,3 +450,14 @@ def test_prefix(self):
             ),
         ]
         self.run_val_unc_fsml_cases(cases_list)
+
+    def test_zero(self):
+        cases_list = [
+            (
+                (0, float("nan")),
+                [
+                    ("!3f", "0 ± nan"),
+                ],
+            ),
+        ]
+        self.run_val_unc_fsml_cases(cases_list)