{"id":"library-desk","title":"A library loan desk","summary":"The loan desk of a small neighborhood library, built from classes. Books and members, a search, loans that know their book and their member, due days and late returns, a limit per member, and a desk that takes commands.","after":"objects-as-attributes","intro":"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.\n\nWhen it's finished, the program reads the library's books and members from the file `library.txt`, one per line:\n\n```text\nbook;B1;Emma;Jane Austen\nbook;B2;Dracula;Bram Stoker\nbook;B3;Persuasion;Jane Austen\nmember;M1;Ada\nmember;M2;Ben\n```\n\nThen the volunteer at the desk types commands. These ones\n\n```text\nbooks\nlend B1 M1\nlend B1 M2\nday 20\nlate\nreturn B1\nlend B3 M2\nmember M2\nsearch austen\ndance\nquit\n```\n\nget these answers:\n\n```text\nDay 1 at the desk.\n> books\nB1 Emma by Jane Austen\nB2 Dracula by Bram Stoker\nB3 Persuasion by Jane Austen\n> lend B1 M1\nLent: Emma (B1) to Ada (M1), due day 15\n> lend B1 M2\nEmma is out until day 15.\n> day 20\nIt is day 20.\n> late\nEmma (B1) to Ada (M1), due day 15, 5 days late\n> return B1\nEmma is back, 5 days late.\n> lend B3 M2\nLent: Persuasion (B3) to Ben (M2), due day 34\n> member M2\nBen (M2) has 1 book:\nPersuasion (B3) to Ben (M2), due day 34\n> search austen\nB1 Emma by Jane Austen\nB3 Persuasion by Jane Austen\n> dance\nI don't know that command.\n> quit\nThe desk is closed.\n```\n\nBen can't borrow Emma while Ada has it, so the desk says so instead of lending one book twice.\n\n### The classes\n\nThe program has four classes, and much of the work is in how their objects point to each other:\n\n- A `Book` has a code, a title and an author, and its current loan: `None` while it's on the shelf.\n- A `Member` has a library card (the card's code, such as `M1`), a name, and a list of their loans.\n- A `Loan` has a book, a member and the day the book is due. Its book and its member are objects, not codes.\n- The `Library` keeps lists of all its books, members and loans, and does the lending.\n\n### What you need\n\nEverything 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).\n\n### How you build it\n\n1. **Books and members:** the first classes, and a library that lists them.\n2. **Finding things:** a book by its code, a member by their card, and books by a word.\n3. **Loans:** a class for a loan, which knows its book and its member.\n4. **Returns and late books:** give a book back, and find what's late.\n5. **A member's books:** each member's own loans, and a limit of 3 books.\n6. **The desk:** read the library from a file and take commands.\n\nThe checks of the first five milestones call your classes' methods directly, as the lessons' checks do. The desk itself comes last.\n","milestones":[{"id":"books-and-members","title":"Books and members","task":"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:\n\n1. `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)`.\n2. `Library`, whose constructor takes no arguments and creates two empty lists, the attributes `books` and `members`, and these methods:\n   - `add_book(book)` adds a `Book` object to the library, and `add_member(member)` adds a `Member` object.\n   - `print_books()` prints the books one per line, in the order they were added, or `No books.` when there are none.\n   - `print_members()` prints the members the same way, or `No members.`\n\nThe check runs:\n\n```python\nlibrary = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nlibrary.print_books()\nlibrary.print_members()\n```\n\nand expects:\n\n```\nB1 Emma by Jane Austen\nB2 Dracula by Bram Stoker\nAda (M1)\nBen (M2)\n```\n","kind":"code","seed":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\n# Write the classes Member and Library here.\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","solution":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            print(book)\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","hints":["`Member` looks much like `Book`: a constructor with the parameters `self, card, name` that stores both in `self`, and a `__str__` that returns an f-string, `f\"{self.name} ({self.card})\"`.","The constructor of `Library` has only `self` as a parameter, and creates the lists: `self.books = []` and `self.members = []`. `add_book` appends to `self.books`.","`print_books` loops `for book in self.books:` and prints each one: `print(book)` uses the book's `__str__`. Check `len(self.books) == 0` for the message of an empty library."],"tests":[{"name":"Two books and two members","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nlibrary.print_books()\nlibrary.print_members()\n","expect":"B1 Emma by Jane Austen\nB2 Dracula by Bram Stoker\nAda (M1)\nBen (M2)","hidden":false},{"name":"A member's attributes","stdin":"","call":"member = Member(\"M7\", \"Grace\")\nprint(member)\nprint(member.card, member.name)\n","expect":"Grace (M7)\nM7 Grace","hidden":true},{"name":"An empty library","stdin":"","call":"library = Library()\nlibrary.print_books()\nlibrary.print_members()\nprint(library.books, library.members)\n","expect":"No books.\nNo members.\n[] []","hidden":true},{"name":"Books in the order they came","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B9\", \"Little Women\", \"Louisa May Alcott\"))\nlibrary.add_book(Book(\"B3\", \"The Time Machine\", \"H. G. Wells\"))\nlibrary.add_book(Book(\"B5\", \"Frankenstein\", \"Mary Shelley\"))\nlibrary.print_books()\nlibrary.print_members()\nprint(len(library.books))\n","expect":"B9 Little Women by Louisa May Alcott\nB3 The Time Machine by H. G. Wells\nB5 Frankenstein by Mary Shelley\nNo members.\n3","hidden":true}],"require":[],"forbid":[]},{"id":"finding","title":"Finding things","task":"At the desk, people say a code or a word, and the program has to find what they mean. Add three methods to `Library`:\n\n1. `find_book(code)` returns the `Book` object with that code, or `None` when the library has no such book.\n2. `find_member(card)` returns the `Member` object with that card, or `None`.\n3. `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.\n\nThe check runs:\n\n```python\nlibrary = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nprint(library.find_book(\"B2\"))\nprint(library.find_book(\"B9\"))\nprint(library.find_member(\"M1\"))\nprint(library.find_member(\"M2\"))\nfor book in library.search(\"austen\"):\n    print(book)\nprint(len(library.search(\"zombies\")))\n```\n\nand expects:\n\n```\nB2 Dracula by Bram Stoker\nNone\nAda (M1)\nNone\nB1 Emma by Jane Austen\nB3 Persuasion by Jane Austen\n0\n```\n","kind":"code","seed":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            print(book)\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","solution":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            print(book)\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","hints":["`find_book` loops over `self.books` and returns a book as soon as `book.code == code`. After the loop, nothing was found: `return None`.","`search` starts from an empty list, `found = []`, and appends each book that matches. Return the list after the loop, even when it's empty.","A book matches when `word.lower() in book.title.lower() or word.lower() in book.author.lower()`."],"tests":[{"name":"Find by code and by word","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nprint(library.find_book(\"B2\"))\nprint(library.find_book(\"B9\"))\nprint(library.find_member(\"M1\"))\nprint(library.find_member(\"M2\"))\nfor book in library.search(\"austen\"):\n    print(book)\nprint(len(library.search(\"zombies\")))\n","expect":"B2 Dracula by Bram Stoker\nNone\nAda (M1)\nNone\nB1 Emma by Jane Austen\nB3 Persuasion by Jane Austen\n0","hidden":false},{"name":"The very same object","stdin":"","call":"library = Library()\nbook = Book(\"B4\", \"Frankenstein\", \"Mary Shelley\")\nmember = Member(\"M3\", \"Chloe\")\nlibrary.add_book(book)\nlibrary.add_member(member)\nprint(library.find_book(\"B4\") is book)\nprint(library.find_member(\"M3\") is member)\nprint(library.find_member(\"m3\"))\n","expect":"True\nTrue\nNone","hidden":true},{"name":"Searching titles in any case","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"The Time Machine\", \"H. G. Wells\"))\nfor book in library.search(\"ULA\"):\n    print(book)\nfor book in library.search(\"the\"):\n    print(book)\nprint(library.search(\"B1\"))\nprint(Library().find_book(\"B1\"))\n","expect":"B2 Dracula by Bram Stoker\nB3 The Time Machine by H. G. Wells\n[]\nNone","hidden":true}],"require":[],"forbid":[]},{"id":"loans","title":"Loans","task":"Now the desk lends books. A loan lasts 14 days: a book lent on day 3 is due on day 17.\n\n1. 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`.\n2. A book gets the attribute `loan`, which is `None` when the book is on the shelf and the `Loan` object while it's lent.\n3. The library gets a third list, the attribute `loans`, empty at first.\n4. `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:\n   - `There is no book B9.` when no book has that code;\n   - `There is no member M9.` when no member has that card;\n   - `Emma is out until day 17.` when the book is already lent (with the book's title and its loan's due day);\n   - 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`.\n5. `print_loans()` prints the loans one per line, in the order they were made, or `No loans.`\n6. `print_books()` shows a lent book with its due day after it: `B1 Emma by Jane Austen, out until day 17`.\n\nThe check runs:\n\n```python\nlibrary = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nprint(library.lend(\"B1\", \"M1\", 3))\nprint(library.lend(\"B1\", \"M2\", 5))\nprint(library.lend(\"B9\", \"M2\", 5))\nprint(library.lend(\"B2\", \"M9\", 5))\nlibrary.print_loans()\nlibrary.print_books()\n```\n\nand expects:\n\n```\nLent: Emma (B1) to Ada (M1), due day 17\nEmma is out until day 17.\nThere is no book B9.\nThere is no member M9.\nEmma (B1) to Ada (M1), due day 17\nB1 Emma by Jane Austen, out until day 17\nB2 Dracula by Bram Stoker\n```\n","kind":"code","seed":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            print(book)\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","solution":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","hints":["In `Loan.__str__`, the book and the member are objects, so you can reach their attributes: `self.book.title` and `self.book.code`. `{self.member}` in an f-string uses the member's own `__str__`.","In `lend`, find both objects first with `find_book` and `find_member`, and return the message as soon as one of them `is None`. `book.loan is not None` means the book is out.","Create the loan with `Loan(book, member, today + 14)`, then `self.loans.append(loan)` and `book.loan = loan`, and return `f\"Lent: {loan}\"`."],"tests":[{"name":"Lend and list","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nprint(library.lend(\"B1\", \"M1\", 3))\nprint(library.lend(\"B1\", \"M2\", 5))\nprint(library.lend(\"B9\", \"M2\", 5))\nprint(library.lend(\"B2\", \"M9\", 5))\nlibrary.print_loans()\nlibrary.print_books()\n","expect":"Lent: Emma (B1) to Ada (M1), due day 17\nEmma is out until day 17.\nThere is no book B9.\nThere is no member M9.\nEmma (B1) to Ada (M1), due day 17\nB1 Emma by Jane Austen, out until day 17\nB2 Dracula by Bram Stoker","hidden":false},{"name":"A loan on its own","stdin":"","call":"book = Book(\"B5\", \"Frankenstein\", \"Mary Shelley\")\nmember = Member(\"M4\", \"Dev\")\nloan = Loan(book, member, 30)\nprint(loan)\nprint(loan.book is book, loan.member is member, loan.due_day)\nprint(book.loan)\n","expect":"Frankenstein (B5) to Dev (M4), due day 30\nTrue True 30\nNone","hidden":true},{"name":"The loan belongs to the book","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.print_loans()\nprint(library.find_book(\"B1\").loan)\nprint(library.lend(\"B1\", \"M1\", 10))\nloan = library.find_book(\"B1\").loan\nprint(loan.member.name, loan.due_day)\nprint(library.lend(\"B1\", \"M1\", 11))\nprint(library.lend(\"B7\", \"M7\", 11))\nprint(len(library.loans))\n","expect":"No loans.\nNone\nLent: Emma (B1) to Ada (M1), due day 24\nAda 24\nEmma is out until day 24.\nThere is no book B7.\n1","hidden":true},{"name":"Several loans in order","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nprint(library.lend(\"B3\", \"M2\", 1))\nprint(library.lend(\"B1\", \"M1\", 20))\nlibrary.print_loans()\nlibrary.print_books()\n","expect":"Lent: Persuasion (B3) to Ben (M2), due day 15\nLent: Emma (B1) to Ada (M1), due day 34\nPersuasion (B3) to Ben (M2), due day 15\nEmma (B1) to Ada (M1), due day 34\nB1 Emma by Jane Austen, out until day 34\nB2 Dracula by Bram Stoker\nB3 Persuasion by Jane Austen, out until day 15","hidden":true}],"require":[],"forbid":[]},{"id":"returns","title":"Returns and late books","task":"Books come back, some of them late.\n\n1. 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.\n2. 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\"`.\n3. `give_back(code, today)` in `Library` returns the book with that code on the day `today`, and returns a message, checking in this order:\n   - `There is no book B9.` when no book has that code;\n   - `Emma is not on loan.` when the book is on the shelf;\n   - 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.\n4. `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.`\n\nThe check runs:\n\n```python\nlibrary = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.lend(\"B1\", \"M1\", 1)\nlibrary.lend(\"B2\", \"M1\", 4)\nlibrary.lend(\"B3\", \"M1\", 10)\nlibrary.print_late(19)\nprint(library.give_back(\"B1\", 19))\nprint(library.give_back(\"B1\", 19))\nprint(library.give_back(\"B3\", 20))\nlibrary.print_late(19)\nlibrary.print_loans()\n```\n\nand expects:\n\n```\nEmma (B1) to Ada (M1), due day 15, 4 days late\nDracula (B2) to Ada (M1), due day 18, 1 day late\nEmma is back, 4 days late.\nEmma is not on loan.\nPersuasion is back.\nDracula (B2) to Ada (M1), due day 18, 1 day late\nDracula (B2) to Ada (M1), due day 18\n```\n","kind":"code","seed":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","solution":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def days_late(self, today: int):\n        if today > self.due_day:\n            return today - self.due_day\n        return 0\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\ndef plural(count: int, word: str):\n    if count == 1:\n        return f\"{count} {word}\"\n    return f\"{count} {word}s\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def give_back(self, code: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        loan = book.loan\n        if loan is None:\n            return f\"{book.title} is not on loan.\"\n        self.loans.remove(loan)\n        book.loan = None\n        days = loan.days_late(today)\n        if days > 0:\n            return f\"{book.title} is back, {plural(days, 'day')} late.\"\n        return f\"{book.title} is back.\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n    def print_late(self, today: int):\n        late = 0\n        for loan in self.loans:\n            days = loan.days_late(today)\n            if days > 0:\n                print(f\"{loan}, {plural(days, 'day')} late\")\n                late += 1\n        if late == 0:\n            print(\"Nothing is late.\")\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","hints":["`days_late` is an `if`: when `today > self.due_day`, return the difference, and otherwise 0.","In `give_back`, the book's own `loan` attribute tells you which loan to end: `loan = book.loan`. `self.loans.remove(loan)` takes that very object out of the list.","In `print_late`, count the late loans as you print them, and print `Nothing is late.` after the loop when the count is still 0. The text `4 days late` is `f\"{plural(days, 'day')} late\"`."],"tests":[{"name":"Give back on time and late","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.lend(\"B1\", \"M1\", 1)\nlibrary.lend(\"B2\", \"M1\", 4)\nlibrary.lend(\"B3\", \"M1\", 10)\nlibrary.print_late(19)\nprint(library.give_back(\"B1\", 19))\nprint(library.give_back(\"B1\", 19))\nprint(library.give_back(\"B3\", 20))\nlibrary.print_late(19)\nlibrary.print_loans()\n","expect":"Emma (B1) to Ada (M1), due day 15, 4 days late\nDracula (B2) to Ada (M1), due day 18, 1 day late\nEmma is back, 4 days late.\nEmma is not on loan.\nPersuasion is back.\nDracula (B2) to Ada (M1), due day 18, 1 day late\nDracula (B2) to Ada (M1), due day 18","hidden":false},{"name":"Days late and plurals","stdin":"","call":"loan = Loan(Book(\"B5\", \"Frankenstein\", \"Mary Shelley\"), Member(\"M4\", \"Dev\"), 17)\nprint(loan.days_late(10), loan.days_late(17), loan.days_late(18), loan.days_late(30))\nprint(plural(1, \"day\"), plural(2, \"day\"), plural(0, \"book\"), plural(1, \"book\"))\n","expect":"0 0 1 13\n1 day 2 days 0 books 1 book","hidden":true},{"name":"Lent again after a return","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nlibrary.print_late(100)\nprint(library.lend(\"B1\", \"M1\", 1))\nprint(library.give_back(\"B9\", 2))\nprint(library.give_back(\"B1\", 16))\nprint(library.find_book(\"B1\").loan)\nlibrary.print_loans()\nprint(library.lend(\"B1\", \"M2\", 16))\nlibrary.print_books()\nlibrary.print_late(30)\nlibrary.print_late(31)\n","expect":"Nothing is late.\nLent: Emma (B1) to Ada (M1), due day 15\nThere is no book B9.\nEmma is back, 1 day late.\nNone\nNo loans.\nLent: Emma (B1) to Ben (M2), due day 30\nB1 Emma by Jane Austen, out until day 30\nNothing is late.\nEmma (B1) to Ben (M2), due day 30, 1 day late","hidden":true}],"require":[],"forbid":[]},{"id":"members-loans","title":"A member's books","task":"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.\n\n1. A member gets the attribute `loans`, a list that is empty for a new member.\n2. `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.\n3. `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:\n\n   ```text\n   Ada (M1) has 2 books:\n   Emma (B1) to Ada (M1), due day 16\n   Dracula (B2) to Ada (M1), due day 16\n   ```\n\n   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.`\n\nThe 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.\n\nThe check runs:\n\n```python\nlibrary = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B4\", \"Frankenstein\", \"Mary Shelley\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nprint(library.lend(\"B1\", \"M1\", 2))\nprint(library.lend(\"B2\", \"M1\", 2))\nprint(library.lend(\"B3\", \"M1\", 3))\nprint(library.lend(\"B4\", \"M1\", 3))\nlibrary.print_member(\"M1\")\nlibrary.print_member(\"M2\")\nprint(library.give_back(\"B2\", 9))\nprint(library.lend(\"B4\", \"M1\", 9))\nlibrary.print_member(\"M1\")\n```\n\nand expects:\n\n```\nLent: Emma (B1) to Ada (M1), due day 16\nLent: Dracula (B2) to Ada (M1), due day 16\nLent: Persuasion (B3) to Ada (M1), due day 17\nAda already has 3 books.\nAda (M1) has 3 books:\nEmma (B1) to Ada (M1), due day 16\nDracula (B2) to Ada (M1), due day 16\nPersuasion (B3) to Ada (M1), due day 17\nBen (M2) has no books.\nDracula is back.\nLent: Frankenstein (B4) to Ada (M1), due day 23\nAda (M1) has 3 books:\nEmma (B1) to Ada (M1), due day 16\nPersuasion (B3) to Ada (M1), due day 17\nFrankenstein (B4) to Ada (M1), due day 23\n```\n","kind":"code","seed":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def days_late(self, today: int):\n        if today > self.due_day:\n            return today - self.due_day\n        return 0\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\ndef plural(count: int, word: str):\n    if count == 1:\n        return f\"{count} {word}\"\n    return f\"{count} {word}s\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def give_back(self, code: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        loan = book.loan\n        if loan is None:\n            return f\"{book.title} is not on loan.\"\n        self.loans.remove(loan)\n        book.loan = None\n        days = loan.days_late(today)\n        if days > 0:\n            return f\"{book.title} is back, {plural(days, 'day')} late.\"\n        return f\"{book.title} is back.\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n    def print_late(self, today: int):\n        late = 0\n        for loan in self.loans:\n            days = loan.days_late(today)\n            if days > 0:\n                print(f\"{loan}, {plural(days, 'day')} late\")\n                late += 1\n        if late == 0:\n            print(\"Nothing is late.\")\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","solution":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\nMAX_BOOKS = 3\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n        self.loans = []\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def days_late(self, today: int):\n        if today > self.due_day:\n            return today - self.due_day\n        return 0\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\ndef plural(count: int, word: str):\n    if count == 1:\n        return f\"{count} {word}\"\n    return f\"{count} {word}s\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        if len(member.loans) >= MAX_BOOKS:\n            return f\"{member.name} already has {MAX_BOOKS} books.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        member.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def give_back(self, code: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        loan = book.loan\n        if loan is None:\n            return f\"{book.title} is not on loan.\"\n        self.loans.remove(loan)\n        loan.member.loans.remove(loan)\n        book.loan = None\n        days = loan.days_late(today)\n        if days > 0:\n            return f\"{book.title} is back, {plural(days, 'day')} late.\"\n        return f\"{book.title} is back.\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n    def print_late(self, today: int):\n        late = 0\n        for loan in self.loans:\n            days = loan.days_late(today)\n            if days > 0:\n                print(f\"{loan}, {plural(days, 'day')} late\")\n                late += 1\n        if late == 0:\n            print(\"Nothing is late.\")\n\n    def print_member(self, card: str):\n        member = self.find_member(card)\n        if member is None:\n            print(f\"There is no member {card}.\")\n        elif len(member.loans) == 0:\n            print(f\"{member} has no books.\")\n        else:\n            print(f\"{member} has {plural(len(member.loans), 'book')}:\")\n            for loan in member.loans:\n                print(loan)\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","hints":["Add `self.loans = []` to the constructor of `Member`. In `lend`, `len(member.loans) >= 3` is the new check, and after creating the loan, `member.loans.append(loan)` next to the library's own append.","In `give_back`, the loan knows its member: `loan.member.loans.remove(loan)` takes it off the member's list.","`print_member` uses `find_member` and checks for `None` first. The `plural` function from the last milestone gives `1 book` and `3 books`."],"tests":[{"name":"Three books at most","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_book(Book(\"B3\", \"Persuasion\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B4\", \"Frankenstein\", \"Mary Shelley\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nprint(library.lend(\"B1\", \"M1\", 2))\nprint(library.lend(\"B2\", \"M1\", 2))\nprint(library.lend(\"B3\", \"M1\", 3))\nprint(library.lend(\"B4\", \"M1\", 3))\nlibrary.print_member(\"M1\")\nlibrary.print_member(\"M2\")\nprint(library.give_back(\"B2\", 9))\nprint(library.lend(\"B4\", \"M1\", 9))\nlibrary.print_member(\"M1\")\n","expect":"Lent: Emma (B1) to Ada (M1), due day 16\nLent: Dracula (B2) to Ada (M1), due day 16\nLent: Persuasion (B3) to Ada (M1), due day 17\nAda already has 3 books.\nAda (M1) has 3 books:\nEmma (B1) to Ada (M1), due day 16\nDracula (B2) to Ada (M1), due day 16\nPersuasion (B3) to Ada (M1), due day 17\nBen (M2) has no books.\nDracula is back.\nLent: Frankenstein (B4) to Ada (M1), due day 23\nAda (M1) has 3 books:\nEmma (B1) to Ada (M1), due day 16\nPersuasion (B3) to Ada (M1), due day 17\nFrankenstein (B4) to Ada (M1), due day 23","hidden":false},{"name":"One book and no member","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nprint(library.lend(\"B2\", \"M2\", 5))\nlibrary.print_member(\"M2\")\nlibrary.print_member(\"M1\")\nlibrary.print_member(\"M9\")\nprint(library.give_back(\"B2\", 5))\nlibrary.print_member(\"M2\")\nprint(Member(\"M5\", \"Eve\").loans)\n","expect":"Lent: Dracula (B2) to Ben (M2), due day 19\nBen (M2) has 1 book:\nDracula (B2) to Ben (M2), due day 19\nAda (M1) has no books.\nThere is no member M9.\nDracula is back.\nBen (M2) has no books.\n[]","hidden":true},{"name":"The same loan in two lists","stdin":"","call":"library = Library()\nlibrary.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.lend(\"B1\", \"M1\", 1)\nmember = library.find_member(\"M1\")\nprint(member.loans[0] is library.loans[0])\nprint(member.loans[0] is library.find_book(\"B1\").loan)\nprint(library.give_back(\"B1\", 40))\nprint(len(member.loans), len(library.loans))\n","expect":"True\nTrue\nEmma is back, 25 days late.\n0 0","hidden":true},{"name":"The limit holds for each member alone","stdin":"","call":"library = Library()\nfor number in range(1, 8):\n    library.add_book(Book(f\"B{number}\", f\"Book {number}\", \"Anonymous\"))\nlibrary.add_member(Member(\"M1\", \"Ada\"))\nlibrary.add_member(Member(\"M2\", \"Ben\"))\nfor number in range(1, 5):\n    print(library.lend(f\"B{number}\", \"M1\", number))\nfor number in range(4, 8):\n    print(library.lend(f\"B{number}\", \"M2\", 10))\nlibrary.print_member(\"M2\")\n","expect":"Lent: Book 1 (B1) to Ada (M1), due day 15\nLent: Book 2 (B2) to Ada (M1), due day 16\nLent: Book 3 (B3) to Ada (M1), due day 17\nAda already has 3 books.\nLent: Book 4 (B4) to Ben (M2), due day 24\nLent: Book 5 (B5) to Ben (M2), due day 24\nLent: Book 6 (B6) to Ben (M2), due day 24\nBen already has 3 books.\nBen (M2) has 3 books:\nBook 4 (B4) to Ben (M2), due day 24\nBook 5 (B5) to Ben (M2), due day 24\nBook 6 (B6) to Ben (M2), due day 24","hidden":true}],"require":[],"forbid":[]},{"id":"the-desk","title":"The desk","task":"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 `;`:\n\n```text\nbook;B1;Emma;Jane Austen\nmember;M1;Ada\n```\n\n1. 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.\n2. 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`:\n   - `books`, `loans` and `late` print the books, the loans, and the loans that are late today.\n   - `lend B1 M1` lends that book to that member today, and `return B1` gives that book back today. Both print the message they get.\n   - `member M1` prints that member and their books.\n   - `search austen` prints the books that match the word, or `No books found.`\n   - `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.\n   - `quit` prints `The desk is closed.` and ends the program.\n   - Anything else, also a known command with the wrong number of words, prints `I don't know that command.`\n\nWith the `library.txt` and the commands shown at the top of this page, the program prints:\n\n```\nDay 1 at the desk.\n> books\nB1 Emma by Jane Austen\nB2 Dracula by Bram Stoker\nB3 Persuasion by Jane Austen\n> lend B1 M1\nLent: Emma (B1) to Ada (M1), due day 15\n> lend B1 M2\nEmma is out until day 15.\n> day 20\nIt is day 20.\n> late\nEmma (B1) to Ada (M1), due day 15, 5 days late\n> return B1\nEmma is back, 5 days late.\n> lend B3 M2\nLent: Persuasion (B3) to Ben (M2), due day 34\n> member M2\nBen (M2) has 1 book:\nPersuasion (B3) to Ben (M2), due day 34\n> search austen\nB1 Emma by Jane Austen\nB3 Persuasion by Jane Austen\n> dance\nI don't know that command.\n> quit\nThe desk is closed.\n```\n","kind":"code","seed":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\nMAX_BOOKS = 3\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n        self.loans = []\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def days_late(self, today: int):\n        if today > self.due_day:\n            return today - self.due_day\n        return 0\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\ndef plural(count: int, word: str):\n    if count == 1:\n        return f\"{count} {word}\"\n    return f\"{count} {word}s\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        if len(member.loans) >= MAX_BOOKS:\n            return f\"{member.name} already has {MAX_BOOKS} books.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        member.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def give_back(self, code: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        loan = book.loan\n        if loan is None:\n            return f\"{book.title} is not on loan.\"\n        self.loans.remove(loan)\n        loan.member.loans.remove(loan)\n        book.loan = None\n        days = loan.days_late(today)\n        if days > 0:\n            return f\"{book.title} is back, {plural(days, 'day')} late.\"\n        return f\"{book.title} is back.\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n    def print_late(self, today: int):\n        late = 0\n        for loan in self.loans:\n            days = loan.days_late(today)\n            if days > 0:\n                print(f\"{loan}, {plural(days, 'day')} late\")\n                late += 1\n        if late == 0:\n            print(\"Nothing is late.\")\n\n    def print_member(self, card: str):\n        member = self.find_member(card)\n        if member is None:\n            print(f\"There is no member {card}.\")\n        elif len(member.loans) == 0:\n            print(f\"{member} has no books.\")\n        else:\n            print(f\"{member} has {plural(len(member.loans), 'book')}:\")\n            for loan in member.loans:\n                print(loan)\n\n\nif __name__ == \"__main__\":\n    library = Library()\n    library.add_book(Book(\"B1\", \"Emma\", \"Jane Austen\"))\n    library.add_book(Book(\"B2\", \"Dracula\", \"Bram Stoker\"))\n    library.add_member(Member(\"M1\", \"Ada\"))\n    library.print_books()\n    library.print_members()\n","solution":"# The loan desk of a small library. Days are plain numbers: day 1, day 2, and so on.\n\nLOAN_DAYS = 14\nMAX_BOOKS = 3\n\n\nclass Book:\n    def __init__(self, code: str, title: str, author: str):\n        self.code = code\n        self.title = title\n        self.author = author\n        self.loan = None\n\n    def __str__(self):\n        return f\"{self.code} {self.title} by {self.author}\"\n\n\nclass Member:\n    def __init__(self, card: str, name: str):\n        self.card = card\n        self.name = name\n        self.loans = []\n\n    def __str__(self):\n        return f\"{self.name} ({self.card})\"\n\n\nclass Loan:\n    def __init__(self, book: Book, member: Member, due_day: int):\n        self.book = book\n        self.member = member\n        self.due_day = due_day\n\n    def days_late(self, today: int):\n        if today > self.due_day:\n            return today - self.due_day\n        return 0\n\n    def __str__(self):\n        return f\"{self.book.title} ({self.book.code}) to {self.member}, due day {self.due_day}\"\n\n\ndef plural(count: int, word: str):\n    if count == 1:\n        return f\"{count} {word}\"\n    return f\"{count} {word}s\"\n\n\nclass Library:\n    def __init__(self):\n        self.books = []\n        self.members = []\n        self.loans = []\n\n    def add_book(self, book: Book):\n        self.books.append(book)\n\n    def add_member(self, member: Member):\n        self.members.append(member)\n\n    def print_books(self):\n        if len(self.books) == 0:\n            print(\"No books.\")\n        for book in self.books:\n            if book.loan is None:\n                print(book)\n            else:\n                print(f\"{book}, out until day {book.loan.due_day}\")\n\n    def print_members(self):\n        if len(self.members) == 0:\n            print(\"No members.\")\n        for member in self.members:\n            print(member)\n\n    def find_book(self, code: str):\n        for book in self.books:\n            if book.code == code:\n                return book\n        return None\n\n    def find_member(self, card: str):\n        for member in self.members:\n            if member.card == card:\n                return member\n        return None\n\n    def search(self, word: str):\n        found = []\n        for book in self.books:\n            if word.lower() in book.title.lower() or word.lower() in book.author.lower():\n                found.append(book)\n        return found\n\n    def lend(self, code: str, card: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        member = self.find_member(card)\n        if member is None:\n            return f\"There is no member {card}.\"\n        if book.loan is not None:\n            return f\"{book.title} is out until day {book.loan.due_day}.\"\n        if len(member.loans) >= MAX_BOOKS:\n            return f\"{member.name} already has {MAX_BOOKS} books.\"\n        loan = Loan(book, member, today + LOAN_DAYS)\n        self.loans.append(loan)\n        member.loans.append(loan)\n        book.loan = loan\n        return f\"Lent: {loan}\"\n\n    def give_back(self, code: str, today: int):\n        book = self.find_book(code)\n        if book is None:\n            return f\"There is no book {code}.\"\n        loan = book.loan\n        if loan is None:\n            return f\"{book.title} is not on loan.\"\n        self.loans.remove(loan)\n        loan.member.loans.remove(loan)\n        book.loan = None\n        days = loan.days_late(today)\n        if days > 0:\n            return f\"{book.title} is back, {plural(days, 'day')} late.\"\n        return f\"{book.title} is back.\"\n\n    def print_loans(self):\n        if len(self.loans) == 0:\n            print(\"No loans.\")\n        for loan in self.loans:\n            print(loan)\n\n    def print_late(self, today: int):\n        late = 0\n        for loan in self.loans:\n            days = loan.days_late(today)\n            if days > 0:\n                print(f\"{loan}, {plural(days, 'day')} late\")\n                late += 1\n        if late == 0:\n            print(\"Nothing is late.\")\n\n    def print_member(self, card: str):\n        member = self.find_member(card)\n        if member is None:\n            print(f\"There is no member {card}.\")\n        elif len(member.loans) == 0:\n            print(f\"{member} has no books.\")\n        else:\n            print(f\"{member} has {plural(len(member.loans), 'book')}:\")\n            for loan in member.loans:\n                print(loan)\n\n\ndef read_library(filename: str):\n    library = Library()\n    with open(filename) as file:\n        for line in file:\n            parts = line.strip().split(\";\")\n            if parts[0] == \"book\":\n                library.add_book(Book(parts[1], parts[2], parts[3]))\n            elif parts[0] == \"member\":\n                library.add_member(Member(parts[1], parts[2]))\n    return library\n\n\ndef main():\n    library = read_library(\"library.txt\")\n    today = 1\n    print(f\"Day {today} at the desk.\")\n    while True:\n        command = input(\"> \")\n        words = command.split()\n        if command == \"quit\":\n            print(\"The desk is closed.\")\n            break\n        if command == \"books\":\n            library.print_books()\n        elif command == \"loans\":\n            library.print_loans()\n        elif command == \"late\":\n            library.print_late(today)\n        elif len(words) == 3 and words[0] == \"lend\":\n            print(library.lend(words[1], words[2], today))\n        elif len(words) == 2 and words[0] == \"return\":\n            print(library.give_back(words[1], today))\n        elif len(words) == 2 and words[0] == \"member\":\n            library.print_member(words[1])\n        elif len(words) == 2 and words[0] == \"search\":\n            books = library.search(words[1])\n            if len(books) == 0:\n                print(\"No books found.\")\n            for book in books:\n                print(book)\n        elif len(words) == 2 and words[0] == \"day\":\n            try:\n                today = int(words[1])\n                print(f\"It is day {today}.\")\n            except ValueError:\n                print(\"The day must be a whole number.\")\n        else:\n            print(\"I don't know that command.\")\n\n\nif __name__ == \"__main__\":\n    main()\n","hints":["`read_library` reads the file as in module 23: `with open(filename) as file:`, then for each line `parts = line.strip().split(\";\")`. A line whose `parts[0]` is `\"book\"` becomes `Book(parts[1], parts[2], parts[3])`, added with `add_book`.","In `main`, keep `today = 1` before the loop. Split each command into words, and tell the commands apart by the first word and the number of words: `len(words) == 3 and words[0] == \"lend\"`.","For `day`, put `today = int(words[1])` inside `try:`, and catch the `ValueError` that `int` raises for a text that isn't a whole number."],"tests":[{"name":"A day at the desk","stdin":"books\nlend B1 M1\nlend B1 M2\nday 20\nlate\nreturn B1\nlend B3 M2\nmember M2\nsearch austen\ndance\nquit\n","files":{"library.txt":"book;B1;Emma;Jane Austen\nbook;B2;Dracula;Bram Stoker\nbook;B3;Persuasion;Jane Austen\nmember;M1;Ada\nmember;M2;Ben\n"},"expect":"Day 1 at the desk.\n> books\nB1 Emma by Jane Austen\nB2 Dracula by Bram Stoker\nB3 Persuasion by Jane Austen\n> lend B1 M1\nLent: Emma (B1) to Ada (M1), due day 15\n> lend B1 M2\nEmma is out until day 15.\n> day 20\nIt is day 20.\n> late\nEmma (B1) to Ada (M1), due day 15, 5 days late\n> return B1\nEmma is back, 5 days late.\n> lend B3 M2\nLent: Persuasion (B3) to Ben (M2), due day 34\n> member M2\nBen (M2) has 1 book:\nPersuasion (B3) to Ben (M2), due day 34\n> search austen\nB1 Emma by Jane Austen\nB3 Persuasion by Jane Austen\n> dance\nI don't know that command.\n> quit\nThe desk is closed.","hidden":false},{"name":"Mistakes at the desk","stdin":"day soon\nday 7.5\nlend B9 M1\nlend B1\nlend B1 M9\nreturn B2\nreturn\nmember M9\nsearch zombies\nLOANS\nloans\nlate\nquit\n","files":{"library.txt":"book;B1;Emma;Jane Austen\nbook;B2;Dracula;Bram Stoker\nmember;M1;Ada\n"},"expect":"Day 1 at the desk.\n> day soon\nThe day must be a whole number.\n> day 7.5\nThe day must be a whole number.\n> lend B9 M1\nThere is no book B9.\n> lend B1\nI don't know that command.\n> lend B1 M9\nThere is no member M9.\n> return B2\nDracula is not on loan.\n> return\nI don't know that command.\n> member M9\nThere is no member M9.\n> search zombies\nNo books found.\n> LOANS\nI don't know that command.\n> loans\nNo loans.\n> late\nNothing is late.\n> quit\nThe desk is closed.","hidden":true},{"name":"Four books for one member","stdin":"lend B1 M1\nday 3\nlend B2 M1\nlend B3 M1\nlend B4 M1\nbooks\nloans\nday 18\nlate\nreturn B2\nreturn B1\nlend B4 M1\nmember M1\nmember M2\nquit\n","files":{"library.txt":"member;M1;Ada\nbook;B1;Emma;Jane Austen\nbook;B2;Dracula;Bram Stoker\nbook;B3;Persuasion;Jane Austen\nbook;B4;Frankenstein;Mary Shelley\nmember;M2;Ben\n"},"expect":"Day 1 at the desk.\n> lend B1 M1\nLent: Emma (B1) to Ada (M1), due day 15\n> day 3\nIt is day 3.\n> lend B2 M1\nLent: Dracula (B2) to Ada (M1), due day 17\n> lend B3 M1\nLent: Persuasion (B3) to Ada (M1), due day 17\n> lend B4 M1\nAda already has 3 books.\n> books\nB1 Emma by Jane Austen, out until day 15\nB2 Dracula by Bram Stoker, out until day 17\nB3 Persuasion by Jane Austen, out until day 17\nB4 Frankenstein by Mary Shelley\n> loans\nEmma (B1) to Ada (M1), due day 15\nDracula (B2) to Ada (M1), due day 17\nPersuasion (B3) to Ada (M1), due day 17\n> day 18\nIt is day 18.\n> late\nEmma (B1) to Ada (M1), due day 15, 3 days late\nDracula (B2) to Ada (M1), due day 17, 1 day late\nPersuasion (B3) to Ada (M1), due day 17, 1 day late\n> return B2\nDracula is back, 1 day late.\n> return B1\nEmma is back, 3 days late.\n> lend B4 M1\nLent: Frankenstein (B4) to Ada (M1), due day 32\n> member M1\nAda (M1) has 2 books:\nPersuasion (B3) to Ada (M1), due day 17\nFrankenstein (B4) to Ada (M1), due day 32\n> member M2\nBen (M2) has no books.\n> quit\nThe desk is closed.","hidden":true},{"name":"Reading the library file","stdin":"","call":"library = read_library(\"library.txt\")\nlibrary.print_books()\nlibrary.print_members()\nprint(library.find_book(\"B8\").author)\n","files":{"library.txt":"book;B7;The Time Machine;H. G. Wells\nmember;M3;Chloe\nbook;B8;Little Women;Louisa May Alcott\n"},"expect":"B7 The Time Machine by H. G. Wells\nB8 Little Women by Louisa May Alcott\nChloe (M3)\nLouisa May Alcott","hidden":true}],"require":[],"forbid":[]}]}