<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P162-timesheet/ | updated: 2026-10-19 -->

# P162 Timesheet

A week of shifts and hours over 8 in a day paid at time and a half, with the day read as the shift, the shop's calendar day and the payroll server's: every midnight that falls inside a long shift pays less.

EML-P project `projects/timesheet` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/timesheet` runs it and `eml project verify projects/timesheet` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: the-counter-reset-at-midnight-and-the-shift-crossed-it (https://efficientnewlanguage.org/cases/645-the-counter-reset-at-midnight-and-the-shift-crossed-it/), the-day-boundary-was-the-servers-and-the-user-was-elsewhere (https://efficientnewlanguage.org/cases/809-the-day-boundary-was-the-servers-and-the-user-was-elsewhere/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
3

3
0
3
6
3
x
4

4
24
4
x
4
9

4
9
0
4
9
17
4
9
x
4
2
12
4
9
6
5
```

Screen:

```text
== Timesheet ==
A week of shifts, and hours over 8 in a day paid at time and a half - by which day?

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 0
Pick a number from 1 to 5.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> x
Pick a number from 1 to 5.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 3
worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> 
Cancelled.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 3
worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> 0
Type a number from 1 to 5.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 3
worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> 6
Type a number from 1 to 5.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 3
worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> x
Type a number from 1 to 5.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 
Cancelled.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 24
Type a whole number from 0 to 23.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> x
Type a whole number from 0 to 23.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 9
hours (1 to 16)> 
Cancelled.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 9
hours (1 to 16)> 0
Type a whole number from 1 to 16.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 9
hours (1 to 16)> 17
Type a whole number from 1 to 16.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 9
hours (1 to 16)> x
Type a whole number from 1 to 16.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 2
hours (1 to 16)> 12
A Wednesday shift 02:00-14:00, 12 hours:
  by the shift:        4 over 8
  by the shop's day:   Wed 12 - 4 over 8
  by the server's day: Tue 7, Wed 5 - 0 over 8
The server's midnight, 09:00 here, cuts it in two.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 9
hours (1 to 16)> 6
A Wednesday shift 09:00-15:00, 6 hours:
  by the shift:        0 over 8
  by the shop's day:   Wed 6 - 0 over 8
  by the server's day: Wed 6 - 0 over 8
No more than 8 hours: no reading pays any at time and a half.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 5
Bye.
```

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

Input:

```text
1
2
3
5
3
4
4
22
10
4
23
11
4
10
10
5
```

Screen:

