PBS Tidbit 1 of Y: Display Values are not Data!
Listener @lbutlr pointed out on Twitter that the sample solution to the challenge set in PBS 88 Icon as I originally posted it in PBS 89 had a bug — it sometimes got its maths spectacularly wrong!
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?
Matching Podcast Episode
Listen along to this instalment.
You can also Download the MP3
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!
You’ll find the full code for the challenge solution on GitHub.
Related Content
- The episode of Chit Chat Across the Pond Podcast for the 8th of February 2020 which is based in this post.
- A blog post from Allison Sheridan explaining how she enhanced her sample solution to the same challenge to deal with currencies with different numbers of decimal places.