23

Is there a reasonably fast way to extract the exponent and mantissa from a Number in Javascript?

AFAIK there's no way to get at the bits behind a Number in Javascript, which makes it seem to me that I'm looking at a factorization problem: finding m and n such that 2^n * m = k for a given k. Since integer factorization is in NP, I can only assume that this would be a fairly hard problem.

I'm implementing a GHC plugin for generating Javascript and need to implement the decodeFloat_Int# and decodeDouble_2Int# primitive operations; I guess I could just rewrite the parts of the base library that uses the operation to do wahtever they're doing in some other way (which shouldn't be too hard since all numeric types have Number as their representation anyway,) but it'd be nice if I didn't have to.

Is there any way to do this in an even remotely performant way, by some dark Javascript voodoo, clever mathematics or some other means, or should I just buckle down and have at the base library?

EDIT Based on ruakh's and Louis Wasserman's excellent answers, I came up with the following implementation, which seems to work well enough:

function getNumberParts(x) {
    if(isNaN(x)) {
        return {mantissa: -6755399441055744, exponent: 972};
    }
    var sig = x > 0 ? 1 : -1;
    if(!isFinite(x)) {
        return {mantissa: sig * 4503599627370496, exponent: 972};
    }
    x = Math.abs(x);
    var exp = Math.floor(Math.log(x)*Math.LOG2E)-52;
    var man = x/Math.pow(2, exp);
    return {mantissa: sig*man, exponent: exp};
}
valderman
  • 8,365
  • 4
  • 22
  • 29
  • 2
    Your solution seems risky to me. In particular, I'm very suspicious of small rounding errors in `Math.log(x) * Math.LOG2E` giving you something like `5.99999996` and the `floor` truncating that down to `5`. Go ahead and do the logarithm yourself; it'll be much less prone to rounding errors. – Louis Wasserman Feb 21 '12 at 20:59
  • @LouisWasserman: that is a very good point; I'll better do that. Thanks! – valderman Feb 21 '12 at 21:09

8 Answers8

32

Using the new ArrayBuffer access arrays, it is actually possible to retrieve the exact mantissa and exponent, by extracting them from the Uint8Array. If you need more speed, consider reusing the Float64Array.

function getNumberParts(x)
{
    var float = new Float64Array(1),
        bytes = new Uint8Array(float.buffer);

    float[0] = x;

    var sign = bytes[7] >> 7,
        exponent = ((bytes[7] & 0x7f) << 4 | bytes[6] >> 4) - 0x3ff;

    bytes[7] = 0x3f;
    bytes[6] |= 0xf0;

    return {
        sign: sign,
        exponent: exponent,
        mantissa: float[0],
    }
}

I've also created some test cases. 0 fails, since there is another representation for 2^-1023.

var tests = [1, -1, .123, -.123, 1.5, -1.5, 1e100, -1e100, 
                    1e-100, -1e-100, Infinity, -Infinity];

tests.forEach(function(x)
{
    var parts = getNumberParts(x),
        value = Math.pow(-1, parts.sign) *
                    Math.pow(2, parts.exponent) *
                    parts.mantissa;

    console.log("Testing: " + x + " " + value);
    console.assert(x === value);
});

console.log("Tests passed");
Bakudan
  • 19,134
  • 9
  • 53
  • 73
copy
  • 3,301
  • 2
  • 29
  • 36
  • this solution will work event on iPad, where denormalized numbers are not supported – 4esn0k Jul 19 '13 at 11:41
  • This returns a floating-point mantissa for me. Isn't the point to return an integral mantissa? – Jonathan Wilbur Jan 11 '20 at 17:38
  • how do you return an integral number in a language that don't have it ? With this very same method you can implement your own number types btw :D – L.Trabacchin Sep 10 '20 at 14:03
  • @JonathanWilbur This algorithm doesn't give you the bits. It gives you what they represent. The mantissa is defined as a number between 1 and 2. Such that mantissa = 1,b51 b50... b0 base 2. https://en.wikipedia.org/wiki/Double-precision_floating-point_format – richardpj Feb 08 '21 at 12:56
6

ECMAScript doesn't define any straightforward way to do this; but for what it's worth, this isn't a "factorization problem" in the same sense as prime factorization.

What you want can theoretically be done very quickly by first handling the sign, then using a binary-tree approach (or logarithm) to find the exponent, and lastly dividing by the relevant power of two to get the mantissa; but unfortunately, it can be somewhat tricky to implement this in practice (what with special cases such as denormalized numbers). I recommend you read through section 8.5 of the ECMAScript specification to get a sense of what cases you'll have to handle.

ruakh
  • 175,680
  • 26
  • 273
  • 307
  • 1
    Factorization is unquestionably in NP, it's just not believed to be NP-hard. You have them mixed up. – Louis Wasserman Feb 21 '12 at 19:37
  • @LouisWasserman: Hmm, I'm finding conflicting information online. I'll just remove that portion of my answer, since it's not really important. Thanks. – ruakh Feb 21 '12 at 19:46
  • Thank you; however, won't this method (at least if the base 2 logarithm is used to obtain the exponent) give a non-integer mantissa for most numbers? – valderman Feb 21 '12 at 19:56
  • @valderman: That's a good question. I'm used to thinking of the mantissa as a value in the range [1.0, 2.0) and the exponent as an integer in the range [-1022, +1023], but EMCAScript presents numbers in terms of an integer in the range [2^52, 2^53) and an integer in the range [-1074, +971] (and therefore avoids the terms "mantissa" and "exponent"), and Haskell *seems* to do the same (though it does use those terms). Even so, this is easily addressed by incorporating an offset of -52 after performing the logarithm. – ruakh Feb 21 '12 at 20:08
  • I updated my answer with code that emulates Haskell's built-in `decodeFloat` operation...but for reference, http://en.wikipedia.org/wiki/NP_(complexity)#Examples correctly states that factorization is in NP. – Louis Wasserman Feb 21 '12 at 20:13
  • 1
    @ruakh: NP is, to put it simply, the class of problems whose solutions may be checked in polynomial time. Multiplication takes polynomial time, therefore factorization is in NP. – hammar Feb 21 '12 at 20:51
  • @hammar: Yeah, I'm not so sure. There seem to be multiple opinions about what "factorization" means (does it mean "finding out if there is any factor between 1 and *n*"? does it mean "finding all prime factors"? etc.), and therefore, multiple statements that are all true, but superficially mutually contradictory, about the complexity of "factorization". (And anyway, it doesn't make sense to describe a problem as "fairly hard" on the basis of its being "in NP", since NP only describes an *upper* bound on complexity, such that the *least*-complex algorithms are all in NP.) – ruakh Feb 21 '12 at 21:19
  • @hammar: Also, I'd challenge the statement that "Multiplication takes polynomial time, therefore factorization is in NP". Even if the conclusion is true, the first does not trivially entail the latter. After all, if a number has *n* bits, then naively checking all factors would require O(2 ^ *n*) multiplications, which is *not* in NP. (We could define "NP" to be in terms of the number itself, rather than its number of bits -- then it would certainly be in NP -- but that doesn't seem to be common practice for this family of problems.) – ruakh Feb 21 '12 at 21:22
  • @ruakh: I think you misunderstood me. We only have to be able to _check_ the solution, e.g. given a factorization `n = x1 * ... * xk`, we can verify the factorization in polynomial time by multiplying the factors together and checking that the result is equal to `n`. Of course, finding the factors in the first place is the part which may be difficult, but that's not what the NP class is concerned with. – hammar Feb 21 '12 at 21:41
  • @hammar: The OP's goal was not to *check* that a given mantissa and exponent yield the specified number, but to *find* a mantissa and exponent that do. I therefore assume that by "integer factorization", (s)he was referring to the process of factorizing an integer. – ruakh Feb 21 '12 at 21:44
  • @ruakh: I was simply trying to clear up the confusion about the NP class. You initially wrote that factorization was not in NP, so I tried to give a simple argument for why it is in fact in NP, though I agree this isn't really related to the original question. – hammar Feb 21 '12 at 21:49
  • @hammar: Ah, fair enough. I don't think I was confused, but I can see why you and Louis Wasserman thought that I was, and I appreciate your trying to clear it up. :-) – ruakh Feb 21 '12 at 22:03
