<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P070-meeting-planner/ | updated: 2026-10-14 -->

# P070 Meeting planner

A day of meetings with their people: booking refuses a time when one of them is already in a meeting (half-open times, so back-to-back is fine), each person's busy time merged, the free time a group shares, and the fewest rooms the day needs with one way to assign them.

EML-P project `projects/meeting-planner` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/meeting-planner` runs it and `eml project verify projects/meeting-planner` 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
standup
a title that is far too long for it
review
Ana,, Ben
Ana1
Ana, Ana
Ana, Ben
25:00
9:3
12:00
11:00
24:01

3
Ana B
Zoe
4
Ana
4
abc
480
4
Ben, Chloe, Dev
240
6
nothing
2
late
Ben
14:59
15:30
2
evening
Gus
23:00
24:00
3
Gus
7
```

Screen:

```text
== Meeting planner ==
A day of meetings: book them, see who is busy, find shared free time, count the rooms.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 0
Pick a number from 1 to 7.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> x
Pick a number from 1 to 7.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 2
title> standup
There is a meeting called standup already.
title> a title that is far too long for it
A title is up to 30 characters.
title> review
people (like Ana, Ben)> Ana,, Ben
Type names of letters, up to 12 each, separated by commas, each once.
people (like Ana, Ben)> Ana1
Type names of letters, up to 12 each, separated by commas, each once.
people (like Ana, Ben)> Ana, Ana
Type names of letters, up to 12 each, separated by commas, each once.
people (like Ana, Ben)> Ana, Ben
from (like 9:30)> 25:00
Type a time like 9:30, from 0:00 to 23:59.
from (like 9:30)> 9:3
Type a time like 9:30, from 0:00 to 23:59.
from (like 9:30)> 12:00
to> 11:00
A meeting has to end after it starts.
to> 24:01
Type a time like 10:15, up to 24:00.
to> 
Cancelled.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 3
person> Ana B
Type a name of letters, up to 12.
person> Zoe
Nobody called Zoe has a meeting.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 4
people (like Ana, Ben)> Ana
at least how many minutes (5 to 480)> 4
Type a whole number from 5 to 480.
at least how many minutes (5 to 480)> abc
Type a whole number from 5 to 480.
at least how many minutes (5 to 480)> 480
Between 09:00 and 17:00, Ana is never free for 480 minutes.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 4
people (like Ana, Ben)> Ben, Chloe, Dev
at least how many minutes (5 to 480)> 240
Between 09:00 and 17:00, Ben, Chloe and Dev are never all free for 240 minutes.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 6
title> nothing
No meeting is called nothing.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 2
title> late
people (like Ana, Ben)> Ben
from (like 9:30)> 14:59
to> 15:30
Ben is already in deploy window, 13:30-15:00 - 1 minute of it would clash.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 2
title> evening
people (like Ana, Ben)> Gus
from (like 9:30)> 23:00
to> 24:00
Booked evening, 23:00-24:00.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 3
person> Gus
Gus is busy 23:00-24:00 - 1 h in all.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 7
Bye.
```

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

Input:

```text
1
3
Ana
4
Ana, Ben
30
4
Chloe, Dev
60
2
planning
Ana, Ben
10:15
11:00
2
all-hands
Eli
9:30
10:20
5
6
all-hands
5
1
7
```

Screen:

