DEV Community

Cover image for Multiplying numbers when they're strings
Matt Ellen
Matt Ellen

Posted on

Multiplying numbers when they're strings

We all know it: javascript's numbers are a long standing meme in the programming community.

There are work arounds, such as using a delta when comparing two floating point numbers

if(Math.abs(a-d) < 0.0001)
{
  //these are close enough to be called the same
}
Enter fullscreen mode Exit fullscreen mode

Or fixes, such as the BigInt class:

let toobig1 = 9007199254740992n;
let toobig2 = 9007199254740993n;
console.log(toobig1 == toobig2) //false! unlike for regular numbers
Enter fullscreen mode Exit fullscreen mode

So we can work with the limitations that IEEE floating point numbers impose upon us.

But just for fun, I want to show how to multiply two arbitrary floating point numbers when they are strings.

The method I will use is basically what I was taught in school, e.g.:

   123
  ×456
------
   738
+ 6150
+49200
------
 56088
Enter fullscreen mode Exit fullscreen mode

There is a limit to this, because the ECMAScript specification has a maximum string length of 2**53 - 1 (i.e. 9007199254740991) and some browsers implement an even stricter limit. Firefox, for example, limits string length to 2**30 - 2 (i.e. 1073741822), but in theory this methods can be used with any two numbers with any number of digits each.

Now, I know you would never put in invalid input, and I certainly would not, but just in case some imbecile uses the function I'm defining a number as anything that matches this regular expression: /^(-?)(\d+)(([.])(\d+))?$/ Which means that there always has to be a number before the decimal place, so this function would reject .2, which might annoy some people, but I'm doing for the sake of simplicity. Also, no thousands separators or similar allowed, and I'm ignoring the fact that some localities use , as the decimal place, and assuming everything is written left to right. I leave all those non-mathematical parts as an exercise to the reader.

All the grouping is so I can use the separate bits.

So, up top, the function looks like:

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`
}
Enter fullscreen mode Exit fullscreen mode

Next I need to detect if the result will be negative.

let aneg = a[0] === '-';
let bneg = b[0] === '-';

let negative = (aneg ^ bneg) === 1;
Enter fullscreen mode Exit fullscreen mode

^ is the XOR operator, and true gets treated as 1 and false as 0.

I'm actually going to do integer multiplication and put the decimal place in afterwards. So the next thing I want to know is how many digits there will be after the decimal place. This is the sum of the number of digits after the decimal place in each number.

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;
Enter fullscreen mode Exit fullscreen mode

You can see I'm also mashing together the integer and fractional parts of each number.

Now I need to do each partial calculation, just in case I'm asked to show my working. Don't forget those carries!

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);
}
Enter fullscreen mode Exit fullscreen mode

The first thing I do it turn the string of digits into an array of single digit numbers. I reverse the order because I want to work from right to left.

Personally I prefer for loops over calling .forEach, but that's just habit rather than any other reason.

The calculation has an outer loop and an inner loop.

The first thing I'm doing in the outer loop (let part = (Array(ai)).fill(0);) is making sure that each partial calculation lines up the units, tens, hundreds, etc., correctly, with the units on the right.

Next I need to add each array in the array of arrays together, to end with one array which is the result. Sounds like a reduce operation if ever there was one.

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;
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

Of course I need to deal with the simple case where there's only one nested array.

Now I need to figure out where the decimal place might go.

if(finalDecCount > 0)
{
  resultDigits.splice(resultDigits.length - finalDecCount, 0, '.');
}
Enter fullscreen mode Exit fullscreen mode

Ohh splice! How splicy.

And finally I add in a - if the result if negative, join it all together and return.

if(negative)
{
  resultDigits.unshift('-');
}

return resultDigits.join('');
Enter fullscreen mode Exit fullscreen mode

You can see the full code in this gist.

Feel free to riff on it, and let me know optimisations or different approaches you'd take!

Top comments (0)