```text
== Timesheet ==
A week of shifts, and hours over 8 in a day paid at time and a half - by which day?

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 1
The week in the shop's time, UTC+9. The payroll server keeps UTC, so its midnight is 09:00 here.
Hours over 8 in a day are paid at time and a half: 20.00 an hour, 30.00 for each hour over 8.
  Ana   Mon Tue Wed Thu Fri 09:00-19:00, 10 hours
  Ben   Mon Tue Wed Thu Fri 22:00-08:00, 10 hours
  Cleo  Mon Tue Wed Thu Fri 15:00-01:00, 10 hours
  Dan   Mon 07:00-23:00, 16 hours; Wed Thu Fri 07:00-15:00, 8 hours
  Eve   Mon Wed Fri 20:00-08:00, 12 hours

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 2
Hours over 8 in a day, with the day read three ways:
  worker  hours  by shift  shop day  server day
  Ana        50        10        10          10
  Ben        50        10         8          10
  Cleo       50        10         9          10
  Dan        40         8         8           6
  Eve        36        12         0          12
  all       226        50        35          48
Pay for the week: 5,020.00 by the shift, 4,870.00 by the shop's day, 5,000.00 by the server's day.
Hours over 8 paid as plain hours by the shop's day: Ben 2, Cleo 1 and Eve 12. A shift that crosses midnight starts its count again at 0.
Hours over 8 paid as plain hours by the server's day: Dan 2. The server's midnight is 09:00 here, and it cuts the shifts that cross it.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 3
worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> 5
Eve by the shift:        Mon 12, Wed 12, Fri 12 - 12 hours over 8
Eve by the shop's day:   Mon 4, Tue 8, Wed 4, Thu 8, Fri 4, Sat 8 - 0 hours over 8
Eve by the server's day: Mon 12, Wed 12, Fri 12 - 12 hours over 8

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 3
worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> 4
Dan by the shift:        Mon 16, Wed 8, Thu 8, Fri 8 - 8 hours over 8
Dan by the shop's day:   Mon 16, Wed 8, Thu 8, Fri 8 - 8 hours over 8
Dan by the server's day: Sun 2, Mon 14, Tue 2, Wed 8, Thu 8, Fri 6 - 6 hours over 8

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 22
hours (1 to 16)> 10
A Wednesday shift 22:00-08:00, 10 hours:
  by the shift:        2 over 8
  by the shop's day:   Wed 2, Thu 8 - 0 over 8
  by the server's day: Wed 10 - 2 over 8
Midnight in the shop cuts it in two, and each part counts from 0.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 23
hours (1 to 16)> 11
A Wednesday shift 23:00-10:00, 11 hours:
  by the shift:        3 over 8
  by the shop's day:   Wed 1, Thu 10 - 2 over 8
  by the server's day: Wed 10, Thu 1 - 2 over 8
Midnight in the shop cuts it in two, and each part counts from 0.
The server's midnight, 09:00 here, cuts it in two.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 4
start hour (0 to 23)> 10
hours (1 to 16)> 10
A Wednesday shift 10:00-20:00, 10 hours:
  by the shift:        2 over 8
  by the shop's day:   Wed 10 - 2 over 8
  by the server's day: Wed 10 - 2 over 8
No midnight falls inside it: every reading pays the same.

1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P162 timesheet: a week of shifts and "hours over 8 in a day are paid at
# time and a half" - the day read as the shift, the shop's calendar day and
# the payroll server's day; one worker's days, and a shift of your own.
import shifts

def plain(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 pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def money(cents):
    cents % 100 => c
    int((cents - c) / 100) => w
    str(w) => s
    if w >= 1000:
        str(int((w - w % 1000) / 1000)) + "," + str(w % 1000 + 1000)[1:] => s
    if c < 10:
        return s + ".0" + str(c)
    return s + "." + str(c)

def clock(h):
    h % 24 => h
    if h < 10:
        return "0" + str(h) + ":00"
    return str(h) + ":00"

def span(s):
    return clock(s[1]) + "-" + clock(s[1] + s[2]) + ", " + str(s[2]) + " hours"

def show_shifts():
    "The week in the shop's time, UTC+9. The payroll server keeps UTC, so its midnight is 09:00 here." ^0
    "Hours over 8 in a day are paid at time and a half: 20.00 an hour, 30.00 for each hour over 8." ^0
    for w in shifts.workers():
        # shifts with the same hours, grouped: [start, hours, days]
        [] => groups
        for s in w[1]:
            False => found
            for g in groups:
                if g[0] == s[1] and g[1] == s[2]:
                    g[2] + " " + shifts.day_name(s[0]) => g[2]
                    True => found
            if not found:
                groups + [[s[1], s[2], shifts.day_name(s[0])]] => groups
        "" => line
        for g in groups:
            if line != "":
                line + "; " => line
            line + g[2] + " " + span([0, g[0], g[1]]) => line
        ("  " + pad(w[0], 6) + line) ^0

def readings(w):
    # [hours, over by shift, over by the shop's day, over by the server's day]
    return [shifts.total(w[1]), shifts.over_shifts(w[1]), shifts.over_days(shifts.days(w[1], False)), shifts.over_days(shifts.days(w[1], True))]

def names(list):
    # "Ben 2, Cleo 1 and Eve 12"
    "" => out
    0 => i
    while i < len(list):
        if i > 0 and i == len(list) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + list[i] => out
        i + 1 => i
    return out

def show_overtime():
    "Hours over 8 in a day, with the day read three ways:" ^0
    ("  " + pad("worker", 8) + lpad("hours", 5) + lpad("by shift", 10) + lpad("shop day", 10) + lpad("server day", 12)) ^0
    [0, 0, 0, 0] => sums
    [] => lost_shop
    [] => lost_server
    for w in shifts.workers():
        readings(w) => r
        ("  " + pad(w[0], 8) + lpad(str(r[0]), 5) + lpad(str(r[1]), 10) + lpad(str(r[2]), 10) + lpad(str(r[3]), 12)) ^0
        0 => k
        while k < 4:
            sums[k] + r[k] => sums[k]
            k + 1 => k
        if r[2] < r[1]:
            lost_shop + [w[0] + " " + str(r[1] - r[2])] => lost_shop
        if r[3] < r[1]:
            lost_server + [w[0] + " " + str(r[1] - r[3])] => lost_server
    ("  " + pad("all", 8) + lpad(str(sums[0]), 5) + lpad(str(sums[1]), 10) + lpad(str(sums[2]), 10) + lpad(str(sums[3]), 12)) ^0
    ("Pay for the week: " + money(shifts.pay(sums[0], sums[1])) + " by the shift, " + money(shifts.pay(sums[0], sums[2])) + " by the shop's day, " + money(shifts.pay(sums[0], sums[3])) + " by the server's day.") ^0
    if len(lost_shop) > 0:
        ("Hours over 8 paid as plain hours by the shop's day: " + names(lost_shop) + ". A shift that crosses midnight starts its count again at 0.") ^0
    if len(lost_server) > 0:
        ("Hours over 8 paid as plain hours by the server's day: " + names(lost_server) + ". The server's midnight is 09:00 here, and it cuts the shifts that cross it.") ^0

def day_list(ds):
    "" => out
    for e in ds:
        if out != "":
            out + ", " => out
        out + shifts.day_name(e[0]) + " " + str(e[1]) => out
    return out

def show_worker():
    plain(input("worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) != 1 or not digits(t) or int(t) < 1 or int(t) > 5:
        "Type a number from 1 to 5." ^0
        return
    shifts.workers()[int(t) - 1] => w
    "" => line
    for s in w[1]:
        if line != "":
            line + ", " => line
        line + shifts.day_name(s[0]) + " " + str(s[2]) => line
    (w[0] + " by the shift:        " + line + " - " + str(shifts.over_shifts(w[1])) + " hours over 8") ^0
    shifts.days(w[1], False) => a
    (w[0] + " by the shop's day:   " + day_list(a) + " - " + str(shifts.over_days(a)) + " hours over 8") ^0
    shifts.days(w[1], True) => b
    (w[0] + " by the server's day: " + day_list(b) + " - " + str(shifts.over_days(b)) + " hours over 8") ^0

def own():
    plain(input("start hour (0 to 23)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 2 or not digits(t) or int(t) > 23:
        "Type a whole number from 0 to 23." ^0
        return
    plain(input("hours (1 to 16)> ")) => n
    if n == "":
        "Cancelled." ^0
        return
    if len(n) > 2 or not digits(n) or int(n) < 1 or int(n) > 16:
        "Type a whole number from 1 to 16." ^0
        return
    [[2, int(t), int(n)]] => one
    readings(["yours", one]) => r
    shifts.days(one, False) => a
    shifts.days(one, True) => b
    ("A Wednesday shift " + span(one[0]) + ":") ^0
    ("  by the shift:        " + str(r[1]) + " over 8") ^0
    ("  by the shop's day:   " + day_list(a) + " - " + str(r[2]) + " over 8") ^0
    ("  by the server's day: " + day_list(b) + " - " + str(r[3]) + " over 8") ^0
    if r[1] == 0:
        "No more than 8 hours: no reading pays any at time and a half." ^0
        return
    if r[2] < r[1]:
        "Midnight in the shop cuts it in two, and each part counts from 0." ^0
    if r[3] < r[1]:
        "The server's midnight, 09:00 here, cuts it in two." ^0
    if r[2] == r[1] and r[3] == r[1]:
        "No midnight falls inside it: every reading pays the same." ^0

"== Timesheet ==" ^0
"A week of shifts, and hours over 8 in a day paid at time and a half - by which day?" ^0
True => running
while running:
    "" ^0
    "1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_shifts()
    elif choice == "2":
        show_overtime()
    elif choice == "3":
        show_worker()
    elif choice == "4":
        own()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import shifts

def plain(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 pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def money(cents):
    c = cents % 100
    w = int((cents - c) / 100)
    s = str(w)
    if w >= 1000:
        s = str(int((w - w % 1000) / 1000)) + "," + str(w % 1000 + 1000)[1:]
    if c < 10:
        return s + ".0" + str(c)
    return s + "." + str(c)

def clock(h):
    h = h % 24
    if h < 10:
        return "0" + str(h) + ":00"
    return str(h) + ":00"

def span(s):
    return clock(s[1]) + "-" + clock(s[1] + s[2]) + ", " + str(s[2]) + " hours"

def show_shifts():
    print("The week in the shop's time, UTC+9. The payroll server keeps UTC, so its midnight is 09:00 here.")
    print("Hours over 8 in a day are paid at time and a half: 20.00 an hour, 30.00 for each hour over 8.")
    for w in shifts.workers():
        groups = []
        for s in w[1]:
            found = False
            for g in groups:
                if g[0] == s[1] and g[1] == s[2]:
                    g[2] = g[2] + " " + shifts.day_name(s[0])
                    found = True
            if not found:
                groups = groups + [[s[1], s[2], shifts.day_name(s[0])]]
        line = ""
        for g in groups:
            if line != "":
                line = line + "; "
            line = line + g[2] + " " + span([0, g[0], g[1]])
        print("  " + pad(w[0], 6) + line)

def readings(w):
    return [shifts.total(w[1]), shifts.over_shifts(w[1]), shifts.over_days(shifts.days(w[1], False)), shifts.over_days(shifts.days(w[1], True))]

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

def show_overtime():
    print("Hours over 8 in a day, with the day read three ways:")
    print("  " + pad("worker", 8) + lpad("hours", 5) + lpad("by shift", 10) + lpad("shop day", 10) + lpad("server day", 12))
    sums = [0, 0, 0, 0]
    lost_shop = []
    lost_server = []
    for w in shifts.workers():
        r = readings(w)
        print("  " + pad(w[0], 8) + lpad(str(r[0]), 5) + lpad(str(r[1]), 10) + lpad(str(r[2]), 10) + lpad(str(r[3]), 12))
        k = 0
        while k < 4:
            sums[k] = sums[k] + r[k]
            k = k + 1
        if r[2] < r[1]:
            lost_shop = lost_shop + [w[0] + " " + str(r[1] - r[2])]
        if r[3] < r[1]:
            lost_server = lost_server + [w[0] + " " + str(r[1] - r[3])]
    print("  " + pad("all", 8) + lpad(str(sums[0]), 5) + lpad(str(sums[1]), 10) + lpad(str(sums[2]), 10) + lpad(str(sums[3]), 12))
    print("Pay for the week: " + money(shifts.pay(sums[0], sums[1])) + " by the shift, " + money(shifts.pay(sums[0], sums[2])) + " by the shop's day, " + money(shifts.pay(sums[0], sums[3])) + " by the server's day.")
    if len(lost_shop) > 0:
        print("Hours over 8 paid as plain hours by the shop's day: " + names(lost_shop) + ". A shift that crosses midnight starts its count again at 0.")
    if len(lost_server) > 0:
        print("Hours over 8 paid as plain hours by the server's day: " + names(lost_server) + ". The server's midnight is 09:00 here, and it cuts the shifts that cross it.")

def day_list(ds):
    out = ""
    for e in ds:
        if out != "":
            out = out + ", "
        out = out + shifts.day_name(e[0]) + " " + str(e[1])
    return out

def show_worker():
    t = plain(input("worker (1 Ana, 2 Ben, 3 Cleo, 4 Dan, 5 Eve)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) != 1 or not digits(t) or int(t) < 1 or int(t) > 5:
        print("Type a number from 1 to 5.")
        return
    w = shifts.workers()[int(t) - 1]
    line = ""
    for s in w[1]:
        if line != "":
            line = line + ", "
        line = line + shifts.day_name(s[0]) + " " + str(s[2])
    print(w[0] + " by the shift:        " + line + " - " + str(shifts.over_shifts(w[1])) + " hours over 8")
    a = shifts.days(w[1], False)
    print(w[0] + " by the shop's day:   " + day_list(a) + " - " + str(shifts.over_days(a)) + " hours over 8")
    b = shifts.days(w[1], True)
    print(w[0] + " by the server's day: " + day_list(b) + " - " + str(shifts.over_days(b)) + " hours over 8")

def own():
    t = plain(input("start hour (0 to 23)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 2 or not digits(t) or int(t) > 23:
        print("Type a whole number from 0 to 23.")
        return
    n = plain(input("hours (1 to 16)> "))
    if n == "":
        print("Cancelled.")
        return
    if len(n) > 2 or not digits(n) or int(n) < 1 or int(n) > 16:
        print("Type a whole number from 1 to 16.")
        return
    one = [[2, int(t), int(n)]]
    r = readings(["yours", one])
    a = shifts.days(one, False)
    b = shifts.days(one, True)
    print("A Wednesday shift " + span(one[0]) + ":")
    print("  by the shift:        " + str(r[1]) + " over 8")
    print("  by the shop's day:   " + day_list(a) + " - " + str(r[2]) + " over 8")
    print("  by the server's day: " + day_list(b) + " - " + str(r[3]) + " over 8")
    if r[1] == 0:
        print("No more than 8 hours: no reading pays any at time and a half.")
        return
    if r[2] < r[1]:
        print("Midnight in the shop cuts it in two, and each part counts from 0.")
    if r[3] < r[1]:
        print("The server's midnight, 09:00 here, cuts it in two.")
    if r[2] == r[1] and r[3] == r[1]:
        print("No midnight falls inside it: every reading pays the same.")

print("== Timesheet ==")
print("A week of shifts, and hours over 8 in a day paid at time and a half - by which day?")
running = True
while running:
    print("")
    print("1) the shifts  2) overtime  3) one worker's days  4) a shift of your own  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_shifts()
    elif choice == "2":
        show_overtime()
    elif choice == "3":
        show_worker()
    elif choice == "4":
        own()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### shifts.eml

```eml
# P162 timesheet - a shop's week of shifts and the rule "hours over 8 in a
# day are paid at time and a half", read three ways. As in the corpus case
# the-counter-reset-at-midnight-and-the-shift-crossed-it, a count that
# returns to zero at midnight starts again in the middle of any shift that
# crosses it, so a long night shift is two short days. And as in
# the-day-boundary-was-the-servers-and-the-user-was-elsewhere, the payroll
# server's midnight is UTC, nine hours behind the shop: its day ends at 09:00
# here. Times are whole hours from Monday 00:00 in the shop.

