<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P012-library-loans/ | updated: 2026-10-03 -->

# P012 Library loans

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.

EML-P project `projects/library-loans` in the EML language repo: 4 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/library-loans` runs it and `eml project verify projects/library-loans` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: library-catalog (https://efficientnewlanguage.org/cases/026-library-catalog/), days-between-dates (https://efficientnewlanguage.org/cases/111-days-between-dates/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
today
2027-02-29
2028-13-01
1999-12-31
2028-02-29
0
4
5
3
9
x

3
2
0

3
2
4
3
1
4
3
2
1
3
2
2
3
2
4
6
6
1
7
2028-02-28
2028-02-30

7
2028-04-10
5
4
4
3
2
8
Ana-Maria Lopez-Fernandez
ben

8
Dana
2
9
```

Screen:

```text
== Library loans ==
today's date (YYYY-MM-DD)> today
Write the date as YYYY-MM-DD, for example 2028-02-20.
today's date (YYYY-MM-DD)> 2027-02-29
2027-02-29 is not a date: February 2027 has 28 days.
today's date (YYYY-MM-DD)> 2028-13-01
There is no month 13.
today's date (YYYY-MM-DD)> 1999-12-31
The year must be from 2000 to 2099.
today's date (YYYY-MM-DD)> 2028-02-29

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 0
Pick a number from 1 to 9.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 4
Nothing is on loan.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 5
Nothing is on loan.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 9
Type a member number from 1 to 2, or nothing to cancel.
member number> x
Type a member number from 1 to 2, or nothing to cancel.
member number> 
Cancelled.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
book number> 0
Type a book number from 1 to 6, or nothing to cancel.
book number> 
Cancelled.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
book number> 4
Ben borrows The Time Machine, due 2028-03-14.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 1
book number> 4
The Time Machine is on loan until 2028-03-14.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
book number> 1
Ben borrows Pride and Prejudice, due 2028-03-14.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
book number> 2
Ben borrows Moby-Dick, due 2028-03-14.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
Ben already has 3 books, the most at once.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 4
book number> 6
Little Women is not on loan.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 6
member number> 1
Ana owes nothing.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 7
new date (YYYY-MM-DD)> 2028-02-28
Dates only move forward; today is 2028-02-29.
new date (YYYY-MM-DD)> 2028-02-30
2028-02-30 is not a date: February 2028 has 29 days.
new date (YYYY-MM-DD)> 
Cancelled.

== Library == today is 2028-02-29
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 7
new date (YYYY-MM-DD)> 2028-04-10
Today is now 2028-04-10. 3 loans are late.

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 5

-- loans: due date, book, member --
  2028-03-14  The Time Machine     Ben, 27 days late, fine so far 5.00
  2028-03-14  Pride and Prejudice  Ben, 27 days late, fine so far 5.00
  2028-03-14  Moby-Dick            Ben, 27 days late, fine so far 5.00

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 4
book number> 4
The Time Machine is back from Ben, 27 days late: fine 5.00 (the most for one book); Ben owes 5.00.

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
Ben owes 5.00; fines are paid before borrowing.

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 8
name> Ana-Maria Lopez-Fernandez
A name has at most 20 characters.
name> ben
There is already a member called Ben.
name> 
Cancelled.

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 8
name> Dana
Member #3: Dana.

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 2

-- members --
  #1 Ana                  0 books  owes 0.00
  #2 Ben                  2 books  owes 5.00
  #3 Dana                 0 books  owes 0.00

== Library == today is 2028-04-10
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 9
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
2028-02-20
1
3
1
2
3
1
3
7
2028-02-27
3
2
5
5
7
2028-03-08
5
4
2
3
1
6
1
3
1
1
4
5
8
Chen
2
1
9
```

Screen:

