diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..b3051b28e 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -4,6 +4,27 @@ // but it isn't working... // Fix anything that isn't working +/*const address = { + houseNumber: 42, + street: "Imaginary Road", + city: "Manchester", + country: "England", + postcode: "XYZ 123", +}; + +console.log(`My house number is ${address[0]}`);*/ + +//--------------------------------------------------------------------------------- + +/*Predict and explain first... +address is an object, not an array. +Objects don’t have numeric indexes like [0]. +So address[0] returns undefined. +That’s why the output would be: +My house number is undefined +*/ + +//Fixed code const address = { houseNumber: 42, street: "Imaginary Road", @@ -12,4 +33,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..03b722310 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -3,7 +3,7 @@ // 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 -const author = { +/*const author = { firstName: "Zadie", lastName: "Smith", occupation: "writer", @@ -13,4 +13,30 @@ const author = { for (const value of author) { console.log(value); +}*/ + +//------------------------------------------------------------------------------------------ + +// Prediction and explanation + +/*This gives an error because: +author is a plain object +for...of only works with iterables +Objects are not iterable by default +So JavaScript throws: +TypeError: author is not iterable + */ + +//fixed code + +const author = { + firstName: "Zadie", + lastName: "Smith", + occupation: "writer", + age: 40, + alive: true, +}; + +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..81cafba92 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -4,7 +4,7 @@ // Each ingredient should be logged on a new line // How can you fix it? -const recipe = { +/*const recipe = { title: "bruschetta", serves: 2, ingredients: ["olive oil", "tomatoes", "salt", "pepper"], @@ -12,4 +12,41 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe}`);*/ + +//---------------------------------------------------------------------------------------------------------- + +// Prediction and explanation +/*The output will be: +bruschetta serves 2 + ingredients: +[object Object] + +The template literal creates a single string. +${recipe.title} is replaced with "bruschetta". +${recipe.serves} is replaced with 2. +${recipe} is an object, so JavaScript must convert it to a string before inserting it into the template literal. +A plain object's default string representation is "[object Object]". + +So JavaScript effectively creates this string before passing it to console.log(): + +bruschetta serves 2 + ingredients: +[object Object] + +If the goal is to print each ingredient, we need to access the ingredients array (recipe.ingredients) instead of the whole recipe object. */ + +//Fixed code + +const recipe = { + title: "bruschetta", + serves: 2, + ingredients: ["olive oil", "tomatoes", "salt", "pepper"], +}; + +console.log(`${recipe.title} serves ${recipe.serves} ingredients:`); +recipe.ingredients.forEach(printIngredients); + +function printIngredients(item) { + console.log(item); +} diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..aa3980d0f 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,8 @@ -function contains() {} +function contains(obj, val) { + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + return false; + } + return Object.keys(obj).includes(val); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..f3f9e3302 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -1,5 +1,26 @@ const contains = require("./contains.js"); +describe("contains", () => { + test("Returns true if the object contains the key", () => { + expect(contains({ a: 1, b: 2 }, "a")).toEqual(true); + expect(contains({ cat: 1, dog: 2, cow: 2 }, "cow")).toEqual(true); + }); + + test("Returns false if the object doesn't contain the key", () => { + expect(contains({ a: 1, b: 2 }, "c")).toEqual(false); + expect(contains({ cat: 1, dog: 2, cow: 2 }, "pig")).toEqual(false); + }); + + test("Returns false if the object is empty", () => { + expect(contains({}, "c")).toEqual(false); + }); + test("Returns false if the input is invalid", () => { + expect(contains([1, 2, 3, 4], 3)).toEqual(false); + expect(contains(null, 3)).toEqual(false); + expect(contains("Hello World", "o")).toEqual(false); + }); +}); + /* Implement a function called contains that checks an object contains a particular property @@ -20,7 +41,7 @@ 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.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..7436745f2 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,15 @@ -function createLookup() { - // implementation here +function createLookup(countryCurrencyPairs) { + let newObj = {}; + if (!Array.isArray(countryCurrencyPairs)) { + throw new Error("Input must be an array"); + } + + countryCurrencyPairs.forEach((pair) => { + if (Array.isArray(pair) && pair.length === 2) { + newObj[pair[0]] = pair[1]; + } + }); + return newObj; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..e7d87c67c 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,6 +1,6 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); +//test.todo("creates a country currency code lookup for multiple codes"); /* @@ -33,3 +33,49 @@ It should return: 'CA': 'CAD' } */ + +describe("createLookup", () => { + test("Return an object with country codes and currency codes", () => { + const input = [ + ["US", "USD"], + ["CA", "CAD"], + ]; + const output = { + US: "USD", + CA: "CAD", + }; + expect(createLookup(input)).toEqual(output); + }); + test("Ignores invalid inner array values", () => { + const input = [["US", "USD"], "Hello", ["CA", "CAD"]]; + const output = { + US: "USD", + CA: "CAD", + }; + expect(createLookup(input)).toEqual(output); + }); + test("returns empty object for empty inner arrays", () => { + expect(createLookup([[]])).toEqual({}); + }); + test("Handle repeat keys", () => { + const input = [ + ["US", "USD"], + ["CA", "CAD"], + ["US", "US-Dollar"], + ]; + const output = { + US: "US-Dollar", + CA: "CAD", + }; + expect(createLookup(input)).toEqual(output); + }); + + test("Throws an error for invalid input", () => { + expect(() => createLookup(null)).toThrow(); + expect(() => createLookup("Hello World")).toThrow(); + expect(() => createLookup({})).toThrow(); + }); + test("Return an empty object with empty input", () => { + expect(createLookup([])).toEqual({}); + }); +}); diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..b5a7e631c 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,13 +1,33 @@ function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { - return queryParams; + if (typeof queryString !== "string") { + throw new Error("Invalid input"); } - const keyValuePairs = queryString.split("&"); + if (queryString.length === 0) return queryParams; - for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; + const parseQueryStringArray = queryString.replace(/\+/g, " ").split("&"); + let key; + let value; + for (const pair of parseQueryStringArray) { + if (pair === "") continue; + let equalIndex = pair.indexOf("="); + if (equalIndex === -1) { + key = pair; + value = ""; + } else { + key = decodeURIComponent(pair.slice(0, equalIndex)); + value = decodeURIComponent(pair.slice(equalIndex + 1)); + } + if (Object.hasOwn(queryParams, key)) { + if (Array.isArray(queryParams[key])) { + queryParams[key].push(value); + } else { + queryParams[key] = [queryParams[key]]; + queryParams[key].push(value); + } + } else { + queryParams[key] = value; + } } return queryParams; diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 328b8df61..37bd6b2e5 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -3,7 +3,7 @@ // Below are some test cases the implementation doesn't handle well. // Fix the implementation for these tests, 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"); test("should parse values containing '='", () => { expect(parseQueryString("equation=a=b-2")).toEqual({ @@ -25,6 +25,36 @@ test("should accept empty string as key or as value", () => { expect(parseQueryString("=")).toEqual({ "": "" }); }); +test("should return empty object for empty input", () => { + expect(parseQueryString("")).toEqual({}); +}); + +test("throws an error for invalid input", () => { + expect(() => parseQueryString(null)).toThrow("Invalid input"); + expect(() => parseQueryString({})).toThrow("Invalid input"); + expect(() => parseQueryString([])).toThrow("Invalid input"); + expect(() => parseQueryString(undefined)).toThrow("Invalid input"); + expect(() => parseQueryString(123)).toThrow("Invalid input"); + expect(() => parseQueryString(true)).toThrow("Invalid input"); +}); + +test("should work on duplicate empty keys", () => { + expect(parseQueryString("=one&=two")).toEqual({ + "": ["one", "two"], + }); +}); +test("should work on duplicate empty values", () => { + expect(parseQueryString("key=&key=")).toEqual({ + key: ["", ""], + }); +}); + +test("should work on & inside values", () => { + expect(parseQueryString("text=Tom%26Jerry")).toEqual({ + text: "Tom&Jerry", + }); +}); + test("should decode percent-encoded characters", () => { expect(parseQueryString("%24half=1%2F2")).toEqual({ $half: "1/2", diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..ae408126d 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,11 @@ -function tally() {} - +function tally(input) { + const obj = {}; + if (input === null || !Array.isArray(input)) { + throw new Error("Invalid input"); + } + input.forEach((el) => { + obj[el] = (obj[el] || 0) + 1; + }); + return obj; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..46af5d381 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -23,7 +23,7 @@ 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.todo("tally on an empty array returns an empty object"); // Given an array with duplicate items // When passed to tally @@ -32,3 +32,35 @@ test.todo("tally on an empty array returns an empty object"); // Given an invalid input like a string // When passed to tally // Then it should throw an error + +describe("tally", () => { + test("Return an empty object for empty array input", () => { + expect(tally([])).toEqual({}); + }); + test("Return an object containing the count for each unique item", () => { + expect(tally(["cat"])).toEqual({ cat: 1 }); + expect(tally(["cat", "dog", "rat", "duck"])).toEqual({ + cat: 1, + dog: 1, + rat: 1, + duck: 1, + }); + }); + test("Return an object containing the count for duplicate items", () => { + expect(tally(["cat", "dog", "rat", "cat", "duck", "dog"])).toEqual({ + cat: 2, + dog: 2, + rat: 1, + duck: 1, + }); + expect(tally([1, 2, 1])).toEqual({ + 1: 2, + 2: 1, + }); + }); + test("throws an error for invalid input", () => { + expect(() => tally("Hello World")).toThrow("Invalid input"); + expect(() => tally({ cat: 3 })).toThrow("Invalid input"); + expect(() => tally(null)).toThrow("Invalid input"); + }); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..11a8c7c29 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -6,7 +6,7 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} -function invert(obj) { +/*function invert(obj) { const invertedObj = {}; for (const [key, value] of Object.entries(obj)) { @@ -14,16 +14,56 @@ function invert(obj) { } return invertedObj; -} +}*/ + +//------------------------------------------------------------------------------------------ // 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 } +// { key: 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? +//It returns, +/* [["a", 1],["b", 2]] + + It is needed because the program needs to access both the key and the value of each object property in order to invert them. + Object.entries() gives us each key-value pair together, so we can swap them + */ + +//------------------------------------------------------------------------------------------ // d) Explain why the current return value is different from the target output +/*The current return value is different from the target output because key is a variable, +but invertedObj.key does not use the variable's value. +In JavaScript, dot notation treats key as a literal property name, so it creates a property called "key" in the object. +To use the value stored inside the key variable as the property name, we need bracket notation: invertedObj[key]. */ + +//------------------------------------------------------------------------------------------ // e) Fix the implementation of invert (and write tests to prove it's fixed!) + +function invert(obj) { + const invertedObj = {}; + + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + throw new Error("Invalid input"); + } + + for (const [key, value] of Object.entries(obj)) { + invertedObj[value] = key; + } + + return invertedObj; +} +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..cab1e208a --- /dev/null +++ b/Sprint-2/interpret/invert.test.js @@ -0,0 +1,24 @@ +const invert = require("./invert.js"); +describe("invert", () => { + test("returns error for an invalid input", () => { + expect(() => invert("Hello World")).toThrow("Invalid input"); + expect(() => invert(["cat", "dog", "pig"])).toThrow("Invalid input"); + expect(() => invert(null)).toThrow("Invalid input"); + }); + + test("returns an empty object for an empty object input", () => { + expect(invert({})).toEqual({}); + }); + + test("handles an object with a single property", () => { + expect(invert({ cat: 3 })).toEqual({ 3: "cat" }); + }); + + test("returns an object with keys and values swapped", () => { + expect(invert({ cat: 3, dog: 2 })).toEqual({ 3: "cat", 2: "dog" }); + }); + + test("handles duplicate values by keeping the last key", () => { + expect(invert({ cat: 3, dog: 2, pig: 3 })).toEqual({ 3: "pig", 2: "dog" }); + }); +}); diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..0dfe8293c 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,19 @@ 3. Order the results to find out which word is the most common in the input */ + +function countWords(Str) { + if (Str.trim() === "" || typeof Str !== "string") { + return {}; + } + + const stringSet = Str.trim() + .toLowerCase() + .replace(/[.,!?]/g, "") + .split(/\s+/); + const wordObject = {}; + for (const el of stringSet) { + wordObject[el] = (wordObject[el] || 0) + 1; + } + return wordObject; +} diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..445fdc079 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -8,9 +8,9 @@ // 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 calculateMode(list) { +// track frequency of each value +/*let freqs = new Map(); for (let num of list) { if (typeof num !== "number") { @@ -19,8 +19,10 @@ function calculateMode(list) { freqs.set(num, (freqs.get(num) || 0) + 1); } +*/ - // Find the value with the highest frequency +// Find the value with the highest frequency +/* let maxFreq = 0; let mode; for (let [num, freq] of freqs) { @@ -31,6 +33,37 @@ function calculateMode(list) { } return maxFreq === 0 ? NaN : mode; + return true; } +calculateMode([1, 3, "2", 2, 3, null]); + +module.exports = calculateMode;*/ + +function calculateFrequencies(list) { + const freqs = new Map(); + + for (let num of list) { + if (typeof num === "number") { + freqs.set(num, (freqs.get(num) || 0) + 1); + } + } + return freqs; +} +function findMode(freqs) { + let maxFreq = 0; + let mode; + for (let [num, freq] of freqs) { + if (freq > maxFreq) { + maxFreq = freq; + mode = num; + } + } + return maxFreq === 0 ? NaN : mode; +} + +function calculateMode(list) { + const freqs = calculateFrequencies(list); + return findMode(freqs); +} module.exports = calculateMode; diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..e56296d80 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -3,7 +3,7 @@ // Given an object of coins // When this till object is passed to totalTill // Then it should return the total amount in pounds - +/* function totalTill(till) { let total = 0; @@ -21,11 +21,39 @@ const till = { "20p": 10, }; const totalAmount = totalTill(till); +console.log(totalAmount);*/ + +//----------------------------------------------------------------------------------------------------- // 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? +// Objects are not directly iterable, so we cannot use a for...of loop on an object. +// Object.entries converts the object into an array of key-value pairs. +// Each pair is an array containing [key, value], which allows us to use for...of with destructuring to access coin and quantity separately. // c) What does coin * quantity evaluate to inside the for...of loop? +// It calculates the total value of each type of coin by multiplying the coin value by its quantity. +// However, coin is initially a string like "50p", so it must be converted into a number first before multiplication. // d) Write a test for this function to check it works and then fix the implementation of totalTill + +function totalTill(till) { + let total = 0; + + for (const [coin, quantity] of Object.entries(till)) { + const coinValue = Number(coin.replace("p", "")); + total += coinValue * quantity; + } + + return `£${(total / 100).toFixed(2)}`; +} + +const till = { + "1p": 10, + "5p": 6, + "50p": 4, + "20p": 10, +}; +const totalAmount = totalTill(till);