<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P030-room-booking/ | updated: 2026-10-06 -->

# P030 Room booking

Books three meeting rooms of 4, 8 and 12 seats over a working week. A booking that overlaps another in the same room is refused, naming the booking it clashes with and the rooms that are free instead - back-to-back bookings do not clash; each day has a schedule, free rooms can be looked up, and bookings can be cancelled.

EML-P project `projects/room-booking` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/room-booking` runs it and `eml project verify projects/room-booking` 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: interval-overlap-detector (https://efficientnewlanguage.org/cases/205-interval-overlap-detector/).

## Sessions

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

Input:

```text
0
x
2
Sat

3
1
Maple

1
Oak
funday
Tue
7:30
25:00
19:45
19:30
20:15
20:00
5
0
4

1
Pine
Wed
08:00
12:30
12:00
13
8
A title that is thirty-one long
Workshop
1
pine
wed
11:59
12:01
2
3
abc
#7

3
#1
4
Wed
08:00
12:00
12
4

5
```

Screen:

```text
== Room booking ==
Rooms: Oak (4 seats), Pine (8) and Cedar (12); Mon to Fri, 08:00 to 20:00.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 0
Pick a number from 1 to 5.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> x
Pick a number from 1 to 5.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 2
day (Mon to Fri)> Sat
Type a weekday, Mon to Fri.
day (Mon to Fri)> 
Cancelled.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 3
There are no bookings.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Maple
There are three rooms: Oak, Pine and Cedar.
room (Oak, Pine or Cedar)> 
Cancelled.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Oak
day (Mon to Fri)> funday
Type a weekday, Mon to Fri.
day (Mon to Fri)> Tue
from (HH:MM)> 7:30
The rooms are open from 08:00 to 20:00; type a start such as 09:30.
from (HH:MM)> 25:00
The rooms are open from 08:00 to 20:00; type a start such as 09:30.
from (HH:MM)> 19:45
to (HH:MM)> 19:30
Type an end after 19:45 and no later than 20:00.
to (HH:MM)> 20:15
Type an end after 19:45 and no later than 20:00.
to (HH:MM)> 20:00
people> 5
Oak has 4 seats.
people> 0
Type a number of people from 1 to 12.
people> 4
title> 
Cancelled.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Pine
day (Mon to Fri)> Wed
from (HH:MM)> 08:00
to (HH:MM)> 12:30
A booking can last at most 4 hours.
to (HH:MM)> 12:00
people> 13
Type a number of people from 1 to 12.
people> 8
title> A title that is thirty-one long
A title can have at most 30 characters.
title> Workshop
Booked #1: Pine, Wed 08:00-12:00, Workshop (8 people).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> pine
day (Mon to Fri)> wed
from (HH:MM)> 11:59
to (HH:MM)> 12:01
people> 2
Pine is booked then:
  #1 08:00-12:00 Workshop
Free then for 2 people: Oak (4 seats) and Cedar (12 seats).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 3
booking number> abc
Type a booking number, such as 3.
booking number> #7
There is no booking #7.
booking number> 
Cancelled.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 3
booking number> #1
Cancelled #1: Pine, Wed 08:00-12:00, Workshop.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 4
day (Mon to Fri)> Wed
from (HH:MM)> 08:00
to (HH:MM)> 12:00
people> 12
Free on Wed 08:00-12:00 for 12 people: Cedar (12 seats).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 4
day (Mon to Fri)> 
Cancelled.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 5
Bye.
```

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

Input:

```text
1
Oak
mon
09:00
09:45
3
Standup
1
Oak
Monday
09:45
10:30
4
Design review
1
oak
Mon
10:00
11:00
3
1
Cedar
Mon
13:00
14:00
12
All hands
1
Pine
Mon
13:30
14:30
6
Interview
2
Mon
4
Mon
13:00
14:00
6
4
Tue
14:00
15:30
6
3
2
1
Oak
Mon
10:00
11:00
3
Planning
2
mon
5
```

Screen:

```text
== Room booking ==
Rooms: Oak (4 seats), Pine (8) and Cedar (12); Mon to Fri, 08:00 to 20:00.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Oak
day (Mon to Fri)> mon
from (HH:MM)> 09:00
to (HH:MM)> 09:45
people> 3
title> Standup
Booked #1: Oak, Mon 09:00-09:45, Standup (3 people).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Oak
day (Mon to Fri)> Monday
from (HH:MM)> 09:45
to (HH:MM)> 10:30
people> 4
title> Design review
Booked #2: Oak, Mon 09:45-10:30, Design review (4 people).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> oak
day (Mon to Fri)> Mon
from (HH:MM)> 10:00
to (HH:MM)> 11:00
people> 3
Oak is booked then:
  #2 09:45-10:30 Design review
