Python Arena

A library loan desk

The neighborhood library on Elm Street has a few shelves of books and a loan desk run by volunteers. In this project you write the program for that desk. Days are plain day numbers (day 1, day 2, and so on), so no calendar is needed. A loan lasts 14 days: a book lent on day 3 is due back on day 17.

When it's finished, the program reads the library's books and members from the file library.txt, one per line:

book;B1;Emma;Jane Austen
book;B2;Dracula;Bram Stoker
book;B3;Persuasion;Jane Austen
member;M1;Ada
member;M2;Ben

Then the volunteer at the desk types commands. These ones

books
lend B1 M1
lend B1 M2
day 20
late
return B1
lend B3 M2
member M2
search austen
dance
quit

get these answers:

Day 1 at the desk.
> books
B1 Emma by Jane Austen
B2 Dracula by Bram Stoker
B3 Persuasion by Jane Austen
> lend B1 M1
Lent: Emma (B1) to Ada (M1), due day 15
> lend B1 M2
Emma is out until day 15.
> day 20
It is day 20.
> late
Emma (B1) to Ada (M1), due day 15, 5 days late
> return B1
Emma is back, 5 days late.
> lend B3 M2
Lent: Persuasion (B3) to Ben (M2), due day 34
> member M2
Ben (M2) has 1 book:
Persuasion (B3) to Ben (M2), due day 34
> search austen
B1 Emma by Jane Austen
B3 Persuasion by Jane Austen
> dance
I don't know that command.
> quit
The desk is closed.

Ben can't borrow Emma while Ada has it, so the desk says so instead of lending one book twice.

The classes