```text
== Library loans ==
today's date (YYYY-MM-DD)> 2028-02-20

== Library == today is 2028-02-20
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 1

-- books --
  1) Pride and Prejudice  Jane Austen        on the shelf
  2) Moby-Dick            Herman Melville    on the shelf
  3) Frankenstein         Mary Shelley       on the shelf
  4) The Time Machine     H. G. Wells        on the shelf
  5) Dracula              Bram Stoker        on the shelf
  6) Little Women         Louisa May Alcott  on the shelf

== Library == today is 2028-02-20
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 1
book number> 2
Ana borrows Moby-Dick, due 2028-03-05.

== Library == today is 2028-02-20
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 1
book number> 3
Ana borrows Frankenstein, due 2028-03-05.

== Library == today is 2028-02-20
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 7
new date (YYYY-MM-DD)> 2028-02-27
Today is now 2028-02-27.

== Library == today is 2028-02-27
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 2
book number> 5
Ben borrows Dracula, due 2028-03-12.

== Library == today is 2028-02-27
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 5

-- loans: due date, book, member --
  2028-03-05  Moby-Dick            Ana
  2028-03-05  Frankenstein         Ana
  2028-03-12  Dracula              Ben

== Library == today is 2028-02-27
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 7
new date (YYYY-MM-DD)> 2028-03-08
Today is now 2028-03-08. 2 loans are late.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 5

-- loans: due date, book, member --
  2028-03-05  Moby-Dick            Ana, 3 days late, fine so far 0.75
  2028-03-05  Frankenstein         Ana, 3 days late, fine so far 0.75
  2028-03-12  Dracula              Ben

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 4
book number> 2
Moby-Dick is back from Ana, 3 days late: fine 0.75; Ana owes 0.75.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 1
Ana owes 0.75; fines are paid before borrowing.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 6
member number> 1
Ana pays 0.75; nothing is owed now.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 3
member number> 1
book number> 1
Ana borrows Pride and Prejudice, due 2028-03-22.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 4
book number> 5
Dracula is back from Ben, on time.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 8
name> Chen
Member #3: Chen.

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 2

-- members --
  #1 Ana                  2 books  owes 0.00
  #2 Ben                  0 books  owes 0.00
  #3 Chen                 0 books  owes 0.00

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 1

-- books --
  1) Pride and Prejudice  Jane Austen        due back 2028-03-22
  2) Moby-Dick            Herman Melville    on the shelf
  3) Frankenstein         Mary Shelley       due back 2028-03-05 (late)
  4) The Time Machine     H. G. Wells        on the shelf
  5) Dracula              Bram Stoker        on the shelf
  6) Little Women         Louisa May Alcott  on the shelf

== Library == today is 2028-03-08
1) books  2) members  3) borrow  4) return  5) loans
6) pay fines  7) change date  8) new member  9) quit
choice> 9
Bye.
```

## Modules

### main.eml

