Listener @lbutlr pointed out on Twitter that the sample solution to the challenge set in PBS 88 as I originally posted it in PBS 89 had a bug — it sometimes got its maths spectacularly wrong!

PBS 88 Solution Bug

It’s important to note that the bug did not affect all currencies, just some currencies.

The line of code for doing the currency conversion is supremely simple:

const convAmount = baseAmount * rate;

How on earth can there be a bug that is something so simple that only manifests for some currencies but not for others?

Diagnosing the Problem

The first thing I noticed was that the bug affected low-value currencies. When each unit of the currency has a low value then the exchange rates will involve small numbers. Maybe the combination of a big base amount and a tiny exchange rate was exceeding the amount of precision a JavaScript number can store?

Javascript numbers are 64bit numbers (double-precision numbers or simply doubles in programming jargon), so they can only capture so much detail — as a science student I ran into the limit of doubles when doing homework assignments, so I knew it was at least conceivable. Looking more closely that didn’t make sense — the numbers were not big enough, and the rates not small enough for that to be a reasonable explanation.

I was on the right track though — it was a precision problem, but not with JavaScript’s number storage, but with my code!

As explained in the description of the solution in PBS 89, I used data attributes to store the rate within the relevant row of the card using HTML data attributes. Without thinking about it the value I stored in the data attribute was the rate as displayed to the user — rounded to 2 decimal places!

The data attribute is added into the currency’s item within this part of my Mustache template:

<li class="list-group-item currencyRate" data-currency="{{{code}}}" data-rate="{{{rate}}}">

And the rate is added into the view with this line:

cardView.rates.push({
  code: cc,
  rate: numeral(curData.rates[cc]).format('0,0[.]00'),
  ...CURRENCIES[cc]
});

And there we have it — the view was originally written purely to present information to the user, and I then re-used that view to inject data into the DOM elements. This is a great illustration of why you want to avoid passing information formatted for consumption by humans into computations!

Bottom line — as I suspected, it was indeed a loss of precision, but one of my own making — oops!

There is a silver lining though, I have the power to fix problems I created 🙂

Fixing the Bug

The solution is fundamentally very simple — add the true rate into view as well as the formatted rate, then use that true rate to populate the data attribute.

The first step was to tweak the code that creates the view to add a new key named rawRate:

cardView.rates.push({
  code: cc,
  rate: numeral(curData.rates[cc]).format('0,0[.]00'),
  rawRate: curData.rates[cc],
  ...CURRENCIES[cc]
});

The template could then be updated to use the raw rate:

<li class="list-group-item currencyRate" data-currency="{{{code}}}" data-rate="{{{rawRate}}}">

Much better!

PBS 88 Bug Fixed

You’ll find the full code for the challenge solution on GitHub.

Related Content