diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..36d2f865d 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..65577aef5 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -3,6 +3,26 @@ // 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 = { + //firstName: "Zadie", + //lastName: "Smith", + //occupation: "writer", + //age: 40, + //alive: true, +//}; + +//for (const value of author) { + //console.log(value); +//} + +// If you run this snippet, JavaScript will throw a TypeError: author is not iterable. +// The for...of loop is designed specifically for iterable objects (like Arrays, Strings, Sets, or Maps) +// Plain JavaScript objects ({}) are not iterable by default with for...of. +// They are collection of key-value pairs without a guaranteed built-in iteration order sequence. + +// to fix this, i wii specifically want to stick with for...of to loop directly through the property values: +// i will use Object.values() with for...of + const author = { firstName: "Zadie", lastName: "Smith", @@ -11,6 +31,7 @@ const author = { alive: true, }; -for (const value of author) { +// Object.values(author) returns an array: ["Zadie", "Smith", "writer", 40, true] +for (const value of Object.values(author)) { console.log(value); -} +} \ No newline at end of file diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..cacb16acd 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -13,3 +13,26 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: ${recipe}`); + +// Predict and explain +// Two things are going wrong here: + +//Printing the whole object instead of individual pieces: At the very end, +// ${recipe} tries to drop the entire recipe object into the text string. +// When JavaScript tries to turn a plain object into text like that, it defaults to printing [object Object]. +//Ingredients are not separated: The code doesn't include a loop or a tool to extract the ingredients array and put each item on its own new line. + +// we can fix this code by argeting the specific properties of the object (recipe.title, recipe.serves) and using the .join("\n") method on the ingredients array. +// The .join("\n") method takes a list and glues the items together using a newline character (\n), which forces each ingredient onto its own line. + +// so here is the correct code to fix it +const recipe = { + title: "bruschetta", + serves: 2, + ingredients: ["olive oil", "tomatoes", "salt", "pepper"], +}; + +// Use .join("\n") to separate each ingredient with a new line +console.log(`${recipe.title} serves ${recipe.serves} +ingredients: +${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..897ba78bf 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,11 @@ -function contains() {} +function contains(obj, prop) { + // Guard clause: Ensure obj is a valid, non-null, non-array object + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + return false; + } + + // Check if the object directly holds the specified key + return Object.hasOwn(obj, prop); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..673adea77 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -33,3 +33,23 @@ test.todo("contains on empty object returns false"); // Given invalid parameters like an array // When passed to contains // Then it should return false or throw an error + +describe("contains", () => { + test("returns false for an empty object", () => { + expect(contains({}, "a")).toBe(false); + }); + + test("returns true for an existing property name", () => { + expect(contains({ a: 1, b: 2 }, "a")).toBe(true); + }); + + test("returns false for a non-existent property name", () => { + expect(contains({ a: 1, b: 2 }, "c")).toBe(false); + }); + + test("returns false when given invalid parameters (e.g. array, null, primitive)", () => { + expect(contains([1, 2, 3], "0")).toBe(false); + expect(contains(null, "a")).toBe(false); + expect(contains("not an object", "a")).toBe(false); + }); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..5c120898b 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,5 @@ -function createLookup() { - // implementation here +function createLookup(countryCurrencyPairs) { + return Object.fromEntries(countryCurrencyPairs); } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..f237c2211 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -33,3 +33,20 @@ It should return: 'CA': 'CAD' } */ + + +test("creates a country currency code lookup for multiple codes", () => { + const input = [ + ["US", "USD"], + ["CA", "CAD"], + ["GB", "GBP"], + ]; + + const expectedOutput = { + US: "USD", + CA: "CAD", + GB: "GBP", + }; + + expect(createLookup(input)).toEqual(expectedOutput); +}); \ No newline at end of file diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..94c35f045 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,16 +1,54 @@ function parseQueryString(queryString) { - const queryParams = {}; - if (queryString.length === 0) { - return queryParams; + const result = {}; + + if (!queryString) return result; + if (queryString.startsWith("?")) { + queryString = queryString.slice(1); } - const keyValuePairs = queryString.split("&"); - for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; + const decode = (str) => { + try { + return decodeURIComponent(str.replace(/\+/g, " ")); + } catch { + return str; + } + }; + + const pairs = queryString.split("&"); + + for (const pair of pairs) { + if (!pair) continue; + + const equalIndex = pair.indexOf("="); + let rawKey, rawValue; + + if (equalIndex === -1) { + rawKey = pair; + rawValue = ""; + } else { + rawKey = pair.slice(0, equalIndex); + rawValue = pair.slice(equalIndex + 1); + } + + const key = decode(rawKey); + const value = decode(rawValue); + + // Safeguard against prototype pollution while setting keys safely + if (!Object.hasOwn(result, key)) { + Object.defineProperty(result, key, { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + } else if (Array.isArray(result[key])) { + result[key].push(value); + } else { + result[key] = [result[key], value]; + } } - return queryParams; + return result; } module.exports = parseQueryString; diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 328b8df61..a36379141 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -46,3 +46,30 @@ test("should store values of a key in an array when the key has 2 or more values foo: "bar", }); }); + + +describe("parseQueryString additional edge cases", () => { + test("should strip leading '?' if present", () => { + expect(parseQueryString("?foo=bar&baz=qux")).toEqual({ + foo: "bar", + baz: "qux", + }); + }); + + test("should handle empty or missing query string safely", () => { + expect(parseQueryString("")).toEqual({}); + expect(parseQueryString(null)).toEqual({}); + }); + + test("should prevent prototype pollution from '__proto__' keys", () => { + const result = parseQueryString("__proto__=polluted"); + expect(Object.prototype.polluted).toBeUndefined(); + expect(result["__proto__"]).toBe("polluted"); + }); + + test("should gracefully fallback on malformed percent encoding", () => { + expect(parseQueryString("key=%E0%A4")).toEqual({ + key: "%E0%A4", + }); + }); +}); \ No newline at end of file diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..fdc123fea 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,17 @@ -function tally() {} +function tally(items) { + // Guard clause: Ensure input is strictly an array + if (!Array.isArray(items)) { + throw new TypeError("Input must be an array"); + } + + const counts = {}; + + for (const item of items) { + // Safely increment count or initialize to 1 + 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..75eed7fb0 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -32,3 +32,19 @@ 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("tally on an empty array returns an empty object", () => { + expect(tally([])).toEqual({}); + }); + + test("returns counts for unique items", () => { + expect(tally(["a", "a", "b", "c"])).toEqual({ a: 2, b: 1, c: 1 }); + }); + + test("throws an error when passed invalid input like a string", () => { + expect(() => tally("not an array")).toThrow(TypeError); + expect(() => tally(123)).toThrow(TypeError); + expect(() => tally(null)).toThrow(TypeError); + }); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..e9fbe34a3 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -27,3 +27,50 @@ function invert(obj) { // 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!) + +// this are the answers: + +// a) Return value for { a: 1 } +// { key: 1 } + +// b) Return value for { a: 1, b: 2 } +// { key: 2 } + +// c1) Target return value for { a: 1, b: 2 } +// { "1": "a", "2": "b" } + +// c2) What does Object.entries return? Why is it needed? +// What it returns: An array of key-value pairs (as two-element arrays), e.g., [["a", 1], ["b", 2]]. +// Why it’s needed: It lets us easily loop through both keys and values at the same time using for (const [key, value] of Object.entries(obj)). + +// d) Why the current return value is different from target output +// The line invertedObj.key = value; uses dot notation, which treats "key" as a literal property name on the object instead of using the variable key. + +// Furthermore, to invert the object, the value needs to become the new key and the old key needs to become the new value. + +// Because dot notation was used: + +// Every loop iteration overwrote invertedObj["key"] with the variable value. + +// The key/value assignment was backwards. + +// e) Fixed Implementation & Tests +// Fixed invert.js +// Use bracket notation [value] to dynamically set the property name: + +function invert(obj) { + // Guard clause for safety + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + return {}; + } + + const invertedObj = {}; + + for (const [key, value] of Object.entries(obj)) { + invertedObj[value] = key; + } + + return invertedObj; +} + + diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..ed4f6fd87 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -26,3 +26,35 @@ 3. Order the results to find out which word is the most common in the input */ + +function countWordsAdvanced(str) { + if (!str.trim()) return {}; + + // 1 & 2: Convert to lowercase and strip punctuation using Regex + const cleanedStr = str + .toLowerCase() + .replace(/[^\w\s]|_/g, ""); // Removes punctuation like .,!?- etc. + + // Match words, splitting on whitespace + const words = cleanedStr.match(/\b\w+\b/g); + if (!words) return {}; + + const wordCounts = {}; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + wordCounts[word] = (wordCounts[word] || 0) + 1; + } + + // 3: Sort the result by frequency (most common first) + const sortedEntries = Object.entries(wordCounts).sort( + (a, b) => b[1] - a[1] + ); + + // Convert back into an object (maintains insertion order in modern JavaScript) + return Object.fromEntries(sortedEntries); +} + +// Example usage: +console.log(countWordsAdvanced("You and me! And you, and ME...")); +// Output: { and: 3, you: 2, me: 2 } \ No newline at end of file