```eml
# P012 library loans: lend books to members and take them back, with due
# dates on a real calendar and fines for late books. There is no clock: the
# librarian types today's date at the start and moves it forward by hand.
import dates
import library
import text

def ask_number(prompt, low, high, message):
    # A whole number from low to high, asked again until one is typed; -1 if
    # the answer is empty, which cancels.
    while True:
        text.trim(input(prompt)) => answer
        if answer == "":
            return 0 - 1
        text.number(answer) => n
        if n >= low and n <= high:
            return n
        message ^0

def ask_date(prompt, today):
    # A date typed as YYYY-MM-DD, asked again until one is typed. With a
    # today, the date may not be before it, and an empty answer cancels
    # (None); without one, an answer is needed.
    while True:
        text.trim(input(prompt)) => answer
        if answer == "" and today != None:
            return None
        dates.parse(answer) => r
        if r[0] == None:
            r[1] ^0
        elif today != None and dates.days_between(today, r[0]) < 0:
            ("Dates only move forward; today is " + dates.show(today) + ".") ^0
        else:
            return r[0]

def ask_name(prompt, members):
    # A new member's name, asked again until a good one is typed; "" cancels.
    while True:
        text.trim(input(prompt)) => name
        if name == "":
            return ""
        if len(name) > 20:
            "A name has at most 20 characters." ^0
        elif library.member_called(members, name) != None:
            ("There is already a member called " + library.member_called(members, name)[1] + ".") ^0
        else:
            return name

def ask_member(members):
    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:
        return None
    return library.find(members, n)

def ask_book():
    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:
        return None
    return library.find(library.books, n)

"== Library loans ==" ^0
ask_date("today's date (YYYY-MM-DD)> ", None) => today
[[1, "Ana", 0], [2, "Ben", 0]] => members
[] => loans
True => running
while running:
    "" ^0
    ("== Library == today is " + dates.show(today)) ^0
    "1) books  2) members  3) borrow  4) return  5) loans" ^0
    "6) pay fines  7) change date  8) new member  9) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        "" ^0
        "-- books --" ^0
        for b in library.books:
            library.loan_of(loans, b) => l
            "on the shelf" => where
            if l != None:
                "due back " + dates.show(l[2]) => where
                if library.days_late(l, today) > 0:
                    where + " (late)" => where
            ("  " + str(b[0]) + ") " + ("%-21s" % b[1]) + ("%-19s" % b[2]) + where) ^0
    elif choice == "2":
        "" ^0
        "-- members --" ^0
        for m in members:
            ("  #" + str(m[0]) + " " + ("%-21s" % m[1]) + ("%-9s" % text.plural(library.count_loans(loans, m), "book")) + "owes " + text.money(m[2])) ^0
    elif choice == "3":
        ask_member(members) => m
        if m == None:
            "Cancelled." ^0
        elif library.why_not_borrow(loans, m) != "":
            library.why_not_borrow(loans, m) ^0
        else:
            ask_book() => b
            if b == None:
                "Cancelled." ^0
            elif library.loan_of(loans, b) != None:
                (b[1] + " is on loan until " + dates.show(library.loan_of(loans, b)[2]) + ".") ^0
            else:
                dates.add_days(today, library.loan_days) => due
                loans + [[b, m, due]] => loans
                (m[1] + " borrows " + b[1] + ", due " + dates.show(due) + ".") ^0
    elif choice == "4":
        if len(loans) == 0:
            "Nothing is on loan." ^0
        else:
            ask_book() => b
            if b == None:
                "Cancelled." ^0
            elif library.loan_of(loans, b) == None:
                (b[1] + " is not on loan.") ^0
            else:
                library.loan_of(loans, b) => l
                library.without(loans, b) => loans
                library.days_late(l, today) => late
                if late == 0:
                    (b[1] + " is back from " + l[1][1] + ", on time.") ^0
                else:
                    library.fine(late) => f
                    l[1][2] + f => l[1][2]
                    "" => most
                    if f == library.most_fine:
                        " (the most for one book)" => most
                    (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
    elif choice == "5":
        if len(loans) == 0:
            "Nothing is on loan." ^0
        else:
            "" ^0
            "-- loans: due date, book, member --" ^0
            for l in loans:
                ("  " + dates.show(l[2]) + "  " + ("%-21s" % l[0][1]) + l[1][1] + library.late_note(l, today)) ^0
    elif choice == "6":
        ask_member(members) => m
        if m == None:
            "Cancelled." ^0
        elif m[2] == 0:
            (m[1] + " owes nothing.") ^0
        else:
            (m[1] + " pays " + text.money(m[2]) + "; nothing is owed now.") ^0
            0 => m[2]
    elif choice == "7":
        ask_date("new date (YYYY-MM-DD)> ", today) => d
        if d == None:
            "Cancelled." ^0
        else:
            d => today
            0 => late
            for l in loans:
                if library.days_late(l, today) > 0:
                    late + 1 => late
            if late == 0:
                ("Today is now " + dates.show(today) + ".") ^0
            elif late == 1:
                ("Today is now " + dates.show(today) + ". 1 loan is late.") ^0
            else:
                ("Today is now " + dates.show(today) + ". " + str(late) + " loans are late.") ^0
    elif choice == "8":
        ask_name("name> ", members) => name
        if name == "":
            "Cancelled." ^0
        else:
            len(members) + 1 => n
            members + [[n, name, 0]] => members
            ("Member #" + str(n) + ": " + name + ".") ^0
    elif choice == "9":
        False => running
    else:
        "Pick a number from 1 to 9." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import dates
import library
import text

def ask_number(prompt, low, high, message):
    while True:
        answer = text.trim(input(prompt))
        if answer == "":
            return 0 - 1
        n = text.number(answer)
        if n >= low and n <= high:
            return n
        print(message)

def ask_date(prompt, today):
    while True:
        answer = text.trim(input(prompt))
        if answer == "" and today != None:
            return None
        r = dates.parse(answer)
        if r[0] == None:
            print(r[1])
        elif today != None and dates.days_between(today, r[0]) < 0:
            print("Dates only move forward; today is " + dates.show(today) + ".")
        else:
            return r[0]

def ask_name(prompt, members):
    while True:
        name = text.trim(input(prompt))
        if name == "":
            return ""
        if len(name) > 20:
            print("A name has at most 20 characters.")
        elif library.member_called(members, name) != None:
            print("There is already a member called " + library.member_called(members, name)[1] + ".")
        else:
            return name

def 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.")
    if n == 0 - 1:
        return None
    return library.find(members, n)

def 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.")
    if n == 0 - 1:
        return None
    return library.find(library.books, n)

print("== Library loans ==")
today = ask_date("today's date (YYYY-MM-DD)> ", None)
members = [[1, "Ana", 0], [2, "Ben", 0]]
loans = []
running = True
while running:
    print("")
    print("== Library == today is " + dates.show(today))
    print("1) books  2) members  3) borrow  4) return  5) loans")
    print("6) pay fines  7) change date  8) new member  9) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        print("")
        print("-- books --")
        for b in library.books:
            l = library.loan_of(loans, b)
            where = "on the shelf"
            if l != None:
                where = "due back " + dates.show(l[2])
                if library.days_late(l, today) > 0:
                    where = where + " (late)"
            print("  " + str(b[0]) + ") " + "%-21s" % b[1] + "%-19s" % b[2] + where)
    elif choice == "2":
        print("")
        print("-- members --")
        for m in members:
            print("  #" + str(m[0]) + " " + "%-21s" % m[1] + "%-9s" % text.plural(library.count_loans(loans, m), "book") + "owes " + text.money(m[2]))
    elif choice == "3":
        m = ask_member(members)
        if m == None:
            print("Cancelled.")
        elif library.why_not_borrow(loans, m) != "":
            print(library.why_not_borrow(loans, m))
        else:
            b = ask_book()
            if b == None:
                print("Cancelled.")
            elif library.loan_of(loans, b) != None:
                print(b[1] + " is on loan until " + dates.show(library.loan_of(loans, b)[2]) + ".")
            else:
                due = dates.add_days(today, library.loan_days)
                loans = loans + [[b, m, due]]
                print(m[1] + " borrows " + b[1] + ", due " + dates.show(due) + ".")
    elif choice == "4":
        if len(loans) == 0:
            print("Nothing is on loan.")
        else:
            b = ask_book()
            if b == None:
                print("Cancelled.")
            elif library.loan_of(loans, b) == None:
                print(b[1] + " is not on loan.")
            else:
                l = library.loan_of(loans, b)
                loans = library.without(loans, b)
                late = library.days_late(l, today)
                if late == 0:
                    print(b[1] + " is back from " + l[1][1] + ", on time.")
                else:
                    f = library.fine(late)
                    l[1][2] = l[1][2] + f
                    most = ""
                    if f == library.most_fine:
                        most = " (the most for one book)"
                    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]) + ".")
    elif choice == "5":
        if len(loans) == 0:
            print("Nothing is on loan.")
        else:
            print("")
            print("-- loans: due date, book, member --")
            for l in loans:
                print("  " + dates.show(l[2]) + "  " + "%-21s" % l[0][1] + l[1][1] + library.late_note(l, today))
    elif choice == "6":
        m = ask_member(members)
        if m == None:
            print("Cancelled.")
        elif m[2] == 0:
            print(m[1] + " owes nothing.")
        else:
            print(m[1] + " pays " + text.money(m[2]) + "; nothing is owed now.")
            m[2] = 0
    elif choice == "7":
        d = ask_date("new date (YYYY-MM-DD)> ", today)
        if d == None:
            print("Cancelled.")
        else:
            today = d
            late = 0
            for l in loans:
                if library.days_late(l, today) > 0:
                    late = late + 1
            if late == 0:
                print("Today is now " + dates.show(today) + ".")
            elif late == 1:
                print("Today is now " + dates.show(today) + ". 1 loan is late.")
            else:
                print("Today is now " + dates.show(today) + ". " + str(late) + " loans are late.")
    elif choice == "8":
        name = ask_name("name> ", members)
        if name == "":
            print("Cancelled.")
        else:
            n = len(members) + 1
            members = members + [[n, name, 0]]
            print("Member #" + str(n) + ": " + name + ".")
    elif choice == "9":
        running = False
    else:
        print("Pick a number from 1 to 9.")
print("Bye.")
```

