如何在Javascript中将浮点数转换为其二进制表示(IEEE 754)?

28

在JavaScript中,将浮点数转换为其二进制表示的最简单方法是什么?(例如1.0 -> 0x3F800000)。

我尝试手动实现,这对于通常的数字来说可以工作,但它在非常大或非常小的数字(没有范围检查)和特殊情况(NaN,infinity等)上失败:

function floatToNumber(flt)
{
    var sign = (flt < 0) ? 1 : 0;
    flt = Math.abs(flt);
    var exponent = Math.floor(Math.log(flt) / Math.LN2);
    var mantissa = flt / Math.pow(2, exponent);

    return (sign << 31) | ((exponent + 127) << 23) | ((mantissa * Math.pow(2, 23)) & 0x7FFFFF);
}

我是否在重新发明轮子?

编辑:我已经改进了我的版本,现在它处理特殊情况。

function assembleFloat(sign, exponent, mantissa)
{
    return (sign << 31) | (exponent << 23) | (mantissa);
}

function floatToNumber(flt)
{
    if (isNaN(flt)) // Special case: NaN
        return assembleFloat(0, 0xFF, 0x1337); // Mantissa is nonzero for NaN

    var sign = (flt < 0) ? 1 : 0;
    flt = Math.abs(flt);
    if (flt == 0.0) // Special case: +-0
        return assembleFloat(sign, 0, 0);

    var exponent = Math.floor(Math.log(flt) / Math.LN2);
    if (exponent > 127 || exponent < -126) // Special case: +-Infinity (and huge numbers)
        return assembleFloat(sign, 0xFF, 0); // Mantissa is zero for +-Infinity

    var mantissa = flt / Math.pow(2, exponent);
    return assembleFloat(sign, exponent + 127, (mantissa * Math.pow(2, 23)) & 0x7FFFFF);
}

我仍然不确定这是否完全正确,但它似乎足够好地工作。 (我仍在寻找现有的实现方案)。


我很好奇你为什么要转换成IEEE单精度表示法。JavaScript数字通常不是以双精度(64位)的形式存储吗? - Mark Dickinson
你正在为小值(指数 < -126)返回无穷大;我不认为这是你想要的。 (此外,-0.0最终会出现错误的符号位,但也许对于你的应用程序来说这并不重要。) - Mark Dickinson
1
Mark Dickinson: 我正在转换为32位的IEEE格式,因为该应用程序将生成一些值,这些值将在内存编辑器中使用(该编辑器使用地址+字节的格式)。根据维基百科,"指数 < -126" 是正确的。负127以下的指数无法表示,指数等于-127(添加偏移后为0)用于表示零和次正常数(也是根据维基百科的说法)。关于-0.0,你说得对。有没有办法检查它是否为-0.0呢?与0.0进行比较会返回true。 - GameZelda
关于指数:输入可以是任何有效的IEEE 754 双精度值;使用您当前的代码,如果您的输入非常小,例如1e-60,则最终输出无穷大的二进制表示。相比之下,输出0.0的表示可能更合适,这是通过将双精度值舍入为单精度值自然获得的。要区分0.0和-0.0,我知道的唯一方法是查看atan2(flt, -1.0)的结果(如果JavaScript提供了copysign,那将是更好的方法)。 - Mark Dickinson
没错,我的代码在负指数部分是错误的(2 ^ -无穷大= 0而不是-无穷大)。我会看一下负零的问题,并稍后更新代码。谢谢! - GameZelda
3
您可以使用类型化数组完成这个功能:https://dev59.com/P2855IYBdhLWcg3wUSgW#10564792 - Janus Troelsen
2个回答

8
新技术正在使这变得更容易,可能也更加向前兼容。我喜欢扩展内置的原型,但并不是每个人都喜欢。因此,请随意修改以下代码以采用传统的过程式方法:
(function() {
    function NumberToArrayBuffer() {
        // Create 1 entry long Float64 array
        return [new Float64Array([this]).buffer];
    }
    function NumberFromArrayBuffer(buffer) {
        // Off course, the buffer must be at least 8 bytes long, otherwise this is a parse error
        return new Float64Array(buffer, 0, 1)[0];
    }
    if(Number.prototype.toArrayBuffer)  {
        console.warn("Overriding existing Number.prototype.toArrayBuffer - this can mean framework conflict, new WEB API conflict or double inclusion.");
    }
    Number.prototype.toArrayBuffer = NumberToArrayBuffer;
    Number.prototype.fromArrayBuffer = NumberFromArrayBuffer;
    // Hide this methods from for-in loops
    Object.defineProperty(Number.prototype, "toArrayBuffer", {enumerable: false});
    Object.defineProperty(Number.prototype, "fromArrayBuffer", {enumerable: false});
})();

测试:

