{
  "id": "P031",
  "slug": "shift-roster",
  "key": "P031-shift-roster",
  "title": "Shift roster",
  "summary": "A week of morning, evening and night shifts for up to 8 people with days off: assign by hand - refused with every rule it would break - or fill automatically, fairest first; a separate audit rechecks the finished roster from scratch against one shift a day, 11 hours of rest, at most 5 shifts a week and no shift on a day off.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P031 - Shift roster\n\nA week of shifts - morning 06:00-14:00, evening 14:00-22:00 and night\n22:00-06:00 - for up to 8 people, each with days off. Shifts are assigned by\nhand or filled automatically, and the finished roster is checked against the\nrules:\n\n- at most one shift a day for anyone;\n- at least 11 hours of rest between the end of one shift and the start of\n  the next;\n- at most 5 shifts a week;\n- no shift on a day off.\n\n- `main.eml` - the menu, people and days off, the questions and their\n  checks, and the roster on screen\n- `rules.eml` - the week, the shifts and the rules: why a person cannot\n  take a shift, the audit of a whole roster, and the automatic fill\n\nHow each part works:\n\n- An assignment by hand is refused with every rule it would break, each as a\n  sentence: after Bo's Monday night (ending 06:00 on Tuesday), Tuesday\n  morning leaves 0 hours of rest and Tuesday evening 8; Tuesday night leaves\n  16 and is fine. Only the neighbouring days can be too close: two days\n  apart there are always at least 24 hours.\n- The automatic fill goes through the empty shifts in time order and gives\n  each to the person with the fewest shifts so far among those the rules\n  allow - the first added on a tie. A shift nobody may take is left empty and\n  named.\n- The check is a separate audit, as in the corpus case\n  `shift-roster-constraints`: it works from the finished roster alone,\n  putting each person's shifts in time order and measuring the gaps, so it\n  does not trust whatever placed them. Setting a day off after a shift on\n  that day was assigned is allowed - the program says so - and the audit\n  reports it.\n- An empty shift is a gap in coverage, not a rule break; the check lists\n  the two separately.\n\nWhat is checked: names of 1 to 12 letters, each different in any case, at\nmost 8; days off as day names (or `none`); a day as a name or its first three\nletters; a shift as `morning`, `evening`, `night` or `m`, `e`, `n`; a shift\nalready someone else's must be cleared first. An empty answer cancels.\n\nSessions: `sessions/basic.in` adds four people with weekend and Monday days\noff, gives Bo Monday night, is refused Tuesday morning and evening for Bo\n(0 and 8 hours of rest) but gives him Tuesday night, is refused Saturday for\nAna (a day off), fills the rest automatically (two Sunday shifts nobody may\ntake), checks it, moves Dee's day off onto a shift Dee has, checks again (one\nbreak), clears that shift and checks once more; `sessions/bad-input.in` uses\nthe menu with nobody, adds a name with a digit and a name twice, fills up to\n8 people, gives an unknown person, `Funday` and `afternoon`, a shift that is\ntaken and one Ana already has, a sixth shift for Ana, clears an empty shift,\nand shows and checks a roster with 16 empty shifts.\n\nBuilt on the verified corpus case `shift-roster-constraints` (a greedy\nbuilder and an audit that re-derives every constraint from the finished\nroster alone).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P031 shift roster: people, their days off, and a week of morning, evening\n# and night shifts - assigned by hand or filled automatically - checked\n# against the rules: one shift a day, 11 hours of rest, 5 shifts a week, no\n# shift on a day off.\nimport rules\n\n8 => most_people\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 lower(s):\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper\n    \"abcdefghijklmnopqrstuvwxyz\" => small\n    \"\" => out\n    for c in s:\n        0 => k\n        while k < 26 and upper[k] != c:\n            k + 1 => k\n        if k < 26:\n            out + small[k] => out\n        else:\n            out + c => out\n    return out\n\ndef words(s):\n    [] => out\n    \"\" => w\n    for c in s + \" \":\n        if c == \" \" or c == \",\":\n            if w != \"\":\n                out + [w] => out\n            \"\" => w\n        else:\n            w + c => w\n    return out\n\ndef listed(items):\n    \"\" => out\n    0 => i\n    while i < len(items):\n        if i > 0 and i == len(items) - 1:\n            out + \" and \" => out\n        elif i > 0:\n            out + \", \" => out\n        out + items[i] => out\n        i + 1 => i\n    return out\n\ndef pad(s, width):\n    while len(s) < width:\n        s + \" \" => s\n    return s\n\ndef without_trailing(s):\n    len(s) => e\n    while e > 0 and s[e - 1] == \" \":\n        e - 1 => e\n    return s[0:e]\n\ndef day_of(s):\n    lower(trim(s)) => s\n    [\"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\", \"sunday\"] => full\n    for d in [0:6]:\n        if s == full[d] or (len(s) == 3 and s == full[d][0:3]):\n            return d\n    return 0 - 1\n\ndef shift_of(s):\n    lower(trim(s)) => s\n    for t in [0:2]:\n        if s == rules.shifts[t] or (len(s) == 1 and s == rules.shifts[t][0:1]):\n            return t\n    return 0 - 1\n\ndef person_of(names, s):\n    for p in [0:len(names) - 1]:\n        if lower(names[p]) == lower(trim(s)):\n            return p\n    return 0 - 1\n\ndef ask(prompt, kind, names):\n    # A day, a shift or a person (by kind), or -1 when the answer is empty.\n    while True:\n        input(prompt) => answer\n        if trim(answer) == \"\":\n            return 0 - 1\n        0 - 1 => found\n        if kind == \"day\":\n            day_of(answer) => found\n        elif kind == \"shift\":\n            shift_of(answer) => found\n        else:\n            person_of(names, answer) => found\n        if found >= 0:\n            return found\n        if kind == \"day\":\n            \"Type a day, Mon to Sun.\" ^0\n        elif kind == \"shift\":\n            \"Type morning, evening or night (or m, e, n).\" ^0\n        else:\n            (\"Type one of: \" + listed(names) + \".\") ^0\n\ndef show(roster, names):\n    0 => width\n    for n in names:\n        if len(n) > width:\n            len(n) => width\n    if width < 7:\n        7 => width\n    \"     \" => top\n    for t in [0:2]:\n        top + \"  \" + pad(rules.shifts[t], width) => top\n    without_trailing(top) ^0\n    for d in [0:6]:\n        pad(rules.days[d], 5) => line\n        for t in [0:2]:\n            \"-\" => who\n            if roster[d][t] >= 0:\n                names[roster[d][t]] => who\n            line + \"  \" + pad(who, width) => line\n        without_trailing(line) ^0\n    if len(names) == 0:\n        \"No people yet.\" ^0\n        return\n    [] => counts\n    for p in [0:len(names) - 1]:\n        counts + [names[p] + \" \" + str(rules.shift_count(roster, p))] => counts\n    (\"Shifts: \" + listed(counts) + \".\") ^0\n\ndef empty_slots(roster):\n    [] => out\n    for d in [0:6]:\n        for t in [0:2]:\n            if roster[d][t] == 0 - 1:\n                out + [rules.days[d] + \" \" + rules.shifts[t]] => out\n    return out\n\ndef add_person(names, off):\n    # Returns [names, off], with one more person or as they were.\n    if len(names) >= most_people:\n        (\"There are already \" + str(most_people) + \" people, the most.\") ^0\n        return [names, off]\n    while True:\n        trim(input(\"name> \")) => name\n        if name == \"\":\n            \"Cancelled.\" ^0\n            return [names, off]\n        True => good\n        if len(name) > 12:\n            False => good\n        for c in name:\n            if not (lower(c) in \"abcdefghijklmnopqrstuvwxyz\"):\n                False => good\n        if not good:\n            \"A name is 1 to 12 letters.\" ^0\n        elif person_of(names, name) >= 0:\n            (\"There is already someone called \" + names[person_of(names, name)] + \".\") ^0\n        else:\n            (\"Added \" + name + \", with no days off.\") ^0\n            return [names + [name], off + [[False, False, False, False, False, False, False]]]\n\ndef set_days_off(roster, names, off):\n    ask(\"person> \", \"person\", names) => p\n    if p == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    while True:\n        trim(input(\"days off (such as Sat Sun, or none)> \")) => answer\n        if answer == \"\":\n            \"Cancelled.\" ^0\n            return\n        [False, False, False, False, False, False, False] => chosen\n        True => good\n        if lower(answer) != \"none\":\n            for w in words(answer):\n                day_of(w) => d\n                if d == 0 - 1:\n                    False => good\n                else:\n                    True => chosen[d]\n        if good:\n            chosen => off[p]\n            [] => named\n            for d in [0:6]:\n                if chosen[d]:\n                    named + [rules.days[d]] => named\n            if len(named) == 0:\n                (names[p] + \" has no days off.\") ^0\n            else:\n                (names[p] + \" is off on \" + listed(named) + \".\") ^0\n            for d in [0:6]:\n                for t in [0:2]:\n                    if chosen[d] and roster[d][t] == p:\n                        (names[p] + \" is still on \" + rules.days[d] + \" \" + rules.shifts[t] + \"; the check (7) will report it.\") ^0\n            return\n        \"Type day names such as Mon Tue, or none.\" ^0\n\ndef assign(roster, names, off):\n    if len(names) == 0:\n        \"Add a person first (1).\" ^0\n        return\n    ask(\"person> \", \"person\", names) => p\n    if p == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    ask(\"day> \", \"day\", names) => d\n    if d == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    ask(\"shift> \", \"shift\", names) => s\n    if s == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    roster[d][s] => there\n    if there == p:\n        (names[p] + \" already has \" + rules.days[d] + \" \" + rules.shifts[s] + \".\") ^0\n        return\n    if there >= 0:\n        (rules.days[d] + \" \" + rules.shifts[s] + \" is \" + names[there] + \"'s; clear it first (4).\") ^0\n        return\n    rules.reasons_against(roster, off, names, p, d, s) => why\n    if len(why) > 0:\n        (names[p] + \" cannot take \" + rules.days[d] + \" \" + rules.shifts[s] + \":\") ^0\n        for w in why:\n            (\"  \" + w) ^0\n        return\n    p => roster[d][s]\n    (rules.days[d] + \" \" + rules.shifts[s] + \": \" + names[p] + \".\") ^0\n\ndef clear(roster, names):\n    ask(\"day> \", \"day\", names) => d\n    if d == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    ask(\"shift> \", \"shift\", names) => s\n    if s == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    if roster[d][s] == 0 - 1:\n        (rules.days[d] + \" \" + rules.shifts[s] + \" is empty already.\") ^0\n    else:\n        (\"Cleared \" + rules.days[d] + \" \" + rules.shifts[s] + \" (was \" + names[roster[d][s]] + \").\") ^0\n        0 - 1 => roster[d][s]\n\ndef check(roster, names, off):\n    rules.audit(roster, off, names) => breaks\n    if len(breaks) == 0:\n        \"Every rule holds.\" ^0\n    else:\n        if len(breaks) == 1:\n            \"1 rule break:\" ^0\n        else:\n            (str(len(breaks)) + \" rule breaks:\") ^0\n        for b in breaks:\n            (\"  \" + b) ^0\n    empty_slots(roster) => left\n    if len(left) == 0:\n        \"Every shift is covered.\" ^0\n    elif len(left) == 1:\n        (\"1 shift is empty: \" + left[0] + \".\") ^0\n    elif len(left) <= 6:\n        (str(len(left)) + \" shifts are empty: \" + listed(left) + \".\") ^0\n    else:\n        (str(len(left)) + \" shifts are empty: \" + listed(left[0:6] + [str(len(left) - 6) + \" more\"]) + \".\") ^0\n\n\"== Shift roster ==\" ^0\n\"Shifts: morning 06-14, evening 14-22, night 22-06. Rules: one shift a day, 11 hours of rest between shifts, at most 5 shifts a week, none on a day off.\" ^0\n[] => names\n[] => off\nrules.empty_roster() => roster\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) add a person  2) days off  3) assign a shift  4) clear a shift\" ^0\n    \"5) fill automatically  6) show the roster  7) check the rules  8) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        add_person(names, off) => added\n        added[0] => names\n        added[1] => off\n    elif choice == \"8\":\n        False => running\n    elif choice == \"2\" or choice == \"3\" or choice == \"4\" or choice == \"5\" or choice == \"6\" or choice == \"7\":\n        if len(names) == 0 and choice != \"6\" and choice != \"7\":\n            \"Add a person first (1).\" ^0\n        elif choice == \"2\":\n            set_days_off(roster, names, off)\n        elif choice == \"3\":\n            assign(roster, names, off)\n        elif choice == \"4\":\n            clear(roster, names)\n        elif choice == \"5\":\n            rules.fill(roster, off, names) => result\n            result[0] => roster\n            (\"Filled \" + str(result[1]) + \" shifts.\") ^0\n            if len(result[2]) > 0:\n                (\"Left empty - nobody the rules allow: \" + listed(result[2]) + \".\") ^0\n            show(roster, names)\n        elif choice == \"6\":\n            show(roster, names)\n        else:\n            check(roster, names, off)\n    else:\n        \"Pick a number from 1 to 8.\" ^0\n\"Bye.\" ^0\n",
      "python": "import rules\nmost_people = 8\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 lower(s):\n    upper = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    small = \"abcdefghijklmnopqrstuvwxyz\"\n    out = \"\"\n    for c in s:\n        k = 0\n        while k < 26 and upper[k] != c:\n            k = k + 1\n        if k < 26:\n            out = out + small[k]\n        else:\n            out = out + c\n    return out\n\ndef words(s):\n    out = []\n    w = \"\"\n    for c in s + \" \":\n        if c == \" \" or c == \",\":\n            if w != \"\":\n                out = out + [w]\n            w = \"\"\n        else:\n            w = w + c\n    return out\n\ndef listed(items):\n    out = \"\"\n    i = 0\n    while i < len(items):\n        if i > 0 and i == len(items) - 1:\n            out = out + \" and \"\n        elif i > 0:\n            out = out + \", \"\n        out = out + items[i]\n        i = i + 1\n    return out\n\ndef pad(s, width):\n    while len(s) < width:\n        s = s + \" \"\n    return s\n\ndef without_trailing(s):\n    e = len(s)\n    while e > 0 and s[e - 1] == \" \":\n        e = e - 1\n    return s[0:e]\n\ndef day_of(s):\n    s = lower(trim(s))\n    full = [\"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\", \"sunday\"]\n    for d in range(0, 7):\n        if s == full[d] or len(s) == 3 and s == full[d][0:3]:\n            return d\n    return 0 - 1\n\ndef shift_of(s):\n    s = lower(trim(s))\n    for t in range(0, 3):\n        if s == rules.shifts[t] or len(s) == 1 and s == rules.shifts[t][0:1]:\n            return t\n    return 0 - 1\n\ndef person_of(names, s):\n    for p in range(0, len(names)):\n        if lower(names[p]) == lower(trim(s)):\n            return p\n    return 0 - 1\n\ndef ask(prompt, kind, names):\n    while True:\n        answer = input(prompt)\n        if trim(answer) == \"\":\n            return 0 - 1\n        found = 0 - 1\n        if kind == \"day\":\n            found = day_of(answer)\n        elif kind == \"shift\":\n            found = shift_of(answer)\n        else:\n            found = person_of(names, answer)\n        if found >= 0:\n            return found\n        if kind == \"day\":\n            print(\"Type a day, Mon to Sun.\")\n        elif kind == \"shift\":\n            print(\"Type morning, evening or night (or m, e, n).\")\n        else:\n            print(\"Type one of: \" + listed(names) + \".\")\n\ndef show(roster, names):\n    width = 0\n    for n in names:\n        if len(n) > width:\n            width = len(n)\n    if width < 7:\n        width = 7\n    top = \"     \"\n    for t in range(0, 3):\n        top = top + \"  \" + pad(rules.shifts[t], width)\n    print(without_trailing(top))\n    for d in range(0, 7):\n        line = pad(rules.days[d], 5)\n        for t in range(0, 3):\n            who = \"-\"\n            if roster[d][t] >= 0:\n                who = names[roster[d][t]]\n            line = line + \"  \" + pad(who, width)\n        print(without_trailing(line))\n    if len(names) == 0:\n        print(\"No people yet.\")\n        return\n    counts = []\n    for p in range(0, len(names)):\n        counts = counts + [names[p] + \" \" + str(rules.shift_count(roster, p))]\n    print(\"Shifts: \" + listed(counts) + \".\")\n\ndef empty_slots(roster):\n    out = []\n    for d in range(0, 7):\n        for t in range(0, 3):\n            if roster[d][t] == 0 - 1:\n                out = out + [rules.days[d] + \" \" + rules.shifts[t]]\n    return out\n\ndef add_person(names, off):\n    if len(names) >= most_people:\n        print(\"There are already \" + str(most_people) + \" people, the most.\")\n        return [names, off]\n    while True:\n        name = trim(input(\"name> \"))\n        if name == \"\":\n            print(\"Cancelled.\")\n            return [names, off]\n        good = True\n        if len(name) > 12:\n            good = False\n        for c in name:\n            if not lower(c) in \"abcdefghijklmnopqrstuvwxyz\":\n                good = False\n        if not good:\n            print(\"A name is 1 to 12 letters.\")\n        elif person_of(names, name) >= 0:\n            print(\"There is already someone called \" + names[person_of(names, name)] + \".\")\n        else:\n            print(\"Added \" + name + \", with no days off.\")\n            return [names + [name], off + [[False, False, False, False, False, False, False]]]\n\ndef set_days_off(roster, names, off):\n    p = ask(\"person> \", \"person\", names)\n    if p == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    while True:\n        answer = trim(input(\"days off (such as Sat Sun, or none)> \"))\n        if answer == \"\":\n            print(\"Cancelled.\")\n            return\n        chosen = [False, False, False, False, False, False, False]\n        good = True\n        if lower(answer) != \"none\":\n            for w in words(answer):\n                d = day_of(w)\n                if d == 0 - 1:\n                    good = False\n                else:\n                    chosen[d] = True\n        if good:\n            off[p] = chosen\n            named = []\n            for d in range(0, 7):\n                if chosen[d]:\n                    named = named + [rules.days[d]]\n            if len(named) == 0:\n                print(names[p] + \" has no days off.\")\n            else:\n                print(names[p] + \" is off on \" + listed(named) + \".\")\n            for d in range(0, 7):\n                for t in range(0, 3):\n                    if chosen[d] and roster[d][t] == p:\n                        print(names[p] + \" is still on \" + rules.days[d] + \" \" + rules.shifts[t] + \"; the check (7) will report it.\")\n            return\n        print(\"Type day names such as Mon Tue, or none.\")\n\ndef assign(roster, names, off):\n    if len(names) == 0:\n        print(\"Add a person first (1).\")\n        return\n    p = ask(\"person> \", \"person\", names)\n    if p == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    d = ask(\"day> \", \"day\", names)\n    if d == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    s = ask(\"shift> \", \"shift\", names)\n    if s == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    there = roster[d][s]\n    if there == p:\n        print(names[p] + \" already has \" + rules.days[d] + \" \" + rules.shifts[s] + \".\")\n        return\n    if there >= 0:\n        print(rules.days[d] + \" \" + rules.shifts[s] + \" is \" + names[there] + \"'s; clear it first (4).\")\n        return\n    why = rules.reasons_against(roster, off, names, p, d, s)\n    if len(why) > 0:\n        print(names[p] + \" cannot take \" + rules.days[d] + \" \" + rules.shifts[s] + \":\")\n        for w in why:\n            print(\"  \" + w)\n        return\n    roster[d][s] = p\n    print(rules.days[d] + \" \" + rules.shifts[s] + \": \" + names[p] + \".\")\n\ndef clear(roster, names):\n    d = ask(\"day> \", \"day\", names)\n    if d == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    s = ask(\"shift> \", \"shift\", names)\n    if s == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    if roster[d][s] == 0 - 1:\n        print(rules.days[d] + \" \" + rules.shifts[s] + \" is empty already.\")\n    else:\n        print(\"Cleared \" + rules.days[d] + \" \" + rules.shifts[s] + \" (was \" + names[roster[d][s]] + \").\")\n        roster[d][s] = 0 - 1\n\ndef check(roster, names, off):\n    breaks = rules.audit(roster, off, names)\n    if len(breaks) == 0:\n        print(\"Every rule holds.\")\n    else:\n        if len(breaks) == 1:\n            print(\"1 rule break:\")\n        else:\n            print(str(len(breaks)) + \" rule breaks:\")\n        for b in breaks:\n            print(\"  \" + b)\n    left = empty_slots(roster)\n    if len(left) == 0:\n        print(\"Every shift is covered.\")\n    elif len(left) == 1:\n        print(\"1 shift is empty: \" + left[0] + \".\")\n    elif len(left) <= 6:\n        print(str(len(left)) + \" shifts are empty: \" + listed(left) + \".\")\n    else:\n        print(str(len(left)) + \" shifts are empty: \" + listed(left[0:6] + [str(len(left) - 6) + \" more\"]) + \".\")\n\nprint(\"== Shift roster ==\")\nprint(\"Shifts: morning 06-14, evening 14-22, night 22-06. Rules: one shift a day, 11 hours of rest between shifts, at most 5 shifts a week, none on a day off.\")\nnames = []\noff = []\nroster = rules.empty_roster()\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) add a person  2) days off  3) assign a shift  4) clear a shift\")\n    print(\"5) fill automatically  6) show the roster  7) check the rules  8) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        added = add_person(names, off)\n        names = added[0]\n        off = added[1]\n    elif choice == \"8\":\n        running = False\n    elif choice == \"2\" or choice == \"3\" or choice == \"4\" or choice == \"5\" or choice == \"6\" or choice == \"7\":\n        if len(names) == 0 and choice != \"6\" and choice != \"7\":\n            print(\"Add a person first (1).\")\n        elif choice == \"2\":\n            set_days_off(roster, names, off)\n        elif choice == \"3\":\n            assign(roster, names, off)\n        elif choice == \"4\":\n            clear(roster, names)\n        elif choice == \"5\":\n            result = rules.fill(roster, off, names)\n            roster = result[0]\n            print(\"Filled \" + str(result[1]) + \" shifts.\")\n            if len(result[2]) > 0:\n                print(\"Left empty - nobody the rules allow: \" + listed(result[2]) + \".\")\n            show(roster, names)\n        elif choice == \"6\":\n            show(roster, names)\n        else:\n            check(roster, names, off)\n    else:\n        print(\"Pick a number from 1 to 8.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "rules.eml",
      "eml": "# P031 shift roster - the week, the shifts and the rules. The roster is 7\n# days of 3 shifts, each holding a person's index or -1; off[p] is that\n# person's 7 days, True where the day is off.\n#\n# The rules: one shift a day at most; at least 11 hours of rest between the\n# end of one shift and the start of the next; at most 5 shifts a week; no\n# shift on a day off. The night shift runs from 22:00 to 06:00 the next day.\n#\n# As in the corpus case shift-roster-constraints, checking one assignment\n# (reasons_against) and auditing a finished roster (audit) are written\n# separately: the audit works from the roster alone, by putting each\n# person's shifts in time order and measuring the gaps.\n\n[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] => days\n[\"morning\", \"evening\", \"night\"] => shifts\n[6, 14, 22] => starts\n[14, 22, 30] => ends\n11 => least_rest\n5 => most_shifts\n\ndef shift_count(roster, p):\n    0 => n\n    for day in roster:\n        for who in day:\n            if who == p:\n                n + 1 => n\n    return n\n\ndef empty_roster():\n    [] => out\n    for d in [0:6]:\n        out + [[0 - 1, 0 - 1, 0 - 1]] => out\n    return out\n\ndef reasons_against(roster, off, names, p, d, s):\n    # Why person p cannot take shift s on day d, as sentences; an empty list\n    # means p can. The slot itself is not looked at: the caller decides what\n    # to do when it is someone else's.\n    names[p] => name\n    [] => out\n    if off[p][d]:\n        out + [name + \" is off on \" + days[d] + \".\"] => out\n    for t in [0:2]:\n        if t != s and roster[d][t] == p:\n            out + [name + \" already works \" + days[d] + \" \" + shifts[t] + \".\"] => out\n    if d > 0:\n        for t in [0:2]:\n            if roster[d - 1][t] == p:\n                24 + starts[s] - ends[t] => rest\n                if rest < least_rest:\n                    out + [name + \" works \" + days[d - 1] + \" \" + shifts[t] + \": \" + str(rest) + \" hours of rest, at least \" + str(least_rest) + \".\"] => out\n    if d < 6:\n        for t in [0:2]:\n            if roster[d + 1][t] == p:\n                24 + starts[t] - ends[s] => rest\n                if rest < least_rest:\n                    out + [name + \" works \" + days[d + 1] + \" \" + shifts[t] + \": \" + str(rest) + \" hours of rest, at least \" + str(least_rest) + \".\"] => out\n    shift_count(roster, p) => n\n    if roster[d][s] == p:\n        n - 1 => n\n    if n >= most_shifts:\n        out + [name + \" already has \" + str(n) + \" shifts this week, the most.\"] => out\n    return out\n\ndef audit(roster, off, names):\n    # Every rule break in the roster, found from the roster alone.\n    [] => out\n    for p in [0:len(names) - 1]:\n        # p's shifts as [start hour, end hour, day, shift], in time order\n        [] => mine\n        for d in [0:6]:\n            for s in [0:2]:\n                if roster[d][s] == p:\n                    mine + [[24 * d + starts[s], 24 * d + ends[s], d, s]] => mine\n        for d in [0:6]:\n            0 => that_day\n            for m in mine:\n                if m[2] == d:\n                    that_day + 1 => that_day\n            if that_day > 1:\n                out + [names[p] + \" works \" + str(that_day) + \" shifts on \" + days[d] + \".\"] => out\n            if off[p][d] and that_day > 0:\n                out + [names[p] + \" works on \" + days[d] + \", a day off.\"] => out\n        for i in [1:len(mine) - 1]:\n            mine[i][0] - mine[i - 1][1] => gap\n            if gap < least_rest:\n                out + [names[p] + \" works \" + days[mine[i - 1][2]] + \" \" + shifts[mine[i - 1][3]] + \" and \" + days[mine[i][2]] + \" \" + shifts[mine[i][3]] + \": \" + str(gap) + \" hours of rest, at least \" + str(least_rest) + \".\"] => out\n        if len(mine) > most_shifts:\n            out + [names[p] + \" has \" + str(len(mine)) + \" shifts, at most \" + str(most_shifts) + \".\"] => out\n    return out\n\ndef fill(roster, off, names):\n    # Every empty shift, in time order, goes to the person with the fewest\n    # shifts so far among those the rules allow (the first added on a tie).\n    # Returns [roster, filled, left empty as \"Day shift\"].\n    0 => filled\n    [] => left\n    for d in [0:6]:\n        for s in [0:2]:\n            if roster[d][s] == 0 - 1:\n                0 - 1 => best\n                for p in [0:len(names) - 1]:\n                    if len(reasons_against(roster, off, names, p, d, s)) == 0:\n                        if best == 0 - 1 or shift_count(roster, p) < shift_count(roster, best):\n                            p => best\n                if best == 0 - 1:\n                    left + [days[d] + \" \" + shifts[s]] => left\n                else:\n                    best => roster[d][s]\n                    filled + 1 => filled\n    return [roster, filled, left]\n",
      "python": "days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\nshifts = [\"morning\", \"evening\", \"night\"]\nstarts = [6, 14, 22]\nends = [14, 22, 30]\nleast_rest = 11\nmost_shifts = 5\n\ndef shift_count(roster, p):\n    n = 0\n    for day in roster:\n        for who in day:\n            if who == p:\n                n = n + 1\n    return n\n\ndef empty_roster():\n    out = []\n    for d in range(0, 7):\n        out = out + [[0 - 1, 0 - 1, 0 - 1]]\n    return out\n\ndef reasons_against(roster, off, names, p, d, s):\n    name = names[p]\n    out = []\n    if off[p][d]:\n        out = out + [name + \" is off on \" + days[d] + \".\"]\n    for t in range(0, 3):\n        if t != s and roster[d][t] == p:\n            out = out + [name + \" already works \" + days[d] + \" \" + shifts[t] + \".\"]\n    if d > 0:\n        for t in range(0, 3):\n            if roster[d - 1][t] == p:\n                rest = 24 + starts[s] - ends[t]\n                if rest < least_rest:\n                    out = out + [name + \" works \" + days[d - 1] + \" \" + shifts[t] + \": \" + str(rest) + \" hours of rest, at least \" + str(least_rest) + \".\"]\n    if d < 6:\n        for t in range(0, 3):\n            if roster[d + 1][t] == p:\n                rest = 24 + starts[t] - ends[s]\n                if rest < least_rest:\n                    out = out + [name + \" works \" + days[d + 1] + \" \" + shifts[t] + \": \" + str(rest) + \" hours of rest, at least \" + str(least_rest) + \".\"]\n    n = shift_count(roster, p)\n    if roster[d][s] == p:\n        n = n - 1\n    if n >= most_shifts:\n        out = out + [name + \" already has \" + str(n) + \" shifts this week, the most.\"]\n    return out\n\ndef audit(roster, off, names):\n    out = []\n    for p in range(0, len(names)):\n        mine = []\n        for d in range(0, 7):\n            for s in range(0, 3):\n                if roster[d][s] == p:\n                    mine = mine + [[24 * d + starts[s], 24 * d + ends[s], d, s]]\n        for d in range(0, 7):\n            that_day = 0\n            for m in mine:\n                if m[2] == d:\n                    that_day = that_day + 1\n            if that_day > 1:\n                out = out + [names[p] + \" works \" + str(that_day) + \" shifts on \" + days[d] + \".\"]\n            if off[p][d] and that_day > 0:\n                out = out + [names[p] + \" works on \" + days[d] + \", a day off.\"]\n        for i in range(1, len(mine)):\n            gap = mine[i][0] - mine[i - 1][1]\n            if gap < least_rest:\n                out = out + [names[p] + \" works \" + days[mine[i - 1][2]] + \" \" + shifts[mine[i - 1][3]] + \" and \" + days[mine[i][2]] + \" \" + shifts[mine[i][3]] + \": \" + str(gap) + \" hours of rest, at least \" + str(least_rest) + \".\"]\n        if len(mine) > most_shifts:\n            out = out + [names[p] + \" has \" + str(len(mine)) + \" shifts, at most \" + str(most_shifts) + \".\"]\n    return out\n\ndef fill(roster, off, names):\n    filled = 0\n    left = []\n    for d in range(0, 7):\n        for s in range(0, 3):\n            if roster[d][s] == 0 - 1:\n                best = 0 - 1\n                for p in range(0, len(names)):\n                    if len(reasons_against(roster, off, names, p, d, s)) == 0:\n                        if best == 0 - 1 or shift_count(roster, p) < shift_count(roster, best):\n                            best = p\n                if best == 0 - 1:\n                    left = left + [days[d] + \" \" + shifts[s]]\n                else:\n                    roster[d][s] = best\n                    filled = filled + 1\n    return [roster, filled, left]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nnine\n2\n3\n4\n5\n6\n7\n1\nAna1\nAna\n1\nana\n\n1\nBo\n1\nCy\n1\nDee\n1\nEve\n1\nFay\n1\nGus\n1\nHal\n1\n2\nZed\nAna\nSat Funday\nnone\n3\nAna\nSomeday\nMon\nafternoon\nmorning\n3\nBo\nMon\nmorning\n3\nAna\nMon\nmorning\n3\nAna\nTue\nmorning\n3\nAna\nWed\nmorning\n3\nAna\nThu\nmorning\n3\nAna\nFri\nmorning\n3\nAna\nSat\nmorning\n4\nSun\nnight\n4\nMon\n\n6\n7\n8\n",
      "screen": "== Shift roster ==\nShifts: morning 06-14, evening 14-22, night 22-06. Rules: one shift a day, 11 hours of rest between shifts, at most 5 shifts a week, none on a day off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 0\nPick a number from 1 to 8.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> nine\nPick a number from 1 to 8.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 2\nAdd a person first (1).\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nAdd a person first (1).\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 4\nAdd a person first (1).\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 5\nAdd a person first (1).\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 6\n       morning  evening  night\nMon    -        -        -\nTue    -        -        -\nWed    -        -        -\nThu    -        -        -\nFri    -        -        -\nSat    -        -        -\nSun    -        -        -\nNo people yet.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 7\nEvery rule holds.\n21 shifts are empty: Mon morning, Mon evening, Mon night, Tue morning, Tue evening, Tue night and 15 more.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Ana1\nA name is 1 to 12 letters.\nname> Ana\nAdded Ana, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> ana\nThere is already someone called Ana.\nname> \nCancelled.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Bo\nAdded Bo, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Cy\nAdded Cy, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Dee\nAdded Dee, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Eve\nAdded Eve, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Fay\nAdded Fay, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Gus\nAdded Gus, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Hal\nAdded Hal, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nThere are already 8 people, the most.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 2\nperson> Zed\nType one of: Ana, Bo, Cy, Dee, Eve, Fay, Gus and Hal.\nperson> Ana\ndays off (such as Sat Sun, or none)> Sat Funday\nType day names such as Mon Tue, or none.\ndays off (such as Sat Sun, or none)> none\nAna has no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Someday\nType a day, Mon to Sun.\nday> Mon\nshift> afternoon\nType morning, evening or night (or m, e, n).\nshift> morning\nMon morning: Ana.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Bo\nday> Mon\nshift> morning\nMon morning is Ana's; clear it first (4).\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Mon\nshift> morning\nAna already has Mon morning.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Tue\nshift> morning\nTue morning: Ana.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Wed\nshift> morning\nWed morning: Ana.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Thu\nshift> morning\nThu morning: Ana.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Fri\nshift> morning\nFri morning: Ana.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Sat\nshift> morning\nAna cannot take Sat morning:\n  Ana already has 5 shifts this week, the most.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 4\nday> Sun\nshift> night\nSun night is empty already.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 4\nday> Mon\nshift> \nCancelled.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 6\n       morning  evening  night\nMon    Ana      -        -\nTue    Ana      -        -\nWed    Ana      -        -\nThu    Ana      -        -\nFri    Ana      -        -\nSat    -        -        -\nSun    -        -        -\nShifts: Ana 5, Bo 0, Cy 0, Dee 0, Eve 0, Fay 0, Gus 0 and Hal 0.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 7\nEvery rule holds.\n16 shifts are empty: Mon evening, Mon night, Tue evening, Tue night, Wed evening, Wed night and 10 more.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nAna\n1\nBo\n1\nCy\n1\nDee\n2\nAna\nSat Sun\n2\ndee\nmon\n3\nBo\nMon\nnight\n3\nbo\ntue\nm\n3\nBo\nTuesday\nevening\n3\nBo\nTue\nn\n3\nAna\nSat\nmorning\n5\n7\n2\nDee\nMon, Sun\n7\n4\nSun\nmorning\n7\n6\n8\n",
      "screen": "== Shift roster ==\nShifts: morning 06-14, evening 14-22, night 22-06. Rules: one shift a day, 11 hours of rest between shifts, at most 5 shifts a week, none on a day off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Ana\nAdded Ana, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Bo\nAdded Bo, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Cy\nAdded Cy, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 1\nname> Dee\nAdded Dee, with no days off.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 2\nperson> Ana\ndays off (such as Sat Sun, or none)> Sat Sun\nAna is off on Sat and Sun.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 2\nperson> dee\ndays off (such as Sat Sun, or none)> mon\nDee is off on Mon.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Bo\nday> Mon\nshift> night\nMon night: Bo.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> bo\nday> tue\nshift> m\nBo cannot take Tue morning:\n  Bo works Mon night: 0 hours of rest, at least 11.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Bo\nday> Tuesday\nshift> evening\nBo cannot take Tue evening:\n  Bo works Mon night: 8 hours of rest, at least 11.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Bo\nday> Tue\nshift> n\nTue night: Bo.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 3\nperson> Ana\nday> Sat\nshift> morning\nAna cannot take Sat morning:\n  Ana is off on Sat.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 5\nFilled 17 shifts.\nLeft empty - nobody the rules allow: Sun evening and Sun night.\n       morning  evening  night\nMon    Ana      Cy       Bo\nTue    Dee      Ana      Bo\nWed    Cy       Dee      Ana\nThu    Bo       Cy       Dee\nFri    Ana      Bo       Cy\nSat    Dee      Bo       Cy\nSun    Dee      -        -\nShifts: Ana 4, Bo 5, Cy 5 and Dee 5.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 7\nEvery rule holds.\n2 shifts are empty: Sun evening and Sun night.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 2\nperson> Dee\ndays off (such as Sat Sun, or none)> Mon, Sun\nDee is off on Mon and Sun.\nDee is still on Sun morning; the check (7) will report it.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 7\n1 rule break:\n  Dee works on Sun, a day off.\n2 shifts are empty: Sun evening and Sun night.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 4\nday> Sun\nshift> morning\nCleared Sun morning (was Dee).\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 7\nEvery rule holds.\n3 shifts are empty: Sun morning, Sun evening and Sun night.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 6\n       morning  evening  night\nMon    Ana      Cy       Bo\nTue    Dee      Ana      Bo\nWed    Cy       Dee      Ana\nThu    Bo       Cy       Dee\nFri    Ana      Bo       Cy\nSat    Dee      Bo       Cy\nSun    -        -        -\nShifts: Ana 4, Bo 5, Cy 5 and Dee 4.\n\n1) add a person  2) days off  3) assign a shift  4) clear a shift\n5) fill automatically  6) show the roster  7) check the rules  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "shift-roster-constraints",
      "caseId": "197-shift-roster-constraints",
      "title": "Shift roster: the builder does not get to grade itself"
    }
  ],
  "updated": "2026-10-06"
}
