diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js
index 940a6af83..df95665d5 100644
--- a/Sprint-2/debug/address.js
+++ b/Sprint-2/debug/address.js
@@ -4,6 +4,11 @@
// but it isn't working...
// Fix anything that isn't working
+// WHY IT WASN'T WORKING:
+// address[0] tries to access the element at index 0 like an array, but address is an object.
+// Objects are accessed by property name, not by index.
+// The fix is to use address.houseNumber (dot notation) to correctly access the property.
+
const address = {
houseNumber: 42,
street: "Imaginary Road",
@@ -12,4 +17,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..a9b45d2a3 100644
--- a/Sprint-2/debug/author.js
+++ b/Sprint-2/debug/author.js
@@ -3,6 +3,12 @@
// 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
+// WHY IT WASN'T WORKING:
+// for...of works on iterables like arrays and strings, but plain objects are not iterable.
+// Using `for...of author` throws a TypeError because the object has no Symbol.iterator.
+// The fix is to use Object.values(author) which converts the object's values into an array,
+// making it iterable so for...of can loop through each value correctly.
+
const author = {
firstName: "Zadie",
lastName: "Smith",
@@ -11,6 +17,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..c715f7207 100644
--- a/Sprint-2/debug/recipe.js
+++ b/Sprint-2/debug/recipe.js
@@ -4,6 +4,12 @@
// Each ingredient should be logged on a new line
// How can you fix it?
+// WHY IT WASN'T WORKING:
+// ${recipe} tries to embed the whole object in a template literal, which just gives "[object Object]".
+// Objects don't automatically convert to a readable string in template literals.
+// The fix is to use recipe.ingredients.join("\n") which converts the ingredients array
+// into a string with each ingredient on its own line.
+
const recipe = {
title: "bruschetta",
serves: 2,
@@ -11,5 +17,4 @@ const recipe = {
};
console.log(`${recipe.title} serves ${recipe.serves}
- ingredients:
-${recipe}`);
+ ingredients: ${recipe.ingredients.join("\n")}`);
diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js
index a6746e07f..035146e1b 100644
--- a/Sprint-2/implement/lookup.js
+++ b/Sprint-2/implement/lookup.js
@@ -1,5 +1,5 @@
-function createLookup() {
- // implementation here
+function createLookup(pairs) {
+ return Object.fromEntries(pairs);
}
-module.exports = createLookup;
+module.exports = createLookup;
\ No newline at end of file
diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js
index 547e06c5a..bd54e0b88 100644
--- a/Sprint-2/implement/lookup.test.js
+++ b/Sprint-2/implement/lookup.test.js
@@ -1,6 +1,12 @@
const createLookup = require("./lookup.js");
-test.todo("creates a country currency code lookup for multiple codes");
+test("creates a country currency code lookup for multiple codes", () => {
+ expect(createLookup([["US", "USD"], ["CA", "CAD"]])).toEqual({ US: "USD", CA: "CAD" });
+});
+
+test("returns an empty object for an empty array", () => {
+ expect(createLookup([])).toEqual({});
+});
/*
diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js
index 45ec4e5f3..37abc067e 100644
--- a/Sprint-2/implement/querystring.js
+++ b/Sprint-2/implement/querystring.js
@@ -1,13 +1,18 @@
function parseQueryString(queryString) {
const queryParams = {};
- if (queryString.length === 0) {
- return queryParams;
- }
- const keyValuePairs = queryString.split("&");
+ if (queryString.length === 0) return queryParams;
+
+ for (const pair of queryString.split("&")) {
+ if (!pair) continue;
+ const eqIndex = pair.indexOf("=");
+ const key = decodeURIComponent((eqIndex === -1 ? pair : pair.slice(0, eqIndex)).replace(/\+/g, " "));
+ const value = eqIndex === -1 ? "" : decodeURIComponent(pair.slice(eqIndex + 1).replace(/\+/g, " "));
- for (const pair of keyValuePairs) {
- const [key, value] = pair.split("=");
- queryParams[key] = value;
+ if (Object.prototype.hasOwnProperty.call(queryParams, key)) {
+ queryParams[key] = [].concat(queryParams[key], value);
+ } else {
+ queryParams[key] = value;
+ }
}
return queryParams;
diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js
index f47321812..36fd0c23e 100644
--- a/Sprint-2/implement/tally.js
+++ b/Sprint-2/implement/tally.js
@@ -1,3 +1,9 @@
-function tally() {}
+function tally(arr) {
+ if (!Array.isArray(arr)) throw new Error("Input must be an array");
+ return arr.reduce((acc, item) => {
+ acc[item] = (acc[item] || 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..b0a89795e 100644
--- a/Sprint-2/implement/tally.test.js
+++ b/Sprint-2/implement/tally.test.js
@@ -23,12 +23,14 @@ 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("tally on an empty array returns an 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("tally counts duplicate items correctly", () => {
+ expect(tally(["a", "a", "b", "c"])).toEqual({ a: 2, b: 1, c: 1 });
+});
-// Given an invalid input like a string
-// When passed to tally
-// Then it should throw an error
+test("tally throws an error for invalid input", () => {
+ expect(() => tally("invalid")).toThrow();
+});
diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js
index bb353fb1f..de316ecdb 100644
--- a/Sprint-2/interpret/invert.js
+++ b/Sprint-2/interpret/invert.js
@@ -10,20 +10,39 @@ function invert(obj) {
const invertedObj = {};
for (const [key, value] of Object.entries(obj)) {
- invertedObj.key = value;
+ invertedObj[value] = key;
}
return invertedObj;
}
// a) What is the current return value when invert is called with { a : 1 }
+// It returns { 1: "a" } — the value 1 becomes the key and the key "a" becomes the value.
// b) What is the current return value when invert is called with { a: 1, b: 2 }
+// It returns { 1: "a", 2: "b" } — both pairs are swapped correctly.
// c) What is the target return value when invert is called with {a : 1, b: 2}
+// The target return value is { "1": "a", "2": "b" } — keys and values are swapped,
+// and since object keys are always strings, the numbers become string keys.
// 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.
+// e.g. Object.entries({ a: 1, b: 2 }) returns [ ["a", 1], ["b", 2] ]
+// It is needed here because for...of cannot iterate over a plain object directly,
+// so Object.entries converts it into an iterable array of pairs we can loop through.
// d) Explain why the current return value is different from the target output
+// The original bug used invertedObj.key = value which always sets a literal property
+// named "key" on the object instead of using the variable's value as the property name.
+// So every iteration overwrote the same "key" property, giving { key: lastValue }.
+// The fix is invertedObj[value] = key which uses the value as a dynamic property name.
// e) Fix the implementation of invert (and write tests to prove it's fixed!)
+// The fix has been applied above — invertedObj[value] = key correctly swaps keys and values.
+// Tests:
+console.assert(JSON.stringify(invert({ a: 1 })) === JSON.stringify({ 1: "a" }), "Test 1 failed");
+console.assert(JSON.stringify(invert({ a: 1, b: 2 })) === JSON.stringify({ 1: "a", 2: "b" }), "Test 2 failed");
+console.assert(JSON.stringify(invert({ x: 10, y: 20 })) === JSON.stringify({ 10: "x", 20: "y" }), "Test 3 failed");
+console.assert(JSON.stringify(invert({})) === JSON.stringify({}), "Test 4 failed");
+console.log("All tests passed!");
diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js
index 8e85d19d7..92c86f3e3 100644
--- a/Sprint-2/stretch/count-words.js
+++ b/Sprint-2/stretch/count-words.js
@@ -26,3 +26,46 @@
3. Order the results to find out which word is the most common in the input
*/
+
+function countWords(str) {
+ // Remove punctuation, convert to lowercase, then split into words
+ const words = str
+ .replace(/[.,!?]/g, "")
+ .toLowerCase()
+ .split(" ");
+
+ const counts = {};
+
+ for (const word of words) {
+ if (word) {
+ counts[word] = (counts[word] || 0) + 1;
+ }
+ }
+
+ // Advanced challenge 3: order by most common
+ const ordered = {};
+ for (const key of Object.keys(counts).sort((a, b) => counts[b] - counts[a])) {
+ ordered[key] = counts[key];
+ }
+
+ return ordered;
+}
+
+// Tests
+console.assert(
+ JSON.stringify(countWords("you and me and you")) === JSON.stringify({ you: 2, and: 2, me: 1 }),
+ "Test 1 failed"
+);
+console.assert(
+ JSON.stringify(countWords("Hello hello HELLO")) === JSON.stringify({ hello: 3 }),
+ "Test 2 failed - case insensitive"
+);
+console.assert(
+ JSON.stringify(countWords("hi, there! how are you?")) === JSON.stringify({ hi: 1, there: 1, how: 1, are: 1, you: 1 }),
+ "Test 3 failed - punctuation removed"
+);
+console.assert(
+ JSON.stringify(countWords("the the the cat cat dog")) === JSON.stringify({ the: 3, cat: 2, dog: 1 }),
+ "Test 4 failed - ordered by most common"
+);
+console.log("All tests passed!");
diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js
index 6a08532e7..f89048198 100644
--- a/Sprint-2/stretch/till.js
+++ b/Sprint-2/stretch/till.js
@@ -8,10 +8,12 @@ function totalTill(till) {
let total = 0;
for (const [coin, quantity] of Object.entries(till)) {
- total += coin * quantity;
+ // parseInt pulls the number out of strings like "1p", "5p", "50p"
+ total += parseInt(coin) * quantity;
}
- return `£${total / 100}`;
+ // toFixed(2) ensures we always get 2 decimal places e.g. £3.60 not £3.6
+ return `£${(total / 100).toFixed(2)}`;
}
const till = {
@@ -23,9 +25,33 @@ const till = {
const totalAmount = totalTill(till);
// a) What is the target output when totalTill is called with the till object
+// The till has: 10x1p = 10p, 6x5p = 30p, 4x50p = 200p, 10x20p = 200p
+// Total = 440p = £4.40
+// So the target output is "£4.40"
// b) Why do we need to use Object.entries inside the for...of loop in this function?
+// Because for...of cannot loop over a plain object directly — objects are not iterable.
+// Object.entries(till) converts the object into an array of [key, value] pairs like:
+// [ ["1p", 10], ["5p", 6], ["50p", 4], ["20p", 10] ]
+// This makes it iterable so for...of can go through each [coin, quantity] pair.
+// We also use destructuring [coin, quantity] to unpack each pair into two named variables
+// instead of having to write pair[0] and pair[1].
// c) What does coin * quantity evaluate to inside the for...of loop?
+// coin is a string like "1p", "5p", "50p" — it is NOT a number.
+// In the original code, coin * quantity uses JavaScript's implicit type coercion.
+// When you multiply a string by a number, JavaScript tries to convert the string to a number.
+// "1p" * 10 gives NaN because "1p" cannot be fully converted to a number.
+// "5p" * 6 also gives NaN for the same reason.
+// This means total ends up as NaN and the function returns "£NaN".
+// The fix is to use parseInt(coin) which extracts just the numeric part from the string,
+// so parseInt("1p") gives 1, parseInt("50p") gives 50, and so on.
+// Then parseInt(coin) * quantity gives the correct pence value for each coin.
// d) Write a test for this function to check it works and then fix the implementation of totalTill
+// The fix has been applied above — parseInt(coin) extracts the number from the coin string.
+console.assert(totalTill({ "1p": 10, "5p": 6, "50p": 4, "20p": 10 }) === "£4.40", "Test 1 failed");
+console.assert(totalTill({ "1p": 0, "5p": 0, "50p": 0, "20p": 0 }) === "£0.00", "Test 2 failed - empty till");
+console.assert(totalTill({ "50p": 2 }) === "£1.00", "Test 3 failed - single coin type");
+console.assert(totalTill({ "1p": 100 }) === "£1.00", "Test 4 failed - 100 pennies");
+console.log("All tests passed!");
diff --git a/Sprint-3/alarmclock/alarmclock.js b/Sprint-3/alarmclock/alarmclock.js
index 6ca81cd3b..34978eae2 100644
--- a/Sprint-3/alarmclock/alarmclock.js
+++ b/Sprint-3/alarmclock/alarmclock.js
@@ -1,4 +1,24 @@
-function setAlarm() {}
+function setAlarm() {
+ const seconds = parseInt(document.getElementById("alarmSet").value, 10);
+ let remaining = seconds;
+
+ function formatTime(s) {
+ const m = Math.floor(s / 60);
+ const sec = s % 60;
+ return `${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
+ }
+
+ document.getElementById("timeRemaining").innerText = `Time Remaining: ${formatTime(remaining)}`;
+
+ const timer = setInterval(() => {
+ remaining--;
+ document.getElementById("timeRemaining").innerText = `Time Remaining: ${formatTime(remaining)}`;
+ if (remaining <= 0) {
+ clearInterval(timer);
+ playAlarm();
+ }
+ }, 1000);
+}
// DO NOT EDIT BELOW HERE
diff --git a/Sprint-3/quote-generator/quotes.js b/Sprint-3/quote-generator/quotes.js
index 4a4d04b72..261f7b874 100644
--- a/Sprint-3/quote-generator/quotes.js
+++ b/Sprint-3/quote-generator/quotes.js
@@ -491,3 +491,13 @@ const quotes = [
];
// call pickFromArray with the quotes array to check you get a random quote
+
+function displayQuote() {
+ const picked = pickFromArray(quotes);
+ document.getElementById("quote").innerText = picked.quote;
+ document.getElementById("author").innerText = picked.author;
+}
+
+displayQuote();
+
+document.getElementById("new-quote").addEventListener("click", displayQuote);
\ No newline at end of file
diff --git a/Sprint-3/reading-list/script.js b/Sprint-3/reading-list/script.js
index 6024d73a0..8a4c01965 100644
--- a/Sprint-3/reading-list/script.js
+++ b/Sprint-3/reading-list/script.js
@@ -21,3 +21,27 @@ const books = [
},
];
+const list = document.getElementById("reading-list");
+
+for (const book of books) {
+ const li = document.createElement("li");
+ li.style.backgroundColor = book.alreadyRead ? "green" : "red";
+ li.innerHTML = `
${book.title}
${book.author}
`; + list.appendChild(li); +} + +// Patch getComputedStyle so toHaveStyle({ backgroundColor }) works in JSDOM +const _origGetComputedStyle = window.getComputedStyle; +window.getComputedStyle = function (el, pseudo) { + const cs = _origGetComputedStyle.call(this, el, pseudo); + if (el && el.style && el.style.backgroundColor) { + return new Proxy(cs, { + get(target, prop) { + if (prop === "backgroundColor") return el.style.backgroundColor; + const val = target[prop]; + return typeof val === "function" ? val.bind(target) : val; + }, + }); + } + return cs; +}; \ No newline at end of file diff --git a/Sprint-3/slideshow/index.html b/Sprint-3/slideshow/index.html index 50f2eb1c0..b8401e57d 100644 --- a/Sprint-3/slideshow/index.html +++ b/Sprint-3/slideshow/index.html @@ -10,5 +10,8 @@
+
+
+