The program has four classes, and much of the work is in how their objects point to each other:

  • A Book has a code, a title and an author, and its current loan: None while it's on the shelf.
  • A Member has a library card (the card's code, such as M1), a name, and a list of their loans.
  • A Loan has a book, a member and the day the book is due. Its book and its member are objects, not codes.
  • The Library keeps lists of all its books, members and loans, and does the lending.

What you need

Everything up to module 38: reading a file (module 23), try and except (module 25), and above all classes: attributes and the constructor __init__ (module 34), methods (module 35), __str__ (module 36), objects in lists and as arguments, and is (module 37), and objects as attributes of other objects, and None (module 38).

How you build it

  1. Books and members: the first classes, and a library that lists them.
  2. Finding things: a book by its code, a member by their card, and books by a word.
  3. Loans: a class for a loan, which knows its book and its member.
  4. Returns and late books: give a book back, and find what's late.
  5. A member's books: each member's own loans, and a limit of 3 books.
  6. The desk: read the library from a file and take commands.

The checks of the first five milestones call your classes' methods directly, as the lessons' checks do. The desk itself comes last.

Milestones

  1. Books and members
  2. Finding things
  3. Loans
  4. Returns and late books
  5. A member's books
  6. The desk

Milestone 1: Books and members

The class Book is ready: a book has a code, such as B1, a title and an author, and print shows it as B1 Emma by Jane Austen. The main block uses two classes that don't exist yet. Write them:

  1. Member, with a constructor Member(card, name) that stores the member's library card (its code, such as M1) and name in the attributes card and name, and a __str__ method that returns the name and the card in parentheses: Ada (M1).
  2. Library, whose constructor takes no arguments and creates two empty lists, the attributes books and members, and these methods:
    • add_book(book) adds a Book object to the library, and add_member(member) adds a Member object.
    • print_books() prints the books one per line, in the order they were added, or No books. when there are none.
    • print_members() prints the members the same way, or No members.

The check runs:

library = Library()
library.add_book(Book("B1", "Emma", "Jane Austen"))
library.add_book(Book("B2", "Dracula", "Bram Stoker"))
library.add_member(Member("M1", "Ada"))
library.add_member(Member("M2", "Ben"))
library.print_books()
library.print_members()

and expects:

B1 Emma by Jane Austen
B2 Dracula by Bram Stoker
Ada (M1)
Ben (M2)

Milestone 2: Finding things

At the desk, people say a code or a word, and the program has to find what they mean. Add three methods to Library:

  1. find_book(code) returns the Book object with that code, or None when the library has no such book.
  2. find_member(card) returns the Member object with that card, or None.
  3. search(word) returns a list of the books whose title or author contains the word, in the order of the library's list. Upper and lower case don't matter: austen finds Jane Austen's books, and so does AUSTEN. The string method lower() returns a string in lower case, so compare the lower-case versions.

The check runs:

library = Library()
library.add_book(Book("B1", "Emma", "Jane Austen"))
library.add_book(Book("B2", "Dracula", "Bram Stoker"))
library.add_book(Book("B3", "Persuasion", "Jane Austen"))
library.add_member(Member("M1", "Ada"))
print(library.find_book("B2"))
print(library.find_book("B9"))
print(library.find_member("M1"))
print(library.find_member("M2"))
for book in library.search("austen"):
    print(book)
print(len(library.search("zombies")))

and expects:

B2 Dracula by Bram Stoker
None
Ada (M1)
None
B1 Emma by Jane Austen
B3 Persuasion by Jane Austen
0

Milestone 3: Loans

Now the desk lends books. A loan lasts 14 days: a book lent on day 3 is due on day 17.

  1. Write the class Loan. Its constructor Loan(book, member, due_day) stores a Book object, a Member object and the due day, a whole number, in the attributes book, member and due_day. Its __str__ returns the book's title and code, the member, and the due day: Emma (B1) to Ada (M1), due day 17.
  2. A book gets the attribute loan, which is None when the book is on the shelf and the Loan object while it's lent.
  3. The library gets a third list, the attribute loans, empty at first.
  4. lend(code, card, today) in Library lends the book with that code to the member with that card on the day today, and returns a message as a string, checking in this order:
    • There is no book B9. when no book has that code;
    • There is no member M9. when no member has that card;
    • Emma is out until day 17. when the book is already lent (with the book's title and its loan's due day);
    • otherwise it creates the loan, adds it to the library's loans, sets it as the book's loan, and returns Lent: followed by the loan: Lent: Emma (B1) to Ada (M1), due day 17.
  5. print_loans() prints the loans one per line, in the order they were made, or No loans.
  6. print_books() shows a lent book with its due day after it: B1 Emma by Jane Austen, out until day 17.

The check runs:

library = Library()
library.add_book(Book("B1", "Emma", "Jane Austen"))
library.add_book(Book("B2", "Dracula", "Bram Stoker"))
library.add_member(Member("M1", "Ada"))
library.add_member(Member("M2", "Ben"))
print(library.lend("B1", "M1", 3))
print(library.lend("B1", "M2", 5))
print(library.lend("B9", "M2", 5))
print(library.lend("B2", "M9", 5))
library.print_loans()
library.print_books()

and expects:

Lent: Emma (B1) to Ada (M1), due day 17
Emma is out until day 17.
There is no book B9.
There is no member M9.
Emma (B1) to Ada (M1), due day 17
B1 Emma by Jane Austen, out until day 17
B2 Dracula by Bram Stoker

Milestone 4: Returns and late books

Books come back, some of them late.

  1. Give Loan a method days_late(today), which returns how many days after the due day today is: 3 for a book due on day 17 when today is day 20. A book that isn't late, returned on its due day or before, is 0 days late.
  2. Write a function plural(count, word) outside the classes, which returns the count and the word, with an s added when the count isn't 1: plural(3, "day") returns "3 days", and plural(1, "day") returns "1 day".
  3. give_back(code, today) in Library returns the book with that code on the day today, and returns a message, checking in this order:
    • There is no book B9. when no book has that code;
    • Emma is not on loan. when the book is on the shelf;
    • otherwise the loan is removed from the library's loans, the book's loan is None again, and the message is Emma is back., or Emma is back, 3 days late. for a late book.
  4. print_late(today) prints each loan that is late on the day today, in the order of the loans, with how late it is after it: Emma (B1) to Ada (M1), due day 15, 4 days late. When nothing is late, it prints Nothing is late.

The check runs:

library = Library()
library.add_book(Book("B1", "Emma", "Jane Austen"))
library.add_book(Book("B2", "Dracula", "Bram Stoker"))
library.add_book(Book("B3", "Persuasion", "Jane Austen"))
library.add_member(Member("M1", "Ada"))
library.lend("B1", "M1", 1)
library.lend("B2", "M1", 4)
library.lend("B3", "M1", 10)
library.print_late(19)
print(library.give_back("B1", 19))
print(library.give_back("B1", 19))
print(library.give_back("B3", 20))
library.print_late(19)
library.print_loans()

and expects:

Emma (B1) to Ada (M1), due day 15, 4 days late
Dracula (B2) to Ada (M1), due day 18, 1 day late
Emma is back, 4 days late.
Emma is not on loan.
Persuasion is back.
Dracula (B2) to Ada (M1), due day 18, 1 day late
Dracula (B2) to Ada (M1), due day 18

Milestone 5: A member's books

The library's rule is that a member may have at most 3 books at a time. To know how many books a member has, each member now keeps a list of their own loans.

  1. A member gets the attribute loans, a list that is empty for a new member.

  2. lend adds the new loan to the member's list as well as to the library's, and give_back removes it from both. Before lending, lend checks one more thing, after the other checks: a member who already has 3 books gets Ada already has 3 books. (with their name), and no loan is made.

  3. print_member(card) in Library prints the member and how many books they have, and then their loans, one per line, in the order they were made:

    Ada (M1) has 2 books:
    Emma (B1) to Ada (M1), due day 16
    Dracula (B2) to Ada (M1), due day 16

    With exactly one book, the first line says has 1 book:, and a member without books prints only Ben (M2) has no books. When no member has that card, it prints There is no member M9.

The library's list and the member's list hold the very same Loan objects: a loan isn't copied when you add it to a second list.

The check runs:

library = Library()
library.add_book(Book("B1", "Emma", "Jane Austen"))
library.add_book(Book("B2", "Dracula", "Bram Stoker"))
library.add_book(Book("B3", "Persuasion", "Jane Austen"))
library.add_book(Book("B4", "Frankenstein", "Mary Shelley"))
library.add_member(Member("M1", "Ada"))
library.add_member(Member("M2", "Ben"))
print(library.lend("B1", "M1", 2))
print(library.lend("B2", "M1", 2))
print(library.lend("B3", "M1", 3))
print(library.lend("B4", "M1", 3))
library.print_member("M1")
library.print_member("M2")
print(library.give_back("B2", 9))
print(library.lend("B4", "M1", 9))
library.print_member("M1")

and expects:

Lent: Emma (B1) to Ada (M1), due day 16
Lent: Dracula (B2) to Ada (M1), due day 16
Lent: Persuasion (B3) to Ada (M1), due day 17
Ada already has 3 books.
Ada (M1) has 3 books:
Emma (B1) to Ada (M1), due day 16
Dracula (B2) to Ada (M1), due day 16
Persuasion (B3) to Ada (M1), due day 17
Ben (M2) has no books.
Dracula is back.
Lent: Frankenstein (B4) to Ada (M1), due day 23
Ada (M1) has 3 books:
Emma (B1) to Ada (M1), due day 16
Persuasion (B3) to Ada (M1), due day 17
Frankenstein (B4) to Ada (M1), due day 23

Milestone 6: The desk

Everything is ready for the desk itself. The library's books and members are in the file library.txt, one per line, with the parts separated by ;:

book;B1;Emma;Jane Austen
member;M1;Ada
  1. Write a function read_library(filename), outside the classes, which reads the file and returns a new Library with its books and members, in the order of the file.
  2. Write a function main(), and replace everything in the main block with a call to it. main reads library.txt, starts on day 1, and prints Day 1 at the desk. Then it reads commands with the prompt > until quit:
    • books, loans and late print the books, the loans, and the loans that are late today.
    • lend B1 M1 lends that book to that member today, and return B1 gives that book back today. Both print the message they get.
    • member M1 prints that member and their books.
    • search austen prints the books that match the word, or No books found.
    • day 20 makes it day 20, and prints It is day 20. When the day isn't a whole number, it prints The day must be a whole number. instead, and the day stays as it was.
    • quit prints The desk is closed. and ends the program.
    • Anything else, also a known command with the wrong number of words, prints I don't know that command.

With the library.txt and the commands shown at the top of this page, the program prints:

Day 1 at the desk.
> books
B1 Emma by Jane Austen
B2 Dracula by Bram Stoker
B3 Persuasion by Jane Austen
> lend B1 M1
Lent: Emma (B1) to Ada (M1), due day 15
> lend B1 M2
Emma is out until day 15.
> day 20
It is day 20.
> late
Emma (B1) to Ada (M1), due day 15, 5 days late
> return B1
Emma is back, 5 days late.
> lend B3 M2
Lent: Persuasion (B3) to Ben (M2), due day 34
> member M2
Ben (M2) has 1 book:
Persuasion (B3) to Ben (M2), due day 34
> search austen
B1 Emma by Jane Austen
B3 Persuasion by Jane Austen
> dance
I don't know that command.
> quit
The desk is closed.