```text
== Meeting planner ==
A day of meetings: book them, see who is busy, find shared free time, count the rooms.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 1
  09:00-09:15  standup (Ana, Ben and Chloe)
  09:15-10:15  design review (Ana and Ben)
  10:00-10:30  one-to-one (Chloe and Dev)
  12:00-13:00  lunch talk (Ben, Chloe and Dev)
  13:00-14:00  retro (Ana and Dev)
  13:30-15:00  deploy window (Ben)
  15:30-16:15  interview (Ana and Chloe)

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 3
person> Ana
Ana is busy 09:00-10:15, 13:00-14:00, 15:30-16:15 - 3 h in all.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 4
people (like Ana, Ben)> Ana, Ben
at least how many minutes (5 to 480)> 30
Between 09:00 and 17:00, Ana and Ben are all free for 30 minutes or more at 10:15-12:00 (1 h 45 min), 15:00-15:30 (30 min), 16:15-17:00 (45 min).

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 4
people (like Ana, Ben)> Chloe, Dev
at least how many minutes (5 to 480)> 60
Between 09:00 and 17:00, Chloe and Dev are all free for 60 minutes or more at 10:30-12:00 (1 h 30 min), 14:00-15:30 (1 h 30 min).

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 2
title> planning
people (like Ana, Ben)> Ana, Ben
from (like 9:30)> 10:15
to> 11:00
Booked planning, 10:15-11:00.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 2
title> all-hands
people (like Ana, Ben)> Eli
from (like 9:30)> 9:30
to> 10:20
Booked all-hands, 09:30-10:20.
The day now needs 3 rooms.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 5
At most 3 meetings run at once, first at 10:00 - so 3 rooms are enough:
  room 1: 09:00-09:15 standup, 09:15-10:15 design review, 10:15-11:00 planning, 13:30-15:00 deploy window
  room 2: 09:30-10:20 all-hands, 12:00-13:00 lunch talk, 15:30-16:15 interview
  room 3: 10:00-10:30 one-to-one, 13:00-14:00 retro

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 6
title> all-hands
Removed all-hands.

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 5
At most 2 meetings run at once, first at 10:00 - so 2 rooms are enough:
  room 1: 09:00-09:15 standup, 09:15-10:15 design review, 10:15-11:00 planning, 13:00-14:00 retro, 15:30-16:15 interview
  room 2: 10:00-10:30 one-to-one, 12:00-13:00 lunch talk, 13:30-15:00 deploy window

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 1
  09:00-09:15  standup (Ana, Ben and Chloe)
  09:15-10:15  design review (Ana and Ben)
  10:00-10:30  one-to-one (Chloe and Dev)
  10:15-11:00  planning (Ana and Ben)
  12:00-13:00  lunch talk (Ben, Chloe and Dev)
  13:00-14:00  retro (Ana and Dev)
  13:30-15:00  deploy window (Ben)
  15:30-16:15  interview (Ana and Chloe)

1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit
choice> 7
Bye.
```

## Modules

### main.eml

