Project P031

Shift roster

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.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-06

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

A week of shifts - morning 06:00-14:00, evening 14:00-22:00 and night 22:00-06:00 - for up to 8 people, each with days off. Shifts are assigned by hand or filled automatically, and the finished roster is checked against the rules:

  • at most one shift a day for anyone;
  • at least 11 hours of rest between the end of one shift and the start of the next;
  • at most 5 shifts a week;
  • no shift on a day off.
  • main.eml - the menu, people and days off, the questions and their checks, and the roster on screen
  • rules.eml - the week, the shifts and the rules: why a person cannot take a shift, the audit of a whole roster, and the automatic fill

How each part works:

  • An assignment by hand is refused with every rule it would break, each as a sentence: after Bo's Monday night (ending 06:00 on Tuesday), Tuesday morning leaves 0 hours of rest and Tuesday evening 8; Tuesday night leaves 16 and is fine. Only the neighbouring days can be too close: two days apart there are always at least 24 hours.
  • The automatic fill goes through the empty shifts in time order and gives each to the person with the fewest shifts so far among those the rules allow - the first added on a tie. A shift nobody may take is left empty and named.
  • The check is a separate audit, as in the corpus case shift-roster-constraints: it works from the finished roster alone, putting each person's shifts in time order and measuring the gaps, so it does not trust whatever placed them. Setting a day off after a shift on that day was assigned is allowed - the program says so - and the audit reports it.
  • An empty shift is a gap in coverage, not a rule break; the check lists the two separately.

What is checked: names of 1 to 12 letters, each different in any case, at most 8; days off as day names (or none); a day as a name or its first three letters; a shift as morning, evening, night or m, e, n; a shift already someone else's must be cleared first. An empty answer cancels.

Sessions: sessions/basic.in adds four people with weekend and Monday days off, gives Bo Monday night, is refused Tuesday morning and evening for Bo (0 and 8 hours of rest) but gives him Tuesday night, is refused Saturday for Ana (a day off), fills the rest automatically (two Sunday shifts nobody may take), checks it, moves Dee's day off onto a shift Dee has, checks again (one break), clears that shift and checks once more; sessions/bad-input.in uses the menu with nobody, adds a name with a digit and a name twice, fills up to 8 people, gives an unknown person, Funday and afternoon, a shift that is taken and one Ana already has, a sixth shift for Ana, clears an empty shift, and shows and checks a roster with 16 empty shifts.

Built on the verified corpus case shift-roster-constraints (a greedy builder and an audit that re-derives every constraint from the finished roster alone).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Shift roster ==
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.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 0
Pick a number from 1 to 8.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> nine
Pick a number from 1 to 8.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 2
Add a person first (1).

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
Add a person first (1).

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 4
Add a person first (1).

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 5
Add a person first (1).

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 6
       morning  evening  night
Mon    -        -        -
Tue    -        -        -
Wed    -        -        -
Thu    -        -        -
Fri    -        -        -
Sat    -        -        -
Sun    -        -        -
No people yet.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 7
Every rule holds.
21 shifts are empty: Mon morning, Mon evening, Mon night, Tue morning, Tue evening, Tue night and 15 more.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Ana1
A name is 1 to 12 letters.
name> Ana
Added Ana, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> ana
There is already someone called Ana.
name> 
Cancelled.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Bo
Added Bo, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Cy
Added Cy, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Dee
Added Dee, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Eve
Added Eve, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Fay
Added Fay, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Gus
Added Gus, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Hal
Added Hal, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
There are already 8 people, the most.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 2
person> Zed
Type one of: Ana, Bo, Cy, Dee, Eve, Fay, Gus and Hal.
person> Ana
days off (such as Sat Sun, or none)> Sat Funday
Type day names such as Mon Tue, or none.
days off (such as Sat Sun, or none)> none
Ana has no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Someday
Type a day, Mon to Sun.
day> Mon
shift> afternoon
Type morning, evening or night (or m, e, n).
shift> morning
Mon morning: Ana.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Bo
day> Mon
shift> morning
Mon morning is Ana's; clear it first (4).

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Mon
shift> morning
Ana already has Mon morning.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Tue
shift> morning
Tue morning: Ana.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Wed
shift> morning
Wed morning: Ana.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Thu
shift> morning
Thu morning: Ana.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Fri
shift> morning
Fri morning: Ana.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Sat
shift> morning
Ana cannot take Sat morning:
  Ana already has 5 shifts this week, the most.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 4
