Convert "float" to bytes in Javascript without Float32Array
Asked Answered
N

4

8

Okay so I'm an a fairly annoying situation where I don't have access to typed arrays such as Float32Array, but still need to be able to convert a Javascript number into bytes. Now, an integer I can handle just fine, but I have no idea how to do it for a floating point value.

I've solved the problem of doing it the other way around (bytes into a float), but documentation on converting from float to bytes is pretty scarce, as most language just let you read the pointer or have common classes for handling it.

Ideally I'd like to be able to convert floats into both 4-byte and 8-byte representations, and choose which one to use. However, code that can simply take a number and spit it out as 8-bytes would still be great, as I can probably come up with the 32-bit version myself from there.

Nocturn answered 10/4, 2013 at 20:3 Comment(2)
toString(2) will help? (42).toString(2)Nightcap
You might also find some use of Number.toExponentialBatey
N
10

Okay, so I actually figured it out, so I'll share my solution for single and double precision. Now I can't guarantee that they're 100% standards compliant, but they require no loops and seem to work just fine:

Single precision (given a decimal value outputs a single 32-bit big endian integer with the binary representation):

function toFloat32(value) {
    var bytes = 0;
    switch (value) {
        case Number.POSITIVE_INFINITY: bytes = 0x7F800000; break;
        case Number.NEGATIVE_INFINITY: bytes = 0xFF800000; break;
        case +0.0: bytes = 0x40000000; break;
        case -0.0: bytes = 0xC0000000; break;
        default:
            if (Number.isNaN(value)) { bytes = 0x7FC00000; break; }

            if (value <= -0.0) {
                bytes = 0x80000000;
                value = -value;
            }

            var exponent = Math.floor(Math.log(value) / Math.log(2));
            var significand = ((value / Math.pow(2, exponent)) * 0x00800000) | 0;

            exponent += 127;
            if (exponent >= 0xFF) {
                exponent = 0xFF;
                significand = 0;
            } else if (exponent < 0) exponent = 0;

            bytes = bytes | (exponent << 23);
            bytes = bytes | (significand & ~(-1 << 23));
        break;
    }
    return bytes;
};

Double precision (given a decimal value outputs two 32-bit integers with the binary representation in big-endian order):

function toFloat64(value) {
    if ((byteOffset + 8) > this.byteLength) 
        throw "Invalid byteOffset: Cannot write beyond view boundaries.";

    var hiWord = 0, loWord = 0;
    switch (value) {
        case Number.POSITIVE_INFINITY: hiWord = 0x7FF00000; break;
        case Number.NEGATIVE_INFINITY: hiWord = 0xFFF00000; break;
        case +0.0: hiWord = 0x40000000; break;
        case -0.0: hiWord = 0xC0000000; break;
        default:
            if (Number.isNaN(value)) { hiWord = 0x7FF80000; break; }

            if (value <= -0.0) {
                hiWord = 0x80000000;
                value = -value;
            }

            var exponent = Math.floor(Math.log(value) / Math.log(2));
            var significand = Math.floor((value / Math.pow(2, exponent)) * Math.pow(2, 52));

            loWord = significand & 0xFFFFFFFF;
            significand /= Math.pow(2, 32);

            exponent += 1023;
            if (exponent >= 0x7FF) {
                exponent = 0x7FF;
                significand = 0;
            } else if (exponent < 0) exponent = 0;

            hiWord = hiWord | (exponent << 20);
            hiWord = hiWord | (significand & ~(-1 << 20));
        break;
    }

    return [hiWord, loWord];
};

Apologies for any mistakes in copy/pasting, also the code ommits any handling of endianness, though it's fairly easy to add.

Thanks to everyone posting suggestions, but I ended up figuring out mostly on my own, as I wanted to avoid looping as much as possible for speed; it's still not exactly blazingly fast but it'll do =)

Nocturn answered 16/4, 2013 at 17:23 Comment(0)
G
1

See BinaryParser.encodeFloat here.

