From 7ef97ad0aac64cfce9ac6a2aa02b87f44e7c3cd6 Mon Sep 17 00:00:00 2001 From: Khor Biel Date: Wed, 12 Nov 2025 01:19:51 +0000 Subject: [PATCH 1/4] Completed Sprint 2 Implement tasks (contains, lookup, tally, querystring) with passing tests --- Sprint-2/debug/address.js | 2 +- Sprint-2/debug/author.js | 15 +++++++++-- Sprint-2/debug/recipe.js | 12 ++++++++- Sprint-2/implement/contains.js | 10 ++++++- Sprint-2/implement/contains.test.js | 14 +++++++++- Sprint-2/implement/lookup.js | 12 +++++++-- Sprint-2/implement/lookup.test.js | 17 +++++++++++- Sprint-2/implement/querystring.js | 22 ++++++++++++--- Sprint-2/implement/tally.js | 21 ++++++++++++++- Sprint-2/implement/tally.test.js | 10 ++++++- Sprint-2/interpret/invert.js | 42 +++++++++++++++++++++++------ 11 files changed, 155 insertions(+), 22 deletions(-) diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..36d2f865d 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -12,4 +12,4 @@ const address = { postcode: "XYZ 123", }; -console.log(`My house number is ${address[0]}`); +console.log(`My house number is ${address.houseNumber}`); diff --git a/Sprint-2/debug/author.js b/Sprint-2/debug/author.js index 8c2125977..f0b1a2dbb 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -11,6 +11,17 @@ const author = { alive: true, }; -for (const value of author) { +// Prediction: +// The code will not log the property values of the author object as intended. +// This causes the error because author is not iterable. + + // Explanation: + +// The original code likely used a for...in loop which iterates over the keys of the object, not the values. +// Therefore, logging the key would not give the desired property values. +// By using Object.values(author), we can directly iterate over the values of the object. + +// Fixed code: +for (const value of Object.values(author)) { console.log(value); -} +} \ No newline at end of file diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..d9d7678ae 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -10,6 +10,16 @@ const recipe = { ingredients: ["olive oil", "tomatoes", "salt", "pepper"], }; + +// Prediction: +// The code will not log the ingredients correctly because it is trying to log the entire recipe object instead of just the ingredients array. +// This will result in an output that includes the object structure rather than the individual ingredients. + +// Explanation: +// The template literal is incorrectly using `${recipe}` which outputs the whole object instead of iterating over the ingredients array. +// To fix this, we need to access the ingredients property and format it correctly, possibly by joining the array elements with new line characters. + +// Fixed code: console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients.join('\n')}`); \ No newline at end of file diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..0f9373e2f 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,11 @@ -function contains() {} +function contains(obj, key) { + // Check if the first argument is a valid object + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + return false; + } + + // Check if the key exists in the object + return Object.prototype.hasOwnProperty.call(obj, key); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..d32c99356 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -20,16 +20,28 @@ as the object doesn't contains a key of 'c' // 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("contains on object with existing property returns true", () => { + 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("contains on object with non-existent property returns false", () => { + expect(contains({ a: 1, b: 2 }, "c")).toBe(false); +}); // Given invalid parameters like an array // When passed to contains // Then it should return false or throw an error +test("contains on invalid parameters returns false", () => { + expect(contains([], "a")).toBe(false); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..b30c2f990 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,13 @@ -function createLookup() { - // implementation here +function createLookup(countryCurrencyPairs) { + const lookup = {}; + + for (const pair of countryCurrencyPairs) { + const countryCode = pair[0]; + const currencyCode = pair[1]; + lookup[countryCode] = currencyCode; + } + + return lookup; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..75d077a31 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,6 +1,21 @@ 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", () => { + // Given: An array of arrays representing country code and currency code pairs + const countryCurrencyPairs = [ + ["US", "USD"], + ["CA", "CAD"], + ]; + + // When: createLookup function is called with the country-currency array + const result = createLookup(countryCurrencyPairs); + + // Then: It should return an object where keys are country codes and values are currency codes + expect(result).toEqual({ + US: "USD", + CA: "CAD", + }); +}); /* diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..de7074099 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,13 +1,29 @@ function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { + + if (!queryString || queryString.length === 0) { return queryParams; } + + // Remove leading '?' if present + if (queryString.startsWith("?")) { + queryString = queryString.substring(1); + } + const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; + // Only split on the first '=' to handle values containing '=' + const equalsIndex = pair.indexOf("="); + if (equalsIndex === -1) { + // Handle keys without values + const key = decodeURIComponent(pair); + queryParams[key] = ""; + } else { + const key = decodeURIComponent(pair.substring(0, equalsIndex)); + const value = decodeURIComponent(pair.substring(equalsIndex + 1)); + queryParams[key] = value; + } } return queryParams; diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..885a33825 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,22 @@ -function tally() {} +function tally(items) { + // Validate input + if (!Array.isArray(items)) { + throw new Error("Input must be an array"); + } + + // Start with an empty object + const result = {}; + + // Loop through the array and count + for (const item of items) { + if (result[item]) { + result[item] += 1; + } else { + result[item] = 1; + } + } + + return result; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..69a328501 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -23,12 +23,20 @@ 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("returns correct counts for array with duplicate items", () => { + expect(tally(["a", "a", "b", "c"])).toEqual({ a: 2, b: 1, c: 1 }); +}); // Given an invalid input like a string // When passed to tally // Then it should throw an error +test("throws an error for invalid input (not an array)", () => { + expect(() => tally("a")).toThrow("Input must be an array"); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..76cd7825d 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -6,24 +6,50 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} -function invert(obj) { - const invertedObj = {}; - - for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; - } - return invertedObj; -} // a) What is the current return value when invert is called with { a : 1 } +// Object.entries({ a: 1 }) → [["a", 1]] +// The loop runs once with key = "a" and value = 1 +// Inside the loop: +// invertedObj.key = value; +// creates a property literally named "key", not "a" +// So the result is { key: 1 }. +// Answer: { key: 1 } // b) What is the current return value when invert is called with { a: 1, b: 2 } +// Object.entries({ a: 1, b: 2 }) → [["a", 1], ["b", 2]] +// First loop: invertedObj.key = 1 → { key: 1 } +// Second loop: invertedObj.key = 2 → overwrites previous value +// Final result: { key: 2 } +// Answer: { key: 2 } // c) What is the target return value when invert is called with {a : 1, b: 2} +// { "1": "a", "2": "b" } +// The goal is to swap keys and values, so keys become values and values become keys // c) What does Object.entries return? Why is it needed in this program? +// Object.entries(obj) returns an array of [key, value] pairs from the object +// Object.entries({ a: 1, b: 2 }); +// returns [["a", 1], ["b", 2]] +// It’s needed so that the loop: +// for (const [key, value] of Object.entries(obj)) +// can easily access both the key and value in one step. // d) Explain why the current return value is different from the target output +// The current code uses invertedObj.key = value, which creates a property literally named "key". +// To use the variable key dynamically, you must use bracket notation: +// invertedObj[value] = key; // e) Fix the implementation of invert (and write tests to prove it's fixed!) + +function invert(obj) { + const invertedObj = {}; + for (const [key, value] of Object.entries(obj)) { + invertedObj[value] = key; + } + return invertedObj; +} + +module.exports = invert; + From 106d47e68046fadb5faa0d3cbef0b12e9d5575c6 Mon Sep 17 00:00:00 2001 From: Khor Biel Date: Fri, 21 Nov 2025 18:30:20 +0000 Subject: [PATCH 2/4] fix(contains): fix object syntax and add edge case tests --- Sprint-2/implement/contains.js | 6 +++--- Sprint-2/implement/contains.test.js | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index 0f9373e2f..504fdc413 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,11 +1,11 @@ function contains(obj, key) { - // Check if the first argument is a valid object - if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + + if (typeof obj !== "object" || obj === null) { return false; } - // Check if the key exists in the object return Object.prototype.hasOwnProperty.call(obj, key); } module.exports = contains; + diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index d32c99356..13727ced0 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -17,6 +17,7 @@ as the object doesn't contains a key of 'c' // When passed an object and a property name // Then it should return true if the object contains the property, false otherwise + // Given an empty object // When passed to contains // Then it should return false @@ -24,7 +25,6 @@ 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 @@ -45,3 +45,25 @@ test("contains on object with non-existent property returns false", () => { test("contains on invalid parameters returns false", () => { expect(contains([], "a")).toBe(false); }); + +// === NEW TESTS ADDED BELOW === + +// Test for inherited properties (should return false) +test("contains should return false for inherited properties", () => { + expect(contains({a: 1, b: 2}, 'toString')).toBe(false); +}); + +// Test with arrays (should work properly) +test("contains should work with arrays", () => { + expect(contains(['a', 'b', 'c'], '0')).toBe(true); + expect(contains(['a', 'b', 'c'], '1')).toBe(true); + expect(contains(['a', 'b', 'c'], 'length')).toBe(false); +}); + +// Test with array as object +test("contains should handle arrays correctly", () => { + const arr = ['x', 'y', 'z']; + expect(contains(arr, '0')).toBe(true); + expect(contains(arr, '2')).toBe(true); + expect(contains(arr, '3')).toBe(false); +}); \ No newline at end of file From 683286485c5fb4d587ae740342e4029c5319ae17 Mon Sep 17 00:00:00 2001 From: Khor Biel Date: Fri, 21 Nov 2025 18:54:45 +0000 Subject: [PATCH 3/4] fix(tally): use Object.create(null) to avoid inherited properties --- Sprint-2/implement/tally.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index 885a33825..91b726034 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,13 +1,12 @@ function tally(items) { - // Validate input + if (!Array.isArray(items)) { throw new Error("Input must be an array"); } - // Start with an empty object - const result = {}; + const result = Object.create(null); - // Loop through the array and count + for (const item of items) { if (result[item]) { result[item] += 1; @@ -17,6 +16,7 @@ function tally(items) { } return result; + } module.exports = tally; From e76a105cdd8fda5c75e7109e630530dac100c38c Mon Sep 17 00:00:00 2001 From: Khor Biel Date: Fri, 21 Nov 2025 19:27:06 +0000 Subject: [PATCH 4/4] `fix: correct contains.test.js array tests to properly check non-existing indices instead of length property` --- Sprint-2/implement/contains.js | 4 ++-- Sprint-2/implement/contains.test.js | 8 ++++--- Sprint-2/stretch/count-words.js | 36 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index 504fdc413..689b70b9b 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,11 +1,11 @@ function contains(obj, key) { - + // Check if the first argument is a valid object if (typeof obj !== "object" || obj === null) { return false; } + // Check if the key exists in the object (only own properties) return Object.prototype.hasOwnProperty.call(obj, key); } module.exports = contains; - diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 13727ced0..7925af3ab 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -55,11 +55,13 @@ test("contains should return false for inherited properties", () => { // Test with arrays (should work properly) test("contains should work with arrays", () => { - expect(contains(['a', 'b', 'c'], '0')).toBe(true); - expect(contains(['a', 'b', 'c'], '1')).toBe(true); - expect(contains(['a', 'b', 'c'], 'length')).toBe(false); + expect(contains(["a", "b", "c"], "0")).toBe(true); + expect(contains(["a", "b", "c"], "1")).toBe(true); + expect(contains(["a", "b", "c"], "2")).toBe(true); + expect(contains(["a", "b", "c"], "3")).toBe(false); // check index that does not exist }); + // Test with array as object test("contains should handle arrays correctly", () => { const arr = ['x', 'y', 'z']; diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..b1f445d7c 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,39 @@ 3. Order the results to find out which word is the most common in the input */ + + + +function countWords(str) { + // Use Object.create(null) to avoid inherited properties + const wordCounts = Object.create(null); + + if (!str || str.trim() === "") { + return wordCounts; + } + + // Split into words and handle punctuation/case + const words = str.split(/\s+/); + + for (const word of words) { + // Remove punctuation and convert to lowercase + const cleanWord = word + .toLowerCase() + .replace(/[.,!?]/g, "") + .trim(); + + // Skip empty strings + if (cleanWord === "") continue; + + // Count the word + if (wordCounts[cleanWord]) { + wordCounts[cleanWord] += 1; + } else { + wordCounts[cleanWord] = 1; + } + } + + return wordCounts; +} + +module.exports = countWords; \ No newline at end of file