diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index c2f5b7d3b06..bac320fa8b0 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -4936,6 +4936,12 @@ "In this lab, you will build a function that finds the missing letter in a given range of consecutive letters and returns it." ] }, + "lab-smart-pantry-restocker": { + "title": "Build a Smart Pantry Restocker", + "intro": [ + "In this lab, you will build a small pantry management program using basic JavaScript concepts like arrays, objects, loops, and conditionals." + ] + }, "lab-proofreading-tool": { "title": "Build a Proofreading Tool", "intro": [ diff --git a/curriculum/challenges/english/blocks/lab-smart-pantry-restocker/69a5f35669099ed52f8563b1.md b/curriculum/challenges/english/blocks/lab-smart-pantry-restocker/69a5f35669099ed52f8563b1.md new file mode 100644 index 00000000000..79605137a50 --- /dev/null +++ b/curriculum/challenges/english/blocks/lab-smart-pantry-restocker/69a5f35669099ed52f8563b1.md @@ -0,0 +1,372 @@ +--- +id: 69a5f35669099ed52f8563b1 +title: Build a Smart Pantry Restocker +challengeType: 26 +dashedName: lab-smart-pantry-restocker +--- + +# --description-- + +In this lab, you will build a small pantry management program using basic JavaScript concepts like arrays, objects, loops, and conditionals. + +You will simulate receiving a shipment of pantry items, deciding what to do with each item, and organizing the results for storage. + +**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab. + +**User Stories:** + +1. You should implement a `parseShipment(rawData)` function that takes an array of strings and returns an array of objects with `{ sku, name, qty, expires, zone }` properties. + +2. You should implement a `planRestock(pantry, shipment)` function that compares the current pantry with the incoming shipment and returns an array of actions in the form `{ type: "restock" | "discard" | "donate", item }`. + +3. You should implement a `groupByZone(actions)` function that groups the actions into storage zones based on each item’s `zone` property. + +4. You should implement a `clonePantry(pantry)` function that returns a deep copy of the pantry so planning changes do not affect the original list. A deep copy means creating a new array with new objects, so modifying the copy does not change the original pantry. + +5. You should use all of the functions together to process a shipment and log the final grouped result object to the console. + +# --before-each-- + +```js +const spy = __helpers.spyOn(console, "log"); +const getLogs = () => spy.calls.map(call => call?.[0]); +``` + +# --hints-- + +You should define a function named `parseShipment` that accepts one array of strings called `rawData` as parameter. + +``` js +assert.isFunction(parseShipment); +assert.lengthOf(parseShipment, 1); +``` + +Your `parseShipment` function should convert shipment strings in the array into objects with the properties: `sku`, `name`, `qty`, `expires`, and `zone`. + +``` js +const rawData = ["A10|Tomatoes|5|2027-01-01|fridge"]; +const result = parseShipment(rawData); + +assert.isArray(result); +assert.isObject(result[0]); +assert.deepEqual(result[0], { + sku: "A10", + name: "Tomatoes", + qty: 5, + expires: "2027-01-01", + zone: "fridge" +}); +``` + +Duplicate SKUs in the shipment should be ignored. + +``` js +const rawData = [ + "C32|Eggs|3|2027-01-01", + "C32|Eggs|3|2027-01-01" +]; + +const result = parseShipment(rawData); + +assert.lengthOf(result, 1); +assert.strictEqual(result[0].sku, "C32"); +``` + +When `zone` is not present in a shipment string, you should assign it to `general`. + +``` js +const rawData = ["B21|Bananas|10|2027-01-01"]; +const result = parseShipment(rawData); +assert.strictEqual(result[0].zone, "general"); +``` + +The `qty` value should be converted into a number. + +``` js +const rawData = ["A10|Tomatoes|5|2027-01-01|fridge"]; +const result = parseShipment(rawData); +assert.strictEqual(result[0].qty, 5); +assert.isNumber(result[0].qty); +``` + +You should define a function named `planRestock` that accepts two parameters: `pantry` and `shipment`. + +``` js +assert.isFunction(planRestock); +assert.lengthOf(planRestock, 2); +``` + +Your `planRestock` function should return an array of actions with the properties of `type` and `item`. + +``` js +const pantry = [{ sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }]; +const shipment = [{ sku: "A10", name: "Tomatoes", qty: 6, expires: "2027-01-01", zone: "fridge" }]; + +const result = planRestock(pantry, shipment); + +assert.isArray(result); +assert.isObject(result[0]); +assert.property(result[0], "type"); +assert.property(result[0], "item"); +``` + +Items with a quantity of `0` or less in shipment should create an action with type `discard`, regardless of whether the item is in the pantry. + +``` js +const pantry = [{ sku: "D43", name: "Pineapples", qty: 2, expires: "2027-01-01", zone: "general" }]; +const shipment = [{ sku: "D43", name: "Pineapples", qty: 0, expires: "2027-01-01", zone: "general" }, + { sku: "E54", name: "Peppers", qty: -1, expires: "2027-01-01", zone: "fridge" } + ]; + +const result = planRestock(pantry, shipment); + +assert.strictEqual(result[0].type, "discard"); +assert.strictEqual(result[1].type, "discard"); +``` + +When a shipment item already exists in the pantry (same `sku`), the action should be `restock`. + +``` js +const pantry = [{ sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }]; +const shipment = [{ sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }]; + +const result = planRestock(pantry, shipment); + +assert.strictEqual(result[0].type, "restock"); +``` + +If a shipment item does not exist in the pantry, the action should be `"donate"`. + +``` js +const pantry = []; +const shipment = [{ sku: "C32", name: "Eggs", qty: 3, expires: "2027-01-01", zone: "fridge" }]; + +const result = planRestock(pantry, shipment); + +assert.strictEqual(result[0].type, "donate"); +``` + +You should define a function named `groupByZone` that accepts one parameter called `actions`. + +``` js +assert.isFunction(groupByZone); +assert.lengthOf(groupByZone, 1); +``` + +Your function `groupByZone` should return the actions grouped by the `zone` property of each object. + +``` js + +const actions = [ + { type: "restock", item: { sku: "A1", zone: "fridge" } }, + { type: "restock", item: { sku: "B1", zone: "pantry" } }, +]; + +const result = groupByZone(actions); + +assert.isObject(result); +assert.isArray(result.fridge); +assert.isArray(result.pantry); +``` + +You should define a function named `clonePantry` that accepts one parameter called `pantry`. + +``` js +assert.isFunction(clonePantry); +assert.lengthOf(clonePantry, 1); +``` + +`clonePantry` should return a new array instead of the original pantry array. + +``` js +const pantry = [{ sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }]; +const pantryClone = clonePantry(pantry); + +assert.isArray(pantryClone); +assert.notStrictEqual(pantryClone, pantry); +``` + +The objects inside the cloned pantry should also be new objects (deep copy). + +``` js +const pantry = [{ sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }]; +const pantryClone = clonePantry(pantry); + +assert.notStrictEqual(pantryClone[0], pantry[0]); +assert.deepEqual(pantryClone[0], pantry[0]); + +pantryClone[0].qty = 100; +assert.strictEqual(pantry[0].qty, 4); +``` + +The functions should work together to process a shipment and group the resulting actions. + +``` js +const pantry = [{ sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }]; +const rawData = ["A10|Tomatoes|5|2027-01-01|fridge"]; + +const shipment = parseShipment(rawData); +const pantryClone = clonePantry(pantry); +const actions = planRestock(pantryClone, shipment); +const grouped = groupByZone(actions); + +assert.isObject(grouped); +assert.strictEqual(grouped.fridge[0].item.qty, 5); +``` + +You should log the resulting actions grouped by zones. + +``` js +const groupedLog = getLogs().find(log => { + if (!log || typeof log !== "object" || Array.isArray(log)) return false; + + return Object.values(log).some(zoneActions => { + if (!Array.isArray(zoneActions)) return false; + + return zoneActions.some(action => + action && + ["restock", "discard", "donate"].includes(action.type) && + action.item && + typeof action.item.sku === "string" + ); + }); +}); + +assert.isOk(groupedLog, "Log the grouped actions object to the console."); +``` + +# --seed-- + +## --seed-contents-- + +```js + +``` + +# --solutions-- + +```js + +function parseShipment(rawData) { + + // Early return if rawData is not an Array + if (!Array.isArray(rawData)) return []; + + const result = []; + const seenSKUs = []; + + for (let line of rawData) { + + const parts = line.split("|"); + + const sku = parts[0].trim(); + + if (seenSKUs.includes(sku)) continue; // Avoid duplicates + + const name = parts[1].trim(); + const qty = Number(parts[2].trim()); // Parse qty into a number + const expires = parts[3].trim(); + const zone = ( parts[4] ) ? parts[4].trim() : "general"; + + result.push({ + sku: sku, + name: name, + qty: qty, + expires: expires, + zone: zone + }); + + seenSKUs.push(sku); + + } + + return result; +} + +function planRestock(pantry, shipment) { + const actions = []; + + for (let item of shipment) { + + var exists = false; + + for (let stock of pantry) + { + if (stock.sku === item.sku) + { + exists = true; + } + } + + if (item.qty > 0) { + if (exists) { + actions.push({ type: "restock", item: item }); + } else{ + actions.push({ type: "donate", item: item }); + } + } else { + actions.push({ type: "discard", item: item }); + } + } + + return actions; +} + +function groupByZone(actions) { + + const byZone = {}; + + for (const action of actions) { + + const zone = action.item.zone; + + if (!byZone[zone]) { + byZone[zone] = []; + } + + byZone[zone].push(action); + } + + return byZone; + +} + +function clonePantry(pantry) { + const pantryClone = []; + + for (let item of pantry) { + pantryClone.push({ + sku: item.sku, + name: item.name, + qty: item.qty, + expires: item.expires, + zone: item.zone + }); + } + + return pantryClone; +} + +const pantry = [ + { sku: "A10", name: "Tomatoes", qty: 4, expires: "2027-01-01", zone: "fridge" }, + { sku: "D43", name: "Pineapples", qty: 2, expires: "2020-01-01", zone: "general" } +]; + +const rawData = [ + "A10|Tomatoes|5|2027-01-01", // Restock existing item + "B21|Bananas|10|2027-01-01", // Donate new item without zone + "C32|Eggs|3|2027-01-01|fridge", // Donate to a defined zone + "C32|Eggs|3|2027-01-01", // Duplicated SKU in shipment + "D43|Pineapples|0|2027-01-01", // Discard with quantity of 0 + "E54|Peppers|-1|2027-01-01|fridge" // Discard even if it's not in pantry +]; + +const shipment = parseShipment(rawData); +const pantryCopy = clonePantry(pantry); +const actions = planRestock(pantryCopy, shipment); +const grouped = groupByZone(actions); + +// Show items expanded in zones +console.log(grouped); + +``` diff --git a/curriculum/structure/blocks/lab-smart-pantry-restocker.json b/curriculum/structure/blocks/lab-smart-pantry-restocker.json new file mode 100644 index 00000000000..5736c09708f --- /dev/null +++ b/curriculum/structure/blocks/lab-smart-pantry-restocker.json @@ -0,0 +1,14 @@ +{ + "blockLabel": "lab", + "blockLayout": "link", + "isUpcomingChange": false, + "usesMultifileEditor": true, + "dashedName": "lab-smart-pantry-restocker", + "challengeOrder": [ + { + "id": "69a5f35669099ed52f8563b1", + "title": "Build a Smart Pantry Restocker" + } + ], + "helpCategory": "JavaScript" +} diff --git a/curriculum/structure/superblocks/javascript-v9.json b/curriculum/structure/superblocks/javascript-v9.json index e54ee867ce0..2220d953832 100644 --- a/curriculum/structure/superblocks/javascript-v9.json +++ b/curriculum/structure/superblocks/javascript-v9.json @@ -109,6 +109,7 @@ "lab-repeat-a-string", "workshop-festival-crowd-flow-simulator", "lab-missing-letter-detector", + "lab-smart-pantry-restocker", "lab-proofreading-tool", "review-javascript-loops", "quiz-javascript-loops"