{
  "id": "P012",
  "slug": "library-loans",
  "key": "P012-library-loans",
  "title": "Library loans",
  "summary": "Lend books to members and take them back: due dates on a real calendar (leap years included), at most three books each, fines for late returns that must be paid before borrowing again, and new members - from a text menu, with the date moved forward by hand.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P012 - Library loans\n\nA small library of six books and its members. Members borrow books and bring\nthem back; every loan has a due date, a late book earns a fine, and new\nmembers can join. There is no clock: the librarian types today's date at the\nstart and moves it forward by hand, so the same session always gives the\nsame dates.\n\n- `main.eml` - the menu and its questions, with their checks\n- `library.eml` - the books and the lending rules: who may borrow, when a\n  loan is late, and what the fine is\n- `dates.eml` - calendar dates: the leap-year rule, the days in a month,\n  counting the days between two dates, adding days to a date, and reading and\n  writing YYYY-MM-DD\n- `text.eml` - trimming, lower case, reading a whole number, money and plurals\n\nThe rules: a loan lasts 14 days; a member has at most 3 books at once, and\ncannot borrow while owing a fine; a book returned late costs 0.25 for every\nday past its due date, at most 5.00 a book. The fine is added to what the\nmember owes, and paying clears it. The loans list shows, for every late\nbook, the days it is late and the fine so far.\n\nDates are real Gregorian dates from 2000 to 2099. A year divisible by 4 is a\nleap year, except one divisible by 100, except one divisible by 400 after all,\nso a loan made on 2028-02-20 is due on 2028-03-05 and one made on 2028-02-29\nis due on 2028-03-14. Days are counted from 2000-01-01, which makes the days\nlate a subtraction.\n\nWhat is checked: a date is written YYYY-MM-DD, with a month that exists and a\nday that month has (2027-02-29 is refused, 2028-02-29 is not), and today's\ndate only moves forward; a member or book number is one that exists; a new\nmember's name has at most 20 characters and is not already taken in any\ncase. Anything else asks again; nothing cancels, except for the first date,\nwhich is needed.\n\nSessions: `sessions/basic.in` starts on 2028-02-20, lends two books to one\nmember and one to another, moves the date until two loans are late, takes one\nback with a fine, refuses a loan until the fine is paid, takes a book back on\ntime, adds a member, and lists the members and the books;\n`sessions/bad-input.in` types dates that are not dates, out of range or\nbefore today, picks numbers that do not exist, borrows a book that is out,\ngoes over three books, returns a book that is on the shelf, pays when nothing\nis owed, lets three loans run 27 days late - the fine stops at 5.00 - and\ntries a name that is too long and one that is taken.\n\nBuilt on the verified corpus cases `library-catalog` (books lent and\nreturned, and a loan refused when a book is already out) and\n`days-between-dates` (the days between two dates, with the full leap-year\nrule).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P012 library loans: lend books to members and take them back, with due\n# dates on a real calendar and fines for late books. There is no clock: the\n# librarian types today's date at the start and moves it forward by hand.\nimport dates\nimport library\nimport text\n\ndef ask_number(prompt, low, high, message):\n    # A whole number from low to high, asked again until one is typed; -1 if\n    # the answer is empty, which cancels.\n    while True:\n        text.trim(input(prompt)) => answer\n        if answer == \"\":\n            return 0 - 1\n        text.number(answer) => n\n        if n >= low and n <= high:\n            return n\n        message ^0\n\ndef ask_date(prompt, today):\n    # A date typed as YYYY-MM-DD, asked again until one is typed. With a\n    # today, the date may not be before it, and an empty answer cancels\n    # (None); without one, an answer is needed.\n    while True:\n        text.trim(input(prompt)) => answer\n        if answer == \"\" and today != None:\n            return None\n        dates.parse(answer) => r\n        if r[0] == None:\n            r[1] ^0\n        elif today != None and dates.days_between(today, r[0]) < 0:\n            (\"Dates only move forward; today is \" + dates.show(today) + \".\") ^0\n        else:\n            return r[0]\n\ndef ask_name(prompt, members):\n    # A new member's name, asked again until a good one is typed; \"\" cancels.\n    while True:\n        text.trim(input(prompt)) => name\n        if name == \"\":\n            return \"\"\n        if len(name) > 20:\n            \"A name has at most 20 characters.\" ^0\n        elif library.member_called(members, name) != None:\n            (\"There is already a member called \" + library.member_called(members, name)[1] + \".\") ^0\n        else:\n            return name\n\ndef ask_member(members):\n    ask_number(\"member number> \", 1, len(members), \"Type a member number from 1 to \" + str(len(members)) + \", or nothing to cancel.\") => n\n    if n == 0 - 1:\n        return None\n    return library.find(members, n)\n\ndef ask_book():\n    ask_number(\"book number> \", 1, len(library.books), \"Type a book number from 1 to \" + str(len(library.books)) + \", or nothing to cancel.\") => n\n    if n == 0 - 1:\n        return None\n    return library.find(library.books, n)\n\n\"== Library loans ==\" ^0\nask_date(\"today's date (YYYY-MM-DD)> \", None) => today\n[[1, \"Ana\", 0], [2, \"Ben\", 0]] => members\n[] => loans\nTrue => running\nwhile running:\n    \"\" ^0\n    (\"== Library == today is \" + dates.show(today)) ^0\n    \"1) books  2) members  3) borrow  4) return  5) loans\" ^0\n    \"6) pay fines  7) change date  8) new member  9) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        \"\" ^0\n        \"-- books --\" ^0\n        for b in library.books:\n            library.loan_of(loans, b) => l\n            \"on the shelf\" => where\n            if l != None:\n                \"due back \" + dates.show(l[2]) => where\n                if library.days_late(l, today) > 0:\n                    where + \" (late)\" => where\n            (\"  \" + str(b[0]) + \") \" + (\"%-21s\" % b[1]) + (\"%-19s\" % b[2]) + where) ^0\n    elif choice == \"2\":\n        \"\" ^0\n        \"-- members --\" ^0\n        for m in members:\n            (\"  #\" + str(m[0]) + \" \" + (\"%-21s\" % m[1]) + (\"%-9s\" % text.plural(library.count_loans(loans, m), \"book\")) + \"owes \" + text.money(m[2])) ^0\n    elif choice == \"3\":\n        ask_member(members) => m\n        if m == None:\n            \"Cancelled.\" ^0\n        elif library.why_not_borrow(loans, m) != \"\":\n            library.why_not_borrow(loans, m) ^0\n        else:\n            ask_book() => b\n            if b == None:\n                \"Cancelled.\" ^0\n            elif library.loan_of(loans, b) != None:\n                (b[1] + \" is on loan until \" + dates.show(library.loan_of(loans, b)[2]) + \".\") ^0\n            else:\n                dates.add_days(today, library.loan_days) => due\n                loans + [[b, m, due]] => loans\n                (m[1] + \" borrows \" + b[1] + \", due \" + dates.show(due) + \".\") ^0\n    elif choice == \"4\":\n        if len(loans) == 0:\n            \"Nothing is on loan.\" ^0\n        else:\n            ask_book() => b\n            if b == None:\n                \"Cancelled.\" ^0\n            elif library.loan_of(loans, b) == None:\n                (b[1] + \" is not on loan.\") ^0\n            else:\n                library.loan_of(loans, b) => l\n                library.without(loans, b) => loans\n                library.days_late(l, today) => late\n                if late == 0:\n                    (b[1] + \" is back from \" + l[1][1] + \", on time.\") ^0\n                else:\n                    library.fine(late) => f\n                    l[1][2] + f => l[1][2]\n                    \"\" => most\n                    if f == library.most_fine:\n                        \" (the most for one book)\" => most\n                    (b[1] + \" is back from \" + l[1][1] + \", \" + text.plural(late, \"day\") + \" late: fine \" + text.money(f) + most + \"; \" + l[1][1] + \" owes \" + text.money(l[1][2]) + \".\") ^0\n    elif choice == \"5\":\n        if len(loans) == 0:\n            \"Nothing is on loan.\" ^0\n        else:\n            \"\" ^0\n            \"-- loans: due date, book, member --\" ^0\n            for l in loans:\n                (\"  \" + dates.show(l[2]) + \"  \" + (\"%-21s\" % l[0][1]) + l[1][1] + library.late_note(l, today)) ^0\n    elif choice == \"6\":\n        ask_member(members) => m\n        if m == None:\n            \"Cancelled.\" ^0\n        elif m[2] == 0:\n            (m[1] + \" owes nothing.\") ^0\n        else:\n            (m[1] + \" pays \" + text.money(m[2]) + \"; nothing is owed now.\") ^0\n            0 => m[2]\n    elif choice == \"7\":\n        ask_date(\"new date (YYYY-MM-DD)> \", today) => d\n        if d == None:\n            \"Cancelled.\" ^0\n        else:\n            d => today\n            0 => late\n            for l in loans:\n                if library.days_late(l, today) > 0:\n                    late + 1 => late\n            if late == 0:\n                (\"Today is now \" + dates.show(today) + \".\") ^0\n            elif late == 1:\n                (\"Today is now \" + dates.show(today) + \". 1 loan is late.\") ^0\n            else:\n                (\"Today is now \" + dates.show(today) + \". \" + str(late) + \" loans are late.\") ^0\n    elif choice == \"8\":\n        ask_name(\"name> \", members) => name\n        if name == \"\":\n            \"Cancelled.\" ^0\n        else:\n            len(members) + 1 => n\n            members + [[n, name, 0]] => members\n            (\"Member #\" + str(n) + \": \" + name + \".\") ^0\n    elif choice == \"9\":\n        False => running\n    else:\n        \"Pick a number from 1 to 9.\" ^0\n\"Bye.\" ^0\n",
      "python": "import dates\nimport library\nimport text\n\ndef ask_number(prompt, low, high, message):\n    while True:\n        answer = text.trim(input(prompt))\n        if answer == \"\":\n            return 0 - 1\n        n = text.number(answer)\n        if n >= low and n <= high:\n            return n\n        print(message)\n\ndef ask_date(prompt, today):\n    while True:\n        answer = text.trim(input(prompt))\n        if answer == \"\" and today != None:\n            return None\n        r = dates.parse(answer)\n        if r[0] == None:\n            print(r[1])\n        elif today != None and dates.days_between(today, r[0]) < 0:\n            print(\"Dates only move forward; today is \" + dates.show(today) + \".\")\n        else:\n            return r[0]\n\ndef ask_name(prompt, members):\n    while True:\n        name = text.trim(input(prompt))\n        if name == \"\":\n            return \"\"\n        if len(name) > 20:\n            print(\"A name has at most 20 characters.\")\n        elif library.member_called(members, name) != None:\n            print(\"There is already a member called \" + library.member_called(members, name)[1] + \".\")\n        else:\n            return name\n\ndef ask_member(members):\n    n = ask_number(\"member number> \", 1, len(members), \"Type a member number from 1 to \" + str(len(members)) + \", or nothing to cancel.\")\n    if n == 0 - 1:\n        return None\n    return library.find(members, n)\n\ndef ask_book():\n    n = ask_number(\"book number> \", 1, len(library.books), \"Type a book number from 1 to \" + str(len(library.books)) + \", or nothing to cancel.\")\n    if n == 0 - 1:\n        return None\n    return library.find(library.books, n)\n\nprint(\"== Library loans ==\")\ntoday = ask_date(\"today's date (YYYY-MM-DD)> \", None)\nmembers = [[1, \"Ana\", 0], [2, \"Ben\", 0]]\nloans = []\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Library == today is \" + dates.show(today))\n    print(\"1) books  2) members  3) borrow  4) return  5) loans\")\n    print(\"6) pay fines  7) change date  8) new member  9) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\":\n        print(\"\")\n        print(\"-- books --\")\n        for b in library.books:\n            l = library.loan_of(loans, b)\n            where = \"on the shelf\"\n            if l != None:\n                where = \"due back \" + dates.show(l[2])\n                if library.days_late(l, today) > 0:\n                    where = where + \" (late)\"\n            print(\"  \" + str(b[0]) + \") \" + \"%-21s\" % b[1] + \"%-19s\" % b[2] + where)\n    elif choice == \"2\":\n        print(\"\")\n        print(\"-- members --\")\n        for m in members:\n            print(\"  #\" + str(m[0]) + \" \" + \"%-21s\" % m[1] + \"%-9s\" % text.plural(library.count_loans(loans, m), \"book\") + \"owes \" + text.money(m[2]))\n    elif choice == \"3\":\n        m = ask_member(members)\n        if m == None:\n            print(\"Cancelled.\")\n        elif library.why_not_borrow(loans, m) != \"\":\n            print(library.why_not_borrow(loans, m))\n        else:\n            b = ask_book()\n            if b == None:\n                print(\"Cancelled.\")\n            elif library.loan_of(loans, b) != None:\n                print(b[1] + \" is on loan until \" + dates.show(library.loan_of(loans, b)[2]) + \".\")\n            else:\n                due = dates.add_days(today, library.loan_days)\n                loans = loans + [[b, m, due]]\n                print(m[1] + \" borrows \" + b[1] + \", due \" + dates.show(due) + \".\")\n    elif choice == \"4\":\n        if len(loans) == 0:\n            print(\"Nothing is on loan.\")\n        else:\n            b = ask_book()\n            if b == None:\n                print(\"Cancelled.\")\n            elif library.loan_of(loans, b) == None:\n                print(b[1] + \" is not on loan.\")\n            else:\n                l = library.loan_of(loans, b)\n                loans = library.without(loans, b)\n                late = library.days_late(l, today)\n                if late == 0:\n                    print(b[1] + \" is back from \" + l[1][1] + \", on time.\")\n                else:\n                    f = library.fine(late)\n                    l[1][2] = l[1][2] + f\n                    most = \"\"\n                    if f == library.most_fine:\n                        most = \" (the most for one book)\"\n                    print(b[1] + \" is back from \" + l[1][1] + \", \" + text.plural(late, \"day\") + \" late: fine \" + text.money(f) + most + \"; \" + l[1][1] + \" owes \" + text.money(l[1][2]) + \".\")\n    elif choice == \"5\":\n        if len(loans) == 0:\n            print(\"Nothing is on loan.\")\n        else:\n            print(\"\")\n            print(\"-- loans: due date, book, member --\")\n            for l in loans:\n                print(\"  \" + dates.show(l[2]) + \"  \" + \"%-21s\" % l[0][1] + l[1][1] + library.late_note(l, today))\n    elif choice == \"6\":\n        m = ask_member(members)\n        if m == None:\n            print(\"Cancelled.\")\n        elif m[2] == 0:\n            print(m[1] + \" owes nothing.\")\n        else:\n            print(m[1] + \" pays \" + text.money(m[2]) + \"; nothing is owed now.\")\n            m[2] = 0\n    elif choice == \"7\":\n        d = ask_date(\"new date (YYYY-MM-DD)> \", today)\n        if d == None:\n            print(\"Cancelled.\")\n        else:\n            today = d\n            late = 0\n            for l in loans:\n                if library.days_late(l, today) > 0:\n                    late = late + 1\n            if late == 0:\n                print(\"Today is now \" + dates.show(today) + \".\")\n            elif late == 1:\n                print(\"Today is now \" + dates.show(today) + \". 1 loan is late.\")\n            else:\n                print(\"Today is now \" + dates.show(today) + \". \" + str(late) + \" loans are late.\")\n    elif choice == \"8\":\n        name = ask_name(\"name> \", members)\n        if name == \"\":\n            print(\"Cancelled.\")\n        else:\n            n = len(members) + 1\n            members = members + [[n, name, 0]]\n            print(\"Member #\" + str(n) + \": \" + name + \".\")\n    elif choice == \"9\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 9.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "library.eml",
      "eml": "# P012 library loans - the books and the lending rules. A loan lasts 14\n# days; a member has at most 3 books at once and cannot borrow while owing a\n# fine; a late book costs 0.25 a day, at most 5.00. Members and loans change\n# while the program runs, so they are kept by main.eml and passed in here:\n# a member is [number, name, fines owed in cents], and a loan is\n# [book, member, due date].\nimport dates\nimport text\n\n14 => loan_days\n3 => most_books\n25 => fine_per_day\n500 => most_fine\n\n# A book is [number, title, author].\n[\n    [1, \"Pride and Prejudice\", \"Jane Austen\"],\n    [2, \"Moby-Dick\", \"Herman Melville\"],\n    [3, \"Frankenstein\", \"Mary Shelley\"],\n    [4, \"The Time Machine\", \"H. G. Wells\"],\n    [5, \"Dracula\", \"Bram Stoker\"],\n    [6, \"Little Women\", \"Louisa May Alcott\"],\n] => books\n\ndef find(items, n):\n    # The book or member numbered n, or None.\n    for it in items:\n        if it[0] == n:\n            return it\n    return None\n\ndef member_called(members, name):\n    # The member whose name is name in any case, or None.\n    for m in members:\n        if text.lower(m[1]) == text.lower(name):\n            return m\n    return None\n\ndef loan_of(loans, book):\n    # The loan of book, or None if it is on the shelf.\n    for l in loans:\n        if l[0][0] == book[0]:\n            return l\n    return None\n\ndef without(loans, book):\n    # The loans without the one of book.\n    [] => kept\n    for l in loans:\n        if l[0][0] != book[0]:\n            kept + [l] => kept\n    return kept\n\ndef count_loans(loans, member):\n    0 => n\n    for l in loans:\n        if l[1][0] == member[0]:\n            n + 1 => n\n    return n\n\ndef why_not_borrow(loans, member):\n    # Why member may not borrow now, or \"\" if they may.\n    if member[2] > 0:\n        return member[1] + \" owes \" + text.money(member[2]) + \"; fines are paid before borrowing.\"\n    if count_loans(loans, member) >= most_books:\n        return member[1] + \" already has \" + str(most_books) + \" books, the most at once.\"\n    return \"\"\n\ndef days_late(loan, today):\n    # How many days past its due date the loan is today; 0 if it is not late.\n    dates.days_between(loan[2], today) => d\n    if d > 0:\n        return d\n    return 0\n\ndef fine(late):\n    # The fine for a book this many days late.\n    if late * fine_per_day > most_fine:\n        return most_fine\n    return late * fine_per_day\n\ndef late_note(loan, today):\n    # \", 3 days late, fine so far 0.75\" for a late loan; \"\" otherwise.\n    days_late(loan, today) => late\n    if late == 0:\n        return \"\"\n    return \", \" + text.plural(late, \"day\") + \" late, fine so far \" + text.money(fine(late))\n",
      "python": "import dates\nimport text\nloan_days = 14\nmost_books = 3\nfine_per_day = 25\nmost_fine = 500\nbooks = [[1, \"Pride and Prejudice\", \"Jane Austen\"], [2, \"Moby-Dick\", \"Herman Melville\"], [3, \"Frankenstein\", \"Mary Shelley\"], [4, \"The Time Machine\", \"H. G. Wells\"], [5, \"Dracula\", \"Bram Stoker\"], [6, \"Little Women\", \"Louisa May Alcott\"]]\n\ndef find(items, n):\n    for it in items:\n        if it[0] == n:\n            return it\n    return None\n\ndef member_called(members, name):\n    for m in members:\n        if text.lower(m[1]) == text.lower(name):\n            return m\n    return None\n\ndef loan_of(loans, book):\n    for l in loans:\n        if l[0][0] == book[0]:\n            return l\n    return None\n\ndef without(loans, book):\n    kept = []\n    for l in loans:\n        if l[0][0] != book[0]:\n            kept = kept + [l]\n    return kept\n\ndef count_loans(loans, member):\n    n = 0\n    for l in loans:\n        if l[1][0] == member[0]:\n            n = n + 1\n    return n\n\ndef why_not_borrow(loans, member):\n    if member[2] > 0:\n        return member[1] + \" owes \" + text.money(member[2]) + \"; fines are paid before borrowing.\"\n    if count_loans(loans, member) >= most_books:\n        return member[1] + \" already has \" + str(most_books) + \" books, the most at once.\"\n    return \"\"\n\ndef days_late(loan, today):\n    d = dates.days_between(loan[2], today)\n    if d > 0:\n        return d\n    return 0\n\ndef fine(late):\n    if late * fine_per_day > most_fine:\n        return most_fine\n    return late * fine_per_day\n\ndef late_note(loan, today):\n    late = days_late(loan, today)\n    if late == 0:\n        return \"\"\n    return \", \" + text.plural(late, \"day\") + \" late, fine so far \" + text.money(fine(late))\n"
    },
    {
      "name": "dates.eml",
      "eml": "# P012 library loans - calendar dates. A date is [year, month, day] in the\n# Gregorian calendar, for the years 2000 to 2099. Days are counted from\n# 2000-01-01, so the gap between two dates is a subtraction.\n\n[\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"] => month_names\n\ndef is_leap(year):\n    # Divisible by 4 is a leap year, except divisible by 100, except\n    # divisible by 400 after all.\n    if year % 400 == 0:\n        return True\n    if year % 100 == 0:\n        return False\n    return year % 4 == 0\n\ndef days_in_month(year, month):\n    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] => lengths\n    if month == 2 and is_leap(year):\n        return 29\n    return lengths[month - 1]\n\ndef day_number(d):\n    # Days from 2000-01-01, which is day 0, to d.\n    0 => total\n    for y in [2000:d[0] - 1]:\n        if is_leap(y):\n            total + 366 => total\n        else:\n            total + 365 => total\n    for m in [1:d[1] - 1]:\n        total + days_in_month(d[0], m) => total\n    return total + d[2] - 1\n\ndef days_between(a, b):\n    # How many days b comes after a; negative if b comes first.\n    return day_number(b) - day_number(a)\n\ndef add_days(d, n):\n    # The date n days after d, for n >= 0.\n    d[0] => y\n    d[1] => m\n    d[2] + n => day\n    while day > days_in_month(y, m):\n        day - days_in_month(y, m) => day\n        m + 1 => m\n        if m == 13:\n            1 => m\n            y + 1 => y\n    return [y, m, day]\n\ndef show(d):\n    return str(d[0]) + \"-\" + (\"%02d\" % d[1]) + \"-\" + (\"%02d\" % d[2])\n\ndef parse(s):\n    # [date, \"\"] if s is a date written YYYY-MM-DD, otherwise [None, what is\n    # wrong with it].\n    \"Write the date as YYYY-MM-DD, for example 2028-02-20.\" => shape\n    if len(s) != 10 or s[4] != \"-\" or s[7] != \"-\":\n        return [None, shape]\n    for i in [0, 1, 2, 3, 5, 6, 8, 9]:\n        if not (s[i] in \"0123456789\"):\n            return [None, shape]\n    int(s[0:4]) => y\n    int(s[5:7]) => m\n    int(s[8:10]) => day\n    if y < 2000 or y > 2099:\n        return [None, \"The year must be from 2000 to 2099.\"]\n    if m < 1 or m > 12:\n        return [None, \"There is no month \" + str(m) + \".\"]\n    if day < 1 or day > days_in_month(y, m):\n        return [None, s + \" is not a date: \" + month_names[m - 1] + \" \" + str(y) + \" has \" + str(days_in_month(y, m)) + \" days.\"]\n    return [[y, m, day], \"\"]\n",
      "python": "month_names = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"]\n\ndef is_leap(year):\n    if year % 400 == 0:\n        return True\n    if year % 100 == 0:\n        return False\n    return year % 4 == 0\n\ndef days_in_month(year, month):\n    lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n    if month == 2 and is_leap(year):\n        return 29\n    return lengths[month - 1]\n\ndef day_number(d):\n    total = 0\n    for y in range(2000, d[0]):\n        if is_leap(y):\n            total = total + 366\n        else:\n            total = total + 365\n    for m in range(1, d[1]):\n        total = total + days_in_month(d[0], m)\n    return total + d[2] - 1\n\ndef days_between(a, b):\n    return day_number(b) - day_number(a)\n\ndef add_days(d, n):\n    y = d[0]\n    m = d[1]\n    day = d[2] + n\n    while day > days_in_month(y, m):\n        day = day - days_in_month(y, m)\n        m = m + 1\n        if m == 13:\n            m = 1\n            y = y + 1\n    return [y, m, day]\n\ndef show(d):\n    return str(d[0]) + \"-\" + \"%02d\" % d[1] + \"-\" + \"%02d\" % d[2]\n\ndef parse(s):\n    shape = \"Write the date as YYYY-MM-DD, for example 2028-02-20.\"\n    if len(s) != 10 or s[4] != \"-\" or s[7] != \"-\":\n        return [None, shape]\n    for i in [0, 1, 2, 3, 5, 6, 8, 9]:\n        if not s[i] in \"0123456789\":\n            return [None, shape]\n    y = int(s[0:4])\n    m = int(s[5:7])\n    day = int(s[8:10])\n    if y < 2000 or y > 2099:\n        return [None, \"The year must be from 2000 to 2099.\"]\n    if m < 1 or m > 12:\n        return [None, \"There is no month \" + str(m) + \".\"]\n    if day < 1 or day > days_in_month(y, m):\n        return [None, s + \" is not a date: \" + month_names[m - 1] + \" \" + str(y) + \" has \" + str(days_in_month(y, m)) + \" days.\"]\n    return [[y, m, day], \"\"]\n"
    },
    {
      "name": "text.eml",
      "eml": "# P012 library loans - reading what is typed and writing numbers and money.\n# The interpreter that checks every session does not run string methods yet,\n# so the text handling is written out here.\n\ndef trim(s):\n    # s without the spaces at either end.\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef lower(s):\n    # s with A-Z turned into a-z; everything else as it was.\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper_letters\n    \"abcdefghijklmnopqrstuvwxyz\" => lower_letters\n    \"\" => out\n    for c in s:\n        c => d\n        for i in [0:25]:\n            if upper_letters[i] == c:\n                lower_letters[i] => d\n        out + d => out\n    return out\n\ndef number(s):\n    # The value of s if it is digits only (at least one), otherwise -1.\n    if s == \"\":\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in \"0123456789\"):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\n\ndef money(c):\n    # Whole cents as 12.50.\n    str(c) => s\n    while len(s) < 3:\n        \"0\" + s => s\n    return s[0:len(s) - 2] + \".\" + s[len(s) - 2:len(s)]\n\ndef plural(n, word):\n    # \"1 day\", \"3 days\".\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n",
      "python": "def trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef lower(s):\n    upper_letters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    lower_letters = \"abcdefghijklmnopqrstuvwxyz\"\n    out = \"\"\n    for c in s:\n        d = c\n        for i in range(0, 26):\n            if upper_letters[i] == c:\n                d = lower_letters[i]\n        out = out + d\n    return out\n\ndef number(s):\n    if s == \"\":\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in \"0123456789\":\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n\ndef money(c):\n    s = str(c)\n    while len(s) < 3:\n        s = \"0\" + s\n    return s[0:len(s) - 2] + \".\" + s[len(s) - 2:len(s)]\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "today\n2027-02-29\n2028-13-01\n1999-12-31\n2028-02-29\n0\n4\n5\n3\n9\nx\n\n3\n2\n0\n\n3\n2\n4\n3\n1\n4\n3\n2\n1\n3\n2\n2\n3\n2\n4\n6\n6\n1\n7\n2028-02-28\n2028-02-30\n\n7\n2028-04-10\n5\n4\n4\n3\n2\n8\nAna-Maria Lopez-Fernandez\nben\n\n8\nDana\n2\n9\n",
      "screen": "== Library loans ==\ntoday's date (YYYY-MM-DD)> today\nWrite the date as YYYY-MM-DD, for example 2028-02-20.\ntoday's date (YYYY-MM-DD)> 2027-02-29\n2027-02-29 is not a date: February 2027 has 28 days.\ntoday's date (YYYY-MM-DD)> 2028-13-01\nThere is no month 13.\ntoday's date (YYYY-MM-DD)> 1999-12-31\nThe year must be from 2000 to 2099.\ntoday's date (YYYY-MM-DD)> 2028-02-29\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 0\nPick a number from 1 to 9.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 4\nNothing is on loan.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 5\nNothing is on loan.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 9\nType a member number from 1 to 2, or nothing to cancel.\nmember number> x\nType a member number from 1 to 2, or nothing to cancel.\nmember number> \nCancelled.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nbook number> 0\nType a book number from 1 to 6, or nothing to cancel.\nbook number> \nCancelled.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nbook number> 4\nBen borrows The Time Machine, due 2028-03-14.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 1\nbook number> 4\nThe Time Machine is on loan until 2028-03-14.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nbook number> 1\nBen borrows Pride and Prejudice, due 2028-03-14.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nbook number> 2\nBen borrows Moby-Dick, due 2028-03-14.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nBen already has 3 books, the most at once.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 4\nbook number> 6\nLittle Women is not on loan.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 6\nmember number> 1\nAna owes nothing.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 7\nnew date (YYYY-MM-DD)> 2028-02-28\nDates only move forward; today is 2028-02-29.\nnew date (YYYY-MM-DD)> 2028-02-30\n2028-02-30 is not a date: February 2028 has 29 days.\nnew date (YYYY-MM-DD)> \nCancelled.\n\n== Library == today is 2028-02-29\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 7\nnew date (YYYY-MM-DD)> 2028-04-10\nToday is now 2028-04-10. 3 loans are late.\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 5\n\n-- loans: due date, book, member --\n  2028-03-14  The Time Machine     Ben, 27 days late, fine so far 5.00\n  2028-03-14  Pride and Prejudice  Ben, 27 days late, fine so far 5.00\n  2028-03-14  Moby-Dick            Ben, 27 days late, fine so far 5.00\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 4\nbook number> 4\nThe Time Machine is back from Ben, 27 days late: fine 5.00 (the most for one book); Ben owes 5.00.\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nBen owes 5.00; fines are paid before borrowing.\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 8\nname> Ana-Maria Lopez-Fernandez\nA name has at most 20 characters.\nname> ben\nThere is already a member called Ben.\nname> \nCancelled.\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 8\nname> Dana\nMember #3: Dana.\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 2\n\n-- members --\n  #1 Ana                  0 books  owes 0.00\n  #2 Ben                  2 books  owes 5.00\n  #3 Dana                 0 books  owes 0.00\n\n== Library == today is 2028-04-10\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 9\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "2028-02-20\n1\n3\n1\n2\n3\n1\n3\n7\n2028-02-27\n3\n2\n5\n5\n7\n2028-03-08\n5\n4\n2\n3\n1\n6\n1\n3\n1\n1\n4\n5\n8\nChen\n2\n1\n9\n",
      "screen": "== Library loans ==\ntoday's date (YYYY-MM-DD)> 2028-02-20\n\n== Library == today is 2028-02-20\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 1\n\n-- books --\n  1) Pride and Prejudice  Jane Austen        on the shelf\n  2) Moby-Dick            Herman Melville    on the shelf\n  3) Frankenstein         Mary Shelley       on the shelf\n  4) The Time Machine     H. G. Wells        on the shelf\n  5) Dracula              Bram Stoker        on the shelf\n  6) Little Women         Louisa May Alcott  on the shelf\n\n== Library == today is 2028-02-20\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 1\nbook number> 2\nAna borrows Moby-Dick, due 2028-03-05.\n\n== Library == today is 2028-02-20\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 1\nbook number> 3\nAna borrows Frankenstein, due 2028-03-05.\n\n== Library == today is 2028-02-20\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 7\nnew date (YYYY-MM-DD)> 2028-02-27\nToday is now 2028-02-27.\n\n== Library == today is 2028-02-27\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 2\nbook number> 5\nBen borrows Dracula, due 2028-03-12.\n\n== Library == today is 2028-02-27\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 5\n\n-- loans: due date, book, member --\n  2028-03-05  Moby-Dick            Ana\n  2028-03-05  Frankenstein         Ana\n  2028-03-12  Dracula              Ben\n\n== Library == today is 2028-02-27\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 7\nnew date (YYYY-MM-DD)> 2028-03-08\nToday is now 2028-03-08. 2 loans are late.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 5\n\n-- loans: due date, book, member --\n  2028-03-05  Moby-Dick            Ana, 3 days late, fine so far 0.75\n  2028-03-05  Frankenstein         Ana, 3 days late, fine so far 0.75\n  2028-03-12  Dracula              Ben\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 4\nbook number> 2\nMoby-Dick is back from Ana, 3 days late: fine 0.75; Ana owes 0.75.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 1\nAna owes 0.75; fines are paid before borrowing.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 6\nmember number> 1\nAna pays 0.75; nothing is owed now.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 3\nmember number> 1\nbook number> 1\nAna borrows Pride and Prejudice, due 2028-03-22.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 4\nbook number> 5\nDracula is back from Ben, on time.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 8\nname> Chen\nMember #3: Chen.\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 2\n\n-- members --\n  #1 Ana                  2 books  owes 0.00\n  #2 Ben                  0 books  owes 0.00\n  #3 Chen                 0 books  owes 0.00\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 1\n\n-- books --\n  1) Pride and Prejudice  Jane Austen        due back 2028-03-22\n  2) Moby-Dick            Herman Melville    on the shelf\n  3) Frankenstein         Mary Shelley       due back 2028-03-05 (late)\n  4) The Time Machine     H. G. Wells        on the shelf\n  5) Dracula              Bram Stoker        on the shelf\n  6) Little Women         Louisa May Alcott  on the shelf\n\n== Library == today is 2028-03-08\n1) books  2) members  3) borrow  4) return  5) loans\n6) pay fines  7) change date  8) new member  9) quit\nchoice> 9\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "library-catalog",
      "caseId": "026-library-catalog",
      "title": "Case corpus: a self-authored library checkout catalog"
    },
    {
      "slug": "days-between-dates",
      "caseId": "111-days-between-dates",
      "title": "Days between dates"
    }
  ],
  "updated": "2026-10-03"
}
