Parts rollup
A bike's bill of materials with a part shared by three parents: parts and cost per bike multiplied down every path, rightly, and a tooling cost paid once that the same tree rollup counts once per path.
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 bike's bill of materials: each part made of other parts, so many of each, with a cost per unit and a tooling cost paid once per kind of part.
main.eml- the menu, the parts list, the parts one bike needs, the costs rolled up two ways, and changing a quantitybom.eml- the parts, the order they are reached, units per bike, cost per bike, tooling rolled up as a tree and once per part, the paths to each part, and a changed quantity
How each part works:
- The bearing kit goes into the frame, each wheel and the drivetrain, so the list is a graph, not a tree: three paths reach it.
- How many of each part a bike needs multiplies the quantities down every path: 4 bearing kits, 64 spokes. That is right, because each path is parts that really go into the bike, and so is the cost per bike, 398.
- As in the corpus case
rollup-double-counts-shared-parent, the same rollup is wrong for a cost paid once. Rolled up as a tree, the tooling is 1750; once for each kind of part it is 1150. The 600 too much is the bearing kit's tooling, 300, counted on two extra paths: the overstatement is always each shared part's tooling times its extra paths.
What is checked: menu choices 1 to 5; a part that has parts, one of its own parts, and a quantity from 1 to 99. An empty answer cancels.
Sessions: sessions/basic.in gives: - the parts list, the parts per bike and the costs; - 36 spokes a wheel, and the parts and costs again.
sessions/bad-input.in gives: - menu choices 0 and x; - changes: a cancel, spoke (no parts of its own) and cart, then wheel with a cancel and chain, and wheel and spoke with a cancel, 0, 100 and x; - 3 wheels a bike, and the parts and costs.
Built on the verified corpus case rollup-double-counts-shared-parent (a recursive rollup over a hierarchy that is not a tree).
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== Parts rollup ==
A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 0
Pick a number from 1 to 5.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> x
Pick a number from 1 to 5.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)>
Cancelled.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> spoke
Type a part that is made of other parts: bike, frame, wheel or drivetrain.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> cart
Type a part that is made of other parts: bike, frame, wheel or drivetrain.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts>
Cancelled.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> chain
chain is not one of the parts of wheel.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)>
Cancelled.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 0
Type a whole number from 1 to 99.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 100
Type a whole number from 1 to 99.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> x
Type a whole number from 1 to 99.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> bike
which of its parts> wheel
how many (1 to 99)> 3
A bike now takes 3 x wheel.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 2
Parts for one bike, multiplied down every path:
frame 1
wheel 3
drivetrain 1
bearing kit 5
rim 3
spoke 96
chain 1
crank 1
Multiplying down every path is right here: each path is parts that really go into the bike.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 3
Cost of one bike, each part's own cost times how many: 478
Tooling, paid once for each kind of part:
part tooling paths counted by the tree
frame 500 1 500
bearing kit 300 3 900
rim 200 1 200
crank 150 1 150
rolled up as a tree: 1750; once for each part: 1150
The tree counts the tooling of a shared part once for every path to it: 600 too much. The same rollup is right for the cost per bike and wrong for a cost paid once.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 5
Bye.
What was typed (37 lines)
0
x
4
4
spoke
4
cart
4
wheel
4
wheel
chain
4
wheel
spoke
4
wheel
spoke
0
4
wheel
spoke
100
4
wheel
spoke
x
4
bike
wheel
3
2
3
5
basic
interpreter: byte-equal== Parts rollup ==
A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 1
The parts list, each part under every part that uses it:
1 x bike
1 x frame
1 x bearing kit
2 x wheel
1 x rim
32 x spoke
1 x bearing kit
1 x drivetrain
1 x chain
1 x crank
1 x bearing kit
bearing kit is used under 3 parts: the list is a graph, not a tree.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 2
Parts for one bike, multiplied down every path:
frame 1
wheel 2
drivetrain 1
bearing kit 4
rim 2
spoke 64
chain 1
crank 1
Multiplying down every path is right here: each path is parts that really go into the bike.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 3
Cost of one bike, each part's own cost times how many: 398
Tooling, paid once for each kind of part:
part tooling paths counted by the tree
frame 500 1 500
bearing kit 300 3 900
rim 200 1 200
crank 150 1 150
rolled up as a tree: 1750; once for each part: 1150
The tree counts the tooling of a shared part once for every path to it: 600 too much. The same rollup is right for the cost per bike and wrong for a cost paid once.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 36
A wheel now takes 36 x spoke.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 2
Parts for one bike, multiplied down every path:
frame 1
wheel 2
drivetrain 1
bearing kit 4
rim 2
spoke 72
chain 1
crank 1
Multiplying down every path is right here: each path is parts that really go into the bike.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 3
Cost of one bike, each part's own cost times how many: 406
Tooling, paid once for each kind of part:
part tooling paths counted by the tree
frame 500 1 500
bearing kit 300 3 900
rim 200 1 200
crank 150 1 150
rolled up as a tree: 1750; once for each part: 1150
The tree counts the tooling of a shared part once for every path to it: 600 too much. The same rollup is right for the cost per bike and wrong for a cost paid once.
1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit
choice> 5
Bye.
What was typed (10 lines)
1
2
3
4
wheel
spoke
36
2
3
5
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# P140 parts rollup: the parts list as a tree, how many of each part a bike
# needs, the bike's costs rolled up two ways, and changing a quantity.
import bom
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 tree(b, part, q, depth):
(" " + " " * depth + str(q) + " x " + part) ^0
for c in b[part][2]:
tree(b, c[0], c[1], depth + 1)
def show(b):
"The parts list, each part under every part that uses it:" ^0
tree(b, "bike", 1, 0)
bom.paths(b, "bike", {}) => ps
for p in bom.names(b):
if ps[p] > 1:
(" " + p + " is used under " + str(ps[p]) + " parts: the list is a graph, not a tree.") ^0
def per_bike(b):
bom.units(b, "bike", 1, {}) => u
"Parts for one bike, multiplied down every path:" ^0
for p in bom.names(b):
if p != "bike":
(" " + pad(p, 12) + lpad(str(u[p]), 4)) ^0
"Multiplying down every path is right here: each path is parts that really go into the bike." ^0
def costs(b):
bom.unit_cost(b, "bike") => c
("Cost of one bike, each part's own cost times how many: " + str(c)) ^0
bom.tooling_by_path(b, "bike") => t1
bom.tooling_once(b) => t2
bom.paths(b, "bike", {}) => ps
"Tooling, paid once for each kind of part:" ^0
" part tooling paths counted by the tree" ^0
for p in bom.names(b):
if b[p][1] > 0:
(" " + pad(p, 13) + lpad(str(b[p][1]), 9) + lpad(str(ps[p]), 8) + lpad(str(b[p][1] * ps[p]), 22)) ^0
(" rolled up as a tree: " + str(t1) + "; once for each part: " + str(t2)) ^0
if t1 > t2:
("The tree counts the tooling of a shared part once for every path to it: " + str(t1 - t2) + " too much. The same rollup is right for the cost per bike and wrong for a cost paid once.") ^0
def find(b, name):
for p in b:
if p == name:
return True
return False
def change(b):
plain(input("part (one that has parts)> ")) => parent
if parent == "":
"Cancelled." ^0
return b
if not find(b, parent) or len(b[parent][2]) == 0:
"Type a part that is made of other parts: bike, frame, wheel or drivetrain." ^0
return b
plain(input("which of its parts> ")) => child
if child == "":
"Cancelled." ^0
return b
False => ok
for c in b[parent][2]:
if c[0] == child:
True => ok
if not ok:
(child + " is not one of the parts of " + parent + ".") ^0
return b
plain(input("how many (1 to 99)> ")) => t
if t == "":
"Cancelled." ^0
return b
if len(t) > 2:
"Type a whole number from 1 to 99." ^0
return b
for c in t:
if c < "0" or c > "9":
"Type a whole number from 1 to 99." ^0
return b
if int(t) < 1:
"Type a whole number from 1 to 99." ^0
return b
("A " + parent + " now takes " + t + " x " + child + ".") ^0
return bom.set_quantity(b, parent, child, int(t))
bom.parts() => b
"== Parts rollup ==" ^0
"A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once." ^0
True => running
while running:
"" ^0
"1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show(b)
elif choice == "2":
per_bike(b)
elif choice == "3":
costs(b)
elif choice == "4":
change(b) => b
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import bom
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 tree(b, part, q, depth):
print(" " + " " * depth + str(q) + " x " + part)
for c in b[part][2]:
tree(b, c[0], c[1], depth + 1)
def show(b):
print("The parts list, each part under every part that uses it:")
tree(b, "bike", 1, 0)
ps = bom.paths(b, "bike", {})
for p in bom.names(b):
if ps[p] > 1:
print(" " + p + " is used under " + str(ps[p]) + " parts: the list is a graph, not a tree.")
def per_bike(b):
u = bom.units(b, "bike", 1, {})
print("Parts for one bike, multiplied down every path:")
for p in bom.names(b):
if p != "bike":
print(" " + pad(p, 12) + lpad(str(u[p]), 4))
print("Multiplying down every path is right here: each path is parts that really go into the bike.")
def costs(b):
c = bom.unit_cost(b, "bike")
print("Cost of one bike, each part's own cost times how many: " + str(c))
t1 = bom.tooling_by_path(b, "bike")
t2 = bom.tooling_once(b)
ps = bom.paths(b, "bike", {})
print("Tooling, paid once for each kind of part:")
print(" part tooling paths counted by the tree")
for p in bom.names(b):
if b[p][1] > 0:
print(" " + pad(p, 13) + lpad(str(b[p][1]), 9) + lpad(str(ps[p]), 8) + lpad(str(b[p][1] * ps[p]), 22))
print(" rolled up as a tree: " + str(t1) + "; once for each part: " + str(t2))
if t1 > t2:
print("The tree counts the tooling of a shared part once for every path to it: " + str(t1 - t2) + " too much. The same rollup is right for the cost per bike and wrong for a cost paid once.")
def find(b, name):
for p in b:
if p == name:
return True
return False
def change(b):
parent = plain(input("part (one that has parts)> "))
if parent == "":
print("Cancelled.")
return b
if not find(b, parent) or len(b[parent][2]) == 0:
print("Type a part that is made of other parts: bike, frame, wheel or drivetrain.")
return b
child = plain(input("which of its parts> "))
if child == "":
print("Cancelled.")
return b
ok = False
for c in b[parent][2]:
if c[0] == child:
ok = True
if not ok:
print(child + " is not one of the parts of " + parent + ".")
return b
t = plain(input("how many (1 to 99)> "))
if t == "":
print("Cancelled.")
return b
if len(t) > 2:
print("Type a whole number from 1 to 99.")
return b
for c in t:
if c < "0" or c > "9":
print("Type a whole number from 1 to 99.")
return b
if int(t) < 1:
print("Type a whole number from 1 to 99.")
return b
print("A " + parent + " now takes " + t + " x " + child + ".")
return bom.set_quantity(b, parent, child, int(t))
b = bom.parts()
print("== Parts rollup ==")
print("A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.")
running = True
while running:
print("")
print("1) parts list 2) parts per bike 3) costs 4) change a quantity 5) quit")
choice = plain(input("choice> "))
if choice == "1":
show(b)
elif choice == "2":
per_bike(b)
elif choice == "3":
costs(b)
elif choice == "4":
b = change(b)
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
bom.eml
eml# P140 parts rollup - a bill of materials: each part made of other parts,
# so many of each. A part used under several parents makes the list a graph,
# not a tree. Counting how many of each part a bike needs, and its cost per
# bike, multiplies down every path, and that is right: each path is real
# parts. As in the corpus case rollup-double-counts-shared-parent, a cost
# paid once per part - the tooling to make it - is counted once for every
# path that reaches the part when rolled up the same way.
def parts():
# part: [own cost per unit (labour, or the part itself), tooling paid
# once, [[child, quantity], ...]]
return {"bike": [40, 0, [["frame", 1], ["wheel", 2], ["drivetrain", 1]]], "frame": [120, 500, [["bearing kit", 1]]], "wheel": [15, 0, [["rim", 1], ["spoke", 32], ["bearing kit", 1]]], "drivetrain": [20, 0, [["chain", 1], ["crank", 1], ["bearing kit", 1]]], "rim": [25, 200, []], "spoke": [1, 0, []], "chain": [12, 0, []], "crank": [30, 150, []], "bearing kit": [8, 300, []]}
def names(bom):
# parts in the order they are first reached from the bike
["bike"] => out
0 => k
while k < len(out):
for c in bom[out[k]][2]:
False => seen
for x in out:
if x == c[0]:
True => seen
if not seen:
out + [c[0]] => out
k + 1 => k
return out
def units(bom, part, times, acc):
# how many of each part one of part needs, times times, added into acc
if part in acc:
acc[part] + times => acc[part]
else:
times => acc[part]
for c in bom[part][2]:
units(bom, c[0], times * c[1], acc)
return acc
def unit_cost(bom, part):
# the cost of one: its own, and each child's times how many
bom[part][0] => c
for ch in bom[part][2]:
c + ch[1] * unit_cost(bom, ch[0]) => c
return c
def tooling_by_path(bom, part):
# the tooling rolled up as a tree: own tooling, plus each child's rolled
# up tooling (once per child, not per unit)
bom[part][1] => t
for ch in bom[part][2]:
t + tooling_by_path(bom, ch[0]) => t
return t
def tooling_once(bom):
0 => t
for p in names(bom):
t + bom[p][1] => t
return t
def paths(bom, part, acc):
# how many paths from the bike reach each part
if part in acc:
acc[part] + 1 => acc[part]
else:
1 => acc[part]
for ch in bom[part][2]:
paths(bom, ch[0], acc)
return acc
def set_quantity(bom, parent, child, q):
{} => out
for p in bom:
[] => kids
for c in bom[p][2]:
if p == parent and c[0] == child:
kids + [[c[0], q]] => kids
else:
kids + [[c[0], c[1]]] => kids
[bom[p][0], bom[p][1], kids] => out[p]
return out
Python projection (bom.py)
def parts():
return {"bike": [40, 0, [["frame", 1], ["wheel", 2], ["drivetrain", 1]]], "frame": [120, 500, [["bearing kit", 1]]], "wheel": [15, 0, [["rim", 1], ["spoke", 32], ["bearing kit", 1]]], "drivetrain": [20, 0, [["chain", 1], ["crank", 1], ["bearing kit", 1]]], "rim": [25, 200, []], "spoke": [1, 0, []], "chain": [12, 0, []], "crank": [30, 150, []], "bearing kit": [8, 300, []]}
def names(bom):
out = ["bike"]
k = 0
while k < len(out):
for c in bom[out[k]][2]:
seen = False
for x in out:
if x == c[0]:
seen = True
if not seen:
out = out + [c[0]]
k = k + 1
return out
def units(bom, part, times, acc):
if part in acc:
acc[part] = acc[part] + times
else:
acc[part] = times
for c in bom[part][2]:
units(bom, c[0], times * c[1], acc)
return acc
def unit_cost(bom, part):
c = bom[part][0]
for ch in bom[part][2]:
c = c + ch[1] * unit_cost(bom, ch[0])
return c
def tooling_by_path(bom, part):
t = bom[part][1]
for ch in bom[part][2]:
t = t + tooling_by_path(bom, ch[0])
return t
def tooling_once(bom):
t = 0
for p in names(bom):
t = t + bom[p][1]
return t
def paths(bom, part, acc):
if part in acc:
acc[part] = acc[part] + 1
else:
acc[part] = 1
for ch in bom[part][2]:
paths(bom, ch[0], acc)
return acc
def set_quantity(bom, parent, child, q):
out = {}
for p in bom:
kids = []
for c in bom[p][2]:
if p == parent and c[0] == child:
kids = kids + [[c[0], q]]
else:
kids = kids + [[c[0], c[1]]]
out[p] = [bom[p][0], bom[p][1], kids]
return out