Chapter 28 — Input and Output: The Report Engine
Streams are one of C++'s most elegant ideas: a single interface — <<, >>,
getline — that works identically whether the bytes are going to the terminal,
a file, or a string in memory. This lab makes that abstraction physical by
building a "report engine": a small library that parses raw score lines, formats
aligned table rows, assembles a full report, writes it to a real file, and reads
it back — plus a utility that counts valid integers in a stream until it hits
junk or EOF.
You implement five functions in starter/report.cpp:
- Task 1 — parse a
"name score"line withstd::istringstream - Task 2 — format one aligned row with
std::ostringstream+<iomanip> - Task 3 — assemble the full multi-line table
- Task 4 — write the table to a real file and read it back
- Task 5 — count valid integers in any stream until EOF/junk
Every function exercises a distinct stream concept; together they cover the full
chapter. The CS6340 connection is direct: Lab 1 loads a seed corpus from a file
(ifstream + getline), writes coverage results to another file (ofstream <<),
and passes std::ostream& around so the same function works for both console
and file output. You are building exactly those habits here.
Your tasks
Parse a score line (
std::istringstream+ state check).parseScoreLine(line)creates astd::istringstreamfrom the raw string, extracts the name token and double score with>>, checks the stream state, and validates the range[0.0, 100.0]. Returns aScoreRecordwithok = trueon success,ok = falseon any failure (malformed input, out-of-range score). The key idiom isif (in >> name >> score)— using the extraction result as a boolean (notes 28.5).Format a table row (
std::ostringstream+<iomanip>).formatRow(name, score)builds the aligned row"Ada 92.5"(name left-justified in a 10-char field; score right-justified in a 6-char field with 1 decimal) using anostringstreamand the manipulatorsstd::left,std::right,std::setw,std::fixed,std::setprecision. The tests assert exact character-for-character equality — the tricky part is knowing thatstd::setwresets after each insertion whilestd::fixedandstd::setprecisionpersist (notes 28.3).Assemble the full table (
ostringstream).buildReport(records)writes a header row, a separator row, and oneformatRowcall per record into a singleostringstream, then returns.str(). UseformatRowfor data rows; do not re-implement the alignment. An emptyrecordsvector produces only the 2-line header + separator.Round-trip through a real file (
ofstream/ifstream).writeReport(path, report)opens anofstream, checksif (!file) return;, writes the report string.readReportFirstLine(path)opens anifstream, checks open success, reads the first line withstd::getline, and returns it (notes 28.6). The tests write totests/tmp_report.txtand verify the header line comes back correctly.Stream-state loop (
while (in >> val)).countValidInts(in)counts integers extracted from anystd::istream&until the first extraction failure (bad token or EOF). The canonical one-liner idiom:while (in >> val) ++count;. Do NOT callin.clear()— the function stops at the first failure, per the stream-state contract (notes 28.5).
Success criteria
parseScoreLine: valid lines, leading/trailing whitespace (>> skips it), boundary scores 0.0 and 100.0, out-of-range scores (101.0, −1.0), malformed input (missing score, non-numeric token), and empty string — all must return the correctokflag.formatRow: exact character-for-character string equality for names of varying length (3, 4, 10 chars), scores from 0.0 to 100.0 — the alignment must be pixel-perfect.buildReport: exact 2-line header for empty input, presence of data rows fromformatRow, and a full exact-string match for a 2-record report.writeReport/readReportFirstLine: first line of the written file matches the expected header; non-existent file path returns""; invalid write path does not crash.countValidInts: all-valid stream (3), junk mid-stream stopping before the next valid int (1), junk at start (0), empty stream (0), negative ints (valid), 10 ints, junk at end (stops correctly), whitespace-only stream (0).
Concepts practiced
- Stream hierarchy —
istream/ostreamas the uniform base;cin,cout,ifstream,ofstream,istringstream, andostringstreamall descend from the same base (notes 28.1). Acceptingstd::istream&orstd::ostream&in a function makes it stream-polymorphic. std::istringstream— parse structured fields out of astd::stringusing the same>>operator you use onstd::cin(notes 28.4)std::ostringstream— build a formattedstd::stringwithout manual concatenation; retrieve it with.str()(notes 28.4)<iomanip>manipulators —std::left,std::right,std::setw,std::fixed,std::setprecisionfor column-aligned table formatting; knowing which manipulators persist vs. reset (notes 28.3)- Stream states —
goodbit,failbit,eofbit; using the extraction result as a boolean (if (in >> name >> score)); the while-extraction idiomwhile (in >> val)(notes 28.5) std::ifstream/std::ofstream— file I/O with open-success check (if (!file) return;),getlineon a file, sequential read/write (notes 28.6)- Reused from earlier chapters: structs (Ch 13),
std::vector/ range-for(Ch 16),std::stringmanipulation (Ch 5),const-correctness (Ch 5)
Constraints
- Allowed:
std::istringstream,std::ostringstream,std::ifstream,std::ofstream,<iomanip>manipulators,std::getline,if (!stream)open checks,std::vector+ range-for,std::string,struct ScoreRecord,while/forloops,if/else,doublearithmetic and comparisons. - Required idioms:
if (in >> name >> score)for simultaneous extraction + state check;while (in >> val)for the counting loop;if (!file) return;after every file open;out.str()to retrieve ostringstream contents. - Forbidden (not yet taught or not the point): raw C-style I/O (
printf,scanf,fopen,fclose), manual string-padding with loops (usestd::setw),std::stringstreamwhereistringstreamorostringstreamis specific enough (prefer the more specific type — it communicates intent), callingin.clear()insidecountValidInts(it must stop at the first failure, not recover). - File paths:
writeReportandreadReportFirstLineuse the path as given; the tests pass"tests/tmp_report.txt"which is relative to the chapter-28/ directory (the Makefile's working directory). Do not hard-code paths.
Build & run locally
make # compile-check starter/report.cpp (should work immediately)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # run the grader against the reference (peek if stuck)
make clean # remove build artifacts (including tests/tmp_report.txt)make test is the grader — it calls every function across many inputs. There
is no interactive make run for this lab (streams are the output; the grader
shows everything you need).
Hints
Task 1 — istringstream construction and the if-extraction idiom
std::istringstream in { line }; // "wrap" the string in an input stream
std::string name {};
double score {};
if (in >> name >> score) // true if BOTH extractions succeeded
{
if (score >= 0.0 && score <= 100.0)
return ScoreRecord{ true, name, score };
}
return ScoreRecord{}; // default has ok = falsestd::istringstream is in <sstream>. Once constructed, it behaves exactly like
std::cin but draws from the string. The chained extraction returns the stream,
which evaluates to true only if no error flag is set (notes 28.5).
Task 2 — manipulator order matters; setw resets; fixed persists
std::ostringstream out {};
out << std::left << std::setw(10) << name; // name field
out << std::right << std::fixed << std::setprecision(1)
<< std::setw(6) << score; // score field
return out.str();std::setw(N) applies only to the immediately following insertion and then
resets to 0 (notes 28.3). std::fixed and std::setprecision(1) stay active
for all subsequent floating-point insertions — harmless here since we output
only one number per call.
Task 3 — header and separator widths
std::ostringstream out {};
// Header: "Name" left in 10-char field, "Score" right in 6-char field
out << std::left << std::setw(10) << "Name"
<< std::right << std::setw(6) << "Score" << '\n';
// Separator: 9-dash name part, space, 6-dash score part
out << std::left << std::setw(9) << "---------"
<< ' '
<< std::right << std::setw(6) << "------" << '\n';
// Data rows
for (const ScoreRecord& r : records)
out << formatRow(r.name, r.score) << '\n';
return out.str();The header uses setw(10) for name (same as formatRow) but only setw(6)
for "Score" because the word is 5 chars — one padding space on the left.
The separator uses setw(9) for the dashes (the name field is 10, but 9 dashes
look cleaner) then a literal space before the 6-dash score divider.
Task 4 — ofstream write, ifstream first-line read
void writeReport(const std::string& path, const std::string& report)
{
std::ofstream file { path };
if (!file) return; // open failed — wrong path, permissions, etc.
file << report; // same << as std::cout, writes to disk
}
std::string readReportFirstLine(const std::string& path)
{
std::ifstream file { path };
if (!file) return ""; // open failed
std::string line {};
std::getline(file, line); // reads up to (not including) the '\n'
return line;
}The destructor of ofstream flushes and closes the file — no explicit
.close() needed. std::getline on an ifstream works identically to
std::getline(std::cin, line) — same function, different stream kind.
Task 5 — the while-extraction idiom
int count { 0 };
int val {};
while (in >> val) // extract; if it succeeds, val is updated and condition is true
++count;
return count;When >> fails (bad token OR EOF), it sets failbit (and eofbit for EOF).
The stream then evaluates to false in a boolean context — while exits.
All subsequent >> calls on the same failed stream also fail immediately
without changing val, so hitting one bad token stops the entire count.
(notes 28.5)
Stretch goals
- Make
buildReportsort the rows by score (descending) before printing — requiresstd::sort+ a lambda comparator (Chapter 18/20). - Add a
parseReport(std::istream&)inverse: read a report file back into astd::vector<ScoreRecord>by parsing each line after the 2-line header — this deepens theifstream+getline+istringstreamcombination. - Handle the
>>/getlinemixing trap (notes 28.2): write areadCsvLinefunction that reads a full CSV line withstd::getlineand then uses anistringstreamto split on commas — a real-world pattern for seed-corpus files. - Write a
countValidIntsWithRecoveryvariant that skips bad tokens and keeps counting (usingin.clear()+in.ignore()— notes 28.5). Compare its behavior tocountValidIntsto see the state-management difference clearly. - Replace the file path
std::stringparameters withstd::filesystem::path(C++17 — a preview of the filesystem library, formally beyond Ch 28).
// Chapter 28 — Input and Output · Project: The Report Engine (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the five TASK blocks below. Each maps 1:1 to a task in the README and
// to a declaration in ../report.h. The bodies currently return PLACEHOLDERS so
// the file compiles immediately — that's why `make test` is RED right now. Your
// job is to turn it GREEN by implementing each stream operation.
//
// make build compile your code (should already work)
// make test grade it (RED until you fill these in)
// make test-solution run the grader against the reference if you get stuck
//
// THE STREAM MINDSET: streams are buffers of characters with a read/write
// position and a set of STATE FLAGS. You write TO them with <<, read FROM them
// with >>, and always check whether the last operation succeeded.
//
// Key includes for this lab:
// <sstream> — std::istringstream, std::ostringstream (notes 28.4)
// <iomanip> — std::setw, std::setprecision, std::left, std::right, std::fixed
// (notes 28.3)
// <fstream> — std::ifstream, std::ofstream (notes 28.6)
#include "../report.h"
#include <fstream> // std::ifstream, std::ofstream
#include <iomanip> // std::setw, std::setprecision, std::left, std::right, std::fixed
#include <sstream> // std::istringstream, std::ostringstream
// ─── TASK 1: parseScoreLine — std::istringstream extraction + state check ────
//
// Use std::istringstream to parse the two fields out of `line`.
// The stream abstraction lets you treat a std::string exactly like std::cin:
//
// std::istringstream in { line };
// std::string name {};
// double score {};
// if (in >> name >> score) { /* both fields extracted */ }
//
// After extracting name and score, validate:
// - both extractions succeeded (the if(...) above handles this)
// - score is in [0.0, 100.0] (a plain >= / <= check is fine)
//
// If everything is valid, return ScoreRecord{ true, name, score }.
// Otherwise return ScoreRecord{} (the default has ok=false).
//
// Hint: std::istringstream is declared in <sstream>. Create it on the stack
// (stack allocation is fine — no heap needed for a string stream).
//
// >>> YOUR CODE HERE <<<
//
ScoreRecord parseScoreLine(const std::string& /*line*/)
{
return ScoreRecord{}; // placeholder — always reports parse failure
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: formatRow — std::ostringstream + <iomanip> ──────────────────────
//
// Build the formatted row in an std::ostringstream, then return .str().
//
// The manipulators you need (all from <iomanip>):
// std::left — left-align in the next field
// std::right — right-align in the next field (the default for numbers)
// std::setw(N) — field width for the NEXT inserted item (resets after use)
// std::fixed — use fixed-point notation (persists on the stream)
// std::setprecision(N) — decimal places when std::fixed is active (persists)
//
// Target output (exact — the tests assert character-for-character):
// formatRow("Ada", 92.5) == "Ada 92.5" (Ada+7sp + 2sp+92.5 = 16 chars)
// formatRow("Bob", 88.0) == "Bob 88.0"
// formatRow("Zhao", 71.3) == "Zhao 71.3"
//
// Layout: name LEFT in 10-char field, score RIGHT in 6-char field, 1 decimal.
// That means: out << std::left << std::setw(10) << name
// << std::right << std::fixed << std::setprecision(1)
// << std::setw(6) << score;
// (Write them in this order — std::setw applies only to the NEXT item.)
//
// >>> YOUR CODE HERE <<<
//
std::string formatRow(const std::string& /*name*/, double /*score*/)
{
return ""; // placeholder — empty row
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: buildReport — assemble the table with an ostringstream ──────────
//
// Build the report in a single std::ostringstream:
//
// 1. Write the header row:
// out << std::left << std::setw(10) << "Name"
// << std::right << std::setw(6) << "Score" << '\n';
//
// 2. Write the separator row:
// out << std::left << std::setw(9) << "---------"
// << ' '
// << std::right << std::setw(6) << "------" << '\n';
// (9 dashes for the name column, space, 6 dashes for score column)
//
// 3. For each ScoreRecord `r` in `records`:
// out << formatRow(r.name, r.score) << '\n';
//
// Return out.str(). An empty `records` vector produces only the 2-line header.
//
// TIP: manipulators like std::fixed and std::setprecision PERSIST on the stream.
// std::setw resets after each insertion. If the header/separator look wrong,
// check that your formatRow call hasn't left std::fixed active on the oss.
// (It has — and that's fine here, because std::setw(6) on a string still
// right-pads to 6 chars regardless of fixed/float mode.)
//
// >>> YOUR CODE HERE <<<
//
std::string buildReport(const std::vector<ScoreRecord>& /*records*/)
{
return ""; // placeholder — empty report
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4a: writeReport — std::ofstream ────────────────────────────────────
//
// Steps:
// 1. Declare: std::ofstream file { path };
// 2. Check: if (!file) return; // could not open -> silently bail
// 3. Write: file << report; // dump the whole report string
//
// The file is created (or truncated) automatically by std::ofstream. After the
// function returns, `file` goes out of scope and its destructor flushes + closes
// the underlying file — no explicit close() needed.
//
// >>> YOUR CODE HERE <<<
//
void writeReport(const std::string& /*path*/, const std::string& /*report*/)
{
// placeholder — does nothing (file is never written)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4b: readReportFirstLine — std::ifstream + std::getline ─────────────
//
// Steps:
// 1. Declare: std::ifstream file { path };
// 2. Check: if (!file) return ""; // could not open
// 3. Read: std::string line {};
// std::getline(file, line);
// 4. Return: line
//
// std::getline reads up to (but not including) the first '\n'. For a multi-line
// report, it returns exactly the header row — the lightweight round-trip check
// in the tests. (notes 28.2 — getline on an ifstream works identically to
// getline on std::cin.)
//
// >>> YOUR CODE HERE <<<
//
std::string readReportFirstLine(const std::string& /*path*/)
{
return ""; // placeholder — always returns empty (as if open failed)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: countValidInts — stream-state loop ───────────────────────────────
//
// The cleanest form: let the extraction attempt BE the loop condition.
//
// int count { 0 };
// int val {};
// while (in >> val) // true while extraction succeeds; false at EOF or junk
// ++count;
// return count;
//
// Why this works (notes 28.5): when >> fails (bad token OR EOF), it sets failbit
// (and eofbit at EOF). The stream then converts to bool as `false`, breaking the
// loop. Every subsequent >> on the SAME failed stream also returns false without
// extracting anything — so we stop at the first bad token, never skip it.
//
// Do NOT call in.clear() inside this function — the spec says to stop at the
// first failure, not recover. (Recovery would be a different function.)
//
// >>> YOUR CODE HERE <<<
//
int countValidInts(std::istream& /*in*/)
{
return 0; // placeholder — always returns 0 (as if all extractions failed)
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// Chapter 28 — Input and Output · Project: The Report Engine (REFERENCE SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation of ../report.h.
// Peek only after you've taken a real swing at starter/report.cpp — the learning
// is in wiring the stream operations yourself, then comparing.
//
// Every function demonstrates the UNIFORM-ABSTRACTION principle:
// • Tasks 1-3: string streams (istringstream/ostringstream) — treat a std::string
// like a stream, parse from it or build into it, no file system involved.
// • Task 4: file streams (ifstream/ofstream) — the exact same << / >> / getline
// operations, but now they go to/from disk.
// • Task 5: accepts std::istream& — works with ANY stream kind the caller supplies
// (istringstream in tests, std::cin in a real program, ifstream for files).
//
// CS6340 / Lab 1 connection: this is exactly the pattern for loading a seed corpus
// (ifstream -> getline) and logging results (ofstream << formatted line).
#include "../report.h"
#include <fstream> // std::ifstream, std::ofstream (notes 28.6)
#include <iomanip> // std::setw, std::setprecision, etc. (notes 28.3)
#include <sstream> // std::istringstream, std::ostringstream (notes 28.4)
// ─── TASK 1: parseScoreLine — std::istringstream extraction + state check ────
//
// std::istringstream is the "read-from-a-string" stream (notes 28.4).
// Constructing one with a std::string sets its internal buffer to that string.
// After that, all the usual input-stream operations (>>, getline, .fail(), etc.)
// work exactly as if you were reading from std::cin or an ifstream.
//
// The key state-check idiom (notes 28.5):
// if (in >> name >> score)
// This evaluates the expression in a boolean context. The expression:
// 1. Tries to extract `name` (a string token, stops at whitespace).
// 2. Tries to extract `score` (a double).
// 3. Returns the stream — which converts to `true` if BOTH succeeded.
// If EITHER extraction fails (malformed line, non-numeric score, EOF mid-line),
// the whole condition is false and we fall into the else branch.
ScoreRecord parseScoreLine(const std::string& line)
{
// Create an input string stream from the raw line.
// Think of `in` as a tiny std::cin that reads from `line` instead of the
// keyboard — the same extraction operators apply. (notes 28.4)
std::istringstream in { line };
std::string name {};
double score { 0.0 };
// The single most important idiom in this lab: use the extraction result
// as a boolean. If the stream enters a failed state at ANY point during
// this chained extraction, the whole condition is false. (notes 28.5)
if (in >> name >> score)
{
// Additional RANGE validation: stream state can be good but the value
// semantically invalid for our domain. Type validation (parse) and
// range/semantic validation are separate concerns. (notes 28.5)
if (score >= 0.0 && score <= 100.0)
return ScoreRecord{ true, name, score };
}
// Anything not explicitly returned above is a failed parse.
return ScoreRecord{};
}
// ─── TASK 2: formatRow — std::ostringstream + <iomanip> ──────────────────────
//
// std::ostringstream is the "write-to-a-string" stream (notes 28.4).
// We build the formatted row in it, then return the accumulated string via .str().
// This avoids manual string concatenation with ugly std::to_string calls.
//
// <iomanip> manipulator cheat-sheet (notes 28.3):
// std::left — left-align following insertions
// std::right — right-align following insertions (default for numbers)
// std::setw(N) — NEXT insertion fills a field of exactly N chars
// (RESETS after each insertion — must re-apply every time)
// std::fixed — use fixed-point decimal notation (PERSISTS)
// std::setprecision(N) — N digits after the decimal point when std::fixed is
// active (PERSISTS)
//
// Expected exact output (tests assert char-for-char) — each row is 16 chars:
// "Ada 92.5" (name "Ada" left in setw(10) = "Ada"+7 spaces;
// score right in setw(6) = 2 spaces+"92.5" -> 9-space gap)
// "Bob 88.0"
// "Zhao 71.3" ("Zhao"+6 spaces, then " 71.3")
std::string formatRow(const std::string& name, double score)
{
std::ostringstream out {};
// ── Name: left-aligned in a 10-char wide field ───────────────────────────
// std::left + std::setw(10) together mean: pad name on the RIGHT to 10 chars.
// std::setw resets after this insertion, so it won't affect the score below.
out << std::left << std::setw(10) << name;
// ── Score: right-aligned in a 6-char field, 1 decimal place ─────────────
// std::right puts any padding on the LEFT (so the number hugs the right wall).
// std::fixed + std::setprecision(1) pin exactly one digit after the decimal.
// Both std::fixed and std::setprecision(1) persist, but that is fine — we
// only format one number per call.
out << std::right << std::fixed << std::setprecision(1) << std::setw(6) << score;
return out.str();
}
// ─── TASK 3: buildReport — assemble the whole table ──────────────────────────
//
// One ostringstream, three phases: header, separator, data rows.
// Using a single oss avoids repeatedly concatenating heap strings.
//
// IMPORTANT MANIPULATOR ORDERING NOTE:
// After the first call to formatRow, the oss has std::fixed + std::setprecision(1)
// active on it (those manipulators persist). The header and separator are written
// BEFORE any formatRow call, so they aren't affected. If you were to write the
// header AFTER data rows, you'd need to save/restore the flags — which is why
// we write the header first.
std::string buildReport(const std::vector<ScoreRecord>& records)
{
std::ostringstream out {};
// ── Phase 1: header row ───────────────────────────────────────────────────
// "Name " (10-char left field) + " Score" (6-char right field)
// We apply the same layout widths as the data rows so columns align.
out << std::left << std::setw(10) << "Name"
<< std::right << std::setw(6) << "Score" << '\n';
// ── Phase 2: separator row ────────────────────────────────────────────────
// 9 dashes for the name column + space + 6 dashes for the score column.
// (std::setw(9) for 9-char left field, then 1 literal space, then setw(6)
// for the 6-char score dashes field.)
out << std::left << std::setw(9) << "---------"
<< ' '
<< std::right << std::setw(6) << "------" << '\n';
// ── Phase 3: data rows ────────────────────────────────────────────────────
// Reuse formatRow — one call per record, one '\n' per line.
// Range-for (Chapter 16) over the vector of ScoreRecords.
for (const ScoreRecord& r : records)
out << formatRow(r.name, r.score) << '\n';
return out.str();
}
// ─── TASK 4a: writeReport — std::ofstream ────────────────────────────────────
//
// std::ofstream opens a file for writing (notes 28.6). Constructing it with a
// path tries to open that file immediately; if the file doesn't exist it is
// created; if it does exist it is truncated (default std::ios::trunc behavior).
// The destructor automatically flushes and closes the file — no close() needed.
//
// CRITICAL pattern: ALWAYS check `if (!file)` after opening. On autograders and
// CI boxes, missing permissions or wrong working directories cause silent data
// loss if you don't check. (notes 28.6)
void writeReport(const std::string& path, const std::string& report)
{
std::ofstream file { path };
if (!file)
return; // could not open: wrong path, permissions, etc. -> silent bail
// The << operator on an ofstream behaves identically to std::cout << or
// ostringstream <<. The same stream abstraction at work. (notes 28.1)
file << report;
// `file` goes out of scope here; its destructor calls close(), which flushes
// any buffered data to the OS. If you ever need to reuse the file handle in
// the same scope, call file.close() explicitly.
}
// ─── TASK 4b: readReportFirstLine — std::ifstream + std::getline ─────────────
//
// std::ifstream opens a file for reading. std::getline(file, line) reads one
// line (up to but not including the '\n'), exactly as getline(std::cin, line).
// This is the stream-polymorphism payoff: the SAME getline function that reads
// interactive input also reads from files. (notes 28.2, 28.6)
std::string readReportFirstLine(const std::string& path)
{
std::ifstream file { path };
if (!file)
return ""; // could not open
std::string line {};
std::getline(file, line); // read just the first line (the header row)
return line;
}
// ─── TASK 5: countValidInts — stream-state loop ──────────────────────────────
//
// The idiomatic stream-state loop (notes 28.5):
//
// while (in >> val) { ... }
//
// When >> fails:
// • EOF reached -> eofbit + failbit set -> stream converts to false -> loop exits
// • Bad token -> failbit set -> stream converts to false -> loop exits
//
// Once failbit is set, every subsequent >> on the SAME stream immediately returns
// false without modifying `val`. This means if we see "10 abc 30", we count 10
// (success), then stop at "abc" (failure) — we never reach 30. That is the
// CORRECT behavior for a "count valid ints until first failure" function.
//
// CS6340 insight: the same pattern lets you drain a fuzzer's coverage-count list
// from a file until the format changes or the file ends — one loop, no bookkeeping.
int countValidInts(std::istream& in)
{
int count { 0 };
int val {};
// The loop condition IS the extraction. Beautiful C++ idiom:
// "keep extracting as long as you can; stop the moment you can't."
while (in >> val)
++count;
return count;
}
// Chapter 28 — Input and Output · Project: The Report Engine (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// A tiny no-framework unit-test harness (same style as drills/CLAUDE.md spec).
// It includes ../report.h and calls each API function across many inputs.
// Each CHECK that fails prints its expression and line number. Any failure ->
// non-zero exit -> `make test` is RED.
//
// The Makefile links this file against starter/report.cpp for `make test` and
// against solution/report.cpp for `make test-solution`.
//
// File I/O NOTE: Task 4 writes/reads tests/tmp_report.txt (relative to the
// Makefile's working directory, which is the chapter-28/ folder). `make clean`
// removes this file.
#include <iostream>
#include <sstream> // std::istringstream
#include <string>
#include <vector>
#include "../report.h"
static int fails = 0;
// CHECK: assert a boolean condition; on failure, report what and where.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Task 1: parseScoreLine ────────────────────────────────────────────────
// Normal cases
{
ScoreRecord r { parseScoreLine("Ada 92.5") };
CHECK(r.ok == true);
CHECK(r.name == "Ada");
CHECK(r.score == 92.5);
}
{
ScoreRecord r { parseScoreLine("Bob 88.0") };
CHECK(r.ok == true);
CHECK(r.name == "Bob");
CHECK(r.score == 88.0);
}
// Leading / trailing whitespace — >> skips it (notes 28.2)
{
ScoreRecord r { parseScoreLine(" Zhao 71.3 ") };
CHECK(r.ok == true);
CHECK(r.name == "Zhao");
CHECK(r.score == 71.3);
}
// Boundary scores: exactly 0.0 and 100.0 are in range
{
ScoreRecord r { parseScoreLine("Min 0.0") };
CHECK(r.ok == true);
CHECK(r.score == 0.0);
}
{
ScoreRecord r { parseScoreLine("Max 100.0") };
CHECK(r.ok == true);
CHECK(r.score == 100.0);
}
// Edge: score out of range -> ok == false
{
ScoreRecord r { parseScoreLine("Over 101.0") };
CHECK(r.ok == false);
}
{
ScoreRecord r { parseScoreLine("Neg -1.0") };
CHECK(r.ok == false);
}
// Edge: malformed lines (missing score, non-numeric) -> ok == false
{
ScoreRecord r { parseScoreLine("NoScore") };
CHECK(r.ok == false);
}
{
ScoreRecord r { parseScoreLine("BadScore abc") };
CHECK(r.ok == false);
}
// Edge: trailing junk AFTER a valid name+score is ignored -> ok == true.
// The contract (report.h TASK 1) only needs the first two fields; >> stops
// after the score and never inspects the rest of the line. (notes 28.4)
{
ScoreRecord r { parseScoreLine("Cleo 64.0 extra junk 99") };
CHECK(r.ok == true);
CHECK(r.name == "Cleo");
CHECK(r.score == 64.0);
}
// Edge: empty line -> ok == false
{
ScoreRecord r { parseScoreLine("") };
CHECK(r.ok == false);
}
// ── Task 2: formatRow — exact-string asserts ──────────────────────────────
// Layout: name left-justified in 10-char field, score right-justified in
// 6-char field with 1 decimal place. Total row width = 16 chars.
//
// "Ada" + 7 spaces = 10 chars (name field, left in setw(10))
// " 92.5" = 6 chars (score field, right-aligned: 2 spaces + "92.5")
// -> "Ada 92.5" (16 chars total; 9 spaces between "Ada" and "92.5")
//
// Values chosen to be exactly representable in decimal with 1dp:
// 92.5 = 185/2, 88.0 = exact integer, 71.3 rounds cleanly to "71.3"
CHECK(formatRow("Ada", 92.5) == "Ada 92.5"); // Ada+7sp+2sp+92.5
CHECK(formatRow("Bob", 88.0) == "Bob 88.0");
CHECK(formatRow("Zhao", 71.3) == "Zhao 71.3");
// Score that uses all 6 chars (100.0 = 5 chars, 1 padding space)
CHECK(formatRow("Ali", 100.0) == "Ali 100.0");
// Name exactly 10 chars -> no padding after name
CHECK(formatRow("Aleksandra", 75.0) == "Aleksandra 75.0");
// Minimum score: " 0.0" in 6-char right field = 3 spaces + 0.0
CHECK(formatRow("X", 0.0) == "X 0.0");
// ── Task 3: buildReport ───────────────────────────────────────────────────
// Column layout constants (must match formatRow):
// Name column: 10 chars wide, left-aligned
// Score column: 6 chars wide, right-aligned
//
// Header: "Name " (10 left) + " Score" (6 right) -> "Name Score"
// Sep: "---------" (9 chars, left in setw(9)) + " " + "------" (6 right)
// -> "--------- ------"
//
// Empty records -> just 2-line header
{
std::vector<ScoreRecord> empty {};
std::string rpt { buildReport(empty) };
std::string expected { "Name Score\n--------- ------\n" };
CHECK(rpt == expected);
}
// Two records -> header + separator + 2 data rows
{
std::vector<ScoreRecord> recs {
{ true, "Ada", 92.5 },
{ true, "Bob", 88.0 }
};
std::string rpt { buildReport(recs) };
// Header present
CHECK(rpt.find("Name") != std::string::npos);
CHECK(rpt.find("Score") != std::string::npos);
// Data rows use formatRow output
CHECK(rpt.find(formatRow("Ada", 92.5)) != std::string::npos);
CHECK(rpt.find(formatRow("Bob", 88.0)) != std::string::npos);
// Each data row is followed by '\n'
std::string ada_row { formatRow("Ada", 92.5) + '\n' };
CHECK(rpt.find(ada_row) != std::string::npos);
}
// Single record
{
std::vector<ScoreRecord> recs { { true, "Zhao", 71.3 } };
std::string rpt { buildReport(recs) };
CHECK(rpt.find("Zhao") != std::string::npos);
CHECK(rpt.find(formatRow("Zhao", 71.3)) != std::string::npos);
}
// Full exact-string check for a two-record report
{
std::vector<ScoreRecord> recs {
{ true, "Ada", 92.5 },
{ true, "Bob", 88.0 }
};
std::string expected {
"Name Score\n"
"--------- ------\n"
"Ada 92.5\n"
"Bob 88.0\n"
};
CHECK(buildReport(recs) == expected);
}
// ── Task 4: writeReport / readReportFirstLine ─────────────────────────────
// File is written to tests/tmp_report.txt (relative to chapter-28/ where
// make runs). `make clean` removes it.
{
std::string path { "tests/tmp_report.txt" };
std::vector<ScoreRecord> recs {
{ true, "Ada", 92.5 },
{ true, "Bob", 88.0 }
};
std::string report { buildReport(recs) };
// Write the report to the file
writeReport(path, report);
// Read back the first line and verify it matches the header
// std::getline returns the line WITHOUT the trailing '\n'
std::string first_line { readReportFirstLine(path) };
CHECK(first_line == "Name Score");
// Edge: non-existent path -> readReportFirstLine returns ""
std::string no_line { readReportFirstLine("tests/no_such_file_xyz.txt") };
CHECK(no_line == "");
}
// Edge: writeReport to non-writable path -> should not crash (silent no-op)
{
// Passing an unwritable path; the function must return without throwing
writeReport("/no_such_dir_xyz/report.txt", "test");
// If we got here, it did not crash or throw — that is the test
CHECK(true);
}
// ── Task 5: countValidInts ────────────────────────────────────────────────
// Normal: all valid integers
{
std::istringstream in { "10 20 30" };
CHECK(countValidInts(in) == 3);
}
// Junk mid-stream: stops at first non-int (notes 28.5 — failbit propagates)
{
std::istringstream in { "10 abc 30" };
CHECK(countValidInts(in) == 1); // "10" ok, "abc" fails -> stop before "30"
}
// Junk at start: zero valid
{
std::istringstream in { "abc 10 20" };
CHECK(countValidInts(in) == 0);
}
// Empty stream: zero valid
{
std::istringstream in { "" };
CHECK(countValidInts(in) == 0);
}
// Single valid int
{
std::istringstream in { "42" };
CHECK(countValidInts(in) == 1);
}
// Negative integers are valid (notes 28.2 — >> parses sign)
{
std::istringstream in { "-5 -10 7" };
CHECK(countValidInts(in) == 3);
}
// Many ints in a row
{
std::istringstream in { "1 2 3 4 5 6 7 8 9 10" };
CHECK(countValidInts(in) == 10);
}
// Junk at end (after valid ints)
{
std::istringstream in { "100 200 bad" };
CHECK(countValidInts(in) == 2);
}
// Whitespace only
{
std::istringstream in { " " };
CHECK(countValidInts(in) == 0);
}
// ── Summary ───────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all report-engine checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails
<< " check(s) failed -- fill in the TASK blocks in report.cpp.\n";
return fails ? 1 : 0;
}
# Chapter 28 — Input and Output · The Report Engine · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Layout: report.h (provided, declarations only) lives in the chapter root;
# the learner implements starter/report.cpp; the grader is tests/tests.cpp.
#
# File I/O note: Task 4 writes tests/tmp_report.txt (relative to this directory,
# which is the working directory when make is invoked). `make clean` removes it.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# ── build — compile-check the learner's starter/report.cpp ───────────────────
# Produces a warning-clean .o (object file only — no main, so no link).
# This is the "make" step in the learner's red→green loop.
build:
$(CXX) $(CXXFLAGS) -c starter/report.cpp -o starter/report.o
@echo "OK starter/report.cpp compiles. Now run: make test"
# run — build and run a minimal demo using the starter implementation
run: build
@echo "(No interactive driver for this lab — use 'make test' to grade.)"
# ── test — grade the LEARNER's code ──────────────────────────────────────────
# Compile the grader against the LEARNER's starter/report.cpp.
# RED until the TASK blocks are filled in; GREEN once they're correct.
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/report.cpp -o tests/run
@./tests/run || echo "FAIL fill in the TASK blocks in starter/report.cpp until every check passes."
# ── solution — build and run the reference (for comparison / if stuck) ────────
solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/report.cpp -o tests/run_sol
@./tests/run_sol
# ── test-solution — proof the lab is solvable ─────────────────────────────────
# The reference MUST pass every check. This is our verification gate.
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/report.cpp -o tests/run_sol
@./tests/run_sol
# ── clean — remove all build artifacts ────────────────────────────────────────
clean:
rm -f starter/report.o tests/run tests/run_sol
rm -f tests/tmp_report.txt
rm -rf tests/run.dSYM tests/run_sol.dSYM
// Chapter 28 — Input and Output · Project: The Report Engine
// ─────────────────────────────────────────────────────────────────────────────
// This header is the CONTRACT between you and the grader. It declares the five
// API functions that build a formatted score-report table and round-trip it
// through a real file. DO NOT EDIT THIS FILE — tests/tests.cpp includes it, and
// so do BOTH starter/report.cpp (yours) and solution/report.cpp (the reference).
//
// THE BIG IDEA OF THIS LAB: streams are a UNIFORM ABSTRACTION over strings,
// files, and the terminal. Every function in this API either:
// (a) accepts / returns an std::string (built internally with ostringstream), or
// (b) accepts a std::ostream& / std::istream& so it works with ANY stream kind.
//
// Why does this matter for CS6340?
// In Lab 1 you will: read seed-corpus lines from a file (ifstream), log
// coverage results to another (ofstream), and pass diagnostic streams around.
// The same output function can write to std::cout for dev runs and to an
// ofstream in the autograder harness — zero code change, because both ARE
// std::ostream. This lab builds that muscle.
//
// Notes cross-reference: the section numbers below refer to
// ../../notes/chapter-28.md (always check before citing).
//
// Header guard (Chapter 2): prevents multiple-inclusion ODR errors.
#ifndef REPORT_H
#define REPORT_H
#include <istream> // std::istream — base input (notes 28.1)
#include <ostream> // std::ostream — base output (notes 28.1)
#include <string> // std::string
#include <vector> // std::vector (Chapter 16 — reused here)
// ─────────────────────────────────────────────────────────────────────────────
// ScoreRecord — one parsed entry from a raw "name score" line.
// ok = true -> the parse succeeded; name and score are valid
// ok = false -> the line was malformed; name and score are undefined
// name = the alphanumeric name token (no spaces)
// score = the numeric score (must be in [0.0, 100.0])
// ─────────────────────────────────────────────────────────────────────────────
struct ScoreRecord
{
bool ok { false };
std::string name {};
double score { 0.0 };
};
// ─── TASK 1 ──────────────────────────────────────────────────────────────────
// parseScoreLine(line) — parse a "name score" line with std::istringstream.
//
// Input: a std::string containing exactly one name token followed by one
// floating-point score, e.g. "Ada 92.5" or " Bob 88.0 ".
// Whitespace before/between/after the two tokens is fine (>> skips it).
//
// Return a ScoreRecord with:
// ok = true if extraction of BOTH name AND score succeeded
// AND score is in the range [0.0, 100.0]
// ok = false for anything else (malformed, out-of-range, extra junk is ok
// to ignore — the first two fields are all we need)
//
// Constraint: use std::istringstream (notes 28.4) for extraction; do NOT use
// hand-rolled string parsing (substr, find, etc.). Check stream state with
// if(in >> name >> score) (notes 28.5).
//
// Hint: the extraction operator >> already skips leading whitespace.
ScoreRecord parseScoreLine(const std::string& line);
// ─── TASK 2 ──────────────────────────────────────────────────────────────────
// formatRow(name, score) — format one aligned table row as a std::string.
//
// Return a string of EXACTLY this form (the grader does exact-string asserts):
//
// "Ada 92.5" — name LEFT-justified in a 10-char wide field (16 total),
// score RIGHT-justified in a 6-char field, 1 decimal.
// "Ada " = 10 chars (3-char name + 7 spaces)
// " 92.5" = 6 chars (2 spaces + 92.5) — right-aligned in 6-char field
// Total row width = 16 chars.
//
// Layout constants (hardcoded is fine — they're part of the format spec):
// NAME_WIDTH = 10 (std::left + std::setw(10))
// SCORE_WIDTH = 6 (std::right + std::setw(6))
// SCORE_PREC = 1 (std::fixed + std::setprecision(1))
//
// Constraint: use std::ostringstream (notes 28.4) + <iomanip> manipulators
// (std::left, std::right, std::setw, std::fixed, std::setprecision — notes 28.3).
// Do NOT build the string with manual padding (spaces + std::to_string, etc.).
//
// Tricky bit: std::setw resets after every item; std::fixed + std::setprecision
// persist. Make sure the ORDER of manipulators matches the expected layout.
std::string formatRow(const std::string& name, double score);
// ─── TASK 3 ──────────────────────────────────────────────────────────────────
// buildReport(records) — assemble a complete multi-line table.
//
// Given a vector of valid ScoreRecords (all ok == true), produce a string of
// this exact shape (header + separator are 16 chars wide; each data row is
// formatRow(...) — also 16 chars — plus a trailing '\n'):
//
// "Name Score\n" <- header
// "--------- ------\n" <- separator
// "Ada 92.5\n" <- each data row is formatRow(r.name, r.score)+'\n'
// "Bob 88.0\n"
// ...
//
// EXACT column layout (no trailing spaces on the header/separator lines):
// Header: "Name" left in setw(10) = "Name"+6 spaces, then "Score" right in
// setw(6) = 1 leading space+"Score" -> "Name Score" (7-space gap)
// Separator: "---------" (9 dashes, left setw(9)) + " " + "------" (6 right)
//
// The expected header row string is:
// "Name Score\n" (Name + 7 spaces + Score)
// The expected separator row string is:
// "--------- ------\n" (9 dashes + 1 space + 6 dashes)
//
// Each data row: formatRow(r.name, r.score) + '\n'
//
// Constraint: build the whole report with a SINGLE std::ostringstream (do not
// concatenate multiple buildReport calls). Use formatRow for data rows.
// An empty records vector produces only the header + separator (2 lines).
std::string buildReport(const std::vector<ScoreRecord>& records);
// ─── TASK 4 ──────────────────────────────────────────────────────────────────
// writeReport(path, report) — write the report string to a file.
// readReportFirstLine(path) — read the FIRST line back from that file.
//
// writeReport:
// Open path with std::ofstream (notes 28.6). If the open fails, do nothing
// (just return). Otherwise write the `report` string into the file.
// The file is created (or truncated) by default — that is fine for this task.
//
// readReportFirstLine:
// Open path with std::ifstream (notes 28.6). If the open fails, return "".
// Otherwise read the FIRST LINE with std::getline and return it.
// (A first-line round-trip check is a lightweight way to verify the file
// write: if the header came back, the write worked.)
//
// Constraint: use std::ofstream / std::ifstream (not fstream). Always check
// whether opening succeeded with `if (!file)` (notes 28.6). Do NOT use C stdio.
void writeReport(const std::string& path, const std::string& report);
std::string readReportFirstLine(const std::string& path);
// ─── TASK 5 ──────────────────────────────────────────────────────────────────
// countValidInts(in) — count integers extracted from a stream until EOF/failure.
//
// Extract integers from `in` one at a time with `>>`. For each successful
// extraction, increment the count. Stop as soon as extraction fails (either EOF
// or a non-integer token). Return the count.
//
// Key insight (notes 28.5): once failbit is set, every subsequent extraction
// on the SAME stream also fails — so once you hit junk, stop immediately.
// The caller feeds you an istringstream so you can test it with a known string.
//
// Example: "10 20 30" -> 3 (all valid)
// "10 abc 30" -> 1 (stops at "abc"; 30 is never seen)
// "abc" -> 0 (fails on the first token)
// "" -> 0 (empty stream, EOF immediately)
//
// Constraint: use a while loop whose condition IS the extraction attempt
// (i.e. `while (in >> val)`). Do NOT call in.clear() — the function should
// stop at the first failure, not try to recover. (notes 28.5)
int countValidInts(std::istream& in);
#endif // REPORT_H
make test locally
(see “Build & run locally” above).