Case Study 1: Designing a Student Information System
Context
GradeBook Pro has been growing steadily throughout this course. We have built input routines, computed averages, and worked with arrays of grades. But so far, student data has been scattered across parallel arrays — one for names, one for IDs, one for grades. It is time to redesign GradeBook Pro using records to create a proper Student Information System (SIS) that models students, courses, and enrollment as coherent data structures.
This case study walks through the design process from requirements to working code, illustrating how records transform a fragile, parallel-array program into a well-structured system.
The Requirements
Our Student Information System needs to track:
- Students — Each student has a name, a unique ID, an email address, a GPA, and an enrollment status.
- Courses — Each course has a code (like "CS101"), a title, a number of credits, and a maximum enrollment.
- Enrollments — A student can be enrolled in multiple courses, and each enrollment has an associated grade.
- Grade calculation — The system should compute a student's GPA from their course grades and credit hours.
Step 1: Identify the Entities
Before writing any code, we identify the core entities and their attributes. This is data modeling — deciding what record types we need and what fields each should contain.
| Entity | Attributes |
|---|---|
| Student | Name, ID, Email, GPA, Enrolled (active/inactive), list of enrollments |
| Course | Code, Title, Credits, MaxStudents, CurrentStudents |
| Enrollment | CourseCode, Grade (A=4, B=3, C=2, D=1, F=0), Credits |
Step 2: Define the Record Types
We work bottom-up, defining simpler types first since more complex types depend on them.
type
TLetterGrade = (lgA, lgB, lgC, lgD, lgF, lgNone);
TEnrollment = record
CourseCode: string[10];
Grade: TLetterGrade;
Credits: Integer;
end;
TStudent = record
Name: string;
ID: Integer;
Email: string;
GPA: Real;
Active: Boolean;
Enrollments: array[1..8] of TEnrollment;
NumEnrollments: Integer;
end;
TCourse = record
Code: string[10];
Title: string;
Credits: Integer;
MaxStudents: Integer;
CurrentStudents: Integer;
end;
Design decisions explained:
TLetterGradeis an enumerated type, not a string. This prevents typos and enablescasestatements.TEnrollmentis a separate record type. It could have been embedded directly inTStudent, but making it its own type enables us to write procedures that operate on single enrollments.TStudentnests an array ofTEnrollmentrecords. Each student can enroll in up to 8 courses.NumEnrollmentstracks how many slots are in use.string[10]for course codes limits the field to 10 characters, saving memory compared to a fullstring(which is 255 characters by default in short-string mode).
Step 3: Core Operations
Computing GPA
GPA is a weighted average: sum of (grade points * credits) divided by total credits.
function GradePoints(G: TLetterGrade): Real;
begin
case G of
lgA: GradePoints := 4.0;
lgB: GradePoints := 3.0;
lgC: GradePoints := 2.0;
lgD: GradePoints := 1.0;
lgF: GradePoints := 0.0;
lgNone: GradePoints := 0.0;
end;
end;
procedure CalculateGPA(var Student: TStudent);
var
I: Integer;
TotalPoints, TotalCredits: Real;
begin
TotalPoints := 0.0;
TotalCredits := 0.0;
for I := 1 to Student.NumEnrollments do
if Student.Enrollments[I].Grade <> lgNone then
begin
TotalPoints := TotalPoints +
GradePoints(Student.Enrollments[I].Grade) *
Student.Enrollments[I].Credits;
TotalCredits := TotalCredits + Student.Enrollments[I].Credits;
end;
if TotalCredits > 0 then
Student.GPA := TotalPoints / TotalCredits
else
Student.GPA := 0.0;
end;
Notice that we pass the student by var because we are modifying the GPA field. Courses with grade lgNone (not yet graded) are excluded from the calculation.
Enrolling a Student in a Course
function EnrollStudent(var Student: TStudent; const Course: TCourse): Boolean;
begin
EnrollStudent := False;
if Student.NumEnrollments >= 8 then
begin
WriteLn('Error: Student is already enrolled in 8 courses.');
Exit;
end;
if not Student.Active then
begin
WriteLn('Error: Student is not active.');
Exit;
end;
Inc(Student.NumEnrollments);
Student.Enrollments[Student.NumEnrollments].CourseCode := Course.Code;
Student.Enrollments[Student.NumEnrollments].Grade := lgNone;
Student.Enrollments[Student.NumEnrollments].Credits := Course.Credits;
EnrollStudent := True;
end;
The function returns Boolean to indicate success or failure, allowing the caller to respond appropriately.
Assigning a Grade
procedure AssignGrade(var Student: TStudent;
const CourseCode: string;
NewGrade: TLetterGrade);
var
I: Integer;
Found: Boolean;
begin
Found := False;
for I := 1 to Student.NumEnrollments do
if Student.Enrollments[I].CourseCode = CourseCode then
begin
Student.Enrollments[I].Grade := NewGrade;
Found := True;
Break;
end;
if not Found then
WriteLn('Error: Student is not enrolled in ', CourseCode)
else
CalculateGPA(Student); { Recalculate GPA after grade change }
end;
After assigning a grade, we immediately recalculate the student's GPA. This keeps the data consistent — the GPA is never stale.
Displaying a Student Transcript
procedure PrintTranscript(const Student: TStudent);
var
I: Integer;
GradeStr: string;
begin
WriteLn('==========================================');
WriteLn('STUDENT TRANSCRIPT');
WriteLn('==========================================');
WriteLn('Name: ', Student.Name);
WriteLn('ID: ', Student.ID);
WriteLn('Email: ', Student.Email);
WriteLn('Status: ', BoolToStr(Student.Active, 'Active', 'Inactive'));
WriteLn('GPA: ', Student.GPA:0:2);
WriteLn;
WriteLn('Course':10, 'Credits':10, 'Grade':10);
WriteLn(StringOfChar('-', 30));
for I := 1 to Student.NumEnrollments do
begin
case Student.Enrollments[I].Grade of
lgA: GradeStr := 'A';
lgB: GradeStr := 'B';
lgC: GradeStr := 'C';
lgD: GradeStr := 'D';
lgF: GradeStr := 'F';
lgNone: GradeStr := '--';
end;
WriteLn(Student.Enrollments[I].CourseCode:10,
Student.Enrollments[I].Credits:10,
GradeStr:10);
end;
WriteLn('==========================================');
end;
This procedure uses const because it only reads the student data.
Step 4: Putting It Together
var
Students: array[1..50] of TStudent;
Courses: array[1..20] of TCourse;
NumStudents, NumCourses: Integer;
A complete session might look like this:
{ Initialize a student }
Students[1].Name := 'Priya Sharma';
Students[1].ID := 20260001;
Students[1].Email := 'psharma@university.edu';
Students[1].Active := True;
Students[1].NumEnrollments := 0;
{ Set up courses }
Courses[1].Code := 'CS101';
Courses[1].Title := 'Intro to Computer Science';
Courses[1].Credits := 3;
Courses[2].Code := 'MATH201';
Courses[2].Title := 'Calculus II';
Courses[2].Credits := 4;
Courses[3].Code := 'ENG102';
Courses[3].Title := 'Composition';
Courses[3].Credits := 3;
{ Enroll and grade }
EnrollStudent(Students[1], Courses[1]);
EnrollStudent(Students[1], Courses[2]);
EnrollStudent(Students[1], Courses[3]);
AssignGrade(Students[1], 'CS101', lgA);
AssignGrade(Students[1], 'MATH201', lgB);
AssignGrade(Students[1], 'ENG102', lgA);
PrintTranscript(Students[1]);
Output:
==========================================
STUDENT TRANSCRIPT
==========================================
Name: Priya Sharma
ID: 20260001
Email: psharma@university.edu
Status: Active
GPA: 3.70
Course Credits Grade
------------------------------
CS101 3 A
MATH201 4 B
ENG102 3 A
==========================================
The GPA is (43 + 34 + 4*3) / (3+4+3) = 36/10 = 3.60. Wait — let us recalculate: A in CS101 (4.0 * 3 = 12), B in MATH201 (3.0 * 4 = 12), A in ENG102 (4.0 * 3 = 12). Total points = 36, total credits = 10, GPA = 3.60. (The output would show 3.60, not 3.70 — this is a good reminder to always verify computed values.)
Lessons Learned
-
Records make data modeling natural. Each entity in our domain (student, course, enrollment) maps to a record type. The code mirrors the problem.
-
Nested records handle relationships. A student's enrollments are inside the student record, making it impossible for enrollment data to become separated from student data.
-
Enumerated types improve reliability. Using
TLetterGradeinstead of strings eliminates typos and enables exhaustivecasestatements. The compiler warns if we forget a case. -
constparameters document intent. WhenPrintTranscripttakesconst Student: TStudent, readers immediately know the function is read-only. -
Keeping derived data consistent. GPA is derived from grades. By recalculating it whenever a grade changes, we ensure it is never stale. An alternative design would compute GPA on demand rather than storing it.
Your Turn
- Add a
DropCourseprocedure that removes an enrollment from a student (shift remaining enrollments down) and recalculates the GPA. - Add a
FindStudentfunction that searches the array by ID. - Add a
ClassListprocedure that takes a course code and the students array, and prints all students enrolled in that course. - Consider: should
GPAbe stored in the record, or computed on demand? What are the trade-offs?