day> Sun
shift> night
Sun night is empty already.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 4
day> Mon
shift> 
Cancelled.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 6
       morning  evening  night
Mon    Ana      -        -
Tue    Ana      -        -
Wed    Ana      -        -
Thu    Ana      -        -
Fri    Ana      -        -
Sat    -        -        -
Sun    -        -        -
Shifts: Ana 5, Bo 0, Cy 0, Dee 0, Eve 0, Fay 0, Gus 0 and Hal 0.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 7
Every rule holds.
16 shifts are empty: Mon evening, Mon night, Tue evening, Tue night, Wed evening, Wed night and 10 more.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 8
Bye.
What was typed (77 lines)
0
nine
2
3
4
5
6
7
1
Ana1
Ana
1
ana

1
Bo
1
Cy
1
Dee
1
Eve
1
Fay
1
Gus
1
Hal
1
2
Zed
Ana
Sat Funday
none
3
Ana
Someday
Mon
afternoon
morning
3
Bo
Mon
morning
3
Ana
Mon
morning
3
Ana
Tue
morning
3
Ana
Wed
morning
3
Ana
Thu
morning
3
Ana
Fri
morning
3
Ana
Sat
morning
4
Sun
night
4
Mon

6
7
8

basic

interpreter: byte-equal
== Shift roster ==
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.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Ana
Added Ana, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Bo
Added Bo, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Cy
Added Cy, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 1
name> Dee
Added Dee, with no days off.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 2
person> Ana
days off (such as Sat Sun, or none)> Sat Sun
Ana is off on Sat and Sun.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 2
person> dee
days off (such as Sat Sun, or none)> mon
Dee is off on Mon.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Bo
day> Mon
shift> night
Mon night: Bo.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> bo
day> tue
shift> m
Bo cannot take Tue morning:
  Bo works Mon night: 0 hours of rest, at least 11.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Bo
day> Tuesday
shift> evening
Bo cannot take Tue evening:
  Bo works Mon night: 8 hours of rest, at least 11.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Bo
day> Tue
shift> n
Tue night: Bo.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 3
person> Ana
day> Sat
shift> morning
Ana cannot take Sat morning:
  Ana is off on Sat.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 5
Filled 17 shifts.
Left empty - nobody the rules allow: Sun evening and Sun night.
       morning  evening  night
Mon    Ana      Cy       Bo
Tue    Dee      Ana      Bo
Wed    Cy       Dee      Ana
Thu    Bo       Cy       Dee
Fri    Ana      Bo       Cy
Sat    Dee      Bo       Cy
Sun    Dee      -        -
Shifts: Ana 4, Bo 5, Cy 5 and Dee 5.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 7
Every rule holds.
2 shifts are empty: Sun evening and Sun night.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 2
person> Dee
days off (such as Sat Sun, or none)> Mon, Sun
Dee is off on Mon and Sun.
Dee is still on Sun morning; the check (7) will report it.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 7
1 rule break:
  Dee works on Sun, a day off.
2 shifts are empty: Sun evening and Sun night.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 4
day> Sun
shift> morning
Cleared Sun morning (was Dee).

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 7
Every rule holds.
3 shifts are empty: Sun morning, Sun evening and Sun night.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 6
       morning  evening  night
Mon    Ana      Cy       Bo
Tue    Dee      Ana      Bo
Wed    Cy       Dee      Ana
Thu    Bo       Cy       Dee
Fri    Ana      Bo       Cy
Sat    Dee      Bo       Cy
Sun    -        -        -
Shifts: Ana 4, Bo 5, Cy 5 and Dee 4.

