LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
SwiftBeginner~1.5 hours

Student Grading System

Process a list of students with optional marks and produce grade summaries using guard let and enums.

OptionalsEnumsFunctions

Overview

Not every student has been graded yet, and Swift has a specific, type-checked way to represent "this value might genuinely not exist": the Optional. A Student whose marks field is typed Int? instead of plain Int is not a workaround — it is the compiler forcing every piece of code that reads marks to explicitly decide what "no marks yet" means for that particular operation, rather than smuggling a sentinel value like -1 through the program and hoping every reader remembers what it means.

By the end of this tutorial you will have a console application that stores a roster of Student values with optional marks, an enum Grade that turns a numeric score into a closed set of letter grades, and a report generator that safely unwraps every optional along the way — using guard let to handle the "ungraded" case early, and compactMap to compute a class average over only the students who actually have a score.

What You'll Build
  • A Student struct with a genuinely optional marks: Int? field.
  • A Grade enum with raw String values mapping scores to letter grades.
  • A gradeForMarks(_:) function using a switch over numeric ranges.
  • A summary(for:) function that uses guard let to handle ungraded students.
  • A classAverage(_:) function using compactMap and reduce to average only recorded scores.
  • A readLine()-driven menu loop for adding students and generating the report.

Prerequisites

  • Structs — stored properties, let vs. var, and struct initializers.
  • Optionals — Int?, guard let, if let, and nil-coalescing with ??.
  • Enums — enum cases and raw values (enum Grade: String).
  • Functions — parameters, return types, and calling one function from another.
  • Collection basics — Array, for-in, and passing an [Student] into a function.

Project Structure

The program lives in a single file, main.swift. Student is a plain data struct with no behavior of its own. Grade is a raw-value enum — a closed, compiler-checked set of the only letter grades that can ever exist in this program, which rules out an entire category of bugs a plain String field could introduce (typos like "Aplus" simply cannot compile as a Grade case). gradeForMarks(_:), summary(for:), classAverage(_:), and printReport(_:) are free functions — none of them need to be methods on Student, because none of them depend on any single instance's private state; they each just take the data they need as a parameter and return a result.

Step 1: Define the Student Struct With Optional Marks

marks is typed Int?, not Int, because nil here does not mean zero — it means this student has not been graded yet. Modeling the "ungraded" state as a real Optional, instead of a magic number, is what forces the rest of the program to handle it explicitly instead of accidentally treating an ungraded student as having scored 0.

// marks is a genuine Optional here — nil doesn't mean "zero," it means "this
// student hasn't been graded yet." Modeling that as Int? instead of using a
// sentinel value like -1 is what forces every piece of code that reads marks
// to explicitly decide what "ungraded" means for that operation.
struct Student {
let name: String
let marks: Int? // nil = not yet graded; 0...100 = an actual recorded score
}

Step 2: Define the Grade Enum

A raw-value enum gives each case a fixed underlying value — here, a String — that can be read back with .rawValue, which is exactly what a display label needs. Using an enum instead of a plain String field makes "what are the valid grades" a closed set the compiler checks, rather than an open-ended collection of string literals that could drift or typo anywhere they are used.

// A raw-value enum: each case carries a fixed String ("A+", "A", ...) read
// back with .rawValue. An enum is the right tool here instead of a plain
// String, because it makes "what are the valid grades" a closed,
// compiler-checked set instead of an open-ended set of magic strings.
enum Grade: String {
case aPlus = "A+"
case a = "A"
case b = "B"
case c = "C"
case fail = "Fail"
}

Step 3: Map a Score to a Grade

gradeForMarks(_:) is a free function, not a method on anything, because it has no dependency on instance state — it just takes a score and returns a Grade. Swift's switch supports matching against numeric ranges directly: 90...100 is a closed range including both endpoints, while 75..<90 is a half-open range that excludes 90, which is exactly what is needed so the boundaries between grades never overlap or leave a gap.

// A plain function: it takes a score and returns a Grade with no side
// effects and no dependency on any instance state, so it has no reason to
// live on Student or Grade as a method.
func gradeForMarks(_ marks: Int) -> Grade {
switch marks {
case 90...100: return .aPlus // Closed range: 90 through 100 inclusive
case 75..<90: return .a // Half-open range: 75 up to (not including) 90
case 60..<75: return .b
case 40..<60: return .c
default: return .fail // Anything else, including an invalid score below 0, counts as a fail
}
}
Example Usage

Click Run to see what this code prints.

Step 4: Summarize One Student With guard let

guard let is preferred here over if let because the "no marks" case is the exceptional path that should return its own message immediately — everything after the guard statement can then safely assume marks is a real, unwrapped Int, with no further nil-checking needed for the rest of the function.

// guard let is preferred over if let here because the "no marks" case is the
// exceptional path that should exit early with its own message — the rest of
// the function can then safely assume marks is a real, unwrapped Int.
func summary(for student: Student) -> String {
guard let marks = student.marks else {
return "\(student.name): No marks recorded yet"
}
let grade = gradeForMarks(marks)
return "\(student.name): \(marks)/100 -> Grade \(grade.rawValue)"
}

Step 5: Compute the Class Average With compactMap

