4
\$\begingroup\$

This is an exercise I solved in the PPP book in C++. If there's a better way I could have done something, please tell me. (No main() and Date (just year)) Thank you!

For context here is the exercise:

  1. This exercise and the next few require you to design and implement a Book class, such as you can imagine as part of software for a library. Class Book should have members for the ISBN, title, author, and copyright date. Also store data on whether or not the book is checked out. Create functions for returning those data values. Create functions for checking a book in and out. Do simple validation of data entered into a Book; for example, accept ISBNs only of the form n-n-n- x where n is an integer and x is a digit or a letter. Store an ISBN as a string.

  2. Add operators for the Book class. Have the == operator check whether the ISBN numbers are the same for two books. Have != also compare the ISBN numbers. Have a << print out the title, author, and ISBN on separate lines.

  3. Create an enumerated type for the Book class called Genre. Have the types be fiction, nonfiction, periodical, biography, and children. Give each book a Genre and make appropriate changes to the Book constructor and member functions.

  4. Create a Patron class for the library. The class will have a user’s name, library card number, and library fees (if owed). Have functions that access this data, as well as a function to set the fee of the user. Have a helper function that returns a Boolean (bool) depending on whether or not the user owes a fee.

  5. Create a Library class. Include vectors of Books and Patrons. Include a struct called Transaction. Have it include a Book, a Patron, and a Date from the chapter. Make a vector of Transactions. Create functions to add books to the library, add patrons to the library, and check out books. Whenever a user checks out a book, have the library make sure that both the user and the book are in the library. If they aren’t, report an error. Then check to make sure that the user owes no fees. If the user does, report an error. If not, create a Transaction, and place it in the vector of Transactions. Also write a function that will return a vector that contains the names of all Patrons who owe fees.

My solution:

Book.hpp:

#include <array>
#include <string>
#include <stdexcept>

class Book {
public:
    enum class Genre {
        fiction,
        nonfiction,
        periodical,
        biography,
        children,
        genre_size
    };

public:
    // Book() = default; Doesn't make logical sense to have a default constructor here. 
    Book(const std::string& isbn, const std::string& title, const std::string& author, std::size_t copyRightYear, Genre genre);

    const std::string& getIsbn() const { return m_isbn; }
    const std::string& getTitle() const { return m_title; }
    const std::string& getAuthor() const { return m_author; }
    std::size_t getCopyRightYear() const { return m_copyRightYear; }
    const std::string& getGenre() const {
        static const std::array<std::string, static_cast<std::size_t>(Genre::genre_size)> genres {
            "fiction",
            "nonfiction",
            "periodical",
            "biography",
            "children"
        };
        return genres.at(static_cast<std::size_t>(m_genre));
    }

    void bookCheckIn();
    void bookCheckOut();
    bool isCheckedOut() const { return m_isCheckedOut; }

private:
    bool validateIsbn(const std::string &isbn) const; // n-n-n-x where n is an integer and x is a digit or a letter

private:
    std::string m_isbn;
    std::string m_title;
    std::string m_author;
    std::size_t m_copyRightYear;
    Genre m_genre;
    bool m_isCheckedOut{};
};

bool operator==(const Book& lhs, const Book& rhs);
bool operator!=(const Book& lhs, const Book& rhs);
std::ostream& operator<<(std::ostream& os, const Book& book);

Book.cpp

#include "Book.hpp"

#include <iostream>
#include <algorithm>
#include <cctype>

Book::Book(const std::string& isbn, const std::string& title, const std::string& author, std::size_t copyRightYear, Genre genre)
    : m_isbn{ isbn },
      m_title{ title },
      m_author{ author },
      m_copyRightYear{ copyRightYear },
      m_genre{ genre },
      m_isCheckedOut{ true }
{
    if (!validateIsbn(isbn))
        throw std::runtime_error{"Book::Book(): Invalid isbn."};
}

void Book::bookCheckIn() {
    if (!isCheckedOut()) {
        std::cout << "You have not checked out any book.\n";
        return;
    }
    m_isCheckedOut = false;
}
void Book::bookCheckOut() { 
    if (isCheckedOut()) {
        std::cout << "You already checked out a book.\n";
        return;
    }
    m_isCheckedOut = true;
}

