diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..6ad482e39 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,5 +1,15 @@ // Predict and explain first... +/* +prediction: My house number is undefined +Reason: the code is trying to access the key 0 of the address object, which +does not exist. + +Object properties aren't accessible by index. Author likely tried to treat it +the same as an array. Arrays are a special type of object which allows indexing into (some) +of its values. +*/ + // This code should log out the houseNumber from the address object // but it isn't working... // Fix anything that isn't working @@ -12,4 +22,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..509be84f2 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,4 +1,11 @@ // Predict and explain first... +/* +Prediction: TypeError: author is not iterable + +Cause: Plain Js objects aren't iterable the way an array's elements are. +To iterate over keys and/or values use the Object.keys(), Object.values(), +or Object.entries() methods to covert the data into an array 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 @@ -11,6 +18,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..894f7f472 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -1,5 +1,13 @@ // Predict and explain first... +/* +Prediction: the ingredients: ${recipe} part will only print something like [Object object] + +Cause: The code is attempting to print the recipe object itself, rather than the ingredients. +In Js, when printing objects, it doesn't print a string of key, pair values of its +properties. Should be printing the ingredients property, which is an array, which does print out its values. +*/ + // 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? @@ -12,4 +20,4 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients}`); diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..22480ed6a 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,12 @@ -function contains() {} +function isObject(item) { + return typeof item === "object" && item !== null && !Array.isArray(item); +} + +function contains(object, key) { + if (isObject(object)) { + return Object.keys(object).includes(key); + } + throw new TypeError("Item is not plain object, Date, or Map"); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..c994dfd0a 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("given an empty object, should return false", () => { + expect(contains({}, "a")).toEqual(false); +}); // Given an object with properties // When passed to contains with an existing property name // Then it should return true +test("given an object with a property, and property name that exists, should return true", () => { + expect(contains({ a: 12, b: 2 }, "a")).toEqual(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, and property that does not exist in object, should return false", () => { + expect(contains({ a: 12, b: 2 }, "c")).toEqual(false); +}); // Given invalid parameters like an array // When passed to contains // Then it should return false or throw an error +test("given invalid parameter, like an array, should return false or throw error", () => { + expect(() => contains(22, "a")).toThrow(TypeError); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..43d15d403 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,25 @@ -function createLookup() { - // implementation here +// check if arry is 2d, and each element is a k,v pair and k,v both strings +function is2dKeyValStringArray(arr) { + return ( + Array.isArray(arr) && + arr.every((element) => { + return ( + Array.isArray(element) && + element.length === 2 && + typeof element[0] === "string" && + typeof element[1] === "string" + ); + }) + ); +} + +function createLookup(countryAndCurrency) { + // bit lazy, should have tests for type, and content seperately, + // to throw more specific errors + if (!is2dKeyValStringArray(countryAndCurrency)) { + throw new Error("Input is not valid type and/or format"); + } + return Object.fromEntries(countryAndCurrency); } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..f6e2722d9 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,7 +1,5 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); - /* Create a lookup object of key value pairs from an array of code pairs @@ -33,3 +31,57 @@ It should return: 'CA': 'CAD' } */ + +test("converts 2d array to object", () => { + expect( + createLookup([ + ["a", "x"], + ["b", "y"], + ["c", "z"], + ]) + ).toEqual({ a: "x", b: "y", c: "z" }); +}); + +test("convert empty array, to empty object", () => { + expect(createLookup([])).toEqual({}); +}); + +test("convert single 2d array, to object with one key", () => { + expect(createLookup([["a", "wow"]])).toEqual({ a: "wow" }); +}); + +// this is the behaviour of Object.fromEntries(), I keep it for consistency +test("when there are duplicate keys, later entries overwrite earlier entries", () => { + expect( + createLookup([ + ["a", "ff"], + ["b", "gg"], + ["a", "hh"], + ]) + ).toEqual({ b: "gg", a: "hh" }); +}); + +test("when input is not an array throw error", () => { + expect(() => createLookup("asdf")).toThrow(); + expect(() => createLookup(1)).toThrow(); + expect(() => createLookup({ a: 12 })).toThrow(); +}); + +test("when input is not a 2d array throw error", () => { + expect(() => createLookup(["a", "b"])).toThrow(); + expect(() => createLookup([["a", "b"]])).not.toThrow(); +}); + +test("when length of internal array not 1 < arr.length < 3", () => { + expect(() => createLookup(["a"])).toThrow(); + expect(() => createLookup(["a", "b", "c"])).toThrow(); +}); + +test("when non-string k, v pairs throw error", () => { + expect(() => + createLookup([ + [1, 2], + [3, 4], + ]) + ).toThrow(); +}); diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..bb42c66e5 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,13 +1,45 @@ function parseQueryString(queryString) { const queryParams = {}; - if (queryString.length === 0) { - return queryParams; - } + if (!queryString || typeof queryString !== "string") return queryParams; + + if (queryString.trim() === "") return queryParams; + const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; + const firstEqualsIndex = pair.indexOf("="); + let key, value; + + // Checks for pairs with key but no value like debug&verbose&name=Alice + // by keeping them, prevent loss of information + if (firstEqualsIndex === -1) { + key = pair.trim(); + value = ""; + } else { + // splits on first equal, but not any of the rest + key = pair.slice(0, firstEqualsIndex).trim(); + value = pair.slice(firstEqualsIndex + 1).trim(); + } + + // skips next part if the ends up being an empty str after slice and trim + if (!key) continue; + + /* use hasOwnProperty to prevent conflict with built-in/inherited properties + like "toString", if the query string has a "toString=value". + + if a key already exists on queryParams, but is not an array, then create an array + and store the the old value and new value in the array + */ + if (Object.prototype.hasOwnProperty.call(queryParams, key)) { + if (queryParams[key] === value || queryParams[key].includes(value)) + continue; + if (!Array.isArray(queryParams[key])) { + 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 3e218b789..d4c1ab4aa 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -1,12 +1,83 @@ // 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. +// 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"); +// empty string +test("empty query string returns {}", () => { + expect(parseQueryString("")).toEqual({}); +}); + +// null or undefined +test("empty query string returns {}", () => { + expect(parseQueryString(null)).toEqual({}); + expect(parseQueryString(undefined)).toEqual({}); + expect(parseQueryString()).toEqual({}); +}); + +// happy path +// single valid kv pair +test("a single k, v with no special chars return object with matching k, v pair", () => { + expect(parseQueryString("mykey=myvalue")).toEqual({ mykey: "myvalue" }); +}); +// mutiple valid kv pairs +test("multiple k, v pairs with no special chars return object with matching k, v pairs", () => { + expect(parseQueryString("mykey=myvalue&otherkey=othervalue&third=3")).toEqual( + { + mykey: "myvalue", + otherkey: "othervalue", + third: "3", + } + ); +}); + +// special characters in key, can't handle = in key, as that is fundamentally unparsable +test("special character in key without URL encoding", () => { + expect(parseQueryString("my+key=myvalue")).toEqual({ + "my+key": "myvalue", + }); +}); +// special characters in value test("parses querystring values containing =", () => { expect(parseQueryString("equation=x=y+1")).toEqual({ - "equation": "x=y+1", + equation: "x=y+1", + }); +}); + +// missing values +test("key with no value return object with the key and value of empty string", () => { + expect(parseQueryString("foo=")).toEqual({ foo: "" }); +}); + +//valueless flags +test("when given valueless keys, keep the key with empty string as value", () => { + expect(parseQueryString("debug&verbose")).toEqual({ debug: "", verbose: "" }); +}); + +// ignore keyless values +test("ignores pairs with no key", () => { + expect(parseQueryString("=value")).toEqual({}); +}); + +// duplicate keys with different values (array) +test("collects duplicate keys into an array", () => { + expect(parseQueryString("color=red&color=blue")).toEqual({ + color: ["red", "blue"], + }); +}); + +// duplicate k,v pair +test("if k,v already exists, or value array for a key already as value, ignore", () => { + expect(parseQueryString("color=red&color=red")).toEqual({ color: "red" }); +}); + +// white spaces +test("trims whitespace from keys and values", () => { + expect(parseQueryString("name= Alice &age= 30 ")).toEqual({ + name: "Alice", + age: "30", }); }); diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..94c934ef4 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,14 @@ -function tally() {} +function tally(list) { + if (!Array.isArray(list)) { + throw new TypeError("Invalid input: not an array"); + } + return (itemCountMapping = list.reduce((acc, curr) => { + if (typeof curr !== "string" && curr !== "number") { + throw new Error(`Invalid element: ${curr} must be a string or number`); + } + acc[curr] = (acc[curr] ?? 0) + 1; + return acc; + }, {})); +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..ea8652e24 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("given empty array, returns 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("given array with duplicate elements, returns correct count of each item", () => { + expect(tally(["a", "a", "b"])).toEqual({ a: 2, b: 1 }); +}); // Given an invalid input like a string // When passed to tally // Then it should throw an error +test("given invalid input like a string, throw error", () => { + expect(() => tally("asdf")).toThrow(TypeError); +}); + +test("elements of the list must be string or number, or throw error", () => { + expect(() => tally([{ a: 12 }, { b: "c" }, { a: 12 }])).toThrow(); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..378d88567 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -7,23 +7,63 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} function invert(obj) { + // need obj == null because 'type of obj === null' does not work in Js + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + throw new TypeError("Input must be a plain object"); + } + const invertedObj = {}; for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; + // if a value is not a number or string, it can't be used as a key in the inverted version + if (typeof value !== "string" && typeof value !== "number") { + throw new TypeError(`Value ${JSON.stringify(value)} is not a valid key`); + } + invertedObj[String(value)] = String(key); } return invertedObj; } +module.exports = invert; // a) What is the current return value when invert is called with { a : 1 } +/* +It would return {key: 1}. The dot nation means that it uses the string "key" as the key. +and the value for that key is the value from the object being iterated over. +*/ // b) What is the current return value when invert is called with { a: 1, b: 2 } +/* +It would return {key : 2}. First it would do as in a, and the overwrite the value of the key "key" +from 1 to 2. +*/ // c) What is the target return value when invert is called with {a : 1, b: 2} +/* +The target is {"1": "a", "2": "b"} +*/ // c) What does Object.entries return? Why is it needed in this program? +/* +It returns a 2d array containing arrays of key value pairs from the object of the form: +[[key1, value1], [key2, value2], [key3, value3]]. + +It is needed because can't iterate over objects the same way as arrays. Objects are a bit more +complicated, because you have to decide if you want to iterate over the keys, values, or both. +you also have to decide if you want to include inherited properties. +Object.entries() makes this assumption for you, by including only the object's own properties. +It also just makes this particular task easier by accessing the key and value together. +If you wanted to include inherited properties you'd use (for ... in) loop. + +*/ // d) Explain why the current return value is different from the target output +/* +The dot nation means that it uses the string "key" as the key. +and the value for that key is the value from the object being iterated over. +So it just continuously overwrites that one "key" property of the to-be-returned object. + +It's also swapping key, value from the old object. +*/ // e) Fix the implementation of invert (and write tests to prove it's fixed!) diff --git a/Sprint-2/interpret/invert.test.js b/Sprint-2/interpret/invert.test.js new file mode 100644 index 000000000..34673bb2f --- /dev/null +++ b/Sprint-2/interpret/invert.test.js @@ -0,0 +1,47 @@ +const invert = require("./invert.js"); + +test("Inverts simple object with string key and string value", () => { + expect(invert({ a: "x", b: "y", c: "z" })).toEqual({ + x: "a", + y: "b", + z: "c", + }); +}); + +test("Returns empty object if input is an empty object", () => { + expect(invert({})).toEqual({}); +}); + +test("Throws error for non-object inputs", () => { + expect(() => invert("asdf")).toThrow(TypeError); + expect(() => invert([1, "a", 2, "b"])).toThrow(TypeError); + expect(() => invert()).toThrow(); + expect(() => invert(null)).toThrow(); +}); + +test("Throws errors for non-string, non-numeric keys or values", () => { + expect(() => invert({ a: { b: 12 } })).toThrow(TypeError); +}); + +test("Duplicate values become one key, where the last key in original becomes its new value", () => { + expect(invert({ a: "x", b: "x" })).toEqual({ x: "b" }); +}); + +test("numeric keys become string converted to value", () => { + expect(invert({ 1: "a", 2: "b" })).toEqual({ a: "1", b: "2" }); +}); + +test("invert twice returns original if all values are unique", () => { + expect(invert(invert({ a: "x", b: "y", x: "z" }))).toEqual({ + a: "x", + b: "y", + x: "z", + }); +}); + +test("invert twice does not returns original if all values are not unique", () => { + expect(invert(invert({ a: "x", b: "y", c: "y" }))).toEqual({ + a: "x", + c: "y", + }); +}); 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",