{
  "id": "P139",
  "slug": "tolerance-grouping",
  "key": "P139-tolerance-grouping",
  "title": "Tolerance grouping",
  "summary": "Measurements the same within a tolerance - reflexive and symmetric but not transitive - grouped by first match, which depends on the order, and by chaining, which joins values far apart; and records matched on name, email or phone that chain the same way.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P139 - Tolerance grouping\n\nMeasurements count as the same when they differ by at most a tolerance.\nGrouping them by that rule gives different answers depending on how the\ngrouping is done and in what order the values come.\n\n- `main.eml` - the menu, the values, the relation's properties, grouping\n  in this order and in every order, records matched on any of three\n  fields, and the settings\n- `near.eml` - the relation and its properties, first match and chaining,\n  every order, and the records\n\nHow each part works:\n\n- Values are whole tenths of a millimetre; the default ones are 10.0,\n  10.3, 10.6, 10.9, 11.2 and 13.0 with a tolerance of 0.3.\n- As in the corpus case `tolerance-equality-grouping`, the relation is\n  reflexive and symmetric every time, and not transitive: 10.0 ~ 10.3 and\n  10.3 ~ 10.6, but not 10.0 ~ 10.6. So it does not split the values into\n  groups at all.\n- First match puts each value in the first group whose first value is\n  near it: over the 720 orders it gives 6 different groupings, of 3 or 4\n  groups, and never puts values more than twice the tolerance apart.\n  Chaining joins every group a value is near: the same grouping in every\n  order, and 10.0 and 11.2, 1.2 mm apart, in one group.\n- As in the corpus case `match-predicate-not-transitive`, four records\n  match when they share a name, an email or a phone: r0 ~ r1 ~ r2 ~ r3,\n  while r0 and r3 share nothing. First match gives 3 different answers\n  over the 24 orders; chaining puts all four together.\n\nWhat is checked: menu choices 1 to 8; 2 to 7 values from 0.0 to 99.9 with\nat most one decimal; a tolerance from 0.0 to 5.0. An empty answer\ncancels.\n\nSessions: `sessions/basic.in` gives:\n- the values, the properties, this order, every order and the records;\n- the same values in another order, grouped again;\n- a tolerance of 1.0 mm, and every order.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- values: a cancel, one value, eight, 1.23, x, 100.0 and 1.;\n- tolerances: a cancel, 5.1 and -1, then 0.0: every order and the\n  properties, with no chain at all.\n\nBuilt on the verified corpus cases `tolerance-equality-grouping` (close\nenough to be the same is not an equivalence) and\n`match-predicate-not-transitive` (a record match of three clauses that\ndoes not chain).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P139 tolerance grouping: the values, the relation's properties, grouping\n# in one order and in every order, records matched on any of three fields,\n# and the settings.\nimport near\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 mm(v):\n    return str(int(v / 10)) + \".\" + str(v % 10)\n\ndef list_text(vs):\n    \"\" => out\n    for v in vs:\n        if out != \"\":\n            out + \" \" => out\n        out + mm(v) => out\n    return out\n\ndef groups_text(gs):\n    \"\" => out\n    for g in gs:\n        if out != \"\":\n            out + \"  \" => out\n        out + \"{\" + list_text(g) + \"}\" => out\n    return out\n\ndef show(vs, tol):\n    (\"Values, in mm, in this order: \" + list_text(vs)) ^0\n    (\"Two values are the same when they differ by at most \" + mm(tol) + \" mm.\") ^0\n\ndef props(vs, tol):\n    near.properties(vs, tol) => p\n    (\"  reflexive, a ~ a:            \" + str(p[1]) + \" of \" + str(p[0]) + \" values\") ^0\n    (\"  symmetric, a ~ b as b ~ a:   \" + str(p[3]) + \" of \" + str(p[2]) + \" pairs\") ^0\n    (\"  transitive, a ~ b ~ c gives a ~ c: \" + str(p[5]) + \" of \" + str(p[4]) + \" chains\") ^0\n    if p[5] < p[4]:\n        \"Not transitive: so it does not split the values into groups, and 'the group of x' names nothing.\" ^0\n\ndef this_order(vs, tol):\n    near.first_match(vs, tol) => a\n    near.canonical(near.chaining(vs, tol)) => c\n    (\"  first match: \" + groups_text(near.canonical(a))) ^0\n    (\"  chaining:    \" + groups_text(c)) ^0\n    0 => span\n    for g in c:\n        if g[len(g) - 1] - g[0] > span:\n            g[len(g) - 1] - g[0] => span\n    if span > tol:\n        (\"  Chaining puts values \" + mm(span) + \" mm apart in one group, through the ones between.\") ^0\n\ndef every(vs, tol):\n    near.every_order(vs, tol) => r\n    (\"Every order of the \" + str(len(vs)) + \" values, \" + str(r[0]) + \" orders:\") ^0\n    if r[1] == 1:\n        (\"  first match: 1 grouping, of \" + str(r[2]) + \" groups\") ^0\n    else:\n        (\"  first match: \" + str(r[1]) + \" different groupings, from \" + str(r[2]) + \" to \" + str(r[3]) + \" groups\") ^0\n    (\"  chaining:    \" + str(r[4]) + \" grouping, the widest group spanning \" + mm(r[5]) + \" mm\") ^0\n    if r[1] > 1:\n        \"First match depends on the order the values come in; chaining does not, and joins values far apart instead.\" ^0\n\ndef show_records():\n    near.records() => rs\n    \"Records, matching when they share a name, an email or a phone:\" ^0\n    for k in [0:3]:\n        (\"  r\" + str(k) + \"  \" + pad(rs[k][0], 11) + pad(rs[k][1], 10) + rs[k][2]) ^0\n    \"     r0 r1 r2 r3\" => head\n    head ^0\n    for i in [0:3]:\n        \"  r\" + str(i) => line\n        for j in [0:3]:\n            if near.same(rs[i], rs[j]):\n                line + \"  1\" => line\n            else:\n                line + \"  0\" => line\n        line ^0\n    near.record_orders() => r\n    (\"Every order of the 4 records, \" + str(r[0]) + \" orders: first match gives \" + str(r[1]) + \" different answers, chaining \" + str(r[2]) + \".\") ^0\n    \"Chaining puts r0 and r3 together, and they share nothing: the match is not transitive either.\" ^0\n\ndef set_values(vs):\n    \"Values in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\" ^0\n    plain(input(\"values> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return vs\n    [] => out\n    \"\" => w\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                parse(w) => v\n                if v < 0:\n                    \"Each value is a number from 0.0 to 99.9, with at most one decimal.\" ^0\n                    return vs\n                out + [v] => out\n            \"\" => w\n        else:\n            w + c => w\n    if len(out) < 2 or len(out) > 7:\n        \"Type 2 to 7 values.\" ^0\n        return vs\n    (\"Values now \" + list_text(out) + \".\") ^0\n    return out\n\ndef parse(w):\n    # tenths, or -1\n    \"\" => a\n    \"\" => b\n    False => dot\n    for c in w:\n        if c == \".\" and not dot:\n            True => dot\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                b + c => b\n            else:\n                a + c => a\n        else:\n            return 0 - 1\n    if len(a) == 0 or len(a) > 2 or len(b) > 1:\n        return 0 - 1\n    if dot and len(b) == 0:\n        return 0 - 1\n    10 * int(a) => v\n    if len(b) == 1:\n        v + int(b) => v\n    return v\n\ndef set_tol(tol):\n    plain(input(\"tolerance in mm (0.0 to 5.0)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return tol\n    parse(t) => v\n    if v < 0 or v > 50:\n        \"Type a tolerance from 0.0 to 5.0, with at most one decimal.\" ^0\n        return tol\n    (\"Tolerance now \" + mm(v) + \" mm.\") ^0\n    return v\n\n[100, 103, 106, 109, 112, 130] => vs\n3 => tol\n\n\"== Tolerance grouping ==\" ^0\n\"Measurements count as the same when they differ by at most a tolerance; grouping them by it.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(vs, tol)\n    elif choice == \"2\":\n        props(vs, tol)\n    elif choice == \"3\":\n        this_order(vs, tol)\n    elif choice == \"4\":\n        every(vs, tol)\n    elif choice == \"5\":\n        show_records()\n    elif choice == \"6\":\n        set_values(vs) => vs\n    elif choice == \"7\":\n        set_tol(tol) => tol\n    elif choice == \"8\":\n        False => running\n    else:\n        \"Pick a number from 1 to 8.\" ^0\n\"Bye.\" ^0\n",
      "python": "import near\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 mm(v):\n    return str(int(v / 10)) + \".\" + str(v % 10)\n\ndef list_text(vs):\n    out = \"\"\n    for v in vs:\n        if out != \"\":\n            out = out + \" \"\n        out = out + mm(v)\n    return out\n\ndef groups_text(gs):\n    out = \"\"\n    for g in gs:\n        if out != \"\":\n            out = out + \"  \"\n        out = out + \"{\" + list_text(g) + \"}\"\n    return out\n\ndef show(vs, tol):\n    print(\"Values, in mm, in this order: \" + list_text(vs))\n    print(\"Two values are the same when they differ by at most \" + mm(tol) + \" mm.\")\n\ndef props(vs, tol):\n    p = near.properties(vs, tol)\n    print(\"  reflexive, a ~ a:            \" + str(p[1]) + \" of \" + str(p[0]) + \" values\")\n    print(\"  symmetric, a ~ b as b ~ a:   \" + str(p[3]) + \" of \" + str(p[2]) + \" pairs\")\n    print(\"  transitive, a ~ b ~ c gives a ~ c: \" + str(p[5]) + \" of \" + str(p[4]) + \" chains\")\n    if p[5] < p[4]:\n        print(\"Not transitive: so it does not split the values into groups, and 'the group of x' names nothing.\")\n\ndef this_order(vs, tol):\n    a = near.first_match(vs, tol)\n    c = near.canonical(near.chaining(vs, tol))\n    print(\"  first match: \" + groups_text(near.canonical(a)))\n    print(\"  chaining:    \" + groups_text(c))\n    span = 0\n    for g in c:\n        if g[len(g) - 1] - g[0] > span:\n            span = g[len(g) - 1] - g[0]\n    if span > tol:\n        print(\"  Chaining puts values \" + mm(span) + \" mm apart in one group, through the ones between.\")\n\ndef every(vs, tol):\n    r = near.every_order(vs, tol)\n    print(\"Every order of the \" + str(len(vs)) + \" values, \" + str(r[0]) + \" orders:\")\n    if r[1] == 1:\n        print(\"  first match: 1 grouping, of \" + str(r[2]) + \" groups\")\n    else:\n        print(\"  first match: \" + str(r[1]) + \" different groupings, from \" + str(r[2]) + \" to \" + str(r[3]) + \" groups\")\n    print(\"  chaining:    \" + str(r[4]) + \" grouping, the widest group spanning \" + mm(r[5]) + \" mm\")\n    if r[1] > 1:\n        print(\"First match depends on the order the values come in; chaining does not, and joins values far apart instead.\")\n\ndef show_records():\n    rs = near.records()\n    print(\"Records, matching when they share a name, an email or a phone:\")\n    for k in range(0, 4):\n        print(\"  r\" + str(k) + \"  \" + pad(rs[k][0], 11) + pad(rs[k][1], 10) + rs[k][2])\n    head = \"     r0 r1 r2 r3\"\n    print(head)\n    for i in range(0, 4):\n        line = \"  r\" + str(i)\n        for j in range(0, 4):\n            if near.same(rs[i], rs[j]):\n                line = line + \"  1\"\n            else:\n                line = line + \"  0\"\n        print(line)\n    r = near.record_orders()\n    print(\"Every order of the 4 records, \" + str(r[0]) + \" orders: first match gives \" + str(r[1]) + \" different answers, chaining \" + str(r[2]) + \".\")\n    print(\"Chaining puts r0 and r3 together, and they share nothing: the match is not transitive either.\")\n\ndef set_values(vs):\n    print(\"Values in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\")\n    t = plain(input(\"values> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return vs\n    out = []\n    w = \"\"\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                v = parse(w)\n                if v < 0:\n                    print(\"Each value is a number from 0.0 to 99.9, with at most one decimal.\")\n                    return vs\n                out = out + [v]\n            w = \"\"\n        else:\n            w = w + c\n    if len(out) < 2 or len(out) > 7:\n        print(\"Type 2 to 7 values.\")\n        return vs\n    print(\"Values now \" + list_text(out) + \".\")\n    return out\n\ndef parse(w):\n    a = \"\"\n    b = \"\"\n    dot = False\n    for c in w:\n        if c == \".\" and not dot:\n            dot = True\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                b = b + c\n            else:\n                a = a + c\n        else:\n            return 0 - 1\n    if len(a) == 0 or len(a) > 2 or len(b) > 1:\n        return 0 - 1\n    if dot and len(b) == 0:\n        return 0 - 1\n    v = 10 * int(a)\n    if len(b) == 1:\n        v = v + int(b)\n    return v\n\ndef set_tol(tol):\n    t = plain(input(\"tolerance in mm (0.0 to 5.0)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return tol\n    v = parse(t)\n    if v < 0 or v > 50:\n        print(\"Type a tolerance from 0.0 to 5.0, with at most one decimal.\")\n        return tol\n    print(\"Tolerance now \" + mm(v) + \" mm.\")\n    return v\n\nvs = [100, 103, 106, 109, 112, 130]\ntol = 3\nprint(\"== Tolerance grouping ==\")\nprint(\"Measurements count as the same when they differ by at most a tolerance; grouping them by it.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show(vs, tol)\n    elif choice == \"2\":\n        props(vs, tol)\n    elif choice == \"3\":\n        this_order(vs, tol)\n    elif choice == \"4\":\n        every(vs, tol)\n    elif choice == \"5\":\n        show_records()\n    elif choice == \"6\":\n        vs = set_values(vs)\n    elif choice == \"7\":\n        tol = set_tol(tol)\n    elif choice == \"8\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 8.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "near.eml",
      "eml": "# P139 tolerance grouping - measurements are \"the same\" when they differ\n# by at most the tolerance. As in the corpus case\n# tolerance-equality-grouping, that relation is reflexive and symmetric and\n# not transitive, so it does not split the values into groups: grouping the\n# first match decides by the order the values come in, and chaining joins\n# values far apart through the ones between. As in\n# match-predicate-not-transitive, a record match \"same email, or same\n# phone, or same name\" fails the same way.\n#\n# Values are whole tenths of a millimetre.\n\ndef near(a, b, tol):\n    return a - b <= tol and b - a <= tol\n\ndef properties(vs, tol):\n    # [values, reflexive, pairs, symmetric, chains a~b~c, transitive]\n    0 => refl\n    for a in vs:\n        if near(a, a, tol):\n            refl + 1 => refl\n    0 => pairs\n    0 => sym\n    0 => chains\n    0 => trans\n    for i in [0:len(vs) - 1]:\n        for j in [0:len(vs) - 1]:\n            pairs + 1 => pairs\n            if near(vs[i], vs[j], tol) == near(vs[j], vs[i], tol):\n                sym + 1 => sym\n            if i != j and near(vs[i], vs[j], tol):\n                for k in [0:len(vs) - 1]:\n                    if k != i and k != j and near(vs[j], vs[k], tol):\n                        chains + 1 => chains\n                        if near(vs[i], vs[k], tol):\n                            trans + 1 => trans\n    return [len(vs), refl, pairs, sym, chains, trans]\n\ndef first_match(vs, tol):\n    # each value joins the first group whose first value is near it\n    [] => groups\n    for v in vs:\n        0 - 1 => g\n        for k in [0:len(groups) - 1]:\n            if g < 0 and near(groups[k][0], v, tol):\n                k => g\n        if g < 0:\n            groups + [[v]] => groups\n        else:\n            groups[g] + [v] => groups[g]\n    return groups\n\ndef chaining(vs, tol):\n    # a value joins every group it is near, and those groups become one\n    [] => groups\n    for v in vs:\n        [v] => merged\n        [] => rest\n        for gr in groups:\n            False => touch\n            for x in gr:\n                if near(x, v, tol):\n                    True => touch\n            if touch:\n                merged + gr => merged\n            else:\n                rest + [gr] => rest\n        rest + [merged] => groups\n    return groups\n\ndef canonical(groups):\n    # the groups as sorted values, the groups in order of their smallest\n    [] => gs\n    for g in groups:\n        gs + [sort(g)] => gs\n    return sort_groups(gs)\n\ndef sort(xs):\n    [] => out\n    for x in xs:\n        [] => nxt\n        False => placed\n        for y in out:\n            if not placed and x < y:\n                nxt + [x] => nxt\n                True => placed\n            nxt + [y] => nxt\n        if not placed:\n            nxt + [x] => nxt\n        nxt => out\n    return out\n\ndef sort_groups(gs):\n    [] => out\n    for g in gs:\n        [] => nxt\n        False => placed\n        for h in out:\n            if not placed and g[0] < h[0]:\n                nxt + [g] => nxt\n                True => placed\n            nxt + [h] => nxt\n        if not placed:\n            nxt + [g] => nxt\n        nxt => out\n    return out\n\ndef orders(xs):\n    if len(xs) <= 1:\n        return [xs]\n    [] => out\n    for i in [0:len(xs) - 1]:\n        [] => rest\n        for j in [0:len(xs) - 1]:\n            if j != i:\n                rest + [xs[j]] => rest\n        for p in orders(rest):\n            out + [[xs[i]] + p] => out\n    return out\n\ndef every_order(vs, tol):\n    # [orders, different first-match groupings, fewest and most groups,\n    #  different chaining groupings, the widest chained group's span]\n    {} => fm\n    {} => ch\n    100000 => fewest\n    0 => most\n    0 => span\n    orders(vs) => os\n    for o in os:\n        first_match(o, tol) => a\n        1 => fm[str(canonical(a))]\n        if len(a) < fewest:\n            len(a) => fewest\n        if len(a) > most:\n            len(a) => most\n        canonical(chaining(o, tol)) => c\n        1 => ch[str(c)]\n        for g in c:\n            if g[len(g) - 1] - g[0] > span:\n                g[len(g) - 1] - g[0] => span\n    return [len(os), len(fm), fewest, most, len(ch), span]\n\ndef records():\n    # [name, email, phone]\n    return [[\"Pat Lee\", \"pat@home\", \"555-0101\"], [\"Pat Lee\", \"kim@work\", \"555-0199\"], [\"Kim Ito\", \"kim@work\", \"555-0123\"], [\"Kim Ito Jr\", \"kj@mail\", \"555-0123\"]]\n\ndef same(a, b):\n    return a[0] == b[0] or a[1] == b[1] or a[2] == b[2]\n\ndef record_groups(order, chain):\n    # the records' numbers, grouped by first match (each joins the first\n    # group whose first record it matches) or by chaining\n    records() => rs\n    [] => groups\n    for i in order:\n        if chain:\n            [i] => merged\n            [] => rest\n            for gr in groups:\n                False => touch\n                for x in gr:\n                    if same(rs[x], rs[i]):\n                        True => touch\n                if touch:\n                    merged + gr => merged\n                else:\n                    rest + [gr] => rest\n            rest + [merged] => groups\n        else:\n            0 - 1 => g\n            for k in [0:len(groups) - 1]:\n                if g < 0 and same(rs[groups[k][0]], rs[i]):\n                    k => g\n            if g < 0:\n                groups + [[i]] => groups\n            else:\n                groups[g] + [i] => groups[g]\n    return canonical(groups)\n\ndef record_orders():\n    # [orders, different first-match answers, different chaining answers]\n    {} => fm\n    {} => ch\n    orders([0, 1, 2, 3]) => os\n    for o in os:\n        1 => fm[str(record_groups(o, False))]\n        1 => ch[str(record_groups(o, True))]\n    return [len(os), len(fm), len(ch)]\n",
      "python": "def near(a, b, tol):\n    return a - b <= tol and b - a <= tol\n\ndef properties(vs, tol):\n    refl = 0\n    for a in vs:\n        if near(a, a, tol):\n            refl = refl + 1\n    pairs = 0\n    sym = 0\n    chains = 0\n    trans = 0\n    for i in range(0, len(vs)):\n        for j in range(0, len(vs)):\n            pairs = pairs + 1\n            if near(vs[i], vs[j], tol) == near(vs[j], vs[i], tol):\n                sym = sym + 1\n            if i != j and near(vs[i], vs[j], tol):\n                for k in range(0, len(vs)):\n                    if k != i and k != j and near(vs[j], vs[k], tol):\n                        chains = chains + 1\n                        if near(vs[i], vs[k], tol):\n                            trans = trans + 1\n    return [len(vs), refl, pairs, sym, chains, trans]\n\ndef first_match(vs, tol):\n    groups = []\n    for v in vs:\n        g = 0 - 1\n        for k in range(0, len(groups)):\n            if g < 0 and near(groups[k][0], v, tol):\n                g = k\n        if g < 0:\n            groups = groups + [[v]]\n        else:\n            groups[g] = groups[g] + [v]\n    return groups\n\ndef chaining(vs, tol):\n    groups = []\n    for v in vs:\n        merged = [v]\n        rest = []\n        for gr in groups:\n            touch = False\n            for x in gr:\n                if near(x, v, tol):\n                    touch = True\n            if touch:\n                merged = merged + gr\n            else:\n                rest = rest + [gr]\n        groups = rest + [merged]\n    return groups\n\ndef canonical(groups):\n    gs = []\n    for g in groups:\n        gs = gs + [sort(g)]\n    return sort_groups(gs)\n\ndef sort(xs):\n    out = []\n    for x in xs:\n        nxt = []\n        placed = False\n        for y in out:\n            if not placed and x < y:\n                nxt = nxt + [x]\n                placed = True\n            nxt = nxt + [y]\n        if not placed:\n            nxt = nxt + [x]\n        out = nxt\n    return out\n\ndef sort_groups(gs):\n    out = []\n    for g in gs:\n        nxt = []\n        placed = False\n        for h in out:\n            if not placed and g[0] < h[0]:\n                nxt = nxt + [g]\n                placed = True\n            nxt = nxt + [h]\n        if not placed:\n            nxt = nxt + [g]\n        out = nxt\n    return out\n\ndef orders(xs):\n    if len(xs) <= 1:\n        return [xs]\n    out = []\n    for i in range(0, len(xs)):\n        rest = []\n        for j in range(0, len(xs)):\n            if j != i:\n                rest = rest + [xs[j]]\n        for p in orders(rest):\n            out = out + [[xs[i]] + p]\n    return out\n\ndef every_order(vs, tol):\n    fm = {}\n    ch = {}\n    fewest = 100000\n    most = 0\n    span = 0\n    os = orders(vs)\n    for o in os:\n        a = first_match(o, tol)\n        fm[str(canonical(a))] = 1\n        if len(a) < fewest:\n            fewest = len(a)\n        if len(a) > most:\n            most = len(a)\n        c = canonical(chaining(o, tol))\n        ch[str(c)] = 1\n        for g in c:\n            if g[len(g) - 1] - g[0] > span:\n                span = g[len(g) - 1] - g[0]\n    return [len(os), len(fm), fewest, most, len(ch), span]\n\ndef records():\n    return [[\"Pat Lee\", \"pat@home\", \"555-0101\"], [\"Pat Lee\", \"kim@work\", \"555-0199\"], [\"Kim Ito\", \"kim@work\", \"555-0123\"], [\"Kim Ito Jr\", \"kj@mail\", \"555-0123\"]]\n\ndef same(a, b):\n    return a[0] == b[0] or a[1] == b[1] or a[2] == b[2]\n\ndef record_groups(order, chain):\n    rs = records()\n    groups = []\n    for i in order:\n        if chain:\n            merged = [i]\n            rest = []\n            for gr in groups:\n                touch = False\n                for x in gr:\n                    if same(rs[x], rs[i]):\n                        touch = True\n                if touch:\n                    merged = merged + gr\n                else:\n                    rest = rest + [gr]\n            groups = rest + [merged]\n        else:\n            g = 0 - 1\n            for k in range(0, len(groups)):\n                if g < 0 and same(rs[groups[k][0]], rs[i]):\n                    g = k\n            if g < 0:\n                groups = groups + [[i]]\n            else:\n                groups[g] = groups[g] + [i]\n    return canonical(groups)\n\ndef record_orders():\n    fm = {}\n    ch = {}\n    os = orders([0, 1, 2, 3])\n    for o in os:\n        fm[str(record_groups(o, False))] = 1\n        ch[str(record_groups(o, True))] = 1\n    return [len(os), len(fm), len(ch)]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n6\n\n6\n1.0\n6\n1 2 3 4 5 6 7 8\n6\n1.23\n6\nx\n6\n100.0\n6\n1.\n7\n\n7\n5.1\n7\n-1\n7\n0\n4\n2\n8\n",
      "screen": "== Tolerance grouping ==\nMeasurements count as the same when they differ by at most a tolerance; grouping them by it.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 0\nPick a number from 1 to 8.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> x\nPick a number from 1 to 8.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> \nCancelled.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> 1.0\nType 2 to 7 values.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> 1 2 3 4 5 6 7 8\nType 2 to 7 values.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> 1.23\nEach value is a number from 0.0 to 99.9, with at most one decimal.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> x\nEach value is a number from 0.0 to 99.9, with at most one decimal.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> 100.0\nEach value is a number from 0.0 to 99.9, with at most one decimal.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> 1.\nEach value is a number from 0.0 to 99.9, with at most one decimal.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 7\ntolerance in mm (0.0 to 5.0)> \nCancelled.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 7\ntolerance in mm (0.0 to 5.0)> 5.1\nType a tolerance from 0.0 to 5.0, with at most one decimal.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 7\ntolerance in mm (0.0 to 5.0)> -1\nType a tolerance from 0.0 to 5.0, with at most one decimal.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 7\ntolerance in mm (0.0 to 5.0)> 0\nTolerance now 0.0 mm.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 4\nEvery order of the 6 values, 720 orders:\n  first match: 1 grouping, of 6 groups\n  chaining:    1 grouping, the widest group spanning 0.0 mm\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 2\n  reflexive, a ~ a:            6 of 6 values\n  symmetric, a ~ b as b ~ a:   36 of 36 pairs\n  transitive, a ~ b ~ c gives a ~ c: 0 of 0 chains\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n5\n6\n11.2 10.0 13.0 10.6 10.9 10.3\n3\n7\n1.0\n4\n8\n",
      "screen": "== Tolerance grouping ==\nMeasurements count as the same when they differ by at most a tolerance; grouping them by it.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 1\nValues, in mm, in this order: 10.0 10.3 10.6 10.9 11.2 13.0\nTwo values are the same when they differ by at most 0.3 mm.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 2\n  reflexive, a ~ a:            6 of 6 values\n  symmetric, a ~ b as b ~ a:   36 of 36 pairs\n  transitive, a ~ b ~ c gives a ~ c: 0 of 6 chains\nNot transitive: so it does not split the values into groups, and 'the group of x' names nothing.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 3\n  first match: {10.0 10.3}  {10.6 10.9}  {11.2}  {13.0}\n  chaining:    {10.0 10.3 10.6 10.9 11.2}  {13.0}\n  Chaining puts values 1.2 mm apart in one group, through the ones between.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 4\nEvery order of the 6 values, 720 orders:\n  first match: 6 different groupings, from 3 to 4 groups\n  chaining:    1 grouping, the widest group spanning 1.2 mm\nFirst match depends on the order the values come in; chaining does not, and joins values far apart instead.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 5\nRecords, matching when they share a name, an email or a phone:\n  r0  Pat Lee    pat@home  555-0101\n  r1  Pat Lee    kim@work  555-0199\n  r2  Kim Ito    kim@work  555-0123\n  r3  Kim Ito Jr kj@mail   555-0123\n     r0 r1 r2 r3\n  r0  1  1  0  0\n  r1  1  1  1  0\n  r2  0  1  1  1\n  r3  0  0  1  1\nEvery order of the 4 records, 24 orders: first match gives 3 different answers, chaining 1.\nChaining puts r0 and r3 together, and they share nothing: the match is not transitive either.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 6\nValues in mm with one decimal, such as 10.0 10.3 12.5 (2 to 7 values, 0.0 to 99.9).\nvalues> 11.2 10.0 13.0 10.6 10.9 10.3\nValues now 11.2 10.0 13.0 10.6 10.9 10.3.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 3\n  first match: {10.0 10.3}  {10.6}  {10.9 11.2}  {13.0}\n  chaining:    {10.0 10.3 10.6 10.9 11.2}  {13.0}\n  Chaining puts values 1.2 mm apart in one group, through the ones between.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 7\ntolerance in mm (0.0 to 5.0)> 1.0\nTolerance now 1.0 mm.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 4\nEvery order of the 6 values, 720 orders:\n  first match: 3 different groupings, from 2 to 3 groups\n  chaining:    1 grouping, the widest group spanning 1.2 mm\nFirst match depends on the order the values come in; chaining does not, and joins values far apart instead.\n\n1) values  2) properties  3) this order  4) every order  5) records  6) new values  7) tolerance  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "tolerance-equality-grouping",
      "caseId": "272-tolerance-equality-grouping",
      "title": "Tolerance equality — no implementation is wrong, because nothing is right"
    },
    {
      "slug": "match-predicate-not-transitive",
      "caseId": "299-match-predicate-not-transitive",
      "title": "Match predicate not transitive — there is no grouping to be right about"
    }
  ],
  "updated": "2026-10-17"
}