def zone():
    # the shop is UTC+9
    return 9

def rate():
    # cents an hour, and for each hour over 8
    return 2000

def over_rate():
    return 3000

def day_name(k):
    # k = -1 is the Sunday before
    return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][k + 1]

def workers():
    # [name, shifts]; a shift is [day (0 = Monday), start hour, hours]
    return [["Ana", [[0, 9, 10], [1, 9, 10], [2, 9, 10], [3, 9, 10], [4, 9, 10]]],
            ["Ben", [[0, 22, 10], [1, 22, 10], [2, 22, 10], [3, 22, 10], [4, 22, 10]]],
            ["Cleo", [[0, 15, 10], [1, 15, 10], [2, 15, 10], [3, 15, 10], [4, 15, 10]]],
            ["Dan", [[0, 7, 16], [2, 7, 8], [3, 7, 8], [4, 7, 8]]],
            ["Eve", [[0, 20, 12], [2, 20, 12], [4, 20, 12]]]]

def day_of(h):
    # the whole day hour h falls in, h >= 0
    return int((h - h % 24) / 24)

def days(shifts, server):
    # hours worked on each day, as [[day, hours], ...] in order: the shop's
    # days, or the server's (UTC) days, where day -1 is the Sunday before
    [] => out
    for s in shifts:
        0 => k
        while k < s[2]:
            s[0] * 24 + s[1] + k => h
            if server:
                day_of(h + 24 - zone()) - 1 => d
            else:
                day_of(h) => d
            False => found
            for e in out:
                if e[0] == d:
                    e[1] + 1 => e[1]
                    True => found
            if not found:
                out + [[d, 1]] => out
            k + 1 => k
    return out