```eml
# P070 meeting planner: a day of meetings, each with its people. Booking
# refuses a time when one of them is already in a meeting; there is each
# person's busy time merged, the free time a group shares, and the fewest
# rooms the day needs.
import intervals

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 digits(s):
    # The value of a string of 1 or 2 digits, or -1.
    if len(s) == 0 or len(s) > 2:
        return -1
    0 => n
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n * 10 + int(c) => n
    return n

def number(s):
    # A whole number of up to 4 digits, or -1.
    if len(s) == 0 or len(s) > 4:
        return -1
    0 => n
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n * 10 + int(c) => n
    return n

def minutes(k):
    if k == 1:
        return "1 minute"
    return str(k) + " minutes"

def clock(m):
    # Minutes from midnight as HH:MM.
    0 => h
    m => r
    while r >= 60:
        r - 60 => r
        h + 1 => h
    "" => hh
    if h < 10:
        "0" => hh
    "" => mm
    if r < 10:
        "0" => mm
    return hh + str(h) + ":" + mm + str(r)

def span(x):
    return clock(x[0]) + "-" + clock(x[1])

def length(m):
    # 135 -> "2 h 15 min"
    0 => h
    m => r
    while r >= 60:
        r - 60 => r
        h + 1 => h
    if h == 0:
        return str(r) + " min"
    if r == 0:
        return str(h) + " h"
    return str(h) + " h " + str(r) + " min"

def time_of(s):
    # "9:30" or "09:30" as minutes from midnight; 24:00 is the end of the
    # day. -1 if it is not a time.
    0 => k
    while k < len(s) and s[k] != ":":
        k + 1 => k
    if k == len(s) or len(s) - k != 3:
        return -1
    digits(s[0:k]) => h
    digits(s[k + 1:len(s)]) => m
    if h < 0 or m < 0 or m > 59 or h > 24 or (h == 24 and m > 0):
        return -1
    return h * 60 + m

def joined(names):
    # ["Ana", "Ben", "Dev"] -> "Ana, Ben and Dev"
    "" => s
    for i in [0:len(names) - 1]:
        if i > 0 and i == len(names) - 1:
            s + " and " => s
        elif i > 0:
            s + ", " => s
        s + names[i] => s
    return s

def is_name(s):
    if len(s) == 0 or len(s) > 12:
        return False
    for c in s:
        if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z")):
            return False
    return True

def names_of(s):
    # "Ana, Ben" -> ["Ana", "Ben"]; [] if a name is not letters (up to 12)
    # or comes twice.
    [] => out
    0 => i
    while i <= len(s):
        i => j
        while j < len(s) and s[j] != ",":
            j + 1 => j
        trim(s[i:j]) => name
        if not is_name(name):
            return []
        for other in out:
            if other == name:
                return []
        out + [name] => out
        j + 1 => i
    return out

def attends(meeting, name):
    for p in meeting[3]:
        if p == name:
            return True
    return False

def spans(meetings):
    [0] * len(meetings) => xs
    for i in [0:len(meetings) - 1]:
        [meetings[i][1], meetings[i][2]] => xs[i]
    return xs

def in_order(meetings):
    # Indices of the meetings by start time.
    [0] * len(meetings) => xs
    for i in [0:len(meetings) - 1]:
        [meetings[i][1], meetings[i][2], i] => xs[i]
    intervals.by_start(xs) => xs
    [0] * len(xs) => out
    for i in [0:len(xs) - 1]:
        xs[i][2] => out[i]
    return out

def show(meetings):
    if len(meetings) == 0:
        "No meetings booked." ^0
        return
    for i in in_order(meetings):
        meetings[i] => m
        ("  " + span([m[1], m[2]]) + "  " + m[0] + " (" + joined(m[3]) + ")") ^0

def ask_times():
    # [start, end] or [] when cancelled.
    while True:
        trim(input("from (like 9:30)> ")) => a
        if a == "":
            return []
        time_of(a) => start
        if start >= 0 and start < 1440:
            while True:
                trim(input("to> ")) => b
                if b == "":
                    return []
                time_of(b) => end
                if end > start:
                    return [start, end]
                if end >= 0:
                    "A meeting has to end after it starts." ^0
                else:
                    "Type a time like 10:15, up to 24:00." ^0
        else:
            "Type a time like 9:30, from 0:00 to 23:59." ^0

def add(meetings):
    while True:
        trim(input("title> ")) => title
        if title == "":
            "Cancelled." ^0
            return meetings
        True => fresh
        for m in meetings:
            if m[0] == title:
                False => fresh
        if len(title) > 30:
            "A title is up to 30 characters." ^0
        elif not fresh:
            ("There is a meeting called " + title + " already.") ^0
        else:
            while True:
                trim(input("people (like Ana, Ben)> ")) => s
                if s == "":
                    "Cancelled." ^0
                    return meetings
                names_of(s) => people
                if len(people) > 0:
                    ask_times() => t
                    if len(t) == 0:
                        "Cancelled." ^0
                        return meetings
                    for p in people:
                        for m in meetings:
                            if attends(m, p) and intervals.overlaps(t, [m[1], m[2]]):
                                (p + " is already in " + m[0] + ", " + span([m[1], m[2]]) + " - " + minutes(intervals.shared(t, [m[1], m[2]])) + " of it would clash.") ^0
                                return meetings
                    intervals.rooms(spans(meetings))[0] => before
                    meetings + [[title, t[0], t[1], people]] => meetings
                    intervals.rooms(spans(meetings))[0] => after
                    ("Booked " + title + ", " + span(t) + ".") ^0
                    if after > before:
                        ("The day now needs " + str(after) + " rooms.") ^0
                    return meetings
                "Type names of letters, up to 12 each, separated by commas, each once." ^0

def remove(meetings):
    trim(input("title> ")) => title
    if title == "":
        "Cancelled." ^0
        return meetings
    [] => out
    for m in meetings:
        if m[0] != title:
            out + [m] => out
    if len(out) == len(meetings):
        ("No meeting is called " + title + ".") ^0
    else:
        ("Removed " + title + ".") ^0
    return out

def busy(meetings):
    while True:
        trim(input("person> ")) => name
        if name == "":
            "Cancelled." ^0
            return
        if is_name(name):
            [] => xs
            for m in meetings:
                if attends(m, name):
                    xs + [[m[1], m[2]]] => xs
            if len(xs) == 0:
                ("Nobody called " + name + " has a meeting.") ^0
                return
            intervals.merged(xs) => ms
            0 => total
            "" => s
            for x in ms:
                if s != "":
                    s + ", " => s
                s + span(x) => s
                total + x[1] - x[0] => total
            (name + " is busy " + s + " - " + length(total) + " in all.") ^0
            return
        "Type a name of letters, up to 12." ^0

def common_free(meetings):
    while True:
        trim(input("people (like Ana, Ben)> ")) => s
        if s == "":
            "Cancelled." ^0
            return
        names_of(s) => people
        if len(people) > 0:
            while True:
                trim(input("at least how many minutes (5 to 480)> ")) => a
                if a == "":
                    "Cancelled." ^0
                    return
                number(a) => least
                if least < 5 or least > 480:
                    "Type a whole number from 5 to 480." ^0
                else:
                    [] => xs
                    for m in meetings:
                        for p in people:
                            if attends(m, p):
                                xs + [[m[1], m[2]]] => xs
                    intervals.free(xs, 540, 1020, least) => gaps
                    if len(gaps) == 0 and len(people) == 1:
                        ("Between 09:00 and 17:00, " + people[0] + " is never free for " + minutes(least) + ".") ^0
                        return
                    if len(gaps) == 0:
                        ("Between 09:00 and 17:00, " + joined(people) + " are never all free for " + minutes(least) + ".") ^0
                        return
                    "" => line
                    for g in gaps:
                        if line != "":
                            line + ", " => line
                        line + span(g) + " (" + length(g[1] - g[0]) + ")" => line
                    if len(people) == 1:
                        ("Between 09:00 and 17:00, " + people[0] + " is free for " + minutes(least) + " or more at " + line + ".") ^0
                    else:
                        ("Between 09:00 and 17:00, " + joined(people) + " are all free for " + minutes(least) + " or more at " + line + ".") ^0
                    return
        else:
            "Type names of letters, up to 12 each, separated by commas, each once." ^0

def rooms(meetings):
    if len(meetings) == 0:
        "No meetings, no rooms." ^0
        return
    spans(meetings) => xs
    intervals.busiest(xs) => b
    intervals.rooms(xs) => r
    ("At most " + str(b[0]) + " meetings run at once, first at " + clock(b[1]) + " - so " + str(r[0]) + " rooms are enough:") ^0
    for k in [1:r[0]]:
        "" => line
        for i in in_order(meetings):
            if r[1][i] == k:
                if line != "":
                    line + ", " => line
                line + span([meetings[i][1], meetings[i][2]]) + " " + meetings[i][0] => line
        ("  room " + str(k) + ": " + line) ^0

[
    ["standup", 540, 555, ["Ana", "Ben", "Chloe"]],
    ["design review", 555, 615, ["Ana", "Ben"]],
    ["one-to-one", 600, 630, ["Chloe", "Dev"]],
    ["lunch talk", 720, 780, ["Ben", "Chloe", "Dev"]],
    ["retro", 780, 840, ["Ana", "Dev"]],
    ["deploy window", 810, 900, ["Ben"]],
    ["interview", 930, 975, ["Ana", "Chloe"]],
] => meetings

"== Meeting planner ==" ^0
"A day of meetings: book them, see who is busy, find shared free time, count the rooms." ^0
True => running
while running:
    "" ^0
    "1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        show(meetings)
    elif choice == "2":
        add(meetings) => meetings
    elif choice == "3":
        busy(meetings)
    elif choice == "4":
        common_free(meetings)
    elif choice == "5":
        rooms(meetings)
    elif choice == "6":
        remove(meetings) => meetings
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import intervals

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 digits(s):
    if len(s) == 0 or len(s) > 2:
        return -1
    n = 0
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n = n * 10 + int(c)
    return n

def number(s):
    if len(s) == 0 or len(s) > 4:
        return -1
    n = 0
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n = n * 10 + int(c)
    return n

def minutes(k):
    if k == 1:
        return "1 minute"
    return str(k) + " minutes"

def clock(m):
    h = 0
    r = m
    while r >= 60:
        r = r - 60
        h = h + 1
    hh = ""
    if h < 10:
        hh = "0"
    mm = ""
    if r < 10:
        mm = "0"
    return hh + str(h) + ":" + mm + str(r)

def span(x):
    return clock(x[0]) + "-" + clock(x[1])

def length(m):
    h = 0
    r = m
    while r >= 60:
        r = r - 60
        h = h + 1
    if h == 0:
        return str(r) + " min"
    if r == 0:
        return str(h) + " h"
    return str(h) + " h " + str(r) + " min"

def time_of(s):
    k = 0
    while k < len(s) and s[k] != ":":
        k = k + 1
    if k == len(s) or len(s) - k != 3:
        return -1
    h = digits(s[0:k])
    m = digits(s[k + 1:len(s)])
    if h < 0 or m < 0 or m > 59 or h > 24 or h == 24 and m > 0:
        return -1
    return h * 60 + m

def joined(names):
    s = ""
    for i in range(0, len(names)):
        if i > 0 and i == len(names) - 1:
            s = s + " and "
        elif i > 0:
            s = s + ", "
        s = s + names[i]
    return s

def is_name(s):
    if len(s) == 0 or len(s) > 12:
        return False
    for c in s:
        if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z"):
            return False
    return True

def names_of(s):
    out = []
    i = 0
    while i <= len(s):
        j = i
        while j < len(s) and s[j] != ",":
            j = j + 1
        name = trim(s[i:j])
        if not is_name(name):
            return []
        for other in out:
            if other == name:
                return []
        out = out + [name]
        i = j + 1
    return out

def attends(meeting, name):
    for p in meeting[3]:
        if p == name:
            return True
    return False

def spans(meetings):
    xs = [0] * len(meetings)
    for i in range(0, len(meetings)):
        xs[i] = [meetings[i][1], meetings[i][2]]
    return xs

def in_order(meetings):
    xs = [0] * len(meetings)
    for i in range(0, len(meetings)):
        xs[i] = [meetings[i][1], meetings[i][2], i]
    xs = intervals.by_start(xs)
    out = [0] * len(xs)
    for i in range(0, len(xs)):
        out[i] = xs[i][2]
    return out

def show(meetings):
    if len(meetings) == 0:
        print("No meetings booked.")
        return
    for i in in_order(meetings):
        m = meetings[i]
        print("  " + span([m[1], m[2]]) + "  " + m[0] + " (" + joined(m[3]) + ")")

def ask_times():
    while True:
        a = trim(input("from (like 9:30)> "))
        if a == "":
            return []
        start = time_of(a)
        if start >= 0 and start < 1440:
            while True:
                b = trim(input("to> "))
                if b == "":
                    return []
                end = time_of(b)
                if end > start:
                    return [start, end]
                if end >= 0:
                    print("A meeting has to end after it starts.")
                else:
                    print("Type a time like 10:15, up to 24:00.")
        else:
            print("Type a time like 9:30, from 0:00 to 23:59.")

def add(meetings):
    while True:
        title = trim(input("title> "))
        if title == "":
            print("Cancelled.")
            return meetings
        fresh = True
        for m in meetings:
            if m[0] == title:
                fresh = False
        if len(title) > 30:
            print("A title is up to 30 characters.")
        elif not fresh:
            print("There is a meeting called " + title + " already.")
        else:
            while True:
                s = trim(input("people (like Ana, Ben)> "))
                if s == "":
                    print("Cancelled.")
                    return meetings
                people = names_of(s)
                if len(people) > 0:
                    t = ask_times()
                    if len(t) == 0:
                        print("Cancelled.")
                        return meetings
                    for p in people:
                        for m in meetings:
                            if attends(m, p) and intervals.overlaps(t, [m[1], m[2]]):
                                print(p + " is already in " + m[0] + ", " + span([m[1], m[2]]) + " - " + minutes(intervals.shared(t, [m[1], m[2]])) + " of it would clash.")
                                return meetings
                    before = intervals.rooms(spans(meetings))[0]
                    meetings = meetings + [[title, t[0], t[1], people]]
                    after = intervals.rooms(spans(meetings))[0]
                    print("Booked " + title + ", " + span(t) + ".")
                    if after > before:
                        print("The day now needs " + str(after) + " rooms.")
                    return meetings
                print("Type names of letters, up to 12 each, separated by commas, each once.")

def remove(meetings):
    title = trim(input("title> "))
    if title == "":
        print("Cancelled.")
        return meetings
    out = []
    for m in meetings:
        if m[0] != title:
            out = out + [m]
    if len(out) == len(meetings):
        print("No meeting is called " + title + ".")
    else:
        print("Removed " + title + ".")
    return out

def busy(meetings):
    while True:
        name = trim(input("person> "))
        if name == "":
            print("Cancelled.")
            return
        if is_name(name):
            xs = []
            for m in meetings:
                if attends(m, name):
                    xs = xs + [[m[1], m[2]]]
            if len(xs) == 0:
                print("Nobody called " + name + " has a meeting.")
                return
            ms = intervals.merged(xs)
            total = 0
            s = ""
            for x in ms:
                if s != "":
                    s = s + ", "
                s = s + span(x)
                total = total + x[1] - x[0]
            print(name + " is busy " + s + " - " + length(total) + " in all.")
            return
        print("Type a name of letters, up to 12.")

def common_free(meetings):
    while True:
        s = trim(input("people (like Ana, Ben)> "))
        if s == "":
            print("Cancelled.")
            return
        people = names_of(s)
        if len(people) > 0:
            while True:
                a = trim(input("at least how many minutes (5 to 480)> "))
                if a == "":
                    print("Cancelled.")
                    return
                least = number(a)
                if least < 5 or least > 480:
                    print("Type a whole number from 5 to 480.")
                else:
                    xs = []
                    for m in meetings:
                        for p in people:
                            if attends(m, p):
                                xs = xs + [[m[1], m[2]]]
                    gaps = intervals.free(xs, 540, 1020, least)
                    if len(gaps) == 0 and len(people) == 1:
                        print("Between 09:00 and 17:00, " + people[0] + " is never free for " + minutes(least) + ".")
                        return
                    if len(gaps) == 0:
                        print("Between 09:00 and 17:00, " + joined(people) + " are never all free for " + minutes(least) + ".")
                        return
                    line = ""
                    for g in gaps:
                        if line != "":
                            line = line + ", "
                        line = line + span(g) + " (" + length(g[1] - g[0]) + ")"
                    if len(people) == 1:
                        print("Between 09:00 and 17:00, " + people[0] + " is free for " + minutes(least) + " or more at " + line + ".")
                    else:
                        print("Between 09:00 and 17:00, " + joined(people) + " are all free for " + minutes(least) + " or more at " + line + ".")
                    return
        else:
            print("Type names of letters, up to 12 each, separated by commas, each once.")

def rooms(meetings):
    if len(meetings) == 0:
        print("No meetings, no rooms.")
        return
    xs = spans(meetings)
    b = intervals.busiest(xs)
    r = intervals.rooms(xs)
    print("At most " + str(b[0]) + " meetings run at once, first at " + clock(b[1]) + " - so " + str(r[0]) + " rooms are enough:")
    for k in range(1, r[0]+1):
        line = ""
        for i in in_order(meetings):
            if r[1][i] == k:
                if line != "":
                    line = line + ", "
                line = line + span([meetings[i][1], meetings[i][2]]) + " " + meetings[i][0]
        print("  room " + str(k) + ": " + line)

meetings = [["standup", 540, 555, ["Ana", "Ben", "Chloe"]], ["design review", 555, 615, ["Ana", "Ben"]], ["one-to-one", 600, 630, ["Chloe", "Dev"]], ["lunch talk", 720, 780, ["Ben", "Chloe", "Dev"]], ["retro", 780, 840, ["Ana", "Dev"]], ["deploy window", 810, 900, ["Ben"]], ["interview", 930, 975, ["Ana", "Chloe"]]]
print("== Meeting planner ==")
print("A day of meetings: book them, see who is busy, find shared free time, count the rooms.")
running = True
while running:
    print("")
    print("1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        show(meetings)
    elif choice == "2":
        meetings = add(meetings)
    elif choice == "3":
        busy(meetings)
    elif choice == "4":
        common_free(meetings)
    elif choice == "5":
        rooms(meetings)
    elif choice == "6":
        meetings = remove(meetings)
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")
```

