{
  "id": "P140",
  "slug": "parts-rollup",
  "key": "P140-parts-rollup",
  "title": "Parts rollup",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P140 - Parts rollup\n\nA bike's bill of materials: each part made of other parts, so many of\neach, with a cost per unit and a tooling cost paid once per kind of part.\n\n- `main.eml` - the menu, the parts list, the parts one bike needs, the\n  costs rolled up two ways, and changing a quantity\n- `bom.eml` - the parts, the order they are reached, units per bike, cost\n  per bike, tooling rolled up as a tree and once per part, the paths to\n  each part, and a changed quantity\n\nHow each part works:\n\n- The bearing kit goes into the frame, each wheel and the drivetrain, so\n  the list is a graph, not a tree: three paths reach it.\n- How many of each part a bike needs multiplies the quantities down every\n  path: 4 bearing kits, 64 spokes. That is right, because each path is\n  parts that really go into the bike, and so is the cost per bike, 398.\n- As in the corpus case `rollup-double-counts-shared-parent`, the same\n  rollup is wrong for a cost paid once. Rolled up as a tree, the tooling\n  is 1750; once for each kind of part it is 1150. The 600 too much is the\n  bearing kit's tooling, 300, counted on two extra paths: the overstatement\n  is always each shared part's tooling times its extra paths.\n\nWhat is checked: menu choices 1 to 5; a part that has parts, one of its\nown parts, and a quantity from 1 to 99. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the parts list, the parts per bike and the costs;\n- 36 spokes a wheel, and the parts and costs again.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- changes: a cancel, spoke (no parts of its own) and cart, then wheel\n  with a cancel and chain, and wheel and spoke with a cancel, 0, 100 and\n  x;\n- 3 wheels a bike, and the parts and costs.\n\nBuilt on the verified corpus case `rollup-double-counts-shared-parent` (a\nrecursive rollup over a hierarchy that is not a tree).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P140 parts rollup: the parts list as a tree, how many of each part a bike\n# needs, the bike's costs rolled up two ways, and changing a quantity.\nimport bom\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef tree(b, part, q, depth):\n    (\"  \" + \"  \" * depth + str(q) + \" x \" + part) ^0\n    for c in b[part][2]:\n        tree(b, c[0], c[1], depth + 1)\n\ndef show(b):\n    \"The parts list, each part under every part that uses it:\" ^0\n    tree(b, \"bike\", 1, 0)\n    bom.paths(b, \"bike\", {}) => ps\n    for p in bom.names(b):\n        if ps[p] > 1:\n            (\"  \" + p + \" is used under \" + str(ps[p]) + \" parts: the list is a graph, not a tree.\") ^0\n\ndef per_bike(b):\n    bom.units(b, \"bike\", 1, {}) => u\n    \"Parts for one bike, multiplied down every path:\" ^0\n    for p in bom.names(b):\n        if p != \"bike\":\n            (\"  \" + pad(p, 12) + lpad(str(u[p]), 4)) ^0\n    \"Multiplying down every path is right here: each path is parts that really go into the bike.\" ^0\n\ndef costs(b):\n    bom.unit_cost(b, \"bike\") => c\n    (\"Cost of one bike, each part's own cost times how many: \" + str(c)) ^0\n    bom.tooling_by_path(b, \"bike\") => t1\n    bom.tooling_once(b) => t2\n    bom.paths(b, \"bike\", {}) => ps\n    \"Tooling, paid once for each kind of part:\" ^0\n    \"  part           tooling   paths   counted by the tree\" ^0\n    for p in bom.names(b):\n        if b[p][1] > 0:\n            (\"  \" + pad(p, 13) + lpad(str(b[p][1]), 9) + lpad(str(ps[p]), 8) + lpad(str(b[p][1] * ps[p]), 22)) ^0\n    (\"  rolled up as a tree: \" + str(t1) + \"; once for each part: \" + str(t2)) ^0\n    if t1 > t2:\n        (\"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\n\ndef find(b, name):\n    for p in b:\n        if p == name:\n            return True\n    return False\n\ndef change(b):\n    plain(input(\"part (one that has parts)> \")) => parent\n    if parent == \"\":\n        \"Cancelled.\" ^0\n        return b\n    if not find(b, parent) or len(b[parent][2]) == 0:\n        \"Type a part that is made of other parts: bike, frame, wheel or drivetrain.\" ^0\n        return b\n    plain(input(\"which of its parts> \")) => child\n    if child == \"\":\n        \"Cancelled.\" ^0\n        return b\n    False => ok\n    for c in b[parent][2]:\n        if c[0] == child:\n            True => ok\n    if not ok:\n        (child + \" is not one of the parts of \" + parent + \".\") ^0\n        return b\n    plain(input(\"how many (1 to 99)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return b\n    if len(t) > 2:\n        \"Type a whole number from 1 to 99.\" ^0\n        return b\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            \"Type a whole number from 1 to 99.\" ^0\n            return b\n    if int(t) < 1:\n        \"Type a whole number from 1 to 99.\" ^0\n        return b\n    (\"A \" + parent + \" now takes \" + t + \" x \" + child + \".\") ^0\n    return bom.set_quantity(b, parent, child, int(t))\n\nbom.parts() => b\n\n\"== Parts rollup ==\" ^0\n\"A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(b)\n    elif choice == \"2\":\n        per_bike(b)\n    elif choice == \"3\":\n        costs(b)\n    elif choice == \"4\":\n        change(b) => b\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import bom\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef tree(b, part, q, depth):\n    print(\"  \" + \"  \" * depth + str(q) + \" x \" + part)\n    for c in b[part][2]:\n        tree(b, c[0], c[1], depth + 1)\n\ndef show(b):\n    print(\"The parts list, each part under every part that uses it:\")\n    tree(b, \"bike\", 1, 0)\n    ps = bom.paths(b, \"bike\", {})\n    for p in bom.names(b):\n        if ps[p] > 1:\n            print(\"  \" + p + \" is used under \" + str(ps[p]) + \" parts: the list is a graph, not a tree.\")\n\ndef per_bike(b):\n    u = bom.units(b, \"bike\", 1, {})\n    print(\"Parts for one bike, multiplied down every path:\")\n    for p in bom.names(b):\n        if p != \"bike\":\n            print(\"  \" + pad(p, 12) + lpad(str(u[p]), 4))\n    print(\"Multiplying down every path is right here: each path is parts that really go into the bike.\")\n\ndef costs(b):\n    c = bom.unit_cost(b, \"bike\")\n    print(\"Cost of one bike, each part's own cost times how many: \" + str(c))\n    t1 = bom.tooling_by_path(b, \"bike\")\n    t2 = bom.tooling_once(b)\n    ps = bom.paths(b, \"bike\", {})\n    print(\"Tooling, paid once for each kind of part:\")\n    print(\"  part           tooling   paths   counted by the tree\")\n    for p in bom.names(b):\n        if b[p][1] > 0:\n            print(\"  \" + pad(p, 13) + lpad(str(b[p][1]), 9) + lpad(str(ps[p]), 8) + lpad(str(b[p][1] * ps[p]), 22))\n    print(\"  rolled up as a tree: \" + str(t1) + \"; once for each part: \" + str(t2))\n    if t1 > t2:\n        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.\")\n\ndef find(b, name):\n    for p in b:\n        if p == name:\n            return True\n    return False\n\ndef change(b):\n    parent = plain(input(\"part (one that has parts)> \"))\n    if parent == \"\":\n        print(\"Cancelled.\")\n        return b\n    if not find(b, parent) or len(b[parent][2]) == 0:\n        print(\"Type a part that is made of other parts: bike, frame, wheel or drivetrain.\")\n        return b\n    child = plain(input(\"which of its parts> \"))\n    if child == \"\":\n        print(\"Cancelled.\")\n        return b\n    ok = False\n    for c in b[parent][2]:\n        if c[0] == child:\n            ok = True\n    if not ok:\n        print(child + \" is not one of the parts of \" + parent + \".\")\n        return b\n    t = plain(input(\"how many (1 to 99)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return b\n    if len(t) > 2:\n        print(\"Type a whole number from 1 to 99.\")\n        return b\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            print(\"Type a whole number from 1 to 99.\")\n            return b\n    if int(t) < 1:\n        print(\"Type a whole number from 1 to 99.\")\n        return b\n    print(\"A \" + parent + \" now takes \" + t + \" x \" + child + \".\")\n    return bom.set_quantity(b, parent, child, int(t))\n\nb = bom.parts()\nprint(\"== Parts rollup ==\")\nprint(\"A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show(b)\n    elif choice == \"2\":\n        per_bike(b)\n    elif choice == \"3\":\n        costs(b)\n    elif choice == \"4\":\n        b = change(b)\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "bom.eml",
      "eml": "# P140 parts rollup - a bill of materials: each part made of other parts,\n# so many of each. A part used under several parents makes the list a graph,\n# not a tree. Counting how many of each part a bike needs, and its cost per\n# bike, multiplies down every path, and that is right: each path is real\n# parts. As in the corpus case rollup-double-counts-shared-parent, a cost\n# paid once per part - the tooling to make it - is counted once for every\n# path that reaches the part when rolled up the same way.\n\ndef parts():\n    # part: [own cost per unit (labour, or the part itself), tooling paid\n    #  once, [[child, quantity], ...]]\n    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, []]}\n\ndef names(bom):\n    # parts in the order they are first reached from the bike\n    [\"bike\"] => out\n    0 => k\n    while k < len(out):\n        for c in bom[out[k]][2]:\n            False => seen\n            for x in out:\n                if x == c[0]:\n                    True => seen\n            if not seen:\n                out + [c[0]] => out\n        k + 1 => k\n    return out\n\ndef units(bom, part, times, acc):\n    # how many of each part one of part needs, times times, added into acc\n    if part in acc:\n        acc[part] + times => acc[part]\n    else:\n        times => acc[part]\n    for c in bom[part][2]:\n        units(bom, c[0], times * c[1], acc)\n    return acc\n\ndef unit_cost(bom, part):\n    # the cost of one: its own, and each child's times how many\n    bom[part][0] => c\n    for ch in bom[part][2]:\n        c + ch[1] * unit_cost(bom, ch[0]) => c\n    return c\n\ndef tooling_by_path(bom, part):\n    # the tooling rolled up as a tree: own tooling, plus each child's rolled\n    # up tooling (once per child, not per unit)\n    bom[part][1] => t\n    for ch in bom[part][2]:\n        t + tooling_by_path(bom, ch[0]) => t\n    return t\n\ndef tooling_once(bom):\n    0 => t\n    for p in names(bom):\n        t + bom[p][1] => t\n    return t\n\ndef paths(bom, part, acc):\n    # how many paths from the bike reach each part\n    if part in acc:\n        acc[part] + 1 => acc[part]\n    else:\n        1 => acc[part]\n    for ch in bom[part][2]:\n        paths(bom, ch[0], acc)\n    return acc\n\ndef set_quantity(bom, parent, child, q):\n    {} => out\n    for p in bom:\n        [] => kids\n        for c in bom[p][2]:\n            if p == parent and c[0] == child:\n                kids + [[c[0], q]] => kids\n            else:\n                kids + [[c[0], c[1]]] => kids\n        [bom[p][0], bom[p][1], kids] => out[p]\n    return out\n",
      "python": "def parts():\n    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, []]}\n\ndef names(bom):\n    out = [\"bike\"]\n    k = 0\n    while k < len(out):\n        for c in bom[out[k]][2]:\n            seen = False\n            for x in out:\n                if x == c[0]:\n                    seen = True\n            if not seen:\n                out = out + [c[0]]\n        k = k + 1\n    return out\n\ndef units(bom, part, times, acc):\n    if part in acc:\n        acc[part] = acc[part] + times\n    else:\n        acc[part] = times\n    for c in bom[part][2]:\n        units(bom, c[0], times * c[1], acc)\n    return acc\n\ndef unit_cost(bom, part):\n    c = bom[part][0]\n    for ch in bom[part][2]:\n        c = c + ch[1] * unit_cost(bom, ch[0])\n    return c\n\ndef tooling_by_path(bom, part):\n    t = bom[part][1]\n    for ch in bom[part][2]:\n        t = t + tooling_by_path(bom, ch[0])\n    return t\n\ndef tooling_once(bom):\n    t = 0\n    for p in names(bom):\n        t = t + bom[p][1]\n    return t\n\ndef paths(bom, part, acc):\n    if part in acc:\n        acc[part] = acc[part] + 1\n    else:\n        acc[part] = 1\n    for ch in bom[part][2]:\n        paths(bom, ch[0], acc)\n    return acc\n\ndef set_quantity(bom, parent, child, q):\n    out = {}\n    for p in bom:\n        kids = []\n        for c in bom[p][2]:\n            if p == parent and c[0] == child:\n                kids = kids + [[c[0], q]]\n            else:\n                kids = kids + [[c[0], c[1]]]\n        out[p] = [bom[p][0], bom[p][1], kids]\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\nspoke\n4\ncart\n4\nwheel\n\n4\nwheel\nchain\n4\nwheel\nspoke\n\n4\nwheel\nspoke\n0\n4\nwheel\nspoke\n100\n4\nwheel\nspoke\nx\n4\nbike\nwheel\n3\n2\n3\n5\n",
      "screen": "== Parts rollup ==\nA bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> \nCancelled.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> spoke\nType a part that is made of other parts: bike, frame, wheel or drivetrain.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> cart\nType a part that is made of other parts: bike, frame, wheel or drivetrain.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> \nCancelled.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> chain\nchain is not one of the parts of wheel.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> spoke\nhow many (1 to 99)> \nCancelled.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> spoke\nhow many (1 to 99)> 0\nType a whole number from 1 to 99.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> spoke\nhow many (1 to 99)> 100\nType a whole number from 1 to 99.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> spoke\nhow many (1 to 99)> x\nType a whole number from 1 to 99.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> bike\nwhich of its parts> wheel\nhow many (1 to 99)> 3\nA bike now takes 3 x wheel.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 2\nParts for one bike, multiplied down every path:\n  frame          1\n  wheel          3\n  drivetrain     1\n  bearing kit    5\n  rim            3\n  spoke         96\n  chain          1\n  crank          1\nMultiplying down every path is right here: each path is parts that really go into the bike.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 3\nCost of one bike, each part's own cost times how many: 478\nTooling, paid once for each kind of part:\n  part           tooling   paths   counted by the tree\n  frame              500       1                   500\n  bearing kit        300       3                   900\n  rim                200       1                   200\n  crank              150       1                   150\n  rolled up as a tree: 1750; once for each part: 1150\nThe 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.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\nwheel\nspoke\n36\n2\n3\n5\n",
      "screen": "== Parts rollup ==\nA bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 1\nThe parts list, each part under every part that uses it:\n  1 x bike\n    1 x frame\n      1 x bearing kit\n    2 x wheel\n      1 x rim\n      32 x spoke\n      1 x bearing kit\n    1 x drivetrain\n      1 x chain\n      1 x crank\n      1 x bearing kit\n  bearing kit is used under 3 parts: the list is a graph, not a tree.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 2\nParts for one bike, multiplied down every path:\n  frame          1\n  wheel          2\n  drivetrain     1\n  bearing kit    4\n  rim            2\n  spoke         64\n  chain          1\n  crank          1\nMultiplying down every path is right here: each path is parts that really go into the bike.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 3\nCost of one bike, each part's own cost times how many: 398\nTooling, paid once for each kind of part:\n  part           tooling   paths   counted by the tree\n  frame              500       1                   500\n  bearing kit        300       3                   900\n  rim                200       1                   200\n  crank              150       1                   150\n  rolled up as a tree: 1750; once for each part: 1150\nThe 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.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 4\npart (one that has parts)> wheel\nwhich of its parts> spoke\nhow many (1 to 99)> 36\nA wheel now takes 36 x spoke.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 2\nParts for one bike, multiplied down every path:\n  frame          1\n  wheel          2\n  drivetrain     1\n  bearing kit    4\n  rim            2\n  spoke         72\n  chain          1\n  crank          1\nMultiplying down every path is right here: each path is parts that really go into the bike.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 3\nCost of one bike, each part's own cost times how many: 406\nTooling, paid once for each kind of part:\n  part           tooling   paths   counted by the tree\n  frame              500       1                   500\n  bearing kit        300       3                   900\n  rim                200       1                   200\n  crank              150       1                   150\n  rolled up as a tree: 1750; once for each part: 1150\nThe 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.\n\n1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "rollup-double-counts-shared-parent",
      "caseId": "304-rollup-double-counts-shared-parent",
      "title": "Rollup double counts shared parent — the thing it is walking is not a tree"
    }
  ],
  "updated": "2026-10-17"
}