6

Integer factorization is nowhere near necessary for this.

The exponent is basically going to be the floor of the base-2 logarithm, which isn't that hard to compute.

The following code passes QuickCheck tests, as well as tests on infinity and negative infinity:

minNormalizedDouble :: Double
minNormalizedDouble = 2 ^^ (-1022)

powers :: [(Int, Double)]
powers = [(b, 2.0 ^^ fromIntegral b) | i <- [9, 8..0], let b = bit i]

exponentOf :: Double -> Int
exponentOf d
  | d < 0   = exponentOf (-d)
  | d < minNormalizedDouble = -1024
  | d < 1   = 
      let go (dd, accum) (p, twoP)
            | dd * twoP < 1 = (dd * twoP, accum - p)
            | otherwise = (dd, accum)
      in snd $ foldl' go (d, 0) powers
  | otherwise   =
      let go (x, accum) (p, twoP)
            | x * twoP <= d = (x * twoP, accum + p)
            | otherwise = (x, accum)
    in 1 + (snd $ foldl' go (1.0, 0) powers)


decode :: Double -> (Integer, Int)
decode 0.0 = (0, 0)
decode d
  | isInfinite d, d > 0 = (4503599627370496, 972)
  | isInfinite d, d < 0 = (-4503599627370496, 972)
  | isNaN d             = (-6755399441055744, 972)
  | otherwise       =
      let
        e = exponentOf d - 53
        twoE = 2.0 ^^ e
         in (round (d / twoE), e)

I tested it using quickCheck (\ d -> decodeFloat d == decode d), and explicitly tested it separately on positive and negative infinities.

The only primitive operations used here are left-shift, double multiplication, double division, and infinity and NaN testing, which Javascript supports to the best of my knowledge.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • Yep. I would've had it up sooner if Haskell's `decodeFloat` operation wasn't so far from what I'd expected! I've been doing a _lot_ of IEEE754 hackery in Java lately, though, which helped. – Louis Wasserman Feb 21 '12 at 20:20
  • Your `minNormalizedDouble` is actually denormalized, the smallest positive normalized `Double` is `0.5 ^ 1022`. Due to `a ^^ b = 1/(a ^ (-b))` for `b < 0`, the exponentiation overflows for `0 < d < minNormalizedDouble`, giving nonsense results (you're rounding `Infinity`). `let e = exponentOf d - 53; twoE | e < 0 = 0.5 ^ (-e) | otherwise = 2 ^ e` fixes that. In what way is `decodeFloat` far from what you expected? – Daniel Fischer Feb 21 '12 at 21:01
  • Fixed. I guess I feel like I expect `Int64` to get involved rather than `Integer`; in particular, I'd like a way to get the bits of the double directly in an `Int64`. – Louis Wasserman Feb 21 '12 at 21:18
4

For base 10 you can get mantissa and exponent in an array with

   var myarray = (number.toExponential() + '').split("e");
   // then ...
   var mantissa = parseFloat(myarray[0]);
   var exponent = parseInt(myarray[1]);

If you don't care if the result parts are text instead of numbers and if there might be a plus sign on the front of the exponent part, then you can skip the parseFloat and parseInt steps and just take the parts directly from the array at [0] and [1].

3

While I liked the accepted solution, using it to work on arbitrary base re-introduced all of the errors caused by Math.log and Math.pow. Here is a small implementation for any base: x = mantisse * b^exponent

function numberParts(x, b) {
  var exp = 0
  var sgn = 0
  if (x === 0) return { sign: 0, mantissa: 0, exponent: 0 }
  if (x<0) sgn=1, x=-x
  while (x>b) x/=b, exp++
  while (x<1) x*=b, exp--
  return { sign: sgn, mantissa: x, exponent: exp }
}

The NaN and Infinite cases can easily be added. If the distinction between +0 and -0 is important:

if (1/x === Infinity) return { sign: 0, mantissa: 0, exponent: 0 }
if (1/x === -Infinity) return { sign: 1, mantissa: 0, exponent: 0 }
Hurelu
  • 1,458
  • 1
  • 14
  • 23
  • Oh gosh finally found this. I've seen your answer 10 days ago but used `log`/`pow` anyway. Then I started having rounding issues and went looking for your answer. I googled for every relevant keyword I could remember, read all answers in tons of similar questions, then spent half a minute looking through my whole browse history. Never so glad for reading 8 lines of code. Thank you :) – MaiaVictor Aug 28 '16 at 04:02
  • Please note that there is an error in case x is zero! You should catch that using an if. – Girts Strazdins Jan 30 '17 at 06:25
  • 2
    @user1703497 thanks, made some edits for the common `0` case and the less common `-0` and `+0` cases – Hurelu Jan 30 '17 at 07:57
  • 3
    ... found it again. Damn, how many times I'll need this on my life? – MaiaVictor Aug 12 '17 at 07:29
  • 1
    This is the fastest of all the solutions by quite a margin as of 7/28/2021. Comparison: https://jsbench.me/hokro6nel4/1 – brainbag Jul 29 '21 at 00:37
3

What about following to get the exponent?:

let exp = String(number.toExponential());
exp = Number(exp.substr(exp.lastIndexOf('e')+1));

1000 will result in exp = 3

velop
  • 3,102
  • 1
  • 27
  • 30
1

My Haskell is non-existent. Here's a solution in JavaScript. As others have noted, the key is to calculate the binary logarithm to get the exponent.

From http://blog.coolmuse.com/2012/06/21/getting-the-exponent-and-mantissa-from-a-javascript-number/


function decodeIEEE64 ( value ) {

    if ( typeof value !== "number" )
        throw new TypeError( "value must be a Number" );

    var result = {
        isNegative : false,
        exponent : 0,
        mantissa : 0
    };

    if ( value === 0 ) {

        return result;
    }

    // not finite?
    if ( !isFinite( value ) ) {

        result.exponent = 2047;

        if ( isNaN( value ) ) {

            result.isNegative = false;
            result.mantissa = 2251799813685248; // QNan

        } else {

            result.isNegative = value === -Infinity;
            result.mantissa = 0;

        }

        return result;
    }

    // negative?
    if ( value < 0 ) {
        result.isNegative = true;
        value = -value;
    }

    // calculate biased exponent
    var e = 0;
    if ( value >= Math.pow( 2, -1022 ) ) {   // not denormalized

        // calculate integer part of binary logarithm
        var r = value;

        while ( r < 1 )  { e -= 1; r *= 2; }
        while ( r >= 2 ) { e += 1; r /= 2; }

        e += 1023;  // add bias
    }
    result.exponent = e;

    // calculate mantissa
    if ( e != 0 ) {

        var f = value / Math.pow( 2, e - 1023 );
        result.mantissa = Math.floor( (f - 1) * Math.pow( 2, 52 ) );

    } else { // denormalized

        result.mantissa = Math.floor( value / Math.pow( 2, -1074 ) );

    }

    return result;
}
Monroe Thomas
  • 4,962
  • 1
  • 17
  • 21
-1

If you only need mantissa length,

Number.prototype.mantissaLength = function(){
    var m = this.toString(), d = m.indexOf('.') + 1;
    return d? m.length - d:0;
}

var x = 1234.5678;
var mantL = x.mantissaLength();
adarsh
  • 6,738
  • 4
  • 30
  • 52
Lewiss
  • 17
  • 2