### intervals.eml

```eml
# P070 meeting planner - times as minutes from midnight, every interval
# half-open: [start, end) holds start but not end, so a meeting that ends
# at 10:00 and one that starts at 10:00 do not overlap.

def overlaps(a, b):
    # The corpus case interval-overlap-detector: [a0, a1) and [b0, b1)
    # overlap exactly when a0 < b1 and b0 < a1. With <= instead, meetings
    # back to back would clash; checking one side only would call
    # 09:00-10:00 and 11:00-12:00 a clash.
    return a[0] < b[1] and b[0] < a[1]

def shared(a, b):
    # How many minutes two intervals have in common (0 if none).
    a[0] => lo
    if b[0] > lo:
        b[0] => lo
    a[1] => hi
    if b[1] < hi:
        b[1] => hi
    if hi > lo:
        return hi - lo
    return 0

def by_start(xs):
    # A copy sorted by start, then end - insertion sort, which keeps equal
    # intervals in the order they came.
    [0] * len(xs) => out
    for i in [0:len(xs) - 1]:
        xs[i] => out[i]
    for i in [1:len(out) - 1]:
        out[i] => x
        i - 1 => j
        while j >= 0 and (out[j][0] > x[0] or (out[j][0] == x[0] and out[j][1] > x[1])):
            out[j] => out[j + 1]
            j - 1 => j
        x => out[j + 1]
    return out

def merged(xs):
    # The busy time the intervals cover, as the fewest intervals: sorted by
    # start, each one either joins the last (it starts before or just as
    # the last ends) or begins a new one.
    by_start(xs) => s
    [] => out
    for x in s:
        if len(out) > 0 and x[0] <= out[len(out) - 1][1]:
            out[len(out) - 1] => last
            if x[1] > last[1]:
                [last[0], x[1]] => out[len(out) - 1]
        else:
            out + [[x[0], x[1]]] => out
    return out

def free(busy, lo, hi, least):
    # The gaps of at least `least` minutes between lo and hi that no
    # interval in busy covers.
    merged(busy) => m
    [] => out
    lo => t
    for x in m:
        if x[1] > lo and x[0] < hi:
            if x[0] - t >= least:
                out + [[t, x[0]]] => out
            if x[1] > t:
                x[1] => t
    if hi - t >= least:
        out + [[t, hi]] => out
    return out

def rooms(xs):
    # The fewest rooms that hold every meeting, and one way to do it.
    # Meetings are taken by start time; each goes into the room that
    # emptied first, if that room is free by then, or into a new room.
    # That never uses more rooms than the most meetings running at one
    # moment, and that many are needed - so it is the fewest.
    # Returns [room count, [room number for each meeting in xs]].
    [0] * len(xs) => order
    for i in [0:len(xs) - 1]:
        [xs[i][0], xs[i][1], i] => order[i]
    by_start(order) => order
    [] => ends
    [0] * len(xs) => room
    for x in order:
        -1 => best
        for r in [0:len(ends) - 1]:
            if ends[r] <= x[0] and (best == -1 or ends[r] < ends[best]):
                r => best
        if best == -1:
            ends + [x[1]] => ends
            len(ends) - 1 => best
        else:
            x[1] => ends[best]
        best + 1 => room[x[2]]
    return [len(ends), room]

def busiest(xs):
    # The most meetings running at one moment, and the first moment it
    # happens: it is always some meeting's start time.
    0 => most
    0 => when
    for x in xs:
        0 => k
        for y in xs:
            if y[0] <= x[0] and x[0] < y[1]:
                k + 1 => k
        if k > most or (k == most and x[0] < when):
            k => most
            x[0] => when
    return [most, when]
```

