Массив байтов в 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 в виде отдельных цифр, удваивая каждую предыдущую цифру и сглаживая десятки из.
тогда все осталось, чтобы добавить биты, которые мы хотим, который использует аналогичный метод для сглаживания десятков.
(и ответ дается в обратном!)