bool Book::validateIsbn(const std::string& isbn) const {
    if (isbn.length() != 7)
        return false;

    for (int i{}; i < 7; i += 2) {
        char currentChar{ isbn[i] };

        if (!isdigit(currentChar) && !isalpha(currentChar))
            return false;
    }

    if (isbn[1] != '-' || isbn[3] != '-')
        return false;

    char lastChar{ isbn[6] };
    if (!isdigit(lastChar) && !isalpha(lastChar))
        return false;

    return true;
}

bool operator==(const Book& lhs, const Book& rhs) {
    return lhs.getIsbn() == rhs.getIsbn();
}

bool operator!=(const Book& lhs, const Book& rhs) {
    return !(lhs == rhs);
}

std::ostream& operator<<(std::ostream& os, const Book& book) {
    os << "Title: " << book.getTitle() << '\n'
       << "Author: " << book.getAuthor() << '\n'
       << "ISBN: " << book.getIsbn() << '\n'
       << "Is checked out? " << std::boolalpha << book.isCheckedOut() << '\n';
    return os;
}

Patron.hpp

#include "Book.hpp"

#include <ostream>

class Patron {
public:
    // Patron() = default; again no need for a default constructor here
    Patron(const std::string& name, const std::string& libraryCardNumber);

    const std::string& getName() const { return m_name; }
    const std::string& getCardNumber() const { return m_libraryCardNumber; }
    double getFees() const { return m_libraryFees; }

    void setFees(double fees);
    bool owesLibraryFees() const { return m_libraryFees > 0.0; }

private:
    std::string m_name;
    std::string m_libraryCardNumber;
    double m_libraryFees{};
};

std::ostream& operator<<(std::ostream& os, const Patron& patron);

Patron.cpp

#include "Patron.hpp"

#include <stdexcept>

Patron::Patron(const std::string& name, const std::string& libraryCardNumber)
    : m_name{ name }, m_libraryCardNumber{ libraryCardNumber } { }

void Patron::setFees(double fee) {
    if (fee <= 0.0)
        throw std::runtime_error{"setFee(double fee): parameter fee was <= 0.0."};
    m_libraryFees = fee;
}

std::ostream& operator<<(std::ostream& os, const Patron& patron) {
    os << "Name: " << patron.getName() << '\n'
       << "library Card Number: " << patron.getCardNumber() << '\n'
       << "Fees: " << '$' << patron.getFees() << '\n';
    return os;
}

Library.hpp

#include "Book.hpp"
#include "Patron.hpp"

#include <iostream>
#include <vector>

class Library {
public:
    struct Transaction {
        Book book;
        Patron patron;
    };

public:
    Library() = default;

    void addBook();
    void addPatron();
    void checkBookOut();
    const Book& getBook(const std::string& title) const;
    const Patron& getPatron(const std::string& libraryCardNumber) const;
    const std::vector<std::string> getPatronsWithFees() const;

private:
    Book::Genre parseGenre(const std::string &genre) const;
    bool checkPatron(const std::string& name, const std::string& cardNumber) const;
    bool checkBook(const std::string& book) const;

private:
    std::vector<Book> m_books;
    std::vector<Patron> m_patrons;
    std::vector<Transaction> m_transactions;
};

Library.cpp

#include "Library.hpp"

#include <iostream>

Book::Genre Library::parseGenre(const std::string& genre) const {
    Book::Genre parsedGenre;

    if (genre == "fiction")
        parsedGenre = Book::Genre::fiction;
    else if (genre == "nonfiction")
        parsedGenre = Book::Genre::nonfiction;
    else if (genre == "periodical")
        parsedGenre = Book::Genre::periodical;
    else if (genre == "biography")
        parsedGenre = Book::Genre::biography;
    else if (genre == "children")
        parsedGenre = Book::Genre::children;
    else {
        throw std::runtime_error("Invalid genre: " + genre);
    }

    return parsedGenre;
}

bool Library::checkPatron(const std::string& name, const std::string& cardNumber) const {
    for (const auto& patron : m_patrons) {
        if (patron.getName() == name && patron.getCardNumber() == cardNumber) return true;
    }
    return false;
}

