From c048d180e09a0665b600b6b921399a8c43efb401 Mon Sep 17 00:00:00 2001 From: Nik Bezdzenariy Date: Fri, 10 Apr 2026 17:14:04 +0200 Subject: [PATCH 1/4] fix(debug): resolve scope errors in module data --- Sprint-2/debug/address.js | 4 +++- Sprint-2/debug/author.js | 10 +++++++--- Sprint-2/debug/recipe.js | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..9f28f0468 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,4 +1,6 @@ // Predict and explain first... +// objects are not arrays and don`t have ordinal numbers (0, 1, 2) +// their data is accessed by keys (names) // This code should log out the houseNumber from the address object // but it isn't working... @@ -12,4 +14,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..329f215b4 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,4 +1,8 @@ // Predict and explain first... +// for...of loop +// designed for arrays +// doesn't work with objects +// objects in JS are not iterable // 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 @@ -11,6 +15,6 @@ const author = { alive: true, }; -for (const value of author) { - console.log(value); -} +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..2b4a55aa1 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -1,4 +1,5 @@ // Predict and explain first... +// ${recipe} object inserted in console.log. // This program should log out the title, how many it serves and the ingredients. // Each ingredient should be logged on a new line @@ -12,4 +13,4 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients.join(`\n`)}`); From 87fed8e8af4c1c654fcd2907424feb82cdec9402 Mon Sep 17 00:00:00 2001 From: Nik Bezdzenariy Date: Fri, 10 Apr 2026 17:14:24 +0200 Subject: [PATCH 2/4] feat(implement): add lookup and tally logic --- Sprint-2/implement/contains.js | 10 ++++- Sprint-2/implement/contains.test.js | 24 +++++++++++- Sprint-2/implement/lookup.js | 42 +++++++++++++++++++- Sprint-2/implement/lookup.test.js | 54 +++++++++++++++++++++++++- Sprint-2/implement/querystring.js | 18 ++++++++- Sprint-2/implement/querystring.test.js | 19 +++++++++ Sprint-2/implement/tally.js | 13 ++++++- Sprint-2/implement/tally.test.js | 16 +++++++- 8 files changed, 187 insertions(+), 9 deletions(-) diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..c9451f8a1 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 obj is actually an obj not null or array + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { + return false; + } + + // use 'in' operator to check if key exists in object + return key in obj; +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..750a7c8c2 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -1,5 +1,28 @@ const contains = require("./contains.js"); +test ("returns false for empty object", () => { + expect(contains({}, 'a')).toBe(false); +}); + +test("returns true when property exists", () => { + expect(contains({a: 1, b: 2}, 'a')).toBe(true); + expect(contains({name: 'John'}, 'name')).toBe(true); +}); + +test("returns false when property doesn't exist", () => { + expect(contains({a: 1, b: 2}, 'c')).toBe(false); + expect(contains({x: 10}, 'y')).toBe(false); +}); + +test("returns false for invalid input (array)", () => { + expect(contains([1, 2, 3], '0')).toBe(false); +}); + +test("returns false for null or undefined", () => { + expect(contains(null, 'a')).toBe(false); + expect(contains(undefined, 'a')).toBe(false); +}); + /* Implement a function called contains that checks an object contains a particular property @@ -20,7 +43,6 @@ 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"); // Given an object with properties // When passed to contains with an existing property name diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..e579e8ab4 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,45 @@ -function createLookup() { +/** + * creates a lookup object from an array of key-value pairs + * + * @param {Array>} pairs - array of [key, value] pairs + * @returns {Object} - lookup object with keys mapped to values + * @throws {Typeerror} - if input is not a valid array of pairs + * + * @example + * createLookup([['US', 'USD'], ['CA', 'CAD']]) + * // Returns: { US: 'USD', CA: 'CAD' } + */ +function createLookup(pairs) { // implementation here + // 1. validate input + if (!Array.isArray(pairs)) { + throw new TypeError('Input must be an array'); + } + + // 2. initialize empty result object + const lookup = {}; + + // 3. iterate through each pair + for (const pair of pairs) { + // 3a. validate each pair: must be an array with 2 elements + if (!Array.isArray(pair) || pair.length !== 2) { + throw new TypeError('Each pair must be an array of exactly 2 elements'); + } + + // 3b. extract key and value + const [key, value] = pair; // destructing assignment + + // 3c. validate key must be a string + if (typeof key !== 'string') { + throw new TypeError('Keys must be stings'); + } + + // 3d. add to lookup object + lookup[key] = value; + } + + // 4. return the constructed lookup object + return lookup; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..21b18dd71 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,9 +1,59 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); +// happy path: valid input +test("creates a lookup object from valid pairs", () => { + const pairs = [['US', 'USD'], ['CA', 'CAD']]; + const result = createLookup(pairs); -/* + expect(result).toEqual({ + US: 'USD', + CA: 'CAD' + }); +}); +// empty array +test("returns empty object for empty array", () => { + expect(createLookup([])).toEqual({}); +}); + +// single pair +test("handle single pair correctly", () => { + const pairs = [['UK', 'GBP']]; + expect(createLookup(pairs)).toEqual({ UK: 'GBP' }); +}); + +// duplicates keys (last value wins) +test("handle duplicates keys by keeping last value", () => { + const pairs = [['US', 'USD'], ['US', 'EUR']]; + expect(createLookup(pairs)).toEqual({ US: 'EUR' }); +}); + +// error handling: not an array +test("throws TypeError when input is not an array", () => { + expect(() => createLookup(null)).toThrow(TypeError); + expect(() => createLookup('invalid')).toThrow(TypeError); + expect(() => createLookup({a: 1})).toThrow(TypeError); +}); + +// error handling: invalid pair (not array) +test("throws TypeError when pair is not an array", () => { + const pairs = [['US', 'USD'], 'invalid']; + expect(() => createLookup(pairs)).toThrow(TypeError); +}); + +// error handling: invalid pair (wrong length) +test("throws TypeError when pair has wrong length", () => { + const pairs = [['US', 'USD'], ['CA']]; // missing value + expect(() => createLookup(pairs)).toThrow(TypeError); +}); + +// error handling: non-string key +test("throws TypeError when key is not a string", () => { + const pairs = [[42, 'USD']]; // number as key + expect(() => createLookup(pairs)).toThrow(TypeError); +}); + +/** Create a lookup object of key value pairs from an array of code pairs Acceptance Criteria: diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..ab9ad74c8 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,12 +1,26 @@ function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { + if (!queryString || queryString.length === 0) { return queryParams; } + const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); + // divide only by the first sign '=' + const firstEqualIndex = pair.indexOf("="); + + // If there is no '=', + // consider it a key without a value (or skip it) + if (firstEqualIndex === -1) { + queryParams[pair] = ""; + continue; + } + + const key = pair.slice(0, firstEqualIndex); + const value = pair.slice(firstEqualIndex +1); + + // const [key, value] = pair.split("="); queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..e716caeac 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -10,3 +10,22 @@ test("parses querystring values containing =", () => { "equation": "x=y+1", }); }); + +test("handles keys without values", () => { + expect(parseQueryString("name")).toEqual({ name: ""}); +}); + +test("handles empty values", () => { + expect(parseQueryString("name=")).toEqual({ name: "" }); +}); + +test("parses simple query string", () => { + expect(parseQueryString("name=John&age=30")).toEqual({ + name: "John", + age: "30", + }); +}); + +test("returns empty objects for empty strings", () => { + expect(parseQueryString("")).toEqual({}); +}); diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..750096cb5 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,14 @@ -function tally() {} +function tally(items) { + if(!Array.isArray(items)) { + throw new TypeError("Input must be an array"); + } + + const counts = {}; + for (const item of items) { + // for no key, initialize 0 and add 1 + counts[item] = (counts[item] || 0) + 1; + } + return counts; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..9481e40c0 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -1,5 +1,20 @@ const tally = require("./tally.js"); +test("tally on an empty array returns an empty object", () => { + expect(tally([])).toEqual({}); +}); + +test("tally counts frequency of items in an array", () => { + expect(tally(['a'])).toEqual({ a: 1 }); + expect(tally(['a', 'a', 'a'])).toEqual({ a: 3 }); + expect(tally(['a', 'a', 'b', 'c'])).toEqual({ a:2, b:1, c:1});; +}); + +test("tally throws TypeError on invalid input", () => { + expect(() => tally("not an array")).toThrow(TypeError); + expect(() => tally(null)).toThrow(TypeError); +}); + /** * tally array * @@ -23,7 +38,6 @@ 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"); // Given an array with duplicate items // When passed to tally From 20d1179fbcbb89f8ed868ce8b5a10b078cfbb337 Mon Sep 17 00:00:00 2001 From: Nik Bezdzenariy Date: Sat, 11 Apr 2026 20:10:15 +0200 Subject: [PATCH 3/4] fix(sprint-2): correct typo in invert.js type validation --- Sprint-2/interpret/invert.js | 33 ++++++++++++++++++++++++++++++- Sprint-2/interpret/invert.test.js | 29 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) 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..8269000d2 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -7,23 +7,54 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} function invert(obj) { + // 1. valiadate input + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { + throw new TypeError('Input must be a plain object'); + } + + // 2. initialize empty results object const invertedObj = {}; + // 3. iterate through key-value pairs for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; + // 4. swap: value becomes key, bey becomes value + invertedObj[value] = key; } return invertedObj; } +module.exports = invert; + // a) What is the current return value when invert is called with { a : 1 } +// invert({ a: 1 }) +// Result: { key: 1 } +// invertedObj.key = value → creates a "key" property (string), not a key variable // b) What is the current return value when invert is called with { a: 1, b: 2 } +// invert({ a: 1, b: 2 }) +// Result: { key: 2 } +// First iteration: { key: 1 } +// Second iteration: { key: 2 } ← overwritten! // c) What is the target return value when invert is called with {a : 1, b: 2} +// result: +// { "1": "a", "2": "b" } +// Values ​​become keys, keys become values // c) What does Object.entries return? Why is it needed in this program? +// Object.entries({ a: 1, b: 2 }) +// returns: [["a", 1], ["b", 2]] +// Why is this necessary? +// To access both the key and the value simultaneously +// Destructuring is used: const [key, value] = ["a", 1] // d) Explain why the current return value is different from the target output +// 1. Dot notation instead of bracket notation: +// invertedObj.key = value; // ❌ Creates a "key" property (string) +// invertedObj[key] = value; // ✅ Uses the value of the key variable +// 2. Incorrect key-value order: +// invertedObj[key] = value; // ❌ Key remains the key +// invertedObj[value] = key; // ✅ Value becomes the key // e) Fix the implementation of invert (and write tests to prove it's fixed!) diff --git a/Sprint-2/interpret/invert.test.js b/Sprint-2/interpret/invert.test.js new file mode 100644 index 000000000..4424fa6b7 --- /dev/null +++ b/Sprint-2/interpret/invert.test.js @@ -0,0 +1,29 @@ +const invert = require("./invert.js"); + +// happy path +test("inverts a single key-value pair", () => { + expect(invert({ a: 1})).toEqual({ "1": "a" }); +}); + +test("inverts with string values", () => { + expect(invert({ x: "hello", y: "world" })).toEqual({ + hello: "x", + world: "y" + }); +}); + +// edge cases +test("handles empty pbject", () => { + expect(invert({})).toEqual({}); +}); + +test("handles duplicate values (last key wins)", () => { + expect(invert({ a:1, b: 1 })).toEqual({ "1": "b" }); +}); + +// error handling +test("throws TypeError for invalid input", () => { + expect(() => invert(null)).toThrow(TypeError); + expect(() => invert([1, 2])).toThrow(TypeError); + expect(() => invert("string")).toThrow(TypeError); +}); \ No newline at end of file From 095a8ae52e1e8bfd73d25b024427394f5f1b2131 Mon Sep 17 00:00:00 2001 From: Nik Bezdzenariy Date: Sun, 12 Apr 2026 13:48:14 +0200 Subject: [PATCH 4/4] feat(sprint-2): complete stretch tasks and tests --- Sprint-2/stretch/count-words.js | 54 +++++++++++++++++++++++++ Sprint-2/stretch/count-words.test.js | 59 ++++++++++++++++++++++++++++ Sprint-2/stretch/mode.js | 47 ++++++++++++++++++++++ Sprint-2/stretch/till.js | 35 +++++++++++++++++ Sprint-2/stretch/totalTill.test.js | 54 +++++++++++++++++++++++++ 5 files changed, 249 insertions(+) create mode 100644 Sprint-2/stretch/count-words.test.js create mode 100644 Sprint-2/stretch/totalTill.test.js diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..eaf430415 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,57 @@ 3. Order the results to find out which word is the most common in the input */ + +/** + * counts the frequency of words in aa string. + * + * @param {string} text - the input string + * @returns {object} - object with words as keys and counts as values + * + * @example + * countWords("hello world hello") + * // returns: { hello: 2, world: 1 } + */ +function countWords(text) { + // 1. validate input type + if (typeof text !== 'string') { + throw new TypeError('Input must be a string'); + } + + // 2. handle empty string edge case + if (text.length === 0) { + return {}; + } + + // 3. convert to lowercase (ignore case) + const lowerText = text.toLowerCase(); + + // 4. remove punctuation (adv challenge) + // replace all non-word char (except spaces) with empty str + // /[^\w\s]/g - replaces everything that is NOT (^) a word char (\w) or whitespace (\s) + const cleanText = lowerText.replace(/[^\w\s]/g, ''); + + // 5. split into words + // /\s+/ - splits by one or more (+) whitespace characters (\s) + const words = cleanText.split(/\s+/); // split by one or more spaces + + // 6. initialize empty result objects + const counts = {}; + + // 7. count each word + for (const word of words) { + // skip empty strings (from extra spaces at start/end) + if (word === '') { + continue; + } + + // increment counter using tally pattern + // Tally pattern: if key doesn't exist (undefined), use 0, then add 1 + counts[word] = (counts[word] || 0) + 1; + } + + // 8. return counts object + return counts; +} + +module.exports = countWords \ No newline at end of file diff --git a/Sprint-2/stretch/count-words.test.js b/Sprint-2/stretch/count-words.test.js new file mode 100644 index 000000000..285e944e4 --- /dev/null +++ b/Sprint-2/stretch/count-words.test.js @@ -0,0 +1,59 @@ +const countWords = require("./count-words.js") + +// basic func +test("counts word frequency", () => { + expect(countWords("you and me and you")).toEqual({ + you: 2, + and: 2, + me: 1 + }); +}); + +// edge cases: empty sting +test("returns empty object for empty string", () => { + expect(countWords(" ")).toEqual({}); +}); + +// case insensitivity +test("ignores case", () => { + expect(countWords("Hello hello HELLO")).toEqual({ + hello: 3 + }); +}); + +// punctuation removal +test("removes punctuation", () => { + expect(countWords("hello, world!")).toEqual({ + hello: 1, + world: 1 + }); +}); + +// multiple spaces +test("handles multiple spaces", () => { + expect(countWords("word1 word2")).toEqual({ + word1: 1, + word2: 1 + }); +}); + +// error handling +test("throws TypeError for non-string input", () => { + expect(() => countWords(123)).toThrow(TypeError); + expect(() => countWords(null)).toThrow(TypeError); + expect(() => countWords(undefined)).toThrow(TypeError); +}); + +// complex example +test("handles complex sentence", () => { + expect(countWords("The quick brown fox jumps over the lazy dog")).toEqual({ + the: 2, + quick: 1, + brown: 1, + fox: 1, + jumps: 1, + over: 1, + lazy: 1, + dog: 1 + }); +}); \ No newline at end of file diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..05fa30112 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -8,6 +8,11 @@ // refactor calculateMode by splitting up the code // into smaller functions using the stages above +/* +// given an implementation of calculateMode +// refactor by splitting into smaller functions + +// 1. track frequency of each value function calculateMode(list) { // track frequency of each value let freqs = new Map(); @@ -34,3 +39,45 @@ function calculateMode(list) { } module.exports = calculateMode; +*/ + +function getFrequencies(list) { + const freqs = new Map(); + + for (const num of list) { + if (typeof num !== 'number') { + continue; + } + + freqs.set(num, (freqs.get(num) || 0) + 1); + } + + return freqs; +} + +// stage 2: find the value with the highest frequency +function findMode(freqs) { + let maxFreq = 0; + let mode = NaN; + + for (const [num, freq] of freqs) { + if(freq > maxFreq) { + mode = num; + maxFreq = freq; + } + } + + return mode; +} + +// main function (refactored) +function calculateMode(list) { + if (!Array.isArray(list) || list.length === 0) { + return NaN; + } + + const frequencies = getFrequencies(list); + return findMode(frequencies); +} + +module.exports = calculateMode; \ No newline at end of file diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..9af0b3e32 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -4,6 +4,7 @@ // When this till object is passed to totalTill // Then it should return the total amount in pounds +/* function totalTill(till) { let total = 0; @@ -23,9 +24,43 @@ const till = { const totalAmount = totalTill(till); // a) What is the target output when totalTill is called with the till object +// bug: +// "1p" * 10 // NaN (string × number = NaN) // b) Why do we need to use Object.entries inside the for...of loop in this function? +// to get both key and value simultaneously through destructuring. +// Object.entries() turns an object into an array of [key, value] pairs: +Object.entries(till) +[["1p", 10], ["5p", 6], ["50p", 4], ["20p", 10]] +// allows to use destructuring in a loop: +for (const [coin, quantity] of Object.entries(till)) { +// coin = "1p", quantity = 10 +} // c) What does coin * quantity evaluate to inside the for...of loop? +// NaN because "1p" is a string, not a number. +// therefore, doesn't work // d) Write a test for this function to check it works and then fix the implementation of totalTill +// fix: use parseInt(coin) to extract a number from a string. +*/ + +// totalTill takes an object representing coins in a till + +function totalTill(till) { + let totalInPence = 0; + + for (const [coin, quantity] of Object.entries(till)) { + // extract number from string "50p" -> 50 + const coinValue = parseInt(coin); + + if (!isNaN(coinValue)) { + totalInPence += coinValue * quantity; + } + } + + // convert pence to pounds and format to 2 decimal places + return `£${(totalInPence / 100).toFixed(2)}`; +} + +module.exports = totalTill; \ No newline at end of file diff --git a/Sprint-2/stretch/totalTill.test.js b/Sprint-2/stretch/totalTill.test.js new file mode 100644 index 000000000..ea3bbcbbe --- /dev/null +++ b/Sprint-2/stretch/totalTill.test.js @@ -0,0 +1,54 @@ +const totalTill = require("./till.js"); + +// basic func +test("calculates total amount in pounds", () => { + const till = { + "1p": 10, + "5p": 6, + "50p": 4, + "20p": 10, + }; + + expect (totalTill(till)).toBe("£4.40"); +}); + +// edge case: empty till +test("handles empty till", () => { + expect(totalTill({})).toBe("£0.00"); +}); + +// single coin type +test("handles single coin type", () => { + const till = { + "1p": 100, + }; + + expect(totalTill(till)).toBe("£1.00"); +}); + +// large amounts +test("handle large amounts", () => { + const till = { + "1p": 50, + "2p": 50, + "5p": 20, + "10p": 10, + "20p": 5, + "50p": 2, + "100p": 1, // £1 coin + "200p": 1, // £2 coin + }; + + // 50 + 100 + 100 + 100 + 100 + 100 + 100 + 200 = 850p = £8.50 + expect(totalTill(till)).toBe("£8.50"); +}); + +// zero quantities +test("handles zero quantites", () => { + const till = { + "1p": 0, + "5p": 0, + }; + + expect(totalTill(till)).toBe("£0.00"); +}); \ No newline at end of file