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.
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 screenrules.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]