21

Are there functions in the Python library or numpy that take a float as input and return its decimal scientific notation decomposition, i.e. mantissa and exponent? Or is there a BRIEF way to accomplish this without resorting to string conversions or using a for loop to determine the exponent? Writing such a function wouldn't be difficult, I'm just shocked that I'm having trouble finding an existing one in math, decimal or numpy.

e.g. if fexp and fman are the functions giving the exponent and mantissa of the decimal floating point representation of a float then we'd expect the following statements to all return true:

fexp(154.3) == 2.0
fman(154.3) == 1.543
fexp(-1000) == 3.0
fman(-1000) == -1.0

In short, this would be a "decimal version" of math.frexp.

jpm
  • 697
  • 2
  • 5
  • 19
  • Can you give sample input and expected output for a few situations? – tadman Jul 26 '17 at 16:02
  • So whats exactly wrong with frexp? Seems to me its the most obvious starting point. – jsalonen Jul 26 '17 at 17:09
  • 1
    does frexp have a base 10 option? – jpm Jul 26 '17 at 17:09
  • Would you accept any rounding errors? And no, its doesnt have that option. – jsalonen Jul 26 '17 at 17:14
  • i was thinking about rounding errors but that's kinda outside of the gist of the question. ideally there'd be a function that returns a number with a floating point representation that matches the floating point representation of the number we expect, but rounding errors are acceptable – jpm Jul 26 '17 at 17:17

2 Answers2

23

One way to avoid string conversions is to implement the methods using Decimals:

from decimal import Decimal

def fexp(number):
    (sign, digits, exponent) = Decimal(number).as_tuple()
    return len(digits) + exponent - 1

def fman(number):
    return Decimal(number).scaleb(-fexp(number)).normalize()

Note that using floating point numbers, its not possible to calculate mantissa and exponent without rounding. The reason is that floating point numbers are stored as base 2 fractions. For example stored float value for 154.3 is 154.30000000000001136868377216160297393798828125. Floats are displayed in console as accurate numbers, because (in CPython) they are always rounded when serialized using a hard-coded precision of 17.

jsalonen
  • 29,593
  • 15
  • 91
  • 109
  • I think this is a better approach since it doesn't require mathematical operations, and it's how we would do it in our head. But I would modify fman to use a similar approach as fexp with the Decimal package. That is, is there a way to get it without strings or arithmetic? – jpm Jul 31 '17 at 17:09
  • Like you said, rounding errors will be present whenever a value is converted to float. But maybe creating a decimal object for fman where the sign and digits are the same but exponent = len(digits)-1, then we convert that number to a float if needed – jpm Jul 31 '17 at 17:13
  • Updated fman to use methods from Decimal class (still not yet perfect but hopefully more cleaner to the point). Also, regarding precision: for as long as you use decimals, there won't be any rounding problems. So yeah, for as long as you avoid floats, its always precise (although not as quick or space-efficient as floats). – jsalonen Aug 01 '17 at 17:44
  • 1
    Your bold statement is technically wrong. All base-2 numbers can be exactly represented in base 10 (though the exponent means that floats may use over 1000 digits in the process) – o11c Aug 26 '17 at 04:49
  • @o11c Feel free to suggest more accurate wording as an edit. – jsalonen Aug 27 '17 at 09:55
  • 3
    @jsalonen I suggest to remove the note about rounding. In your example 154.3 is stored in memory as EXACTLY 154.30000000000001136868377216160297393798828125 not roughly. Base-2 floating point numbers cannot hold Base-10 ones exactly but it is not true for Base-10 FP numbers storing Base-2 ones. The reason is simple arithmetic. Every 2^-N can be represented exactly in Base-10 (0.5, 0.25, 0.125, etc) and a final result is just a sum of them. Otherwise great answer. As the question is about decomposing floats, a statement about composing them can lead to misunderstanding. – Igor Mikushkin Jan 25 '18 at 23:20
10

I'm hoping there's a better answer but I came up with

from math import floor, log10

def fexp(f):
    return int(floor(log10(abs(f)))) if f != 0 else 0

def fman(f):
    return f/10**fexp(f)
jpm
  • 697
  • 2
  • 5
  • 19