Python projection of intervals.eml:

```python
def overlaps(a, b):
    return a[0] < b[1] and b[0] < a[1]

def shared(a, b):
    lo = a[0]
    if b[0] > lo:
        lo = b[0]
    hi = a[1]
    if b[1] < hi:
        hi = b[1]
    if hi > lo:
        return hi - lo
    return 0

def by_start(xs):
    out = [0] * len(xs)
    for i in range(0, len(xs)):
        out[i] = xs[i]
    for i in range(1, len(out)):
        x = out[i]
        j = i - 1
        while j >= 0 and (out[j][0] > x[0] or out[j][0] == x[0] and out[j][1] > x[1]):
            out[j + 1] = out[j]
            j = j - 1
        out[j + 1] = x
    return out

def merged(xs):
    s = by_start(xs)
    out = []
    for x in s:
        if len(out) > 0 and x[0] <= out[len(out) - 1][1]:
            last = out[len(out) - 1]
            if x[1] > last[1]:
                out[len(out) - 1] = [last[0], x[1]]
        else:
            out = out + [[x[0], x[1]]]
    return out

def free(busy, lo, hi, least):
    m = merged(busy)
    out = []
    t = lo
    for x in m:
        if x[1] > lo and x[0] < hi:
            if x[0] - t >= least:
                out = out + [[t, x[0]]]
            if x[1] > t:
                t = x[1]
    if hi - t >= least:
        out = out + [[t, hi]]
    return out

def rooms(xs):
    order = [0] * len(xs)
    for i in range(0, len(xs)):
        order[i] = [xs[i][0], xs[i][1], i]
    order = by_start(order)
    ends = []
    room = [0] * len(xs)
    for x in order:
        best = -1
        for r in range(0, len(ends)):
            if ends[r] <= x[0] and (best == -1 or ends[r] < ends[best]):
                best = r
        if best == -1:
            ends = ends + [x[1]]
            best = len(ends) - 1
        else:
            ends[best] = x[1]
        room[x[2]] = best + 1
    return [len(ends), room]

def busiest(xs):
    most = 0
    when = 0
    for x in xs:
        k = 0
        for y in xs:
            if y[0] <= x[0] and x[0] < y[1]:
                k = k + 1
        if k > most or k == most and x[0] < when:
            most = k
            when = x[0]
    return [most, when]
```

