Skip to content
2 changes: 1 addition & 1 deletion Sprint-2/debug/address.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
25 changes: 23 additions & 2 deletions Sprint-2/debug/author.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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);
}
}
23 changes: 23 additions & 0 deletions Sprint-2/debug/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")}`);
10 changes: 9 additions & 1 deletion Sprint-2/implement/contains.js
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 20 additions & 1 deletion Sprint-2/implement/contains.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ 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");

// Given an object with properties
// When passed to contains with an existing property name
Expand All @@ -33,3 +32,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);
});
});
4 changes: 2 additions & 2 deletions Sprint-2/implement/lookup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
function createLookup() {
// implementation here
function createLookup(countryCurrencyPairs) {
return Object.fromEntries(countryCurrencyPairs);
}

module.exports = createLookup;
19 changes: 17 additions & 2 deletions Sprint-2/implement/lookup.test.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -33,3 +31,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);
});
54 changes: 46 additions & 8 deletions Sprint-2/implement/querystring.js
Original file line number Diff line number Diff line change
@@ -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;
27 changes: 27 additions & 0 deletions Sprint-2/implement/querystring.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
});
});
16 changes: 15 additions & 1 deletion Sprint-2/implement/tally.js
Original file line number Diff line number Diff line change
@@ -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;
17 changes: 16 additions & 1 deletion Sprint-2/implement/tally.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ 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");

// Given an array with duplicate items
// When passed to tally
Expand All @@ -32,3 +31,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);
});
});
47 changes: 47 additions & 0 deletions Sprint-2/interpret/invert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


32 changes: 32 additions & 0 deletions Sprint-2/stretch/count-words.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Loading