1) add a person  2) days off  3) assign a shift  4) clear a shift
5) fill automatically  6) show the roster  7) check the rules  8) quit
choice> 8
Bye.
What was typed (46 lines)
1
Ana
1
Bo
1
Cy
1
Dee
2
Ana
Sat Sun
2
dee
mon
3
Bo
Mon
night
3
bo
tue
m
3
Bo
Tuesday
evening
3
Bo
Tue
n
3
Ana
Sat
morning
5
7
2
Dee
Mon, Sun
7
4
Sun
morning
7
6
8

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P031 shift roster: people, their days off, and a week of morning, evening
# and night shifts - assigned by hand or filled automatically - checked
# against the rules: one shift a day, 11 hours of rest, 5 shifts a week, no
# shift on a day off.
import rules

8 => most_people

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

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

def words(s):
    [] => out
    "" => w
    for c in s + " ":
        if c == " " or c == ",":
            if w != "":
                out + [w] => out
            "" => w
        else:
            w + c => w
    return out

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

def pad(s, width):
    while len(s) < width:
        s + " " => s
    return s

def without_trailing(s):
    len(s) => e
    while e > 0 and s[e - 1] == " ":
        e - 1 => e
    return s[0:e]

def day_of(s):
    lower(trim(s)) => s
    ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] => full
    for d in [0:6]:
        if s == full[d] or (len(s) == 3 and s == full[d][0:3]):
            return d
    return 0 - 1

def shift_of(s):
    lower(trim(s)) => s
    for t in [0:2]:
        if s == rules.shifts[t] or (len(s) == 1 and s == rules.shifts[t][0:1]):
            return t
    return 0 - 1

def person_of(names, s):
    for p in [0:len(names) - 1]:
        if lower(names[p]) == lower(trim(s)):
            return p
    return 0 - 1

def ask(prompt, kind, names):
    # A day, a shift or a person (by kind), or -1 when the answer is empty.
    while True:
        input(prompt) => answer
        if trim(answer) == "":
            return 0 - 1
        0 - 1 => found
        if kind == "day":
            day_of(answer) => found
        elif kind == "shift":
            shift_of(answer) => found
        else:
            person_of(names, answer) => found
        if found >= 0:
            return found
        if kind == "day":
            "Type a day, Mon to Sun." ^0
        elif kind == "shift":
            "Type morning, evening or night (or m, e, n)." ^0
        else:
            ("Type one of: " + listed(names) + ".") ^0

def show(roster, names):
    0 => width
    for n in names:
        if len(n) > width:
            len(n) => width
    if width < 7:
        7 => width
    "     " => top
    for t in [0:2]:
        top + "  " + pad(rules.shifts[t], width) => top
    without_trailing(top) ^0
    for d in [0:6]:
        pad(rules.days[d], 5) => line
        for t in [0:2]:
            "-" => who
            if roster[d][t] >= 0:
                names[roster[d][t]] => who
            line + "  " + pad(who, width) => line
        without_trailing(line) ^0
    if len(names) == 0:
        "No people yet." ^0
        return
    [] => counts
    for p in [0:len(names) - 1]:
        counts + [names[p] + " " + str(rules.shift_count(roster, p))] => counts
    ("Shifts: " + listed(counts) + ".") ^0

def empty_slots(roster):
    [] => out
    for d in [0:6]:
        for t in [0:2]:
            if roster[d][t] == 0 - 1:
                out + [rules.days[d] + " " + rules.shifts[t]] => out
    return out

def add_person(names, off):
    # Returns [names, off], with one more person or as they were.
    if len(names) >= most_people:
        ("There are already " + str(most_people) + " people, the most.") ^0
        return [names, off]
    while True:
        trim(input("name> ")) => name
        if name == "":
            "Cancelled." ^0
            return [names, off]
        True => good
        if len(name) > 12:
            False => good
        for c in name:
            if not (lower(c) in "abcdefghijklmnopqrstuvwxyz"):
                False => good
        if not good:
            "A name is 1 to 12 letters." ^0
        elif person_of(names, name) >= 0:
            ("There is already someone called " + names[person_of(names, name)] + ".") ^0
        else:
            ("Added " + name + ", with no days off.") ^0
            return [names + [name], off + [[False, False, False, False, False, False, False]]]

