mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 10:22:16 +00:00
feat (curriculum): add lab Smart Pantry Restocker (#66298)
Co-authored-by: sembauke <semboot699@gmail.com>
This commit is contained in:
committed by
GitHub
parent
68d3856079
commit
b0a6be3fe7
@@ -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": [
|
||||
|
||||
+372
@@ -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);
|
||||
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user