Free then for 3 people: Pine (8 seats) and Cedar (12 seats).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Cedar
day (Mon to Fri)> Mon
from (HH:MM)> 13:00
to (HH:MM)> 14:00
people> 12
title> All hands
Booked #3: Cedar, Mon 13:00-14:00, All hands (12 people).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Pine
day (Mon to Fri)> Mon
from (HH:MM)> 13:30
to (HH:MM)> 14:30
people> 6
title> Interview
Booked #4: Pine, Mon 13:30-14:30, Interview (6 people).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 2
day (Mon to Fri)> Mon
Mon:
  Oak (4 seats)     09:00-09:45  #1 Standup (3 people)
                    09:45-10:30  #2 Design review (4 people)
  Pine (8 seats)    13:30-14:30  #4 Interview (6 people)
  Cedar (12 seats)  13:00-14:00  #3 All hands (12 people)

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 4
day (Mon to Fri)> Mon
from (HH:MM)> 13:00
to (HH:MM)> 14:00
people> 6
No room is free on Mon 13:00-14:00 for 6 people.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 4
day (Mon to Fri)> Tue
from (HH:MM)> 14:00
to (HH:MM)> 15:30
people> 6
Free on Tue 14:00-15:30 for 6 people: Pine (8 seats) and Cedar (12 seats).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 3
booking number> 2
Cancelled #2: Oak, Mon 09:45-10:30, Design review.

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 1
room (Oak, Pine or Cedar)> Oak
day (Mon to Fri)> Mon
from (HH:MM)> 10:00
to (HH:MM)> 11:00
people> 3
title> Planning
Booked #5: Oak, Mon 10:00-11:00, Planning (3 people).

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 2
day (Mon to Fri)> mon
Mon:
  Oak (4 seats)     09:00-09:45  #1 Standup (3 people)
                    10:00-11:00  #5 Planning (3 people)
  Pine (8 seats)    13:30-14:30  #4 Interview (6 people)
  Cedar (12 seats)  13:00-14:00  #3 All hands (12 people)

1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P030 room booking: three meeting rooms over a working week. A booking that
# overlaps another in the same room is refused, with the booking it clashes
# with and the rooms that are free instead; each day has a schedule, and
# bookings can be cancelled.
import bookings
import hours

8 * 60 => opening
20 * 60 => closing
4 * 60 => longest

def listed(items):
    "" => out
    0 => i
    while i < len(items):
        if i > 0 and i == len(items) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + items[i] => out
        i + 1 => i
    return out

def head_count(n):
    if n == 1:
        return "1 person"
    return str(n) + " people"

def whole_number(s):
    # 1 to 3 digits, with an optional # in front; -1 for anything else.
    hours.trim(s) => s
    if s != "" and s[0] == "#":
        s[1:len(s)] => s
    if s == "" or len(s) > 3:
        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 ask_room():
    while True:
        hours.trim(input("room (Oak, Pine or Cedar)> ")) => answer
        if answer == "":
            return 0 - 1
        bookings.room_of(answer) => r
        if r >= 0:
            return r
        "There are three rooms: Oak, Pine and Cedar." ^0

def ask_day():
    while True:
        input("day (Mon to Fri)> ") => answer
        if hours.trim(answer) == "":
            return 0 - 1
        hours.day_of(answer) => d
        if d >= 0:
            return d
        "Type a weekday, Mon to Fri." ^0

