diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..66b62aecc 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,5 +1,10 @@ // Predict and explain first... +// This outputs undefined because address[0] tries to access an object like an array. +// Objects use property names, not numeric indices. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors + // This code should log out the houseNumber from the address object // but it isn't working... // Fix anything that isn't working @@ -12,4 +17,5 @@ const address = { postcode: "XYZ 123", }; -console.log(`My house number is ${address[0]}`); +// Fix: Dot notation accesses the property correctly +console.log(`My house number is ${address.houseNumber}`); diff --git a/Sprint-2/debug/author.js b/Sprint-2/debug/author.js index 8c2125977..acf633799 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,5 +1,10 @@ // Predict and explain first... +// This throws a TypeError because plain objects cannot be used with for...of directly. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values + // 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 +16,7 @@ const author = { alive: true, }; -for (const value of author) { +// Fix: Object.values() returns an array that can be looped over +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..06de93d0d 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -1,5 +1,11 @@ // Predict and explain first... +// This outputs [object Object] because putting an object in a template string calls toString(). +// The default toString() for objects just returns "[object Object]". +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join + // 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? @@ -10,6 +16,7 @@ const recipe = { ingredients: ["olive oil", "tomatoes", "salt", "pepper"], }; +// Fix: Accesses each property directly and uses join() to format the ingredients console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients.join("\n")}`); diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..6c0e188ca 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,16 @@ -function contains() {} +// Checks if an object has a specific property. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray + +function contains(obj, propertyName) { + // Returns false for invalid inputs (null, arrays, or non-objects) + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + return false; + } + + return obj.hasOwnProperty(propertyName); +} module.exports = contains; diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..90b5e0a8a 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -8,7 +8,7 @@ E.g. contains({a: 1, b: 2}, 'a') // returns true as the object contains a key of 'a' E.g. contains({a: 1, b: 2}, 'c') // returns false -as the object doesn't contains a key of 'c' +as the object doesn't contain a key of 'c' */ // Acceptance criteria: @@ -17,19 +17,54 @@ as the object doesn't contains a key of 'c' // When passed an object and a property name // Then it should return true if the object contains the property, false otherwise +// Returns true when the property exists +test("contains returns true if the property exists", () => { + expect(contains({ a: 1, b: 2 }, "a")).toBe(true); +}); + // Given an empty object // When passed to contains // Then it should return false -test.todo("contains on empty object returns false"); + +// An empty object has no properties +test("contains returns false for an empty object", () => { + expect(contains({}, "a")).toBe(false); +}); // Given an object with properties // When passed to contains with an existing property name // Then it should return true +// Finds a named property in a typical object +test("contains returns true for another property that exists", () => { + expect(contains({ name: "Lisa", age: 73 }, "name")).toBe(true); +}); + // Given an object with properties -// When passed to contains with a non-existent property name +// When passed to contains with a property name that does not exist +// Then it should return false + +// Returns false for a missing property +test("contains returns false for a property that does not exist", () => { + expect(contains({ name: "Leo", age: 19 }, "height")).toBe(false); +}); + +// Given an object +// When passed to contains with a property that does not exist // Then it should return false +// Returns false for a property not in the object +test("contains returns false when the property is not present", () => { + expect(contains({ a: 1, b: 2 }, "c")).toBe(false); +}); + // Given invalid parameters like an array // When passed to contains // Then it should return false or throw an error + +// Handles invalid inputs gracefully +test("contains handles invalid parameters", () => { + expect(contains([], "a")).toBe(false); + expect(contains(null, "a")).toBe(false); + expect(contains(undefined, "a")).toBe(false); +}); diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..fbef830f3 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,20 @@ -function createLookup() { - // implementation here +// Converts an array of pairs into a lookup object. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#bracket_notation + +function createLookup(arrayOfPairs) { + const lookupObject = {}; + + for (const pair of arrayOfPairs) { + const key = pair[0]; + const value = pair[1]; + + // Bracket notation allows using a variable as the property name + lookupObject[key] = value; + } + + return lookupObject; } module.exports = createLookup; diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..2ca0a2a92 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,18 @@ It should return: 'CA': 'CAD' } */ + +// Converts multiple pairs into a lookup object +test("creates a country currency code lookup for multiple codes", () => { + const input = [ + ["US", "USD"], + ["CA", "CAD"], + ]; + + const expectedOutput = { + US: "USD", + CA: "CAD", + }; + + expect(createLookup(input)).toEqual(expectedOutput); +}); diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..3c8aaf6ce 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,12 +1,26 @@ +// Parses a query string into an object. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice + function parseQueryString(queryString) { const queryParams = {}; + + // Returns early if the string is empty if (queryString.length === 0) { return queryParams; } + const keyValuePairs = queryString.split("&"); for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); + // Finds only the first "=" since values may contain "=" too + const firstEqualsIndex = pair.indexOf("="); + + const key = pair.slice(0, firstEqualsIndex); + const value = pair.slice(firstEqualsIndex + 1); + queryParams[key] = value; } diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..6b2c0fa51 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -3,10 +3,54 @@ // 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. -const parseQueryString = require("./querystring.js") +const parseQueryString = require("./querystring.js"); +// Handles values containing "=" test("parses querystring values containing =", () => { expect(parseQueryString("equation=x=y+1")).toEqual({ - "equation": "x=y+1", + equation: "x=y+1", + }); +}); + +// Parses a simple key-value pair +test("parses a simple key-value pair", () => { + expect(parseQueryString("name=John")).toEqual({ + name: "Samantha", + }); +}); + +// Parses multiple pairs separated by "&" +test("parses multiple key-value pairs", () => { + expect(parseQueryString("name=John&age=25")).toEqual({ + name: "Samantha", + age: "33", + }); +}); + +// Returns an empty object for an empty string +test("returns an empty object for an empty string", () => { + expect(parseQueryString("")).toEqual({}); +}); + +// Handles keys with empty values +test("parses a key with no value after the equals sign", () => { + expect(parseQueryString("name=")).toEqual({ + name: "", + }); +}); + +// Preserves multiple "=" in values +test("parses querystring with multiple = signs in value", () => { + expect(parseQueryString("formula=a=b=c")).toEqual({ + formula: "a=b=c", + }); +}); + +// Handles a mix of simple and complex values +test("parses mixed simple and complex key-value pairs", () => { + expect(parseQueryString("name=John&equation=1+1=2&city=London")).toEqual({ + name: "Samantha", + equation: "1+1=2", + city: "London", }); }); diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..a5a869dcd 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,30 @@ -function tally() {} +// Counts how often each item appears in an array. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of + +function tally(arrayOfItems) { + // Checks the input is actually an array + const inputIsNotAnArray = !Array.isArray(arrayOfItems); + + if (inputIsNotAnArray) { + throw new Error("Input must be an array"); + } + + const countObject = {}; + + for (const item of arrayOfItems) { + // If the item is already counted, add 1; otherwise start at 1 + const itemAlreadyCounted = countObject[item] !== undefined; + + if (itemAlreadyCounted) { + countObject[item] = countObject[item] + 1; + } else { + countObject[item] = 1; + } + } + + return countObject; +} module.exports = tally; diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..fd4affadd 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -20,15 +20,74 @@ const tally = require("./tally.js"); // When passed an array of items // Then it should return an object containing the count for each unique item +// Counts a single item +test("tally counts a single item in an array", () => { + const input = ["a"]; + + const expectedOutput = { a: 1 }; + + expect(tally(input)).toEqual(expectedOutput); +}); + +// Counts repeated items correctly +test("tally counts multiple occurrences of the same item", () => { + const input = ["a", "a", "a"]; + + const expectedOutput = { a: 3 }; + + expect(tally(input)).toEqual(expectedOutput); +}); + +// Counts different items with varying frequencies +test("tally counts different items with varying frequencies", () => { + const input = ["a", "a", "b", "c"]; + + const expectedOutput = { a: 2, b: 1, c: 1 }; + + expect(tally(input)).toEqual(expectedOutput); +}); + // 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"); + +// Returns an empty object for an empty array +test("tally on an empty array returns an empty object", () => { + const input = []; + + const expectedOutput = {}; + + expect(tally(input)).toEqual(expectedOutput); +}); // Given an array with duplicate items // When passed to tally // Then it should return counts for each unique item -// Given an invalid input like a string +// Handles real-world data with duplicates +test("tally handles an array with many duplicate items", () => { + const input = ["apple", "banana", "apple", "orange", "banana", "apple"]; + + const expectedOutput = { apple: 3, banana: 2, orange: 1 }; + + expect(tally(input)).toEqual(expectedOutput); +}); + +// Given invalid input like a string // When passed to tally // Then it should throw an error + +// Throws an error for a string input +test("tally throws an error when given a string instead of an array", () => { + expect(() => tally("not an array")).toThrow("Input must be an array"); +}); + +// Throws an error for null +test("tally throws an error when given null", () => { + expect(() => tally(null)).toThrow("Input must be an array"); +}); + +// Throws an error for undefined +test("tally throws an error when given undefined", () => { + expect(() => tally(undefined)).toThrow("Input must be an array"); +}); diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..97b8bf749 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -6,24 +6,42 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} -function invert(obj) { - const invertedObj = {}; - - for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; - } - - return invertedObj; -} +// Swaps the keys and values of an object. +// References: +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#bracket_notation // a) What is the current return value when invert is called with { a : 1 } +// { key: 1 } — the literal string "key" is used as the property name. // b) What is the current return value when invert is called with { a: 1, b: 2 } +// { key: 2 } — each iteration overwrites the same "key" property. // c) What is the target return value when invert is called with {a : 1, b: 2} +// { "1": "a", "2": "b" } — values become keys and keys become values. // c) What does Object.entries return? Why is it needed in this program? +// Object.entries returns an array of [key, value] pairs. +// It is needed because for...of cannot iterate over plain objects directly. // d) Explain why the current return value is different from the target output +// The bug uses dot notation (invertedObj.key), which creates a literal property called "key". +// Bracket notation (invertedObj[value]) uses the variable's value as the property name instead. // e) Fix the implementation of invert (and write tests to prove it's fixed!) + +function invert(obj) { + const invertedObj = {}; + + for (const [key, value] of Object.entries(obj)) { + // Bracket notation allows using the value as a property name + invertedObj[value] = key; + } + + return invertedObj; +} + +// Testing the fix with console.log +console.log(invert({ a: 1 })); // Expected: { "1": "a" } +console.log(invert({ a: 1, b: 2 })); // Expected: { "1": "a", "2": "b" } +console.log(invert({ x: 10, y: 20 })); // Expected: { "10": "x", "20": "y" } diff --git a/Sprint-3/alarmclock/alarmclock.js b/Sprint-3/alarmclock/alarmclock.js index 6ca81cd3b..a971aca5a 100644 --- a/Sprint-3/alarmclock/alarmclock.js +++ b/Sprint-3/alarmclock/alarmclock.js @@ -1,4 +1,54 @@ -function setAlarm() {} +// Stores the interval ID to allow clearing the timer when needed +let intervalId; + +// Reference: https://developer.mozilla.org/en-US/docs/Web/API/setInterval +function setAlarm() { + const input = document.getElementById("alarmSet"); + let totalSeconds = parseInt(input.value); + + if (isNaN(totalSeconds) || totalSeconds < 0) { + alert("Please enter a valid positive number for the time."); + return; + } + + // Prevents multiple timers from running simultaneously + if (intervalId) clearInterval(intervalId); + + // Ensures a clean visual state before starting a new countdown + document.body.style.backgroundColor = ""; + + updateDisplay(totalSeconds); + + intervalId = setInterval(() => { + totalSeconds--; + updateDisplay(totalSeconds); + + if (totalSeconds <= 0) { + clearInterval(intervalId); + playAlarm(); + document.body.style.backgroundColor = "#db4d4d"; + } + }, 1000); +} + +function updateDisplay(totalSeconds) { + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + const formattedMinutes = String(minutes).padStart(2, "0"); + const formattedSeconds = String(seconds).padStart(2, "0"); + document.getElementById( + "timeRemaining" + ).innerText = `Time Remaining: ${formattedMinutes}:${formattedSeconds}`; +} + +// Resets the alarm state completely, allowing the user to set a new alarm +// The setup() listener handles pausing the audio separately +document.getElementById("stop").addEventListener("click", () => { + if (intervalId) clearInterval(intervalId); + document.body.style.backgroundColor = ""; + document.getElementById("timeRemaining").innerText = "Time Remaining: 00:00"; + document.getElementById("alarmSet").value = ""; +}); // DO NOT EDIT BELOW HERE diff --git a/Sprint-3/alarmclock/index.html b/Sprint-3/alarmclock/index.html index 48e2e80d9..ff2d3b453 100644 --- a/Sprint-3/alarmclock/index.html +++ b/Sprint-3/alarmclock/index.html @@ -4,7 +4,7 @@ - Title here + Alarm clock app
diff --git a/Sprint-3/quote-generator/index.html b/Sprint-3/quote-generator/index.html index 30b434bcf..3feba441d 100644 --- a/Sprint-3/quote-generator/index.html +++ b/Sprint-3/quote-generator/index.html @@ -3,13 +3,21 @@ - Title here + Quote generator app + + -

