Logic and mathematical thinking Foundation
Mathematical reasoning
Use expressions, functions, equations, ratios and percentages to compute discounts, VAT and interest, and keep money exact by counting in kobo instead of decimals.
BY THE END OF THIS LESSON YOU CAN
- Read and write expressions, and tell a mathematical equation from a JavaScript assignment
- Model a rule as a function and solve simple equations and inequalities for the unknown, then check the answer by substitution
- Compute ratios, percentages, discounts, VAT and simple interest, including reverse VAT and successive discounts
- Explain why 0.1 + 0.2 is not 0.3, and keep money exact as whole kobo with one deliberate rounding step
- Split an amount into parts that always add back up to the whole
The customer paid exactly, and the till said "short"
A supermarket till adds up a customer's items: a bag of rice at ₦1,999.99, a tin of tomatoes at ₦450.70 and a loaf of bread at ₦250.30. The screen shows ₦2,700.99. The customer pays exactly ₦2,700.99 by transfer, and the till refuses the sale:
const prices = [1999.99, 450.7, 250.3];
let total = 0;
for (const price of prices) {
total = total + price;
}
const paid = 2700.99;
console.log("total:", total);
if (paid >= total) {
console.log("Payment complete");
} else {
console.log("Payment short by", total - paid);
}
node short.js and of the browser terminaltotal: 2700.9900000000002 Payment short by 4.547473508864641e-13
The total is not ₦2,700.99; it is a tiny bit more. The difference, written 4.547…e-13 (scientific notation: about 0.00000000000045), is far less than one kobo, but >= does not care how small a difference is. The display rounded the total to two decimals, so nobody could see it.
This lesson is about the mathematics inside programs: expressions, functions, equations, ratios and percentages, which you need for discounts, VAT, interest and bill splitting. It is not about hard maths. It is about being exact: knowing which numbers a computer can hold precisely, where rounding must happen, and how to check an answer. By the end you will see why the till above should never have used decimals for money at all.
Variables and expressions
In mathematics, a variable is a letter that stands for a number: in "the price of n loaves at ₦250 each is 250n", n is a variable. An expression is a combination of numbers, variables and operations that has a value once you know the variables: 250 * n, price * quantity + deliveryFee.
JavaScript expressions work the same way, with the same order of operations you learned at school: parentheses first, then ** (power), then *, / and %, then + and -:
const price = 250;
const quantity = 4;
const deliveryFee = 1500;
console.log(price * quantity + deliveryFee); // multiplication first
console.log(price * (quantity + deliveryFee)); // parentheses change it
console.log(2 ** 10); // 2 to the power 10
console.log(23 % 6); // remainder of 23 / 6
node expressions.js and of the browser terminal2500 376000 1024 5
% is the remainder operator: 23 divided by 6 is 3 with 5 left over, so 23 % 6 is 5. It answers everyday questions: is a number even (n % 2 === 0)? How many eggs are left after filling boxes of 6?
= in maths and = in code
In mathematics, x = x + 1 is a false statement: no number equals itself plus one. In JavaScript it is an instruction: "compute x + 1, then store the result in x". The = in code is assignment; it has a direction, right to left. Equality, the mathematical meaning, is written === and gives a boolean.
let stock = 10;
stock = stock - 3; // assignment: take 3 out of stock
console.log(stock);
console.log(stock === 7); // equality: a question, true or false
node assign.js and of the browser terminal7 true
Whole division: floor, ceil and remainder
Many business questions divide and then need a whole number. Math.floor(x) rounds down to a whole number and Math.ceil(x) rounds up. Which one is right depends on the question, not on the numbers:
const eggs = 23;
const perBox = 6;
console.log("full boxes:", Math.floor(eggs / perBox)); // you can only sell full boxes
console.log("boxes needed:", Math.ceil(eggs / perBox)); // to pack every egg
console.log("eggs left over:", eggs % perBox);
const budget = 20000;
const itemPrice = 2500;
console.log("items you can afford:", Math.floor(budget / itemPrice));
node whole-division.js and of the browser terminalfull boxes: 3 boxes needed: 4 eggs left over: 5 items you can afford: 8
"How many can I fill or afford?" rounds down; you cannot buy 8.7 items. "How many do I need to hold everything?" rounds up; 3.8 boxes means 4 boxes. Picking the wrong one gives an answer that is off by one, which you will meet again in Why doesn't this work?.
Functions: rules that turn inputs into outputs
A mathematical function is a rule that gives exactly one output for each input: f(x) = 2x + 3 turns 5 into 13, always. A delivery price list is a function: the input is the order total, the output is the fee. A JavaScript function can model the same rule, and when it depends only on its inputs, it behaves exactly like the mathematical one:
// Delivery: ₦2,000 under ₦10,000; ₦1,000 up to ₦50,000; free above that.
function deliveryFee(orderTotal) {
if (orderTotal < 10000) return 2000;
if (orderTotal <= 50000) return 1000;
return 0;
}
for (const total of [9999, 10000, 50000, 50001]) {
console.log(total, "->", deliveryFee(total));
}
node delivery-fee.js and of the browser terminal9999 -> 2000 10000 -> 1000 50000 -> 1000 50001 -> 0
A function defined in pieces like this is called piecewise. The interesting inputs are the boundaries where one piece ends and the next begins; that is why the loop tests 9,999, 10,000, 50,000 and 50,001, exactly as in Conditional reasoning.
Equations and inequalities: solving for the unknown
An equation says two expressions are equal and asks which value of the variable makes it true. Programs need this whenever they know the result and must find the input.
A shop prints prices including VAT. Nigeria's VAT rate is 7.5%, so a price including VAT is the net price times 1.075. A receipt must show the net price and the VAT separately. The equation is:
net × 1.075 = 10,750
Divide both sides by 1.075 to get net on its own: net = 10,750 ÷ 1.075 = 10,000. Rearranging an equation like this, doing the same thing to both sides until the unknown stands alone, is how you turn "I know the result" into a formula for the input.
Check by substitution
The most useful habit from school maths: after you solve an equation, put the answer back in and see whether it works. In code, that is a test:
function netFromGross(gross) {
return gross / 1.075;
}
const gross = 10750;
const net = netFromGross(gross);
console.log("net:", net);
console.log("check: net * 1.075 =", net * 1.075, "->", net * 1.075 === gross);
node reverse-vat.js and of the browser terminalnet: 10000 check: net * 1.075 = 10750 -> true
Inequalities
An inequality uses <, <=, > or >= instead of =, and its answer is a range of values rather than one. "Ada saves ₦15,000 a month. After how many months does she have at least ₦100,000?" is the inequality 15,000 × m ≥ 100,000. Dividing both sides by 15,000 gives m ≥ 6.67. The months are whole, so the smallest answer is 7: a Math.ceil question. You can also let a loop find it, which is the method that still works when there is no neat formula:
const monthly = 15000;
const target = 100000;
console.log("by formula:", Math.ceil(target / monthly));
let months = 0;
let saved = 0;
while (saved < target) {
months = months + 1;
saved = saved + monthly;
}
console.log("by loop:", months, "months, saved", saved);
node months.js and of the browser terminalby formula: 7 by loop: 7 months, saved 105000
Two methods that agree are strong evidence that both are right. The loop runs while the condition saved < target is true, which is the opposite of the goal saved >= target: De Morgan again. Sequences uses the same kind of loop for savings that earn interest, where no simple division works.
Ratios and proportions
A ratio compares quantities: two business partners share profit in the ratio 2 : 3, meaning for every 2 parts the first gets, the second gets 3. There are 5 parts in total, so the first gets 2/5 of the profit and the second 3/5. A proportion says two ratios are equal, which lets you scale: if 3 kg of rice costs ₦4,500, then 5 kg costs 4,500 × 5/3 = ₦7,500.
Ratios are also how you compare prices fairly. The unit price is the price divided by the quantity, the price of one gram or one litre:
const small = { grams: 400, price: 1800 };
const large = { grams: 900, price: 3800 };
const smallPerGram = small.price / small.grams;
const largePerGram = large.price / large.grams;
console.log("small: ₦" + smallPerGram.toFixed(2) + " per gram");
console.log("large: ₦" + largePerGram.toFixed(2) + " per gram");
console.log(largePerGram < smallPerGram ? "large is cheaper per gram" : "small is cheaper per gram");
node unit-price.js and of the browser terminalsmall: ₦4.50 per gram large: ₦4.22 per gram large is cheaper per gram
x.toFixed(2) turns a number into text with exactly two decimals, rounding as needed. Use it only for display; it gives back a string, not a number. The + between a string and a number joins them as text.
Percentages: discounts, VAT and interest
Per cent means "per hundred". 15% is 15/100 = 0.15. Three calculations cover almost every percentage question in business software:
| Question | Formula | Example |
|---|---|---|
| What is p% of x? | x × p / 100 | 15% of ₦8,000 = ₦1,200 |
| x increased by p% | x × (1 + p/100) | ₦10,000 + 7.5% VAT = ₦10,750 |
| x decreased by p% | x × (1 − p/100) | ₦8,000 − 15% = ₦6,800 |
| What percentage is a of b? | a / b × 100 | ₦1,200 of ₦8,000 is 15% |
| Percentage change from old to new | (new − old) / old × 100 | ₦8,000 to ₦9,000 is +12.5% |
const price = 8000;
console.log("15% of price:", price * 15 / 100);
console.log("after 15% discount:", price * (1 - 15 / 100));
console.log("with 7.5% VAT:", 10000 * (1 + 7.5 / 100));
console.log("change 8000 -> 9000:", (9000 - 8000) / 8000 * 100, "%");
node percent.js and of the browser terminal15% of price: 1200 after 15% discount: 6800 with 7.5% VAT: 10750 change 8000 -> 9000: 12.5 %
Percentages do not add
Two traps catch people who treat percentages like plain numbers.
Successive discounts multiply. A 20% sale, then a 10% loyalty discount on the sale price, is not 30% off. The second discount applies to a smaller number.
Up then down does not return to the start. A price raised by 20% and then cut by 20% ends below where it started, because the cut is 20% of the higher price.
const price = 10000;
const twoDiscounts = price * (1 - 0.2) * (1 - 0.1);
console.log("20% then 10%:", twoDiscounts, "=", (price - twoDiscounts) * 100 / price, "% off");
const upThenDown = price * 1.2 * 0.8;
console.log("+20% then -20%:", upThenDown);
node percent-traps.js and of the browser terminal20% then 10%: 7200 = 28 % off +20% then -20%: 9600
A related confusion: if an interest rate goes from 5% to 7%, it rose by 2 percentage points, but by 40 per cent (2 is 40% of 5). Say which one you mean; loan adverts rely on people mixing them up.
Simple interest
Simple interest pays the same amount every year: interest = principal × rate × years, where the principal is the amount saved or borrowed. ₦200,000 at 10% a year for 3 years earns 200,000 × 0.10 × 3 = ₦60,000. Most real savings accounts pay compound interest instead, where each year's interest itself earns interest; that is a sequence, and it is the main example in the next lesson.
Why 0.1 + 0.2 is not 0.3
Back to the till. JavaScript stores numbers in a format called floating point (the standard is IEEE 754, used by almost every language). It stores numbers in binary, base 2. In binary, many simple decimal fractions have no exact form, just as 1/3 has no exact decimal form (0.3333… never ends). So 0.1 is stored as the closest binary number, which is a tiny bit off, and the tiny errors show up when you calculate:
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
console.log(1.15 * 100);
console.log(3 * 1.1);
let total = 0;
for (let i = 0; i < 10; i++) {
total = total + 0.1;
}
console.log("ten times 0.1:", total);
node float.js and of the browser terminal0.30000000000000004 false 114.99999999999999 3.3000000000000003 ten times 0.1: 0.9999999999999999
Whole numbers do not have this problem. Every whole number up to 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER) is stored exactly, and adding, subtracting and multiplying whole numbers in that range gives exact results. That fact is the whole solution for money.
Do not "fix" it with toFixed
Rounding withtoFixed hides the error on screen but leaves it in the number, as the till showed: the display said ₦2,700.99 while the comparison saw the extra bit. toFixed also has its own surprises: (1.005).toFixed(2) gives "1.00", because 1.005 is really stored as 1.00499999…Money as whole kobo
A naira is 100 kobo. If you store every amount as a whole number of kobo, ₦1,999.99 becomes 199999, and every addition and subtraction is exact. You convert to naira only at the edges: when reading what a person typed, and when showing an amount on screen. This is how banks and payment providers such as Paystack and Stripe handle amounts: their APIs take amounts in the smallest unit (kobo, cents).
const pricesKobo = [199999, 45070, 25030];
let totalKobo = 0;
for (const price of pricesKobo) {
totalKobo = totalKobo + price;
}
const paidKobo = 270099;
console.log("total:", totalKobo);
console.log(paidKobo >= totalKobo ? "Payment complete" : "Payment short");
function formatNaira(kobo) {
return "₦" + (kobo / 100).toFixed(2);
}
console.log(formatNaira(totalKobo));
node kobo.js and of the browser terminaltotal: 270099 Payment complete ₦2700.99
The till bug is gone, and the conversion to naira happens in one place, only for display. For nicer display with thousands separators, JavaScript's built-in Intl.NumberFormat formats currency for a given locale:
const naira = new Intl.NumberFormat("en-NG", { style: "currency", currency: "NGN" });
console.log(naira.format(270099 / 100));
console.log(naira.format(5000000 / 100));
node format.js and of the browser terminal₦2,700.99 ₦50,000.00
Where rounding must happen
Addition and subtraction of kobo never need rounding. Percentages do: 7.5% VAT on 2,450 kobo is 183.75 kobo, and you cannot charge three-quarters of a kobo. So there is exactly one rounding step, and you must decide:
- How to round.
Math.roundrounds to the nearest whole number, with halves going up (183.5 becomes 184). Tax authorities and payment contracts sometimes specify a rule; follow it. - When to round: on each line of the invoice, or once on the total. The answers can differ.
To avoid a floating-point rate like 0.075, store rates as whole basis points: one basis point is 1/100 of a per cent, so 7.5% is 750 basis points, and VAT is amount * 750 / 10000. The multiplication stays in whole numbers, and there is only one division, right before the rounding.
REASON IT OUT
Round each line, or round the total?
A kiosk sells sachets of water at ₦24.50 (2,450 kobo). A customer buys 3. VAT is 7.5%. Before running any code:
- What is the exact VAT on one sachet, in kobo? On three?
- If you round the VAT on each line and add, what do you get? If you add the lines and round once?
- Which one is "right"? Who decides?
- Whatever you choose, what must be true of the numbers printed on the receipt?
Show the reasoning
Exact VAT on one sachet is 2,450 × 750 / 10,000 = 183.75 kobo; on three it is 551.25 kobo.
Rounding each line gives 184 × 3 = 552 kobo. Rounding the total gives 551 kobo. They differ by one kobo, because each line rounded up by a quarter kobo and three quarters add up to more than a half.
Neither is wrong mathematically; it is a business and legal rule, and tax rules in many countries specify one. Your job is to ask, implement the chosen rule in one function, and use that function everywhere, so that the website, the app and the receipt printer never disagree.
Whichever you choose, the receipt must add up: the printed lines must sum to the printed total. If you round per line, the total VAT is the sum of the rounded lines; you must not round the total separately and print a different number.
function vatKobo(amountKobo, rateBasisPoints) {
return Math.round(amountKobo * rateBasisPoints / 10000);
}
const sachet = 2450;
const quantity = 3;
const vatRate = 750; // 7.5%
const perLine = vatKobo(sachet, vatRate) * quantity;
const onTotal = vatKobo(sachet * quantity, vatRate);
console.log("exact VAT on one sachet:", sachet * vatRate / 10000);
console.log("rounded per line:", perLine);
console.log("rounded on total:", onTotal);
node round-when.js and of the browser terminalexact VAT on one sachet: 183.75 rounded per line: 552 rounded on total: 551
Splitting an amount without losing a kobo
Three friends split a ₦100.00 bill. ₦100 / 3 is ₦33.333…, and rounding each share to ₦33.33 collects only ₦99.99. A kobo vanished. In a payments system, vanished kobo are real money that someone must account for.
The fix works in whole kobo: give everyone the rounded-down share, work out how many kobo are left over with %, and hand those out one each to the first few people:
function splitKobo(totalKobo, people) {
const base = Math.floor(totalKobo / people);
const leftover = totalKobo % people;
const shares = [];
for (let i = 0; i < people; i++) {
shares.push(i < leftover ? base + 1 : base);
}
return shares;
}
const shares = splitKobo(10000, 3);
console.log(shares);
let sum = 0;
for (const share of shares) sum = sum + share;
console.log("sum:", sum, sum === 10000 ? "(nothing lost)" : "(MONEY LOST)");
console.log(splitKobo(100, 6));
node split.js and of the browser terminal[ 3334, 3333, 3333 ] sum: 10000 (nothing lost) [ 17, 17, 17, 17, 16, 16 ]
shares.push(x) adds x to the end of an array. The leftover is always smaller than the number of people (a remainder always is), so no one gets more than one extra kobo, and the shares always add back to the total. The same technique splits a payment between a seller and a platform fee, or spreads a discount across the lines of an invoice.
A ratio split works the same way. To share profit 2 : 3, compute each share rounded down from its fraction, then give the leftover kobo out one at a time. Exercise 1 asks you to do it.
Build: an invoice in kobo
Put the pieces together. The invoice has lines (unit price in kobo and quantity), an optional percentage discount on the subtotal, and 7.5% VAT on the discounted amount. The rules the business chose: discount and VAT are each rounded once, on the whole amount, with Math.round. The function checks its inputs first, and the output shows that the printed numbers add up:
function percentOf(amountKobo, basisPoints) {
return Math.round(amountKobo * basisPoints / 10000);
}
function formatNaira(kobo) {
return "₦" + (kobo / 100).toFixed(2);
}
function invoice(lines, discountBasisPoints, vatBasisPoints) {
let subtotal = 0;
for (const line of lines) {
if (!Number.isInteger(line.priceKobo) || line.priceKobo < 0) throw new Error("bad price: " + line.name);
if (!Number.isInteger(line.quantity) || line.quantity < 1) throw new Error("bad quantity: " + line.name);
subtotal = subtotal + line.priceKobo * line.quantity;
}
const discount = percentOf(subtotal, discountBasisPoints);
const afterDiscount = subtotal - discount;
const vat = percentOf(afterDiscount, vatBasisPoints);
const total = afterDiscount + vat;
return { subtotal, discount, vat, total };
}
const lines = [
{ name: "Rice 5kg", priceKobo: 1250000, quantity: 1 },
{ name: "Tomato tin", priceKobo: 45070, quantity: 3 },
{ name: "Water sachet", priceKobo: 2450, quantity: 3 },
];
const result = invoice(lines, 1000, 750); // 10% discount, 7.5% VAT
console.log("subtotal:", formatNaira(result.subtotal));
console.log("discount:", formatNaira(result.discount));
console.log("VAT: ", formatNaira(result.vat));
console.log("total: ", formatNaira(result.total));
console.log("adds up:", result.subtotal - result.discount + result.vat === result.total);
try {
invoice([{ name: "Bread", priceKobo: 250.3, quantity: 1 }], 0, 750);
} catch (error) {
console.log(error.message);
}
node invoice.js and of the browser terminalsubtotal: ₦13925.60 discount: ₦1392.56 VAT: ₦939.98 total: ₦13473.02 adds up: true bad price: Bread
Things to notice:
- The function returns an object:
{ subtotal, discount, vat, total }is short for{ subtotal: subtotal, discount: discount, … }. - Only two numbers are ever rounded (discount and VAT), each once, each in the same helper. Everything else is whole-number addition and multiplication, which is exact.
- A price in naira with decimals (
250.3) is refused, not silently accepted. Mixing naira and kobo is the most common money bug of all, and a guard catches it at the door.throw new Error(…)stops the function with an error message, andtry/catchcatches it; the lesson on errors covers them.
Production concerns
- Kobo everywhere inside, naira only at the edges. Name variables with their unit (
priceKobo,totalKobo) so a mix-up is visible in the code. - Currencies differ. The naira and the US dollar have 100 minor units; the Japanese yen has none, and the Kuwaiti dinar has 1,000. A system handling several currencies stores the currency next to every amount and looks up the number of minor units.
- Size limits.
Number.MAX_SAFE_INTEGERkobo is about ₦90 trillion, enough for a shop but not for every national-scale total. Beyond it, JavaScript'sBigInttype holds whole numbers of any size exactly. Databases have exact decimal types (NUMERIC), and you will use them in Databases. - One rounding rule, one function. Write the rule down (half up, per line or per total), implement it once, and test the boundary cases: exactly half a kobo, just under, just over.
- Parse carefully. A person types "2,700.99". Turning that text into 270099 kobo without going through a floating-point number (split on the dot, check the digits) is safer than
Math.round(parseFloat(text) * 100), which can land on the wrong side of a half.
Practice
TRY IT YOURSELF
Split profit by ratio
Two partners share ₦100,000.01 of profit (10,000,001 kobo) in the ratio 2 : 3. Compute each share in whole kobo so that the shares add up exactly to the total. Hint: compute the first share rounded down, and give the second partner the rest.
Show a solution
const profitKobo = 10000001;
const first = Math.floor(profitKobo * 2 / 5);
const second = profitKobo - first;
console.log(first, second);
console.log("adds up:", first + second === profitKobo);
console.log("ratio check:", (first / second).toFixed(4), "vs", (2 / 3).toFixed(4));
node ratio-split.js and of the browser terminal4000000 6000001 adds up: true ratio check: 0.6667 vs 0.6667
Computing the last share as "total minus the others" guarantees the parts add up; the leftover kobo goes to the last partner. With more partners, use the leftover-distribution idea from splitKobo. The ratio check confirms the split is still 2 : 3 to four decimals.
TRY IT YOURSELF
Reverse VAT in kobo
Shelf prices include 7.5% VAT. Write splitGross(grossKobo) returning { net, vat } in whole kobo so that net + vat is exactly the gross price. Try ₦10,750.00 and ₦24.50.
Show a solution
function splitGross(grossKobo) {
const net = Math.round(grossKobo * 10000 / 10750);
const vat = grossKobo - net;
return { net, vat };
}
console.log(splitGross(1075000));
console.log(splitGross(2450));
node split-gross.js and of the browser terminal{ net: 1000000, vat: 75000 }
{ net: 2279, vat: 171 }Solving net × 1.075 = gross gives net = gross ÷ 1.075, written with whole numbers as gross * 10000 / 10750. Only the net is rounded; the VAT is the difference, so the two always add back to the shelf price. Check by substitution: 1,000,000 + 75,000 = 1,075,000.
TRY IT YOURSELF
Best deal
A customer has a ₦10,000 basket. Offer A is 25% off. Offer B is 15% off, then another 10% off the reduced price. Offer C is ₦2,400 off. Compute the final price of each in whole naira, and print the best offer.
Show a solution
const basket = 10000;
const offerA = basket * (100 - 25) / 100;
const offerB = basket * (100 - 15) / 100 * (100 - 10) / 100;
const offerC = basket - 2400;
console.log("A:", offerA, "B:", offerB, "C:", offerC);
let best = "A";
let bestPrice = offerA;
if (offerB < bestPrice) {
best = "B";
bestPrice = offerB;
}
if (offerC < bestPrice) {
best = "C";
bestPrice = offerC;
}
console.log("best:", best, bestPrice);
node best-deal.js and of the browser terminalA: 7500 B: 7650 C: 7600 best: A 7500
Offer B looks like "25% off" but is only 23.5% off, because the second discount applies to ₦8,500, not ₦10,000. Writing the percentages as (100 - p) / 100 with whole numbers keeps the intermediate values exact here.
Recap
- An expression combines values with operators in a fixed order;
%gives the remainder.=assigns,===asks whether two values are equal. Math.flooranswers "how many can I fill or afford",Math.ceilanswers "how many do I need".- Solve equations by doing the same to both sides, then check by substituting the answer back: that check is your test.
- p% of x is x × p / 100. Successive discounts multiply; +20% then −20% does not return to the start; percentage points are not per cent.
- Floating point cannot store 0.1 exactly, so never compare money decimals with
===or>=. Store whole kobo, round once with a stated rule, and format only for display. - Split amounts with floor plus leftover distribution, or compute the last part as "total minus the others", so the parts always add up to the whole.
Next: Sequences, where numbers change step by step: instalments, savings with compound interest, and the difference between linear and exponential growth.
Test yourself
Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.