def ask_times():
    # [start, end] inside opening hours, at most 4 hours long; [] cancels.
    0 - 1 => start
    while start == 0 - 1:
        input("from (HH:MM)> ") => answer
        if hours.trim(answer) == "":
            return []
        hours.minute_of(answer) => start
        if start == 0 - 1 or start < opening or start >= closing:
            "The rooms are open from 08:00 to 20:00; type a start such as 09:30." ^0
            0 - 1 => start
    0 - 1 => end
    while end == 0 - 1:
        input("to (HH:MM)> ") => answer
        if hours.trim(answer) == "":
            return []
        hours.minute_of(answer) => end
        if end == 0 - 1 or end <= start or end > closing:
            ("Type an end after " + hours.clock(start) + " and no later than 20:00.") ^0
            0 - 1 => end
        elif end - start > longest:
            "A booking can last at most 4 hours." ^0
            0 - 1 => end
    return [start, end]

def ask_people(most):
    while True:
        input("people> ") => answer
        if hours.trim(answer) == "":
            return 0 - 1
        whole_number(answer) => n
        if n >= 1 and n <= most:
            return n
        ("Type a number of people from 1 to " + str(most) + ".") ^0

def room_name(r):
    return bookings.rooms[r] + " (" + str(bookings.seats[r]) + " seats)"

def describe(b):
    return "#" + str(b[0]) + ": " + bookings.rooms[b[1]] + ", " + hours.days[b[2]] + " " + hours.span(b[3], b[4]) + ", " + b[6]

def say_free(found, people):
    [] => names
    for r in found:
        names + [room_name(r)] => names
    if len(names) == 0:
        ("No room is free then for " + head_count(people) + ".") ^0
    else:
        ("Free then for " + head_count(people) + ": " + listed(names) + ".") ^0

def book(state):
    # state is [bookings, next number]; returns the new state.
    ask_room() => r
    if r == 0 - 1:
        "Cancelled." ^0
        return state
    ask_day() => d
    if d == 0 - 1:
        "Cancelled." ^0
        return state
    ask_times() => t
    if len(t) == 0:
        "Cancelled." ^0
        return state
    ask_people(12) => people
    while people > bookings.seats[r]:
        (bookings.rooms[r] + " has " + str(bookings.seats[r]) + " seats.") ^0
        ask_people(12) => people
    if people == 0 - 1:
        "Cancelled." ^0
        return state
    bookings.clashes(state[0], r, d, t[0], t[1]) => taken
    if len(taken) > 0:
        (bookings.rooms[r] + " is booked then:") ^0
        for b in taken:
            ("  #" + str(b[0]) + " " + hours.span(b[3], b[4]) + " " + b[6]) ^0
        say_free(bookings.free_rooms(state[0], d, t[0], t[1], people), people)
        return state
    "" => title
    while title == "":
        hours.trim(input("title> ")) => title
        if title == "":
            "Cancelled." ^0
            return state
        if len(title) > 30:
            "A title can have at most 30 characters." ^0
            "" => title
    [state[1], r, d, t[0], t[1], people, title] => b
    ("Booked " + describe(b) + " (" + head_count(people) + ").") ^0
    return [state[0] + [b], state[1] + 1]

def schedule(state):
    ask_day() => d
    if d == 0 - 1:
        "Cancelled." ^0
        return
    (hours.days[d] + ":") ^0
    0 => width
    for r in [0:len(bookings.rooms) - 1]:
        if len(room_name(r)) > width:
            len(room_name(r)) => width
    for r in [0:len(bookings.rooms) - 1]:
        room_name(r) => label
        while len(label) < width:
            label + " " => label
        bookings.in_order(state[0], r, d) => day_bookings
        if len(day_bookings) == 0:
            ("  " + label + "  free all day") ^0
        else:
            for b in day_bookings:
                ("  " + label + "  " + hours.span(b[3], b[4]) + "  #" + str(b[0]) + " " + b[6] + " (" + head_count(b[5]) + ")") ^0
                " " * width => label

def cancel(state):
    if len(state[0]) == 0:
        "There are no bookings." ^0
        return state
    while True:
        input("booking number> ") => answer
        if hours.trim(answer) == "":
            "Cancelled." ^0
            return state
        whole_number(answer) => n
        None => found
        for b in state[0]:
            if b[0] == n:
                b => found
        if n == 0 - 1:
            "Type a booking number, such as 3." ^0
        elif found == None:
            ("There is no booking #" + str(n) + ".") ^0
        else:
            ("Cancelled " + describe(found) + ".") ^0
            return [bookings.without(state[0], n), state[1]]

