Мы все это знаем: числа в javascript — это давний мем в сообществе программистов.
Существуют обходные пути, например, использование дельты при сравнении двух чисел с плавающей запятой.
if(Math.abs(a-d) < 0.0001)
{
//these are close enough to be called the same
}
Или исправления, такие как класс BigInt:
let toobig1 = 9007199254740992n;
let toobig2 = 9007199254740993n;
console.log(toobig1 == toobig2) //false! unlike for regular numbers
Итак, мы можем работать с ограничениями, которые накладывают на нас числа с плавающей запятой IEEE.
Но просто для развлечения я хочу показать, как перемножить два произвольных числа с плавающей запятой, если они являются строками.
Метод, который я буду использовать, в основном похож на тот, которому меня учили в школе:
123
×456
------
738
+ 6150
+49200
------
56088
Этому есть предел, поскольку спецификация ECMAScript устанавливает максимальную длину строки 2**53 — 1 (т.е. 9007199254740991), а некоторые браузеры применяют еще более строгое ограничение. Firefox, например, ограничивает длину строки до 2**30 — 2 (т.е. 1073741822), но теоретически этот метод можно использовать с любыми двумя числами с любым количеством цифр в каждом.
Я знаю, что вы никогда не введете недействительные данные, и я, конечно, не стану этого делать, но на случай, если какой-нибудь имбецил воспользуется функцией, я определяю число как все, что соответствует этому регулярному выражению: /^(-?)(d+)(([.])(d+))?$/
Что означает, что перед десятичной дробью всегда должно быть число, так что эта функция не примет .2
, что может раздражать некоторых людей, но я делаю это ради простоты. Также не допускаются разделители тысяч и т.п., и я игнорирую тот факт, что некоторые страны используют ,
в качестве десятичного знака, и предполагаю, что все пишется слева направо. Я оставляю все эти нематематические детали на усмотрение читателя.
Все группировки нужны для того, чтобы я мог использовать отдельные биты.
Итак, сверху функция выглядит следующим образом:
let am = a.match(/^(-?)(d+)(([.])(d+))?$/)
if(am === null)
{
throw `Format Error: ${a} is not a valid number`
}
let bm = b.match(/^(-?)(d+)(([.])(d+))?$/)
if(bm === null)
{
throw `Format Error: ${b} is not a valid number`
}
Далее мне нужно определить, будет ли результат отрицательным.
let aneg = a[0] === '-';
let bneg = b[0] === '-';
let negative = (aneg ^ bneg) === 1;
^
— это оператор XOR, и true
обрабатывается как 1
, а false
как 0
.
На самом деле я собираюсь выполнить целочисленное умножение и после этого поставить десятичную дробь. Поэтому следующее, что я хочу знать, это сколько цифр будет после запятой. Это сумма количества цифр после запятой в каждом числе.
let adecCount = 0;
let anum = am[2];
if(am[5])
{
adecCount = am[5].length
anum += am[5];
}
let bdecCount = 0;
let bnum = bm[2];
if(bm[5])
{
bdecCount = bm[5].length
bnum += bm[5];
}
let finalDecCount = adecCount + bdecCount;
Вы видите, что я также объединяю целые и дробные части каждого числа.
Теперь мне нужно выполнить каждое частичное вычисление на случай, если меня попросят показать результаты работы. Не забудьте про переносы!
let partresults = [];
let adigits = anum.split('').reverse().map(s => parseInt(s, 10));
let bdigits = bnum.split('').reverse().map(s => parseInt(s, 10));
for(let ai = 0; ai < adigits.length; ai++)
{
let part = (Array(ai)).fill(0);
let carry = 0
let da = adigits[ai];
for(let db of bdigits)
{
let mul = (db*da) + carry;
carry = Math.floor(mul/10);
mul = mul%10;
part.unshift(mul);
}
if(carry > 0)
{
part.unshift(carry);
}
partresults.push(part);
}
Первое, что я делаю, это превращаю строку цифр в массив одноразрядных чисел. Я изменил порядок, потому что хочу работать справа налево.
Лично я предпочитаю циклы for
, а не вызов .forEach
, но это просто привычка, а не какая-то другая причина.
В вычислении есть внешний цикл и внутренний цикл.
Первое, что я делаю во внешнем цикле (let part = (Array(ai)).fill(0);
), это убеждаюсь, что в каждом частичном вычислении единицы, десятки, сотни и т.д. выстроены правильно, с единицами справа.
Далее мне нужно сложить каждый массив в массиве массивов вместе, чтобы в итоге получить один массив, который и будет результатом. Похоже на операцию reduce, если таковая вообще существует.
let resultDigits = [];
if(partresults.length === 1)
{
resultDigits = partresults[0];
}
else
{
resultDigits = partresults.reduce((agg, arr) =>
{
while(agg.length < arr.length)
{
agg.unshift(0);
}
let carry = 0;
for(let arri = arr.length-1; arri >= 0; arri--)
{
let agd = agg[arri];
let ard = arr[arri];
let value = agd + ard + carry;
if(value > 9)
{
carry = Math.floor(value/10);
value = value % 10;
}
else
{
carry = 0;
}
agg[arri] = value;
}
if(carry > 0)
{
agg.unshift(carry);
}
return agg;
}, []);
}
Конечно, мне нужно разобраться с простым случаем, когда есть только один вложенный массив.
Теперь мне нужно выяснить, где может находиться десятичная дробь.
if(finalDecCount > 0)
{
resultDigits.splice(resultDigits.length - finalDecCount, 0, '.');
}
Оооо, сращивание! Как красиво.
И наконец, я добавляю -
, если результат отрицательный, соединяю все вместе и возвращаюсь.
if(negative)
{
resultDigits.unshift('-');
}
return resultDigits.join('');
Вы можете увидеть полный код в этом gist.
Не стесняйтесь использовать его и дайте мне знать об оптимизациях или других подходах, которые вы бы использовали!