Case Study 2: GradeBook Pro — First Version
The Scenario
Ms. Alvarez teaches Introduction to Biology at Riverside Community College. She has a class of up to 30 students and needs a program that can:
- Store each student's name, numeric grade (0-100), and days of attendance (out of 40 class sessions).
- Calculate the class average, highest grade, and lowest grade.
- Display an honor roll (students with grades 90 or above).
- Show a grade distribution: how many A's, B's, C's, D's, and F's.
- Identify students at risk of failing due to poor attendance (below 75% of sessions).
- Display a formatted class report.
This is the first version of GradeBook Pro, our anchor example for Part II. In this chapter, we use parallel arrays. In Chapter 10, we will refactor it to use records. In later chapters, we will add file I/O, sorting, and a menu-driven interface.
Data Architecture: Parallel Arrays
We represent the class using three parallel arrays plus a count:
program GradeBookPro;
const
MAX_STUDENTS = 30;
TOTAL_SESSIONS = 40;
HONOR_THRESHOLD = 90;
ATTENDANCE_RISK = 0.75; { 75% attendance threshold }
type
TNameArray = array[1..MAX_STUDENTS] of String;
TGradeArray = array[1..MAX_STUDENTS] of Integer;
TAttendArray = array[1..MAX_STUDENTS] of Integer; { days present }
var
names: TNameArray;
grades: TGradeArray;
attendance: TAttendArray;
count: Integer; { actual number of students }
The parallel structure means that names[5], grades[5], and attendance[5] all refer to the same student — the fifth one entered. This correspondence is critical and fragile. If any operation rearranges one array without rearranging the others, the data becomes corrupted.
Input: Reading Student Data
procedure ReadStudents(var n: TNameArray;
var g: TGradeArray;
var a: TAttendArray;
var cnt: Integer);
var
inputName: String;
inputGrade, inputAttend: Integer;
begin
cnt := 0;
WriteLn('Enter student data. Type an empty name to stop.');
WriteLn;
repeat
Write('Student name (or Enter to stop): ');
ReadLn(inputName);
if inputName = '' then
Break;
if cnt >= MAX_STUDENTS then
begin
WriteLn('Maximum of ', MAX_STUDENTS, ' students reached.');
Break;
end;
{ Validate grade }
repeat
Write(' Grade (0-100): ');
ReadLn(inputGrade);
if (inputGrade < 0) or (inputGrade > 100) then
WriteLn(' Invalid. Please enter a value between 0 and 100.');
until (inputGrade >= 0) and (inputGrade <= 100);
{ Validate attendance }
repeat
Write(' Days attended (0-', TOTAL_SESSIONS, '): ');
ReadLn(inputAttend);
if (inputAttend < 0) or (inputAttend > TOTAL_SESSIONS) then
WriteLn(' Invalid. Please enter a value between 0 and ', TOTAL_SESSIONS, '.');
until (inputAttend >= 0) and (inputAttend <= TOTAL_SESSIONS);
Inc(cnt);
n[cnt] := inputName;
g[cnt] := inputGrade;
a[cnt] := inputAttend;
WriteLn;
until False; { loop exits via Break }
end;
Notice the input validation loops. Real programs cannot assume the user will provide valid data. Each nested repeat..until ensures the value is within the acceptable range before storing it.
Analysis Functions
Class Average
function ClassAverage(const g: TGradeArray; cnt: Integer): Real;
var
i, total: Integer;
begin
if cnt = 0 then
begin
ClassAverage := 0.0;
Exit;
end;
total := 0;
for i := 1 to cnt do
total := total + g[i];
ClassAverage := total / cnt;
end;
Finding Extremes
procedure FindExtremes(const g: TGradeArray;
cnt: Integer;
var highIdx, lowIdx: Integer);
var
i: Integer;
begin
highIdx := 1;
lowIdx := 1;
for i := 2 to cnt do
begin
if g[i] > g[highIdx] then
highIdx := i;
if g[i] < g[lowIdx] then
lowIdx := i;
end;
end;
This returns indices rather than values, so we can look up the corresponding student name. This is the key advantage of FindMaxIndex over FindMax.
Honor Roll
procedure DisplayHonorRoll(const n: TNameArray;
const g: TGradeArray;
cnt: Integer);
var
i, honorCount: Integer;
begin
WriteLn;
WriteLn('--- HONOR ROLL (', HONOR_THRESHOLD, '+) ---');
honorCount := 0;
for i := 1 to cnt do
if g[i] >= HONOR_THRESHOLD then
begin
WriteLn(' ', n[i]:20, ' ', g[i]:3);
Inc(honorCount);
end;
if honorCount = 0 then
WriteLn(' No students qualify for the honor roll.')
else
WriteLn(' (', honorCount, ' student(s) on honor roll)');
end;
Grade Distribution
procedure DisplayDistribution(const g: TGradeArray; cnt: Integer);
var
i: Integer;
dist: array['A'..'F'] of Integer;
ch: Char;
maxCount: Integer;
bar: String;
begin
for ch := 'A' to 'F' do
dist[ch] := 0;
for i := 1 to cnt do
case g[i] of
90..100: Inc(dist['A']);
80..89: Inc(dist['B']);
70..79: Inc(dist['C']);
60..69: Inc(dist['D']);
else
Inc(dist['F']);
end;
{ Find maximum for scaling the bar chart }
maxCount := 0;
for ch := 'A' to 'F' do
if (ch <> 'E') and (dist[ch] > maxCount) then
maxCount := dist[ch];
WriteLn;
WriteLn('--- GRADE DISTRIBUTION ---');
for ch := 'A' to 'F' do
begin
if ch = 'E' then Continue; { skip E — not a letter grade }
Write(' ', ch, ' (');
case ch of
'A': Write('90-100');
'B': Write('80-89 ');
'C': Write('70-79 ');
'D': Write('60-69 ');
'F': Write(' 0-59 ');
end;
Write('): ');
{ Draw bar }
bar := '';
for i := 1 to dist[ch] do
bar := bar + '#';
Write(bar:15);
WriteLn(' [', dist[ch], ']');
end;
end;
This combines the grade distribution from Section 9.9 with a simple horizontal bar chart. The character-indexed dist array makes the code natural and readable.
Attendance Risk Report
procedure DisplayAttendanceRisk(const n: TNameArray;
const a: TAttendArray;
cnt: Integer);
var
i, riskCount: Integer;
pct: Real;
begin
WriteLn;
WriteLn('--- ATTENDANCE RISK REPORT ---');
WriteLn(' Students below ', (ATTENDANCE_RISK * 100):0:0,
'% attendance (', Round(ATTENDANCE_RISK * TOTAL_SESSIONS),
'/', TOTAL_SESSIONS, ' sessions):');
WriteLn;
riskCount := 0;
for i := 1 to cnt do
begin
pct := a[i] / TOTAL_SESSIONS;
if pct < ATTENDANCE_RISK then
begin
WriteLn(' ', n[i]:20, ' ',
a[i], '/', TOTAL_SESSIONS, ' sessions (',
(pct * 100):5:1, '%)');
Inc(riskCount);
end;
end;
if riskCount = 0 then
WriteLn(' All students meet the attendance threshold.')
else
WriteLn(' (', riskCount, ' student(s) at risk)');
end;
Full Report
procedure DisplayFullReport(const n: TNameArray;
const g: TGradeArray;
const a: TAttendArray;
cnt: Integer);
var
avg: Real;
highIdx, lowIdx, i: Integer;
begin
WriteLn;
WriteLn('========================================');
WriteLn(' GRADEBOOK PRO — CLASS REPORT');
WriteLn('========================================');
WriteLn;
WriteLn('Students enrolled: ', cnt);
if cnt = 0 then
begin
WriteLn('No students to report.');
Exit;
end;
{ Class roster }
WriteLn;
WriteLn('--- CLASS ROSTER ---');
WriteLn(' Name':22, 'Grade':7, 'Attend':8, 'Pct':7);
WriteLn(' ', StringOfChar('-', 42));
for i := 1 to cnt do
WriteLn(' ', n[i]:20, g[i]:7,
a[i]:5, '/', TOTAL_SESSIONS,
(a[i] / TOTAL_SESSIONS * 100):6:1, '%');
{ Statistics }
avg := ClassAverage(g, cnt);
FindExtremes(g, cnt, highIdx, lowIdx);
WriteLn;
WriteLn('--- CLASS STATISTICS ---');
WriteLn(' Class average: ', avg:0:1);
WriteLn(' Highest grade: ', g[highIdx], ' (', n[highIdx], ')');
WriteLn(' Lowest grade: ', g[lowIdx], ' (', n[lowIdx], ')');
WriteLn(' Grade range: ', g[highIdx] - g[lowIdx], ' points');
{ Sections }
DisplayHonorRoll(n, g, cnt);
DisplayDistribution(g, cnt);
DisplayAttendanceRisk(n, a, cnt);
end;
Sample Session
Enter student data. Type an empty name to stop.
Student name (or Enter to stop): Alice Chen
Grade (0-100): 95
Days attended (0-40): 38
Student name (or Enter to stop): Bob Martinez
Grade (0-100): 78
Days attended (0-40): 35
Student name (or Enter to stop): Carol Washington
Grade (0-100): 62
Days attended (0-40): 22
Student name (or Enter to stop): David Kim
Grade (0-100): 88
Days attended (0-40): 39
Student name (or Enter to stop): Eva Johansson
Grade (0-100): 91
Days attended (0-40): 37
Student name (or Enter to stop):
========================================
GRADEBOOK PRO — CLASS REPORT
========================================
Students enrolled: 5
--- CLASS ROSTER ---
Name Grade Attend Pct
------------------------------------------
Alice Chen 95 38/40 95.0%
Bob Martinez 78 35/40 87.5%
Carol Washington 62 22/40 55.0%
David Kim 88 39/40 97.5%
Eva Johansson 91 37/40 92.5%
--- CLASS STATISTICS ---
Class average: 82.8
Highest grade: 95 (Alice Chen)
Lowest grade: 62 (Carol Washington)
Grade range: 33 points
--- HONOR ROLL (90+) ---
Alice Chen 95
Eva Johansson 91
(2 student(s) on honor roll)
--- GRADE DISTRIBUTION ---
A (90-100): ## [2]
B (80-89 ): # [1]
C (70-79 ): # [1]
D (60-69 ): # [1]
F ( 0-59 ): [0]
--- ATTENDANCE RISK REPORT ---
Students below 75% attendance (30/40 sessions):
Carol Washington 22/40 sessions ( 55.0%)
(1 student(s) at risk)
Design Analysis
What Works Well
-
Separation of concerns. Each procedure handles one task.
ClassAverageonly computes the average;DisplayHonorRollonly displays the honor roll. This makes each piece testable and reusable. -
Constants for configuration.
MAX_STUDENTS,TOTAL_SESSIONS,HONOR_THRESHOLD, andATTENDANCE_RISKare all constants. Changing the honor threshold from 90 to 85 requires editing one line. -
Named types for safety.
TNameArray,TGradeArray, andTAttendArrayprevent accidentally passing grades where names are expected. -
Input validation. The program rejects invalid grades and attendance values immediately, before they can corrupt the data.
What We Will Improve
-
Parallel arrays are fragile. We must always pass
names,grades, andattendancetogether — and ensure they stay synchronized. In Chapter 10, we will replace them with anarray of TStudent, where each record groups a student's name, grade, and attendance. -
No persistent storage. When the program ends, all data is lost. Chapter 12 (Files) will add the ability to save and load gradebook data.
-
No sorting. We cannot display students sorted by grade or by name. Chapter 11 or a later chapter on sorting algorithms will add this capability.
-
Fixed capacity. The 30-student limit wastes memory for small classes and cannot accommodate large ones. Dynamic arrays of records will solve this in Chapter 10.
Key Takeaways from GradeBook Pro v1
- Parallel arrays work for prototyping but become a liability as programs grow. They are a stepping stone to records.
- The algorithms are independent of the application.
FindExtremes,ClassAverage, and the distribution histogram are generic patterns that apply to any dataset. - Character-indexed arrays (like
dist['A'..'F']) produce remarkably clean code when the problem domain maps naturally to characters. - Constants and named types make the program configurable and type-safe — two qualities that scale with program complexity.
- Input validation is not optional. Real users will enter "abc" when you expect a number, -5 when you expect a positive value, and 200 when the maximum is 100. Handle it.
GradeBook Pro will return in every chapter of Part II, evolving from this parallel-array prototype into a polished, record-based, file-backed application with sorting and search capabilities.