Grovel answered 10/4, 2013 at 20:37 Comment(1)
This library uses with (and I'm sure a few other no-nos) and thus is not strict compliant! Be cautious when using!Gravestone
G
1

You can use a JavaScript implementation of IEEE 754 like the one at http://ysangkok.github.io/IEEE-754/index.xhtml . It uses Emscripten and gmp.js.

Gonick answered 16/1, 2014 at 17:4 Comment(0)
M
0

The following code should be fully spec-compliant (supports -0 (in fact, the previous answer returned an incorrect result even for positive zero), supports correct rounding unlike currently most upvoted answer, supports subnormal numbers). It supports half-precision floats (16-bit), single-precision (32-bit), and double-precision (64-bit).

const minInfinity16 = (2 - 2 ** -11) * 2 ** 15, minNormal16 = (1 - 2 ** -11) * 2 ** -14, recMinSubnormal16 = 2 ** 10 * 2 ** 14, recSignificandDenom16 = 2 ** 10;
const minInfinity32 = (2 - 2 ** -24) * 2 ** 127, minNormal32 = (1 - 2 ** -24) * 2 ** -126, recMinSubnormal32 = 2 ** 23 * 2 ** 126, recSignificandDenom32 = 2 ** 23;
const minNormal64 = 2 * 2 ** -1023, recSignificandDenom64 = 2 ** 52, recMinSubnormal64Multiplier = 2 ** 1022;
const p32 = 2 ** 32;

function roundTiesToEven(num) {
    const r = Math.round(num);
    return (r === num + 0.5) && (r % 2 === 1) ? r - 1 : r;
}

function toFloat16(value) {
    if (Number.isNaN(value)) return 0x7e00; // NaN
    if (value === 0) return (1 / value === -Infinity) << 15; // +0 or -0
    const neg = value < 0;
    if (neg) value = -value;
    if (value >= minInfinity16) return neg << 15 | 0x7c00; // Infinity
    if (value < minNormal16) return neg << 15 | roundTiesToEven(value * recMinSubnormal16); // subnormal
    // normal
    const exponent = Math.log2(value) | 0;
    if (exponent === -15) return neg << 15 | recSignificandDenom16; // we round from a value between 2 ** -15 * (1 + 1022/1024) (the largest subnormal) and 2 ** -14 * (1 + 0/1024) (the smallest normal) to the latter (former impossible because of the subnormal check above)
    const significand = roundTiesToEven((value * 2 ** -exponent - 1) * recSignificandDenom16);
    if (significand === recSignificandDenom16) return neg << 15 | exponent + 16 << 10; // we round from a value between 2 ** n * (1 + 1023/1024) and 2 ** (n + 1) * (1 + 0/1024) to the latter
    return neg << 15 | exponent + 15 << 10 | significand;
}

function toFloat32(value) {
    if (Number.isNaN(value)) return 0x7fc00000; // NaN
    if (value === 0) return (1 / value === -Infinity) << 31; // +0 or -0
    const neg = value < 0;
    if (neg) value = -value;
    if (value >= minInfinity32) return neg << 31 | 0x7f800000; // Infinity
    if (value < minNormal32) return neg << 31 | roundTiesToEven(value * recMinSubnormal32); // subnormal
    // normal
    const exponent = Math.log2(value) | 0;
    if (exponent === -127) return neg << 31 | recSignificandDenom32;
    const significand = roundTiesToEven((value * 2 ** -exponent - 1) * recSignificandDenom32);
    if (significand === recSignificandDenom32) return neg << 31 | exponent + 128 << 23;
    return neg << 31 | exponent + 127 << 23 | significand;
}

function toFloat64(value) { // big-endian
    if (Number.isNaN(value)) return [0x7ff80000, 0]; // NaN
    if (value === 0) return [(1 / value === -Infinity) << 31, 0]; // +0 or -0
    const neg = value < 0;
    if (neg) value = -value;
    if (value === Infinity) return [neg << 31 | 0x7ff00000, 0]; // Infinity
    if (value < minNormal64) { // subnormal
        const significand = value * recSignificandDenom64 * recMinSubnormal64Multiplier;
        return [neg << 31 | significand / p32, significand | 0];
    }
    // normal
    const exponent = Math.log2(value) | 0;
    const significand = (value * 2 ** -exponent - 1) * recSignificandDenom64;
    return [neg << 31 | exponent + 1023 << 20 | significand / p32, significand | 0];
}

Performance optimizations welcome!

Mclane answered 2/7, 2024 at 9:0 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.