diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..b99ea43cf 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,4 +1,7 @@ // Predict and explain first... +//Prediction: The will be an error at ${address[0]}. +//Explanation: calling an object using the index as in array. Instead we should use the dot method(.address) or the brackets["address"]. + // This code should log out the houseNumber from the address object // but it isn't working... @@ -12,4 +15,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..3a540064c 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,4 +1,6 @@ // Predict and explain first... +// Prediction: Going to shows an error in the for loop. +// Explanation: we should have used the for .. in method (The for...in statement iterates over all enumerable string properties of an object (ignoring properties keyed by symbols), including inherited enumerable properties.) // 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 +13,6 @@ const author = { alive: true, }; -for (const value of author) { - console.log(value); +for (const value in author){ + console.log(`${value}: ${author[value]}`); } diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..bcc190551 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -1,4 +1,6 @@ // Predict and explain first... +// Prediction: An error in logging out the ingredients. +// Explanation: Because of the way to get the value or an element from an array should used the dot notation for example and reach each element using its index. // 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 +14,6 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients.join("\n")}`); + +module.exports = recipe; diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..33e75c7b5 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,10 @@ -function contains() {} +function contains(input, propertyName) { + + if (input !== null && typeof input === "object" && !Array.isArray(input)) { + return Object.prototype.hasOwnProperty.call(input, propertyName); + } + + return false; +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..1f8310810 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -17,19 +17,49 @@ 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 +test("given an object contains a property name, returns true", () => { + const input = { a: 1, b: 2 }; + const propertyName = "a"; + expect(contains(input, propertyName)).toBe(true); +}); + // Given an empty object // When passed to contains // Then it should return false -test.todo("contains on empty object returns false"); + +test("given an empty object, returns false", () => { + const input = {}; + const propertyName = "a"; + expect(contains(input, propertyName)).toBe(false); +}); // Given an object with properties // When passed to contains with an existing property name // Then it should return true +test("given an object contains a property name, returns true", () => { + const input = { a: 1, b: 2 }; + const propertyName = "a"; + expect(contains(input, propertyName)).toBe(true); +}); + // Given an object with properties // When passed to contains with a non-existent property name // Then it should return false +test("given an object does not contain a property name, returns false", () => { + const input = { a: 1, b: 2 }; + const propertyName = "c"; + expect(contains(input, propertyName)).toBe(false); +}); + + // Given invalid parameters like an array // When passed to contains // Then it should return false or throw an error + +test("given invalid parameters like an array, returns false", () => { + const input = [1, 2, 3]; + const propertyName = "0"; + expect(contains(input, propertyName)).toBe(false); +}); \ No newline at end of file diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..1accb2d54 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,11 @@ -function createLookup() { - // implementation here +function createLookup(pairs) { + const lookup = {}; + + for (const [country, currency] of pairs) { + lookup[country] = currency; + } + + return lookup; } -module.exports = createLookup; +module.exports = createLookup; \ No newline at end of file diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..0838285d4 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,7 +1,20 @@ 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", () => { + const input = [ + ["US", "USD"], + ["CA", "CAD"], + ["GB", "GBP"], + ]; + + const result = createLookup(input); + + expect(result).toEqual({ + US: "USD", + CA: "CAD", + GB: "GBP", + }); +}); /* Create a lookup object of key value pairs from an array of code pairs diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..5a2783014 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,12 +1,33 @@ function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { - return queryParams; + + if (!queryString) return queryParams; + + if (queryString.startsWith("?")) { + queryString = queryString.slice(1); } + const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); + if (!pair) continue; + + const firstEq = pair.indexOf("="); + + let key; + let value; + + if (firstEq === -1) { + key = pair; + value = ""; + } else { + key = pair.slice(0, firstEq); + value = pair.slice(firstEq + 1); + } + + key = decodeURIComponent(key); + value = decodeURIComponent(value); + queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..42ec0039d 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -10,3 +10,31 @@ test("parses querystring values containing =", () => { "equation": "x=y+1", }); }); + +test("parses multiple key=value pairs", () => { + expect(parseQueryString("a=1&b=2")).toEqual({ a: "1", b: "2" }); +}); + +test("handles empty value (a=)", () => { + expect(parseQueryString("a=")).toEqual({ a: "" }); +}); + +test("handles key with no '=' (a)", () => { + expect(parseQueryString("a")).toEqual({ a: "" }); +}); + +test("last value wins for repeated keys", () => { + expect(parseQueryString("a=1&a=2")).toEqual({ a: "2" }); +}); + +test("supports leading question mark", () => { + expect(parseQueryString("?a=1&b=2")).toEqual({ a: "1", b: "2" }); +}); + +test("ignores empty pairs from trailing or consecutive &", () => { + expect(parseQueryString("a=1&&b=2&")).toEqual({ a: "1", b: "2" }); +}); + +test("handles empty key (=value) producing empty-string key", () => { + expect(parseQueryString("=value")).toEqual({ "": "value" }); +}); \ No newline at end of file diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..dc79bc3dd 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,19 @@ -function tally() {} +function tally(items) { + if (!Array.isArray(items)) { + throw new Error("tally expects an array"); + } + + const counts = Object.create(null); + + for (const item of items) { + if (counts[item] === undefined) { + counts[item] = 1; + } else { + 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..8a8893067 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -19,16 +19,37 @@ const tally = require("./tally.js"); // 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("tally returns counts for each unique item", () => { + expect(tally(["a", "a", "b", "c"])).toEqual({ + a: 2, + b: 1, + c: 1, + }); +}); // Given an empty array // When passed to tally // Then it should return an empty object -test.todo("tally on an empty array returns an empty object"); +test("tally on an empty array returns an empty object", () => { + expect(tally([])).toEqual({}); +}); // Given an array with duplicate items // When passed to tally // Then it should return counts for each unique item +test("tally counts each unique item", () => { + expect(tally(["a", "a", "b", "c", "a"])).toEqual({ + a: 3, + b: 1, + c: 1, + }); +}); // Given an invalid input like a string // When passed to tally // Then it should throw an error +test("tally throws an error when given a non-array", () => { + expect(() => tally("not an array")).toThrow(); + expect(() => tally(123)).toThrow(); + expect(() => tally({})).toThrow(); +}); \ No newline at end of file diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..7c7aa6230 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -16,14 +16,52 @@ function invert(obj) { return invertedObj; } -// a) What is the current return value when invert is called with { a : 1 } +// a) What is the current return value when invert is called with { a : 1 } >> { key: 1 } -// b) What is the current return value when invert is called with { a: 1, b: 2 } +// b) What is the current return value when invert is called with { a: 1, b: 2 } >> { key: 2 } -// c) What is the target return value when invert is called with {a : 1, b: 2} +// c) What is the target return value when invert is called with {a : 1, b: 2} >> { "1": "a", "2": "b" } -// c) What does Object.entries return? Why is it needed in this program? +// c) What does Object.entries return? Why is it needed in this program? >> Object.entries returns an array of key-value paris. -// d) Explain why the current return value is different from the target output +// d) Explain why the current return value is different from the target output >> Because invertedObj.key = value creates a property called (key) instead of using hte variable key. // e) Fix the implementation of invert (and write tests to prove it's fixed!) +// The fixed code: +function invert(obj) { + const invertedObj = {}; + + for (const [key, value] of Object.entries(obj)) { + invertedObj[value] = key; + } + + return invertedObj; +} + +module.exports = invert; + +// Tests: +const invert = require("./invert.js"); + +// Single key +test("invert works for one key-value pair", () => { + expect(invert({ a: 1 })).toEqual({ "1": "a" }); +}); + +// Multiple keys +test("invert swaps keys and values for multiple pairs", () => { + expect(invert({ a: 1, b: 2 })).toEqual({ + "1": "a", + "2": "b", + }); +}); + +// Empty object +test("invert of an empty object returns empty", () => { + expect(invert({})).toEqual({}); +}); + +// Values become string keys +test("invert always returns string keys", () => { + expect(invert({ x: 10 })).toEqual({ "10": "x" }); +}); diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..f805275ba 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,51 @@ 3. Order the results to find out which word is the most common in the input */ + + + +/* The implementation: +function countWords(str) { + const result = {}; + + if (!str || str.trim().length === 0) { + return result; + } + + const words = str.split(" "); + + for (const word of words) { + if (result[word] === undefined) { + result[word] = 1; + } else { + result[word] += 1; + } + } + + return result; +} + +module.exports = countWords; +*/ + +/* The tests: + +const countWords = require("./countWords.js"); + +test("counts words from a string", () => { + expect(countWords("you and me and you")).toEqual({ + you: 2, + and: 2, + me: 1, + }); +}); + +test("returns empty object for empty string", () => { + expect(countWords("")).toEqual({}); + expect(countWords(" ")).toEqual({}); +}); + +test("counts correctly with single word", () => { + expect(countWords("hello")).toEqual({ hello: 1 }); +}); +*/ \ No newline at end of file diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..f1dfd8bec 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -8,29 +8,38 @@ // 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) { - if (typeof num !== "number") { - continue; - } +// Stage 1: count frequencies +function countFrequencies(list) { + const freqs = new Map(); + for (const num of list) { + if (typeof num !== "number") continue; freqs.set(num, (freqs.get(num) || 0) + 1); } - // Find the value with the highest frequency + return freqs; +} + +// Stage 2: find the value with highest frequency +function findMode(freqs) { let maxFreq = 0; let mode; - for (let [num, freq] of freqs) { + + for (const [num, freq] of freqs.entries()) { if (freq > maxFreq) { - mode = num; maxFreq = freq; + mode = num; } } return maxFreq === 0 ? NaN : mode; } +// Main function +function calculateMode(list) { + const freqs = countFrequencies(list); + return findMode(freqs); +} + module.exports = calculateMode; + diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..d47c4b906 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -22,10 +22,32 @@ const till = { }; const totalAmount = totalTill(till); -// a) What is the target output when totalTill is called with the till object +// a) What is the target output when totalTill is called with the till object >> "£4.40" -// b) Why do we need to use Object.entries inside the for...of loop in this function? +// b) Why do we need to use Object.entries inside the for...of loop in this function? >> To iterate over both the keys (coin types) and values (quantities) of the till object. -// c) What does coin * quantity evaluate to inside the for...of loop? +// c) What does coin * quantity evaluate to inside the for...of loop? >> It evaluates to NaN because coin is a string (e.g., "1p") and cannot be directly multiplied by a number (quantity). // d) Write a test for this function to check it works and then fix the implementation of totalTill + +// Fixed implementation +function totalTillFixed(till) { + let total = 0; + + for (const [coin, quantity] of Object.entries(till)) { + const coinValue = parseInt(coin); + total += coinValue * quantity; + } + + return `£${(total / 100).toFixed(2)}`; +} + +// Test +const tillTest = { + "1p": 10, + "5p": 6, + "50p": 4, + "20p": 10, +}; +const totalAmountTest = totalTillFixed(tillTest); +console.log(totalAmountTest);