### library.eml

```eml
# P012 library loans - the books and the lending rules. A loan lasts 14
# days; a member has at most 3 books at once and cannot borrow while owing a
# fine; a late book costs 0.25 a day, at most 5.00. Members and loans change
# while the program runs, so they are kept by main.eml and passed in here:
# a member is [number, name, fines owed in cents], and a loan is
# [book, member, due date].
import dates
import text

14 => loan_days
3 => most_books
25 => fine_per_day
500 => most_fine

# A book is [number, title, author].
[
    [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"],
] => books

def find(items, n):
    # The book or member numbered n, or None.
    for it in items:
        if it[0] == n:
            return it
    return None

def member_called(members, name):
    # The member whose name is name in any case, or None.
    for m in members:
        if text.lower(m[1]) == text.lower(name):
            return m
    return None

def loan_of(loans, book):
    # The loan of book, or None if it is on the shelf.
    for l in loans:
        if l[0][0] == book[0]:
            return l
    return None

def without(loans, book):
    # The loans without the one of book.
    [] => kept
    for l in loans:
        if l[0][0] != book[0]:
            kept + [l] => kept
    return kept

def count_loans(loans, member):
    0 => n
    for l in loans:
        if l[1][0] == member[0]:
            n + 1 => n
    return n

def why_not_borrow(loans, member):
    # Why member may not borrow now, or "" if they may.
    if member[2] > 0:
        return member[1] + " owes " + text.money(member[2]) + "; fines are paid before borrowing."
    if count_loans(loans, member) >= most_books:
        return member[1] + " already has " + str(most_books) + " books, the most at once."
    return ""

def days_late(loan, today):
    # How many days past its due date the loan is today; 0 if it is not late.
    dates.days_between(loan[2], today) => d
    if d > 0:
        return d
    return 0

def fine(late):
    # The fine for a book this many days late.
    if late * fine_per_day > most_fine:
        return most_fine
    return late * fine_per_day

def late_note(loan, today):
    # ", 3 days late, fine so far 0.75" for a late loan; "" otherwise.
    days_late(loan, today) => late
    if late == 0:
        return ""
    return ", " + text.plural(late, "day") + " late, fine so far " + text.money(fine(late))
```

