Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
20 changes: 20 additions & 0 deletions Sprint-2/implement/contains.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
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;
17 changes: 17 additions & 0 deletions Sprint-2/implement/lookup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
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;
16 changes: 16 additions & 0 deletions Sprint-2/implement/tally.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
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