diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..7dd3f55c0 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,9 +1,3 @@ -// Predict and explain first... - -// This code should log out the houseNumber from the address object -// but it isn't working... -// Fix anything that isn't working - const address = { houseNumber: 42, street: "Imaginary Road", @@ -12,4 +6,9 @@ const address = { postcode: "XYZ 123", }; +/* Prediction */ +// This will throw an error. This is because unlike lists, key values are selected using a '.' console.log(`My house number is ${address[0]}`); + +/* Fix */ +console.log(`My house number is ${address.houseNumber}`); diff --git a/Sprint-2/debug/author.js b/Sprint-2/debug/author.js index 8c2125977..030f4da04 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,8 +1,4 @@ -// Predict and explain first... - -// 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 - +/* ======== Initial Script ======= */ const author = { firstName: "Zadie", lastName: "Smith", @@ -11,6 +7,25 @@ const author = { alive: true, }; -for (const value of author) { - console.log(value); +try { + for (const value of author) { + console.log(value); + } +} catch (error) { + console.error("Error:", error.message); +} + +/* ============== Prediction ============== */ +// It will print out only the name of the keys (i.e. firstName, lastName, +// occupation, age, and alive), but not their actual values. + +/* ============== Actual Result ============== */ +// Error: author is not iterable. +// This error occurs because the for...of loop can only be used with +// iterable objects like arrays, strings, etc. Since 'author' is an object, +// it is not iterable. To fix this, we can use a for...in loop instead. + +/* ============== Corrected Script ============== */ +for (const key in author) { + console.log(author[key]); } diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..249c7e216 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -4,6 +4,7 @@ // Each ingredient should be logged on a new line // How can you fix it? +/* ============== Initial Script ============== */ const recipe = { title: "bruschetta", serves: 2, @@ -13,3 +14,18 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: ${recipe}`); + +/* ============== Prediction ============== */ +// It will throw an error when trying to print the ingredients. +// This is because the entire recipe object was called instead of +// just its ingredients property. JavaScript cannot print an object, +// hence the error is thrown. + +/// ============== Actual Result ============== */ +// It tunrs out that JavaScript does not throw an error when +// trying to print an object. Instead, it prints out [object Object]. + +/* ============== Corrected Script ============== */ +console.log(`${recipe.title} serves ${recipe.serves} + ingredients: + - ${recipe.ingredients.join("\n - ")}`); diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..c2cc039e3 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,11 @@ -function contains() {} +function contains(object, property) { + if (typeof object !== "object" || object === null || Array.isArray(object)) { + throw new TypeError( + "First argument must be an object in the form { key: value }" + ); + } + + return object.hasOwnProperty(property); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..93df9b50a 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -1,35 +1,70 @@ const contains = require("./contains.js"); -/* -Implement a function called contains that checks an object contains a -particular property +describe("contains", () => { + // Case 1: Should return true if the property exists in object. + test("should return true when object contains passed property name", () => { + const objsWithValidProps = [ + [{ a: 1, b: 2 }, "b"], + [{ name: "John", age: 30 }, "name"], + [{ nested: { key: "value" } }, "nested"], + [{ id: 123, status: "active", language: "JavaScript" }, "status"], + [{ data: [], items: null }, "data"], + ]; -E.g. contains({a: 1, b: 2}, 'a') // returns true -as the object contains a key of 'a' + objsWithValidProps.forEach(([obj, prop]) => { + expect(contains(obj, prop)).toEqual(true); + }); + }); -E.g. contains({a: 1, b: 2}, 'c') // returns false -as the object doesn't contains a key of 'c' -*/ + // Case 2: Should return false if the object does not contain the given property. + test("should return false when object does not contain passed property name", () => { + const objsWithoutProps = [ + [{ a: 1, b: 2 }, "c"], + [{ name: "John", age: 30 }, "email"], + [{ nested: { key: "value" } }, "nonexistent"], + [{ id: 123, status: "active", language: "JavaScript" }, "description"], + [{ data: [], items: null }, "nonexistent"], + ]; -// Acceptance criteria: + objsWithoutProps.forEach(([obj, prop]) => { + expect(contains(obj, prop)).toEqual(false); + }); + }); -// 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 + // Case 3: Should return false if the object is empty. + test("should return false when object is empty", () => { + expect(contains({}, "anyProperty")).toEqual(false); + }); -// Given an empty object -// When passed to contains -// Then it should return false -test.todo("contains on empty object returns false"); + // Case 4: Should return false for properties that only exist in the prototype chain + test("should return false for properties in prototype chain", () => { + const objsWithProtoProps = [ + [{ a: 1 }, "toString"], + [{ name: "John", age: 30 }, "hasOwnProperty"], + [{ nested: { key: "value" } }, "isPrototypeOf"], + ]; -// Given an object with properties -// When passed to contains with an existing property name -// Then it should return true + objsWithProtoProps.forEach(([obj, prop]) => { + expect(contains(obj, prop)).toEqual(false); + }); + }); -// Given an object with properties -// When passed to contains with a non-existent property name -// Then it should return false + // Case 5: Should throw an error if a non-object is passed + test("should throw error when non-object is passed", () => { + const nonObjects = [ + null, + undefined, + 42, + "The Curse", + true, + Infinity, + ["string"], + ]; -// Given invalid parameters like an array -// When passed to contains -// Then it should return false or throw an error + nonObjects.forEach((nonObj) => { + expect(() => contains(nonObj, "prop")).toThrow( + "First argument must be an object in the form { key: value }" + ); + }); + }); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..8b46fd14a 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,35 @@ -function createLookup() { - // implementation here +function createLookup(countryCurrencyPairs) { + let countryCurrencyObj = {}; + + for (const countryCurrencyPair of countryCurrencyPairs) { + if (!Array.isArray(countryCurrencyPair)) { + throw new Error("Country-currency pairs must be in array format"); + } + + if (countryCurrencyPair.length !== 2) { + throw new Error( + "Country-currency pairs must contain exactly two elements: country and currency" + ); + } + + let [country, currency] = countryCurrencyPair; + + if (typeof country !== "string" || typeof currency !== "string") { + throw new Error("Country-currency pairs must be in string format"); + } + + if (country.trim() === "" || currency.trim() === "") { + throw new Error("Country and currency codes cannot be empty"); + } + + if (countryCurrencyObj[country]) { + throw new Error(`Duplicate country code found: ${country}`); + } + + countryCurrencyObj[country] = currency; + } + + return countryCurrencyObj; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..bcc85f149 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,35 +1,112 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); +describe("createLookup", () => { + // Case 1: Returns an empty object if no country-currency pairs are provided + test("returns an empty object if no country-currency pairs are provided", () => { + const countryCurrencyPairs = []; + const currencyObj = createLookup(countryCurrencyPairs); -/* + expect(currencyObj).toEqual({}); + }); -Create a lookup object of key value pairs from an array of code pairs + // Case 2: Returns country currency code lookup for a single country-currency pair + test("creates a country currency code lookup for a single code pair", () => { + const countryCurrencyPairs = [["US", "USD"]]; + const currencyObj = createLookup(countryCurrencyPairs); -Acceptance Criteria: + expect(currencyObj).toEqual({ + US: "USD", + }); + }); -Given - - An array of arrays representing country code and currency code pairs - e.g. [['US', 'USD'], ['CA', 'CAD']] + // Case 3: Returns country currency codes lookup for multiple country-currency pairs + test("creates a country currency code lookup for multiple codes", () => { + const countryCurrencyPairs = [ + ["US", "USD"], + ["CA", "CAD"], + ["GB", "GBP"], + ["ZA", "ZAR"], + ["NG", "NGN"], + ]; -When - - createLookup function is called with the country-currency array as an argument + const inputCurrencyPairObj = createLookup(countryCurrencyPairs); + const outputCurrencyPairObj = { + US: "USD", + CA: "CAD", + GB: "GBP", + ZA: "ZAR", + NG: "NGN", + }; -Then - - It should return an object where: - - The keys are the country codes - - The values are the corresponding currency codes + expect(inputCurrencyPairObj).toEqual(outputCurrencyPairObj); + }); -Example -Given: [['US', 'USD'], ['CA', 'CAD']] + // Case 4: Throws an error if a country-currency pair is not an array + test("throws an error if a country-currency pair is not an array", () => { + const countryCurrencyPairs = [ + ["US", "USD"], + ["CA", "CAD"], + "GB-GBP", + ["ZA", "ZAR"], + ["NG", "NGN"], + ]; -When -createLookup(countryCurrencyPairs) is called + expect(() => createLookup(countryCurrencyPairs)).toThrow( + "Country-currency pairs must be in array format" + ); + }); -Then -It should return: - { - 'US': 'USD', - 'CA': 'CAD' - } -*/ + // Case 5: Throws an error if a country-currency pair is missing a country or currency + test("throws an error if a country-currency pair is missing a country or currency", () => { + const countryCurrencyPairs = [ + ["US", ""], + ["", "CAD"], + [" ", "GBP"], + ]; + + for (const currencyPair of countryCurrencyPairs) { + expect(() => createLookup([currencyPair])).toThrow( + "Country and currency codes cannot be empty" + ); + } + }); + + // Case 6: Throws an error if a country-currency pair contains more than two elements + test("throws an error if a country-currency pair contains more than two elements", () => { + const countryCurrencyPairs = [["GB", "GBP", "ZAR"]]; + + expect(() => createLookup(countryCurrencyPairs)).toThrow( + "Country-currency pairs must contain exactly two elements: country and currency" + ); + }); + + // Case 7: Throws and error if a country-currency pair is duplicated + test("throws an error if a country-currency pair is duplicated", () => { + const countryCurrencyPairs = [ + ["US", "USD"], + ["CA", "CAD"], + ["US", "USD"], + ]; + + expect(() => createLookup(countryCurrencyPairs)).toThrow( + "Duplicate country code found: US" + ); + }); + + // Case 8: Throws an error if non-string values are used as country or currency codes + test("throws an error if non-string values are used as country or currency codes", () => { + const countryCurrencyPairs = [ + [{ name: "United States" }, "USD"], + ["CA", Infinity], + ["GB", 1.21], + [undefined, "ZAR"], + ["NG", null], + ]; + + for (const currencyPair of countryCurrencyPairs) { + expect(() => createLookup([currencyPair])).toThrow( + "Country-currency pairs must be in string format" + ); + } + }); +}); diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..6c5b56ab7 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,12 +1,31 @@ function parseQueryString(queryString) { + if (typeof queryString !== "string") { + throw new Error("Input must be a string"); + } + const queryParams = {}; - if (queryString.length === 0) { + + if (queryString.trim().length === 0) { return queryParams; } const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); + const separatorIndex = pair.indexOf("="); + + if (separatorIndex <= 0 || separatorIndex === pair.length - 1) { + throw new Error( + "String must contain both a key and a value separated by '='" + ); + } + + const key = pair.slice(0, separatorIndex).trim(); + const value = pair.slice(separatorIndex + 1).trim(); + + if (key.length === 0 || value.length === 0) { + throw new Error("Neither the key nor value can be purely whitespace"); + } + queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..1cce8e7e9 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -1,12 +1,155 @@ -// In the prep, we implemented a function to parse query strings. -// Unfortunately, it contains several bugs! -// Below is one test case for an edge case the implementation doesn't handle well. -// Fix the implementation for this test, 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") +describe("parseQueryString", () => { + // Case 1: Returns an empty object when given an empty query string + test("parses an empty querystring", () => { + expect(parseQueryString("")).toEqual({}); + expect(parseQueryString(" ")).toEqual({}); + }); + + // Case 2: String contains one key-value pair and 1 '=' character + test("parses querystring values containing =", () => { + const singlePair = [ + ["name=John", { name: "John" }], + ["age=30", { age: "30" }], + ["city=New York", { city: "New York" }], + ]; + + singlePair.forEach(([input, targetOutput]) => { + expect(parseQueryString(input)).toEqual(targetOutput); + }); + }); + + // Case 3: String contains one key-value pair and multiple '=' character + test("parses querystring values containing multiple =", () => { + expect(parseQueryString("equation=x=y+1")).toEqual({ + equation: "x=y+1", + }); + }); + + // Case 4: String contains multiple key-value pairs separated by '&' with one '=' character each + test("parses multiple key-value pairs", () => { + const multiplePairs = [ + ["name=John&age=30", { name: "John", age: "30" }], + ["city=New York&country=USA", { city: "New York", country: "USA" }], + [ + "key1=value1&key2=value2&key3=value3", + { key1: "value1", key2: "value2", key3: "value3" }, + ], + [ + "Nicaragua=Managua&Honduras=Tegucigalpa&El Salvador=San Salvador&Costa Rica=San José", + { + Nicaragua: "Managua", + Honduras: "Tegucigalpa", + "El Salvador": "San Salvador", + "Costa Rica": "San José", + }, + ], + ]; + + multiplePairs.forEach(([input, targetOutput]) => { + expect(parseQueryString(input)).toEqual(targetOutput); + }); + }); + + // Case 5: String contains multiple key-value pairs separated by '&' with multiple '=' character each + test("parses multiple key-value pairs with multiple =", () => { + const multiplePairsWithEquals = [ + [ + "equation1=x=y+1&equation2=a=b+c", + { + equation1: "x=y+1", + equation2: "a=b+c", + }, + ], + [ + "equation2=a!=b+c&equation3=p=q*r", + { + equation2: "a!=b+c", + equation3: "p=q*r", + }, + ], + [ + "equation3=p!=q*r&equation4=m=n+o&equation5=x=y/1", + { + equation3: "p!=q*r", + equation4: "m=n+o", + equation5: "x=y/1", + }, + ], + ]; + + multiplePairsWithEquals.forEach(([input, targetOutput]) => { + expect(parseQueryString(input)).toEqual(targetOutput); + }); + }); + + // Case 6: Throws an error when given an invalid input like a non-string + test("throws an error when passed a non-string input", () => { + const invalidInputs = [ + [123, -67, 3.14], + { name: "John Locke" }, + { title: "Lost" }, + {}, + null, + undefined, + [ + [ + "equation1=x=y+1&equation2=a=b+c", + { + equation1: "x=y+1", + }, + ], + ], + ]; + + invalidInputs.forEach((input) => { + expect(() => parseQueryString(input)).toThrow("Input must be a string"); + }); + }); + + // Case 7: String contains a space between the key and value + test("parses querystring with spaces between key and value", () => { + const pairsWithSpaces = [ + ["name =John Doe", { name: "John Doe" }], + ["city = New York", { city: "New York" }], + ["country =United States", { country: "United States" }], + ]; + + pairsWithSpaces.forEach(([input, targetOutput]) => { + expect(parseQueryString(input)).toEqual(targetOutput); + }); + }); + + // Case 8: Throws an error when given a string with missing key or value + test("throws an error when passed a string with missing key or value", () => { + const invalidQueryStrings = [ + "=valueOnly", + "keyOnly=", + "=valueOnly&key2=value2", + "key1=value1&=valueOnly", + ]; + + invalidQueryStrings.forEach((input) => { + expect(() => parseQueryString(input)).toThrow( + "String must contain both a key and a value separated by '='" + ); + }); + }); + + // Case 9: Throws an error when given a string with missing key or value and spaces in '=' separator + test("throws an error when passed a string with missing key or value and spaces in '=' separator", () => { + const invalidQueryStringsWithSpaces = [ + " =valueOnly", + "keyOnly= ", + " =valueOnly&key2=value2", + "key1=value1& =valueOnly", + ]; -test("parses querystring values containing =", () => { - expect(parseQueryString("equation=x=y+1")).toEqual({ - "equation": "x=y+1", + invalidQueryStringsWithSpaces.forEach((input) => { + expect(() => parseQueryString(input)).toThrow( + "Neither the key nor value can be purely whitespace" + ); + }); }); }); diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..b825d41be 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,19 @@ -function tally() {} +function tally(array) { + if (!Array.isArray(array)) { + throw new Error("Input must be an array"); + } + + const counts = {}; + + for (const prop of array) { + if (Object.hasOwn(counts, prop)) { + counts[prop] += 1; + } else { + counts[prop] = 1; + } + } + + return counts; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..972786bd3 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -1,34 +1,46 @@ const tally = require("./tally.js"); -/** - * tally array - * - * In this task, you'll need to implement a function called tally - * that will take a list of items and count the frequency of each item - * in an array - * - * For example: - * - * tally(['a']), target output: { a: 1 } - * tally(['a', 'a', 'a']), target output: { a: 3 } - * tally(['a', 'a', 'b', 'c']), target output: { a : 2, b: 1, c: 1 } - */ +describe("tally()", () => { + // Case 1: Returns an empty object when an empty array is passed to tally + test("tally on an empty array returns an empty object", () => { + expect(tally([])).toEqual({}); + }); -// Acceptance criteria: + // Case 2: Returns counts for each unique item when given an array without duplicate items + test("tally on an array without duplicate items returns counts for each unique item", () => { + const tallyWithUniqueItems = [ + [["a"], { a: 1 }], + [["a", "b", "c"], { a: 1, b: 1, c: 1 }], + [[1, 2, 3], { 1: 1, 2: 1, 3: 1 }], + [[true, false], { true: 1, false: 1 }], + [[null, undefined], { null: 1, undefined: 1 }], + ]; -// Given a function called tally -// When passed an array of items -// Then it should return an object containing the count for each unique item + tallyWithUniqueItems.forEach(([inputArray, targetOutput]) => { + expect(tally(inputArray)).toEqual(targetOutput); + }); + }); -// 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"); + // Case 3: Return counts for each unique item when given an array with duplicate items + test("tally on an array with duplicate items returns counts for each unique item", () => { + const tallyWithDuplicateItems = [ + [["a", "a", "b", "c"], { a: 2, b: 1, c: 1 }], + [[1, 2, 2, 3], { 1: 1, 2: 2, 3: 1 }], + [[true, true, false], { true: 2, false: 1 }], + [[null, null, undefined], { null: 2, undefined: 1 }], + ]; -// Given an array with duplicate items -// When passed to tally -// Then it should return counts for each unique item + tallyWithDuplicateItems.forEach(([inputArray, targetOutput]) => { + expect(tally(inputArray)).toEqual(targetOutput); + }); + }); -// Given an invalid input like a string -// When passed to tally -// Then it should throw an error + // Case 4: Throws an error when given an invalid input like a string + test("tally throws an error when passed a non-array input", () => { + const invalidInputs = ["invalid", 123, {}, null, undefined]; + + invalidInputs.forEach((input) => { + expect(() => tally(input)).toThrow("Input must be an array"); + }); + }); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..5bca55a57 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -1,29 +1,35 @@ -// Let's define how invert should work - -// Given an object -// When invert is passed this object -// Then it should swap the keys and values in the object - -// 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; } +console.assert( + JSON.stringify(invert({ a: 1 })) === JSON.stringify({ 1: "a" }), + "Test case 1 failed" +); +console.assert( + JSON.stringify(invert({ a: 1, b: 2 })) === JSON.stringify({ 1: "a", 2: "b" }), + "Test case 2 failed" +); + // 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} +// { key: 2 } // c) What does Object.entries return? Why is it needed in this program? +// Object.entries returns an array of key-value pairs from the object. // d) Explain why the current return value is different from the target output +// It incorrectly assigns 'key' // e) Fix the implementation of invert (and write tests to prove it's fixed!) diff --git a/Sprint-2/package-lock.json b/Sprint-2/package-lock.json index 9b4c725d6..ceda7296e 100644 --- a/Sprint-2/package-lock.json +++ b/Sprint-2/package-lock.json @@ -56,6 +56,7 @@ "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.25.7", @@ -1368,6 +1369,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28",