Project: A Library Catalogue

This final project combines the course into one program: a library catalogue that loads books from a file, answers commands typed on standard input, lends and returns books, searches, reports statistics and saves its state. It uses classes with invariants, containers and algorithms, std::optional, exceptions, std::format and file streams — the everyday toolkit of modern C++.

Commands

CommandEffect
listAll books, sorted by title
search TEXTBooks whose title or author contains the text (case-insensitive)
lend ID NAMELend a book to a member
return IDReturn a book
statsCounts by genre and books on loan
save FILEWrite the catalogue in the same format it reads
quitStop

The data file

One book per line: id, title, author, year and genre, separated by |:

TXT books.txt
101|The Pragmatic Programmer|Hunt & Thomas|1999|computing
102|Dune|Frank Herbert|1965|fiction
103|A Brief History of Time|Stephen Hawking|1988|science
104|Clean Code|Robert C. Martin|2008|computing
105|The Left Hand of Darkness|Ursula K. Le Guin|1969|fiction

The program

Below is the complete program, followed by a sample session: the commands typed as input, and everything the program printed.

C++ library.cpp
#include <algorithm>
#include <cctype>
#include <format>
#include <fstream>
#include <iostream>
#include <map>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

// ---------- domain types ----------

enum class Genre { computing, fiction, science, other };

Genre parse_genre(const std::string& s) {
    if (s == "computing") return Genre::computing;
    if (s == "fiction") return Genre::fiction;
    if (s == "science") return Genre::science;
    return Genre::other;
}

std::string to_string(Genre g) {
    switch (g) {
        case Genre::computing: return "computing";
        case Genre::fiction:   return "fiction";
        case Genre::science:   return "science";
        case Genre::other:     return "other";
    }
    return "other";
}

class Book {
public:
    Book(int id, std::string title, std::string author, int year, Genre genre)
        : id_(id), title_(std::move(title)), author_(std::move(author)), year_(year), genre_(genre) {
        if (id_ <= 0) throw std::invalid_argument("book id must be positive");
        if (title_.empty()) throw std::invalid_argument("book needs a title");
    }

    int id() const { return id_; }
    const std::string& title() const { return title_; }
    const std::string& author() const { return author_; }
    int year() const { return year_; }
    Genre genre() const { return genre_; }
    const std::optional<std::string>& borrower() const { return borrower_; }

    void lend_to(const std::string& member) {
        if (borrower_) throw std::logic_error(std::format("'{}' is already on loan to {}", title_, *borrower_));
        borrower_ = member;
    }
    void give_back() {
        if (!borrower_) throw std::logic_error(std::format("'{}' is not on loan", title_));
        borrower_.reset();
    }

private:
    int id_;
    std::string title_;
    std::string author_;
    int year_;
    Genre genre_;
    std::optional<std::string> borrower_;
};

// ---------- the catalogue ----------

class Catalogue {
public:
    void add(Book book) {
        auto id = book.id();
        if (!books_.try_emplace(id, std::move(book)).second) {
            throw std::invalid_argument(std::format("duplicate id {}", id));
        }
    }

    Book& get(int id) {
        auto it = books_.find(id);
        if (it == books_.end()) throw std::out_of_range(std::format("no book with id {}", id));
        return it->second;
    }

    std::vector<const Book*> sorted_by_title() const {
        std::vector<const Book*> result;
        for (const auto& [id, book] : books_) result.push_back(&book);
        std::ranges::sort(result, {}, [](const Book* b) { return b->title(); });
        return result;
    }

    std::vector<const Book*> search(const std::string& text) const {
        std::vector<const Book*> result;
        for (const auto& [id, book] : books_) {
            if (contains_ignore_case(book.title(), text) || contains_ignore_case(book.author(), text)) {
                result.push_back(&book);
            }
        }
        return result;
    }

    std::map<Genre, int> count_by_genre() const {
        std::map<Genre, int> counts;
        for (const auto& [id, book] : books_) ++counts[book.genre()];
        return counts;
    }

    int on_loan() const {
        return static_cast<int>(std::ranges::count_if(books_, [](const auto& entry) {
            return entry.second.borrower().has_value();
        }));
    }

    std::size_t size() const { return books_.size(); }

    void save(std::ostream& out) const {
        for (const auto& [id, b] : books_) {
            out << std::format("{}|{}|{}|{}|{}\n", id, b.title(), b.author(), b.year(), to_string(b.genre()));
        }
    }

private:
    static bool contains_ignore_case(const std::string& haystack, const std::string& needle) {
        auto lower = [](std::string s) {
            for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
            return s;
        };
        return lower(haystack).find(lower(needle)) != std::string::npos;
    }

    std::map<int, Book> books_;     // sorted by id
};

// ---------- input/output ----------

Catalogue load(const std::string& path) {
    std::ifstream in{path};
    if (!in) throw std::runtime_error("cannot open " + path);
    Catalogue cat;
    std::string line;
    int line_no = 0;
    while (std::getline(in, line)) {
        ++line_no;
        std::vector<std::string> fields;
        std::istringstream ss{line};
        for (std::string f; std::getline(ss, f, '|');) fields.push_back(f);
        try {
            if (fields.size() != 5) throw std::invalid_argument("expected 5 fields");
            cat.add(Book{std::stoi(fields[0]), fields[1], fields[2], std::stoi(fields[3]), parse_genre(fields[4])});
        } catch (const std::exception& e) {
            std::cerr << std::format("{}:{}: skipped ({})\n", path, line_no, e.what());
        }
    }
    return cat;
}

