diff --git a/Sprint-2/debug/address.js b/Sprint-2/debug/address.js index 940a6af83..36baf9306 100644 --- a/Sprint-2/debug/address.js +++ b/Sprint-2/debug/address.js @@ -1,5 +1,8 @@ // Predict and explain first... + // address is an object, so we can't access it by address[0] like in array we need to use a notation +// address.houseNumber, address.street, address.city etc ... + // This code should log out the houseNumber from the address object // but it isn't working... // Fix anything that isn't working @@ -12,4 +15,9 @@ const address = { postcode: "XYZ 123", }; -console.log(`My house number is ${address[0]}`); + +console.log(`My house number is ${address.houseNumber}`); + + + +// Address.js log function corrected diff --git a/Sprint-2/debug/author.js b/Sprint-2/debug/author.js index 8c2125977..e7187d820 100644 --- a/Sprint-2/debug/author.js +++ b/Sprint-2/debug/author.js @@ -1,5 +1,9 @@ // Predict and explain first... +// I thought the value in the log needed to get attached with the author like value.author +// But the error was author is not iterable as it is an object, so we need to modify the for ... of loop as +// for(const value of Object.values(author)) + // 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 +15,8 @@ const author = { alive: true, }; -for (const value of author) { +for (const value of Object.values(author)) { console.log(value); } + +// Author.js corrected. \ No newline at end of file diff --git a/Sprint-2/debug/recipe.js b/Sprint-2/debug/recipe.js index 6cbdd22cd..1940376c4 100644 --- a/Sprint-2/debug/recipe.js +++ b/Sprint-2/debug/recipe.js @@ -12,4 +12,11 @@ const recipe = { console.log(`${recipe.title} serves ${recipe.serves} ingredients: -${recipe}`); +${recipe.ingredients.join("\n")}`); + + +// I guess, because the values of ingredients are given as array, +// the way ingredients is passed to the log is not right. + +// recipe.js log function corrected. +// recipe.js modified "${recipe.ingredients.join("\n")}`) to make ingredients appear on new line. \ No newline at end of file diff --git a/Sprint-2/implement/contains.js b/Sprint-2/implement/contains.js index cd779308a..f3c8c0c33 100644 --- a/Sprint-2/implement/contains.js +++ b/Sprint-2/implement/contains.js @@ -1,3 +1,19 @@ -function contains() {} +function contains(obj, key) { + if (typeof obj !== "object" || obj === null) { + return false; + } +//return key in obj; +return Object.hasOwn(obj, key); + +} module.exports = contains; + +// In contains.js function implemented and tested. + + let obj = {}, propertyName = "toString"; + console.log( propertyName in obj ); // true + console.log( Object.hasOwn(obj, propertyName) ); // false + + // In contains.js modified return function as return Object.hasOwn(obj, key); + // The "if (typeof obj !== "object" || obj === null)" modified. \ No newline at end of file diff --git a/Sprint-2/implement/contains.test.js b/Sprint-2/implement/contains.test.js index 326bdb1f2..5bd2c50e2 100644 --- a/Sprint-2/implement/contains.test.js +++ b/Sprint-2/implement/contains.test.js @@ -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 +test("Should return true when key exists in object", () => { + const obj = { a: 1, b: 2 }; + expect(contains(obj, "a")).toEqual(true); +}); + // Given an empty object // When passed to contains // Then it should return false -test.todo("contains on empty object returns false"); +//test.todo("contains on empty object returns false"); + +test("Should return false when empty object is passed", () => { + const obj = {}; // empty object + expect(contains(obj, "a")).toEqual(false); +}) // Given an object with properties // When passed to contains with an existing property name // Then it should return true +test("Should return true when object has the property", () => { + const obj = { a: 5, c: 6, r: 5 }; + expect(contains(obj, "c")).toEqual(true); +}); // Given an object with properties // When passed to contains with a non-existent property name // Then it should return false +test("Should return false when object does not have the property", () => { + const obj = { a: 5, c: 6, r: 5 }; + expect(contains(obj, "z")).toEqual(false); // non-existent property name +}); // Given invalid parameters like an array // When passed to contains // Then it should return false or throw an error + +test("Should correctly detect keys in arrays", () => { + expect(contains([1, 2, 3], "1")).toEqual(true); // "1" is a key + expect(contains([1, 2, 3], "3")).toEqual(false); // "3" is not a key +}); + +test("Should return false when invalid parameters are used", () => { + expect(contains(null, "a")).toEqual(false); // null + expect(contains(5, "a")).toEqual(false); // number + expect(contains("hello", "a")).toEqual(false); // string +}) + +// In contains.test.js tests for contains.test.js passed + +// contains.test.js re-tested. + +// test case description differentiated. \ No newline at end of file diff --git a/Sprint-2/implement/lookup.js b/Sprint-2/implement/lookup.js index a6746e07f..03176b1c8 100644 --- a/Sprint-2/implement/lookup.js +++ b/Sprint-2/implement/lookup.js @@ -1,5 +1,27 @@ -function createLookup() { - // implementation here +function createLookup(input) { + if (!Array.isArray(input)) { + throw new Error("Input must be an array of [country, currency] pairs"); + } + + const lookup = {}; + + for (const pair of input) { + if (!Array.isArray(pair) || pair.length !== 2) { + throw new Error("Each item must be an array of [country, currency]"); + } + + const [country, currency] = pair; + + if (typeof country !== "string" || typeof currency !== "string") { + throw new Error("Each item must be an array of [country, currency]"); + } + + lookup[country] = currency; + } + + return lookup; } module.exports = createLookup; + +// In lookup.js function createLookup implemented. \ No newline at end of file diff --git a/Sprint-2/implement/lookup.test.js b/Sprint-2/implement/lookup.test.js index 547e06c5a..a59ab3a3b 100644 --- a/Sprint-2/implement/lookup.test.js +++ b/Sprint-2/implement/lookup.test.js @@ -1,6 +1,6 @@ const createLookup = require("./lookup.js"); -test.todo("creates a country currency code lookup for multiple codes"); +//test.todo("creates a country currency code lookup for multiple codes"); /* @@ -33,3 +33,50 @@ It should return: 'CA': 'CAD' } */ +// Case 1: Valid input + +test("returns an object with country codes as keys and currency codes as values", () => { + const input = [ + ["US", "USD"], + ["CA", "CAD"], + ]; + const expected = { US: "USD", CA: "CAD" }; + + expect(createLookup(input)).toEqual(expected); +}); + +// Case 2: Empty array + +test("returns an empty object when an empty array is passed", () => { + expect(createLookup([])).toEqual({}); +}); + +// Case 3: Input not an array + +test("throws an error when input is not an array", () => { + expect(() => createLookup("not an array")).toThrow( + "Input must be an array of [country, currency] pairs" + ); +}); + +// Case 4: Input null or undefined + +test("throws an error when input is null or undefined", () => { + expect(() => createLookup(null)).toThrow( + "Input must be an array of [country, currency] pairs" + ); + expect(() => createLookup(undefined)).toThrow( + "Input must be an array of [country, currency] pairs" + ); +}); + +// Case 5: type of Input elements are not strings + + test("throws an error if inner array elements are not valid strings", () => { + const invalidInput = [['US', 'USD'], ['CA', 123], ['JP', null]]; + expect(() => createLookup(invalidInput)).toThrow( + "Each item must be an array of [country, currency]" + ); + }); + +// In lookup.test.js test cases added and passed. \ No newline at end of file diff --git a/Sprint-2/implement/querystring.js b/Sprint-2/implement/querystring.js index 45ec4e5f3..de4a8eb70 100644 --- a/Sprint-2/implement/querystring.js +++ b/Sprint-2/implement/querystring.js @@ -1,16 +1,46 @@ function parseQueryString(queryString) { - const queryParams = {}; + const parsedParams = {}; // Gets the final key-value pairs + + if (!queryString) return parsedParams; + + // Removes leading '?' if present + + if (queryString.startsWith("?")) { + queryString = queryString.slice(1); + } + if (queryString.length === 0) { - return queryParams; + return parsedParams; } - const keyValuePairs = queryString.split("&"); - for (const pair of keyValuePairs) { - const [key, value] = pair.split("="); - queryParams[key] = value; + // Split the string into individual key-value pairs + const pairs = queryString.split("&"); + + for (const pair of pairs) { + if (!pair) continue; // skip empty segments (like from && or trailing &) eg "name=John&&age=30" + + const equalSignIndex = pair.indexOf("="); + + let paramKey, paramValue; + + if (equalSignIndex === -1) { + + // If '=' not found we have a key exists but value is empty + + paramKey = decodeURIComponent(pair); + paramValue = ""; + } else { + paramKey = decodeURIComponent(pair.slice(0, equalSignIndex)); + paramValue = decodeURIComponent(pair.slice(equalSignIndex + 1)); + } + + parsedParams[paramKey] = paramValue; // overwrite previous value if key repeats } - return queryParams; + return parsedParams; } module.exports = parseQueryString; + +// In querystring.js function implemented. +// Decoding of paramKey = decodeURIComponent(pair); added. \ No newline at end of file diff --git a/Sprint-2/implement/querystring.test.js b/Sprint-2/implement/querystring.test.js index 3e218b789..33ccab265 100644 --- a/Sprint-2/implement/querystring.test.js +++ b/Sprint-2/implement/querystring.test.js @@ -10,3 +10,70 @@ test("parses querystring values containing =", () => { "equation": "x=y+1", }); }); + +// Given an empty query string + +test("Empty querystring returns empty object", () => { + expect(parseQueryString("")).toEqual({}); +}); + +// A single key with value not given + +test("Single Key with No Value", () => { + expect(parseQueryString("key=")).toEqual({ key: "" }); +}); + +// A key without"=" sign + +test("Key Without = Sign", () => { + expect(parseQueryString("KeyAlone")).toEqual({ KeyAlone: "" }); +}); + +// Multiple parameters given + +test("Multiple Parameters", () => { + expect(parseQueryString("name=Smith&age=40&job=Teacher")).toEqual({ + name: "Smith", + age: "40", + job: "Teacher", + }); +}); + +// If values contain special characters + +test("Values Containing Special Characters", () => { + expect(parseQueryString("query=a%20b%26c%3Dd")).toEqual({ query: "a b&c=d" }); +}); + +// Query starting with "?" + +test("Starting with ?", () => { + expect(parseQueryString("?foo=bar")).toEqual({ foo: "bar" }); +}); + +// Given encoded key eg, %20 to " " + +test("Encoded keys, decoded correctly", () => { + expect(parseQueryString("first%20name=John")).toEqual({ + "first name": "John", + }); +}); + +// Mixed encoded + +test("Mixed encoded and plain parts", () => { + expect(parseQueryString("message=Hello%20World%21&Ans=Hello")).toEqual({ + message: "Hello World!", + Ans: "Hello", + }); +}); + +// Ignore repeated symbols --- && +test("Skips any empty parts that appear because of repeated symbols", () => { + expect(parseQueryString("a=1&&b=2")).toEqual({ + a: "1", + b: "2", + }); +}); + +// In querystring.test.js test edge cases added. \ No newline at end of file diff --git a/Sprint-2/implement/tally.js b/Sprint-2/implement/tally.js index f47321812..cad5ee856 100644 --- a/Sprint-2/implement/tally.js +++ b/Sprint-2/implement/tally.js @@ -1,3 +1,29 @@ -function tally() {} +function tally(items) { + if (!Array.isArray(items)) { + throw new Error("Input must be an array"); + } + + if (items.length === 0) { + return {}; + } + + const counts = Object.create(null); + + for (const item of items) { + + // Convert objects and arrays to JSON string + const key = (typeof item === "object" && item !== null) ? JSON.stringify(item) : item; + counts[key] = (counts[key] || 0) + 1; + } + + return counts; +} + +console.log(tally(["toStrin", "toStrin"])); +console.log(tally(["toString", "toString"])); module.exports = tally; + +// In tally.js function tally() implemented. + +// modification made. diff --git a/Sprint-2/implement/tally.test.js b/Sprint-2/implement/tally.test.js index 2ceffa8dd..fbf13122a 100644 --- a/Sprint-2/implement/tally.test.js +++ b/Sprint-2/implement/tally.test.js @@ -23,7 +23,7 @@ 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.todo("tally on an empty array returns an empty object"); // Given an array with duplicate items // When passed to tally @@ -32,3 +32,82 @@ 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 + +// Case 1: Empty array + +test("tally on an empty array returns an empty object", () => { + expect(tally([])).toEqual({}); +}); + +// Case 2: Array with duplicate items + +test("tally with duplicate items", () => { + expect(tally(["a", "a", "a"])).toEqual({ a: 3 }); + expect(tally(["a", "a", "b", "c"])).toEqual({ a: 2, b: 1, c: 1 }); +}); + +// Cas 3: Array of arrays + +test("tally handles arrays as items", () => { + const input = [ + [1, 2], + [1, 2], + [3, 4], + ]; + const output = { + "[1,2]": 2, + "[3,4]": 1, + }; + expect(tally(input)).toEqual(output); +}); + +// case 4: Array of objects + +test("tally handles objects as items", () => { + const input = [{ a: 1 }, { a: 1 }, { b: 2 }]; + const output = { + '{"a":1}': 2, + '{"b":2}': 1, + }; + expect(tally(input)).toEqual(output); +}); + +// Case 5: Mixed array of arrays and objects + +test("tally handles mixed arrays and objects", () => { + const input = [[1, 2], { a: 1 }, [1, 2], { a: 1 }, { b: 2 }]; + const output = { + "[1,2]": 2, + '{"a":1}': 2, + '{"b":2}': 1, + }; + expect(tally(input)).toEqual(output); +}); + +// Case 6: Mixed types (numbers, strings, booleans) + +test("tally handles numbers, strings, and booleans", () => { + const input = [1, "1", true, true, false]; + const output = { 1: 2, true: 2, false: 1 }; + expect(tally(input)).toEqual(output); +}); + +// Case 7: Array with null and undefined + +test("tally handles null and undefined values", () => { + const input = [null, null, undefined]; + const output = { null: 2, undefined: 1 }; + expect(tally(input)).toEqual(output); +}); + +// Case 8: Invalid input: non-array + +test("tally throws error if input is not an array", () => { + expect(() => tally("not an array")).toThrow("Input must be an array"); + expect(() => tally(123)).toThrow("Input must be an array"); + expect(() => tally({ a: 1 })).toThrow("Input must be an array"); + expect(() => tally(null)).toThrow("Input must be an array"); + expect(() => tally(undefined)).toThrow("Input must be an array"); +}); + +// In tally.test.js test cases added and passed. diff --git a/Sprint-2/interpret/invert.js b/Sprint-2/interpret/invert.js index bb353fb1f..2c31d5cff 100644 --- a/Sprint-2/interpret/invert.js +++ b/Sprint-2/interpret/invert.js @@ -7,23 +7,46 @@ // E.g. invert({x : 10, y : 20}), target output: {"10": "x", "20": "y"} function invert(obj) { + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + throw new Error("Input must be an object"); + } + const invertedObj = {}; - for (const [key, value] of Object.entries(obj)) { - invertedObj.key = value; + for (let [key, value] of Object.entries(obj)) { + if (typeof key === "object") { + key = JSON.stringify(key); + } + if (typeof value === "object") { + value = JSON.stringify(value); + } + invertedObj[value] = key; } return invertedObj; } +module.exports = invert; // a) What is the current return value when invert is called with { a : 1 } +// --> current return value i {key: 1} + // b) What is the current return value when invert is called with { a: 1, b: 2 } +// --> current return value i {key: 2} // c) What is the target return value when invert is called with {a : 1, b: 2} +// --> Target return value is {"1": "a", "2": "b" } // c) What does Object.entries return? Why is it needed in this program? +// --> Object.entries returns an array of key and value ['key', 'value'] pairs + // Given { a: 1, b: 2 } returns [['a', '1'],['b', '2']] // d) Explain why the current return value is different from the target output +// --> invertedObj.key = value is creating a property named key. As the loop + //goes through the object each value gets overwritten and we get the last + // argument as a value. // e) Fix the implementation of invert (and write tests to prove it's fixed!) + + +// In invert.js invert function implemented. \ No newline at end of file diff --git a/Sprint-2/interpret/invert.test.js b/Sprint-2/interpret/invert.test.js new file mode 100644 index 000000000..449adf966 --- /dev/null +++ b/Sprint-2/interpret/invert.test.js @@ -0,0 +1,45 @@ +const invert = require("./invert.js"); + +// Case 1: Normal object inversion +test("invert single-level object", () => { + expect(invert({ a: 1, b: 2 })).toEqual({ "1": "a", "2": "b" }); +}); + +// Case 2: object with string values +test("invert object with string values", () => { + expect(invert({ x: "CYF", y: "abc" })).toEqual({ "CYF": "x", "abc": "y" }); +}); + +// Case 3: object with boolean values +test("invert object with boolean values", () => { + expect(invert({ a: true, b: false })).toEqual({ "true": "a", "false": "b" }); +}); + +// Case 4: object with nested object as value +test("invert object with nested object as value", () => { + const input = { a: { x: 1 }, b: 2 }; + const expected = { '{"x":1}': "a", "2": "b" }; + expect(invert(input)).toEqual(expected); +}); + +// Case 5: object with duplicate values +test("invert object with duplicate values", () => { + const input = { a: 1, b: 1 }; + const expected = { "1": "b" }; + expect(invert(input)).toEqual(expected); +}); + +// Case 6: empty object +test("invert empty object", () => { + expect(invert({})).toEqual({}); +}); + +// Case 7: invalid inputs (non-object) +test("throws error for non-object input", () => { + expect(() => invert(null)).toThrow("Input must be an object"); + expect(() => invert(42)).toThrow("Input must be an object"); + expect(() => invert("foo")).toThrow("Input must be an object"); + expect(() => invert([1,2,3])).toThrow("Input must be an object"); +}); + +// In invert.test.js test cases tested. \ No newline at end of file diff --git a/Sprint-2/stretch/count-words.js b/Sprint-2/stretch/count-words.js index 8e85d19d7..7ed1433d9 100644 --- a/Sprint-2/stretch/count-words.js +++ b/Sprint-2/stretch/count-words.js @@ -25,4 +25,36 @@ 2. Ignore the case of the words to find more unique words. e.g. (A === a, Hello === hello) 3. Order the results to find out which word is the most common in the input + */ +function countWords(string) { + // validation of input + if (typeof string !== "string") { + throw new Error("Input should be a string"); + } + if (string === "") return {}; + const counts = Object.create(null) // prevents inherent properties. + + // Remove punctuation and lowercase everything + + const cleanText = string.replace(/[.,!?]/g, "").toLowerCase(); + const words = cleanText.split(" ").filter(Boolean); // removes empty strings + + // Count word frequencies + for (const word of words) { + counts[word] = (counts[word] || 0) + 1; + } + + // Sort by frequency (most common first) + const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); + + return Object.fromEntries(sorted); +} + +console.log(countWords("Hello!, my friend, You and me, and you.")); +console.log(countWords("constructor constructor")); +console.log(countWords(" Hello World ")); + +// In count-words.js function countWords implemented. + +// Modified to handle inherent property cases and to remove empty spaces. diff --git a/Sprint-2/stretch/mode.js b/Sprint-2/stretch/mode.js index 3f7609d79..13e72112b 100644 --- a/Sprint-2/stretch/mode.js +++ b/Sprint-2/stretch/mode.js @@ -8,29 +8,69 @@ // refactor calculateMode by splitting up the code // into smaller functions using the stages above -function calculateMode(list) { - // track frequency of each value - let freqs = new Map(); +// function calculateMode(list) { +// // track frequency of each value +// let freqs = new Map(); - for (let num of list) { - if (typeof num !== "number") { - continue; - } +// for (let num of list) { +// if (typeof num !== "number") { +// continue; +// } + +// freqs.set(num, (freqs.get(num) || 0) + 1); +// } + +// // Find the value with the highest frequency +// let maxFreq = 0; +// let mode; +// for (let [num, freq] of freqs) { +// if (freq > maxFreq) { +// mode = num; +// maxFreq = freq; +// } +// } + +// return maxFreq === 0 ? NaN : mode; +//} + + +// -------------- refactored ------------- // + +// Phase 1: Count the frequency of each number + +function countFrequency(list) { + const frequency = new Map(); - freqs.set(num, (freqs.get(num) || 0) + 1); + for (const num of list) { + if (typeof num !== "number") continue; // skip non-numbers + frequency.set(num, (frequency.get(num) || 0) + 1); } - // Find the value with the highest frequency + return frequency; +} + +// Stage 2: Find the number with the highest frequency +function findMode(frequency) { let maxFreq = 0; let mode; - for (let [num, freq] of freqs) { + + for (const [num, freq] of frequency) { if (freq > maxFreq) { - mode = num; maxFreq = freq; + mode = num; } } return maxFreq === 0 ? NaN : mode; } +// Main function +function calculateMode(list) { + const frequency = countFrequency(list); + return findMode(frequency); +} +console.log(calculateMode([1, 2, 2, 3, 3, 3, 4])); + module.exports = calculateMode; + +// In mode.js function calculateMode refactored. diff --git a/Sprint-2/stretch/mode.test.js b/Sprint-2/stretch/mode.test.js index ca33c28a3..241ebe8de 100644 --- a/Sprint-2/stretch/mode.test.js +++ b/Sprint-2/stretch/mode.test.js @@ -13,20 +13,29 @@ const calculateMode = require("./mode.js"); describe("calculateMode()", () => { test("returns the most frequent number in an array", () => { - const nums = [2, 4, 1, 2, 3, 2, 1]; + const arr = [2, 4, 1, 2, 3, 2, 1]; - expect(calculateMode(nums)).toEqual(2); + expect(calculateMode(arr)).toEqual(2); }); test("returns the first mode in case of multiple modes", () => { - const nums = [1, 2, 2, 3, 3]; + const arr = [1, 2, 2, 3, 3]; - expect(calculateMode(nums)).toEqual(2); + expect(calculateMode(arr)).toEqual(2); }); test("ignores non-number values", () => { - const nums = [1, 3, "2", 2, 3, null]; + const arr = [1, 3, "2", 2, 3, null]; - expect(calculateMode(nums)).toEqual(3); + expect(calculateMode(arr)).toEqual(3); }); + + test("returns NaN for an empty array", () => { + expect(calculateMode([])).toEqual(); +}); + }); + +// In mode.test.js test cases passed + + diff --git a/Sprint-2/stretch/till.js b/Sprint-2/stretch/till.js index 6a08532e7..f95a5bd14 100644 --- a/Sprint-2/stretch/till.js +++ b/Sprint-2/stretch/till.js @@ -5,27 +5,35 @@ // Then it should return the total amount in pounds function totalTill(till) { + if (typeof till !== "object" || till === null || Array.isArray(till)) { + throw new Error("Input should be an object"); + } + let total = 0; for (const [coin, quantity] of Object.entries(till)) { - total += coin * quantity; + const value = parseInt(coin); // extracts the numeric part + + if (isNaN(value)) continue; + + total += value * quantity; } - return `£${total / 100}`; + const pounds = Math.floor(total / 100); + const pence = String(total % 100).padStart(2, "0"); + + return `£${pounds}.${pence}`; } -const till = { - "1p": 10, - "5p": 6, - "50p": 4, - "20p": 10, -}; -const totalAmount = totalTill(till); +module.exports = totalTill; // a) What is the target output when totalTill is called with the till object - +// --> The target output should be £4.40 // b) Why do we need to use Object.entries inside the for...of loop in this function? - +// --> To get both the key and value of the till object inside the loop. // c) What does coin * quantity evaluate to inside the for...of loop? - +// --> It is supposed to calculate the total number of pence // d) Write a test for this function to check it works and then fix the implementation of totalTill + + +// In till.js function implemented. \ No newline at end of file diff --git a/Sprint-2/stretch/till.test.js b/Sprint-2/stretch/till.test.js new file mode 100644 index 000000000..08abbe8d7 --- /dev/null +++ b/Sprint-2/stretch/till.test.js @@ -0,0 +1,49 @@ +const totalTill = require("./till.js"); + +describe("totalTill()", () => { + test("correctly totals a simple till", () => { + const till = { + "1p": 10, // 10p + "5p": 6, // 30p + "20p": 10, // 200p + "50p": 4, // 200p + }; + + expect(totalTill(till)).toBe("£4.40"); + }); + + test("ignores invalid coin types", () => { + const till = { + "1p": 2, // 2p + "abc": 10, // ignore + "£1": 5, + }; + + expect(totalTill(till)).toEqual("£0.02"); + }); + + test("handles empty till", () => { + expect(totalTill({})).toEqual("£0.00"); + }); + + test("throws an error for non-object input", () => { + expect(() => totalTill(null)).toThrow("Input should be an object"); + expect(() => totalTill(5)).toThrow("Input should be an object"); + expect(() => totalTill("hello")).toThrow("Input should be an object"); + expect(() => totalTill([])).toThrow("Input should be an object"); + }); + + test("correct formatting for values under 10p", () => { + const till = { "1p": 3 }; // 3p + expect(totalTill(till)).toEqual("£0.03"); + }); + + test("correct formatting when total is exactly whole pounds", () => { + const till = { "50p": 4, "20p": 5 }; + // 50p*4 = 200p, 20p*5 = 100p → total = 300p = £3.00 + + expect(totalTill(till)).toEqual("£3.00"); + }); +}); + +// In till.test.js test cases written and tested. \ No newline at end of file