## README

# P070 - Meeting planner

A day of meetings, each with its people. Booking refuses a time when one
of them is already in a meeting. There is each person's busy time merged,
the free time a group shares between 09:00 and 17:00, and the fewest rooms
the day needs, with one way to assign them. It starts with seven meetings
for Ana, Ben, Chloe and Dev.

- `main.eml` - the menu, reading titles, names and times, and the reports
- `intervals.eml` - overlap, shared minutes, sorting and merging
  intervals, free time, and the room count with an assignment

How each part works:

- Every time is minutes from midnight, and every meeting is half-open:
  09:15-10:15 holds 09:15 but not 10:15. As in the corpus case
  `interval-overlap-detector`, [a0, a1) and [b0, b1) overlap exactly when
  a0 < b1 and b0 < a1. So a meeting that starts as another ends is no
  clash. With <= instead, every back-to-back booking would be refused.
- Booking checks each person against each of their meetings, and a
  refusal says how many minutes would clash.
- Busy time: a person's meetings sorted by start, each joining the last
  piece if it starts before or just as that piece ends.
- Shared free time: the meetings of everyone named, merged, and the gaps
  of at least the asked length between 09:00 and 17:00.
- Rooms: meetings taken by start time, each into the room that emptied
  first if it is free by then, otherwise into a new room. This never uses
  more rooms than the most meetings running at one moment - and that many
  are needed - so it is the fewest. That moment is always some meeting's
  start, so the program checks only those.

