【问题标题】:Byte Array to Uint64 as a String字节数组到 Uint64 作为字符串
【发布时间】:2017-08-02 08:51:21
【问题描述】:

让我们考虑以下情况。

Go 例程创建一个字节数组,其中将 Uint64 数字 5577006791947779410 打包成 8 个字节的 Big Endian [77, 101, 130, 33, 7, 252, 253, 82]。

在 JavaScript 代码中,我将这些字节作为Uint8Array 接收。我们知道 JavaScript 目前不支持 Uint64 作为安全的数字类型,并且不能对大于 32 位的整数执行按位运算,所以像 buf[0] << 56 这样的东西永远不会起作用。

那么将这些字节直接解码为数字字符串"5577006791947779410"的过程是什么?

P.S. 我知道有 plenty of libraries 用于在 JavaScript 中处理大整数,但通常它们很大并且提供大量数学运算,我不需要这里。我正在寻找一种简单的现代直接解决方案,用于just 将 BE-packed Uint64 和 Int64 字节解码为数字字符串。你有什么想法吗?

【问题讨论】:

    标签: javascript string ecmascript-6 int64 uint64


    【解决方案1】:

    编辑:对于转换 (U)int64,我现在肯定会推荐 @LS_DEV 的解决方案。只有在字节数未知或更大时,我才会使用我的解决方案。

    我从https://stackoverflow.com/a/21668344/3872370开始并修改了它:

    function Int64ToString(bytes, isSigned) {
      const isNegative = isSigned && bytes.length > 0 && bytes[0] >= 0x80;
      const digits = [];
      bytes.forEach((byte, j) => {
        if(isNegative)
          byte = 0x100 - (j == bytes.length - 1 ? 0 : 1) - byte;
        for(let i = 0; byte > 0 || i < digits.length; i++) {
          byte += (digits[i] || 0) * 0x100;
          digits[i] = byte % 10;
          byte = (byte - digits[i]) / 10;
        }
      });
      return (isNegative ? '-' : '') + digits.reverse().join('');
    }
    
    const tests = [
      {
        inp: [77, 101, 130, 33, 7, 252, 253, 82],
        signed: false,
        expectation: '5577006791947779410'
      },
      {
        inp: [255, 255, 255, 255, 255, 255, 255, 255],
        signed: true,
        expectation: '-1'
      },
    ];
    
    tests.forEach(test => {
      const result = Int64ToString(test.inp, test.signed);
      console.log(`${result} ${result !== test.expectation ? '!' : ''}=== ${test.expectation}`);
    });

    首先,通过检查是否设置了最高位 (bytes[0] &gt; 128) 来计算符号。对于负数,必须将位取反 (255 - byte),并且必须将 1 添加到数字中(因此最后一个字节是 256 而不是 255)。

    forEach 循环的基本思想是将每个字节拆分为其十进制数字(byte % 10 并计算开销(byte - digits[i]) / 10 和Math.floor(byte / 10) 的下一个数字)。对于下一个字节,必须添加最后一个字节数字的移位结果(byte += digits[i] * 256 resp。digits[i] &lt;&lt; 8)。

    该代码针对简洁性、简单性和灵活性进行了优化。如果您使用的是字符串而不是字节或数字,并且不想使用任何库,那么转换性能似乎并不重要。否则,该函数可以针对性能进行优化:最多可以同时处理四个字节,一个只需要替换 0x100 和 0x80,另外(在 (U)Int64 的情况下只剩下两个字节组) forEach 循环可以展开。对十进制数字进行分组可能不会提高性能,因为结果字符串必须用零填充,从而需要在最终结果中删除前导零。

    【讨论】:

    • 感谢您的回复!这似乎对uint64 很有效。我需要进行哪些修改才能使其与int64 一起使用?我在这里创建了一个游乐场:jsfiddle.net/wcqLj1qg.
    • 我已经更新了我的答案并将其上传到codepen.io/stephtr/pen/brBvxr 必要的修改是(显然)添加减号,否定位并减去 1。
    • 可以通过将byte &gt; 0 更改为byte 使其更紧凑,因为byte 始终为正数。 j == bytes.length - 1 ? 0 : 1 也可以简单地写成 j != bytes.length - 1,因为布尔值将被强制转换为数字:)
    • 我在写代码的时候想过这个问题,但我认为这会降低可读性。
    【解决方案2】:

    另一种方法:将问题分成两个 uint32 以使计算易于管理。

    考虑更低和更高的 uint32(l 和 h)。完整号码可以写为h*0x100000000+l。考虑到十进制,还可以考虑较低的 9 位和剩余的较高位(ld 和 hd):ld=(h*0x100000000+l)%1000000000 和 hd=(h*0x100000000+l)/1000000000。借助一些算术和代数运算符属性,可以将这些运算分解为安全的“半”64 位运算并在结尾处组合字符串。

    function int64_to_str(a, signed) {
      const negative = signed && a[0] >= 128;
      const H = 0x100000000, D = 1000000000;
      let h = a[3] + a[2] * 0x100 + a[1] * 0x10000 + a[0]*0x1000000;
      let l = a[7] + a[6] * 0x100 + a[5] * 0x10000 + a[4]*0x1000000;
      if(negative) {
         h = H - 1 - h;
         l = H - l;
      }
      const hd = Math.floor(h * H / D + l / D);
      const ld = (((h % D) * (H % D)) % D + l) % D;
      const ldStr = ld + '';
      return (negative ? '-' : '') +
             (hd != 0 ? hd + '0'.repeat(9 - ldStr.length) : '') + ldStr;
    }
    
    let result = int64_to_str([77, 101, 130, 33, 7, 252, 253, 82], false);
    let expectation = '5577006791947779410';
    console.log(result + ' ' + (result === expectation ? '===' : '!==') + ' ' + expectation);
    
    result = int64_to_str([255, 255, 255, 255, 255, 255, 255, 255], true);
    expectation = '-1';
    console.log(result + ' ' + (result === expectation ? '===' : '!==') + ' ' + expectation);

    正如 cmets 中所详述的,即使 (h % D) * (H % D) 可以大于 Number.MAX_SAFE_INTEGER,算法仍然有效,因为丢失的位仍然为零。

    【讨论】:

    • 尽管这将是处理固定大小整数的更好方法,但在某些情况下它不会正常工作。我认为应该删除 Math.trunc(ld/D) 部分,因为第一个求和数在数学上已经包含该部分。更正后,我仍然不确定它是否正常工作。 h*H 的结果可能远高于 Number.MAX_SAFE_INTEGER。通过将其除以D 并截断,精度损失应该会消失。不过,我更担心(h%D)*(H%D),因为D*(H%D) 也太大了,这一次需要全精度。
    • @stephan (h%D)*(H%D),这是一个好点!我可以使用 3 uint22,但今天不行……
    • 我做了一点实验,似乎乘法中引入的错误幸运地被计算模数时引入的错误所补偿(因为D AND H 可以被 0x800 整除)。没有必要将它分成三组。如果你不想依赖这种行为,你可以使用((((h%D)*((H%D)/0x800))%D)*0x800)%D 而不是(h%D)*(H%D)%D(因为D*(H%D)/0x800 小于Number.MAX_SAFE_INTEGER)。
    • 更具体地说,D 和 H%D 的最后 11 位(因此为 0x800)已经为零,因此不会因为隐式将它们设置为零而引入任何错误“精度损失”。再补充一点:当hd 为零时,不应附加前导零。
    • 我更新了您的代码以包括有符号整数支持和hd 的修复:codepen.io/stephtr/pen/EvXqop?editors=0010
    【解决方案3】:

    这是我的解决方案。一般的策略是这样的:

    • 如果数字为负数,则使用 2 的补码取反,并在末尾添加负号
    • 将任意大小的数字表示为 0 到 9 的 LE 数字数组
    • 对于Uint8Array 中的每个字节(从最高有效到最低有效),将运行总和乘以 256,然后加上新字节的值
    • 要将一个数乘以 256,请将其加倍 8 次(自 2 ** 8 == 256 起)
    • 要添加两个数字,请使用小学算法:
      • 从最低有效数字开始
      • 添加两个数字的对应数字
      • 结果数字是总和 mod 10;如果总和为 10 或更多,则进位为 1,否则为 0
      • 继续使用进位添加相应的数字,直到我们添加最高有效数字并且进位为 0

    关于速记的几点说明:

    • n1[i] || 0 获取 n1 的第 i 位。如果这超过了i 的结尾,我们将其视为 0(想象数字前面用无限的 0 表示)。与n2 相同。
    • added &gt; 9 生成一个布尔值,它会自动转换为数字(如果 added &gt;= 10 则为 1,否则为 0)
    • i &lt; n1.length || i &lt; n2.length || carry 检查加数中是否有更多数字或进位是否仍然非零
    • String(b).split('').map(Number).reverse() 转换,例如100 到 '100',然后是 ['1', '0', '0'],然后是 [1, 0, 0],然后是 [0, 0, 1],所以它在 LE base-10 中表示
    • result.reverse().join('') 转换,例如[0, 0, 1] 到 [1, 0, 0],然后是 '100'

    代码:

    function add(n1, n2) {
        const sum = []
        let carry = 0
        for (let i = 0; i < n1.length || i < n2.length || carry; i++) {
            const added = (n1[i] || 0) + (n2[i] || 0) + carry
            sum[i] = added % 10
            carry = added > 9 //floor(added / 10)
        }
        return sum
    }
    function times256(n1) {
        for (let i = 8; i; i--) n1 = add(n1, n1)
        return n1
    }
    function toString(buffer) {
        const isNegative = buffer[0] & 128 //check if high bit is set
        if (isNegative) { //convert to positive, using 2's complement
            buffer = buffer.map(b => ~b) //invert all bits
            let i = buffer.length - 1
            while (buffer[i] === 255) { //add 1 to the number, carrying if necessary
                buffer[i] = 0
                i--
            }
            buffer[i]++
        }
        const result = buffer.reduce((sum, b) =>
            add(
                times256(sum), //multiply sum by 256
                String(b).split('').map(Number).reverse() //then add b
            ),
            []
        )
        const stringResult = result.reverse().join('')
        if (isNegative) return '-' + stringResult
        else return stringResult
    }
    

    【讨论】:

    • 非常感谢您的回复。您的代码和解释简直太棒了。现在我想知道哪种解决方案更好:你的或Stephan's。他的解决方案更短,仅包含 2 个循环,您的策略更详细和清晰。我们可能需要在这里进行一些性能检查。
    • 是的,我相信他的速度可能更快,但我发现这更容易概念化。
    【解决方案4】:

    这是UInt64 版本 - 我无法想象交换是那么困难:

    <!DOCTYPE html>
    <html>
    
    <body>
    <span id='out1'></span>
    <br>
    <span id='out2'></span>
    <br>
    <span id='out3'></span>
    </body>
    
    <script>
    fnl='';
    be=[77, 101, 130, 33, 7, 252, 253, 82];
    
    function paddedBinary(n) {
    pad='';
    sv=128;
    while (sv>n) {pad+='0';sv/=2;}
    return pad+n.toString(2);
    }
    
    for (let i=0;i<8;i++)
    fnl+=paddedBinary(be[i]);
    
    out1.textContent=fnl;
    
    dec=new Array(64);
    for (let i=0;i<64;i++) dec[i]=new Array(21).fill(0);
    
    function make2s() {
    dec[0][0]=1;
    for (let i=1;i<64;i++) {
    for (let j=0;j<21;j++) 
    dec[i][j]=2*dec[i-1][j];
    for (let j=0;j<21;j++) 
    if (dec[i][j]>9) {
    dec[i][j]-=10;
    dec[i][j+1]++;
    }
    }
    }
    
    function int64add(v1,v2) {
    var res=new Array(21).fill(0);
    for (let i=0;i<21;i++)
    res[i]=v1[i]+v2[i];
    for (let i=0;i<21;i++)
    if (res[i]>9) {
    res[i]-=10;
    res[i+1]++;
    }
    return res;
    }
    
    make2s();
    for (let i=0;i<64;i++)
    out2.textContent+=dec[i]+' :: ';
    
    cv=new Array(21).fill(0);
    for (let i=0;i<fnl.length;i++)
    if (fnl[i]=='1') cv=int64add(cv,dec[63-i]);
    
    out3.textContent=cv;
    
    </script>
    </html>

    paddedBinary() 函数返回一个“完整”的 8 位二进制数,因此我们可以将“fnl”创建为 BigEndian 的 64 位字符串。

    由于 JavaScript 不执行完整的 64 位算术,我创建了 dec[] 数组以将 2 的每个幂存储为单独的数字,方法是将前面的每个数字加倍并平滑十位。

    那么剩下的就是添加我们想要的位,使用类似的方法来平滑十位。

    (而且答案是反过来的!)

    【讨论】:

    • 感谢您的回复!
    猜你喜欢
    • 1970-01-01
    • 2011-10-04
    • 1970-01-01
    • 2012-09-24
    • 1970-01-01
    • 2011-02-04
    • 1970-01-01
    • 2021-01-03
    • 1970-01-01
    相关资源
    最近更新 更多