void print_book(const Book& b) {
    std::cout << std::format("{:>4}  {:<27} {:<18} {:>4}  {}\n", b.id(), b.title(), b.author(), b.year(),
                             b.borrower() ? "on loan to " + *b.borrower() : "available");
}

// ---------- the command loop ----------

int main() {
    Catalogue cat;
    try {
        cat = load("books.txt");
    } catch (const std::exception& e) {
        std::cerr << "fatal: " << e.what() << '\n';
        return 1;
    }
    std::cout << std::format("loaded {} books\n", cat.size());

    std::string line;
    while (std::cout << "> " && std::getline(std::cin, line)) {
        std::cout << line << '\n';                          // echo the command
        std::istringstream args{line};
        std::string command;
        args >> command;
        try {
            if (command == "quit") {
                break;
            } else if (command == "list") {
                for (const Book* b : cat.sorted_by_title()) print_book(*b);
            } else if (command == "search") {
                std::string text;
                std::getline(args >> std::ws, text);
                auto found = cat.search(text);
                if (found.empty()) std::cout << "no matches\n";
                for (const Book* b : found) print_book(*b);
            } else if (command == "lend") {
                int id = 0;
                std::string member;
                if (!(args >> id >> member)) throw std::invalid_argument("usage: lend ID NAME");
                Book& b = cat.get(id);
                b.lend_to(member);
                std::cout << std::format("'{}' lent to {}\n", b.title(), member);
            } else if (command == "return") {
                int id = 0;
                if (!(args >> id)) throw std::invalid_argument("usage: return ID");
                Book& b = cat.get(id);
                b.give_back();
                std::cout << std::format("'{}' returned\n", b.title());
            } else if (command == "stats") {
                for (const auto& [genre, n] : cat.count_by_genre()) {
                    std::cout << std::format("{:<10} {}\n", to_string(genre), n);
                }
                std::cout << std::format("{} of {} books on loan\n", cat.on_loan(), cat.size());
            } else if (command == "save") {
                std::string path;
                if (!(args >> path)) throw std::invalid_argument("usage: save FILE");
                std::ofstream out{path};
                cat.save(out);
                std::cout << "saved to " << path << '\n';
            } else if (!command.empty()) {
                std::cout << "unknown command: " << command << '\n';
            }
        } catch (const std::exception& e) {
            std::cout << "error: " << e.what() << '\n';
        }
    }
    std::cout << "bye\n";
}
Input
list
search the
lend 102 sam
lend 102 alex
lend 999 alex
return 104
stats
borrow 101
save catalogue-backup.txt
quit
Output
loaded 5 books
> list
 103  A Brief History of Time     Stephen Hawking    1988  available
 104  Clean Code                  Robert C. Martin   2008  available
 102  Dune                        Frank Herbert      1965  available
 105  The Left Hand of Darkness   Ursula K. Le Guin  1969  available
 101  The Pragmatic Programmer    Hunt & Thomas      1999  available
> search the
 101  The Pragmatic Programmer    Hunt & Thomas      1999  available
 105  The Left Hand of Darkness   Ursula K. Le Guin  1969  available
> lend 102 sam
'Dune' lent to sam
> lend 102 alex
error: 'Dune' is already on loan to sam
> lend 999 alex
error: no book with id 999
> return 104
error: 'Clean Code' is not on loan
> stats
computing  2
fiction    2
science    1
1 of 5 books on loan
> borrow 101
unknown command: borrow
> save catalogue-backup.txt
saved to catalogue-backup.txt
> quit
bye

What it uses

TechniqueLessons
Classes that enforce their invariants in constructors and member functions31–32
std::optional for "maybe on loan"43
enum class with conversion functions20
std::map, try_emplace, structured bindings21
Ranges algorithms with projections and lambdas24–26
Exceptions for errors, caught per command so the program keeps running42
File and string streams; std::format44–45
Non-owning const Book* results pointing into the catalogue27

Challenges

  1. Due dates: record a std::chrono::sys_days due date when lending, and add an overdue command (lesson 46).
  2. Members: add a Member class with a borrowing limit, and reject loans beyond it.
  3. Persistence: save who has each book, and load it back.
  4. Split it up: move Book and Catalogue into their own header and source files, and build with CMake (lessons 48–49).
  5. Test it: write tests for Catalogue with a framework such as Catch2 or GoogleTest, and run them under -fsanitize=address,undefined.

Where to go next

  • Read A Tour of C++ by Bjarne Stroustrup for a compact overview of the whole language, and the C++ Core Guidelines for style.
  • Explore libraries: Qt for desktop apps, SFML or raylib for games, Boost for almost everything, fmt and spdlog for formatting and logging.
  • Contribute to an open-source C++ project, or build something you care about.
  • Practise with the exercises and test yourself with the quiz.