2

So I'm trying to make a function that returns a stringified decimal in the most readable format. These are the requirements I have created for myself:

  1. If the absolute exponent of the number is greater than or equal to 9, it should be presented using scientific notation (eg 25000000000 -> 2.5E+9, 0.0000000036 -> 3.6E-9)
  2. Otherwise, the number should be presented as a standard notation decimal, with minimal trailing zeros (eg 103.400000 -> 103.4, 0.000005600 -> 0.0000056)

Currently, I'm using some code that I modified from this answer, but I can't really see a way to get it to match my requirements exactly.

Currently, my solution is as follows:

def stringifyDecimal(d: Decimal):
    # If d is within a reasonable range for printing as a normal number
    a = abs(math.log10(abs(d)))
    if a < 9:
        r = d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
    else:
        r = d.normalize()
    return str(r)

This works great for larger numbers:

  • stringifyDecimal(D('1E5')) -> '10000'
  • stringifyDecimal(D('1E9')) -> '1E+9'

But for very small fractions, it will always be returned in scientific notation:

  • stringifyDecimal(D('1E-7')) -> '1E-7'

This is because of the ...else d.normalize() part of the 5th line, which returns a Decimal that will stringify to scientific notation by default. Sadly, formatting options won't work for this as they require me to know the exact precision that I want to format to.

Is there a way that I can force the Decimal type to display a very small fraction as a decimal rather than in scientific notation?

Miguel Guthridge
  • 1,444
  • 10
  • 27

2 Answers2

2

By the way, your code fails for 0 because of the log. See what you think about this. For values between 10-7 and 10-9, I multiply by 100, convert to string, then insert the two zeros:

import math
from decimal import Decimal

def stringifyDecimal(d: Decimal):
    # If d is within a reasonable range for printing as a normal number
    if not d:
        return "0"
    a = math.log10(abs(d))
    if 0 < a < 9:
        r = d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
        r = str(r)
    elif -8 < a < -6:
        r = str((d*100).normalize())
        r = r[:2] + '00' + r[2:]
    else:
        r = d.normalize()
        r = str(r)
    return r

tests = [
    "12e12", "11e11", "10e10", "9e9", "8e8", "7e7", "6e6", "5e5", "4e4", "3e3", "2e2", "1e1",
    "0",
    "1e-1", "2e-2", "3e-3", "4e-4", "5e-5", "6e-6", "7e-7", "8e-8", "9e-9", "10e-10"
]

for test in tests:
    print(stringifyDecimal(Decimal(test)))

Output:

1.2E+13
1.1E+12
1E+11
9E+9
800000000
70000000
6000000
500000
40000
3000
200
10
0
0.1
0.02
0.003
0.0004
0.00005
0.000006
0.0000007
0.00000008
9E-9
1E-9
Tim Roberts
  • 48,973
  • 4
  • 21
  • 30
  • This looks pretty great actually! One small fix might be to replace `elif -8 < a < -6:` with `elif -8 <= a < -6:`, as otherwise it won't work correctly for the input `1E-8`. Thanks so much!!! – Miguel Guthridge Jul 03 '21 at 05:18
  • Thankfully, I didn't need to worry about the leading zero as that is dealt with earlier in the program (when it tries to present as a fraction first) – Miguel Guthridge Jul 03 '21 at 05:21
1

Ok so the most effective method I've found is to use string formatting.

d = Decimal('152')
print(f"{d:f}") # Format as a standard decimal
print(f"{d:e}") # Format in scientific notation

from this, my solution is as follows:

def strDecimal_Sci(d: Decimal) -> str:
    return f"{d.normalize():e}"

def strDecimal_Norm(d: Decimal) -> str:
    return f"{d.normalize():f}"

def stringifyDecimal(d: Decimal) -> str:
    if d == 0: return "0"
    a = abs(math.log10(abs(d)))
    if a < 9:
        return strDecimal_Norm(d)
    else:
        return strDecimal_Sci(d)
Miguel Guthridge
  • 1,444
  • 10
  • 27