feat (curriculum): add lab Smart Pantry Restocker (#66298)

Co-authored-by: sembauke <semboot699@gmail.com>
This commit is contained in:
Abelardo Aguado Bueno
2026-05-23 22:37:33 +02:00
committed by GitHub
parent 68d3856079
commit b0a6be3fe7
4 changed files with 393 additions and 0 deletions
@@ -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 items `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);
```