hello there

+

Quote Generator

+ +
+ + + +
diff --git a/Sprint-3/quote-generator/script.js b/Sprint-3/quote-generator/script.js new file mode 100644 index 000000000..3547eafdf --- /dev/null +++ b/Sprint-3/quote-generator/script.js @@ -0,0 +1,46 @@ +const quoteElement = document.querySelector("#quote"); +const authorElement = document.querySelector("#author"); +const newQuoteButton = document.querySelector("#new-quote"); +const autoPlayToggle = document.querySelector("#auto-play-toggle"); +const autoPlayStatus = document.querySelector("#auto-play-status"); + +// Stores the interval ID to allow stopping auto-play when toggled off +let autoPlayInterval; + +// Displays a random quote from the quotes array +function displayRandomQuote() { + const randomQuote = pickFromArray(quotes); + quoteElement.textContent = randomQuote.quote; + authorElement.textContent = randomQuote.author; +} + +// Sets up the event listeners for the application +function setup() { + displayRandomQuote(); + + newQuoteButton.addEventListener("click", displayRandomQuote); + + autoPlayToggle.addEventListener("change", (event) => { + if (event.target.checked) { + startAutoPlay(); + } else { + stopAutoPlay(); + } + }); +} + +// Enables automatic quote rotation every 5 seconds for hands-free browsing +// Reference: https://developer.mozilla.org/en-US/docs/Web/API/setInterval +function startAutoPlay() { + autoPlayStatus.textContent = "(auto play is on)"; + autoPlayInterval = setInterval(displayRandomQuote, 5000); +} + +// Stops the automatic quote rotation when the user wants manual control +function stopAutoPlay() { + autoPlayStatus.textContent = ""; + clearInterval(autoPlayInterval); +} + +// Initializes the application when the window loads +window.addEventListener("load", setup); diff --git a/Sprint-3/quote-generator/style.css b/Sprint-3/quote-generator/style.css index 63cedf2d2..ca8153b01 100644 --- a/Sprint-3/quote-generator/style.css +++ b/Sprint-3/quote-generator/style.css @@ -1 +1,67 @@ -/** Write your CSS in here **/ +/** Styles for the Quote Generator app **/ + +body { + font-family: Georgia, serif; + background-color: #f5f5f5; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: 0; + padding: 20px; + box-sizing: border-box; +} + +h1 { + color: #333; + margin-bottom: 30px; +} + +#quote { + font-size: 1.5rem; + font-style: italic; + color: #555; + max-width: 600px; + text-align: center; + line-height: 1.6; + margin: 20px 0 10px; +} + +#author { + font-size: 1.1rem; + color: #777; + margin-bottom: 30px; +} + +#author::before { + content: "— "; +} + +#new-quote { + background-color: #4a90d9; + color: white; + border: none; + padding: 12px 24px; + font-size: 1rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#new-quote:hover { + background-color: #357abd; +} + +.controls { + margin-top: 20px; + display: flex; + align-items: center; + gap: 10px; + color: #555; +} + +#auto-play-status { + font-style: italic; + color: #4a90d9; +}