Fixed-width report
Payroll lines in fixed columns, shown under a ruler: every line's length checked before its columns are trusted, every error reported at once (and what stopping at the first would have said), and pay added up by department in whole cents.
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
Payroll lines in fixed columns, shown under a column ruler. Each line's length is checked before its columns are trusted, every error is reported at once - with what stopping at the first error would have said - and pay is added up by department in whole cents. It starts with nine lines, five of them bad.
main.eml- the menu, the ruler, the checks, the report, adding and removing linesrecords.eml- the columns, the field checks, the check of all lines, the first error, and pay
How each part works:
- A line is exactly 42 characters: id in columns 0-6 (six digits), name 6-26 (letters, spaces, - ' and .), department 26-30 (four capital letters), hours 30-35 (like 037.5, at most 168.0) and rate 35-42 (like 0025.50, more than 0).
- As the corpus case
fixed-width-record-parserwarns, a short line still slices without complaint - into wrong fields. So the length is checked first, and a line of the wrong length is reported on its own; its columns are not read at all. - As in the corpus case
schema-validator-all-errors, every error of every line is listed, not just the first: one round of fixes instead of one per error. The program also shows the single error that stopping at the first would have reported. A repeated id is an error on the later line. - Pay is hours (in tenths) x rate (in cents) / 10, rounded half up to the cent: 42.5 h x 31.75 = 1,349.375, paid as 1,349.38. Only good lines are paid, by department and in all.
What is checked: menu choices 1 to 6; a new line of any text (it is added, with its errors listed); a line number that exists; at most 20 lines. An empty answer cancels.
Sessions: sessions/basic.in gives: - the nine lines under the ruler, and their seven errors on five lines; - the pay report: SALE 1,845.00, TECH 2,466.58, 4,311.58 in all; - a good line added for Jo Kim and paid; the short line removed, leaving six errors on four lines.
sessions/bad-input.in gives: - menu choices 0 and x; - "abc" added (3 characters), and a line with five errors in one go; - line numbers 0, 99 and x to remove, then cancels; - the check of all eleven lines: 13 errors on 7 of them.
Built on the verified corpus cases fixed-width-record-parser (check the line before trusting its slices) and schema-validator-all-errors (every failure at once, against stopping at the first).
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== Fixed-width report ==
Payroll lines in fixed columns: checked before they are trusted, every error at once.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 0
Pick a number from 1 to 6.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> x
Pick a number from 1 to 6.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 4
new line (42 characters, as on the ruler)> abc
Added, with 1 error:
is 3 characters, not 42 - its columns cannot be trusted
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 4
new line (42 characters, as on the ruler)> 1000aaBad Name 9 Sale999.9abcd.ef
Added, with 5 errors:
id '1000aa' is not six digits
name 'Bad Name 9' must start in column 6 and be letters, spaces, - ' or .
dept 'Sale' is not four capital letters
hours 999.9 is more than the 168 in a week
rate 'abcd.ef' is not like 0025.50
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 5
line number to remove> 0
There is no line 0.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 5
line number to remove> 99
There is no line 99.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 5
line number to remove> x
There is no line x.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 5
line number to remove>
Cancelled.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 4
new line (42 characters, as on the ruler)>
Cancelled.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 2
line 5: id '10000X' is not six digits
line 5: dept 'tech' is not four capital letters
line 6: hours 170.0 is more than the 168 in a week
line 7: dept 'OPS ' is not four capital letters
line 7: rate is 0
line 8: id 100002 is already on line 2
line 9: is 38 characters, not 42 - its columns cannot be trusted
line 10: is 3 characters, not 42 - its columns cannot be trusted
line 11: id '1000aa' is not six digits
line 11: name 'Bad Name 9' must start in column 6 and be letters, spaces, - ' or .
line 11: dept 'Sale' is not four capital letters
line 11: hours 999.9 is more than the 168 in a week
line 11: rate 'abcd.ef' is not like 0025.50
Checking everything: 13 errors on 7 of the 11 lines.
Stopping at the first would have said only: line 5: id '10000X' is not six digits - and the next run would find the next one.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 6
Bye.
What was typed (18 lines)
0
x
4
abc
4
1000aaBad Name 9 Sale999.9abcd.ef
5
0
5
99
5
x
5
4
2
6
basic
interpreter: byte-equal== Fixed-width report ==
Payroll lines in fixed columns: checked before they are trusted, every error at once.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 1
1 2 3 4
012345678901234567890123456789012345678901
1 100001Ana Lopez SALE040.00025.50
2 100002Ben O'Hara SALE037.50022.00
3 100003Chloe Dubois TECH042.50031.75
4 100004Dev Patel TECH038.00029.40
5 10000XEli Moss tech040.00024.00
6 100006Fay Lin SALE170.00025.00
7 100007Gus Young OPS 035.00000.00
8 100002Hana Ito TECH036.00027.50
9 100009Ivo Novak SALE040.0002
id name dept hours rate
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 2
line 5: id '10000X' is not six digits
line 5: dept 'tech' is not four capital letters
line 6: hours 170.0 is more than the 168 in a week
line 7: dept 'OPS ' is not four capital letters
line 7: rate is 0
line 8: id 100002 is already on line 2
line 9: is 38 characters, not 42 - its columns cannot be trusted
Checking everything: 7 errors on 5 of the 9 lines.
Stopping at the first would have said only: line 5: id '10000X' is not six digits - and the next run would find the next one.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 3
1 Ana Lopez, SALE: 040.0 h x 0025.50 = 1,020.00
2 Ben O'Hara, SALE: 037.5 h x 0022.00 = 825.00
3 Chloe Dubois, TECH: 042.5 h x 0031.75 = 1,349.38
4 Dev Patel, TECH: 038.0 h x 0029.40 = 1,117.20
SALE 1,845.00
TECH 2,466.58
all 4,311.58 from 4 good lines; 5 left out until they are fixed
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 4
new line (42 characters, as on the ruler)> 100010Jo Kim TECH040.00030.00
Added; it is good.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 3
1 Ana Lopez, SALE: 040.0 h x 0025.50 = 1,020.00
2 Ben O'Hara, SALE: 037.5 h x 0022.00 = 825.00
3 Chloe Dubois, TECH: 042.5 h x 0031.75 = 1,349.38
4 Dev Patel, TECH: 038.0 h x 0029.40 = 1,117.20
10 Jo Kim, TECH: 040.0 h x 0030.00 = 1,200.00
SALE 1,845.00
TECH 3,666.58
all 5,511.58 from 5 good lines; 5 left out until they are fixed
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 5
line number to remove> 9
Removed line 9.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 2
line 5: id '10000X' is not six digits
line 5: dept 'tech' is not four capital letters
line 6: hours 170.0 is more than the 168 in a week
line 7: dept 'OPS ' is not four capital letters
line 7: rate is 0
line 8: id 100002 is already on line 2
Checking everything: 6 errors on 4 of the 9 lines.
Stopping at the first would have said only: line 5: id '10000X' is not six digits - and the next run would find the next one.
1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit
choice> 6
Bye.
What was typed (10 lines)
1
2
3
4
100010Jo Kim TECH040.00030.00
3
5
9
2
6
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# P089 fixed-width report: payroll lines in fixed columns - shown under a
# column ruler, checked with every error listed (and what stopping at the
# first would have said), and paid out by department in whole cents.
import records
def money(c):
0 => w
c => r
while r >= 100:
r - 100 => r
w + 1 => w
"" => z
if r < 10:
"0" => z
# thousands
str(w) => s
"" => g
0 => k
len(s) - 1 => i
while i >= 0:
if k > 0 and k % 3 == 0:
"," + g => g
s[i] + g => g
k + 1 => k
i - 1 => i
return g + "." + z + str(r)
def lpad(s, n):
while len(s) < n:
" " + s => s
return s
def show(lines):
" " + " 1 2 3 4" => top
top ^0
" 012345678901234567890123456789012345678901" ^0
for i in [0:len(lines) - 1]:
(lpad(str(i + 1), 2) + " " + lines[i]) ^0
" id name dept hours rate" ^0
def check_all(lines):
records.check(lines) => all
0 => bad
0 => count
for i in [0:len(all) - 1]:
if len(all[i]) > 0:
bad + 1 => bad
for e in all[i]:
(" line " + str(i + 1) + ": " + e) ^0
count + 1 => count
if bad == 0:
("All " + str(len(lines)) + " lines are good.") ^0
return
("Checking everything: " + str(count) + " errors on " + str(bad) + " of the " + str(len(lines)) + " lines.") ^0
records.first_error(lines) => f
("Stopping at the first would have said only: line " + str(f[0] + 1) + ": " + f[1] + " - and the next run would find the next one.") ^0
def report(lines):
records.check(lines) => all
[] => depts
[] => sums
0 => total
0 => used
for i in [0:len(lines) - 1]:
if len(all[i]) == 0:
lines[i] => l
records.pay(l) => p
(lpad(str(i + 1), 2) + " " + records.trim_right(l[6:26]) + ", " + l[26:30] + ": " + l[30:35] + " h x " + l[35:42] + " = " + money(p)) ^0
-1 => k
for j in [0:len(depts) - 1]:
if depts[j] == l[26:30]:
j => k
if k == -1:
depts + [l[26:30]] => depts
sums + [p] => sums
else:
sums[k] + p => sums[k]
total + p => total
used + 1 => used
if used == 0:
"No good lines to pay." ^0
return
for j in [0:len(depts) - 1]:
(" " + depts[j] + " " + lpad(money(sums[j]), 12)) ^0
(" all " + lpad(money(total), 12) + " from " + str(used) + " good lines; " + str(len(lines) - used) + " left out until they are fixed") ^0
def add(lines):
if len(lines) == 20:
"There are 20 lines already." ^0
return lines
input("new line (42 characters, as on the ruler)> ") => s
if s == "":
"Cancelled." ^0
return lines
records.check(lines + [s]) => all
all[len(all) - 1] => errs
if len(errs) == 0:
"Added; it is good." ^0
else:
"errors" => w
if len(errs) == 1:
"error" => w
("Added, with " + str(len(errs)) + " " + w + ":") ^0
for e in errs:
(" " + e) ^0
return lines + [s]
def remove(lines):
input("line number to remove> ") => s
if s == "":
"Cancelled." ^0
return lines
0 => n
True => ok
if len(s) > 2:
False => ok
for c in s:
if c >= "0" and c <= "9":
n * 10 + int(c) => n
else:
False => ok
if not ok or n < 1 or n > len(lines):
("There is no line " + s + ".") ^0
return lines
[] => out
for i in [0:len(lines) - 1]:
if i != n - 1:
out + [lines[i]] => out
("Removed line " + str(n) + ".") ^0
return out
[
"100001Ana Lopez SALE040.00025.50",
"100002Ben O'Hara SALE037.50022.00",
"100003Chloe Dubois TECH042.50031.75",
"100004Dev Patel TECH038.00029.40",
"10000XEli Moss tech040.00024.00",
"100006Fay Lin SALE170.00025.00",
"100007Gus Young OPS 035.00000.00",
"100002Hana Ito TECH036.00027.50",
"100009Ivo Novak SALE040.0002",
] => lines
"== Fixed-width report ==" ^0
"Payroll lines in fixed columns: checked before they are trusted, every error at once." ^0
True => running
while running:
"" ^0
"1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit" ^0
input("choice> ") => choice
if choice == "1":
show(lines)
elif choice == "2":
check_all(lines)
elif choice == "3":
report(lines)
elif choice == "4":
add(lines) => lines
elif choice == "5":
remove(lines) => lines
elif choice == "6":
False => running
else:
"Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import records
def money(c):
w = 0
r = c
while r >= 100:
r = r - 100
w = w + 1
z = ""
if r < 10:
z = "0"
s = str(w)
g = ""
k = 0
i = len(s) - 1
while i >= 0:
if k > 0 and k % 3 == 0:
g = "," + g
g = s[i] + g
k = k + 1
i = i - 1
return g + "." + z + str(r)
def lpad(s, n):
while len(s) < n:
s = " " + s
return s
def show(lines):
top = " " + " 1 2 3 4"
print(top)
print(" 012345678901234567890123456789012345678901")
for i in range(0, len(lines)):
print(lpad(str(i + 1), 2) + " " + lines[i])
print(" id name dept hours rate")
def check_all(lines):
all = records.check(lines)
bad = 0
count = 0
for i in range(0, len(all)):
if len(all[i]) > 0:
bad = bad + 1
for e in all[i]:
print(" line " + str(i + 1) + ": " + e)
count = count + 1
if bad == 0:
print("All " + str(len(lines)) + " lines are good.")
return
print("Checking everything: " + str(count) + " errors on " + str(bad) + " of the " + str(len(lines)) + " lines.")
f = records.first_error(lines)
print("Stopping at the first would have said only: line " + str(f[0] + 1) + ": " + f[1] + " - and the next run would find the next one.")
def report(lines):
all = records.check(lines)
depts = []
sums = []
total = 0
used = 0
for i in range(0, len(lines)):
if len(all[i]) == 0:
l = lines[i]
p = records.pay(l)
print(lpad(str(i + 1), 2) + " " + records.trim_right(l[6:26]) + ", " + l[26:30] + ": " + l[30:35] + " h x " + l[35:42] + " = " + money(p))
k = -1
for j in range(0, len(depts)):
if depts[j] == l[26:30]:
k = j
if k == -1:
depts = depts + [l[26:30]]
sums = sums + [p]
else:
sums[k] = sums[k] + p
total = total + p
used = used + 1
if used == 0:
print("No good lines to pay.")
return
for j in range(0, len(depts)):
print(" " + depts[j] + " " + lpad(money(sums[j]), 12))
print(" all " + lpad(money(total), 12) + " from " + str(used) + " good lines; " + str(len(lines) - used) + " left out until they are fixed")
def add(lines):
if len(lines) == 20:
print("There are 20 lines already.")
return lines
s = input("new line (42 characters, as on the ruler)> ")
if s == "":
print("Cancelled.")
return lines
all = records.check(lines + [s])
errs = all[len(all) - 1]
if len(errs) == 0:
print("Added; it is good.")
else:
w = "errors"
if len(errs) == 1:
w = "error"
print("Added, with " + str(len(errs)) + " " + w + ":")
for e in errs:
print(" " + e)
return lines + [s]
def remove(lines):
s = input("line number to remove> ")
if s == "":
print("Cancelled.")
return lines
n = 0
ok = True
if len(s) > 2:
ok = False
for c in s:
if c >= "0" and c <= "9":
n = n * 10 + int(c)
else:
ok = False
if not ok or n < 1 or n > len(lines):
print("There is no line " + s + ".")
return lines
out = []
for i in range(0, len(lines)):
if i != n - 1:
out = out + [lines[i]]
print("Removed line " + str(n) + ".")
return out
lines = ["100001Ana Lopez SALE040.00025.50", "100002Ben O'Hara SALE037.50022.00", "100003Chloe Dubois TECH042.50031.75", "100004Dev Patel TECH038.00029.40", "10000XEli Moss tech040.00024.00", "100006Fay Lin SALE170.00025.00", "100007Gus Young OPS 035.00000.00", "100002Hana Ito TECH036.00027.50", "100009Ivo Novak SALE040.0002"]
print("== Fixed-width report ==")
print("Payroll lines in fixed columns: checked before they are trusted, every error at once.")
running = True
while running:
print("")
print("1) lines 2) check 3) pay report 4) add a line 5) remove a line 6) quit")
choice = input("choice> ")
if choice == "1":
show(lines)
elif choice == "2":
check_all(lines)
elif choice == "3":
report(lines)
elif choice == "4":
lines = add(lines)
elif choice == "5":
lines = remove(lines)
elif choice == "6":
running = False
else:
print("Pick a number from 1 to 6.")
print("Bye.")
records.eml
eml# P089 fixed-width report - payroll lines in fixed columns, checked before
# they are trusted, every error reported at once, and pay added up in whole
# cents. The columns (start, end, end not included):
# id 0-6 six digits
# name 6-26 letters, spaces, - ' and .
# dept 26-30 four capital letters
# hours 30-35 like 037.5 (tenths of an hour, at most 168.0)
# rate 35-42 like 0025.50 (per hour, more than 0)
# A line is exactly 42 characters.
42 => WIDTH
def is_digits(s):
if len(s) == 0:
return False
for c in s:
if not (c >= "0" and c <= "9"):
return False
return True
def value(s):
0 => n
for c in s:
n * 10 + int(c) => n
return n
def trim_right(s):
len(s) => j
while j > 0 and s[j - 1] == " ":
j - 1 => j
return s[0:j]
def field_errors(line):
# Every problem with a line of the right length, as text.
[] => errs
line[0:6] => ident
line[6:26] => name
line[26:30] => dept
line[30:35] => hours
line[35:42] => rate
if not is_digits(ident):
errs + ["id '" + ident + "' is not six digits"] => errs
trim_right(name) => nm
True => ok_name
if nm == "" or name[0] == " ":
False => ok_name
for c in nm:
if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == " " or c == "-" or c == "'" or c == "."):
False => ok_name
if not ok_name:
errs + ["name '" + nm + "' must start in column 6 and be letters, spaces, - ' or ."] => errs
True => ok_dept
for c in dept:
if not (c >= "A" and c <= "Z"):
False => ok_dept
if not ok_dept:
errs + ["dept '" + dept + "' is not four capital letters"] => errs
if not (is_digits(hours[0:3]) and hours[3] == "." and is_digits(hours[4:5])):
errs + ["hours '" + hours + "' is not like 037.5"] => errs
elif value(hours[0:3]) * 10 + value(hours[4:5]) > 1680:
errs + ["hours " + hours + " is more than the 168 in a week"] => errs
if not (is_digits(rate[0:4]) and rate[4] == "." and is_digits(rate[5:7])):
errs + ["rate '" + rate + "' is not like 0025.50"] => errs
elif value(rate[0:4]) * 100 + value(rate[5:7]) == 0:
errs + ["rate is 0"] => errs
return errs
def check(lines):
# Fail-complete, as in the corpus case schema-validator-all-errors:
# every error of every line. A line of the wrong length is reported
# alone - its slices would not mean anything (the corpus case
# fixed-width-record-parser). A repeated id is an error on the later
# line. Returns, per line, its list of errors.
[] => out
[] => ids
for i in [0:len(lines) - 1]:
lines[i] => line
if len(line) != WIDTH:
out + [["is " + str(len(line)) + " characters, not " + str(WIDTH) + " - its columns cannot be trusted"]] => out
else:
field_errors(line) => errs
for k in [0:len(ids) - 1]:
if ids[k][0] == line[0:6] and len(errs) == 0:
errs + ["id " + line[0:6] + " is already on line " + str(ids[k][1] + 1)] => errs
if len(errs) == 0:
ids + [[line[0:6], i]] => ids
out + [errs] => out
return out
def first_error(lines):
# Fail-fast: the first error found, as [line, error], or [] if none.
check(lines) => all
for i in [0:len(all) - 1]:
if len(all[i]) > 0:
return [i, all[i][0]]
return []
def pay(line):
# hours (tenths) x rate (cents) / 10, rounded half up to the cent.
value(line[30:33]) * 10 + value(line[34:35]) => tenths
value(line[35:39]) * 100 + value(line[40:42]) => cents
tenths * cents => t
0 => q
t * 2 + 10 => n
while n >= 20:
20 => m
1 => k
while m + m <= n:
m + m => m
k + k => k
n - m => n
q + k => q
return q
Python projection (records.py)
WIDTH = 42
def is_digits(s):
if len(s) == 0:
return False
for c in s:
if not (c >= "0" and c <= "9"):
return False
return True
def value(s):
n = 0
for c in s:
n = n * 10 + int(c)
return n
def trim_right(s):
j = len(s)
while j > 0 and s[j - 1] == " ":
j = j - 1
return s[0:j]
def field_errors(line):
errs = []
ident = line[0:6]
name = line[6:26]
dept = line[26:30]
hours = line[30:35]
rate = line[35:42]
if not is_digits(ident):
errs = errs + ["id '" + ident + "' is not six digits"]
nm = trim_right(name)
ok_name = True
if nm == "" or name[0] == " ":
ok_name = False
for c in nm:
if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z" or c == " " or c == "-" or c == "'" or c == "."):
ok_name = False
if not ok_name:
errs = errs + ["name '" + nm + "' must start in column 6 and be letters, spaces, - ' or ."]
ok_dept = True
for c in dept:
if not (c >= "A" and c <= "Z"):
ok_dept = False
if not ok_dept:
errs = errs + ["dept '" + dept + "' is not four capital letters"]
if not (is_digits(hours[0:3]) and hours[3] == "." and is_digits(hours[4:5])):
errs = errs + ["hours '" + hours + "' is not like 037.5"]
elif value(hours[0:3]) * 10 + value(hours[4:5]) > 1680:
errs = errs + ["hours " + hours + " is more than the 168 in a week"]
if not (is_digits(rate[0:4]) and rate[4] == "." and is_digits(rate[5:7])):
errs = errs + ["rate '" + rate + "' is not like 0025.50"]
elif value(rate[0:4]) * 100 + value(rate[5:7]) == 0:
errs = errs + ["rate is 0"]
return errs
def check(lines):
out = []
ids = []
for i in range(0, len(lines)):
line = lines[i]
if len(line) != WIDTH:
out = out + [["is " + str(len(line)) + " characters, not " + str(WIDTH) + " - its columns cannot be trusted"]]
else:
errs = field_errors(line)
for k in range(0, len(ids)):
if ids[k][0] == line[0:6] and len(errs) == 0:
errs = errs + ["id " + line[0:6] + " is already on line " + str(ids[k][1] + 1)]
if len(errs) == 0:
ids = ids + [[line[0:6], i]]
out = out + [errs]
return out
def first_error(lines):
all = check(lines)
for i in range(0, len(all)):
if len(all[i]) > 0:
return [i, all[i][0]]
return []
def pay(line):
tenths = value(line[30:33]) * 10 + value(line[34:35])
cents = value(line[35:39]) * 100 + value(line[40:42])
t = tenths * cents
q = 0
n = t * 2 + 10
while n >= 20:
m = 20
k = 1
while m + m <= n:
m = m + m
k = k + k
n = n - m
q = q + k
return q