Массив байтов в Uint64 в виде строки

давайте подумаем о следующей ситуации.

процедура Go создает массив байтов, в котором пакуется тип uint64 в 8 байтах Big Endian [77, 101, 130, 33, 7, 252, 253, 82].

в коде JavaScript я получаю эти байты как Uint8Array. Мы знаем, что JavaScript не поддерживает тип uint64 как безопасный числовой тип и не может выполнять побитовые операции над целыми числами больше 32 бит, поэтому такие вещи как buf[0] << 56 никогда не будет работать.

так что процесс декодирования этих байтов непосредственно в числовую строку "5577006791947779410"?

П. С. Я знаю, что есть много of библиотеки для работы с большими целыми числами в JavaScript, но в целом они огромны и предоставляют множество математических операций, которые мне не нужны. Я ищу простое современное простое решение для просто декодирование be-packed тип uint64 и тип int64 байты в числовую строку. У тебя есть что-нибудь на уме?

4 ответов


EDIT: для преобразования (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] > 128). Для отрицательных чисел биты должны быть отрицательными (255 - byte) и 1 должен быть добавлен к числу (поэтому 256 вместо 255 для последнего байта).

основная идея цикла forEach состоит в том, чтобы разделить каждый байт на его десятичные цифры (byte % 10 и расчета накладных расходов (byte - digits[i]) / 10 респ. Math.floor(byte / 10) для следующей цифры). Для следующего байта нужно добавить смещенный результат цифр последних байтов (byte += digits[i] * 256 респ. digits[i] << 8).

этот код оптимизирован для краткости, простоте и гибкости. Если ты ... работа со строками вместо байтов или чисел и не хотите использовать библиотеки похоже, что производительность преобразования не имеет значения. В противном случае функция может быть оптимизирована для производительности: до четырех байтов можно обрабатывать одновременно, нужно только заменить 0x100 и 0x80, дополнительно (с только двумя группами байтов, оставшимися в случае (U)Int64)forEach петля может быть развернута. Группировка десятичных цифр, вероятно, не повысит производительность, так как результирующие строки должны быть дополнены нулями, вводя необходимость удаления ведущих нулей в конечном результате.


другой подход: разделить задачу на два 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);

как описано в комментариях, что алгоритм работает, хотя (h % D) * (H % D) может сделать больше, чем Number.MAX_SAFE_INTEGER, потому что потерянные биты, тем не менее ноль.


вот мое решение. Общая стратегия такова:--22-->

  • если число отрицательно, отрицайте его, используя дополнение 2 и добавьте отрицательный знак обратно в конце
  • представляют произвольные номера размера как массивы LE цифр от 0 до 9
  • для каждого байта в Uint8Array (от самого до наименее значимого), умножьте общее число на 256 и добавьте к нему значение нового байта
  • чтобы умножить число на 256, удвоьте его 8 раз (так как 2 ** 8 == 256)
  • чтобы добавить два числа, используйте алгоритм начальной школы:
    • начните с наименее значимой цифры
    • добавить соответствующие цифры двух чисел
    • результирующая цифра-это сумма mod 10; carry-1, если сумма равна 10 или более, в противном случае 0
    • продолжайте добавлять соответствующие цифры с переносом, пока мы не добавим наиболее значимые цифры и не перенесем 0

несколько заметок о стенография:

  • n1[i] || 0 получает iй разряд n1. Если это конец i, мы рассматриваем его как 0 (представьте числа, представленные бесконечными 0 перед ними). То же самое с n2.
  • added > 9 производит логическое значение, которое автоматически преобразуется в число (1, Если added >= 10, 0 в противном случае)
  • i < n1.length || i < n2.length || carry проверяет, есть ли больше цифр в любом из добавлений или перенос все еще ненулевое
  • String(b).split('').map(Number).reverse() преобразует, например,100 to '100', потом ['1', '0', '0'], потом [1, 0, 0], потом [0, 0, 1] таким образом, он представлен в LE base-10
  • result.reverse().join('') преобразует, например,[0, 0, 1] to [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
}

это 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 " как 64-битную строку Бигендиана.

поскольку JavaScript не выполняет полную 64-битную арифметику, я создаю dec[] массив для хранения каждой мощности 2 в виде отдельных цифр, удваивая каждую предыдущую цифру и сглаживая десятки из.

тогда все осталось, чтобы добавить биты, которые мы хотим, который использует аналогичный метод для сглаживания десятков.

(и ответ дается в обратном!)