Repository: lmfit/uncertainties

Issue:
ZeroDivisionError when getting the __repr__ if nominal value is too small
Perhaps this is a very obscure bug but it does exist.

When running the following block of code:

```python
n, s = 1.1E-323, 0
v = uncertainties.core.Variable(n, s)
print(v)
```
It will throw a ZeroDivisionError at the print line.

I understand this is because, when converting the variable into a string using `v.__repr__()` it will attempt to factorize out the 1E-323 from both the v.n and the v.s.

It seems that python would only complain about division by zero when n is between [2.5E324, 12.5E324), making it a very specific and obscure problem, which is perhaps why I'm the first person to file an issue for it
I was working with a large enough dataset to hit upon this weird bug.

I know a few ways to workaround this problem on my own (choose all values <12.5E-324 and then set them to zero); but do you think there's a way to write the `__repr__` in such a way to avoid future occurrences of this?

Side note:
Interestingly while testing the behaviour, I found out that python rounds all numbers above 2.5E-324 to the nearest multiple of 5E-324. I guess this is the resolution of IEEE-754 numbers?

Hints from issue comments:
@lebigot: Wow, interesting.

Thanks for the concise but detailed report. A short traceback could be useful too, if you can add one.

I am not close to a computer, but I'll have a look soon: I don't like surprising behaviors. :)@OceanNuclear: Oh right, certainly. Here's the traceback.
```
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-3-d6c1535c9ec8> in <module>
----> 1 print(v)

~/miniconda3/envs/canary/lib/python3.8/site-packages/uncertainties/core.py in __str__(self)
   1841         # string
   1842         # (http://docs.python.org/2/library/string.html#format-specification-mini-language):
-> 1843         return self.format('')
   1844 
   1845     def __format__(self, format_spec):

~/miniconda3/envs/canary/lib/python3.8/site-packages/uncertainties/core.py in format(*args, **kwargs)
   2314         """)
   2315     def format(*args, **kwargs):
-> 2316         return args[0].__format__(*args[1:], **kwargs)
   2317 
   2318     def std_score(self, value):

~/miniconda3/envs/canary/lib/python3.8/site-packages/uncertainties/core.py in __format__(self, format_spec)
   2235             factor = 10.**common_exp
   2236 
-> 2237             nom_val_mantissa = nom_val/factor
   2238             std_dev_mantissa = std_dev/factor
   2239             # Limit for the last digit of the mantissas:

ZeroDivisionError: float division by zero

```
it happens inside the `AffineScalarFunc.__format__` method.@ArnaudPel: We stumbled upon the same issue recently (`uncertainties=3.2.2`).@lebigot: I'm not working on this project anymore, but there are new, active maintainers, who hopefully can look at this, but from the look of it, I'm wondering if the values that the code is manipulating aren't very small (close to underflow).

If you have a minimal example that can reproduce this, that could help.

Otherwise, just exploring this possibility (very negative `common_exp`) could be useful.@ArnaudPel: The original example provided at the beginning of the issue still raises a `ZeroDivisionError`, here is another one:

```python
from uncertainties import ufloat
a = 1e-324
b = 3e-324
assert a < b
print(ufloat(a, 0.0))  # OK
print(ufloat(b, 0.0))  # ZeroDivisionError
```

```
Traceback (most recent call last):
  File "tmp/sandbox.py", line 74, in <module>
    print(ufloat(b, 0.0))  # ZeroDivisionError
  File "lib/python3.10/site-packages/uncertainties/core.py", line 1772, in __str__
    return self.format('')
  File "lib/python3.10/site-packages/uncertainties/core.py", line 2256, in format
    return args[0].__format__(*args[1:], **kwargs)
  File "lib/python3.10/site-packages/uncertainties/core.py", line 2177, in __format__
    nom_val_mantissa = nom_val/factor
ZeroDivisionError: float division by zero
```

(Same outcome in python 3.12)