def free(state):
    ask_day() => d
    if d == 0 - 1:
        "Cancelled." ^0
        return
    ask_times() => t
    if len(t) == 0:
        "Cancelled." ^0
        return
    ask_people(12) => people
    if people == 0 - 1:
        "Cancelled." ^0
        return
    bookings.free_rooms(state[0], d, t[0], t[1], people) => found
    [] => names
    for r in found:
        names + [room_name(r)] => names
    "on " + hours.days[d] + " " + hours.span(t[0], t[1]) + " for " + head_count(people) => when
    if len(names) == 0:
        ("No room is free " + when + ".") ^0
    else:
        ("Free " + when + ": " + listed(names) + ".") ^0

"== Room booking ==" ^0
"Rooms: Oak (4 seats), Pine (8) and Cedar (12); Mon to Fri, 08:00 to 20:00." ^0
[[], 1] => state
True => running
while running:
    "" ^0
    "1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit" ^0
    hours.trim(input("choice> ")) => choice
    if choice == "1":
        book(state) => state
    elif choice == "2":
        schedule(state)
    elif choice == "3":
        cancel(state) => state
    elif choice == "4":
        free(state)
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import bookings
import hours
opening = 8 * 60
closing = 20 * 60
longest = 4 * 60

def listed(items):
    out = ""
    i = 0
    while i < len(items):
        if i > 0 and i == len(items) - 1:
            out = out + " and "
        elif i > 0:
            out = out + ", "
        out = out + items[i]
        i = i + 1
    return out

def head_count(n):
    if n == 1:
        return "1 person"
    return str(n) + " people"

def whole_number(s):
    s = hours.trim(s)
    if s != "" and s[0] == "#":
        s = s[1:len(s)]
    if s == "" or len(s) > 3:
        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 ask_room():
    while True:
        answer = hours.trim(input("room (Oak, Pine or Cedar)> "))
        if answer == "":
            return 0 - 1
        r = bookings.room_of(answer)
        if r >= 0:
            return r
        print("There are three rooms: Oak, Pine and Cedar.")

def ask_day():
    while True:
        answer = input("day (Mon to Fri)> ")
        if hours.trim(answer) == "":
            return 0 - 1
        d = hours.day_of(answer)
        if d >= 0:
            return d
        print("Type a weekday, Mon to Fri.")

def ask_times():
    start = 0 - 1
    while start == 0 - 1:
        answer = input("from (HH:MM)> ")
        if hours.trim(answer) == "":
            return []
        start = hours.minute_of(answer)
        if start == 0 - 1 or start < opening or start >= closing:
            print("The rooms are open from 08:00 to 20:00; type a start such as 09:30.")
            start = 0 - 1
    end = 0 - 1
    while end == 0 - 1:
        answer = input("to (HH:MM)> ")
        if hours.trim(answer) == "":
            return []
        end = hours.minute_of(answer)
        if end == 0 - 1 or end <= start or end > closing:
            print("Type an end after " + hours.clock(start) + " and no later than 20:00.")
            end = 0 - 1
        elif end - start > longest:
            print("A booking can last at most 4 hours.")
            end = 0 - 1
    return [start, end]

def ask_people(most):
    while True:
        answer = input("people> ")
        if hours.trim(answer) == "":
            return 0 - 1
        n = whole_number(answer)
        if n >= 1 and n <= most:
            return n
        print("Type a number of people from 1 to " + str(most) + ".")

def room_name(r):
    return bookings.rooms[r] + " (" + str(bookings.seats[r]) + " seats)"

def describe(b):
    return "#" + str(b[0]) + ": " + bookings.rooms[b[1]] + ", " + hours.days[b[2]] + " " + hours.span(b[3], b[4]) + ", " + b[6]

def say_free(found, people):
    names = []
    for r in found:
        names = names + [room_name(r)]
    if len(names) == 0:
        print("No room is free then for " + head_count(people) + ".")
    else:
        print("Free then for " + head_count(people) + ": " + listed(names) + ".")

