Skill: percent-of-a-number
Percent of a Number Practice
An endless supply of never-repeating percent problems with instant feedback. Benchmark percents, tricky percents, over-100% cases, discounts, and tips — one problem at a time.
1 Practice
Type your answer and press Check answer (or Enter). A wrong answer earns a hint first;
a second miss walks you through the full solution.
How to enter answers
- Plain numbers: type like
20or9. - Money: type a dollar amount like
9.90or32— the$is optional.
2 What you’ll practice
Every problem is generated fresh from 12 templates — a problem never repeats until its whole pool is used up.
Benchmark percents
50%, 10%, 25%, 75%, and 5% of friendly numbers — the 10% stepping stone does the heavy lifting.
Tricky & over-100%
15%, 35%, 45% via benchmark pieces, and percents over 100% (the answer beats the whole).
Discounts & tips
Real money: how much you save, the sale price, the tip, and the total with tip.
3 Watch out for these
The three mistakes students make most often with percents.
Wrong: 25% of 80 = 2.5 × 80 = 200
Right: 25% → 0.25, so 0.25 × 80 = 20. Divide by 100, not by 10.
Wrong: “20% off $40” → $8
Right: $8 is the savings; the price is $40 − $8 = $32.
Wrong: “150% of 40 can’t top 40”
Right: 150% = 1.5, and 1.5 × 40 = 60. Over 100% means more than the whole.
/* Fibo percent-of-a-number practice core (pure logic, no DOM). * UMD: works in node (require) and in the browser (window.PercentPractice). * Money is ALWAYS integer cents. Every generator below is built so the * answer is exact: integer answers for kind 'num', exact cents for 'money'. * API matches the shared page_practice.js glue: * new PracticeSession(seed) -> .next() / .submit(raw) / .reveal() * problem: { promptHTML, tag, answer:{kind,v|cents}, hintHTML, whyHTML, * answerDisplayHTML, accept[], templateId, key, inputs } */ (function (root, factory) { var api = factory(); if (typeof module === 'undefined') { root.PercentPractice = api; } else { module.exports = api; } })(typeof self !== 'undefined' ? self : this, function () { 'use strict';
var PCT = '%'; var MINUS = '\u2212';
function mulberry32(seed) { var a = seed >>> 0; return function () { a |= 0; a = (a + 0x6D2B79F5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function ri(rand, lo, hi) { return lo + Math.floor(rand() * (hi - lo + 1)); } function pick(rand, arr) { return arr[Math.floor(rand() * arr.length)]; } function shuffle(arr, rand) { for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(rand() * (i + 1)); var t = arr[i]; arr[i] = arr[j]; arr[j] = t; } return arr; }
/* money helpers: cents <-> "$d.cc" / "d" */ function moneyStr(cents) { return '$' + (cents / 100).toFixed(2); } function priceStr(cents) { return cents % 100 === 0 ? '$' + (cents / 100) : moneyStr(cents); } function numSpan(v) { return '' + v + ''; } function moneySpan(cents) { return '' + priceStr(cents) + ''; }
/* parse one answer: kind 'num' (plain number) or 'money' (dollar amount) */ function parseAnswer(raw, kind) { var s = String(raw).trim().replace(/[$,\s]+/g, ''); if (kind === 'money') { if (!/^[+-]?\d+(\.\d{1,2})?$/.test(s)) return null; var c = Math.round(parseFloat(s) * 100); if (!isFinite(c)) return null; return c; } if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(s)) return null; var v = parseFloat(s); if (!isFinite(v)) return null; return v; }
/* ---------------- problem assembly ---------------- */ function finishProblem(prompt, ans, hint, steps, tag, inputs) { var disp, plain, v; if (ans.kind === 'money') { v = ans.cents; disp = '' + moneyStr(v) + ''; plain = (v / 100).toFixed(2); } else { v = ans.v; disp = '' + v + ''; plain = String(v); } var why = '
- ' +
steps.map(function (s) { return '
- ' + s + '
'; }).join('') + '
' + '
' + '
'; return { promptHTML: prompt, tag: tag, answer: ans, hintHTML: hint, whyHTML: why, answerDisplayHTML: disp, accept: [plain], inputs: inputs || {} }; }
var HINT_BENCH = 'Hint: 10' + PCT + ' of a number is easy: just move the decimal point one place left. ' + '50' + PCT + ' is half, 25' + PCT + ' is a quarter, and 5' + PCT + ' is half of 10' + PCT + '.'; var HINT_CONV = 'Hint: change the percent to a decimal first (divide by 100: 25' + PCT + ' → 0.25), ' + 'then multiply by the number.'; var HINT_MONEY = 'Hint: find the percent of the price first (that is the ' + 'discount or the tip), then add or subtract from the bill.';
/* ---------------- the 12 templates ---------------- */ var TEMPLATES = [ { id: 'BENCH50', label: 'Benchmark percents', enumerate: function (rand) { var out = []; for (var n = 12; n <= 200; n += 2) out.push({ n: n }); return shuffle(out, rand); }, make: function (p) { return finishProblem( 'What is 50' + PCT + ' of ' + numSpan(p.n) + '?', { kind: 'num', v: p.n / 2 }, HINT_BENCH, ['50' + PCT + ' means one half.', 'Half of ' + p.n + ' is ' + (p.n / 2) + '.'], 'Benchmark percents', { n: p.n }); } }, { id: 'BENCH10', label: 'Benchmark percents', enumerate: function (rand) { var out = []; for (var n = 10; n <= 200; n += 10) out.push({ n: n }); return shuffle(out, rand); }, make: function (p) { return finishProblem( 'What is 10' + PCT + ' of ' + numSpan(p.n) + '?', { kind: 'num', v: p.n / 10 }, HINT_BENCH, ['10' + PCT + ' of a number: move the decimal one place left.', '10' + PCT + ' of ' + p.n + ' is ' + (p.n / 10) + '.'], 'Benchmark percents', { n: p.n }); } }, { id: 'BENCH25', label: 'Benchmark percents', enumerate: function (rand) { var out = []; for (var n = 4; n <= 200; n += 4) out.push({ n: n }); return shuffle(out, rand); }, make: function (p) { return finishProblem( 'What is 25' + PCT + ' of ' + numSpan(p.n) + '?', { kind: 'num', v: p.n / 4 }, HINT_BENCH, ['25' + PCT + ' = 25/100 = 1/4.', 'A quarter of ' + p.n + ' is ' + (p.n / 4) + '.'], 'Benchmark percents', { n: p.n }); } }, { id: 'BENCH75', label: 'Benchmark percents', enumerate: function (rand) { var out = []; for (var n = 4; n <= 200; n += 4) out.push({ n: n }); return shuffle(out, rand); }, make: function (p) { return finishProblem( 'What is 75' + PCT + ' of ' + numSpan(p.n) + '?', { kind: 'num', v: 3 * p.n / 4 }, HINT_BENCH, ['75' + PCT + ' = 3/4: find a quarter, then triple it.', 'A quarter of ' + p.n + ' is ' + (p.n / 4) + '; three quarters is ' + (3 * p.n / 4) + '.'], 'Benchmark percents', { n: p.n }); } }, { id: 'FIVE', label: 'Benchmark percents', enumerate: function (rand) { var out = []; for (var n = 20; n <= 300; n += 20) out.push({ n: n }); return shuffle(out, rand); }, make: function (p) { return finishProblem( 'What is 5' + PCT + ' of ' + numSpan(p.n) + '?', { kind: 'num', v: p.n / 20 }, HINT_BENCH, ['5' + PCT + ' is half of 10' + PCT + '.', '10' + PCT + ' of ' + p.n + ' is ' + (p.n / 10) + '; half of that is ' + (p.n / 20) + '.'], 'Benchmark percents', { n: p.n }); } }, { id: 'OVER100', label: 'Tricky & over-100' + PCT, enumerate: function (rand) { var out = []; var ps = [110, 120, 125, 150, 175, 200]; for (var n = 20; n <= 120; n += 20) { for (var i = 0; i < ps.length; i++) { if ((ps[i] * n) % 100 === 0) out.push({ p: ps[i], n: n }); } } return shuffle(out, rand); }, make: function (p) { var v = p.p * p.n / 100; return finishProblem( 'What is ' + p.p + PCT + ' of ' + numSpan(p.n) + '?', { kind: 'num', v: v }, 'Hint: ' + p.p + PCT + ' is more than the whole, so expect an answer bigger than ' + p.n + '. ' + HINT_CONV, [p.p + PCT + ' → ' + (p.p / 100) + ' as a decimal.', (p.p / 100) + ' × ' + p.n + ' = ' + v + '.', 'That beats ' + p.n + ' — which is fine, since ' + p.p + PCT + ' is more than 100' + PCT + '.'], 'Tricky & over-100' + PCT, { p: p.p, n: p.n }); } }, { id: 'TRICKY', label: 'Tricky & over-100' + PCT, enumerate: function (rand) { var out = []; var ps = [15, 35, 45, 65, 85]; for (var n = 20; n <= 120; n += 20) out.push({ p: ps[Math.floor(rand() * ps.length)], n: n }); return shuffle(out, rand); }, make: function (p) { var v = p.p * p.n / 100; var ten = p.n / 10, five = p.n / 20; return finishProblem( 'What is ' + p.p + PCT + ' of ' + numSpan(p.n) + '? (Try the 10' + PCT + ' benchmark.)', { kind: 'num', v: v }, HINT_BENCH, ['10' + PCT + ' of ' + p.n + ' is ' + ten + '; 5' + PCT + ' of ' + p.n + ' is ' + five + '.', 'Build ' + p.p + PCT + ' from those pieces: ' + p.p + PCT + ' of ' + p.n + ' = ' + v + '.', 'Check with a decimal: 0.' + (p.p < 100 ? String(p.p).padStart(2, '0') : p.p) + ' × ' + p.n + ' = ' + v + '.'], 'Tricky & over-100' + PCT, { p: p.p, n: p.n }); } }, { id: 'DISC_AMT', label: 'Discounts & tips', enumerate: function (rand) { var items = ['jacket', 'headphones', 'bike', 'watch', 'backpack', 'lamp']; var out = []; var ps = [10, 15, 20, 25, 30, 40, 50]; for (var i = 0; i < 40; i++) { var cents = ri(rand, 10, 120) * 100; // whole dollars: exact cents var p = pick(rand, ps); out.push({ cents: cents, p: p, item: pick(rand, items) }); } return shuffle(out, rand); }, make: function (p) { var save = Math.round(p.p * p.cents / 100); return finishProblem( 'A ' + moneySpan(p.cents) + ' ' + p.item + ' is ' + p.p + PCT + ' off. How much do you save?', { kind: 'money', cents: save }, HINT_MONEY, ['"Save" means the discount: ' + p.p + PCT + ' of ' + moneyStr(p.cents) + '.', p.p + PCT + ' → ' + (p.p / 100) + '; ' + (p.p / 100) + ' × ' + moneyStr(p.cents) + ' = ' + moneyStr(save) + '.'], 'Discounts & tips', { cents: p.cents, p: p.p }); } }, { id: 'SALE_PRICE', label: 'Discounts & tips', enumerate: function (rand) { var items = ['jacket', 'headphones', 'bike', 'watch', 'backpack', 'lamp']; var out = []; var ps = [10, 15, 20, 25, 30, 40, 50]; for (var i = 0; i < 40; i++) { var cents = ri(rand, 10, 120) * 100; var p = pick(rand, ps); out.push({ cents: cents, p: p, item: pick(rand, items) }); } return shuffle(out, rand); }, make: function (p) { var save = Math.round(p.p * p.cents / 100); var price = p.cents - save; return finishProblem( 'A ' + moneySpan(p.cents) + ' ' + p.item + ' is ' + p.p + PCT + ' off. What is the sale price?', { kind: 'money', cents: price }, HINT_MONEY, ['First the discount: ' + p.p + PCT + ' of ' + moneyStr(p.cents) + ' = ' + moneyStr(save) + '.', 'Subtract: ' + moneyStr(p.cents) + ' − ' + moneyStr(save) + ' = ' + moneyStr(price) + '.', 'Shortcut: pay ' + (100 - p.p) + PCT + ' → ' + ((100 - p.p) / 100) + ' × ' + moneyStr(p.cents) + ' = ' + moneyStr(price) + '.'], 'Discounts & tips', { cents: p.cents, p: p.p }); } }, { id: 'TIP_AMT', label: 'Discounts & tips', enumerate: function (rand) { var out = []; var ps = [10, 15, 18, 20, 25]; for (var i = 0; i < 40; i++) { var cents = ri(rand, 10, 90) * 100; // whole dollars: exact cents var p = pick(rand, ps); out.push({ cents: cents, p: p }); } return shuffle(out, rand); }, make: function (p) { var tip = Math.round(p.p * p.cents / 100); return finishProblem( 'The bill is ' + moneySpan(p.cents) + ' and you leave a ' + p.p + PCT + ' tip. How much is the tip?', { kind: 'money', cents: tip }, HINT_MONEY, ['Tip = ' + p.p + PCT + ' of ' + moneyStr(p.cents) + '.', p.p + PCT + ' → ' + (p.p / 100) + '; ' + (p.p / 100) + ' × ' + moneyStr(p.cents) + ' = ' + moneyStr(tip) + '.'], 'Discounts & tips', { cents: p.cents, p: p.p }); } }, { id: 'TIP_TOTAL', label: 'Discounts & tips', enumerate: function (rand) { var out = []; var ps = [10, 15, 18, 20, 25]; for (var i = 0; i < 40; i++) { var cents = ri(rand, 10, 90) * 100; var p = pick(rand, ps); out.push({ cents: cents, p: p }); } return shuffle(out, rand); }, make: function (p) { var tip = Math.round(p.p * p.cents / 100); var total = p.cents + tip; return finishProblem( 'The bill is ' + moneySpan(p.cents) + ' with a ' + p.p + PCT + ' tip. What is the total?', { kind: 'money', cents: total }, HINT_MONEY, ['Tip = ' + p.p + PCT + ' of ' + moneyStr(p.cents) + ' = ' + moneyStr(tip) + '.', 'Total = bill + tip = ' + moneyStr(p.cents) + ' + ' + moneyStr(tip) + ' = ' + moneyStr(total) + '.'], 'Discounts & tips', { cents: p.cents, p: p.p }); } }, { id: 'WORD_SAVE', label: 'Discounts & tips', enumerate: function (rand) { var items = ['soccer ball', 'board game', 'water bottle', 'puzzle', 'kite', 'storybook']; var out = []; var ps = [10, 20, 25, 30, 50]; for (var i = 0; i < 30; i++) { var cents = ri(rand, 8, 60) * 100; var p = pick(rand, ps); out.push({ cents: cents, p: p, item: pick(rand, items) }); } return shuffle(out, rand); }, make: function (p) { var save = Math.round(p.p * p.cents / 100); return finishProblem( 'Maya buys a ' + p.item + ' for ' + moneySpan(p.cents) + '. The store takes ' + p.p + PCT + ' off. ' + 'How much does she save?', { kind: 'money', cents: save }, HINT_MONEY, ['Savings = ' + p.p + PCT + ' of ' + moneyStr(p.cents) + '.', p.p + PCT + ' → ' + (p.p / 100) + '; ' + (p.p / 100) + ' × ' + moneyStr(p.cents) + ' = ' + moneyStr(save) + '.'], 'Discounts & tips', { cents: p.cents, p: p.p }); } } ];
/* ---------------- session: scoring, streaks, no-repeat ---------------- */
function PracticeSession(seed) {
this._rand = mulberry32(seed >>> 0);
this.pools = {};
this.seen = {};
this.score = 0;
this.streak = 0;
this.problemNum = 0;
this.current = null;
this.wrongCount = 0;
this.revealed = false;
this._done = false;
}
PracticeSession.prototype._draw = function () {
for (var guard = 0; guard < 200; guard++) {
var t = TEMPLATES[Math.floor(this._rand() * TEMPLATES.length)];
var pool = this.pools[t.id];
if (!pool || pool.length === 0) {
pool = t.enumerate(this._rand).slice();
this.pools[t.id] = pool;
}
var p = pool.pop();
var key = t.id + '|' + JSON.stringify(p);
if (this.seen[key]) continue;
this.seen[key] = 1;
var prob = t.make(p);
prob.templateId = t.id;
prob.key = key;
return prob;
}
this.pools = {}; this.seen = {};
var t2 = TEMPLATES[0], p2 = t2.enumerate(this._rand)[0];
var pr = t2.make(p2);
pr.templateId = t2.id; pr.key = t2.id + '|' + JSON.stringify(p2);
return pr;
};
PracticeSession.prototype.next = function () {
this.current = this._draw();
this.wrongCount = 0;
this.revealed = false;
this._done = false;
this.problemNum++;
return this.current;
};
function answersEqual(pr, parsed) {
if (parsed === null) return false;
if (pr.answer.kind === 'money') return parsed === pr.answer.cents;
return parsed === pr.answer.v;
}
PracticeSession.prototype.submit = function (raw) {
var pr = this.current;
if (!pr) return { status: 'none' };
if (this._done) return { status: 'already' };
var parsed = parseAnswer(raw, pr.answer.kind);
if (parsed === null) return { status: 'invalid' };
var ok = answersEqual(pr, parsed);
if (ok) {
this._done = true;
var firstTry = !(this.wrongCount !== 0 || this.revealed);
if (!this.revealed) {
this.streak++;
if (firstTry) this.score++;
}
return { status: 'correct', firstTry: firstTry, score: this.score, streak: this.streak, answerHTML: pr.answerDisplayHTML };
}
this.wrongCount++;
this.streak = 0;
if (this.wrongCount === 1) return { status: 'hint', hintHTML: pr.hintHTML, streak: 0 };
this.revealed = true;
return { status: 'why', whyHTML: pr.whyHTML, streak: 0 };
};
PracticeSession.prototype.reveal = function () {
if (!this.current) return null;
this.revealed = true;
return this.current.whyHTML;
};
return {
PracticeSession: PracticeSession,
TEMPLATES: TEMPLATES,
parseAnswer: parseAnswer,
moneyStr: moneyStr,
priceStr: priceStr
};
});
/* Shared Fibo practice page glue. Config (set by an inline script before this file): window.FIBO_PRACTICE = { core: 'IntegerPractice', placeholder: '...', entryHelpHTML: '...' } The core must expose: PracticeSession, with .next()/.submit()/.reveal(). Problem objects must provide: tag, promptHTML, answer.kind, hintHTML, whyHTML, answerDisplayHTML. Answers are checked inside the core. */ (function () { 'use strict'; var CFG = window.FIBO_PRACTICE || {}; var CORE = window[CFG.core || '']; if (!CORE) { return; } var session = new CORE.PracticeSession((Date.now() % 2000000000) + 7); function el(id) { return document.getElementById(id); } var input = el('ansInput'), checkBtn = el('checkBtn'), anotherBtn = el('anotherBtn'), showSolBtn = el('showSolBtn'), promptTag = el('promptTag'), promptEl = el('promptEl'), fb = el('fb'), hintBox = el('hintBox'), whyBox = el('whyBox'), statsLine = el('statsLine');
function updateStats() { statsLine.innerHTML = 'Score ' + session.score + ' · Streak ' + session.streak + ' · Problem ' + session.problemNum + ''; } function clearPanels() { fb.className = 'fb'; fb.innerHTML = ''; hintBox.className = 'hintbox'; hintBox.innerHTML = ''; whyBox.className = 'whybox'; whyBox.innerHTML = ''; } function showFb(kind, html) { fb.className = 'fb show ' + kind; fb.innerHTML = html; } function entryHint() { return CFG.entryHelpHTML || 'Type your answer and press Check answer.'; } function newProblem() { var pr = session.next(); promptTag.textContent = pr.tag; promptEl.innerHTML = pr.promptHTML; input.value = ''; if (CFG.placeholder) { input.placeholder = CFG.placeholder; } clearPanels(); showSolBtn.style.display = ''; updateStats(); input.focus(); } function showWhy() { whyBox.className = 'whybox show'; whyBox.innerHTML = '
' + session.current.whyHTML; showSolBtn.style.display = 'none'; } checkBtn.addEventListener('click', function () { var r = session.submit(input.value); updateStats(); if (r.status === 'invalid') { showFb('warn', 'Could not read that. ' + entryHint()); return; } if (r.status === 'already' || r.status === 'none') { return; } if (r.status === 'correct') { if (r.firstTry) { showFb('ok', 'Correct! +1 point. The answer is ' + r.answerHTML + ''); } else if (session.revealed) { showFb('ok', 'That is right. You already saw the solution, so the streak is unchanged. ' + 'Hit Try another for a fresh problem.'); } else { showFb('ok', 'Correct! The answer is ' + r.answerHTML + '. ' + 'Solved after a hint, so no point this time — but the streak grows.'); } showSolBtn.style.display = 'none'; return; } if (r.status === 'hint') { showFb('no', 'Not quite. Try again — here is a hint.'); hintBox.className = 'hintbox show'; hintBox.innerHTML = r.hintHTML; input.focus(); input.select(); return; } if (r.status === 'why') { showFb('no', 'Not quite. Let us walk through it step by step.'); showWhy(); return; } }); anotherBtn.addEventListener('click', newProblem); showSolBtn.addEventListener('click', function () { session.reveal(); showWhy(); updateStats(); }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { checkBtn.click(); } }); newProblem(); })();