Как закодировать целое число в Uint8Array и обратно в integer в JavaScript?

#javascript #bit-manipulation #arraybuffer #uint8array

Вопрос:

Мне нужно добавить сжатие в мой проект, и я решил использовать алгоритм LZJB, который является быстрым, а код небольшим. Нашел эту библиотеку https://github.com/copy/jslzjb-k

Но API не очень хорош, потому что для распаковки файла вам нужна длина входного буфера (поскольку Uint8Array не является динамическим, вам нужно выделить некоторые данные). Итак, я хочу сохранить длину входного буфера как первые несколько байтов Uint8Array, чтобы я мог извлечь это значение и создать выходной Uint8Array на основе этого целого значения.

Я хочу, чтобы функция, которая возвращает Uint8Array из integer, была общей, возможно, сохраните длину байтов в первом байте, чтобы вы знали, сколько данных вам нужно извлечь, чтобы прочитать целое число. Я думаю, мне нужно извлечь эти байты и использовать некоторый сдвиг битов, чтобы получить исходное число. Но я не совсем уверен, как это сделать.

Итак, как я могу написать универсальную функцию, которая преобразует целое число в Uint8Array, которое можно встроить в больший массив, а затем извлечь это число?

Ответ №1:

Общий ответ

Эти функции позволяют любому целому числу (оно использует BigInts внутри, но может принимать числовые аргументы) кодироваться и декодироваться из любой части a Uint8Array . Это несколько излишне, но я хотел научиться работать с целыми числами произвольного размера в JS.

 // n can be a bigint or a number
// bs is an optional Uint8Array of sufficient size
//   if unspecified, a large-enough Uint8Array will be allocated
// start (optional) is the offset 
//   where the length-prefixed number will be written
// returns the resulting Uint8Array
function writePrefixedNum(n, bs, start) {
  start = start || 0;
  let len = start 2; // start, length, and 1 byte min
  for (let i=0x100n; i<n; i<<=8n, len   ) /* increment length */;
  if (bs === undefined) {  
    bs = new Uint8Array(len);
  } else if (bs.length < len) {
        throw `byte array too small; ${bs.length} < ${len}`;
  }
  let r = BigInt(n);
  for (let pos = start 1; pos < len; pos  ) {
    bs[pos] = Number(r amp; 0xffn); 
        r >>= 8n;
  }
  bs[start] = len-start-1; // write byte-count to start byte
  return bs;
}

// bs must be a Uint8Array from where the number will be read
// start (optional, defaults to 0)
//    is where the length-prefixed number can be found
// returns a bigint, which can be coerced to int using Number()
function readPrefixedNum(bs, start) {
  start = start || 0;
  let size = bs[start]; // read byte-count from start byte
  let n = 0n;
  if (bs.length < start size) {
        throw `byte array too small; ${bs.length} < ${start size}`;
  }    
  for (let pos = start size; pos >= start 1; pos --) {
    n <<= 8n;
    n |= BigInt(bs[pos])
  }
  return n;
}

function test(n) {
  const array = undefined;
  const offset = 2;
  let bs = writePrefixedNum(n, undefined, offset);
  console.log(bs);
  let result = readPrefixedNum(bs, offset);
  console.log(n, result, "correct?", n == result)
}

test(0)
test(0x1020304050607080n)
test(0x0807060504030201n) 

Простой 4-байтовый ответ

Этот ответ кодирует 4-байтовые целые числа в и из Uint8Array s.

 function intToArray(i) {
    return Uint8Array.of(
      (iamp;0xff000000)>>24,
      (iamp;0x00ff0000)>>16,
      (iamp;0x0000ff00)>> 8,
      (iamp;0x000000ff)>> 0);
}

function arrayToInt(bs, start) {
    start = start || 0;
    const bytes = bs.subarray(start, start 4); 
    let n = 0;
    for (const byte of bytes.values()) {       
            n = (n<<8)|byte;
    }
    return n;
}

for (let v of [123, 123<<8, 123<<16, 123<<24]) {
  let a = intToArray(v);
  let r = arrayToInt(a, 0);
  console.log(v, a, r);
} 

Комментарии:

1. Почему arrayToInt(new Uint8Array([255, 255, 255, 255])) возвращает -1 , а не максимальное значение?

2. @jcubic Это потому, что отрицательные значения представлены в двоичном формате. Прочитайте о дополнении two . arrayToInt(new Uint8Array([127, 255, 255, 255])) это максимальное 32-разрядное значение со знаком.

3. arrayToInt(new Uint8Array([127, 255, 255, 255])) ВОЗВРАТ 0 .

4. Есть 2 аргумента для arrayToInt (потому что вы сказали, что хотите извлекать числа из больших байтовых массивов). Если вы укажете аргумент (или используете новую версию, в которой по умолчанию он равен 0), он работает — потому что, помимо порядка байтов, он выполняет точно то же самое, что и ваша функция.

5. хорошо, извините, не заметил аргумент, потому что, когда я тестировал ваше решение в первый раз, когда я использовал function arrayToInt(bs, start = 0) { , именно поэтому я прокомментировал, что оно вернуло -1, и вы прокомментировали тот же код и не исправили его вторым аргументом.

Ответ №2:

Вот рабочие функции (основанные на преобразовании целого числа javascript в массив байтов и обратно)

 
function numberToBytes(number) {
    // you can use constant number of bytes by using 8 or 4
    const len = Math.ceil(Math.log2(number) / 8);
    const byteArray = new Uint8Array(len);

    for (let index = 0; index < byteArray.length; index  ) {
        const byte = number amp; 0xff;
        byteArray[index] = byte;
        number = (number - byte) / 256;
    }

    return byteArray;
}

function bytesToNumber(byteArray) {
    let result = 0;
    for (let i = byteArray.length - 1; i >= 0; i--) {
        result = (result * 256)   byteArray[i];
    }

    return resu<
}
 

при использовании const len = Math.ceil(Math.log2(number) / 8); массива нужны только байты. Если вам нужен фиксированный размер, вы можете использовать константу 8 или 4 .
В моем случае я просто сохранил длину байтов в первом байте.

Комментарии:

1. Вы скрыли дополнительную сложность сохранения / чтения 1-го байта, указывающего длину результирующего байтового массива. Также обратите внимание, что тип JS Number не может точно представлять числа более ~ 2 ^ 53, поэтому вам никогда не понадобится больше 7 байт или меньше 1. Мой ответ ограничен (общим) случаем чисел в диапазоне 0-2 ^ 31, избегая этой сложности, предполагая 4-байтовые целые числа.

2. @tucuxi Я хочу, чтобы это было сжатие файлов, если я буду использовать ваш подход, я смогу сохранять файлы только около 1,9 ГБ, также ваш код не работает для вашего примера в комментарии.

3. @tucuxi Я также предпочитаю свое решение, потому что оно действительно работает.

4. пожалуйста, объясните, как ваш ответ работает за пределами 2 ^ 53 — 1 (предел number передаваемого вами аргумента). Вы вызывали arrayToInt только с 1 аргументом. Я по умолчанию присвоил этому аргументу значение 0. Это работало раньше (при передаче всех аргументов), теперь это также работает, даже когда вы не смотрите на код или не читаете примеры.

5. @tucuxi Я предпочитаю свое решение, потому что оно экономит до 7 байт (чтобы не потерять точность), и если кому-то нужно постоянное количество байтов, он может использовать константу для len 8 или 4.