Python projection of library.eml:

```python
import dates
import text
loan_days = 14
most_books = 3
fine_per_day = 25
most_fine = 500
books = [[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"]]

def find(items, n):
    for it in items:
        if it[0] == n:
            return it
    return None

def member_called(members, name):
    for m in members:
        if text.lower(m[1]) == text.lower(name):
            return m
    return None

def loan_of(loans, book):
    for l in loans:
        if l[0][0] == book[0]:
            return l
    return None

def without(loans, book):
    kept = []
    for l in loans:
        if l[0][0] != book[0]:
            kept = kept + [l]
    return kept

def count_loans(loans, member):
    n = 0
    for l in loans:
        if l[1][0] == member[0]:
            n = n + 1
    return n

def why_not_borrow(loans, member):
    if member[2] > 0:
        return member[1] + " owes " + text.money(member[2]) + "; fines are paid before borrowing."
    if count_loans(loans, member) >= most_books:
        return member[1] + " already has " + str(most_books) + " books, the most at once."
    return ""

def days_late(loan, today):
    d = dates.days_between(loan[2], today)
    if d > 0:
        return d
    return 0

def fine(late):
    if late * fine_per_day > most_fine:
        return most_fine
    return late * fine_per_day

def late_note(loan, today):
    late = days_late(loan, today)
    if late == 0:
        return ""
    return ", " + text.plural(late, "day") + " late, fine so far " + text.money(fine(late))
```