def book(state):
    r = ask_room()
    if r == 0 - 1:
        print("Cancelled.")
        return state
    d = ask_day()
    if d == 0 - 1:
        print("Cancelled.")
        return state
    t = ask_times()
    if len(t) == 0:
        print("Cancelled.")
        return state
    people = ask_people(12)
    while people > bookings.seats[r]:
        print(bookings.rooms[r] + " has " + str(bookings.seats[r]) + " seats.")
        people = ask_people(12)
    if people == 0 - 1:
        print("Cancelled.")
        return state
    taken = bookings.clashes(state[0], r, d, t[0], t[1])
    if len(taken) > 0:
        print(bookings.rooms[r] + " is booked then:")
        for b in taken:
            print("  #" + str(b[0]) + " " + hours.span(b[3], b[4]) + " " + b[6])
        say_free(bookings.free_rooms(state[0], d, t[0], t[1], people), people)
        return state
    title = ""
    while title == "":
        title = hours.trim(input("title> "))
        if title == "":
            print("Cancelled.")
            return state
        if len(title) > 30:
            print("A title can have at most 30 characters.")
            title = ""
    b = [state[1], r, d, t[0], t[1], people, title]
    print("Booked " + describe(b) + " (" + head_count(people) + ").")
    return [state[0] + [b], state[1] + 1]

def schedule(state):
    d = ask_day()
    if d == 0 - 1:
        print("Cancelled.")
        return
    print(hours.days[d] + ":")
    width = 0
    for r in range(0, len(bookings.rooms)):
        if len(room_name(r)) > width:
            width = len(room_name(r))
    for r in range(0, len(bookings.rooms)):
        label = room_name(r)
        while len(label) < width:
            label = label + " "
        day_bookings = bookings.in_order(state[0], r, d)
        if len(day_bookings) == 0:
            print("  " + label + "  free all day")
        else:
            for b in day_bookings:
                print("  " + label + "  " + hours.span(b[3], b[4]) + "  #" + str(b[0]) + " " + b[6] + " (" + head_count(b[5]) + ")")
                label = " " * width

def cancel(state):
    if len(state[0]) == 0:
        print("There are no bookings.")
        return state
    while True:
        answer = input("booking number> ")
        if hours.trim(answer) == "":
            print("Cancelled.")
            return state
        n = whole_number(answer)
        found = None
        for b in state[0]:
            if b[0] == n:
                found = b
        if n == 0 - 1:
            print("Type a booking number, such as 3.")
        elif found == None:
            print("There is no booking #" + str(n) + ".")
        else:
            print("Cancelled " + describe(found) + ".")
            return [bookings.without(state[0], n), state[1]]

def free(state):
    d = ask_day()
    if d == 0 - 1:
        print("Cancelled.")
        return
    t = ask_times()
    if len(t) == 0:
        print("Cancelled.")
        return
    people = ask_people(12)
    if people == 0 - 1:
        print("Cancelled.")
        return
    found = bookings.free_rooms(state[0], d, t[0], t[1], people)
    names = []
    for r in found:
        names = names + [room_name(r)]
    when = "on " + hours.days[d] + " " + hours.span(t[0], t[1]) + " for " + head_count(people)
    if len(names) == 0:
        print("No room is free " + when + ".")
    else:
        print("Free " + when + ": " + listed(names) + ".")

print("== Room booking ==")
print("Rooms: Oak (4 seats), Pine (8) and Cedar (12); Mon to Fri, 08:00 to 20:00.")
state = [[], 1]
running = True
while running:
    print("")
    print("1) book a room  2) day schedule  3) cancel a booking  4) free rooms  5) quit")
    choice = hours.trim(input("choice> "))
    if choice == "1":
        state = book(state)
    elif choice == "2":
        schedule(state)
    elif choice == "3":
        state = cancel(state)
    elif choice == "4":
        free(state)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### bookings.eml