What is checked: menu choices 1 to 7; a title of 1 to 30 characters, not
already used; names of letters, up to 12 each, separated by commas, each
once; times as H:MM or HH:MM from 0:00, with 24:00 allowed as an end, and
the end after the start; a length of 5 to 480 minutes. An empty answer
cancels.

Sessions: `sessions/basic.in` gives:
- the seven meetings, and Ana's busy time: 3 h in three pieces;
- the free time Ana and Ben share (30 minutes or more), and Chloe and Dev
  (60 or more);
- "planning" booked for Ana and Ben at 10:15, just as their design review
  ends;
- "all-hands" at 09:30-10:20, which makes it 3 rooms, the rooms shown,
  then removed again: 2 rooms.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- a title already used and one too long; the names "Ana,, Ben", "Ana1"
  and "Ana, Ana"; the times 25:00 and 9:3, an end before the start and
  24:01, then a cancel;
- the name "Ana B", and Zoe, who has no meetings;
- lengths 4 and abc; 480 minutes for Ana and 240 for Ben, Chloe and Dev,
  which no one has;
- removing a meeting that does not exist;
- a booking for Ben refused by a 1-minute clash, and one for Gus ending
  at 24:00.

Built on the verified corpus case `interval-overlap-detector` (half-open
intervals and the a0 < b1 and b0 < a1 test, back-to-back included).
