<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P148-price-table/ | updated: 2026-10-18 -->

# P148 Price table

A parcel rate card keyed on the weights as typed and on whole grams: computed weights that find no entry, and 1, 1.0 and True landing on one key so a misaligned checkbox sets the 1 kg price.

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

Built on verified corpus cases: float-key-instability (https://efficientnewlanguage.org/cases/221-float-key-instability/), dict-key-identity-collapse (https://efficientnewlanguage.org/cases/201-dict-key-identity-collapse/).

## Sessions

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

Input:

```text
0
x
3

3
abc
3
0.1.2
3
0
3
2.5
3
1 1 1
3
0.1 0.1 0.1 0.1 0.1 0.1 0.1
3
123456
3
1 1
5
```

Screen:

```text
== Price table ==
A parcel rate card looked up by weight: keyed on the weights as typed, and on whole grams.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 0
Pick a number from 1 to 5.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> x
Pick a number from 1 to 5.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 
Cancelled.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> abc
Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0.1.2
Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0
Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 2.5
Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 1 1 1
The parcel weighs over 2 kg; the card goes up to 2.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0.1 0.1 0.1 0.1 0.1 0.1 0.1
Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 123456
Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 1 1
Added as floats the weight is 2.0 kg; added as grams, 2000 g.
  keyed as typed: 12.50
  keyed on grams: 12.50

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 5
Bye.
```

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

Input:

```text
1
2
3
0.1 0.2
3
0.4 0.6
3
0.3
3
0.7 0.1 0.2
4
5
```

Screen:

```text
== Price table ==
A parcel rate card looked up by weight: keyed on the weights as typed, and on whole grams.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 1
The rate card, row by row, the weight as it was entered:
   1  0.1 (float)       3.00
   2  0.2 (float)       3.50
   3  0.3 (float)       4.00
   4  0.4 (float)       4.50
   5  0.5 (float)       5.00
   6  0.6 (float)       5.50
   7  0.7 (float)       6.00
   8  0.8 (float)       6.50
   9  0.9 (float)       7.00
  10  1 (int)           7.50
  11  1.1 (float)       8.00
  12  1.2 (float)       8.50
  13  1.3 (float)       9.00
  14  1.4 (float)       9.50
  15  1.5 (float)      10.00
  16  1.6 (float)      10.50
  17  1.7 (float)      11.00
  18  1.8 (float)      11.50
  19  1.9 (float)      12.00
  20  2 (int)          12.50
  21  1.0 (float)       9.00
  22  True (bool)       0.00

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 2
Keyed on the weights as typed: 20 entries from 22 rows.
  1, 1.0 and True are one key; rows 10 (1, int), 21 (1.0, float), 22 (True, bool) all wrote it, and the last one wins: 1 kg costs 0.00.
Keyed on grams, the True refused as not a number: 20 entries; 1000 g costs 9.00, the update typed 1.0.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0.1 0.2
Added as floats the weight is 0.30000000000000004 kg; added as grams, 300 g.
  keyed as typed: no entry for 0.30000000000000004, so the default 12.50 is charged
  keyed on grams: 4.00
The two tables charge 12.50 and 4.00 for the same parcel.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0.4 0.6
Added as floats the weight is 1.0 kg; added as grams, 1000 g.
  keyed as typed: 0.00
  keyed on grams: 9.00
The two tables charge 0.00 and 9.00 for the same parcel.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0.3
Added as floats the weight is 0.3 kg; added as grams, 300 g.
  keyed as typed: 4.00
  keyed on grams: 4.00

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 3
item weights in kg (e.g. 0.1 0.2)> 0.7 0.1 0.2
Added as floats the weight is 1.0 kg; added as grams, 1000 g.
  keyed as typed: 0.00
  keyed on grams: 9.00
The two tables charge 0.00 and 9.00 for the same parcel.

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 4
Every parcel of two of the items 0.1 to 0.9 kg: 45 parcels.
  keyed as typed: 8 find no entry and pay the default, as 0.1 + 0.2 = 0.30000000000000004; 5 find the 1 kg entry the checkbox wrote
  keyed on grams: every one finds its entry
  charged in all: 341.50 keyed as typed, 345.00 keyed on grams
  the weight in all is 45.000 kg both ways, to the gram: a check on the total passes while 13 of the 45 prices are wrong

1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P148 price table: a parcel rate card, the two tables built from it - keyed
# on the weights as typed and on whole grams - a parcel priced both ways,
# and every parcel of two items.
import rates

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 money(c):
    c % 100 => p
    if p < 10:
        return str(int((c - p) / 100)) + ".0" + str(p)
    return str(int((c - p) / 100)) + "." + str(p)

def kg(g):
    # whole grams as kg with three places
    g % 1000 => r
    if r < 10:
        return str(int((g - r) / 1000)) + ".00" + str(r)
    if r < 100:
        return str(int((g - r) / 1000)) + ".0" + str(r)
    return str(int((g - r) / 1000)) + "." + str(r)

def show_card():
    "The rate card, row by row, the weight as it was entered:" ^0
    1 => k
    for r in rates.card():
        ("  " + lpad(str(k), 2) + "  " + pad(str(r[0]) + " (" + rates.kind(r[0]) + ")", 16) + lpad(money(r[1]), 6)) ^0
        k + 1 => k

def show_tables():
    rates.float_table() => ft
    rates.gram_table() => gt
    len(rates.card()) => n
    ("Keyed on the weights as typed: " + str(len(ft)) + " entries from " + str(n) + " rows.") ^0
    "" => who
    1 => k
    for r in rates.card():
        if r[0] == 1:
            if who == "":
                str(k) + " (" + str(r[0]) + ", " + rates.kind(r[0]) + ")" => who
            else:
                who + ", " + str(k) + " (" + str(r[0]) + ", " + rates.kind(r[0]) + ")" => who
        k + 1 => k
    ("  1, 1.0 and True are one key; rows " + who + " all wrote it, and the last one wins: 1 kg costs " + money(ft[1]) + ".") ^0
    ("Keyed on grams, the True refused as not a number: " + str(len(gt)) + " entries; 1000 g costs " + money(gt[1000]) + ", the update typed 1.0.") ^0

def parse(t):
    # the weights in kg, or [] when one is not a weight from 0.001 to 2
    [] => out
    "" => cur
    for c in t + " ":
        if c == " ":
            if cur != "":
                0 => dots
                for d in cur:
                    if d == ".":
                        dots + 1 => dots
                    elif d < "0" or d > "9":
                        return []
                if dots > 1 or cur == "." or len(cur) > 5:
                    return []
                float(cur) => w
                if w < 0.001 or w > 2:
                    return []
                out + [w] => out
                "" => cur
        else:
            cur + c => cur
    return out

def price(ws):
    0 => total
    0 => g
    for w in ws:
        total + w => total
        g + rates.grams(w) => g
    rates.by_float(rates.float_table(), total) => pf
    rates.by_grams(rates.gram_table(), g) => pg
    ("Added as floats the weight is " + str(total) + " kg; added as grams, " + str(g) + " g.") ^0
    if pf < 0:
        ("  keyed as typed: no entry for " + str(total) + ", so the default " + money(rates.default_price()) + " is charged") ^0
        rates.default_price() => pf
    else:
        ("  keyed as typed: " + money(pf)) ^0
    if pg < 0:
        ("  keyed on grams: no entry for " + str(g) + " g, so the default " + money(rates.default_price()) + " is charged") ^0
        rates.default_price() => pg
    else:
        ("  keyed on grams: " + money(pg)) ^0
    return [pf, pg]

def one_parcel():
    plain(input("item weights in kg (e.g. 0.1 0.2)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    parse(t) => ws
    if len(ws) == 0 or len(ws) > 6:
        "Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces." ^0
        return
    0 => g
    for w in ws:
        g + rates.grams(w) => g
    if g > 2000:
        "The parcel weighs over 2 kg; the card goes up to 2." ^0
        return
    price(ws) => p
    if p[0] != p[1]:
        ("The two tables charge " + money(p[0]) + " and " + money(p[1]) + " for the same parcel.") ^0

def pairs():
    rates.float_table() => ft
    rates.gram_table() => gt
    rates.items() => its
    0 => n
    0 => miss
    0 => other
    0 => fsum
    0 => gsum
    0 => cf
    0 => cg
    "" => ex
    0 => i
    while i < len(its):
        i => j
        while j < len(its):
            its[i] + its[j] => total
            rates.grams(its[i]) + rates.grams(its[j]) => g
            n + 1 => n
            fsum + total => fsum
            gsum + g => gsum
            rates.by_float(ft, total) => pf
            rates.by_grams(gt, g) => pg
            if pf < 0:
                miss + 1 => miss
                rates.default_price() => pf
                if ex == "":
                    str(its[i]) + " + " + str(its[j]) + " = " + str(total) => ex
            elif pf != pg:
                other + 1 => other
            cf + pf => cf
            cg + pg => cg
            j + 1 => j
        i + 1 => i
    ("Every parcel of two of the items 0.1 to 0.9 kg: " + str(n) + " parcels.") ^0
    ("  keyed as typed: " + str(miss) + " find no entry and pay the default, as " + ex + "; " + str(other) + " find the 1 kg entry the checkbox wrote") ^0
    ("  keyed on grams: every one finds its entry") ^0
    ("  charged in all: " + money(cf) + " keyed as typed, " + money(cg) + " keyed on grams") ^0
    if rates.grams(fsum) == gsum:
        ("  the weight in all is " + kg(gsum) + " kg both ways, to the gram: a check on the total passes while " + str(miss + other) + " of the " + str(n) + " prices are wrong") ^0
    else:
        ("  the weight in all: " + str(fsum) + " kg added as floats, " + kg(gsum) + " kg as grams") ^0

"== Price table ==" ^0
"A parcel rate card looked up by weight: keyed on the weights as typed, and on whole grams." ^0
True => running
while running:
    "" ^0
    "1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_card()
    elif choice == "2":
        show_tables()
    elif choice == "3":
        one_parcel()
    elif choice == "4":
        pairs()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import rates

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 money(c):
    p = c % 100
    if p < 10:
        return str(int((c - p) / 100)) + ".0" + str(p)
    return str(int((c - p) / 100)) + "." + str(p)

def kg(g):
    r = g % 1000
    if r < 10:
        return str(int((g - r) / 1000)) + ".00" + str(r)
    if r < 100:
        return str(int((g - r) / 1000)) + ".0" + str(r)
    return str(int((g - r) / 1000)) + "." + str(r)

def show_card():
    print("The rate card, row by row, the weight as it was entered:")
    k = 1
    for r in rates.card():
        print("  " + lpad(str(k), 2) + "  " + pad(str(r[0]) + " (" + rates.kind(r[0]) + ")", 16) + lpad(money(r[1]), 6))
        k = k + 1

def show_tables():
    ft = rates.float_table()
    gt = rates.gram_table()
    n = len(rates.card())
    print("Keyed on the weights as typed: " + str(len(ft)) + " entries from " + str(n) + " rows.")
    who = ""
    k = 1
    for r in rates.card():
        if r[0] == 1:
            if who == "":
                who = str(k) + " (" + str(r[0]) + ", " + rates.kind(r[0]) + ")"
            else:
                who = who + ", " + str(k) + " (" + str(r[0]) + ", " + rates.kind(r[0]) + ")"
        k = k + 1
    print("  1, 1.0 and True are one key; rows " + who + " all wrote it, and the last one wins: 1 kg costs " + money(ft[1]) + ".")
    print("Keyed on grams, the True refused as not a number: " + str(len(gt)) + " entries; 1000 g costs " + money(gt[1000]) + ", the update typed 1.0.")

def parse(t):
    out = []
    cur = ""
    for c in t + " ":
        if c == " ":
            if cur != "":
                dots = 0
                for d in cur:
                    if d == ".":
                        dots = dots + 1
                    elif d < "0" or d > "9":
                        return []
                if dots > 1 or cur == "." or len(cur) > 5:
                    return []
                w = float(cur)
                if w < 0.001 or w > 2:
                    return []
                out = out + [w]
                cur = ""
        else:
            cur = cur + c
    return out

def price(ws):
    total = 0
    g = 0
    for w in ws:
        total = total + w
        g = g + rates.grams(w)
    pf = rates.by_float(rates.float_table(), total)
    pg = rates.by_grams(rates.gram_table(), g)
    print("Added as floats the weight is " + str(total) + " kg; added as grams, " + str(g) + " g.")
    if pf < 0:
        print("  keyed as typed: no entry for " + str(total) + ", so the default " + money(rates.default_price()) + " is charged")
        pf = rates.default_price()
    else:
        print("  keyed as typed: " + money(pf))
    if pg < 0:
        print("  keyed on grams: no entry for " + str(g) + " g, so the default " + money(rates.default_price()) + " is charged")
        pg = rates.default_price()
    else:
        print("  keyed on grams: " + money(pg))
    return [pf, pg]

def one_parcel():
    t = plain(input("item weights in kg (e.g. 0.1 0.2)> "))
    if t == "":
        print("Cancelled.")
        return
    ws = parse(t)
    if len(ws) == 0 or len(ws) > 6:
        print("Type 1 to 6 weights from 0.001 to 2 kg, separated by spaces.")
        return
    g = 0
    for w in ws:
        g = g + rates.grams(w)
    if g > 2000:
        print("The parcel weighs over 2 kg; the card goes up to 2.")
        return
    p = price(ws)
    if p[0] != p[1]:
        print("The two tables charge " + money(p[0]) + " and " + money(p[1]) + " for the same parcel.")

def pairs():
    ft = rates.float_table()
    gt = rates.gram_table()
    its = rates.items()
    n = 0
    miss = 0
    other = 0
    fsum = 0
    gsum = 0
    cf = 0
    cg = 0
    ex = ""
    i = 0
    while i < len(its):
        j = i
        while j < len(its):
            total = its[i] + its[j]
            g = rates.grams(its[i]) + rates.grams(its[j])
            n = n + 1
            fsum = fsum + total
            gsum = gsum + g
            pf = rates.by_float(ft, total)
            pg = rates.by_grams(gt, g)
            if pf < 0:
                miss = miss + 1
                pf = rates.default_price()
                if ex == "":
                    ex = str(its[i]) + " + " + str(its[j]) + " = " + str(total)
            elif pf != pg:
                other = other + 1
            cf = cf + pf
            cg = cg + pg
            j = j + 1
        i = i + 1
    print("Every parcel of two of the items 0.1 to 0.9 kg: " + str(n) + " parcels.")
    print("  keyed as typed: " + str(miss) + " find no entry and pay the default, as " + ex + "; " + str(other) + " find the 1 kg entry the checkbox wrote")
    print("  keyed on grams: every one finds its entry")
    print("  charged in all: " + money(cf) + " keyed as typed, " + money(cg) + " keyed on grams")
    if rates.grams(fsum) == gsum:
        print("  the weight in all is " + kg(gsum) + " kg both ways, to the gram: a check on the total passes while " + str(miss + other) + " of the " + str(n) + " prices are wrong")
    else:
        print("  the weight in all: " + str(fsum) + " kg added as floats, " + kg(gsum) + " kg as grams")

print("== Price table ==")
print("A parcel rate card looked up by weight: keyed on the weights as typed, and on whole grams.")
running = True
while running:
    print("")
    print("1) rate card  2) the two tables  3) price a parcel  4) every two-item parcel  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_card()
    elif choice == "2":
        show_tables()
    elif choice == "3":
        one_parcel()
    elif choice == "4":
        pairs()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### rates.eml

```eml
# P148 price table - a parcel rate card looked up by weight, keyed two
# ways. As in the corpus case float-key-instability, a table keyed on the
# weights a person typed has no entry for a weight that arrived by adding,
# 0.1 + 0.2 being 0.30000000000000004; keyed on whole grams, rounded once
# where a weight comes in, it finds every weight the card has. As in the
# corpus case dict-key-identity-collapse, 1, 1.0 and True are one key: the
# row for 1 kg, its later update typed 1.0, and a True a misaligned
# checkbox column put in the weight cell all land on it, and the last one
# written is the price.

def card():
    # [weight in kg as typed, price in cents], in the order the rows come
    [] => out
    1 => k
    while k <= 20:
        if k == 10:
            out + [[1, 300 + 50 * (k - 1)]] => out
        elif k == 20:
            out + [[2, 300 + 50 * (k - 1)]] => out
        else:
            out + [[k / 10, 300 + 50 * (k - 1)]] => out
        k + 1 => k
    return out + [[1.0, 900], [True, 0]]

def default_price():
    # charged when a weight has no entry: the 2 kg rate
    return 1250

def kind(w):
    str(w) => s
    if s == "True" or s == "False":
        return "bool"
    for c in s:
        if c == ".":
            return "float"
    return "int"

def grams(w):
    # a weight in kg to whole grams, rounded half up, once
    return int(w * 1000 + 0.5)

def float_table():
    {} => t
    for r in card():
        r[1] => t[r[0]]
    return t

def gram_table():
    # the same rows keyed on grams; a cell that is not a number is refused
    {} => t
    for r in card():
        if kind(r[0]) != "bool":
            r[1] => t[grams(r[0])]
    return t

def by_float(t, total):
    if total in t:
        return t[total]
    return 0 - 1

def by_grams(t, g):
    if g in t:
        return t[g]
    return 0 - 1

def items():
    return [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
```

Python projection of rates.eml:

```python
def card():
    out = []
    k = 1
    while k <= 20:
        if k == 10:
            out = out + [[1, 300 + 50 * (k - 1)]]
        elif k == 20:
            out = out + [[2, 300 + 50 * (k - 1)]]
        else:
            out = out + [[k / 10, 300 + 50 * (k - 1)]]
        k = k + 1
    return out + [[1.0, 900], [True, 0]]

def default_price():
    return 1250

def kind(w):
    s = str(w)
    if s == "True" or s == "False":
        return "bool"
    for c in s:
        if c == ".":
            return "float"
    return "int"

def grams(w):
    return int(w * 1000 + 0.5)

def float_table():
    t = {}
    for r in card():
        t[r[0]] = r[1]
    return t

def gram_table():
    t = {}
    for r in card():
        if kind(r[0]) != "bool":
            t[grams(r[0])] = r[1]
    return t

def by_float(t, total):
    if total in t:
        return t[total]
    return 0 - 1

def by_grams(t, g):
    if g in t:
        return t[g]
    return 0 - 1

def items():
    return [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
```

## README

# P148 - Price table

A parcel rate card looked up by weight, keyed two ways: on the weights as
they were typed, and on whole grams.

- `main.eml` - the menu, the rate card, the two tables built from it, a
  parcel priced both ways, and every parcel of two items
- `rates.eml` - the card, the default price, the kind of each weight cell,
  grams rounded half up, the two tables, and the two lookups

How each part works:

- The card has a row for every 0.1 kg up to 2 kg, the whole kilos typed as
  1 and 2, then two more rows: the 1 kg price updated, typed 1.0, and a
  True that a misaligned checkbox column put in the weight cell.
- As in the corpus case `dict-key-identity-collapse`, 1, 1.0 and True are
  one key: all three rows write it, the key stays the int 1 it was first
  written as, and the last row wins - 1 kg costs 0.00. Keyed on grams, the
  True is refused as not a number and 1000 g costs 9.00, the update.
- As in the corpus case `float-key-instability`, a parcel's weight arrives
  by adding its items: 0.1 + 0.2 is 0.30000000000000004, which the card
  keyed as typed has no entry for, so the default - the 2 kg rate - is
  charged. Keyed on grams, rounded once where each weight comes in, every
  weight on the card is found.
- Of the 45 parcels of two items from 0.1 to 0.9 kg, keyed as typed 8 find
  no entry and 5 find the 1 kg price the checkbox wrote; the weight in all
  is 45.000 kg both ways, to the gram - a check on the total passes while 13
  prices are wrong.

What is checked: menu choices 1 to 5; 1 to 6 weights from 0.001 to 2 kg,
each up to 5 characters, and a parcel of at most 2 kg. An empty answer
cancels. A weight that is not on the card is not found keyed either way,
and the default is charged.

Sessions: `sessions/basic.in` gives:
- the rate card and the two tables;
- parcels of 0.1 and 0.2, of 0.4 and 0.6, of 0.3 alone, and of 0.7, 0.1
  and 0.2; every two-item parcel.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- weights: a cancel, abc, 0.1.2, 0, 2.5, seven weights and 123456; a parcel
  of 3 kg;
- a parcel of 1 and 1, the 2 kg rate both ways.

Built on the verified corpus cases `float-key-instability` (a table keyed
on decimal amounts, looked up by computed sums, against integer cents) and
`dict-key-identity-collapse` (1, 1.0 and True as one key, the first key
kept and the last value).
