SDD20 — Mini-project 2: Arrays of Records and File I/O
- I can apply the five assessed solution-development activities to a rehearsal task combining arrays of records with file handling
- I can open, read, and close a real file, process its records, then open, write, and close the updated file
- I can identify functional requirements for a problem involving stored, structured data
- I can design a hierarchical structure diagram with subordinate modules, recognised module markers, and IN/OUT data flows
- I can implement genuine sequential file operations, a guarded search, a record update, and a saved round trip
- I can construct and execute a requirement-traceable test plan covering normal, extreme, and exceptional data
- I can evaluate the finished program against all five evaluation criteria
Key vocabulary
Combining records and real file handling — a second rehearsal project
Why this lesson builds on SDD19, not replaces it
SDD19 deliberately kept its scenario narrow — one array, no records, no files — so the five assessed solution-development activities could be practised without extra complexity. This lesson adds arrays of records (SDD6, SDD15) and genuine sequential file handling (SDD11, SDD12): opening a file, reading its text, closing it, processing records, then opening a file for writing, writing the updated text, and closing it. SQA sets the real assignment task annually. This library task is therefore transferable rehearsal in applying analysis, design, implementation, testing, and evaluation to stored structured data; it is not a promise that every annual assignment uses records, CSV data, or this read-search-update-write pattern. The complete seven-stage iterative software lifecycle remains a separate concept and also includes documentation and maintenance.
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 are: open and read a CSV file into an array of book records; find a specific book by title; report "Book not found" and make no change when the search returns -1; reduce a found book's copy count by one only when a copy is available; and write any successful update back to the real file. Each is independently testable. The explicit not-found requirement matters because Python accepts books[-1] as the last record: passing an unchecked search sentinel into the update would silently alter the wrong book.
Stage 2 — Design
The recognised structure diagram must be hierarchical: the top-level processLoan program module controls subordinate modules for loading, searching, safely issuing, and saving. Connector labels show data passed IN and returned OUT. loadBooksFromFile performs the real open/read/close operation before calling the pure readBooks parser; saveBooksToFile calls the pure writeBooks serialiser before the real open/write/close operation. The issueLoan selection checks if index == -1 before any books[index] access, and the top module saves only when the subprogram reports that a record was actually updated.
Stage 3 — Implementation
Implementation separates file access from text processing so each part can be tested clearly. readBooks(fileContents) parses CSV text with manual newline/comma splitting; writeBooks(books) serialises records with manual joining. Their thin wrappers use with open(filename, "r") as file: and file.read(), or with open(filename, "w") as file: and file.write(...). Leaving a with block closes the handle automatically. The record remains an @dataclass, the search remains the standard linear search algorithm, and issueLoan guards the -1 sentinel before indexing. This creates a genuine persistent round trip rather than merely passing an already-supplied string between helper functions.
Stage 4 — Testing
Testing is a construction task, not a supplied completed table. Starting only from FR1–FR5, pupils must devise a comprehensive set of normal, extreme, and exceptional cases; state exact expected results; and link every case to one or more requirements. The plan must include an integrated missing-title attempt that continues as far as the update decision, proving that the -1 result does not change the last record or write a corrupted file. Pupils then compare their expectations with the separately supplied execution evidence and record actual results and pass/fail. This makes requirement traceability visible instead of asking pupils merely to read somebody else's plan.
Stage 5 — Evaluation
Evaluation applies SDD18's five criteria to the combined program: fitness for purpose (does it genuinely load and save the file while meeting FR1–FR5); efficient use of coding constructs (is linear search appropriate); usability (are issued, unavailable, and not-found messages clear); maintainability (does each wrapper or helper have one named job); and robustness. The corrected flow handles a missing title safely and treats an empty file as an empty array, but malformed CSV fields can still raise an error, so robustness remains only partially met. Claims about persistence are supported by reopening the saved file and checking the changed value, not by inspecting a returned string alone.
Worked examples
- FR1 — Open and read a CSV file into an array of book records, then close it
- FR2 — Find a specific book by title
- FR3 — If the search returns
-1, report "Book not found" and make no change - FR4 — Reduce a found book's copies by one only if copies are available
- FR5 — Open and write a successful update back to the CSV file, then close it
↑ OUT: books[]
calls readBooks
↑ OUT: index
↑ OUT: loanIssued
then check copies
only if loanIssued
open/write/close
loadBooksFromFile opens in "r" mode, calls .read(), closes automatically, then parses the text.findBookByTitle returns the matching index or the -1 sentinel.issueLoan checks index == -1 before indexing; a missing or unavailable book returns False.saveBooksToFile serialises, opens in "w" mode, calls .write(), and closes automatically.-1 branch.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 = []
fileContents = fileContents.strip()
if fileContents == "":
return 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 loadBooksFromFile(filename):
with open(filename, "r") as file:
fileContents = file.read()
return readBooks(fileContents)
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 index == -1:
print("Book not found")
return False
if books[index].copies > 0:
books[index].copies = books[index].copies - 1
print("Loan issued for", books[index].title,
"- copies remaining:", books[index].copies)
return True
else:
print("Cannot issue loan -", books[index].title,
"has no copies available")
return False
def writeBooks(books):
lines = []
for book in books:
line = book.title + "," + book.author + "," + str(book.copies)
lines.append(line)
return "\n".join(lines)
def saveBooksToFile(filename, books):
fileContents = writeBooks(books)
with open(filename, "w") as file:
file.write(fileContents)
def processLoan(filename, target):
books = loadBooksFromFile(filename)
index = findBookByTitle(books, target)
loanIssued = issueLoan(books, index)
if loanIssued:
saveBooksToFile(filename, books)
return loanIssued
processLoan(filename, "Edinburgh Nights") opens and reads the file, finds index 3, reduces copies from 2 to 1, opens the same file in write mode, and writes the updated CSV. Reloading with loadBooksFromFile(filename) returns 1, proving persistence after the handles have closed. The zero-copy path returns False and leaves the file unchanged. The missing-title path prints "Book not found", returns False, and leaves every record—including the last one—unchanged.| ID | Requirement link(s) | Your exact test data | Type | Your expected result | Actual result | Pass/fail |
|---|---|---|---|---|---|---|
| T1 | Complete | Devise | Classify | Predict | Record later | Decide later |
| T2 | Complete | Devise | Classify | Predict | Record later | Decide later |
| T3 | Complete | Devise | Classify | Predict | Record later | Decide later |
| T4 | Complete | Devise | Classify | Predict | Record later | Decide later |
| T5 | Complete | Devise | Classify | Predict | Record later | Decide later |
| Run | Test executed after plans were built | Supplied actual result |
|---|---|---|
| A | Real sample file; target "Edinburgh Nights" (2 copies) | Loan confirmed; file rewritten; reopening the file shows 1 copy |
| B | Real sample file; target "The Coding Puzzle" (0 copies) | Loan refused; False returned; file contents unchanged |
| C | Real sample file; target "Not A Real Book"; continue through the update decision | "Book not found" printed; False returned; no write occurred; every record and the last record's 4 copies remained unchanged |
| D | Empty real file; any target | No crash; "Book not found" printed; empty file remained unchanged |
| E | Real file containing Highland Adventures,R. Bell | IndexError while parsing the missing field; no write occurred |
| Criterion | Judgement |
|---|---|
| Fitness for purpose | Met — FR1–FR5 are satisfied for the stated well-formed-file, found, missing-title, and zero-copy paths, including a persisted update. |
| Efficient use of coding constructs | Met — reuses the standard linear search algorithm rather than a more complex or roundabout search method. |
| Usability | Met — confirmation, unavailable, and not-found messages clearly distinguish all three outcomes. |
| Maintainability | Met — the top-level orchestrator and six single-purpose helpers separate parsing, file access, search, update, and serialisation. |
| Robustness | Partial — the -1 and empty-file paths now fail safely, but Run E shows that a malformed CSV line can still raise IndexError. |
findBookByTitle(...) returns -1 insufficient? State two things the integrated missing-title test must confirm after the update decision.
- Writing the file before the update is applied. Calling
saveBooksToFilebeforeissueLoanwould save the old, unchanged copy count — the order in Example 1's pipeline matters. - 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. - Checking
-1only inside the search test. The integrated flow must prove the sentinel is guarded beforebooks[index]and before any file write; otherwise Python can silently update the final record. - Calling returned CSV text a saved file.
writeBooksonly serialises text. Persistence occurs only whensaveBooksToFileopens a real file, calls.write(), closes it, and a later read confirms the change. - Reading a completed test table. Build cases, expected results, categories, and requirement links before looking at the execution evidence; then record actual results and pass/fail yourself.
- Assuming all invalid files behave alike. The empty-file path is now handled safely, while a malformed non-empty row still exposes a different parsing failure that must be tested directly.
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?loadBooksFromFile and saveBooksToFile prove genuine open/read/write/close file handling rather than string-only parsing. (4 marks)saveBooksToFile ran before issueLoan, and why it should not run when issueLoan returns False. (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. Present it as transferable practice in the five assessed solution-development activities for an SQA-set annual task, not as a prediction of the assignment's data/file shape.
Suggested timing: 5 min warm-up + vocab · 10 min assessed-activities overview + notes · 35 min Examples 1–3 · 25 min Example 4, requiring pupils to build their plans before the supplied execution evidence is shown · 10 min Example 5 + "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 corrected implementation has been Python-verified with a real temporary file. Emphasise that the missing-title integrated test proves the final record is unchanged, an empty file is handled safely, and the remaining malformed-row IndexError is the evidence supporting a partial robustness judgement.
SQA command words covered: identify, design, implement, test, evaluate, compare.