From 95c4e7521db95a021f5920fab7a0e19d13a2090e Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 19:25:41 +0000 Subject: [PATCH 01/10] Fix address debug exercise --- Sprint-2/debug/address.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..7385b5e4c 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,8 +1,9 @@ // Predict and explain first... // This code should log out the houseNumber from the address object -// but it isn't working... -// Fix anything that isn't working +// but it isn't working. +// The bug was that the code tried to access the object using index [0]. +// Objects use property names instead of numeric indexes. const address = { houseNumber: 42, @@ -12,4 +13,5 @@ const address = { postcode: "XYZ 123", }; -console.log(`My house number is ${address[0]}`); +// Correctly access the houseNumber property +console.log(`My house number is ${address.houseNumber}`); From a4f6039b26888739b582eb7d329b9b3eb82f7c65 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 19:29:31 +0000 Subject: [PATCH 02/10] Fix author debug exercise --- Sprint-2/debug/author.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sprint-2/debug/author.js b/Sprint-2/debug/author.js index 8c2125977..5145b976a 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,7 +1,8 @@ // Predict and explain first... // This program attempts to log out all the property values in the object. -// But it isn't working. Explain why first and then fix the problem +// It was not working because plain objects are not iterable with for...of. +// To loop through the values, we first convert them into an array using Object.values(). const author = { firstName: "Zadie", @@ -11,6 +12,7 @@ const author = { alive: true, }; -for (const value of author) { +// Loop through all the values in the object +for (const value of Object.values(author)) { console.log(value); } From 6543d0ff151df6a9b596e5f1fd423c5d388ddd43 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 19:32:11 +0000 Subject: [PATCH 03/10] Fix recipe debug exercise --- Sprint-2/debug/recipe.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..7b43deb42 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -1,8 +1,9 @@ // Predict and explain first... // This program should log out the title, how many it serves and the ingredients. -// Each ingredient should be logged on a new line -// How can you fix it? +// Each ingredient should be logged on a new line. +// The original code tried to print the entire recipe object, +// which resulted in "[object Object]" instead of the ingredients. const recipe = { title: "bruschetta", @@ -10,6 +11,11 @@ const recipe = { ingredients: ["olive oil", "tomatoes", "salt", "pepper"], }; -console.log(`${recipe.title} serves ${recipe.serves} - ingredients: -${recipe}`); +// Print title and serving size +console.log(`${recipe.title} serves ${recipe.serves}`); +console.log("ingredients:"); + +// Loop through the ingredients array and print each one +for (const ingredient of recipe.ingredients) { + console.log(ingredient); +} From eb9641b742609bd8eeecf1d778a7e18cc5f098b3 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 19:53:59 +0000 Subject: [PATCH 04/10] Implement contains function and tests --- Sprint-2/implement/contains.js | 26 +++++++++++++++++- Sprint-2/implement/contains.test.js | 42 +++++++++++++---------------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..1527711b2 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,27 @@ -function contains() {} +/** + * contains() + * + * Checks whether an object contains a specific property. + * + * @param {object} obj - The object to check. + * @param {string} propertyName - The property name we want to check. + * @returns {boolean} True if the property exists, otherwise false. + */ + +function contains(obj, propertyName) { + // Validate that obj is actually an object + // and not null or an array + if (obj === null || typeof obj !== "object" || Array.isArray(obj)) { + return false; + } + + // Validate propertyName + if (typeof propertyName !== "string" || propertyName.length === 0) { + return false; + } + + // Check if the object has the property as its own key + return Object.prototype.hasOwnProperty.call(obj, propertyName); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..67bbc115f 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -4,32 +4,28 @@ const contains = require("./contains.js"); Implement a function called contains that checks an object contains a particular property -E.g. contains({a: 1, b: 2}, 'a') // returns true -as the object contains a key of 'a' - -E.g. contains({a: 1, b: 2}, 'c') // returns false -as the object doesn't contains a key of 'c' +E.g. contains({a: 1, b: 2}, "a") // returns true +E.g. contains({a: 1, b: 2}, "c") // returns false */ -// Acceptance criteria: - -// Given a contains function -// When passed an object and a property name -// Then it should return true if the object contains the property, false otherwise +describe("contains()", () => { + test("returns false for an empty object", () => { + expect(contains({}, "a")).toBe(false); + }); -// Given an empty object -// When passed to contains -// Then it should return false -test.todo("contains on empty object returns false"); + test("returns true when the property exists", () => { + expect(contains({ a: 1, b: 2 }, "a")).toBe(true); + }); -// Given an object with properties -// When passed to contains with an existing property name -// Then it should return true + test("returns false when the property does not exist", () => { + expect(contains({ a: 1, b: 2 }, "c")).toBe(false); + }); -// Given an object with properties -// When passed to contains with a non-existent property name -// Then it should return false + test("returns false when given an array", () => { + expect(contains(["a", "b"], "0")).toBe(false); + }); -// Given invalid parameters like an array -// When passed to contains -// Then it should return false or throw an error + test("returns false when given null", () => { + expect(contains(null, "a")).toBe(false); + }); +}); From de8ca144413363cb13e969b415be8cc9001ec085 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 20:03:59 +0000 Subject: [PATCH 05/10] Implement lookup function and tests --- Sprint-2/implement/lookup.js | 36 ++++++++++++++++- Sprint-2/implement/lookup.test.js | 65 +++++++++++++++---------------- 2 files changed, 66 insertions(+), 35 deletions(-) diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..9e424317c 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,37 @@ -function createLookup() { - // implementation here +/** + * createLookup() + * + * Converts an array of [key, value] pairs into a lookup object. + * + * Example: + * [['US', 'USD'], ['CA', 'CAD']] + * + * Returns: + * { US: 'USD', CA: 'CAD' } + */ + +function createLookup(pairs) { + // Ensure the input is an array + if (!Array.isArray(pairs)) { + throw new Error("Expected an array of pairs"); + } + + const lookup = {}; + + // Loop through each pair + for (const pair of pairs) { + // Validate that each pair has exactly two values + if (!Array.isArray(pair) || pair.length !== 2) { + throw new Error("Each item must be a [key, value] pair"); + } + + const [key, value] = pair; + + // Add to lookup object + lookup[key] = value; + } + + return lookup; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..931d21c94 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,35 +1,34 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); - -/* - -Create a lookup object of key value pairs from an array of code pairs - -Acceptance Criteria: - -Given - - An array of arrays representing country code and currency code pairs - e.g. [['US', 'USD'], ['CA', 'CAD']] - -When - - createLookup function is called with the country-currency array as an argument - -Then - - It should return an object where: - - The keys are the country codes - - The values are the corresponding currency codes - -Example -Given: [['US', 'USD'], ['CA', 'CAD']] - -When -createLookup(countryCurrencyPairs) is called - -Then -It should return: - { - 'US': 'USD', - 'CA': 'CAD' - } -*/ +describe("createLookup()", () => { + test("creates a country currency code lookup for multiple codes", () => { + const pairs = [ + ["US", "USD"], + ["CA", "CAD"], + ]; + + expect(createLookup(pairs)).toEqual({ + US: "USD", + CA: "CAD", + }); + }); + + test("returns an empty object for an empty array", () => { + expect(createLookup([])).toEqual({}); + }); + + test("overwrites duplicate keys with the last value", () => { + const pairs = [ + ["US", "USD"], + ["US", "USN"], + ]; + + expect(createLookup(pairs)).toEqual({ + US: "USN", + }); + }); + + test("throws an error when input is not an array", () => { + expect(() => createLookup("invalid")).toThrow(); + }); +}); From ce21823a9f842c0ad10ba870ce95ba2c749855f6 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 20:13:22 +0000 Subject: [PATCH 06/10] Implement tally function and tests --- Sprint-2/implement/tally.js | 32 ++++++++++++++++++++- Sprint-2/implement/tally.test.js | 49 ++++++++++++++------------------ 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..d6ee073a8 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,33 @@ -function tally() {} +/** + * tally() + * + * Counts how many times each item appears in an array. + * + * Example: + * tally(['a','a','b','c']) + * returns { a: 2, b: 1, c: 1 } + */ + +function tally(items) { + // Validate input + if (!Array.isArray(items)) { + throw new Error("Expected an array"); + } + + const counts = {}; + + // Loop through each item in the array + for (const item of items) { + // If the item already exists in the object, increase the count + if (counts[item]) { + counts[item] += 1; + } else { + // Otherwise initialise it + counts[item] = 1; + } + } + + return counts; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..6d80fc996 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -1,34 +1,27 @@ const tally = require("./tally.js"); -/** - * tally array - * - * In this task, you'll need to implement a function called tally - * that will take a list of items and count the frequency of each item - * in an array - * - * For example: - * - * tally(['a']), target output: { a: 1 } - * tally(['a', 'a', 'a']), target output: { a: 3 } - * tally(['a', 'a', 'b', 'c']), target output: { a : 2, b: 1, c: 1 } - */ +describe("tally()", () => { + test("returns an empty object for an empty array", () => { + expect(tally([])).toEqual({}); + }); -// Acceptance criteria: + test("counts a single item", () => { + expect(tally(["a"])).toEqual({ a: 1 }); + }); -// Given a function called tally -// When passed an array of items -// Then it should return an object containing the count for each unique item + test("counts repeated items", () => { + expect(tally(["a", "a", "a"])).toEqual({ a: 3 }); + }); -// 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("counts multiple different items", () => { + expect(tally(["a", "a", "b", "c"])).toEqual({ + a: 2, + b: 1, + c: 1, + }); + }); -// Given an array with duplicate items -// When passed to tally -// Then it should return counts for each unique item - -// Given an invalid input like a string -// When passed to tally -// Then it should throw an error + test("throws an error for invalid input", () => { + expect(() => tally("not-an-array")).toThrow(); + }); +}); From 7aa84ed0cee14bf7490216e5a9750fbb82140b9f Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 20:18:48 +0000 Subject: [PATCH 07/10] Fix querystring parser and add edge case tests --- Sprint-2/implement/querystring.js | 35 +++++++++++++++++-- Sprint-2/implement/querystring.test.js | 48 +++++++++++++++++++++----- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..617ea20e3 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,12 +1,43 @@ +/** + * parseQueryString() + * + * Parses a query string into an object of key-value pairs. + * + * Example: + * parseQueryString("name=Richard&city=Sheffield") + * returns { name: "Richard", city: "Sheffield" } + */ + function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { + + // Return an empty object if the input is an empty string + if (typeof queryString !== "string" || queryString.length === 0) { return queryParams; } + + // Split the full query string into key-value pairs const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); + // Skip empty pairs, for example from a trailing "&" + if (pair === "") { + continue; + } + + // Find the position of the first "=" + const separatorIndex = pair.indexOf("="); + + // If there is no "=" sign, treat it as a key with an empty value + if (separatorIndex === -1) { + queryParams[pair] = ""; + continue; + } + + // Extract the key and everything after the first "=" as the value + const key = pair.slice(0, separatorIndex); + const value = pair.slice(separatorIndex + 1); + queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..431d6fd13 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -1,12 +1,44 @@ -// In the prep, we implemented a function to parse query strings. -// Unfortunately, it contains several bugs! -// Below is one test case for an edge case the implementation doesn't handle well. -// Fix the implementation for this test, and try to think of as many other edge cases as possible - write tests and fix those too. +const parseQueryString = require("./querystring.js"); -const parseQueryString = require("./querystring.js") +describe("parseQueryString()", () => { + test("parses querystring values containing =", () => { + expect(parseQueryString("equation=x=y+1")).toEqual({ + equation: "x=y+1", + }); + }); + + test("returns an empty object for an empty string", () => { + expect(parseQueryString("")).toEqual({}); + }); + + test("parses a single key-value pair", () => { + expect(parseQueryString("name=Richard")).toEqual({ + name: "Richard", + }); + }); + + test("parses multiple key-value pairs", () => { + expect(parseQueryString("name=Richard&city=Sheffield")).toEqual({ + name: "Richard", + city: "Sheffield", + }); + }); + + test("handles a key with an empty value", () => { + expect(parseQueryString("name=")).toEqual({ + name: "", + }); + }); + + test("handles a key with no equals sign", () => { + expect(parseQueryString("name")).toEqual({ + name: "", + }); + }); -test("parses querystring values containing =", () => { - expect(parseQueryString("equation=x=y+1")).toEqual({ - "equation": "x=y+1", + test("ignores an empty trailing pair", () => { + expect(parseQueryString("name=Richard&")).toEqual({ + name: "Richard", + }); }); }); From af702c47d6e31e290db3de14034852cf2656c477 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 20:29:11 +0000 Subject: [PATCH 08/10] Fix invert implementation and add tests --- Sprint-2/interpret/invert.js | 79 ++++++++++++++++++++++++++----- Sprint-2/interpret/invert.test.js | 20 ++++++++ 2 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 Sprint-2/interpret/invert.test.js diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..cce52f2fb 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -4,26 +4,79 @@ // When invert is passed this object // Then it should swap the keys and values in the object -// E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} +// Example: +// invert({ x: 10, y: 20 }) +// should return: +// { "10": "x", "20": "y" } -function invert(obj) { - const invertedObj = {}; +/* +a) What is the current return value when invert({ a: 1 }) is called? - for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; - } +Current output: +{ key: 1 } - return invertedObj; +Reason: +The code uses invertedObj.key which creates a property literally called "key" +instead of using the variable key. + +--------------------------------------------------- + +b) What is the current return value when invert({ a: 1, b: 2 }) is called? + +Current output: +{ key: 2 } + +Reason: +The second iteration overwrites the first value because the property name +is always "key". + +--------------------------------------------------- + +c) What is the target return value when invert({ a: 1, b: 2 }) is called? + +Expected output: +{ + "1": "a", + "2": "b" } -// a) What is the current return value when invert is called with { a : 1 } +Keys and values should be swapped. + +--------------------------------------------------- -// b) What is the current return value when invert is called with { a: 1, b: 2 } +d) What does Object.entries return? Why is it needed? -// c) What is the target return value when invert is called with {a : 1, b: 2} +Object.entries(obj) converts an object into an array of [key, value] pairs. -// c) What does Object.entries return? Why is it needed in this program? +Example: +Object.entries({ a: 1, b: 2 }) -// d) Explain why the current return value is different from the target output +returns: +[ + ["a", 1], + ["b", 2] +] + +This allows us to loop through object properties using for...of. + +--------------------------------------------------- + +e) Why is the current return value different from the target output? + +Because invertedObj.key uses the literal property name "key". +To use the value dynamically as the key, we must use bracket notation: +invertedObj[value] = key; +*/ + +function invert(obj) { + const invertedObj = {}; + + for (const [key, value] of Object.entries(obj)) { + // Correct implementation: swap key and value + invertedObj[value] = key; + } + + return invertedObj; +} -// e) Fix the implementation of invert (and write tests to prove it's fixed!) +module.exports = invert; diff --git a/Sprint-2/interpret/invert.test.js b/Sprint-2/interpret/invert.test.js new file mode 100644 index 000000000..8b269233e --- /dev/null +++ b/Sprint-2/interpret/invert.test.js @@ -0,0 +1,20 @@ +const invert = require("./invert.js"); + +describe("invert()", () => { + test("inverts a single key-value pair", () => { + expect(invert({ a: 1 })).toEqual({ + 1: "a", + }); + }); + + test("inverts multiple key-value pairs", () => { + expect(invert({ a: 1, b: 2 })).toEqual({ + 1: "a", + 2: "b", + }); + }); + + test("returns an empty object for empty input", () => { + expect(invert({})).toEqual({}); + }); +}); From 12fb2314c3cb1a504db5f5019c504b723bc36e43 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 20:40:38 +0000 Subject: [PATCH 09/10] Complete stretch exercises for Sprint 2 --- Sprint-2/stretch/count-words.js | 40 ++++++++++++++++----------------- Sprint-2/stretch/mode.js | 32 +++++++++++++------------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..dbea38d13 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -1,28 +1,26 @@ -/* - Count the number of times a word appears in a given string. +function countWords(text) { + if (typeof text !== "string") { + throw new Error("Input must be a string"); + } - Write a function called countWords that - - takes a string as an argument - - returns an object where - - the keys are the words from the string and - - the values are the number of times the word appears in the string + // remove punctuation and make lowercase + const cleanedText = text.toLowerCase().replace(/[.,!?]/g, ""); - Example - If we call countWords like this: + const words = cleanedText.split(/\s+/); - countWords("you and me and you") then the target output is { you: 2, and: 2, me: 1 } + const counts = {}; - To complete this exercise you should understand - - Strings and string manipulation - - Loops - - Comparison inside if statements - - Setting values on an object + for (const word of words) { + if (!word) continue; -## Advanced challenges + if (counts[word]) { + counts[word]++; + } else { + counts[word] = 1; + } + } -1. Remove all of the punctuation (e.g. ".", ",", "!", "?") to tidy up the results + return counts; +} -2. Ignore the case of the words to find more unique words. e.g. (A === a, Hello === hello) - -3. Order the results to find out which word is the most common in the input -*/ +module.exports = countWords; diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..bef5f3200 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -1,18 +1,7 @@ -// You are given an implementation of calculateMode +function trackFrequencies(list) { + const freqs = new Map(); -// calculateMode's implementation can be broken down into two stages: - -// Stage 1. One part of the code tracks the frequency of each value -// Stage 2. The other part finds the value with the highest frequency - -// refactor calculateMode by splitting up the code -// into smaller functions using the stages above - -function calculateMode(list) { - // track frequency of each value - let freqs = new Map(); - - for (let num of list) { + for (const num of list) { if (typeof num !== "number") { continue; } @@ -20,17 +9,26 @@ function calculateMode(list) { freqs.set(num, (freqs.get(num) || 0) + 1); } - // Find the value with the highest frequency + return freqs; +} + +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; + mode = num; } } return maxFreq === 0 ? NaN : mode; } +function calculateMode(list) { + const freqs = trackFrequencies(list); + return findMode(freqs); +} + module.exports = calculateMode; From e296ae7effaeba30b56d0b0529ff74d4d1c11d54 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 21:31:10 +0000 Subject: [PATCH 10/10] Fix test file location for countWords --- Sprint-2/stretch/count-words.test.js | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Sprint-2/stretch/count-words.test.js diff --git a/Sprint-2/stretch/count-words.test.js b/Sprint-2/stretch/count-words.test.js new file mode 100644 index 000000000..29ec3433f --- /dev/null +++ b/Sprint-2/stretch/count-words.test.js @@ -0,0 +1,31 @@ +const countWords = require("./count-words.js"); + +describe("countWords()", () => { + test("counts repeated words", () => { + expect(countWords("you and me and you")).toEqual({ + you: 2, + and: 2, + me: 1, + }); + }); + + test("handles punctuation stuck to words", () => { + expect(countWords("Hello,World! Hello World!")).toEqual({ + hello: 2, + world: 2, + }); + }); + + test("handles inherited-property words safely", () => { + expect(countWords("constructor constructor")).toEqual({ + constructor: 2, + }); + }); + + test("handles extra spaces correctly", () => { + expect(countWords(" Hello World ")).toEqual({ + hello: 1, + world: 1, + }); + }); +});