```eml
# P030 room booking - rooms, bookings and conflicts. A booking is
# [number, room, day, start, end, people, title]; room and day are indexes,
# start and end are minutes after midnight.

["Oak", "Pine", "Cedar"] => rooms
[4, 8, 12] => seats

def overlaps(a_start, a_end, b_start, b_end):
    # Half-open times [start, end): a booking that ends at 10:00 and one that
    # starts at 10:00 do not overlap - the predicate of the corpus case
    # interval-overlap-detector.
    return a_start < b_end and b_start < a_end

def clashes(bookings, room, day, start, end):
    # The bookings of that room on that day that overlap [start, end), in
    # the order they were made.
    [] => out
    for b in bookings:
        if b[1] == room and b[2] == day and overlaps(start, end, b[3], b[4]):
            out + [b] => out
    return out

def free_rooms(bookings, day, start, end, people):
    # The indexes of the rooms with enough seats and nothing booked in
    # [start, end) that day.
    [] => out
    for r in [0:len(rooms) - 1]:
        if seats[r] >= people and len(clashes(bookings, r, day, start, end)) == 0:
            out + [r] => out
    return out

def in_order(bookings, room, day):
    # That room's bookings on that day, earliest first (insertion sort).
    [] => out
    for b in bookings:
        if b[1] == room and b[2] == day:
            len(out) => j
            while j > 0 and out[j - 1][3] > b[3]:
                j - 1 => j
            out[0:j] + [b] + out[j:len(out)] => out
    return out

def without(bookings, number):
    # The bookings with that number left out.
    [] => out
    for b in bookings:
        if b[0] != number:
            out + [b] => out
    return out

def room_of(name):
    # The index of a room by name, in any case; -1 if there is none.
    for r in [0:len(rooms) - 1]:
        if lower(rooms[r]) == lower(name):
            return r
    return 0 - 1

def lower(s):
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper
    "abcdefghijklmnopqrstuvwxyz" => small
    "" => out
    for c in s:
        0 => k
        while k < 26 and upper[k] != c:
            k + 1 => k
        if k < 26:
            out + small[k] => out
        else:
            out + c => out
    return out
```

Python projection of bookings.eml:

```python
rooms = ["Oak", "Pine", "Cedar"]
seats = [4, 8, 12]

def overlaps(a_start, a_end, b_start, b_end):
    return a_start < b_end and b_start < a_end

def clashes(bookings, room, day, start, end):
    out = []
    for b in bookings:
        if b[1] == room and b[2] == day and overlaps(start, end, b[3], b[4]):
            out = out + [b]
    return out

def free_rooms(bookings, day, start, end, people):
    out = []
    for r in range(0, len(rooms)):
        if seats[r] >= people and len(clashes(bookings, r, day, start, end)) == 0:
            out = out + [r]
    return out

def in_order(bookings, room, day):
    out = []
    for b in bookings:
        if b[1] == room and b[2] == day:
            j = len(out)
            while j > 0 and out[j - 1][3] > b[3]:
                j = j - 1
            out = out[0:j] + [b] + out[j:len(out)]
    return out

def without(bookings, number):
    out = []
    for b in bookings:
        if b[0] != number:
            out = out + [b]
    return out

def room_of(name):
    for r in range(0, len(rooms)):
        if lower(rooms[r]) == lower(name):
            return r
    return 0 - 1

def lower(s):
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    small = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        k = 0
        while k < 26 and upper[k] != c:
            k = k + 1
        if k < 26:
            out = out + small[k]
        else:
            out = out + c
    return out
```

### hours.eml

```eml
# P030 room booking - days and times. A time is kept as minutes after
# midnight; the interpreter that checks every session does not run string
# methods yet, so the text handling is written out here.

["Mon", "Tue", "Wed", "Thu", "Fri"] => days
"0123456789" => digits

def trim(s):
    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):
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper
    "abcdefghijklmnopqrstuvwxyz" => small
    "" => out
    for c in s:
        0 => k
        while k < 26 and upper[k] != c:
            k + 1 => k
        if k < 26:
            out + small[k] => out
        else:
            out + c => out
    return out

def day_of(s):
    # The index of a weekday from its name or its first three letters, in
    # any case ("mon", "Monday"); -1 otherwise.
    lower(trim(s)) => s
    ["monday", "tuesday", "wednesday", "thursday", "friday"] => names
    for i in [0:4]:
        if s == names[i] or (len(s) == 3 and s == names[i][0:3]):
            return i
    return 0 - 1

def minute_of(s):
    # Minutes after midnight for "9:30" or "09:30" (00:00 to 23:59), else -1.
    trim(s) => s
    0 => colon
    while colon < len(s) and s[colon] != ":":
        colon + 1 => colon
    if colon == 0 or colon > 2 or colon + 3 != len(s):
        return 0 - 1
    0 => h
    for c in s[0:colon]:
        if not (c in digits):
            return 0 - 1
        h * 10 + int(c) => h
    if not (s[colon + 1] in digits and s[colon + 2] in digits):
        return 0 - 1
    int(s[colon + 1]) * 10 + int(s[colon + 2]) => m
    if h > 23 or m > 59:
        return 0 - 1
    return h * 60 + m

def clock(minute):
    # 570 as "09:30".
    int((minute - minute % 60) / 60) => h
    minute % 60 => m
    "" => out
    if h < 10:
        "0" => out
    out + str(h) + ":" => out
    if m < 10:
        out + "0" => out
    return out + str(m)

def span(start, end):
    return clock(start) + "-" + clock(end)
```