def over_days(ds):
    # hours over 8 in each day, summed
    0 => t
    for e in ds:
        if e[1] > 8:
            t + e[1] - 8 => t
    return t

def over_shifts(shifts):
    0 => t
    for s in shifts:
        if s[2] > 8:
            t + s[2] - 8 => t
    return t

def total(shifts):
    0 => t
    for s in shifts:
        t + s[2] => t
    return t

def pay(hours, over):
    # cents for a week of hours with that many over 8
    return (hours - over) * rate() + over * over_rate()
```

Python projection of shifts.eml:

```python
def zone():
    return 9

def rate():
    return 2000

def over_rate():
    return 3000

def day_name(k):
    return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][k + 1]

def workers():
    return [["Ana", [[0, 9, 10], [1, 9, 10], [2, 9, 10], [3, 9, 10], [4, 9, 10]]], ["Ben", [[0, 22, 10], [1, 22, 10], [2, 22, 10], [3, 22, 10], [4, 22, 10]]], ["Cleo", [[0, 15, 10], [1, 15, 10], [2, 15, 10], [3, 15, 10], [4, 15, 10]]], ["Dan", [[0, 7, 16], [2, 7, 8], [3, 7, 8], [4, 7, 8]]], ["Eve", [[0, 20, 12], [2, 20, 12], [4, 20, 12]]]]