Solution patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index f273fdd..263ab52 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -30,6 +30,8 @@ Fixes:
 - adjust `codecov.io` configuration so that minor code coverage changes will not result
    in indications that tests are failing. Rather code coverage reports will be purely
    informational for code reviewers. Also fix other minor configuration issues. (#270)
+- fixes to the str representation which occassionally errors when AffineScalarFunc is
+  too small (#135)
 
 3.2.2   2024-July-08
 -----------------------
diff --git a/uncertainties/formatting.py b/uncertainties/formatting.py
index 4049feb..b3e55e6 100644
--- a/uncertainties/formatting.py
+++ b/uncertainties/formatting.py
@@ -965,18 +965,8 @@ def format_ufloat(ufloat_to_format, format_spec):
 
     if pres_type in ("f", "F"):
         use_exp = False
-    elif pres_type in ("e", "E"):
-        if not real_values:
-            use_exp = False
-        else:
-            use_exp = True
-            # !! This calculation might have been already done,
-            # for instance when using the .0e format:
-            # signif_dgt_to_limit() was called before, which
-            # prompted a similar calculation:
-            common_exp = first_digit(round(exp_ref_value, -digits_limit))
 
-    else:  # None, g, G
+    else:  # e, E, g, G, None
         # The rules from
         # https://docs.python.org/3.4/library/string.html#format-specification-mini-language
         # are applied.
@@ -997,22 +987,17 @@ def format_ufloat(ufloat_to_format, format_spec):
         # for floats is used ("-4 <= exponent of rounded value <
         # p"), on the nominal value.
 
-        if not real_values:
-            use_exp = False
-        else:
-            # Common exponent *if* used:
-            common_exp = first_digit(round(exp_ref_value, -digits_limit))
-
-            # print "COMMON EXP TEST VALUE", common_exp
-            # print "LIMIT EXP", common_exp-digits_limit+1
-            # print "WITH digits_limit", digits_limit
-
+        if use_exp := real_values:
             # The number of significant digits of the reference value
             # rounded at digits_limit is exponent-digits_limit+1:
-            if -4 <= common_exp < common_exp - digits_limit + 1:
-                use_exp = False
-            else:
-                use_exp = True
+            common_exp = first_digit(round(exp_ref_value, -digits_limit))
+            common_factor = 10.0**common_exp
+            # cases where this doesn't apply: too many digits when expressed like this,
+            # or the common factor is way too small.
+            if pres_type not in ("e", "E"):  # g, G, None
+                use_exp = not (-4 <= common_exp < common_exp - digits_limit + 1) and (
+                    common_factor != 0.0
+                )
 
     ########################################
 
@@ -1026,11 +1011,8 @@ def format_ufloat(ufloat_to_format, format_spec):
     # exponent should be used.
 
     if use_exp:
-        # Not 10.**(-common_exp), for limit values of common_exp:
-        factor = 10.0**common_exp
-
-        nom_val_mantissa = nom_val / factor
-        std_dev_mantissa = std_dev / factor
+        nom_val_mantissa = nom_val / common_factor
+        std_dev_mantissa = std_dev / common_factor
         # Limit for the last digit of the mantissas:
         signif_limit = digits_limit - common_exp
 


Test patch:
diff --git a/tests/test_formatting.py b/tests/test_formatting.py
index 1910642..5c9bfa5 100644
--- a/tests/test_formatting.py
+++ b/tests/test_formatting.py
@@ -27,6 +27,19 @@ def test_PDG_precision():
         assert formatting.PDG_precision(std_dev) == result
 
 
+def test_small_float():
+    """
+    Make sure that very small floats do not error, even though printing as str
+    causes the number to be rounded to the nearest 324.
+    Suggested by issue #135.
+    """
+    a = 1e-324
+    b = 3e-324
+    assert a < b
+    str(ufloat(a, 0.0))
+    str(ufloat(b, 0.0))
+
+
 def test_repr():
     """Test the representation of numbers with uncertainty."""