Python projection of hours.eml:

```python
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
digits = "0123456789"

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 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    small = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        k = 0
        while k < 26 and upper[k] != c:
            k = k + 1
        if k < 26:
            out = out + small[k]
        else:
            out = out + c
    return out

def day_of(s):
    s = lower(trim(s))
    names = ["monday", "tuesday", "wednesday", "thursday", "friday"]
    for i in range(0, 5):
        if s == names[i] or len(s) == 3 and s == names[i][0:3]:
            return i
    return 0 - 1

def minute_of(s):
    s = trim(s)
    colon = 0
    while colon < len(s) and s[colon] != ":":
        colon = colon + 1
    if colon == 0 or colon > 2 or colon + 3 != len(s):
        return 0 - 1
    h = 0
    for c in s[0:colon]:
        if not c in digits:
            return 0 - 1
        h = h * 10 + int(c)
    if not (s[colon + 1] in digits and s[colon + 2] in digits):
        return 0 - 1
    m = int(s[colon + 1]) * 10 + int(s[colon + 2])
    if h > 23 or m > 59:
        return 0 - 1
    return h * 60 + m

def clock(minute):
    h = int((minute - minute % 60) / 60)
    m = minute % 60
    out = ""
    if h < 10:
        out = "0"
    out = out + str(h) + ":"
    if m < 10:
        out = out + "0"
    return out + str(m)

def span(start, end):
    return clock(start) + "-" + clock(end)
```

## README

# P030 - Room booking

Books three meeting rooms - Oak (4 seats), Pine (8) and Cedar (12) - from
Monday to Friday, 08:00 to 20:00. A booking that overlaps another in the same
room is refused: the program names the booking it clashes with and the rooms
that are free at that time for that many people. Each day has a schedule,
free rooms can be looked up, and bookings can be cancelled by number.

- `main.eml` - the menu, the questions and their checks, and what the screen
  shows
- `bookings.eml` - the rooms, the overlap test, clashes, free rooms, a room's
  day in order, and cancelling
- `hours.eml` - weekdays, reading and writing times

How each part works:

- A booking runs from its start up to, not including, its end. Two bookings
  overlap when each starts before the other ends - the predicate of the
  corpus case `interval-overlap-detector`. So 09:00-09:45 and 09:45-10:30 do
  not clash (back-to-back is the most common case in a real calendar), while
  11:59-12:01 clashes with 08:00-12:00.
- Before a booking is made, the clashes are looked for in that room on that
  day; if there are any, the booking is refused with each clashing booking
  and the list of rooms with enough seats and no clash.
- A day's schedule lists each room's bookings, earliest first, with numbers;
  a room with none is free all day.
- Booking numbers count up from 1 and are never reused, so a cancelled
  number stays gone.

What is checked: a room by name in any case; a weekday by its name or its
first three letters; a start from 08:00 and an end after it, no later than
20:00, at most 4 hours later; 1 to 12 people, and no more than the room's
seats; a title of 1 to 30 characters; a booking number that exists (with or
without `#`). An empty answer cancels.

Sessions: `sessions/basic.in` books a stand-up and a back-to-back design
review in Oak, is refused 10:00-11:00 in Oak (clash with the review; Pine and
Cedar are free), books Cedar and Pine in overlapping hours, shows Monday,
finds no room for 6 people on Monday 13:00-14:00 and two on Tuesday, cancels
the review and then books 10:00-11:00 in Oak; `sessions/bad-input.in` types
menu choices that are not 1 to 5, Saturday, a cancel with no bookings, a room
that does not exist, `funday`, starts before opening and at 25:00, ends
before the start and after closing, too many people for Oak, 0 people, a
booking longer than 4 hours, 13 people, a title of 31 characters, a
two-minute booking inside another, a booking number that is not a number and
one that does not exist.

Built on the verified corpus case `interval-overlap-detector` (half-open
intervals: touching is not overlapping).