(function() {
    function NumberToArrayBuffer() {
        // Create 1 entry long Float64 array
        return new Float64Array([this.valueOf()]).buffer;
    }
    function NumberFromArrayBuffer(buffer) {
        // Off course, the buffer must be ar least 8 bytes long, otherwise this is a parse error
        return new Float64Array(buffer, 0, 1)[0];
    }
    if(Number.prototype.toArrayBuffer)  {
        console.warn("Overriding existing Number.prototype.toArrayBuffer - this can mean framework conflict, new WEB API conflict or double inclusion.");
    }
    Number.prototype.toArrayBuffer = NumberToArrayBuffer;
    Number.fromArrayBuffer = NumberFromArrayBuffer;
    // Hide this methods from for-in loops
    Object.defineProperty(Number.prototype, "toArrayBuffer", {enumerable: false});
    Object.defineProperty(Number, "fromArrayBuffer", {enumerable: false});
})();
var test_numbers = [0.00000001, 666666666666, NaN, Infinity, -Infinity,0,-0];
console.log("Conversion symethry test: ");
test_numbers.forEach(
      function(num) {
               console.log("         ", Number.fromArrayBuffer((num).toArrayBuffer()));
      }
);

console.log("Individual bytes of a Number: ",new Uint8Array((666).toArrayBuffer(),0,8));
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>


4
基本上,它是new Uint8Array(new Float64Array([num]).buffer,0,8)。但是警告信息“将 Number 值存储到 ArrayBuffer 中后可能在其中观察到的位模式不一定与 ECMAScript 实现中使用的该 Number 值的内部表示相同”是否仅指与无关的位模式?还是它也允许位模式更改最终影响可观测值? - Pacerier
@Pacerier 这是一个非常好的问题,我需要进行一些研究,可能会提出一个更好的平台无关的解决方案。 - Tomáš Zato
@Pacerier 一些引擎将数据打包到不相关的位中,因此我认为该警告只是为了解决这些问题。如果写入Float64Array的位模式表示的是可观察到的不同值,那么我认为这将是一个错误。 - Jesse
我可能错了,但我对规范的理解是这是明确定义的。请查看ArrayBuffer部分中的NumericToRawBytes和RawBytesToNumeric算法,它们在读取和存储Float64时明确指定了IEEE-754格式。我认为关于“实现使用的内部表示……”的警告仅意味着CPU的内部浮点寄存器未指定,但必须在将位存储在ArrayBuffers中时转换为IEEE754。 - jw013

6

这里有一个函数,可以在我测试过的所有内容上运行,但它不能区分-0.0和+0.0。

它基于来自http://jsfromhell.com/classes/binary-parser的代码,但专为32位浮点数而设计,并返回整数而不是字符串。我还修改了它以使其更快和(略微)更易读。

// Based on code from Jonas Raoni Soares Silva
// http://jsfromhell.com/classes/binary-parser
function encodeFloat(number) {
    var n = +number,
        status = (n !== n) || n == -Infinity || n == +Infinity ? n : 0,
        exp = 0,
        len = 281, // 2 * 127 + 1 + 23 + 3,
        bin = new Array(len),
        signal = (n = status !== 0 ? 0 : n) < 0,
        n = Math.abs(n),
        intPart = Math.floor(n),
        floatPart = n - intPart,
        i, lastBit, rounded, j, exponent;

    if (status !== 0) {
        if (n !== n) {
            return 0x7fc00000;
        }
        if (n === Infinity) {
            return 0x7f800000;
        }
        if (n === -Infinity) {
            return 0xff800000
        }
    }

    i = len;
    while (i) {
        bin[--i] = 0;
    }

    i = 129;
    while (intPart && i) {
        bin[--i] = intPart % 2;
        intPart = Math.floor(intPart / 2);
    }

    i = 128;
    while (floatPart > 0 && i) {
        (bin[++i] = ((floatPart *= 2) >= 1) - 0) && --floatPart;
    }

    i = -1;
    while (++i < len && !bin[i]);

    if (bin[(lastBit = 22 + (i = (exp = 128 - i) >= -126 && exp <= 127 ? i + 1 : 128 - (exp = -127))) + 1]) {
        if (!(rounded = bin[lastBit])) {
            j = lastBit + 2;
            while (!rounded && j < len) {
                rounded = bin[j++];
            }
        }

        j = lastBit + 1;
        while (rounded && --j >= 0) {
            (bin[j] = !bin[j] - 0) && (rounded = 0);
        }
    }
    i = i - 2 < 0 ? -1 : i - 3;
    while(++i < len && !bin[i]);
    (exp = 128 - i) >= -126 && exp <= 127 ? ++i : exp < -126 && (i = 255, exp = -127);
    (intPart || status !== 0) && (exp = 128, i = 129, status == -Infinity ? signal = 1 : (status !== status) && (bin[i] = 1));

    n = Math.abs(exp + 127);
    exponent = 0;
    j = 0;
    while (j < 8) {
        exponent += (n % 2) << j;
        n >>= 1;
        j++;
    }

    var mantissa = 0;
    n = i + 23;
    for (; i < n; i++) {
        mantissa = (mantissa << 1) + bin[i];
    }
    return ((signal ? 0x80000000 : 0) + (exponent << 23) + mantissa) | 0;
}

5
从二进制转换为浮点数是否有简单方法?或者需要反向运用此算法? - qwertymk
3
这个有没有相应的反向算法? - Kushal Kumar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接