From 95c4e7521db95a021f5920fab7a0e19d13a2090e Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Wed, 18 Mar 2026 19:25:41 +0000 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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, + }); + }); +}); From a22ed15d12c368d44d0e5f4f3529ced09691ac7c Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Thu, 19 Mar 2026 01:49:14 +0000 Subject: [PATCH 11/13] Address mentor feedback for sprint 1 data groups --- Sprint-1/fix/median.js | 27 ++++++++++++++++++++++++--- Sprint-1/implement/dedupe.js | 18 +++++++++++++++++- Sprint-1/implement/dedupe.test.js | 24 ++++++++++++++++++++++-- Sprint-1/implement/max.js | 12 ++++++++++++ Sprint-1/implement/max.test.js | 23 +++++++++++++++++++++-- Sprint-1/implement/sum.js | 12 ++++++++++++ Sprint-1/implement/sum.test.js | 19 ++++++++++++++++++- 7 files changed, 126 insertions(+), 9 deletions(-) diff --git a/Sprint-1/fix/median.js b/Sprint-1/fix/median.js index b22590bc6..01b74c403 100644 --- a/Sprint-1/fix/median.js +++ b/Sprint-1/fix/median.js @@ -6,9 +6,30 @@ // or 'list' has mixed values (the function is expected to sort only numbers). function calculateMedian(list) { - const middleIndex = Math.floor(list.length / 2); - const median = list.splice(middleIndex, 1)[0]; - return median; + // Return null immediately if the input is not an array + if (!Array.isArray(list)) { + return null; + } + + // filter() returns a new array, so this does not modify the original input + const numbersOnly = list.filter((item) => Number.isFinite(item)); + + // Return null if there are no numeric values + if (numbersOnly.length === 0) { + return null; + } + + // Safe to sort directly because numbersOnly is already a new array + const sortedNumbers = numbersOnly.sort((a, b) => a - b); + const middleIndex = Math.floor(sortedNumbers.length / 2); + + // Even number of values: return the average of the two middle values + if (sortedNumbers.length % 2 === 0) { + return (sortedNumbers[middleIndex - 1] + sortedNumbers[middleIndex]) / 2; + } + + // Odd number of values: return the middle value + return sortedNumbers[middleIndex]; } module.exports = calculateMedian; diff --git a/Sprint-1/implement/dedupe.js b/Sprint-1/implement/dedupe.js index 781e8718a..f8494b4a5 100644 --- a/Sprint-1/implement/dedupe.js +++ b/Sprint-1/implement/dedupe.js @@ -1 +1,17 @@ -function dedupe() {} +// Return a new array with duplicate values removed. +// Keep the first occurrence of each value. + +function dedupe(elements) { + const uniqueElements = []; + + for (const element of elements) { + // Add the element only if it is not already in the result array + if (!uniqueElements.includes(element)) { + uniqueElements.push(element); + } + } + + return uniqueElements; +} + +module.exports = dedupe; diff --git a/Sprint-1/implement/dedupe.test.js b/Sprint-1/implement/dedupe.test.js index 23e0f8638..f4336cc48 100644 --- a/Sprint-1/implement/dedupe.test.js +++ b/Sprint-1/implement/dedupe.test.js @@ -2,7 +2,7 @@ const dedupe = require("./dedupe.js"); /* Dedupe Array -📖 Dedupe means **deduplicate** + Dedupe means deduplicate In this kata, you will need to deduplicate the elements of an array @@ -16,12 +16,32 @@ E.g. dedupe([1, 2, 1]) target output: [1, 2] // Given an empty array // When passed to the dedupe function // Then it should return an empty array -test.todo("given an empty array, it returns an empty array"); +test("given an empty array, it returns an empty array", () => { + expect(dedupe([])).toEqual([]); +}); // Given an array with no duplicates // When passed to the dedupe function // Then it should return a copy of the original array +test("given an array with no duplicates, it returns a copy of the original array", () => { + const input = [1, 2, 3]; + const result = dedupe(input); + + expect(result).toEqual(input); + expect(result).not.toBe(input); +}); // Given an array with strings or numbers // When passed to the dedupe function // Then it should remove the duplicate values, preserving the first occurence of each element +test("given an array with duplicate strings, removes duplicates and preserves first occurrence", () => { + expect(dedupe(["a", "a", "a", "b", "b", "c"])).toEqual(["a", "b", "c"]); +}); + +test("given an array with duplicate numbers, removes duplicates and preserves first occurrence", () => { + expect(dedupe([5, 1, 1, 2, 3, 2, 5, 8])).toEqual([5, 1, 2, 3, 8]); +}); + +test("given a mixed duplicate order, removes duplicates and preserves first occurrence", () => { + expect(dedupe([1, 2, 1])).toEqual([1, 2]); +}); diff --git a/Sprint-1/implement/max.js b/Sprint-1/implement/max.js index 6dd76378e..b9017f0bf 100644 --- a/Sprint-1/implement/max.js +++ b/Sprint-1/implement/max.js @@ -1,4 +1,16 @@ +// Find the largest numerical value in an array. +// Non-number values should be ignored. + function findMax(elements) { + let maxValue = -Infinity; + + for (const element of elements) { + if (Number.isFinite(element) && element > maxValue) { + maxValue = element; + } + } + + return maxValue; } module.exports = findMax; diff --git a/Sprint-1/implement/max.test.js b/Sprint-1/implement/max.test.js index 82f18fd88..ebb57b922 100644 --- a/Sprint-1/implement/max.test.js +++ b/Sprint-1/implement/max.test.js @@ -15,29 +15,48 @@ const findMax = require("./max.js"); // Given an empty array // When passed to the max function // Then it should return -Infinity -// Delete this test.todo and replace it with a test. -test.todo("given an empty array, returns -Infinity"); +test("given an empty array, returns -Infinity", () => { + expect(findMax([])).toBe(-Infinity); +}); // Given an array with one number // When passed to the max function // Then it should return that number +test("given an array with one number, returns that number", () => { + expect(findMax([42])).toBe(42); +}); // Given an array with both positive and negative numbers // When passed to the max function // Then it should return the largest number overall +test("given an array with positive and negative numbers, returns the largest number", () => { + expect(findMax([-10, 3, 25, -1])).toBe(25); +}); // Given an array with just negative numbers // When passed to the max function // Then it should return the closest one to zero +test("given an array with only negative numbers, returns the largest one", () => { + expect(findMax([-9, -2, -15, -4])).toBe(-2); +}); // Given an array with decimal numbers // When passed to the max function // Then it should return the largest decimal number +test("given an array with decimal numbers, returns the largest decimal", () => { + expect(findMax([1.2, 3.8, 2.4])).toBe(3.8); +}); // Given an array with non-number values // When passed to the max function // Then it should return the max and ignore non-numeric values +test("given an array with non-number values, ignores them and returns the max", () => { + expect(findMax(["hey", 10, "300", "hi", 60, 10])).toBe(60); +}); // Given an array with only non-number values // When passed to the max function // Then it should return the least surprising value given how it behaves for all other inputs +test("given an array with only non-number values, returns -Infinity", () => { + expect(findMax(["apple", null, undefined, "banana"])).toBe(-Infinity); +}); diff --git a/Sprint-1/implement/sum.js b/Sprint-1/implement/sum.js index 9062aafe3..59f5ecc83 100644 --- a/Sprint-1/implement/sum.js +++ b/Sprint-1/implement/sum.js @@ -1,4 +1,16 @@ +// Sum the numerical values in an array. +// Non-number values should be ignored. + function sum(elements) { + let total = 0; + + for (const element of elements) { + if (Number.isFinite(element)) { + total += element; + } + } + + return total; } module.exports = sum; diff --git a/Sprint-1/implement/sum.test.js b/Sprint-1/implement/sum.test.js index dd0a090ca..0698757ef 100644 --- a/Sprint-1/implement/sum.test.js +++ b/Sprint-1/implement/sum.test.js @@ -13,24 +13,41 @@ const sum = require("./sum.js"); // Given an empty array // When passed to the sum function // Then it should return 0 -test.todo("given an empty array, returns 0") +test("given an empty array, returns 0", () => { + expect(sum([])).toBe(0); +}); // Given an array with just one number // When passed to the sum function // Then it should return that number +test("given an array with one number, returns that number", () => { + expect(sum([7])).toBe(7); +}); // Given an array containing negative numbers // When passed to the sum function // Then it should still return the correct total sum +test("given an array with negative numbers, returns the correct sum", () => { + expect(sum([-5, 10, -2])).toBe(3); +}); // Given an array with decimal/float numbers // When passed to the sum function // Then it should return the correct total sum +test("given an array with decimal numbers, returns the correct sum", () => { + expect(sum([1.2, 0.6, 0.005])).toBeCloseTo(1.805, 10); +}); // Given an array containing non-number values // When passed to the sum function // Then it should ignore the non-numerical values and return the sum of the numerical elements +test("given an array with non-number values, ignores them and sums only numbers", () => { + expect(sum(["hey", 10, "hi", 60, 10])).toBe(80); +}); // Given an array with only non-number values // When passed to the sum function // Then it should return the least surprising value given how it behaves for all other inputs +test("given an array with only non-number values, returns 0", () => { + expect(sum(["apple", null, undefined, "banana"])).toBe(0); +}); From d95006e8ee6a01e87be02842ae613535910f1da5 Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Mon, 23 Mar 2026 17:44:02 +0000 Subject: [PATCH 12/13] Remove Sprint-1 files from Sprint-2 PR --- Sprint-1/fix/median.js | 27 +++------------------------ Sprint-1/implement/dedupe.js | 18 +----------------- Sprint-1/implement/dedupe.test.js | 24 ++---------------------- Sprint-1/implement/max.js | 12 ------------ Sprint-1/implement/max.test.js | 23 ++--------------------- Sprint-1/implement/sum.js | 12 ------------ Sprint-1/implement/sum.test.js | 19 +------------------ 7 files changed, 9 insertions(+), 126 deletions(-) diff --git a/Sprint-1/fix/median.js b/Sprint-1/fix/median.js index 01b74c403..b22590bc6 100644 --- a/Sprint-1/fix/median.js +++ b/Sprint-1/fix/median.js @@ -6,30 +6,9 @@ // or 'list' has mixed values (the function is expected to sort only numbers). function calculateMedian(list) { - // Return null immediately if the input is not an array - if (!Array.isArray(list)) { - return null; - } - - // filter() returns a new array, so this does not modify the original input - const numbersOnly = list.filter((item) => Number.isFinite(item)); - - // Return null if there are no numeric values - if (numbersOnly.length === 0) { - return null; - } - - // Safe to sort directly because numbersOnly is already a new array - const sortedNumbers = numbersOnly.sort((a, b) => a - b); - const middleIndex = Math.floor(sortedNumbers.length / 2); - - // Even number of values: return the average of the two middle values - if (sortedNumbers.length % 2 === 0) { - return (sortedNumbers[middleIndex - 1] + sortedNumbers[middleIndex]) / 2; - } - - // Odd number of values: return the middle value - return sortedNumbers[middleIndex]; + const middleIndex = Math.floor(list.length / 2); + const median = list.splice(middleIndex, 1)[0]; + return median; } module.exports = calculateMedian; diff --git a/Sprint-1/implement/dedupe.js b/Sprint-1/implement/dedupe.js index f8494b4a5..781e8718a 100644 --- a/Sprint-1/implement/dedupe.js +++ b/Sprint-1/implement/dedupe.js @@ -1,17 +1 @@ -// Return a new array with duplicate values removed. -// Keep the first occurrence of each value. - -function dedupe(elements) { - const uniqueElements = []; - - for (const element of elements) { - // Add the element only if it is not already in the result array - if (!uniqueElements.includes(element)) { - uniqueElements.push(element); - } - } - - return uniqueElements; -} - -module.exports = dedupe; +function dedupe() {} diff --git a/Sprint-1/implement/dedupe.test.js b/Sprint-1/implement/dedupe.test.js index f4336cc48..23e0f8638 100644 --- a/Sprint-1/implement/dedupe.test.js +++ b/Sprint-1/implement/dedupe.test.js @@ -2,7 +2,7 @@ const dedupe = require("./dedupe.js"); /* Dedupe Array - Dedupe means deduplicate +📖 Dedupe means **deduplicate** In this kata, you will need to deduplicate the elements of an array @@ -16,32 +16,12 @@ E.g. dedupe([1, 2, 1]) target output: [1, 2] // Given an empty array // When passed to the dedupe function // Then it should return an empty array -test("given an empty array, it returns an empty array", () => { - expect(dedupe([])).toEqual([]); -}); +test.todo("given an empty array, it returns an empty array"); // Given an array with no duplicates // When passed to the dedupe function // Then it should return a copy of the original array -test("given an array with no duplicates, it returns a copy of the original array", () => { - const input = [1, 2, 3]; - const result = dedupe(input); - - expect(result).toEqual(input); - expect(result).not.toBe(input); -}); // Given an array with strings or numbers // When passed to the dedupe function // Then it should remove the duplicate values, preserving the first occurence of each element -test("given an array with duplicate strings, removes duplicates and preserves first occurrence", () => { - expect(dedupe(["a", "a", "a", "b", "b", "c"])).toEqual(["a", "b", "c"]); -}); - -test("given an array with duplicate numbers, removes duplicates and preserves first occurrence", () => { - expect(dedupe([5, 1, 1, 2, 3, 2, 5, 8])).toEqual([5, 1, 2, 3, 8]); -}); - -test("given a mixed duplicate order, removes duplicates and preserves first occurrence", () => { - expect(dedupe([1, 2, 1])).toEqual([1, 2]); -}); diff --git a/Sprint-1/implement/max.js b/Sprint-1/implement/max.js index b9017f0bf..6dd76378e 100644 --- a/Sprint-1/implement/max.js +++ b/Sprint-1/implement/max.js @@ -1,16 +1,4 @@ -// Find the largest numerical value in an array. -// Non-number values should be ignored. - function findMax(elements) { - let maxValue = -Infinity; - - for (const element of elements) { - if (Number.isFinite(element) && element > maxValue) { - maxValue = element; - } - } - - return maxValue; } module.exports = findMax; diff --git a/Sprint-1/implement/max.test.js b/Sprint-1/implement/max.test.js index ebb57b922..82f18fd88 100644 --- a/Sprint-1/implement/max.test.js +++ b/Sprint-1/implement/max.test.js @@ -15,48 +15,29 @@ const findMax = require("./max.js"); // Given an empty array // When passed to the max function // Then it should return -Infinity -test("given an empty array, returns -Infinity", () => { - expect(findMax([])).toBe(-Infinity); -}); +// Delete this test.todo and replace it with a test. +test.todo("given an empty array, returns -Infinity"); // Given an array with one number // When passed to the max function // Then it should return that number -test("given an array with one number, returns that number", () => { - expect(findMax([42])).toBe(42); -}); // Given an array with both positive and negative numbers // When passed to the max function // Then it should return the largest number overall -test("given an array with positive and negative numbers, returns the largest number", () => { - expect(findMax([-10, 3, 25, -1])).toBe(25); -}); // Given an array with just negative numbers // When passed to the max function // Then it should return the closest one to zero -test("given an array with only negative numbers, returns the largest one", () => { - expect(findMax([-9, -2, -15, -4])).toBe(-2); -}); // Given an array with decimal numbers // When passed to the max function // Then it should return the largest decimal number -test("given an array with decimal numbers, returns the largest decimal", () => { - expect(findMax([1.2, 3.8, 2.4])).toBe(3.8); -}); // Given an array with non-number values // When passed to the max function // Then it should return the max and ignore non-numeric values -test("given an array with non-number values, ignores them and returns the max", () => { - expect(findMax(["hey", 10, "300", "hi", 60, 10])).toBe(60); -}); // Given an array with only non-number values // When passed to the max function // Then it should return the least surprising value given how it behaves for all other inputs -test("given an array with only non-number values, returns -Infinity", () => { - expect(findMax(["apple", null, undefined, "banana"])).toBe(-Infinity); -}); diff --git a/Sprint-1/implement/sum.js b/Sprint-1/implement/sum.js index 59f5ecc83..9062aafe3 100644 --- a/Sprint-1/implement/sum.js +++ b/Sprint-1/implement/sum.js @@ -1,16 +1,4 @@ -// Sum the numerical values in an array. -// Non-number values should be ignored. - function sum(elements) { - let total = 0; - - for (const element of elements) { - if (Number.isFinite(element)) { - total += element; - } - } - - return total; } module.exports = sum; diff --git a/Sprint-1/implement/sum.test.js b/Sprint-1/implement/sum.test.js index 0698757ef..dd0a090ca 100644 --- a/Sprint-1/implement/sum.test.js +++ b/Sprint-1/implement/sum.test.js @@ -13,41 +13,24 @@ const sum = require("./sum.js"); // Given an empty array // When passed to the sum function // Then it should return 0 -test("given an empty array, returns 0", () => { - expect(sum([])).toBe(0); -}); +test.todo("given an empty array, returns 0") // Given an array with just one number // When passed to the sum function // Then it should return that number -test("given an array with one number, returns that number", () => { - expect(sum([7])).toBe(7); -}); // Given an array containing negative numbers // When passed to the sum function // Then it should still return the correct total sum -test("given an array with negative numbers, returns the correct sum", () => { - expect(sum([-5, 10, -2])).toBe(3); -}); // Given an array with decimal/float numbers // When passed to the sum function // Then it should return the correct total sum -test("given an array with decimal numbers, returns the correct sum", () => { - expect(sum([1.2, 0.6, 0.005])).toBeCloseTo(1.805, 10); -}); // Given an array containing non-number values // When passed to the sum function // Then it should ignore the non-numerical values and return the sum of the numerical elements -test("given an array with non-number values, ignores them and sums only numbers", () => { - expect(sum(["hey", 10, "hi", 60, 10])).toBe(80); -}); // Given an array with only non-number values // When passed to the sum function // Then it should return the least surprising value given how it behaves for all other inputs -test("given an array with only non-number values, returns 0", () => { - expect(sum(["apple", null, undefined, "banana"])).toBe(0); -}); From 4b01ab03bcd0cfdfc9ba93c21799c2e2d23d2a7f Mon Sep 17 00:00:00 2001 From: Richard Frimpong Date: Tue, 24 Mar 2026 01:23:50 +0000 Subject: [PATCH 13/13] Apply mentor feedback to Sprint 2 files --- Sprint-2/debug/recipe.js | 6 ++-- Sprint-2/implement/contains.js | 19 ++++-------- Sprint-2/implement/contains.test.js | 26 ++++++++++++---- Sprint-2/implement/querystring.js | 39 +++++++++++++---------- Sprint-2/implement/querystring.test.js | 6 ++++ Sprint-2/implement/tally.js | 11 +++---- Sprint-2/implement/tally.test.js | 6 ++++ Sprint-2/stretch/count-words.js | 43 +++++++++++++++++++++----- 8 files changed, 103 insertions(+), 53 deletions(-) diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 7b43deb42..42d351b6d 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -15,7 +15,5 @@ const recipe = { 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); -} +// Print each ingredient on a new line using join() +console.log(recipe.ingredients.join("\n")); diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index 1527711b2..c2b4995bf 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,27 +1,20 @@ /** * contains() * - * Checks whether an object contains a specific property. + * Checks whether an object contains a specific own 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. + * @param {*} propertyName - The property name to check. + * @returns {boolean} True if the object has the property as its own key, otherwise false. */ - function contains(obj, propertyName) { - // Validate that obj is actually an object - // and not null or an array + // Reject null, non-objects, and arrays 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); + // Check own properties only + return Object.hasOwn(obj, propertyName); } module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 67bbc115f..40bc1f961 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -1,11 +1,11 @@ const contains = require("./contains.js"); /* -Implement a function called contains that checks an object contains a -particular property +Implement a function called contains that checks whether an object contains +a particular own property. -E.g. contains({a: 1, b: 2}, "a") // returns true -E.g. contains({a: 1, b: 2}, "c") // returns false +E.g. contains({ a: 1, b: 2 }, "a") // returns true +E.g. contains({ a: 1, b: 2 }, "c") // returns false */ describe("contains()", () => { @@ -21,11 +21,25 @@ describe("contains()", () => { expect(contains({ a: 1, b: 2 }, "c")).toBe(false); }); - test("returns false when given an array", () => { - expect(contains(["a", "b"], "0")).toBe(false); + test("returns false for inherited properties", () => { + expect(contains({ a: 1, b: 2 }, "toString")).toBe(false); + }); + + test("returns false when given an array with a realistic array key", () => { + expect(contains(["a", "b"], 0)).toBe(false); }); test("returns false when given null", () => { expect(contains(null, "a")).toBe(false); }); + + test("supports non-string property names", () => { + const obj = { 3: 12 }; + expect(contains(obj, 3)).toBe(true); + }); + + test("supports empty string as a property name", () => { + const obj = { "": 99 }; + expect(contains(obj, "")).toBe(true); + }); }); diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 617ea20e3..314f95121 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,42 +1,49 @@ /** * parseQueryString() * - * Parses a query string into an object of key-value pairs. + * Parses a query string into an object of decoded key-value pairs. * - * Example: - * parseQueryString("name=Richard&city=Sheffield") - * returns { name: "Richard", city: "Sheffield" } + * Handles: + * - empty strings + * - multiple pairs separated by "&" + * - values containing "=" + * - missing values + * - missing "=" + * - trailing "&" + * - URL-encoded keys and values */ - function parseQueryString(queryString) { - const queryParams = {}; + const queryParams = Object.create(null); - // Return an empty object if the input is an empty string + // Return an empty object if the input is invalid or empty 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) { - // Skip empty pairs, for example from a trailing "&" + // Skip empty pairs, e.g. 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 + let rawKey; + let rawValue; + + // If there is no "=", treat it as a key with an empty value if (separatorIndex === -1) { - queryParams[pair] = ""; - continue; + rawKey = pair; + rawValue = ""; + } else { + rawKey = pair.slice(0, separatorIndex); + rawValue = pair.slice(separatorIndex + 1); } - // Extract the key and everything after the first "=" as the value - const key = pair.slice(0, separatorIndex); - const value = pair.slice(separatorIndex + 1); + const key = decodeURIComponent(rawKey); + const value = decodeURIComponent(rawValue); queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 431d6fd13..d095e470c 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -41,4 +41,10 @@ describe("parseQueryString()", () => { name: "Richard", }); }); + + test("decodes URL-encoded keys and values", () => { + expect(parseQueryString("tags%5B%5D=hello%20world")).toEqual({ + "tags[]": "hello world", + }); + }); }); diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index d6ee073a8..2e1df259c 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -4,25 +4,22 @@ * Counts how many times each item appears in an array. * * Example: - * tally(['a','a','b','c']) + * 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 = {}; + // Create an object with no inherited properties + const counts = Object.create(null); - // 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]) { + if (counts[item] !== undefined) { counts[item] += 1; } else { - // Otherwise initialise it counts[item] = 1; } } diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 6d80fc996..78d533547 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -24,4 +24,10 @@ describe("tally()", () => { test("throws an error for invalid input", () => { expect(() => tally("not-an-array")).toThrow(); }); + + test('counts inherited-looking keys like "toString" correctly', () => { + expect(tally(["toString", "toString"])).toEqual({ + toString: 2, + }); + }); }); diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index dbea38d13..fdb0b3d58 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -1,20 +1,49 @@ +/* + Count the number of times a word appears in a given 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 + - the values are the number of times each word appears + + Example: + countWords("you and me and you") + returns { you: 2, and: 2, me: 1 } + + Advanced improvements included: + - removes punctuation + - ignores case + - handles extra whitespace + - safely counts words like "constructor" +*/ + function countWords(text) { + // Validate input if (typeof text !== "string") { throw new Error("Input must be a string"); } - // remove punctuation and make lowercase - const cleanedText = text.toLowerCase().replace(/[.,!?]/g, ""); + // Convert to lowercase, replace punctuation with spaces, and trim edges + const cleanedText = text + .toLowerCase() + .replace(/[.,!?;:]/g, " ") + .trim(); + // Return an empty object if nothing remains after cleaning + if (cleanedText === "") { + return Object.create(null); + } + + // Split on one or more whitespace characters const words = cleanedText.split(/\s+/); - const counts = {}; + // Use an object with no inherited properties + const counts = Object.create(null); for (const word of words) { - if (!word) continue; - - if (counts[word]) { - counts[word]++; + if (counts[word] !== undefined) { + counts[word] += 1; } else { counts[word] = 1; }