Module 2 · Web Development with JavaScript
JavaScript essentials
Variables, functions and numbers in JavaScript, and the money traps they hide (floating point and rounding), by writing the invoice total function the payment page needs.
About 25 minutes
The problem
The payment page must show an invoice's total, with discount and VAT, exactly as the server calculates it. If the page and the API disagree by a kobo, customers lose trust and support gets calls. JavaScript's numbers have the same floating-point traps as Python's, plus a few of their own.
The concept
Variables
const for values that won't be reassigned (most of them), let for those that will. Avoid the old var.
Functions
JavaScript
function invoiceTotal(lines, discountPct = 0, vatExempt = false) {
// ...
return total;
}Arrow functions are a shorter form: const double = (n) => n * 2;
Numbers
JavaScript has one number type, a 64-bit float. Whole numbers are exact up to about 9 million billion, so kobo as whole numbers is safe; fractions like 0.075 are not exact.
Rounding
Math.round rounds halves up for positive numbers (Math.round(16.5) is 17), unlike Python's round. But it rounds -16.5 to -16, and it can only round what it's given: if a calculation lands on 16.499999999999996 instead of 16.5, it rounds down.
Example
The traps, one line each:
JavaScript
console.log(0.1 + 0.2);
console.log(Math.round(16.5), Math.round(-16.5));
console.log(3 * 0.075, (3 * 75) / 1000);
console.log(8.95 * 100, Math.floor(8.95 * 100));
console.log(Number.isInteger(1250050), Number.MAX_SAFE_INTEGER);0.30000000000000004
17 -16
0.22499999999999998 0.225
894.9999999999999 894
true 90071992547409913 * 0.075 isn't exactly 0.225, because 0.075 can't be stored exactly. The error is tiny, but tiny errors become wrong kobo as soon as code rounds down or truncates: 8.95 * 100 comes out just under 895, so the common Math.floor(price * 100) loses a kobo. Multiplying by 75 and dividing by 1000 keeps the arithmetic in whole numbers until the last step, and lesson 6 converts typed amounts by splitting the text at the decimal point instead of multiplying. Now the invoice total, matching Tallybook's rules from the Software Engineering course:
JavaScript
const VAT_PER_THOUSAND = 75; // 7.5%
const MAX_DISCOUNT_PCT = 20;
function roundHalfUp(n) {
return Math.sign(n) * Math.round(Math.abs(n));
}
function invoiceTotal(lines, discountPct = 0, vatExempt = false) {
if (discountPct < 0 || discountPct > MAX_DISCOUNT_PCT) {
throw new RangeError(`discountPct must be between 0 and ${MAX_DISCOUNT_PCT}`);
}
const subtotal = lines.reduce((sum, [quantity, unitPriceKobo]) => sum + quantity * unitPriceKobo, 0);
const afterDiscount = subtotal - roundHalfUp((subtotal * discountPct) / 100);
const vat = vatExempt ? 0 : roundHalfUp((afterDiscount * VAT_PER_THOUSAND) / 1000);
return afterDiscount + vat;
}
console.log(invoiceTotal([[2, 100000]]));
console.log(invoiceTotal([[2, 100000]], 10));
console.log(invoiceTotal([[1, 220]]));
console.log(invoiceTotal([[1, 10000], [2, 5000], [4, 2500]], 15));215000
193500
237
27413These are the same totals the Python tests in the Software Engineering course expect, including the half-kobo case (220 kobo: VAT 16.5 rounds up to 17). The page and the server now agree because they implement the same rules the same way. In a real system, the page would also show the server's total, so a mismatch is caught (lesson 8).
Walkthrough
- Run the blocks in your console. Try
invoiceTotal([[1, 100000]], 25). What happens? - Why does
roundHalfUpuseMath.absandMath.sign? Test it on -16.5. - Rewrite
roundHalfUpas an arrow function. - Write a function that formats kobo as naira (the task below).
Practice
Practice
What does invoiceTotal([[3, 250000]], 5) return, in kobo?
Task
8 minWrite a JavaScript function koboToNaira(kobo) that returns text like "₦12,500.50" for 1250050, using whole-number arithmetic for the naira and kobo parts (no dividing by 100 into a float). Show two example calls in comments with their results.
Your work is checked for
- A function named koboToNaira
- Uses whole-number division (Math.floor or Math.trunc)
- Uses the remainder for kobo (%)
- Pads kobo to two digits
- Adds thousands separators
- Example calls in comments
Check your understanding
Answer every question to check.