bool Library::checkBook(const std::string& book) const {
    for (const auto& b : m_books) {
        if (b.getTitle() == book && !b.isCheckedOut()) return true;
    }
    return false;
}

void Library::addBook() {
    std::cout << "Please enter a book in the database...\n";
    
    std::cout << "Enter the isbn: ";
    std::string isbn;
    std::cin >> isbn;
    std::cin.ignore();

    std::cout << "Enter the title: ";
    std::string title;
    std::getline(std::cin, title);

    std::cout << "Enter the author: ";
    std::string author;
    std::getline(std::cin, author);

    std::cout << "Enter the copy right year: ";
    std::size_t copyRightYear{};
    std::cin >> copyRightYear;

    std::cout << "Enter genre: ";
    std::string genre;
    std::cin >> genre;

    m_books.emplace_back(isbn, title, author, copyRightYear, parseGenre(genre));
}

void Library::addPatron() {
    std::cout << "Please enter a Patron in the database...\n";

    std::cout << "Enter your name: ";
    std::string name;
    std::getline(std::cin, name);

    std::cout << "Enter library card number: ";
    std::string libraryCardNumber;
    std::cin >> libraryCardNumber;

    m_patrons.emplace_back(name, libraryCardNumber);
}

void Library::checkBookOut() {
    std::cout << "Enter your name and your library card number: ";
    std::string name, libraryCardNumber;
    std::getline(std::cin, name);
    std::getline(std::cin, libraryCardNumber);

    if (!checkPatron(name, libraryCardNumber))
        throw std::runtime_error{"checkBookOut(): person doesn't exist in the database."};

    std::cout << "Enter book title: ";
    std::string title;
    std::getline(std::cin, title);

    if(!checkBook(title))
        throw std::runtime_error{"checkBookOut(): book doesn't exist in the database."};


    Book book{ getBook(title) };
    book.bookCheckOut();

    const Patron& patron{ getPatron(libraryCardNumber) };

    if (patron.owesLibraryFees())
        throw std::runtime_error{"checkBookOut(): patron owns fees."};

    m_transactions.push_back({ book, patron });
}

const Book& Library::getBook(const std::string& title) const {
    for (const auto& book : m_books)
        if (book.getTitle() == title)
            return book;
}

const Patron& Library::getPatron(const std::string& libraryCardNumber) const {
    for (const auto& patron : m_patrons)
        if (patron.getCardNumber() == libraryCardNumber)
            return patron;
}

const std::vector<std::string> Library::getPatronsWithFees() const {
    std::vector<std::string> names;
    names.reserve(m_patrons.size());
    for (const auto& patron : m_patrons) {
        if (patron.owesLibraryFees())
            names.emplace_back(patron.getName());
    }
    return names;
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Hello, the standard for titles on Code Review is simply to state the task accomplished by the code. Otherwise, nice post. \$\endgroup\$
    – slepic
    May 27 at 16:09

1 Answer 1

4
\$\begingroup\$

Overall impression: very competent for a C++ .

Minor: "copyright" is a single word in English, so the spelling of m_copyRightYear with capital R looks odd and can cause frustration.

std::size_t seems a strange choice to represent a year, since it's not a count of things. int or perhaps short would be better.

With the functions that return references to members, there is a danger of creating a dangling reference if the calling code holds the result for longer than the Book object is alive. That doesn't seem to be the case in this program as it stands, but future changes need to have awareness of that constraint (e.g. as the vector moves its members around).

I'm not a big fan of the genres array in getGenre(), nor the genre_size member in the enumeration. I normally implement such functions using switch (with compiler warnings set to complain if any enum value isn't handled).

In modern C++, we don't need to write operator!= because it can be synthesised from operator==.

This error message should go to std::cerr:

if (!isCheckedOut()) {
    std::cout << "You have not checked out any book.\n";
    return;
}

And it's a misleading message (you haven't checked out this book; you may have other books checked out).

We have implemented our own find() here:

for (const auto& patron : m_patrons) {
    if (patron.getName() == name && patron.getCardNumber() == cardNumber) return true;
}
return false;

If we define a Patron::operator==(), we could use a standard <algorithms> function here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.