def set_days_off(roster, names, off):
    ask("person> ", "person", names) => p
    if p == 0 - 1:
        "Cancelled." ^0
        return
    while True:
        trim(input("days off (such as Sat Sun, or none)> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        [False, False, False, False, False, False, False] => chosen
        True => good
        if lower(answer) != "none":
            for w in words(answer):
                day_of(w) => d
                if d == 0 - 1:
                    False => good
                else:
                    True => chosen[d]
        if good:
            chosen => off[p]
            [] => named
            for d in [0:6]:
                if chosen[d]:
                    named + [rules.days[d]] => named
            if len(named) == 0:
                (names[p] + " has no days off.") ^0
            else:
                (names[p] + " is off on " + listed(named) + ".") ^0
            for d in [0:6]:
                for t in [0:2]:
                    if chosen[d] and roster[d][t] == p:
                        (names[p] + " is still on " + rules.days[d] + " " + rules.shifts[t] + "; the check (7) will report it.") ^0
            return
        "Type day names such as Mon Tue, or none." ^0

def assign(roster, names, off):
    if len(names) == 0:
        "Add a person first (1)." ^0
        return
    ask("person> ", "person", names) => p
    if p == 0 - 1:
        "Cancelled." ^0
        return
    ask("day> ", "day", names) => d
    if d == 0 - 1:
        "Cancelled." ^0
        return
    ask("shift> ", "shift", names) => s
    if s == 0 - 1:
        "Cancelled." ^0
        return
    roster[d][s] => there
    if there == p:
        (names[p] + " already has " + rules.days[d] + " " + rules.shifts[s] + ".") ^0
        return
    if there >= 0:
        (rules.days[d] + " " + rules.shifts[s] + " is " + names[there] + "'s; clear it first (4).") ^0
        return
    rules.reasons_against(roster, off, names, p, d, s) => why
    if len(why) > 0:
        (names[p] + " cannot take " + rules.days[d] + " " + rules.shifts[s] + ":") ^0
        for w in why:
            ("  " + w) ^0
        return
    p => roster[d][s]
    (rules.days[d] + " " + rules.shifts[s] + ": " + names[p] + ".") ^0

def clear(roster, names):
    ask("day> ", "day", names) => d
    if d == 0 - 1:
        "Cancelled." ^0
        return
    ask("shift> ", "shift", names) => s
    if s == 0 - 1:
        "Cancelled." ^0
        return
    if roster[d][s] == 0 - 1:
        (rules.days[d] + " " + rules.shifts[s] + " is empty already.") ^0
    else:
        ("Cleared " + rules.days[d] + " " + rules.shifts[s] + " (was " + names[roster[d][s]] + ").") ^0
        0 - 1 => roster[d][s]

def check(roster, names, off):
    rules.audit(roster, off, names) => breaks
    if len(breaks) == 0:
        "Every rule holds." ^0
    else:
        if len(breaks) == 1:
            "1 rule break:" ^0
        else:
            (str(len(breaks)) + " rule breaks:") ^0
        for b in breaks:
            ("  " + b) ^0
    empty_slots(roster) => left
    if len(left) == 0:
        "Every shift is covered." ^0
    elif len(left) == 1:
        ("1 shift is empty: " + left[0] + ".") ^0
    elif len(left) <= 6:
        (str(len(left)) + " shifts are empty: " + listed(left) + ".") ^0
    else:
        (str(len(left)) + " shifts are empty: " + listed(left[0:6] + [str(len(left) - 6) + " more"]) + ".") ^0

"== Shift roster ==" ^0
"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
[] => names
[] => off
rules.empty_roster() => roster
True => running
while running:
    "" ^0
    "1) add a person  2) days off  3) assign a shift  4) clear a shift" ^0
    "5) fill automatically  6) show the roster  7) check the rules  8) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        add_person(names, off) => added
        added[0] => names
        added[1] => off
    elif choice == "8":
        False => running
    elif choice == "2" or choice == "3" or choice == "4" or choice == "5" or choice == "6" or choice == "7":
        if len(names) == 0 and choice != "6" and choice != "7":
            "Add a person first (1)." ^0
        elif choice == "2":
            set_days_off(roster, names, off)
        elif choice == "3":
            assign(roster, names, off)
        elif choice == "4":
            clear(roster, names)
        elif choice == "5":
            rules.fill(roster, off, names) => result
            result[0] => roster
            ("Filled " + str(result[1]) + " shifts.") ^0
            if len(result[2]) > 0:
                ("Left empty - nobody the rules allow: " + listed(result[2]) + ".") ^0
            show(roster, names)
        elif choice == "6":
            show(roster, names)
        else:
            check(roster, names, off)
    else:
        "Pick a number from 1 to 8." ^0
"Bye." ^0
Python projection (main.py)
import rules
most_people = 8

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

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

def words(s):
    out = []
    w = ""
    for c in s + " ":
        if c == " " or c == ",":
            if w != "":
                out = out + [w]
            w = ""
        else:
            w = w + c
    return out

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

def pad(s, width):
    while len(s) < width:
        s = s + " "
    return s

def without_trailing(s):
    e = len(s)
    while e > 0 and s[e - 1] == " ":
        e = e - 1
    return s[0:e]

def day_of(s):
    s = lower(trim(s))
    full = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
    for d in range(0, 7):
        if s == full[d] or len(s) == 3 and s == full[d][0:3]:
            return d
    return 0 - 1

def shift_of(s):
    s = lower(trim(s))
    for t in range(0, 3):
        if s == rules.shifts[t] or len(s) == 1 and s == rules.shifts[t][0:1]:
            return t
    return 0 - 1

def person_of(names, s):
    for p in range(0, len(names)):
        if lower(names[p]) == lower(trim(s)):
            return p
    return 0 - 1

def ask(prompt, kind, names):
    while True:
        answer = input(prompt)
        if trim(answer) == "":
            return 0 - 1
        found = 0 - 1
        if kind == "day":
            found = day_of(answer)
        elif kind == "shift":
            found = shift_of(answer)
        else:
            found = person_of(names, answer)
        if found >= 0:
            return found
        if kind == "day":
            print("Type a day, Mon to Sun.")
        elif kind == "shift":
            print("Type morning, evening or night (or m, e, n).")
        else:
            print("Type one of: " + listed(names) + ".")

def show(roster, names):
    width = 0
    for n in names:
        if len(n) > width:
            width = len(n)
    if width < 7:
        width = 7
    top = "     "
    for t in range(0, 3):
        top = top + "  " + pad(rules.shifts[t], width)
    print(without_trailing(top))
    for d in range(0, 7):
        line = pad(rules.days[d], 5)
        for t in range(0, 3):
            who = "-"
            if roster[d][t] >= 0:
                who = names[roster[d][t]]
            line = line + "  " + pad(who, width)
        print(without_trailing(line))
    if len(names) == 0:
        print("No people yet.")
        return
    counts = []
    for p in range(0, len(names)):
        counts = counts + [names[p] + " " + str(rules.shift_count(roster, p))]
    print("Shifts: " + listed(counts) + ".")

def empty_slots(roster):
    out = []
    for d in range(0, 7):
        for t in range(0, 3):
            if roster[d][t] == 0 - 1:
                out = out + [rules.days[d] + " " + rules.shifts[t]]
    return out

def add_person(names, off):
    if len(names) >= most_people:
        print("There are already " + str(most_people) + " people, the most.")
        return [names, off]
    while True:
        name = trim(input("name> "))
        if name == "":
            print("Cancelled.")
            return [names, off]
        good = True
        if len(name) > 12:
            good = False
        for c in name:
            if not lower(c) in "abcdefghijklmnopqrstuvwxyz":
                good = False
        if not good:
            print("A name is 1 to 12 letters.")
        elif person_of(names, name) >= 0:
            print("There is already someone called " + names[person_of(names, name)] + ".")
        else:
            print("Added " + name + ", with no days off.")
            return [names + [name], off + [[False, False, False, False, False, False, False]]]

def set_days_off(roster, names, off):
    p = ask("person> ", "person", names)
    if p == 0 - 1:
        print("Cancelled.")
        return
    while True:
        answer = trim(input("days off (such as Sat Sun, or none)> "))
        if answer == "":
            print("Cancelled.")
            return
        chosen = [False, False, False, False, False, False, False]
        good = True
        if lower(answer) != "none":
            for w in words(answer):
                d = day_of(w)
                if d == 0 - 1:
                    good = False
                else:
                    chosen[d] = True
        if good:
            off[p] = chosen
            named = []
            for d in range(0, 7):
                if chosen[d]:
                    named = named + [rules.days[d]]
            if len(named) == 0:
                print(names[p] + " has no days off.")
            else:
                print(names[p] + " is off on " + listed(named) + ".")
            for d in range(0, 7):
                for t in range(0, 3):
                    if chosen[d] and roster[d][t] == p:
                        print(names[p] + " is still on " + rules.days[d] + " " + rules.shifts[t] + "; the check (7) will report it.")
            return
        print("Type day names such as Mon Tue, or none.")

def assign(roster, names, off):
    if len(names) == 0:
        print("Add a person first (1).")
        return
    p = ask("person> ", "person", names)
    if p == 0 - 1:
        print("Cancelled.")
        return
    d = ask("day> ", "day", names)
    if d == 0 - 1:
        print("Cancelled.")
        return
    s = ask("shift> ", "shift", names)
    if s == 0 - 1:
        print("Cancelled.")
        return
    there = roster[d][s]
    if there == p:
        print(names[p] + " already has " + rules.days[d] + " " + rules.shifts[s] + ".")
        return
    if there >= 0:
        print(rules.days[d] + " " + rules.shifts[s] + " is " + names[there] + "'s; clear it first (4).")
        return
    why = rules.reasons_against(roster, off, names, p, d, s)
    if len(why) > 0:
        print(names[p] + " cannot take " + rules.days[d] + " " + rules.shifts[s] + ":")
        for w in why:
            print("  " + w)
        return
    roster[d][s] = p
    print(rules.days[d] + " " + rules.shifts[s] + ": " + names[p] + ".")

def clear(roster, names):
    d = ask("day> ", "day", names)
    if d == 0 - 1:
        print("Cancelled.")
        return
    s = ask("shift> ", "shift", names)
    if s == 0 - 1:
        print("Cancelled.")
        return
    if roster[d][s] == 0 - 1:
        print(rules.days[d] + " " + rules.shifts[s] + " is empty already.")
    else:
        print("Cleared " + rules.days[d] + " " + rules.shifts[s] + " (was " + names[roster[d][s]] + ").")
        roster[d][s] = 0 - 1

def check(roster, names, off):
    breaks = rules.audit(roster, off, names)
    if len(breaks) == 0:
        print("Every rule holds.")
    else:
        if len(breaks) == 1:
            print("1 rule break:")
        else:
            print(str(len(breaks)) + " rule breaks:")
        for b in breaks:
            print("  " + b)
    left = empty_slots(roster)
    if len(left) == 0:
        print("Every shift is covered.")
    elif len(left) == 1:
        print("1 shift is empty: " + left[0] + ".")
    elif len(left) <= 6:
        print(str(len(left)) + " shifts are empty: " + listed(left) + ".")
    else:
        print(str(len(left)) + " shifts are empty: " + listed(left[0:6] + [str(len(left) - 6) + " more"]) + ".")

print("== Shift roster ==")
print("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.")
names = []
off = []
roster = rules.empty_roster()
running = True
while running:
    print("")
    print("1) add a person  2) days off  3) assign a shift  4) clear a shift")
    print("5) fill automatically  6) show the roster  7) check the rules  8) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        added = add_person(names, off)
        names = added[0]
        off = added[1]
    elif choice == "8":
        running = False
    elif choice == "2" or choice == "3" or choice == "4" or choice == "5" or choice == "6" or choice == "7":
        if len(names) == 0 and choice != "6" and choice != "7":
            print("Add a person first (1).")
        elif choice == "2":
            set_days_off(roster, names, off)
        elif choice == "3":
            assign(roster, names, off)
        elif choice == "4":
            clear(roster, names)
        elif choice == "5":
            result = rules.fill(roster, off, names)
            roster = result[0]
            print("Filled " + str(result[1]) + " shifts.")
            if len(result[2]) > 0:
                print("Left empty - nobody the rules allow: " + listed(result[2]) + ".")
            show(roster, names)
        elif choice == "6":
            show(roster, names)
        else:
            check(roster, names, off)
    else:
        print("Pick a number from 1 to 8.")
print("Bye.")

rules.eml

eml
# P031 shift roster - the week, the shifts and the rules. The roster is 7
# days of 3 shifts, each holding a person's index or -1; off[p] is that
# person's 7 days, True where the day is off.
#
# The rules: one shift a day at most; at least 11 hours of rest between the
# end of one shift and the start of the next; at most 5 shifts a week; no
# shift on a day off. The night shift runs from 22:00 to 06:00 the next day.
#
# As in the corpus case shift-roster-constraints, checking one assignment
# (reasons_against) and auditing a finished roster (audit) are written
# separately: the audit works from the roster alone, by putting each
# person's shifts in time order and measuring the gaps.

["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] => days
["morning", "evening", "night"] => shifts
[6, 14, 22] => starts
[14, 22, 30] => ends
11 => least_rest
5 => most_shifts

def shift_count(roster, p):
    0 => n
    for day in roster:
        for who in day:
            if who == p:
                n + 1 => n
    return n

def empty_roster():
    [] => out
    for d in [0:6]:
        out + [[0 - 1, 0 - 1, 0 - 1]] => out
    return out

def reasons_against(roster, off, names, p, d, s):
    # Why person p cannot take shift s on day d, as sentences; an empty list
    # means p can. The slot itself is not looked at: the caller decides what
    # to do when it is someone else's.
    names[p] => name
    [] => out
    if off[p][d]:
        out + [name + " is off on " + days[d] + "."] => out
    for t in [0:2]:
        if t != s and roster[d][t] == p:
            out + [name + " already works " + days[d] + " " + shifts[t] + "."] => out
    if d > 0:
        for t in [0:2]:
            if roster[d - 1][t] == p:
                24 + starts[s] - ends[t] => rest
                if rest < least_rest:
                    out + [name + " works " + days[d - 1] + " " + shifts[t] + ": " + str(rest) + " hours of rest, at least " + str(least_rest) + "."] => out
    if d < 6:
        for t in [0:2]:
            if roster[d + 1][t] == p:
                24 + starts[t] - ends[s] => rest
                if rest < least_rest:
                    out + [name + " works " + days[d + 1] + " " + shifts[t] + ": " + str(rest) + " hours of rest, at least " + str(least_rest) + "."] => out
    shift_count(roster, p) => n
    if roster[d][s] == p:
        n - 1 => n
    if n >= most_shifts:
        out + [name + " already has " + str(n) + " shifts this week, the most."] => out
    return out

def audit(roster, off, names):
    # Every rule break in the roster, found from the roster alone.
    [] => out
    for p in [0:len(names) - 1]:
        # p's shifts as [start hour, end hour, day, shift], in time order
        [] => mine
        for d in [0:6]:
            for s in [0:2]:
                if roster[d][s] == p:
                    mine + [[24 * d + starts[s], 24 * d + ends[s], d, s]] => mine
        for d in [0:6]:
            0 => that_day
            for m in mine:
                if m[2] == d:
                    that_day + 1 => that_day
            if that_day > 1:
                out + [names[p] + " works " + str(that_day) + " shifts on " + days[d] + "."] => out
            if off[p][d] and that_day > 0:
                out + [names[p] + " works on " + days[d] + ", a day off."] => out
        for i in [1:len(mine) - 1]:
            mine[i][0] - mine[i - 1][1] => gap
            if gap < least_rest:
                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
        if len(mine) > most_shifts:
            out + [names[p] + " has " + str(len(mine)) + " shifts, at most " + str(most_shifts) + "."] => out
    return out

def fill(roster, off, names):
    # Every empty shift, in time order, goes to the person with the fewest
    # shifts so far among those the rules allow (the first added on a tie).
    # Returns [roster, filled, left empty as "Day shift"].
    0 => filled
    [] => left
    for d in [0:6]:
        for s in [0:2]:
            if roster[d][s] == 0 - 1:
                0 - 1 => best
                for p in [0:len(names) - 1]:
                    if len(reasons_against(roster, off, names, p, d, s)) == 0:
                        if best == 0 - 1 or shift_count(roster, p) < shift_count(roster, best):
                            p => best
                if best == 0 - 1:
                    left + [days[d] + " " + shifts[s]] => left
                else:
                    best => roster[d][s]
                    filled + 1 => filled
    return [roster, filled, left]
Python projection (rules.py)
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
shifts = ["morning", "evening", "night"]
starts = [6, 14, 22]
ends = [14, 22, 30]
least_rest = 11
most_shifts = 5

def shift_count(roster, p):
    n = 0
    for day in roster:
        for who in day:
            if who == p:
                n = n + 1
    return n

def empty_roster():
    out = []
    for d in range(0, 7):
        out = out + [[0 - 1, 0 - 1, 0 - 1]]
    return out

def reasons_against(roster, off, names, p, d, s):
    name = names[p]
    out = []
    if off[p][d]:
        out = out + [name + " is off on " + days[d] + "."]
    for t in range(0, 3):
        if t != s and roster[d][t] == p:
            out = out + [name + " already works " + days[d] + " " + shifts[t] + "."]
    if d > 0:
        for t in range(0, 3):
            if roster[d - 1][t] == p:
                rest = 24 + starts[s] - ends[t]
                if rest < least_rest:
                    out = out + [name + " works " + days[d - 1] + " " + shifts[t] + ": " + str(rest) + " hours of rest, at least " + str(least_rest) + "."]
    if d < 6:
        for t in range(0, 3):
            if roster[d + 1][t] == p:
                rest = 24 + starts[t] - ends[s]
                if rest < least_rest:
                    out = out + [name + " works " + days[d + 1] + " " + shifts[t] + ": " + str(rest) + " hours of rest, at least " + str(least_rest) + "."]
    n = shift_count(roster, p)
    if roster[d][s] == p:
        n = n - 1
    if n >= most_shifts:
        out = out + [name + " already has " + str(n) + " shifts this week, the most."]
    return out

def audit(roster, off, names):
    out = []
    for p in range(0, len(names)):
        mine = []
        for d in range(0, 7):
            for s in range(0, 3):
                if roster[d][s] == p:
                    mine = mine + [[24 * d + starts[s], 24 * d + ends[s], d, s]]
        for d in range(0, 7):
            that_day = 0
            for m in mine:
                if m[2] == d:
                    that_day = that_day + 1
            if that_day > 1:
                out = out + [names[p] + " works " + str(that_day) + " shifts on " + days[d] + "."]
            if off[p][d] and that_day > 0:
                out = out + [names[p] + " works on " + days[d] + ", a day off."]
        for i in range(1, len(mine)):
            gap = mine[i][0] - mine[i - 1][1]
            if gap < least_rest:
                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) + "."]
        if len(mine) > most_shifts:
            out = out + [names[p] + " has " + str(len(mine)) + " shifts, at most " + str(most_shifts) + "."]
    return out

def fill(roster, off, names):
    filled = 0
    left = []
    for d in range(0, 7):
        for s in range(0, 3):
            if roster[d][s] == 0 - 1:
                best = 0 - 1
                for p in range(0, len(names)):
                    if len(reasons_against(roster, off, names, p, d, s)) == 0:
                        if best == 0 - 1 or shift_count(roster, p) < shift_count(roster, best):
                            best = p
                if best == 0 - 1:
                    left = left + [days[d] + " " + shifts[s]]
                else:
                    roster[d][s] = best
                    filled = filled + 1
    return [roster, filled, left]

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.