diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..38187ac38 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,5 +1,7 @@ // Predict and explain first... - +// it will fail because the code is trying to access the houseNumber property of the address object, +// but it is not defined correctly. The correct way to access the houseNumber property is to +// use dot notation, like this: address.houseNumber. // This code should log out the houseNumber from the address object // but it isn't working... // Fix anything that 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..da30f9f5d 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,5 +1,5 @@ // Predict and explain first... - +// for...of can’t iterate plain objects directly, so it will throw a TypeError . // 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 +11,6 @@ const author = { alive: true, }; -for (const value of author) { +for (const value of Object.values(author)) { console.log(value); } diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..2989288a7 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -1,5 +1,5 @@ // Predict and explain first... - +// The console.log will print the whole object instead of the ingredients list. // 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? @@ -10,6 +10,9 @@ const recipe = { ingredients: ["olive oil", "tomatoes", "salt", "pepper"], }; -console.log(`${recipe.title} serves ${recipe.serves} - ingredients: -${recipe}`); +console.log(`${recipe.title} serves ${recipe.serves}`); +console.log("ingredients:"); + +for (const ingredient of recipe.ingredients) { + console.log(ingredient); +} diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..39cb852ce 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,9 @@ -function contains() {} +function contains(object, propertyName) { + if (object === null || typeof object !== "object" || Array.isArray(object)) { + return false; + } + + return Object.prototype.hasOwnProperty.call(object, propertyName); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..6f581a9df 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -20,16 +20,27 @@ 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 returns true for an existing property", () => { + 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 returns false for a non-existent property", () => { + 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 returns false for invalid parameters like an array", () => { + expect(contains(["a", "b"], "0")).toBe(false); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..eed464452 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,20 @@ -function createLookup() { - // implementation here +function createLookup(pairs) { + if (!Array.isArray(pairs)) { + return {}; + } + + const lookup = {}; + + for (const pair of pairs) { + if (!Array.isArray(pair) || pair.length < 2) { + continue; + } + + const [countryCode, currencyCode] = pair; + 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..69146cb48 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,6 +1,22 @@ 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 countryCurrencyPairs = [ + ["US", "USD"], + ["CA", "CAD"], + ["GB", "GBP"], + ]; + + expect(createLookup(countryCurrencyPairs)).toEqual({ + US: "USD", + CA: "CAD", + GB: "GBP", + }); +}); + +test("returns an empty object for invalid input", () => { + expect(createLookup("not-an-array")).toEqual({}); +}); /* diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..cc43623d3 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) { + + if (typeof queryString !== "string" || queryString.length === 0) { + return queryParams; + } + + const normalizedQueryString = queryString.startsWith("?") + ? queryString.slice(1) + : queryString; + + if (normalizedQueryString.length === 0) { return queryParams; } - const keyValuePairs = queryString.split("&"); + + const keyValuePairs = normalizedQueryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); + if (pair.length === 0) { + continue; + } + + const separatorIndex = pair.indexOf("="); + const key = separatorIndex === -1 ? pair : pair.slice(0, separatorIndex); + const value = separatorIndex === -1 ? "" : pair.slice(separatorIndex + 1); + + if (key.length === 0) { + continue; + } + queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..c01965aa1 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -10,3 +10,23 @@ test("parses querystring values containing =", () => { "equation": "x=y+1", }); }); + +test("parses a key without = as an empty string value", () => { + expect(parseQueryString("debug")).toEqual({ + debug: "", + }); +}); + +test("ignores empty pairs between separators", () => { + expect(parseQueryString("a=1&&b=2&")).toEqual({ + a: "1", + b: "2", + }); +}); + +test("supports query strings starting with ?", () => { + expect(parseQueryString("?name=cyf&city=manchester")).toEqual({ + name: "cyf", + city: "manchester", + }); +}); diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..4db1958e6 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 = {}; + + 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..83223ad4e 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -23,12 +23,24 @@ 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("tally returns counts for each unique item", () => { + 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("tally throws an error for invalid input", () => { + expect(() => tally("not-an-array")).toThrow("tally expects an array"); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..4b667e4ab 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -10,12 +10,14 @@ function invert(obj) { const invertedObj = {}; for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; + invertedObj[value] = key; } return invertedObj; } +module.exports = invert; + // a) What is the current return value when invert is called with { a : 1 } // b) What is the current return value when invert is called with { a: 1, b: 2 } diff --git a/Sprint-2/interpret/invert.test.js b/Sprint-2/interpret/invert.test.js new file mode 100644 index 000000000..d2333aa1c --- /dev/null +++ b/Sprint-2/interpret/invert.test.js @@ -0,0 +1,18 @@ +const invert = require("./invert.js"); + +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({}); +}); diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..9beafdcc1 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,28 @@ 3. Order the results to find out which word is the most common in the input */ + +function countWords(text) { + if (typeof text !== "string" || text.trim().length === 0) { + return {}; + } + + const normalisedText = text + .toLowerCase() + .replace(/[.,!?;:]/g, " "); + + const words = normalisedText.split(/\s+/).filter(Boolean); + const wordCounts = {}; + + for (const word of words) { + if (wordCounts[word] === undefined) { + wordCounts[word] = 1; + } else { + wordCounts[word] += 1; + } + } + + return wordCounts; +} + +module.exports = countWords; diff --git a/Sprint-2/stretch/count-words.test.js b/Sprint-2/stretch/count-words.test.js new file mode 100644 index 000000000..2d3b34bca --- /dev/null +++ b/Sprint-2/stretch/count-words.test.js @@ -0,0 +1,19 @@ +const countWords = require("./count-words.js"); + +test("counts repeated words", () => { + expect(countWords("you and me and you")).toEqual({ + you: 2, + and: 2, + me: 1, + }); +}); + +test("ignores case and punctuation", () => { + expect(countWords("Hello, hello! HELLO?")) .toEqual({ + hello: 3, + }); +}); + +test("returns empty object for empty input", () => { + expect(countWords("")).toEqual({}); +}); diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..f341668da 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -8,29 +8,37 @@ // 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(); +function getFrequencies(list) { + const frequencies = new Map(); - for (let num of list) { - if (typeof num !== "number") { + for (const item of list) { + if (typeof item !== "number") { continue; } - freqs.set(num, (freqs.get(num) || 0) + 1); + frequencies.set(item, (frequencies.get(item) || 0) + 1); } - // Find the value with the highest frequency - let maxFreq = 0; + return frequencies; +} + +function getModeFromFrequencies(frequencies) { + let maxFrequency = 0; let mode; - for (let [num, freq] of freqs) { - if (freq > maxFreq) { - mode = num; - maxFreq = freq; + + for (const [value, frequency] of frequencies) { + if (frequency > maxFrequency) { + mode = value; + maxFrequency = frequency; } } - return maxFreq === 0 ? NaN : mode; + return maxFrequency === 0 ? NaN : mode; +} + +function calculateMode(list) { + const frequencies = getFrequencies(list); + return getModeFromFrequencies(frequencies); } module.exports = calculateMode; diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..65245f5b1 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -8,20 +8,13 @@ function totalTill(till) { let total = 0; for (const [coin, quantity] of Object.entries(till)) { - total += coin * quantity; + const coinValueInPence = Number.parseInt(coin, 10); + total += coinValueInPence * quantity; } - return `£${total / 100}`; + return `£${(total / 100).toFixed(2)}`; } -const till = { - "1p": 10, - "5p": 6, - "50p": 4, - "20p": 10, -}; -const totalAmount = totalTill(till); - // a) What is the target output when totalTill is called with the till object // b) Why do we need to use Object.entries inside the for...of loop in this function? @@ -29,3 +22,5 @@ const totalAmount = totalTill(till); // c) What does coin * quantity evaluate to inside the for...of loop? // d) Write a test for this function to check it works and then fix the implementation of totalTill + +module.exports = totalTill; diff --git a/Sprint-2/stretch/till.test.js b/Sprint-2/stretch/till.test.js new file mode 100644 index 000000000..e90cfa7dc --- /dev/null +++ b/Sprint-2/stretch/till.test.js @@ -0,0 +1,16 @@ +const totalTill = require("./till.js"); + +test("totals till values in pounds", () => { + const till = { + "1p": 10, + "5p": 6, + "50p": 4, + "20p": 10, + }; + + expect(totalTill(till)).toBe("£4.40"); +}); + +test("returns £0.00 for an empty till", () => { + expect(totalTill({})).toBe("£0.00"); +});