### dates.eml

```eml
# P012 library loans - calendar dates. A date is [year, month, day] in the
# Gregorian calendar, for the years 2000 to 2099. Days are counted from
# 2000-01-01, so the gap between two dates is a subtraction.

["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] => month_names

def is_leap(year):
    # Divisible by 4 is a leap year, except divisible by 100, except
    # divisible by 400 after all.
    if year % 400 == 0:
        return True
    if year % 100 == 0:
        return False
    return year % 4 == 0

def days_in_month(year, month):
    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] => lengths
    if month == 2 and is_leap(year):
        return 29
    return lengths[month - 1]

def day_number(d):
    # Days from 2000-01-01, which is day 0, to d.
    0 => total
    for y in [2000:d[0] - 1]:
        if is_leap(y):
            total + 366 => total
        else:
            total + 365 => total
    for m in [1:d[1] - 1]:
        total + days_in_month(d[0], m) => total
    return total + d[2] - 1

def days_between(a, b):
    # How many days b comes after a; negative if b comes first.
    return day_number(b) - day_number(a)

def add_days(d, n):
    # The date n days after d, for n >= 0.
    d[0] => y
    d[1] => m
    d[2] + n => day
    while day > days_in_month(y, m):
        day - days_in_month(y, m) => day
        m + 1 => m
        if m == 13:
            1 => m
            y + 1 => y
    return [y, m, day]

def show(d):
    return str(d[0]) + "-" + ("%02d" % d[1]) + "-" + ("%02d" % d[2])

def parse(s):
    # [date, ""] if s is a date written YYYY-MM-DD, otherwise [None, what is
    # wrong with it].
    "Write the date as YYYY-MM-DD, for example 2028-02-20." => shape
    if len(s) != 10 or s[4] != "-" or s[7] != "-":
        return [None, shape]
    for i in [0, 1, 2, 3, 5, 6, 8, 9]:
        if not (s[i] in "0123456789"):
            return [None, shape]
    int(s[0:4]) => y
    int(s[5:7]) => m
    int(s[8:10]) => day
    if y < 2000 or y > 2099:
        return [None, "The year must be from 2000 to 2099."]
    if m < 1 or m > 12:
        return [None, "There is no month " + str(m) + "."]
    if day < 1 or day > days_in_month(y, m):
        return [None, s + " is not a date: " + month_names[m - 1] + " " + str(y) + " has " + str(days_in_month(y, m)) + " days."]
    return [[y, m, day], ""]
```

Python projection of dates.eml:

```python
month_names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]

def is_leap(year):
    if year % 400 == 0:
        return True
    if year % 100 == 0:
        return False
    return year % 4 == 0

def days_in_month(year, month):
    lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    if month == 2 and is_leap(year):
        return 29
    return lengths[month - 1]

def day_number(d):
    total = 0
    for y in range(2000, d[0]):
        if is_leap(y):
            total = total + 366
        else:
            total = total + 365
    for m in range(1, d[1]):
        total = total + days_in_month(d[0], m)
    return total + d[2] - 1

def days_between(a, b):
    return day_number(b) - day_number(a)

def add_days(d, n):
    y = d[0]
    m = d[1]
    day = d[2] + n
    while day > days_in_month(y, m):
        day = day - days_in_month(y, m)
        m = m + 1
        if m == 13:
            m = 1
            y = y + 1
    return [y, m, day]

def show(d):
    return str(d[0]) + "-" + "%02d" % d[1] + "-" + "%02d" % d[2]

def parse(s):
    shape = "Write the date as YYYY-MM-DD, for example 2028-02-20."
    if len(s) != 10 or s[4] != "-" or s[7] != "-":
        return [None, shape]
    for i in [0, 1, 2, 3, 5, 6, 8, 9]:
        if not s[i] in "0123456789":
            return [None, shape]
    y = int(s[0:4])
    m = int(s[5:7])
    day = int(s[8:10])
    if y < 2000 or y > 2099:
        return [None, "The year must be from 2000 to 2099."]
    if m < 1 or m > 12:
        return [None, "There is no month " + str(m) + "."]
    if day < 1 or day > days_in_month(y, m):
        return [None, s + " is not a date: " + month_names[m - 1] + " " + str(y) + " has " + str(days_in_month(y, m)) + " days."]
    return [[y, m, day], ""]
```

