From 7efd118d4fa6a4003a703014d946eac229e90056 Mon Sep 17 00:00:00 2001 From: SalahAlsabhi Date: Fri, 6 Nov 2026 23:37:33 +0000 Subject: [PATCH] Complete Sprint 2 exercises --- Sprint-2/debug/address.js | 7 +++++- Sprint-2/debug/author.js | 10 +++++++- Sprint-2/debug/recipe.js | 8 +++++- Sprint-2/implement/contains.js | 8 +++++- Sprint-2/implement/contains.test.js | 34 +++++++++++++++++++++++++- Sprint-2/implement/lookup.js | 9 +++++-- Sprint-2/implement/lookup.test.js | 9 ++++++- Sprint-2/implement/querystring.js | 34 ++++++++++++++++++++++++-- Sprint-2/implement/tally.js | 11 ++++++++- Sprint-2/implement/tally.test.js | 19 ++++++++++++++- Sprint-2/interpret/invert.js | 38 +++++++++++++++++++++++------ Sprint-2/stretch/count-words.js | 21 ++++++++++++++++ Sprint-2/stretch/mode.js | 24 +++++++++++------- Sprint-2/stretch/till.js | 38 +++++++++++++++++++++-------- 14 files changed, 231 insertions(+), 39 deletions(-) diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..4694634ff 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -12,4 +12,9 @@ const address = { postcode: "XYZ 123", }; -console.log(`My house number is ${address[0]}`); +console.log(`My house number is ${address.houseNumber}`); + + +/* BUG: address[0] is undefined because objects aren't indexed by number. + FIX: useed the property name: address.houseNumber +console.log(`My house number is ${address.houseNumber}`); diff --git a/Sprint-2/debug/author.js b/Sprint-2/debug/author.js index 8c2125977..8bc5d91f0 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -11,6 +11,14 @@ const author = { alive: true, }; -for (const value of author) { +for (const value of Object.values(author)) { console.log(value); } + + +/* BUG: Objects are not iterable, so `for...of author` throws TypeError. + FIX: Used Object.values(author) to loop over the property values. +for (const value of Object.values(author)) { + console.log(value); +} +*/ diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..8c934063e 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -12,4 +12,10 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients.join("\n")}`); + + + +// BUG: ${recipe} logs out the whole object as "[object Object]". +// FIX: print title/serves from their properties, and ingredients with join("\n") +// so each item is on its own line. diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..fa093df68 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,9 @@ -function contains() {} +function contains(obj, prop) { + if (obj === null || typeof obj !== "object" || Array.isArray(obj)) { + throw new Error("contains requires an object"); + } + return Object.hasOwn(obj, prop); +} module.exports = contains; + diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 719272787..bdcf0f9be 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -23,18 +23,50 @@ as an array isn't an object // Given an empty object // When passed to contains // Then it should return false -test.todo("contains on empty object returns false"); +test("contains on empty object returns false", () => { + expect(contains({}, "a")).toBe(false); +}); // Given an object with properties // When passed to contains with an existing property name // Then it should return true +test("returns true when property exists", () => { + expect(contains({ a: 1, b: 2 }, "a")).toBe(true); +}); // Given an object with properties // When passed to contains with a non-existent property name // Then it should return false +test("returns false when property does not exist", () => { + expect(contains({ a: 1, b: 2 }, "c")).toBe(false); +}); // Given a value that isn't an object - an array, a string, a number, // null, or no argument at all // When passed to contains // Then it should throw Error("contains requires an object") // (careful: typeof [] and typeof null are both "object") + +test("throws an error when given an array", () => { + expect(() => contains([3, 4, 5], "a")).toThrow( + new Error("contains requires an object") + ); +}); + +test("throws an error when given a null", () => { + expect(() => contains(null, "a")).toThrow( + new Error("contains requires an object") + ); +}); + +test("throws an error when given a string", () => { + expect(() => contains("hellow", "a")).toThrow( + new Error("contains requires an object") + ); +}); + +test("throws an error when given no argument", () => { + expect(() => contains()).toThrow( + new Error("contains requires an object") + ); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..6d82731be 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,10 @@ -function createLookup() { - // implementation here + +function createLookup(pairs) { + const lookup = {}; + for (const [country, currency] of pairs) { + lookup[country] = currency; + } + return lookup; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..9e3b4a4f9 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,6 +1,13 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); +test("creates a country currency code lookup for multiple codes", () => { + expect( + createLookup([ + ["US", "USD"], + ["CA", "CAD"], + ]) + ).toEqual({ US: "USD", CA: "CAD" }); +}); /* diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..b94400b90 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,16 +1,46 @@ function parseQueryString(queryString) { const queryParams = {}; + if (queryString.length === 0) { return queryParams; } + const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; + if (pair === "") { + continue; + } + + const eqIndex = pair.indexOf("="); + let key; + let value; + + if (eqIndex === -1) { + key = pair; + value = ""; + } else { + key = pair.slice(0, eqIndex); + value = pair.slice(eqIndex + 1); + } + + key = decodeURIComponent(key.replaceAll("+", " ")); + value = decodeURIComponent(value.replaceAll("+", " ")); + + if (Object.hasOwn(queryParams, key)) { + if (Array.isArray(queryParams[key])) { + queryParams[key].push(value); + } else { + queryParams[key] = [queryParams[key], value]; + } + } else { + queryParams[key] = value; + } } return queryParams; } module.exports = parseQueryString; + + diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..09c6f37b2 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,12 @@ -function tally() {} +function tally(items) { + if (!Array.isArray(items)) { + throw new Error("tally requires an array"); + } + const counts = {}; + for (const item of items) { + counts[item] = (counts[item] ?? 0) + 1; + } + return counts; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index ca763c296..5f7f1bac3 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -23,12 +23,29 @@ const tally = require("./tally.js"); // Given an empty array // When passed to tally // Then it should return an empty object -test.todo("tally on an empty array returns an empty object"); +test("tally on an empty array returns an empty object", () => { + expect(tally([])).toEqual({}); +}); // Given an array with duplicate items // When passed to tally // Then it should return counts for each unique item +test("tally on an array with duplicate items, return counts for each unique item", () => { + expect(tally(["a", "a", "c", "b", "c", "b", "c", "e"])).toEqual({ a: 2, c: 3, b: 2, e: 1 }); +}); // Given an invalid input like a string, a number, or no argument at all // When passed to tally // Then it should throw Error("tally requires an array") +test("tally on a string, thows an error", () => { + expect(() => tally("hello")).toThrow(new Error("tally requires an array")); +}); + +test("tally on a number, thows an error", () => { + expect(() => tally(10)).toThrow(new Error("tally requires an array")); +}); + +test("tally on no argument, thows an error", () => { + expect(() => tally()).toThrow(new Error("tally requires an array")); +}); + diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..0695413a2 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -6,24 +6,46 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} -function invert(obj) { +export function invert(obj) { const invertedObj = {}; for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; + invertedObj[value] = key; } return invertedObj; } -// a) What is the current return value when invert is called with { a : 1 } +/* +a) Current return value for invert({ a: 1 }) with the OLD bug: + { key: 1 } + +b) Current return value for invert({ a: 1, b: 2 }) with the OLD bug: + { key: 2 } + (each loop overwrites the same property named "key"; last value wins) + +c) Target return value for invert({ a: 1, b: 2 }): + { "1": "a", "2": "b" } + +Object.entries returns an array of [key, value] pairs for the object. +It is needed so the loop can read both the key and the value of each property. + +d) invertedObj.key always writes to a property literally named "key". + It ignores the loop variable `key`, so keys and values are never swapped, + and earlier entries are overwritten. The result is a new object with only + that one "key" property — not the original object, and not an inverted map. + +e) Fixed by using invertedObj[value] = key (bracket notation with the value + as the new key). Tests in invert.test.js prove the fix. +*/ + + + + + + -// b) What is the current return value when invert is called with { a: 1, b: 2 } -// c) What is the target return value when invert is called with {a : 1, b: 2} -// c) What does Object.entries return? Why is it needed in this program? -// d) Explain why the current return value is different from the target output -// e) Fix the implementation of invert (and write tests to prove it's fixed!) diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..22f6a1292 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,24 @@ 3. Order the results to find out which word is the most common in the input */ + +function countWords(str) { + const counts = {}; + if (str.trim() === "") { + return counts; + } + + const words = str + .toLowerCase() + .replaceAll(/[.,!?]/g, "") + .split(/\s+/) + .filter((w) => w.length > 0); + + for (const word of words) { + counts[word] = (counts[word] ?? 0) + 1; + } + + return counts; +} + +module.exports = countWords; diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 9699bf307..729923ed4 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -12,8 +12,7 @@ // The tests must still pass after your refactor. Run them again to check. -function calculateMode(list) { - // check the input is a non-empty array of numbers +function assertValidNumbers(list) { if (!Array.isArray(list)) { throw new Error("calculateMode requires an array of numbers"); } @@ -25,25 +24,32 @@ function calculateMode(list) { throw new Error("calculateMode requires an array of numbers"); } } +} - // track frequency of each value - let freqs = new Map(); - - for (let num of list) { +function trackFrequencies(list) { + const freqs = new Map(); + for (const num of list) { freqs.set(num, (freqs.get(num) || 0) + 1); } + return freqs; +} - // Find the value with the highest frequency +function findMode(freqs) { let maxFreq = 0; let mode; - for (let [num, freq] of freqs) { + for (const [num, freq] of freqs) { if (freq > maxFreq) { mode = num; maxFreq = freq; } } - return mode; } +function calculateMode(list) { + assertValidNumbers(list); + const freqs = trackFrequencies(list); + return findMode(freqs); +} + module.exports = calculateMode; diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..7a3daca48 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -1,5 +1,5 @@ // totalTill takes an object representing coins in a till - +// // Given an object of coins // When this till object is passed to totalTill // Then it should return the total amount in pounds @@ -8,24 +8,42 @@ function totalTill(till) { let total = 0; for (const [coin, quantity] of Object.entries(till)) { - total += coin * quantity; + const valueInPence = Number(coin.replace("p", "")); + total += valueInPence * quantity; } return `£${total / 100}`; } +module.exports = totalTill; + const till = { "1p": 10, "5p": 6, "50p": 4, "20p": 10, }; -const totalAmount = totalTill(till); - -// a) What is the target output when totalTill is called with the till object - -// b) Why do we need to use Object.entries inside the for...of loop in this function? - -// c) What does coin * quantity evaluate to inside the for...of loop? -// d) Write a test for this function to check it works and then fix the implementation of totalTill +// a) Target output when totalTill is called with the till object above: +// "£3.4" +// Calculation: (10*1) + (6*5) + (4*50) + (10*20) = 10 + 30 + 200 + 200 = 340 pence +// 340 / 100 = 3.4 → "£3.4" + +// b) Object.entries(till) turns the object into an array of [key, value] pairs +// e.g. [["1p", 10], ["5p", 6], ...]. +// for...of needs something iterable; Object.entries lets us read both the +// coin label and its quantity in each iteration. + +// c) Before the fix: coin * quantity was "1p" * 10 (string * number) → NaN. +// After the fix: valueInPence * quantity is e.g. 1 * 10, 5 * 6, 50 * 4, 20 * 10 +// (numbers only), and those products are added into total. + +// d) Example test (till.test.js): +// +// const totalTill = require("./till.js"); +// +// test("sums coins in the till as pounds", () => { +// expect( +// totalTill({ "1p": 10, "5p": 6, "50p": 4, "20p": 10 }) +// ).toBe("£3.4"); +// });