SDD20 — Mini-project 2: Arrays of Records and File I/O
- I can apply the full analysis → design → implementation → testing → evaluation cycle to a program combining arrays of records with file handling
- I can read records from a file, process them using a standard algorithm, and write updated records back to the file
- I can identify functional requirements for a problem involving stored, structured data
- I can design a structure diagram showing file input/output alongside function and procedure calls
- I can implement reading records from a file, searching them with a standard algorithm, updating a record, and writing the results back
- I can build a test plan covering normal, extreme, and erroneous data, including file-related edge cases
- I can evaluate the finished program against all five evaluation criteria
Key vocabulary
Combining records and file handling — the second full cycle
Why this lesson builds on SDD19, not replaces it
SDD19 deliberately kept its scenario narrow — one array, no records, no files — so the five-stage cycle itself could be learned without extra complexity. This lesson adds back exactly what SDD19 left out: arrays of records (SDD6, SDD15) and file handling (SDD11, SDD12), combined together in one program. This combination — reading records from a file, searching or processing them with a standard algorithm, and writing the changed records back — is the closest rehearsal this course has for the actual assignment (Task 1, 25 marks, 6 supervised hours, Feb–Mar 2027), which expects exactly this kind of structured, persistent, file-backed program.
The problem for this lesson
A school library wants a small program to manage its book records: each book has a title, an author, and a number of copies available. The records are stored in a CSV file so they persist between runs. The program should read the records, find a specific book by title using a standard algorithm, issue a loan by reducing that book's copies by one (only if a copy is actually available), and then write the updated records back to the file so the change is saved.
Stage 1 — Analysis
The functional requirements for this problem: read an array of book records (title, author, copies) from a CSV file; find a specific book by title; reduce a found book's copy count by one when a loan is issued, but only if copies are available; and write the updated records back to the file so the change persists. Each of these becomes a specific, testable requirement — notably, "only if copies are available" is itself a functional requirement, not just a nice-to-have, since a program that lets copies go negative has failed this requirement even if it never crashes.
Stage 2 — Design
The design now needs to show file input and output alongside the function/procedure calls already familiar from SDD19: readBooks(filename) (a function, reads the file and returns an array of records); findBookByTitle(books, title) (a function, the standard linear search algorithm reused from SDD13/15, now searching a record field); issueLoan(books, index) (a procedure, updates a record's field in place); and writeBooks(books, filename) (a procedure, writes the array back to the file). Sketching this data flow — file → array of records → search → update → file — before coding prevents a common design gap: forgetting that writeBooks needs to run after issueLoan, not before, or the change is never actually saved.
Stage 3 — Implementation
Implementation combines three previously separate skills into one program: file reading (SDD11) builds the array of records; the linear search standard algorithm (SDD13, extended to records exactly as in SDD15) finds the right one; and file writing (SDD12) saves the result. The record itself is implemented as an @dataclass, consistent with every records lesson since SDD6. Note that issueLoan modifies a record's field directly (books[index].copies = books[index].copies - 1) — this only works correctly because findBookByTitle returned an index, not a copy of the record, exactly the same design point SDD15 made about minimum/maximum-finding on records.
Stage 4 — Testing
Testing this program needs the same three categories as every previous testing lesson (SDD16), but now with file-specific cases included: a normal case (a valid title with copies available); an extreme case (a valid title with exactly zero copies available, testing the "only if available" requirement at its boundary); and an erroneous case specific to file handling, such as an empty file, which SDD11's reading pattern is not designed to survive. A test plan that only tests "does it find the book" without testing the zero-copies boundary or the empty-file case would miss exactly the kind of requirement this problem depends on.
Stage 5 — Evaluation
Evaluation applies SDD18's five criteria to the combined program: fitness for purpose (does it correctly read, find, update, and save records); efficient use of coding constructs (is the standard linear search algorithm used appropriately, rather than something needlessly complex); usability (are loan confirmations and "no copies available" messages clear); maintainability (are the four sub-programs clearly named and each doing one job); and robustness (does the program survive an empty or malformed file, and does it correctly refuse a loan when no copies are available). This is a genuinely larger evaluation than SDD19's, since there are now four sub-programs and a file dependency to judge, not three sub-programs operating purely in memory.
Worked examples
- FR1 — Read an array of book records (title, author, copies) from a CSV file
- FR2 — Find a specific book by title
- FR3 — Reduce a found book's copies by one when a loan is issued, only if copies are available
- FR4 — Write the updated records back to the file
Mystery of the Blue Loch,A. Ferguson,3 Highland Adventures,R. Bell,5 The Coding Puzzle,S. Mackay,0 Edinburgh Nights,L. Grant,2 The Silent Algorithm,K. Reid,4
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
copies: int
def readBooks(fileContents):
books = []
lines = fileContents.split("\n")
for line in lines:
fields = line.split(",")
title = fields[0]
author = fields[1]
copies = int(fields[2])
books.append(Book(title, author, copies))
return books
def findBookByTitle(books, target):
index = 0
found = False
while index < len(books) and not found:
if books[index].title == target:
found = True
else:
index = index + 1
if found:
return index
else:
return -1
def issueLoan(books, index):
if books[index].copies > 0:
books[index].copies = books[index].copies - 1
print("Loan issued for", books[index].title,
"- copies remaining:", books[index].copies)
else:
print("Cannot issue loan -", books[index].title,
"has no copies available")
def writeBooks(books):
lines = []
for book in books:
line = book.title + "," + book.author + "," + str(book.copies)
lines.append(line)
return "\n".join(lines)
findBookByTitle(libraryBooks, "Edinburgh Nights") → 3; issueLoan(libraryBooks, 3) reduces its copies from 2 to 1 and prints a confirmation; issueLoan(libraryBooks, 2) ("The Coding Puzzle", 0 copies) correctly refuses the loan and prints "Cannot issue loan"; writeBooks(libraryBooks) rebuilds all five lines, with Edinburgh Nights now showing 1 instead of 2 — this is the "round trip" from file, to record, to updated record, back to file.| Test data | Type | Expected result | Actual result | Pass/fail |
|---|---|---|---|---|
| Find "Edinburgh Nights", issue loan | Normal | Copies reduced 2 → 1, loan confirmed | Copies reduced 2 → 1, loan confirmed | Pass |
| Find "The Coding Puzzle" (0 copies), issue loan | Extreme — boundary (zero copies) | Loan refused, copies stay at 0 | Loan refused, copies stay at 0 | Pass |
| Find "Not A Real Book" | Erroneous — title not in file | Returns -1, no crash | Returns -1, no crash | Pass |
Read an empty file ("") | Erroneous — file-specific | A clear message, no crash | IndexError: list index out of range | Fail |
readBooks("") still produces one "line" (an empty string), and splitting it by comma gives only one field, so fields[1] raises IndexError rather than being handled gracefully. This is a genuinely new failure mode, distinct from SDD19's empty-array case, since it comes specifically from combining file reading with record construction.
| Criterion | Judgement |
|---|---|
| Fitness for purpose | Met — FR1–FR4 are all satisfied for every valid (non-empty-file) test case. |
| Efficient use of coding constructs | Met — reuses the standard linear search algorithm rather than a more complex or roundabout search method. |
| Usability | Met — issueLoan's confirmation and refusal messages are both clear and specific to the book involved. |
| Maintainability | Met — four clearly-named sub-programs, each with one responsibility, make the round trip easy to follow. |
| Robustness | Partial — Example 4 showed the program crashes on an empty file; a robust version would need to check for this before attempting to build any records. |
readBooks that would fix the empty-file robustness failure found in Example 4, and explain what it should do instead of crashing.
- Writing the file before the update is applied. Calling
writeBooksbeforeissueLoanwould save the old, unchanged copy count — the order in Example 1's design matters, not just which functions exist. - Searching on the whole record instead of one field.
findBookByTitlemust comparebooks[index].titleto the target, not the wholeBookrecord — this is the same misconception SDD15 warned about. - Only testing with a title that exists in the file. A test plan that never searches for a missing title, like "Not A Real Book" in Example 4, would never confirm the standard algorithm's not-found case (
-1) actually works. - Assuming file-reading bugs are the same as the ones already met in SDD16/17. The empty-file
IndexErrorin Example 4 is a genuinely new failure mode specific to combining file reading with record construction — it should be tested for directly, not assumed already covered by earlier lessons' bugs. - Evaluating the program as if it were still SDD19's simpler version. This program has four sub-programs and a file dependency, not three sub-programs working purely in memory — the evaluation needs to consider the added file-handling risk specifically, not just reuse SDD19's judgements unchanged.
When a question combines records with file handling, be explicit in your design and testing answers about which data structure a value lives in at each stage — a value read from a file is initially just text, becomes a record field after parsing, and is only "saved" again once it has been written back out. Blurring these stages together in an answer (for example, describing a change as "saved" when it has only been updated in memory) is a common way marks are lost on this kind of question.
Task Set A — Core questions
findBookByTitle reuse?findBookByTitle return if the title is not found in the array?findBookByTitle(libraryBooks, "Edinburgh Nights") return?issueLoan is called on "The Coding Puzzle" (0 copies)?"") belong to?readBooks("") raises an IndexError rather than simply returning an empty array. (3 marks)writeBooks were called before issueLoan instead of after it. (3 marks)countAvailableBooks(books) that returns a count of how many books in the array have copies > 0, reusing the count-occurrences standard algorithm (generalised to a condition, as in SDD15) rather than strict equality.Task Set B — Extension
Higher Computing Science → Software Design & Development → SDD20
Double period — second and final mini-project before revision. This is the closest rehearsal for the actual assignment the course provides; consider flagging this explicitly to pupils.
Suggested timing: 5 min warm-up + vocab · 10 min stage-flow overview + notes · 40 min working through Examples 1–5, ideally letting pupils attempt Stage 1's functional requirements and Stage 2's design themselves before revealing Examples 1's model version · 5 min "now you try" · remainder Task Set A; Task Set B as homework/extension.
Book titles/authors are entirely invented for this lesson (not real published works or real people) — consistent with the site's privacy rule and avoiding any real-author attribution question.
The empty-file IndexError in Example 4/5 is a genuinely new, Python-verified failure mode distinct from SDD19's empty-array case — worth explicitly drawing pupils' attention to why it's different (text-splitting behaviour on an empty string, not just an empty container being passed in), since this distinction is tested directly in Task Set A Q7 and Extension 2.
SQA command words covered: identify, design, implement, test, evaluate, compare.