Build a study-time report: a command-line program that reads a file, validates each session and totals minutes by topic. This combines owning strings, containers, parsing, error handling and deterministic output. Keep it single-threaded; concurrency should solve a measured problem, not be an automatic project requirement.
The format is a nonempty topic, a tab and integer minutes from one through 1440. Limit input to ten thousand sessions. Repeated topics accumulate, empty input produces an empty report, and invalid input produces no report. Topics are case-sensitive and are not trimmed. Each topic's maximum total is therefore 14,400,000 minutes, safely representable by long long.
Complete implementation
Save as study.cpp. The parser returns owning data, so sessions do not borrow the temporary line buffer.
#include <charconv>
#include <cstddef>
#include <exception>
#include <fstream>
#include <initializer_list>
#include <iostream>
#include <map>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
struct Session {
std::string topic;
int minutes;
};
std::optional<Session> parse_session(std::string_view line) {
const auto tab = line.find('\t');
if (tab == std::string_view::npos || tab == 0) {
return std::nullopt;
}
const auto number = line.substr(tab + 1);
if (number.empty()) {
return std::nullopt;
}
int minutes{};
const auto parsed = std::from_chars(number.data(), number.data() + number.size(), minutes);
if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()
|| minutes < 1 || minutes > 1440) {
return std::nullopt;
}
return Session{std::string{line.substr(0, tab)}, minutes};
}
using Totals = std::map<std::string, long long>;
Totals summarize(std::istream& input) {
Totals totals;
std::string line;
std::size_t rows{};
while (std::getline(input, line)) {
if (rows >= 10000) {
throw std::runtime_error("At most 10000 sessions are supported");
}
++rows;
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
const auto session = parse_session(line);
if (!session) {
throw std::runtime_error("Invalid session at line " + std::to_string(rows));
}
totals[session->topic] += session->minutes;
}
if (input.bad() || (input.fail() && !input.eof())) {
throw std::runtime_error("Reading sessions failed");
}
return totals;
}
void print_report(const Totals& totals, std::ostream& output) {
for (const auto& [topic, minutes] : totals) {
output << topic << '\t' << minutes << '\n';
}
}
void expect(bool condition) {
if (!condition) {
throw std::runtime_error("Self-test failed");
}
}
void self_test() {
const auto valid = parse_session("cpp\t1440");
expect(valid && valid->topic == "cpp" && valid->minutes == 1440);
for (const auto text : {"cpp", "\t30", "cpp\t", "cpp\t0", "cpp\t1441",
"cpp\t30x", "cpp\t999999999999999999999"}) {
expect(!parse_session(text));
}
std::istringstream input{"cpp\t30\ncpp\t15\nrust\t20\n"};
std::ostringstream output;
print_report(summarize(input), output);
expect(output.str() == "cpp\t45\nrust\t20\n");
std::istringstream empty;
expect(summarize(empty).empty());
std::istringstream invalid{"cpp\t30\nbroken\n"};
bool rejected{};
try {
static_cast<void>(summarize(invalid));
} catch (const std::runtime_error&) {
rejected = true;
}
expect(rejected);
std::cout << "Self-tests passed\n";
}
int main(int argc, char* argv[]) {
try {
if (argc != 2) {
throw std::runtime_error("Usage: study INPUT | study --self-test");
}
if (std::string_view{argv[1]} == "--self-test") {
self_test();
return 0;
}
std::ifstream input{argv[1]};
if (!input) {
throw std::runtime_error("Cannot open input");
}
const auto totals = summarize(input);
print_report(totals, std::cout);
std::cout.flush();
if (!std::cout) {
throw std::runtime_error("Writing report failed");
}
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
clang++ -std=c++20 -Wall -Wextra -Wpedantic -g study.cpp -o study
./study --self-test
printf 'cpp\t30\ncpp\t15\nrust\t20\n' > sessions.tsv
./study sessions.tsv
The self-test prints Self-tests passed; the report contains cpp with 45 and rust with 20, separated from their totals by tabs. A map provides deterministic topic ordering. Aggregating n sessions with k topics takes O(n log k) map operations and O(k) stored entries, excluding text lengths.
Test contracts and investigate failures
The self-tests check parsing, range rejection, overflow rejection, aggregation, exact output and empty input. expect stays active in optimized builds; ordinary assert checks can disappear when NDEBUG is defined. Unit tests isolate small operations. Integration tests exercise the executable, its files, output and exit status together.
Add integration cases for a missing file, an invalid second row, CRLF input, a final line without a newline, exactly ten thousand rows and one row beyond the limit. A rejected input must return nonzero without printing a partial report. Check both positive and negative boundaries; a test suite that only covers the sample cannot establish those contracts.
For memory and undefined-behavior checks on a supported Clang installation:
clang++ -std=c++20 -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined study.cpp -o study-sanitized
./study-sanitized --self-test
./study-sanitized sessions.tsv
Sanitizers detect supported faults on executed paths; they do not establish complete correctness. If a test fails, reduce the input, read the first diagnostic and inspect the reported source line. A debugger can stop at a breakpoint and examine values while the program runs. Re-run the failing case after fixing its cause, then the existing suite.
Extend the project deliberately
Separate the parser, aggregation and CLI into headers and source files after this baseline works. Add a topic filter, then test its interaction with repeated topics and case sensitivity. Add export only after specifying overwrite behavior and handling write failures. A row limit does not limit bytes: bounded line lengths and topic lengths are a useful next requirement before processing untrusted large files.
For a final exercise, add a summary sorted by total minutes. Solution guidance: copy the map entries into a vector and sort by descending total with topic as a tie-break. Preserve the original parser contract, test equal totals and explain the additional O(k log k) sorting cost. You should be able to explain ownership, numeric bounds and error behavior without relying on the example output alone.
Primary references
- Clang AddressSanitizer.
- Clang UndefinedBehaviorSanitizer.
- Clang compiler user manual covers debugging and diagnostic options.

