Records Labs (6 scenarios)
← Module home · Attempt each lab’s student tasks before opening its solution. Every lab: scenario → requirements → test data → student tasks → solution → explanation → ⭐ extensions. The first deliverable in every lab is the schema — struct + enum definitions with one-sentence field comments (Lesson 3 §5).
Ground rules. Procedural C++ — records, enums, arrays, functions (Lesson 2’s door rules); no classes, no vectors. Every lab reuses the same skeleton discipline: readX factory → printX const& → per-field passes.
Lab 1 — Student Records
Scenario. A tutor runs a 30-student class on paper. Digitize it: one menu program over a roster of records.
Requirements.
R1. Schema: Student { std::string name; int score; } with a Grade enum (A/B/C/D/F) derived — Grade letterOf(int score) with the course-standard bands (80/70/60/50).
R2. Menu: 1-add (validated, capacity-guarded) · 2-list (Sr. | Name | Score | Grade with the bridge) · 3-statistics (highest, lowest, average, pass count ≥ 50) · 4-sort by score descending · 5-quit.
R3. Construction through readStudent() only — no record is ever built elsewhere.
R4. The grade bridge is the only place that knows the bands — change 80→85 and exactly one function edits.
Test data.
| Input | Expected |
|---|---|
| scores 92, 78, 85, 49, 63 | grades A, C, B, F, D |
| statistics on that set | high 92, low 49, avg 73.4, pass 4 |
| sort then list | 92, 85, 78, 63, 49 |
| add when full (30) | roster full message, no crash |
| score 101 / −3 | rejected by the factory, re-prompted |
Student tasks.
- Write the schema +
letterOf+gradeTextfirst; test the bridge standalone. - Then the roster passes — one function per menu option, all taking
const Student[]except add/sort. - Reflection: the 85→A row in your trace — which function decided that? Follow it: list → letterOf. One source of truth.
Solution.
#include <iostream>
#include <string>
#include <iomanip>
struct Student { std::string name; int score; };
enum class Grade { A, B, C, D, F };
Grade letterOf(int score) { // the bands live HERE only
if (score >= 80) return Grade::A;
if (score >= 70) return Grade::B;
if (score >= 60) return Grade::C;
if (score >= 50) return Grade::D;
return Grade::F;
}
const char* gradeText(Grade g) {
switch (g) {
case Grade::A: return "A"; case Grade::B: return "B";
case Grade::C: return "C"; case Grade::D: return "D";
}
return "F";
}
Student readStudent() { // the only construction door
Student s;
std::cout << "name: ";
std::getline(std::cin, s.name);
do { std::cout << "score: "; std::cin >> s.score;
if (s.score < 0 || s.score > 100) std::cout << "0-100 please\n"; }
while (s.score < 0 || s.score > 100);
std::cin.ignore(1000, '\n');
return s;
}
void printRow(int sr, const Student& s) {
std::cout << std::setw(3) << sr << " | " << std::setw(20) << std::left << s.name
<< " | " << std::setw(3) << std::right << s.score << " | "
<< gradeText(letterOf(s.score)) << '\n';
}
void statistics(const Student r[], int n) {
if (n == 0) { std::cout << "empty\n"; return; }
int hi = r[0].score, lo = r[0].score, total = 0, pass = 0;
for (int i = 0; i < n; i = i + 1) {
if (r[i].score > hi) hi = r[i].score;
if (r[i].score < lo) lo = r[i].score;
total += r[i].score;
if (r[i].score >= 50) pass += 1;
}
std::cout << std::fixed << std::setprecision(1)
<< "high " << hi << " low " << lo << " avg " << (1.0 * total / n)
<< " pass " << pass << "/" << n << '\n';
}
void sortByScore(Student r[], int n) {
for (int i = 0; i < n - 1; i = i + 1) {
int best = i;
for (int j = i + 1; j < n; j = j + 1)
if (r[j].score > r[best].score) best = j;
Student t = r[i]; r[i] = r[best]; r[best] = t; // ONE move
}
}
int main() {
const int CAP = 30;
Student roster[CAP];
int count = 0;
int choice;
do {
std::cout << "\n1-add 2-list 3-stats 4-sort 5-quit: ";
std::cin >> choice; std::cin.ignore(1000, '\n');
if (choice == 1) {
if (count < CAP) { roster[count] = readStudent(); count += 1; }
else std::cout << "roster full\n";
} else if (choice == 2) {
for (int i = 0; i < count; i = i + 1) printRow(i + 1, roster[i]);
} else if (choice == 3) statistics(roster, count);
else if (choice == 4) sortByScore(roster, count);
} while (choice != 5);
}
Explanation. The design win to feel: letterOf and gradeText split the “what band?” question from the “how to show it?” question — the enum member travels through records and switches while the bands and the spelling each live in exactly one function. Every menu option is one pass over records; the sort’s one-move swap is the parallel-arrays dividend, cashed.
⭐ Extensions. ⭐ Add “3b: class grade histogram” (count per Grade — the enum drives the tally array). ⭐⭐ Add name search (case-insensitive contains — Unit 11 toolkit). ⭐⭐⭐ Add edit-with-revalidation (the editX pattern from C10).
Lab 2 — Employee Records
Scenario. A small company’s HR sheet: employees, grades, overtime. Your schema gets to be nested this time.
Requirements.
R1. Schema: struct Name { std::string first; std::string last; }, struct Employee { Name who; Date joined; double base; int overtimeHours; Grade grade; } — reuse the Date and Grade from Lab 1/E11 and [C7’s rates].
R2. Menu: 1-hire (validated factory) · 2-directory (sorted by last name) · 3-payroll table (gross per [C7’s rule]) · 4-find-by-surname (all matches) · 5-quit.
R3. gross is a function of the record — no stored gross field (derive, never store derived data).
R4. Directory sort compares who.last then who.first — the two-key comparison.
Test data.
| Employee | base | OT hrs | grade | gross (300/500/700 rates) |
|---|---|---|---|---|
| Ali Raza, JUNIOR | 40000 | 5 | JUNIOR | 41500 |
| Sana Mir, SENIOR | 90000 | 2 | SENIOR | 91400 |
| Umar Farooq, MID | 60000 | 0 | MID | 60000 |
| Ayesha Khan, MID | 62000 | 3 | MID | 63500 |
Directory order (last name): Farooq, Khan, Mir, Raza. Payroll order (gross desc): Mir 91400, Khan 63500, Raza 41500, Farooq 60000 → sort check: 91400, 63500, 60000, 41500.
Student tasks.
- Schema first — including the decision memo: why
Date joinedis a nested record and not three ints (one sentence). - The two-key comparator: write
bool comesBefore(const Employee& a, const Employee& b)and use it in the sort — sorting logic never inlines the comparison. - Reflection: the payroll table sorts by a derived value. Where do you compute it (per comparison? into a helper array?) and why is not storing it still the right call?
Solution (core functions).
double gross(const Employee& e) { // derived on demand — C7's rule
double rate = 0.0;
if (e.grade == Grade::JUNIOR) rate = 300.0;
else if (e.grade == Grade::MID) rate = 500.0;
else rate = 700.0;
return e.base + e.overtimeHours * rate;
}
bool comesBefore(const Employee& a, const Employee& b) { // two-key: last, then first
if (a.who.last != b.who.last) return a.who.last < b.who.last;
return a.who.first < b.who.first;
}
void sortByLastName(Employee staff[], int n) { // selection sort, comparator-driven
for (int i = 0; i < n - 1; i = i + 1) {
int best = i;
for (int j = i + 1; j < n; j = j + 1)
if (comesBefore(staff[j], staff[best])) best = j;
Employee t = staff[i]; staff[i] = staff[best]; staff[best] = t;
}
}
Explanation. Two ideas to carry off: the comparator function (sort mechanics stay generic; meaning of order lives in one named function — swap “by last name” for “by gross” by writing one new comparator), and derive-don’t-store (gross is always computed from the current base/OT — store it and a base edit makes the stored value a lie). Traces: Ali 40000 + 5×300 = 41500 ✓; Ayesha 62000 + 3×500 = 63500 ✓.
⭐ Extensions. ⭐ Anniversaries: employees whose joined anniversary falls in the current month (compare months only — Date components earn their keep). ⭐⭐ ⭐⭐⭐ Grade-raise: promote+raise with re-validation, printing a before/after table.
Lab 3 — Product Inventory
Scenario. A campus store’s stock: products with quantities and reorder levels. The enum makes its appearance as state.
Requirements.
R1. Schema: enum class StockState { OK, LOW, OUT }, struct Product { std::string name; int qty; double price; int reorderAt; StockState state; } — state is derived from qty via stateOf(qty, reorderAt) (qty ≤ 0 → OUT, < reorderAt → LOW, else OK), updated by one refreshState(Product&).
R2. Menu: 1-receive shipment (qty += n) · 2-sell (qty -= n, refuse oversell) · 3-report (table: name, qty, price, state-text) · 4-value (Σ qty×price) · 5-reorder list (LOW and OUT items, by urgency: OUT first) · 6-quit.
R3. Selling never sets state directly — it changes qty and calls refreshState. One door to state.
R4. Refuse oversell without changing qty (validate, then mutate — the C4 gate).
Test data.
| Action | Product (start qty) | Result qty → state |
|---|---|---|
| sell 3 | Notebook (10, reorder 4) | 7 → OK |
| sell 4 | Notebook (7, reorder 4) | 3 → LOW |
| sell 3 | Notebook (3, reorder 4) | 0 → OUT |
| sell 1 | Notebook (0) | refused — out of stock |
| receive 20 | Notebook (0) | 20 → OK |
| value | Pens(50×Rs.30) + Notebooks(20×Rs.120) | Rs. 3900.0 |
Student tasks.
- Schema +
stateOf+stockTextfirst; drive the state machine on the test table by hand. - Sell: write the guard, then the mutation, then
refreshState— in that order, and note why the order matters. - Reflection: why is
statestored at all, when it’s derivable? (Hint: think about the reorder report’s query pattern vs recomputing on every print — and what rule keeps the stored value honest: exactly one updater function.)
Solution (core).
StockState stateOf(int qty, int reorderAt) {
if (qty <= 0) return StockState::OUT;
if (qty < reorderAt) return StockState::LOW;
return StockState::OK;
}
void refreshState(Product& p) { p.state = stateOf(p.qty, p.reorderAt); }
bool sell(Product& p, int n) {
if (n <= 0 || p.qty < n) return false; // validate BEFORE mutate
p.qty -= n;
refreshState(p); // one door to state
return true;
}
bool receive(Product& p, int n) {
if (n <= 0) return false;
p.qty += n;
refreshState(p);
return true;
}
Explanation. The lab’s teaching point is single-source state: state is derivable, but caching it in the record is a legitimate performance/reading choice only when every mutation path funnels through refreshState — the same discipline Unit 15’s setters will enforce structurally. The reorder list is a two-priority pass (OUT before LOW): a comparator-driven sort or a two-sweep print, your documented choice. Value check: 50×30 + 20×120 = 1500 + 2400 = 3900 ✓.
⭐ Extensions. ⭐ Category enum (Stationery/Books/Other) with a per-category value report. ⭐⭐ Transaction history as a second record array (Sale { productIndex; int qty; Date when; }) — the parallel array that’s honest because it’s a different entity, not a parallel view of the same one.
Lab 4 — Library Books
Scenario. The departmental library: circulation with rules. The full C4 state machine becomes a working catalog.
Requirements.
R1. Schema: [E20’s LibraryBook] plus isbn uniqueness enforced at add time (scan for duplicates — refuse).
R2. Menu: 1-add · 2-catalogue (all books with state-text) · 3-borrow (by ISBN, name + today’s date) · 4-return (by ISBN) · 5-overdue list (BORROWED and due < today, via C1’s isBefore) · 6-quit.
R3. Borrow/return return bool — the caller prints the refusal reason (already borrowed / not borrowed / unknown ISBN).
R4. Borrower name is the "" sentinel when not borrowed — and every printer honours it (never prints an empty “with “ line).
Test data.
| Step | Book | Expected |
|---|---|---|
| add 2 books | C++ Primer, Little Prince | catalogue shows both AVAILABLE |
| add C++ Primer again | duplicate ISBN | refused |
| borrow C++ Primer (today 2026-09-23) | BORROWED, due 2026-10-07, borrower recorded | |
| borrow C++ Primer again | refused: already borrowed | |
| borrow Little Prince | BORROWED | |
| return Little Prince | AVAILABLE, borrower cleared | |
| overdue list (today 2026-10-10) | C++ Primer listed (due 10-07 < today) |
Student tasks.
- Schema +
stateText+findByIsbn(returns an index — −1 convention — say why an index beats a pointer here). - The borrow/return gates from C4, now wired to menu + ISBN lookup.
- Reflection:
findByIsbnreturning −1 vsnullptr— both conventions exist in the course. State the rule for choosing (searching an array → index; searching dynamic/linked structures → pointer; the array needs no nulls, the linked world lives on them).
Solution (core).
int findByIsbn(const LibraryBook shelf[], int n, const std::string& isbn) {
for (int i = 0; i < n; i = i + 1)
if (shelf[i].isbn == isbn) return i;
return -1;
}
bool borrow(LibraryBook& b, const std::string& who, const Date& today) {
if (b.status != BookStatus::AVAILABLE) return false;
b.status = BookStatus::BORROWED;
b.borrower = who;
b.due = addDays(today, 14); // the documented 14-day loan; addDays from C1/C4
return true;
}
bool giveBack(LibraryBook& b) {
if (b.status != BookStatus::BORROWED) return false;
b.status = BookStatus::AVAILABLE;
b.borrower = "";
return true;
}
Explanation. The catalogue is now a state machine with an index: enum states, guarded transitions, and a search convention that returns a position rather than a copy (so borrow/return mutate the shelf’s record — shelf[i] passed by reference through the index). The overdue list composes three unit-skills: enum state filter + isBefore (record comparison) + the printing discipline. Due-date check: 2026-09-23 + 14 = 2026-10-07 ✓.
⭐ Extensions. ⭐⭐ Borrow limit: a borrower’s name may appear on at most 2 BORROWED books (a count pass before the gate). ⭐⭐⭐ Fine report: for overdue books, days-late × Rs.10 per book, as a record array (the C3 rule: return values, print reports).
Lab 5 — Patient Records
Scenario. A ward round tool — the generic programming scenario: any clinic’s daily triage and round report. (Deliberately generic: no medical knowledge required, only records doing record work.)
Requirements.
R1. Schema: enum class Ward { GENERAL, ICU, MATERNITY, PAEDIATRIC }, struct Patient { int id; Name who; int age; Ward ward; bool critical; Date admitted; } — Name and Date nested (E22’s seed, grown up).
R2. Menu: 1-admit (validating factory: unique id, age 0–120, admission date via readDate) · 2-ward round (per-ward patient list) · 3-critical list (cross-ward, top priority first: critical → then lowest id) · 4-discharge (by id; removes from the array — the shift-left) · 5-census (counts per ward + total) · 6-quit.
R3. The ward round groups by enum — the outer loop iterates Ward values (E17’s grouping trick).
R4. Discharge must keep records contiguous (shift) — the count stays the truth.
Test data.
| Patient | ward | critical | Round order | Census |
|---|---|---|---|---|
| #1 Ali Raza, 34 | GENERAL | no | GENERAL: #1, #4 | GENERAL 2 |
| #2 Baby Sana, 0 | PAEDIATRIC | yes | ICU: #3 | ICU 1 |
| #3 Umar Farooq, 51 | ICU | yes | PAEDIATRIC: #2 | PAEDIATRIC 1 |
| #4 Ayesha Khan, 28 | GENERAL | no | MATERNITY: (none) | MATERNITY 0 |
| discharge #1 | GENERAL: #4 only — contiguous ✓ | GENERAL 1 |
Student tasks.
- Schema +
wardText+findPatient(id → index, −1 convention). - Admit: unique-id scan before insert — the duplicate gate from Lab 4’s ISBN, reused.
- Critical list: the three-key champion from [C6] simplified to two keys (critical desc, id asc).
- Reflection: the census loop iterates Ward values, not patients. What property of enums makes that possible — and what would break the loop if a ward were a string?
Solution (core).
void roundForWard(const Patient ward[], int n, Ward w) {
std::cout << "-- " << wardText(w) << " --\n";
bool any = false;
for (int i = 0; i < n; i = i + 1)
if (ward[i].ward == w) { printPatient(ward[i]); any = true; }
if (!any) std::cout << "(empty)\n";
}
// census: for each Ward value, count matches — the enum IS the iteration plan
void discharge(Patient ward[], int& n, int id) {
int i = findPatient(ward, n, id);
if (i == -1) { std::cout << "no such id\n"; return; }
for (int k = i; k < n - 1; k = k + 1) ward[k] = ward[k + 1]; // shift left
n -= 1; // count stays the truth
}
Explanation. Three composable patterns, all procedural: enum-driven grouping (closed sets enumerate, strings don’t — that’s the R4 reflection’s answer: a string’s value set is open, so no outer loop over “all wards” can exist), contiguity discipline (discharge shifts left so n remains the record count — the array invariant from Unit 09), and id-keyed access (find-by-id → index → mutate through the index). The ward round prints per-ward headers whether or not patients exist — a report that silently omits empty wards looks like data loss.
⭐ Extensions. ⭐ Stay-days report: today − admitted per patient (C1’s day arithmetic). ⭐⭐⭐ Transfer: change a patient’s ward with a guard-chain (must exist; source ≠ target; target ward capacity 10).
Lab 6 — Course Registration Data
Scenario. The registrar’s end-of-day job: courses, registrations, and the enrolment report. Two record arrays cooperating — the C5 join, operationalized.
Requirements.
R1. Schemas: [E17’s Course] + Registration { int studentId; std::string courseCode; Date when; }. Course codes are canonical uppercase (enforce at add: “cs101” → “CS101”).
R2. Menu: 1-add course · 2-register (valid course code + unique (studentId, courseCode) pair — no double registration) · 3-drop (remove one registration) · 4-report (per course: code, title, level-text, count, capacity-filler %) · 5-course list for one student (by id) · 6-quit.
R3. Capacity: registration refused when the course is full (Course has int capacity;) — the guard prints both numbers.
R4. The report orders by count descending (the C5 index sort) and totals: courses, seats, taken, fill % (1 dp, cast rule).
Test data.
| Courses (cap 2 each) | Registrations | Expected |
|---|---|---|
| CS101 (Beginner), CS201 (Advanced) | 1→CS101, 2→CS101, 1→CS201 | counts 2 and 1 |
| register 2→CS201, then 3→CS201 | second refused: full (2/2) | |
| register 1→CS101 again | refused: duplicate pair | |
| register “cs201” | canonicalized → CS201, then full-refused | |
| student 1’s list | CS101, CS201 | |
| drop 1→CS101, re-report | CS101 count 1 |
Student tasks.
- Both schemas +
levelText+findCourse(code → index). - The double gate on register: course exists → capacity → duplicate pair. Write the guard chain as three refusals with three distinct messages (the guard-chain discipline).
- Canonicalize-on-entry: one
toUpperCode()helper (Unit 11) applied at add and register — the data stays clean, not the queries. - Reflection: why is
(studentId, courseCode)uniqueness a pair rule, and which loop shape checks it? (Nested scan: for each registration, does an earlier one match both fields?)
Solution (core).
bool registerStudent(Registration regs[], int& regCount, const Course courses[], int courseCount,
int studentId, const std::string& rawCode) {
std::string code = toUpperCode(rawCode);
int ci = findCourse(courses, courseCount, code);
if (ci == -1) { std::cout << "no such course\n"; return false; }
if (countFor(regs, regCount, code) >= courses[ci].capacity) {
std::cout << "full (" << countFor(regs, regCount, code)
<< "/" << courses[ci].capacity << ")\n";
return false;
}
for (int r = 0; r < regCount; r = r + 1)
if (regs[r].studentId == studentId && regs[r].courseCode == code) {
std::cout << "already registered\n"; return false;
}
regs[regCount] = {studentId, code, readDate()};
regCount += 1;
return true;
}
Explanation. The lab’s design lesson: two record types are joined by a key, not by indices — courseCode is the string foreign key, and every operation (count, fill, list-for-student) is a traversal that matches on it. The three-refusal guard chain is the requirements section made executable; the canonicalization at entry is the data-cleaning principle that keeps every later comparison honest (the test table’s lowercase case). Fill % = taken/capacity with the cast: 2/2 → 100.0%.
⭐ Extensions. ⭐ Waitlist record (Waiting { studentId; courseCode; Date when; }) promoted FIFO when a drop opens a seat — the queue pattern from the iteration module with records. ⭐⭐⭐ Timetable clash check: a student can’t register for two courses meeting at the same Time slot (add int slot; to Course — a third join dimension).
Lab index
| Lab | Schema focus | The pattern it drills |
|---|---|---|
| 1 | Student Records | derived enum + single-source bands; the one-move sort |
| 2 | Employee Records | nesting + comparator functions; derive-don’t-store |
| 3 | Product Inventory | cached state with one updater door; validate-then-mutate |
| 4 | Library Books | guarded state machine + key lookup (−1 index convention) |
| 5 | Patient Records | enum-driven grouping + contiguity discipline |
| 6 | Course Registration | two record types joined by a key; triple guard chain |
After the labs: The Student Record Management System — the capstone that assembles schemas, state machines, and joins into one program.