Умножение чисел, когда они являются строками

Мы все это знаем: числа в 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.

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

Оцените статью
devanswers.ru
Добавить комментарий