diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..fde180d6d 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..3667ab42f 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -11,6 +11,10 @@ const author = { alive: true, }; -for (const value of author) { +for (const value of Object.values(author)) { console.log(value); } + + +// It did work because an object is not iterable as an array, but we can use Object.values() +// to get an array of the property values and then iterate over that array. \ No newline at end of file diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..5e389e417 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -4,12 +4,17 @@ // Each ingredient should be logged on a new line // How can you fix it? +// it won't work because the last line is calling the object property recipe and we +// need the ingredientes, the console.log is trying to log the whole array of ingredients +// as a string, but we want to log each ingredient on a new line. We can fix this by +// using a for loop to iterate over the ingredients array and log each ingredient separately. + const recipe = { - title: "bruschetta", + title: "Bruschetta", serves: 2, ingredients: ["olive oil", "tomatoes", "salt", "pepper"], }; -console.log(`${recipe.title} serves ${recipe.serves} - ingredients: -${recipe}`); + +console.log(`Recipe: ${recipe.title} \nserves: ${recipe.serves}`); +console.log(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..5ff840c0a 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,10 @@ -function contains() {} +function contains(obj, property) { + if (typeof obj !== "object" || obj === null || Array.isArray(obj)){ + return false; + } + + return Object.hasOwn(obj, property); +} + module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..7f0d07dc7 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -16,20 +16,51 @@ as the object doesn't contains a key of 'c' // Given a contains function // When passed an object and a property name // Then it should return true if the object contains the property, false otherwise +test("return true when an object has the property name", () => { + expect(contains({a: 1, b: 2}, "a")).toBe(true); +}) // Given an empty object // When passed to contains // Then it should return false -test.todo("contains on empty object returns false"); +test("returns false on empty object ", () => { + 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("return true when pass an object that contains the property name", () => { + expect(contains({name: "John", age: 30}, "name")).toBe(true); +}) // Given an object with properties // When passed to contains with a non-existent property name // Then it should return false +test("return false when pass an object that does not contain the property name", () => { + expect(contains({id: 12, status: "active"}, "age")).toBe(false); +}) // Given invalid parameters like an array -// When passed to contains +// When: Checking for a valid array property like 'length' or an index // Then it should return false or throw an error +test("return false when pass an array instead of an object", () => { + expect(contains(["Banana"], "length")).toBe(false); + expect(contains(["Apple"], "0")).toBe(false); +}) + +// Given a value that is null, undefined, or a primitive (like 123) +// When passed to the contains function +// Then it should return false to prevent errors and ensure only real objects are processed. +test("should return false for null and other non-objects", () => { + expect(contains(null, "prop")).toBe(false); + expect(contains(undefined, "prop")).toBe(false); + expect(contains(123, "toFixed")).toBe(false); +}); + +// Given: A standard object created with literal notation {}. +// When: It is passed to the 'contains' function looking for a built-in property like "toString". +// Then: It should return false because the object is not the "owner" of that property. +test("Should return false for inherited properties like 'toString'", () => { + expect(contains({}, "toString")).toBe(false); +}); \ No newline at end of file diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..dd08c9306 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,9 @@ -function createLookup() { - // implementation here +function createLookup(pairs) { + const result = {}; + for(const [country, currency] of pairs) { + result[country] = currency; + } + return result } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..b6ba7c768 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,6 +1,5 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); /* @@ -33,3 +32,20 @@ It should return: 'CA': 'CAD' } */ + +test("return an object with country codes as keys and currency codes as values", () => { + expect( createLookup([ ["MX", "MXN"], ["JP", "JPY"] ])).toEqual({ MX: "MXN", JP: "JPY"}); + expect( createLookup([ ["AU", "AUD"], ["CH", "CHF"] ])).toEqual({ AU: "AUD", CH: "CHF" }); +}); + +describe("return an object with country codes as keys and currency codes as values", () => { + [ + { input: [["US", "USD"],["CA", "CAD"]], expected: { US: "USD", CA: "CAD" } }, + { input: [["GB", "GBP"],["FR", "EUR"]], expected: { GB: "GBP", FR: "EUR" } }, + { input: [["JP", "JPY"],["AU", "AUD"]], expected: { JP: "JPY", AU: "AUD" } }, + ].forEach(({ input, expected }) => { + it(`should convert ${JSON.stringify(input)} into ${JSON.stringify(expected)}`, () => { + expect(createLookup(input)).toEqual(expected); + }); + }); +}); diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..80852a80d 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,15 +1,25 @@ function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { + if (!queryString || queryString.trim() === "" ) { return queryParams; } const keyValuePairs = queryString.split("&"); - for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; - } + for(const pair of keyValuePairs){ + if(!pair) continue; + + const [key, ...rest] = pair.split("="); + + const value = rest.join("="); + + if (key) { + const decodedKey = decodeURIComponent(key.replace(/\+/g, " ")); + const decodedValue = decodeURIComponent(value.replace(/\+/g, " ")); + queryParams[decodedKey] = decodedValue; + } + + } return queryParams; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..3341bbc01 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -5,8 +5,53 @@ const parseQueryString = require("./querystring.js") -test("parses querystring values containing =", () => { - expect(parseQueryString("equation=x=y+1")).toEqual({ - "equation": "x=y+1", + +// Given a querystring with a value that contains an = symbol ( + URL encoded as %2B and spaces encoded as + ) +// When passed to parseQueryString +// Then it should treat everything after the first = as the value +test('parses querystring values containing (=, +, " ")', () => { + expect(parseQueryString("equation=x=y%2B1")).toEqual({ + equation: "x=y+1", + }); +}); + +// Given an querystring string with spaces encoded as (%20 and +) +// when passed to parseQueryString +// then it should return the string with real spaces in both cases +test("decodes spaces correctly from both %20 and +", () => { + expect(parseQueryString("name=John%20Doe&city=New+York")).toEqual({ + "name": "John Doe", + "city": "New York", }); }); + + +// Given a URL-encoded key like like %5B%5D = [] +// When passed to parseQueryString +// Then it should decode the key to 'tags[]' +test("Handles URL-Encoded keys", () =>{ + expect(parseQueryString("tags%5B%5D=javascript")).toEqual({ + "tags[]": "javascript", + }); +}) + +// Given a string with multiple ampersands like a=1&&b=2&c=3&& +// When passed to parseQueryString +// Then it should ignore the empty segments and return valid pairs +test("Should ignore empty ampersand", () => { + expect(parseQueryString("a=1&&b=2")).toEqual({ + a: "1", + b: "2", + + }); +}) + +// Given a key without a value like 'flag' or 'empty=' +// When passed to parseQueryString +// Then it should return the key with an empty string as value +test("handles keys with no values", () => { + expect(parseQueryString("empty=&flag")).toEqual({ + "empty": "", + "flag": "" + }); +}); \ No newline at end of file diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..f5db8d5b5 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,14 @@ -function tally() {} +function tally(items) { + const counts = Object.create(null); + + if (!Array.isArray(items)) { + throw new Error("Input must be an array"); + } + + for (const item of items) { + 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..3ef605508 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -19,16 +19,29 @@ 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("Should return an object with the count of each unique item in the 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 }); +}); // 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("Should return an empty object when passed an empty array", () => { + expect(tally([])).toEqual({}); +}); // Given an array with duplicate items // When passed to tally // Then it should return counts for each unique item +test("Should return counts for each unique item in an array with duplicate items", () => { + expect(tally(['x', 'y', 'x', 'z', 'y', 'x'])).toEqual({ x: 3, y: 2, z: 1 }); +}); // Given an invalid input like a string // When passed to tally // Then it should throw an error +test("Should throw and error when passed and invalid input like a string", () => { + expect(() => tally('not an array')).toThrow('Input must be an array'); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..db022ed4a 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -6,24 +6,55 @@ // 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; + invertedObj[value] = key; } return invertedObj; } -// a) What is the current return value when invert is called with { a : 1 } +console.log(invert({ a: 1, b: 2 })); // Expected output: { '1': 'a', '2': 'b' } + + + +/* +a) What is the current return value when invert is called with { a : 1 } + * is the following { key: 1 } + +b) What is the current return value when invert is called with { a: 1, b: 2 } + *the first pair is { key: 1 } + *and the second pair the value is { key: '2' } that means the first pair is + being overwritten by the second pair, so the final return value is { key: '2' } + +c) What is the target return value when invert is called with {a : 1, b: 2} + *the target return value is { "1": "a", "2": "b" } + +d) What does Object.entries return? Why is it needed in this program? + *Return value: It returns an array of arrays, where each sub-array is a [key, value] + pair (e.g., [['a', 1]]). + *Why it's needed: Objects are not iterables by default. We need Object.entries + to convert the object into an array so we can use the for...of loop to go through each pair. + +e) Explain why the current return value is different from the target output + *Literal Property Creation: The current code uses dot notation (invertedObj.key), + which creates a property literally named "key" instead of using the value stored + inside the key variable. + + *Overwriting: Because it keeps targetting the same literal property name ("key"), + each iteration overwrites the previous one. This is why the final object only contains + the last value processed. + + *Target vs. Current: The target output requires bracket notation (invertedObj[value]) + to dynamically assign the new keys based on the input's values. -// b) What is the current 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} +f) Fix the implementation of invert (and write tests to prove it's fixed!) + expect({ a: 1, b: 2 })toEqual({ '1': 'a', '2': 'b' } ) now is working as expected -// c) What does Object.entries return? Why is it needed in this program? +**/ -// d) Explain why the current return value is different from the target output -// e) Fix the implementation of invert (and write tests to prove it's fixed!) diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..9f95c1f93 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -8,22 +8,25 @@ // 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; - } +// track frequency of each value +function getFrequency(list) { + let frequency = new Map(); - freqs.set(num, (freqs.get(num) || 0) + 1); + for (let num of list) { + if (typeof num !== "number") continue; + frequency.set(num, (frequency.get(num) || 0) + 1); } + return frequency; +} + - // Find the value with the highest frequency +// Find the value with the highest frequency +function getHighestFrequency(frequency) { let maxFreq = 0; let mode; - for (let [num, freq] of freqs) { + + for (let [num, freq] of frequency) { if (freq > maxFreq) { mode = num; maxFreq = freq; @@ -33,4 +36,11 @@ function calculateMode(list) { return maxFreq === 0 ? NaN : mode; } +// The refactored calculateMode using the stages above +function calculateMode(list) { + const frequency = getFrequency(list); + return getHighestFrequency(frequency); +} + + module.exports = calculateMode; diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..ba9ec4e37 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -8,24 +8,28 @@ function totalTill(till) { let total = 0; for (const [coin, quantity] of Object.entries(till)) { - total += coin * quantity; + const coinValue = parseInt(coin); + total += coinValue * 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 +module.exports = totalTill; -// b) Why do we need to use Object.entries inside the for...of loop in this function? +/* -// c) What does coin * quantity evaluate to inside the for...of loop? +a) What is the target output when totalTill is called with the till object + ----£4.40 because the total value of the coins in the till is 440 pence, which is equivalent to £4.40. -// d) Write a test for this function to check it works and then fix the implementation of totalTill +b) Why do we need to use Object.entries inside the for...of loop in this function? + ----Object.entries allows us to iterate over the key-value pairs of the till object, + otherwise is an object and we cannot directly iterate over it with a for...of loop. + +c) What does coin * quantity evaluate to inside the for...of loop? + ----£NaN because the coin values are strings multiplied by numbers, which results in NaN. + +d) Write a test for this function to check it works and then fix the implementation of totalTill + +**/ \ No newline at end of file diff --git a/Sprint-2/stretch/till.test.js b/Sprint-2/stretch/till.test.js new file mode 100644 index 000000000..5a3958a29 --- /dev/null +++ b/Sprint-2/stretch/till.test.js @@ -0,0 +1,28 @@ +const totalTill = require("./till.js"); + +describe("totalTill", () => { + test("returns the total amount in pounds for a given till object", () => { + const till = { + "1p": 10, + "5p": 6, + "50p": 4, + "20p": 10, + }; + + expect(totalTill(till)).toEqual("£4.40"); + }); + + test("returns £0.00 for an empty till", () => { + const emptyTill = {}; + + expect(totalTill(emptyTill)).toEqual("£0.00"); + }); + + test("handles tills with only one type of coin", () => { + const singleCoinTill = { + "10p": 5, + }; + + expect(totalTill(singleCoinTill)).toEqual("£0.50"); + }); +});