{
  "id": "P070",
  "slug": "meeting-planner",
  "key": "P070-meeting-planner",
  "title": "Meeting planner",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P070 - Meeting planner\n\nA day of meetings, each with its people. Booking refuses a time when one\nof them is already in a meeting. There is each person's busy time merged,\nthe free time a group shares between 09:00 and 17:00, and the fewest rooms\nthe day needs, with one way to assign them. It starts with seven meetings\nfor Ana, Ben, Chloe and Dev.\n\n- `main.eml` - the menu, reading titles, names and times, and the reports\n- `intervals.eml` - overlap, shared minutes, sorting and merging\n  intervals, free time, and the room count with an assignment\n\nHow each part works:\n\n- Every time is minutes from midnight, and every meeting is half-open:\n  09:15-10:15 holds 09:15 but not 10:15. As in the corpus case\n  `interval-overlap-detector`, [a0, a1) and [b0, b1) overlap exactly when\n  a0 < b1 and b0 < a1. So a meeting that starts as another ends is no\n  clash. With <= instead, every back-to-back booking would be refused.\n- Booking checks each person against each of their meetings, and a\n  refusal says how many minutes would clash.\n- Busy time: a person's meetings sorted by start, each joining the last\n  piece if it starts before or just as that piece ends.\n- Shared free time: the meetings of everyone named, merged, and the gaps\n  of at least the asked length between 09:00 and 17:00.\n- Rooms: meetings taken by start time, each into the room that emptied\n  first if it is free by then, otherwise into a new room. This never uses\n  more rooms than the most meetings running at one moment - and that many\n  are needed - so it is the fewest. That moment is always some meeting's\n  start, so the program checks only those.\n\nWhat is checked: menu choices 1 to 7; a title of 1 to 30 characters, not\nalready used; names of letters, up to 12 each, separated by commas, each\nonce; times as H:MM or HH:MM from 0:00, with 24:00 allowed as an end, and\nthe end after the start; a length of 5 to 480 minutes. An empty answer\ncancels.\n\nSessions: `sessions/basic.in` gives:\n- the seven meetings, and Ana's busy time: 3 h in three pieces;\n- the free time Ana and Ben share (30 minutes or more), and Chloe and Dev\n  (60 or more);\n- \"planning\" booked for Ana and Ben at 10:15, just as their design review\n  ends;\n- \"all-hands\" at 09:30-10:20, which makes it 3 rooms, the rooms shown,\n  then removed again: 2 rooms.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a title already used and one too long; the names \"Ana,, Ben\", \"Ana1\"\n  and \"Ana, Ana\"; the times 25:00 and 9:3, an end before the start and\n  24:01, then a cancel;\n- the name \"Ana B\", and Zoe, who has no meetings;\n- lengths 4 and abc; 480 minutes for Ana and 240 for Ben, Chloe and Dev,\n  which no one has;\n- removing a meeting that does not exist;\n- a booking for Ben refused by a 1-minute clash, and one for Gus ending\n  at 24:00.\n\nBuilt on the verified corpus case `interval-overlap-detector` (half-open\nintervals and the a0 < b1 and b0 < a1 test, back-to-back included).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P070 meeting planner: a day of meetings, each with its people. Booking\n# refuses a time when one of them is already in a meeting; there is each\n# person's busy time merged, the free time a group shares, and the fewest\n# rooms the day needs.\nimport intervals\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef digits(s):\n    # The value of a string of 1 or 2 digits, or -1.\n    if len(s) == 0 or len(s) > 2:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef number(s):\n    # A whole number of up to 4 digits, or -1.\n    if len(s) == 0 or len(s) > 4:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef minutes(k):\n    if k == 1:\n        return \"1 minute\"\n    return str(k) + \" minutes\"\n\ndef clock(m):\n    # Minutes from midnight as HH:MM.\n    0 => h\n    m => r\n    while r >= 60:\n        r - 60 => r\n        h + 1 => h\n    \"\" => hh\n    if h < 10:\n        \"0\" => hh\n    \"\" => mm\n    if r < 10:\n        \"0\" => mm\n    return hh + str(h) + \":\" + mm + str(r)\n\ndef span(x):\n    return clock(x[0]) + \"-\" + clock(x[1])\n\ndef length(m):\n    # 135 -> \"2 h 15 min\"\n    0 => h\n    m => r\n    while r >= 60:\n        r - 60 => r\n        h + 1 => h\n    if h == 0:\n        return str(r) + \" min\"\n    if r == 0:\n        return str(h) + \" h\"\n    return str(h) + \" h \" + str(r) + \" min\"\n\ndef time_of(s):\n    # \"9:30\" or \"09:30\" as minutes from midnight; 24:00 is the end of the\n    # day. -1 if it is not a time.\n    0 => k\n    while k < len(s) and s[k] != \":\":\n        k + 1 => k\n    if k == len(s) or len(s) - k != 3:\n        return -1\n    digits(s[0:k]) => h\n    digits(s[k + 1:len(s)]) => m\n    if h < 0 or m < 0 or m > 59 or h > 24 or (h == 24 and m > 0):\n        return -1\n    return h * 60 + m\n\ndef joined(names):\n    # [\"Ana\", \"Ben\", \"Dev\"] -> \"Ana, Ben and Dev\"\n    \"\" => s\n    for i in [0:len(names) - 1]:\n        if i > 0 and i == len(names) - 1:\n            s + \" and \" => s\n        elif i > 0:\n            s + \", \" => s\n        s + names[i] => s\n    return s\n\ndef is_name(s):\n    if len(s) == 0 or len(s) > 12:\n        return False\n    for c in s:\n        if not ((c >= \"a\" and c <= \"z\") or (c >= \"A\" and c <= \"Z\")):\n            return False\n    return True\n\ndef names_of(s):\n    # \"Ana, Ben\" -> [\"Ana\", \"Ben\"]; [] if a name is not letters (up to 12)\n    # or comes twice.\n    [] => out\n    0 => i\n    while i <= len(s):\n        i => j\n        while j < len(s) and s[j] != \",\":\n            j + 1 => j\n        trim(s[i:j]) => name\n        if not is_name(name):\n            return []\n        for other in out:\n            if other == name:\n                return []\n        out + [name] => out\n        j + 1 => i\n    return out\n\ndef attends(meeting, name):\n    for p in meeting[3]:\n        if p == name:\n            return True\n    return False\n\ndef spans(meetings):\n    [0] * len(meetings) => xs\n    for i in [0:len(meetings) - 1]:\n        [meetings[i][1], meetings[i][2]] => xs[i]\n    return xs\n\ndef in_order(meetings):\n    # Indices of the meetings by start time.\n    [0] * len(meetings) => xs\n    for i in [0:len(meetings) - 1]:\n        [meetings[i][1], meetings[i][2], i] => xs[i]\n    intervals.by_start(xs) => xs\n    [0] * len(xs) => out\n    for i in [0:len(xs) - 1]:\n        xs[i][2] => out[i]\n    return out\n\ndef show(meetings):\n    if len(meetings) == 0:\n        \"No meetings booked.\" ^0\n        return\n    for i in in_order(meetings):\n        meetings[i] => m\n        (\"  \" + span([m[1], m[2]]) + \"  \" + m[0] + \" (\" + joined(m[3]) + \")\") ^0\n\ndef ask_times():\n    # [start, end] or [] when cancelled.\n    while True:\n        trim(input(\"from (like 9:30)> \")) => a\n        if a == \"\":\n            return []\n        time_of(a) => start\n        if start >= 0 and start < 1440:\n            while True:\n                trim(input(\"to> \")) => b\n                if b == \"\":\n                    return []\n                time_of(b) => end\n                if end > start:\n                    return [start, end]\n                if end >= 0:\n                    \"A meeting has to end after it starts.\" ^0\n                else:\n                    \"Type a time like 10:15, up to 24:00.\" ^0\n        else:\n            \"Type a time like 9:30, from 0:00 to 23:59.\" ^0\n\ndef add(meetings):\n    while True:\n        trim(input(\"title> \")) => title\n        if title == \"\":\n            \"Cancelled.\" ^0\n            return meetings\n        True => fresh\n        for m in meetings:\n            if m[0] == title:\n                False => fresh\n        if len(title) > 30:\n            \"A title is up to 30 characters.\" ^0\n        elif not fresh:\n            (\"There is a meeting called \" + title + \" already.\") ^0\n        else:\n            while True:\n                trim(input(\"people (like Ana, Ben)> \")) => s\n                if s == \"\":\n                    \"Cancelled.\" ^0\n                    return meetings\n                names_of(s) => people\n                if len(people) > 0:\n                    ask_times() => t\n                    if len(t) == 0:\n                        \"Cancelled.\" ^0\n                        return meetings\n                    for p in people:\n                        for m in meetings:\n                            if attends(m, p) and intervals.overlaps(t, [m[1], m[2]]):\n                                (p + \" is already in \" + m[0] + \", \" + span([m[1], m[2]]) + \" - \" + minutes(intervals.shared(t, [m[1], m[2]])) + \" of it would clash.\") ^0\n                                return meetings\n                    intervals.rooms(spans(meetings))[0] => before\n                    meetings + [[title, t[0], t[1], people]] => meetings\n                    intervals.rooms(spans(meetings))[0] => after\n                    (\"Booked \" + title + \", \" + span(t) + \".\") ^0\n                    if after > before:\n                        (\"The day now needs \" + str(after) + \" rooms.\") ^0\n                    return meetings\n                \"Type names of letters, up to 12 each, separated by commas, each once.\" ^0\n\ndef remove(meetings):\n    trim(input(\"title> \")) => title\n    if title == \"\":\n        \"Cancelled.\" ^0\n        return meetings\n    [] => out\n    for m in meetings:\n        if m[0] != title:\n            out + [m] => out\n    if len(out) == len(meetings):\n        (\"No meeting is called \" + title + \".\") ^0\n    else:\n        (\"Removed \" + title + \".\") ^0\n    return out\n\ndef busy(meetings):\n    while True:\n        trim(input(\"person> \")) => name\n        if name == \"\":\n            \"Cancelled.\" ^0\n            return\n        if is_name(name):\n            [] => xs\n            for m in meetings:\n                if attends(m, name):\n                    xs + [[m[1], m[2]]] => xs\n            if len(xs) == 0:\n                (\"Nobody called \" + name + \" has a meeting.\") ^0\n                return\n            intervals.merged(xs) => ms\n            0 => total\n            \"\" => s\n            for x in ms:\n                if s != \"\":\n                    s + \", \" => s\n                s + span(x) => s\n                total + x[1] - x[0] => total\n            (name + \" is busy \" + s + \" - \" + length(total) + \" in all.\") ^0\n            return\n        \"Type a name of letters, up to 12.\" ^0\n\ndef common_free(meetings):\n    while True:\n        trim(input(\"people (like Ana, Ben)> \")) => s\n        if s == \"\":\n            \"Cancelled.\" ^0\n            return\n        names_of(s) => people\n        if len(people) > 0:\n            while True:\n                trim(input(\"at least how many minutes (5 to 480)> \")) => a\n                if a == \"\":\n                    \"Cancelled.\" ^0\n                    return\n                number(a) => least\n                if least < 5 or least > 480:\n                    \"Type a whole number from 5 to 480.\" ^0\n                else:\n                    [] => xs\n                    for m in meetings:\n                        for p in people:\n                            if attends(m, p):\n                                xs + [[m[1], m[2]]] => xs\n                    intervals.free(xs, 540, 1020, least) => gaps\n                    if len(gaps) == 0 and len(people) == 1:\n                        (\"Between 09:00 and 17:00, \" + people[0] + \" is never free for \" + minutes(least) + \".\") ^0\n                        return\n                    if len(gaps) == 0:\n                        (\"Between 09:00 and 17:00, \" + joined(people) + \" are never all free for \" + minutes(least) + \".\") ^0\n                        return\n                    \"\" => line\n                    for g in gaps:\n                        if line != \"\":\n                            line + \", \" => line\n                        line + span(g) + \" (\" + length(g[1] - g[0]) + \")\" => line\n                    if len(people) == 1:\n                        (\"Between 09:00 and 17:00, \" + people[0] + \" is free for \" + minutes(least) + \" or more at \" + line + \".\") ^0\n                    else:\n                        (\"Between 09:00 and 17:00, \" + joined(people) + \" are all free for \" + minutes(least) + \" or more at \" + line + \".\") ^0\n                    return\n        else:\n            \"Type names of letters, up to 12 each, separated by commas, each once.\" ^0\n\ndef rooms(meetings):\n    if len(meetings) == 0:\n        \"No meetings, no rooms.\" ^0\n        return\n    spans(meetings) => xs\n    intervals.busiest(xs) => b\n    intervals.rooms(xs) => r\n    (\"At most \" + str(b[0]) + \" meetings run at once, first at \" + clock(b[1]) + \" - so \" + str(r[0]) + \" rooms are enough:\") ^0\n    for k in [1:r[0]]:\n        \"\" => line\n        for i in in_order(meetings):\n            if r[1][i] == k:\n                if line != \"\":\n                    line + \", \" => line\n                line + span([meetings[i][1], meetings[i][2]]) + \" \" + meetings[i][0] => line\n        (\"  room \" + str(k) + \": \" + line) ^0\n\n[\n    [\"standup\", 540, 555, [\"Ana\", \"Ben\", \"Chloe\"]],\n    [\"design review\", 555, 615, [\"Ana\", \"Ben\"]],\n    [\"one-to-one\", 600, 630, [\"Chloe\", \"Dev\"]],\n    [\"lunch talk\", 720, 780, [\"Ben\", \"Chloe\", \"Dev\"]],\n    [\"retro\", 780, 840, [\"Ana\", \"Dev\"]],\n    [\"deploy window\", 810, 900, [\"Ben\"]],\n    [\"interview\", 930, 975, [\"Ana\", \"Chloe\"]],\n] => meetings\n\n\"== Meeting planner ==\" ^0\n\"A day of meetings: book them, see who is busy, find shared free time, count the rooms.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(meetings)\n    elif choice == \"2\":\n        add(meetings) => meetings\n    elif choice == \"3\":\n        busy(meetings)\n    elif choice == \"4\":\n        common_free(meetings)\n    elif choice == \"5\":\n        rooms(meetings)\n    elif choice == \"6\":\n        remove(meetings) => meetings\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import intervals\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef digits(s):\n    if len(s) == 0 or len(s) > 2:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef number(s):\n    if len(s) == 0 or len(s) > 4:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef minutes(k):\n    if k == 1:\n        return \"1 minute\"\n    return str(k) + \" minutes\"\n\ndef clock(m):\n    h = 0\n    r = m\n    while r >= 60:\n        r = r - 60\n        h = h + 1\n    hh = \"\"\n    if h < 10:\n        hh = \"0\"\n    mm = \"\"\n    if r < 10:\n        mm = \"0\"\n    return hh + str(h) + \":\" + mm + str(r)\n\ndef span(x):\n    return clock(x[0]) + \"-\" + clock(x[1])\n\ndef length(m):\n    h = 0\n    r = m\n    while r >= 60:\n        r = r - 60\n        h = h + 1\n    if h == 0:\n        return str(r) + \" min\"\n    if r == 0:\n        return str(h) + \" h\"\n    return str(h) + \" h \" + str(r) + \" min\"\n\ndef time_of(s):\n    k = 0\n    while k < len(s) and s[k] != \":\":\n        k = k + 1\n    if k == len(s) or len(s) - k != 3:\n        return -1\n    h = digits(s[0:k])\n    m = digits(s[k + 1:len(s)])\n    if h < 0 or m < 0 or m > 59 or h > 24 or h == 24 and m > 0:\n        return -1\n    return h * 60 + m\n\ndef joined(names):\n    s = \"\"\n    for i in range(0, len(names)):\n        if i > 0 and i == len(names) - 1:\n            s = s + \" and \"\n        elif i > 0:\n            s = s + \", \"\n        s = s + names[i]\n    return s\n\ndef is_name(s):\n    if len(s) == 0 or len(s) > 12:\n        return False\n    for c in s:\n        if not (c >= \"a\" and c <= \"z\" or c >= \"A\" and c <= \"Z\"):\n            return False\n    return True\n\ndef names_of(s):\n    out = []\n    i = 0\n    while i <= len(s):\n        j = i\n        while j < len(s) and s[j] != \",\":\n            j = j + 1\n        name = trim(s[i:j])\n        if not is_name(name):\n            return []\n        for other in out:\n            if other == name:\n                return []\n        out = out + [name]\n        i = j + 1\n    return out\n\ndef attends(meeting, name):\n    for p in meeting[3]:\n        if p == name:\n            return True\n    return False\n\ndef spans(meetings):\n    xs = [0] * len(meetings)\n    for i in range(0, len(meetings)):\n        xs[i] = [meetings[i][1], meetings[i][2]]\n    return xs\n\ndef in_order(meetings):\n    xs = [0] * len(meetings)\n    for i in range(0, len(meetings)):\n        xs[i] = [meetings[i][1], meetings[i][2], i]\n    xs = intervals.by_start(xs)\n    out = [0] * len(xs)\n    for i in range(0, len(xs)):\n        out[i] = xs[i][2]\n    return out\n\ndef show(meetings):\n    if len(meetings) == 0:\n        print(\"No meetings booked.\")\n        return\n    for i in in_order(meetings):\n        m = meetings[i]\n        print(\"  \" + span([m[1], m[2]]) + \"  \" + m[0] + \" (\" + joined(m[3]) + \")\")\n\ndef ask_times():\n    while True:\n        a = trim(input(\"from (like 9:30)> \"))\n        if a == \"\":\n            return []\n        start = time_of(a)\n        if start >= 0 and start < 1440:\n            while True:\n                b = trim(input(\"to> \"))\n                if b == \"\":\n                    return []\n                end = time_of(b)\n                if end > start:\n                    return [start, end]\n                if end >= 0:\n                    print(\"A meeting has to end after it starts.\")\n                else:\n                    print(\"Type a time like 10:15, up to 24:00.\")\n        else:\n            print(\"Type a time like 9:30, from 0:00 to 23:59.\")\n\ndef add(meetings):\n    while True:\n        title = trim(input(\"title> \"))\n        if title == \"\":\n            print(\"Cancelled.\")\n            return meetings\n        fresh = True\n        for m in meetings:\n            if m[0] == title:\n                fresh = False\n        if len(title) > 30:\n            print(\"A title is up to 30 characters.\")\n        elif not fresh:\n            print(\"There is a meeting called \" + title + \" already.\")\n        else:\n            while True:\n                s = trim(input(\"people (like Ana, Ben)> \"))\n                if s == \"\":\n                    print(\"Cancelled.\")\n                    return meetings\n                people = names_of(s)\n                if len(people) > 0:\n                    t = ask_times()\n                    if len(t) == 0:\n                        print(\"Cancelled.\")\n                        return meetings\n                    for p in people:\n                        for m in meetings:\n                            if attends(m, p) and intervals.overlaps(t, [m[1], m[2]]):\n                                print(p + \" is already in \" + m[0] + \", \" + span([m[1], m[2]]) + \" - \" + minutes(intervals.shared(t, [m[1], m[2]])) + \" of it would clash.\")\n                                return meetings\n                    before = intervals.rooms(spans(meetings))[0]\n                    meetings = meetings + [[title, t[0], t[1], people]]\n                    after = intervals.rooms(spans(meetings))[0]\n                    print(\"Booked \" + title + \", \" + span(t) + \".\")\n                    if after > before:\n                        print(\"The day now needs \" + str(after) + \" rooms.\")\n                    return meetings\n                print(\"Type names of letters, up to 12 each, separated by commas, each once.\")\n\ndef remove(meetings):\n    title = trim(input(\"title> \"))\n    if title == \"\":\n        print(\"Cancelled.\")\n        return meetings\n    out = []\n    for m in meetings:\n        if m[0] != title:\n            out = out + [m]\n    if len(out) == len(meetings):\n        print(\"No meeting is called \" + title + \".\")\n    else:\n        print(\"Removed \" + title + \".\")\n    return out\n\ndef busy(meetings):\n    while True:\n        name = trim(input(\"person> \"))\n        if name == \"\":\n            print(\"Cancelled.\")\n            return\n        if is_name(name):\n            xs = []\n            for m in meetings:\n                if attends(m, name):\n                    xs = xs + [[m[1], m[2]]]\n            if len(xs) == 0:\n                print(\"Nobody called \" + name + \" has a meeting.\")\n                return\n            ms = intervals.merged(xs)\n            total = 0\n            s = \"\"\n            for x in ms:\n                if s != \"\":\n                    s = s + \", \"\n                s = s + span(x)\n                total = total + x[1] - x[0]\n            print(name + \" is busy \" + s + \" - \" + length(total) + \" in all.\")\n            return\n        print(\"Type a name of letters, up to 12.\")\n\ndef common_free(meetings):\n    while True:\n        s = trim(input(\"people (like Ana, Ben)> \"))\n        if s == \"\":\n            print(\"Cancelled.\")\n            return\n        people = names_of(s)\n        if len(people) > 0:\n            while True:\n                a = trim(input(\"at least how many minutes (5 to 480)> \"))\n                if a == \"\":\n                    print(\"Cancelled.\")\n                    return\n                least = number(a)\n                if least < 5 or least > 480:\n                    print(\"Type a whole number from 5 to 480.\")\n                else:\n                    xs = []\n                    for m in meetings:\n                        for p in people:\n                            if attends(m, p):\n                                xs = xs + [[m[1], m[2]]]\n                    gaps = intervals.free(xs, 540, 1020, least)\n                    if len(gaps) == 0 and len(people) == 1:\n                        print(\"Between 09:00 and 17:00, \" + people[0] + \" is never free for \" + minutes(least) + \".\")\n                        return\n                    if len(gaps) == 0:\n                        print(\"Between 09:00 and 17:00, \" + joined(people) + \" are never all free for \" + minutes(least) + \".\")\n                        return\n                    line = \"\"\n                    for g in gaps:\n                        if line != \"\":\n                            line = line + \", \"\n                        line = line + span(g) + \" (\" + length(g[1] - g[0]) + \")\"\n                    if len(people) == 1:\n                        print(\"Between 09:00 and 17:00, \" + people[0] + \" is free for \" + minutes(least) + \" or more at \" + line + \".\")\n                    else:\n                        print(\"Between 09:00 and 17:00, \" + joined(people) + \" are all free for \" + minutes(least) + \" or more at \" + line + \".\")\n                    return\n        else:\n            print(\"Type names of letters, up to 12 each, separated by commas, each once.\")\n\ndef rooms(meetings):\n    if len(meetings) == 0:\n        print(\"No meetings, no rooms.\")\n        return\n    xs = spans(meetings)\n    b = intervals.busiest(xs)\n    r = intervals.rooms(xs)\n    print(\"At most \" + str(b[0]) + \" meetings run at once, first at \" + clock(b[1]) + \" - so \" + str(r[0]) + \" rooms are enough:\")\n    for k in range(1, r[0]+1):\n        line = \"\"\n        for i in in_order(meetings):\n            if r[1][i] == k:\n                if line != \"\":\n                    line = line + \", \"\n                line = line + span([meetings[i][1], meetings[i][2]]) + \" \" + meetings[i][0]\n        print(\"  room \" + str(k) + \": \" + line)\n\nmeetings = [[\"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\"]]]\nprint(\"== Meeting planner ==\")\nprint(\"A day of meetings: book them, see who is busy, find shared free time, count the rooms.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        show(meetings)\n    elif choice == \"2\":\n        meetings = add(meetings)\n    elif choice == \"3\":\n        busy(meetings)\n    elif choice == \"4\":\n        common_free(meetings)\n    elif choice == \"5\":\n        rooms(meetings)\n    elif choice == \"6\":\n        meetings = remove(meetings)\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "intervals.eml",
      "eml": "# P070 meeting planner - times as minutes from midnight, every interval\n# half-open: [start, end) holds start but not end, so a meeting that ends\n# at 10:00 and one that starts at 10:00 do not overlap.\n\ndef overlaps(a, b):\n    # The corpus case interval-overlap-detector: [a0, a1) and [b0, b1)\n    # overlap exactly when a0 < b1 and b0 < a1. With <= instead, meetings\n    # back to back would clash; checking one side only would call\n    # 09:00-10:00 and 11:00-12:00 a clash.\n    return a[0] < b[1] and b[0] < a[1]\n\ndef shared(a, b):\n    # How many minutes two intervals have in common (0 if none).\n    a[0] => lo\n    if b[0] > lo:\n        b[0] => lo\n    a[1] => hi\n    if b[1] < hi:\n        b[1] => hi\n    if hi > lo:\n        return hi - lo\n    return 0\n\ndef by_start(xs):\n    # A copy sorted by start, then end - insertion sort, which keeps equal\n    # intervals in the order they came.\n    [0] * len(xs) => out\n    for i in [0:len(xs) - 1]:\n        xs[i] => out[i]\n    for i in [1:len(out) - 1]:\n        out[i] => x\n        i - 1 => j\n        while j >= 0 and (out[j][0] > x[0] or (out[j][0] == x[0] and out[j][1] > x[1])):\n            out[j] => out[j + 1]\n            j - 1 => j\n        x => out[j + 1]\n    return out\n\ndef merged(xs):\n    # The busy time the intervals cover, as the fewest intervals: sorted by\n    # start, each one either joins the last (it starts before or just as\n    # the last ends) or begins a new one.\n    by_start(xs) => s\n    [] => out\n    for x in s:\n        if len(out) > 0 and x[0] <= out[len(out) - 1][1]:\n            out[len(out) - 1] => last\n            if x[1] > last[1]:\n                [last[0], x[1]] => out[len(out) - 1]\n        else:\n            out + [[x[0], x[1]]] => out\n    return out\n\ndef free(busy, lo, hi, least):\n    # The gaps of at least `least` minutes between lo and hi that no\n    # interval in busy covers.\n    merged(busy) => m\n    [] => out\n    lo => t\n    for x in m:\n        if x[1] > lo and x[0] < hi:\n            if x[0] - t >= least:\n                out + [[t, x[0]]] => out\n            if x[1] > t:\n                x[1] => t\n    if hi - t >= least:\n        out + [[t, hi]] => out\n    return out\n\ndef rooms(xs):\n    # The fewest rooms that hold every meeting, and one way to do it.\n    # Meetings are taken by start time; each goes into the room that\n    # emptied first, if that room is free by then, or into a new room.\n    # That never uses more rooms than the most meetings running at one\n    # moment, and that many are needed - so it is the fewest.\n    # Returns [room count, [room number for each meeting in xs]].\n    [0] * len(xs) => order\n    for i in [0:len(xs) - 1]:\n        [xs[i][0], xs[i][1], i] => order[i]\n    by_start(order) => order\n    [] => ends\n    [0] * len(xs) => room\n    for x in order:\n        -1 => best\n        for r in [0:len(ends) - 1]:\n            if ends[r] <= x[0] and (best == -1 or ends[r] < ends[best]):\n                r => best\n        if best == -1:\n            ends + [x[1]] => ends\n            len(ends) - 1 => best\n        else:\n            x[1] => ends[best]\n        best + 1 => room[x[2]]\n    return [len(ends), room]\n\ndef busiest(xs):\n    # The most meetings running at one moment, and the first moment it\n    # happens: it is always some meeting's start time.\n    0 => most\n    0 => when\n    for x in xs:\n        0 => k\n        for y in xs:\n            if y[0] <= x[0] and x[0] < y[1]:\n                k + 1 => k\n        if k > most or (k == most and x[0] < when):\n            k => most\n            x[0] => when\n    return [most, when]\n",
      "python": "def overlaps(a, b):\n    return a[0] < b[1] and b[0] < a[1]\n\ndef shared(a, b):\n    lo = a[0]\n    if b[0] > lo:\n        lo = b[0]\n    hi = a[1]\n    if b[1] < hi:\n        hi = b[1]\n    if hi > lo:\n        return hi - lo\n    return 0\n\ndef by_start(xs):\n    out = [0] * len(xs)\n    for i in range(0, len(xs)):\n        out[i] = xs[i]\n    for i in range(1, len(out)):\n        x = out[i]\n        j = i - 1\n        while j >= 0 and (out[j][0] > x[0] or out[j][0] == x[0] and out[j][1] > x[1]):\n            out[j + 1] = out[j]\n            j = j - 1\n        out[j + 1] = x\n    return out\n\ndef merged(xs):\n    s = by_start(xs)\n    out = []\n    for x in s:\n        if len(out) > 0 and x[0] <= out[len(out) - 1][1]:\n            last = out[len(out) - 1]\n            if x[1] > last[1]:\n                out[len(out) - 1] = [last[0], x[1]]\n        else:\n            out = out + [[x[0], x[1]]]\n    return out\n\ndef free(busy, lo, hi, least):\n    m = merged(busy)\n    out = []\n    t = lo\n    for x in m:\n        if x[1] > lo and x[0] < hi:\n            if x[0] - t >= least:\n                out = out + [[t, x[0]]]\n            if x[1] > t:\n                t = x[1]\n    if hi - t >= least:\n        out = out + [[t, hi]]\n    return out\n\ndef rooms(xs):\n    order = [0] * len(xs)\n    for i in range(0, len(xs)):\n        order[i] = [xs[i][0], xs[i][1], i]\n    order = by_start(order)\n    ends = []\n    room = [0] * len(xs)\n    for x in order:\n        best = -1\n        for r in range(0, len(ends)):\n            if ends[r] <= x[0] and (best == -1 or ends[r] < ends[best]):\n                best = r\n        if best == -1:\n            ends = ends + [x[1]]\n            best = len(ends) - 1\n        else:\n            ends[best] = x[1]\n        room[x[2]] = best + 1\n    return [len(ends), room]\n\ndef busiest(xs):\n    most = 0\n    when = 0\n    for x in xs:\n        k = 0\n        for y in xs:\n            if y[0] <= x[0] and x[0] < y[1]:\n                k = k + 1\n        if k > most or k == most and x[0] < when:\n            most = k\n            when = x[0]\n    return [most, when]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\nstandup\na title that is far too long for it\nreview\nAna,, Ben\nAna1\nAna, Ana\nAna, Ben\n25:00\n9:3\n12:00\n11:00\n24:01\n\n3\nAna B\nZoe\n4\nAna\n4\nabc\n480\n4\nBen, Chloe, Dev\n240\n6\nnothing\n2\nlate\nBen\n14:59\n15:30\n2\nevening\nGus\n23:00\n24:00\n3\nGus\n7\n",
      "screen": "== Meeting planner ==\nA day of meetings: book them, see who is busy, find shared free time, count the rooms.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 2\ntitle> standup\nThere is a meeting called standup already.\ntitle> a title that is far too long for it\nA title is up to 30 characters.\ntitle> review\npeople (like Ana, Ben)> Ana,, Ben\nType names of letters, up to 12 each, separated by commas, each once.\npeople (like Ana, Ben)> Ana1\nType names of letters, up to 12 each, separated by commas, each once.\npeople (like Ana, Ben)> Ana, Ana\nType names of letters, up to 12 each, separated by commas, each once.\npeople (like Ana, Ben)> Ana, Ben\nfrom (like 9:30)> 25:00\nType a time like 9:30, from 0:00 to 23:59.\nfrom (like 9:30)> 9:3\nType a time like 9:30, from 0:00 to 23:59.\nfrom (like 9:30)> 12:00\nto> 11:00\nA meeting has to end after it starts.\nto> 24:01\nType a time like 10:15, up to 24:00.\nto> \nCancelled.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 3\nperson> Ana B\nType a name of letters, up to 12.\nperson> Zoe\nNobody called Zoe has a meeting.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 4\npeople (like Ana, Ben)> Ana\nat least how many minutes (5 to 480)> 4\nType a whole number from 5 to 480.\nat least how many minutes (5 to 480)> abc\nType a whole number from 5 to 480.\nat least how many minutes (5 to 480)> 480\nBetween 09:00 and 17:00, Ana is never free for 480 minutes.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 4\npeople (like Ana, Ben)> Ben, Chloe, Dev\nat least how many minutes (5 to 480)> 240\nBetween 09:00 and 17:00, Ben, Chloe and Dev are never all free for 240 minutes.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 6\ntitle> nothing\nNo meeting is called nothing.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 2\ntitle> late\npeople (like Ana, Ben)> Ben\nfrom (like 9:30)> 14:59\nto> 15:30\nBen is already in deploy window, 13:30-15:00 - 1 minute of it would clash.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 2\ntitle> evening\npeople (like Ana, Ben)> Gus\nfrom (like 9:30)> 23:00\nto> 24:00\nBooked evening, 23:00-24:00.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 3\nperson> Gus\nGus is busy 23:00-24:00 - 1 h in all.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n3\nAna\n4\nAna, Ben\n30\n4\nChloe, Dev\n60\n2\nplanning\nAna, Ben\n10:15\n11:00\n2\nall-hands\nEli\n9:30\n10:20\n5\n6\nall-hands\n5\n1\n7\n",
      "screen": "== Meeting planner ==\nA day of meetings: book them, see who is busy, find shared free time, count the rooms.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 1\n  09:00-09:15  standup (Ana, Ben and Chloe)\n  09:15-10:15  design review (Ana and Ben)\n  10:00-10:30  one-to-one (Chloe and Dev)\n  12:00-13:00  lunch talk (Ben, Chloe and Dev)\n  13:00-14:00  retro (Ana and Dev)\n  13:30-15:00  deploy window (Ben)\n  15:30-16:15  interview (Ana and Chloe)\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 3\nperson> Ana\nAna is busy 09:00-10:15, 13:00-14:00, 15:30-16:15 - 3 h in all.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 4\npeople (like Ana, Ben)> Ana, Ben\nat least how many minutes (5 to 480)> 30\nBetween 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).\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 4\npeople (like Ana, Ben)> Chloe, Dev\nat least how many minutes (5 to 480)> 60\nBetween 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).\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 2\ntitle> planning\npeople (like Ana, Ben)> Ana, Ben\nfrom (like 9:30)> 10:15\nto> 11:00\nBooked planning, 10:15-11:00.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 2\ntitle> all-hands\npeople (like Ana, Ben)> Eli\nfrom (like 9:30)> 9:30\nto> 10:20\nBooked all-hands, 09:30-10:20.\nThe day now needs 3 rooms.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 5\nAt most 3 meetings run at once, first at 10:00 - so 3 rooms are enough:\n  room 1: 09:00-09:15 standup, 09:15-10:15 design review, 10:15-11:00 planning, 13:30-15:00 deploy window\n  room 2: 09:30-10:20 all-hands, 12:00-13:00 lunch talk, 15:30-16:15 interview\n  room 3: 10:00-10:30 one-to-one, 13:00-14:00 retro\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 6\ntitle> all-hands\nRemoved all-hands.\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 5\nAt most 2 meetings run at once, first at 10:00 - so 2 rooms are enough:\n  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\n  room 2: 10:00-10:30 one-to-one, 12:00-13:00 lunch talk, 13:30-15:00 deploy window\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 1\n  09:00-09:15  standup (Ana, Ben and Chloe)\n  09:15-10:15  design review (Ana and Ben)\n  10:00-10:30  one-to-one (Chloe and Dev)\n  10:15-11:00  planning (Ana and Ben)\n  12:00-13:00  lunch talk (Ben, Chloe and Dev)\n  13:00-14:00  retro (Ana and Dev)\n  13:30-15:00  deploy window (Ben)\n  15:30-16:15  interview (Ana and Chloe)\n\n1) meetings  2) book  3) someone's busy time  4) shared free time  5) rooms needed  6) remove  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "interval-overlap-detector",
      "caseId": "205-interval-overlap-detector",
      "title": "Back-to-back is not an overlap"
    }
  ],
  "updated": "2026-10-14"
}