### text.eml

```eml
# P012 library loans - reading what is typed and writing numbers and money.
# The interpreter that checks every session does not run string methods yet,
# so the text handling is written out here.

def trim(s):
    # s without the spaces at either end.
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def lower(s):
    # s with A-Z turned into a-z; everything else as it was.
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if upper_letters[i] == c:
                lower_letters[i] => d
        out + d => out
    return out

def number(s):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return 0 - 1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return 0 - 1
        n * 10 + int(c) => n
    return n

def money(c):
    # Whole cents as 12.50.
    str(c) => s
    while len(s) < 3:
        "0" + s => s
    return s[0:len(s) - 2] + "." + s[len(s) - 2:len(s)]

def plural(n, word):
    # "1 day", "3 days".
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"
```

Python projection of text.eml:

```python
def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def lower(s):
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if upper_letters[i] == c:
                d = lower_letters[i]
        out = out + d
    return out

def number(s):
    if s == "":
        return 0 - 1
    n = 0
    for c in s:
        if not c in "0123456789":
            return 0 - 1
        n = n * 10 + int(c)
    return n

def money(c):
    s = str(c)
    while len(s) < 3:
        s = "0" + s
    return s[0:len(s) - 2] + "." + s[len(s) - 2:len(s)]

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"
```

## README

# P012 - Library loans

A small library of six books and its members. Members borrow books and bring
them back; every loan has a due date, a late book earns a fine, and new
members can join. There is no clock: the librarian types today's date at the
start and moves it forward by hand, so the same session always gives the
same dates.

- `main.eml` - the menu and its questions, with their checks
- `library.eml` - the books and the lending rules: who may borrow, when a
  loan is late, and what the fine is
- `dates.eml` - calendar dates: the leap-year rule, the days in a month,
  counting the days between two dates, adding days to a date, and reading and
  writing YYYY-MM-DD
- `text.eml` - trimming, lower case, reading a whole number, money and plurals

The rules: a loan lasts 14 days; a member has at most 3 books at once, and
cannot borrow while owing a fine; a book returned late costs 0.25 for every
day past its due date, at most 5.00 a book. The fine is added to what the
member owes, and paying clears it. The loans list shows, for every late
book, the days it is late and the fine so far.

Dates are real Gregorian dates from 2000 to 2099. A year divisible by 4 is a
leap year, except one divisible by 100, except one divisible by 400 after all,
so a loan made on 2028-02-20 is due on 2028-03-05 and one made on 2028-02-29
is due on 2028-03-14. Days are counted from 2000-01-01, which makes the days
late a subtraction.

What is checked: a date is written YYYY-MM-DD, with a month that exists and a
day that month has (2027-02-29 is refused, 2028-02-29 is not), and today's
date only moves forward; a member or book number is one that exists; a new
member's name has at most 20 characters and is not already taken in any
case. Anything else asks again; nothing cancels, except for the first date,
which is needed.

Sessions: `sessions/basic.in` starts on 2028-02-20, lends two books to one
member and one to another, moves the date until two loans are late, takes one
back with a fine, refuses a loan until the fine is paid, takes a book back on
time, adds a member, and lists the members and the books;
`sessions/bad-input.in` types dates that are not dates, out of range or
before today, picks numbers that do not exist, borrows a book that is out,
goes over three books, returns a book that is on the shelf, pays when nothing
is owed, lets three loans run 27 days late - the fine stops at 5.00 - and
tries a name that is too long and one that is taken.

Built on the verified corpus cases `library-catalog` (books lent and
returned, and a loan refused when a book is already out) and
`days-between-dates` (the days between two dates, with the full leap-year
rule).