def day_of(h):
    return int((h - h % 24) / 24)

def days(shifts, server):
    out = []
    for s in shifts:
        k = 0
        while k < s[2]:
            h = s[0] * 24 + s[1] + k
            if server:
                d = day_of(h + 24 - zone()) - 1
            else:
                d = day_of(h)
            found = False
            for e in out:
                if e[0] == d:
                    e[1] = e[1] + 1
                    found = True
            if not found:
                out = out + [[d, 1]]
            k = k + 1
    return out

def over_days(ds):
    t = 0
    for e in ds:
        if e[1] > 8:
            t = t + e[1] - 8
    return t

def over_shifts(shifts):
    t = 0
    for s in shifts:
        if s[2] > 8:
            t = t + s[2] - 8
    return t

def total(shifts):
    t = 0
    for s in shifts:
        t = t + s[2]
    return t

def pay(hours, over):
    return (hours - over) * rate() + over * over_rate()
```

## README

# P162 - Timesheet

A shop's week of shifts and the rule "hours over 8 in a day are paid at
time and a half", with the day read three ways.

- `main.eml` - the menu, the shifts, overtime by each reading, one
  worker's days, and a shift of your own
- `shifts.eml` - the workers and their shifts, the hours on each day of
  the shop or of the payroll server, and the hours over 8

How each part works:

- Five workers, in the shop's time (UTC+9): day shifts, night shifts,
  evening shifts, a double shift, and 12-hour nights. 20.00 an hour, 30.00
  for each hour over 8.
- As in the corpus case `the-counter-reset-at-midnight-and-the-shift-crossed-it`,
  a count that returns to zero at midnight starts again in the middle of
  every shift that crosses it. Eve's three 12-hour nights are 12 hours over
  8 by the shift and none by the shop's day: midnight splits each into 4
  and 8. By the shop's day Ben, Cleo and Eve lose 15 hours of overtime.
- As in `the-day-boundary-was-the-servers-and-the-user-was-elsewhere`, the
  payroll server's day is UTC, which ends at 09:00 in the shop: it keeps
  the night shifts whole and cuts Dan's 07:00 starts instead, 2 hours.
- Pay for the week: 5,020.00 by the shift, 4,870.00 by the shop's day,
  5,000.00 by the server's day. A shift of 8 hours or less pays the same
  under every reading; a longer one pays less under any reading whose
  midnight falls inside it.

What is checked: menu choices 1 to 5; a worker from 1 to 5; a start hour
from 0 to 23 and 1 to 16 hours. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the shifts and the overtime;
- Eve's days and Dan's days;
- your own shifts from 22:00 for 10 hours, from 23:00 for 11 and from
  10:00 for 10.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- workers: a cancel, 0, 6 and x;
- start hours: a cancel, 24 and x; then hours with a cancel, 0, 17 and x;
- shifts from 02:00 for 12 hours, which only the server's midnight cuts,
  and from 09:00 for 6.

Built on the verified corpus cases `the-counter-reset-at-midnight-and-the-shift-crossed-it`
(a daily count reset at midnight, two hours into the night shift) and
`the-day-boundary-was-the-servers-and-the-user-was-elsewhere` (a daily count
by the server's midnight, with the users nine hours ahead).