compactMap runs its closure over every element and keeps only the results that are not nil, unwrapping them in the same step — "give me every marks value that actually exists" in one line, instead of a manual loop with an if let and an accumulator array. reduce(0, +) then folds that array of unwrapped scores down into a single sum before dividing by the count.

// compactMap runs the closure over every element and keeps only the non-nil
// results, unwrapping them in the process — exactly "give me every marks
// value that actually exists," in one line instead of a manual loop with an
// if-let and an accumulator array.
func classAverage(_ students: [Student]) -> Double {
let recorded = students.compactMap { $0.marks }
guard !recorded.isEmpty else { return 0 } // Avoid dividing by zero when nobody has been graded yet
let total = recorded.reduce(0, +) // reduce folds the array into a single sum
return Double(total) / Double(recorded.count)
}
func printReport(_ students: [Student]) {
print("\n--- Grade Report ---")
for student in students {
print(summary(for: student))
}
let average = classAverage(students)
print(String(format: "Class average (graded students only): %.1f", average))
}

Step 6: Build the Menu Loop

When adding a student, an empty or unreadable marks line is turned into nil explicitly with a ternary, rather than trying (and failing) to parse it as a number — Int("") also returns nil, so both "left blank" and "typed something non-numeric" naturally collapse into the same "not graded yet" state that Student.marks already models.

var students: [Student] = []
var running = true
while running {
print("""
===== STUDENT GRADING SYSTEM =====
1. Add Student
2. Generate Report
3. Exit
""")
print("Enter your choice: ", terminator: "")
guard let choiceLine = readLine(), let choice = Int(choiceLine) else {
print("Invalid input, try again.")
continue
}
switch choice {
case 1:
print("Enter student name: ", terminator: "")
let name = readLine() ?? "Unnamed Student" // Nil-coalescing: a missing line still produces a usable student
print("Enter marks, or leave blank if not graded yet: ", terminator: "")
let marksLine = readLine() ?? ""
// Int("") returns nil, and so does Int() on anything non-numeric — both
// naturally become "not graded yet" with no extra special-casing needed.
let marks = marksLine.isEmpty ? nil : Int(marksLine)
students.append(Student(name: name, marks: marks))
print("Student added.")
case 2:
printReport(students)
case 3:
print("Goodbye!")
running = false
default:
print("Invalid choice, try again.")
}
}

Complete Code

Here is the full program assembled in the correct order, ready to save as main.swift and run with swift main.swift, or paste into a Swift Playground.

struct Student {
let name: String
let marks: Int?
}
enum Grade: String {
case aPlus = "A+"
case a = "A"
case b = "B"
case c = "C"
case fail = "Fail"
}
func gradeForMarks(_ marks: Int) -> Grade {
switch marks {
case 90...100: return .aPlus
case 75..<90: return .a
case 60..<75: return .b
case 40..<60: return .c
default: return .fail
}
}
func summary(for student: Student) -> String {
guard let marks = student.marks else {
return "\(student.name): No marks recorded yet"
}
let grade = gradeForMarks(marks)
return "\(student.name): \(marks)/100 -> Grade \(grade.rawValue)"
}
func classAverage(_ students: [Student]) -> Double {
let recorded = students.compactMap { $0.marks }
guard !recorded.isEmpty else { return 0 }
let total = recorded.reduce(0, +)
return Double(total) / Double(recorded.count)
}
func printReport(_ students: [Student]) {
print("\n--- Grade Report ---")
for student in students {
print(summary(for: student))
}
let average = classAverage(students)
print(String(format: "Class average (graded students only): %.1f", average))
}
var students: [Student] = []
var running = true
while running {
print("""
===== STUDENT GRADING SYSTEM =====
1. Add Student
2. Generate Report
3. Exit
""")
print("Enter your choice: ", terminator: "")
guard let choiceLine = readLine(), let choice = Int(choiceLine) else {
print("Invalid input, try again.")
continue
}
switch choice {
case 1:
print("Enter student name: ", terminator: "")
let name = readLine() ?? "Unnamed Student"
print("Enter marks, or leave blank if not graded yet: ", terminator: "")
let marksLine = readLine() ?? ""
let marks = marksLine.isEmpty ? nil : Int(marksLine)
students.append(Student(name: name, marks: marks))
print("Student added.")
case 2:
printReport(students)
case 3:
print("Goodbye!")
running = false
default:
print("Invalid choice, try again.")
}
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a Subject enum and track per-subject marks in a [Subject: Int] dictionary instead of one overall score.
  • Add a retest(name:newMarks:) function that replaces a student's marks and re-runs the report.
  • Persist the roster to a JSON file with Codable so grades survive between runs.
  • Add a topStudent(_:) function using max(by:) over students with non-nil marks.
  • Give Grade a computed var passing: Bool property and report a pass/fail count alongside the average.

Summary

You built a grading system where "no marks yet" is a real, type-checked state instead of a magic number, and guard let turns handling that state into an explicit early exit rather than a hidden edge case. The Grade enum kept every possible letter grade to a closed, compiler-verified set, and compactMap let the class average skip ungraded students without a single manual nil-check in the averaging logic itself. That combination — optionals for "might not exist," enums for "one of a fixed set," and small functions that lean on both — is the backbone of idiomatic Swift data processing.