<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P006-inventory/ | updated: 2026-09-28 -->

# P006 Inventory

Items with a unit cost and a safety level, stock received and shipped with every movement logged, a warning for anything below its safety level, and the value of the whole stock - from a text menu.

EML-P project `projects/inventory` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/inventory` runs it and `eml project verify projects/inventory` 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: inventory-tracker (https://efficientnewlanguage.org/cases/025-inventory-tracker/), inventory-ledger (https://efficientnewlanguage.org/cases/161-inventory-ledger/).

## Sessions

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

Input:

```text
9
4
5
6
1

1
bolt m6
1
VERY-LONG-CODE-1
1
B1

1
B1
Bolt
abc
1
B1
Bolt
1.234
1
B1
Bolt
0
1
B1
Bolt
0.10
x
1
B1
Bolt
0.10
-1
1
B1
Bolt
0.10
10
1
b1
2
ZZ
2
B1
0
2
B1
abc
2
B1
100001
3
B1
1
2
B1
10
3
B1
11
3
B1
10
6
7
```

Screen:

```text

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 9
Pick a number from 1 to 7.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 4

-- items: 0 --
  (nothing here)
  total value                                              0.00

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 5

-- below safety level: 0 --
  Nothing is below its safety level.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 6

-- movements: 0 --
  (nothing here)

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> 
An item code has 2 to 12 characters.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> bolt m6
An item code has only letters, digits and dashes.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> VERY-LONG-CODE-1
An item code has 2 to 12 characters.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> 
A name has 1 to 18 characters.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> Bolt
unit cost> abc
A unit cost looks like 4 or 4.50, from 0.01 to 1,000,000.00.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> Bolt
unit cost> 1.234
A unit cost looks like 4 or 4.50, from 0.01 to 1,000,000.00.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> Bolt
unit cost> 0
A unit cost looks like 4 or 4.50, from 0.01 to 1,000,000.00.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> Bolt
unit cost> 0.10
safety level> x
A safety level is a whole number from 0 to 100,000.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> Bolt
unit cost> 0.10
safety level> -1
A safety level is a whole number from 0 to 100,000.

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> B1
name> Bolt
unit cost> 0.10
safety level> 10
Added B1: Bolt, 0.10 each, safety 10.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> b1
There is already an item B1.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> ZZ
No item ZZ.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> B1
quantity> 0
A quantity is a whole number from 1 to 100,000.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> B1
quantity> abc
A quantity is a whole number from 1 to 100,000.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> B1
quantity> 100001
A quantity is a whole number from 1 to 100,000.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 3
item code> B1
quantity> 1
Cannot ship 1 B1: Only 0 on hand.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> B1
quantity> 10
Received 10 B1; on hand 10.

== Inventory: 1 item, value 1.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 3
item code> B1
quantity> 11
Cannot ship 11 B1: Only 10 on hand.

== Inventory: 1 item, value 1.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 3
item code> B1
quantity> 10
Shipped 10 B1; on hand 0.
Warning: B1 is below its safety level of 10.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 6

-- movements: 2 --
    1  in  B1             10   on hand 10
    2  out B1             10   on hand 0

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 7
Bye.
```

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

Input:

```text
1
bolt-m6
Bolt M6
0.12
500
1
NUT-M6
Nut M6
0.05
500
1
DRILL-10
Drill bit 10 mm
45
5
2
BOLT-M6
1200
2
nut-m6
800
2
DRILL-10
30
3
BOLT-M6
750
3
DRILL-10
27
4
5
6
2
BOLT-M6
100
5
7
```

Screen:

```text

== Inventory: 0 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> bolt-m6
name> Bolt M6
unit cost> 0.12
safety level> 500
Added BOLT-M6: Bolt M6, 0.12 each, safety 500.

== Inventory: 1 item, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> NUT-M6
name> Nut M6
unit cost> 0.05
safety level> 500
Added NUT-M6: Nut M6, 0.05 each, safety 500.

== Inventory: 2 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 1
item code> DRILL-10
name> Drill bit 10 mm
unit cost> 45
safety level> 5
Added DRILL-10: Drill bit 10 mm, 45.00 each, safety 5.

== Inventory: 3 items, value 0.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> BOLT-M6
quantity> 1200
Received 1200 BOLT-M6; on hand 1200.

== Inventory: 3 items, value 144.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> nut-m6
quantity> 800
Received 800 NUT-M6; on hand 800.

== Inventory: 3 items, value 184.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> DRILL-10
quantity> 30
Received 30 DRILL-10; on hand 30.

== Inventory: 3 items, value 1,534.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 3
item code> BOLT-M6
quantity> 750
Shipped 750 BOLT-M6; on hand 450.
Warning: BOLT-M6 is below its safety level of 500.

== Inventory: 3 items, value 1,444.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 3
item code> DRILL-10
quantity> 27
Shipped 27 DRILL-10; on hand 3.
Warning: DRILL-10 is below its safety level of 5.

== Inventory: 3 items, value 229.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 4

-- items: 3 --
  BOLT-M6   Bolt M6               450 x      0.12 =       54.00  LOW
  NUT-M6    Nut M6                800 x      0.05 =       40.00
  DRILL-10  Drill bit 10 mm         3 x     45.00 =      135.00  LOW
  total value                                            229.00

== Inventory: 3 items, value 229.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 5

-- below safety level: 2 --
  BOLT-M6   450 on hand, safety 500: order at least 50
  DRILL-10  3 on hand, safety 5: order at least 2

== Inventory: 3 items, value 229.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 6

-- movements: 5 --
    1  in  BOLT-M6      1200   on hand 1200
    2  in  NUT-M6        800   on hand 800
    3  in  DRILL-10       30   on hand 30
    4  out BOLT-M6       750   on hand 450
    5  out DRILL-10       27   on hand 3

== Inventory: 3 items, value 229.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 2
item code> BOLT-M6
quantity> 100
Received 100 BOLT-M6; on hand 550.

== Inventory: 3 items, value 241.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 5

-- below safety level: 1 --
  DRILL-10  3 on hand, safety 5: order at least 2

== Inventory: 3 items, value 241.00 ==
1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit
choice> 7
Bye.
```

## Modules

### main.eml

```eml
# P006 inventory: items with a unit cost and a safety level; stock received
# and shipped, every movement logged; a warning for anything below its
# safety level; the value of the whole stock. The stock lives while the
# program runs; the first twenty projects do not save to files.
import stock
import fields

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def ask_code(inv, must_exist):
    # An item code, upper-cased: one in the stock when must_exist, a new one
    # otherwise. None once the problem has been shown.
    fields.upper(fields.trim(input("item code> "))) => code
    fields.code_problem(code) => problem
    if problem != "":
        problem ^0
        return None
    if must_exist and not (code in inv.items):
        ("No item " + code + ".") ^0
        return None
    if not must_exist and code in inv.items:
        ("There is already an item " + code + ".") ^0
        return None
    return code

def ask_quantity():
    # A whole number from 1 to 100,000, or -1 once the problem has been shown.
    fields.number(fields.trim(input("quantity> "))) => q
    if q < 1 or q > 100000:
        "A quantity is a whole number from 1 to 100,000." ^0
        return -1
    return q

def show_items(inv):
    "" ^0
    ("-- items: " + str(len(inv.items)) + " --") ^0
    if len(inv.items) == 0:
        "  (nothing here)" ^0
    for code in inv.items:
        inv.items[code] => item
        ("  " + ("%-10s" % code) + ("%-18s" % item[0]) + ("%7d" % item[1]) + " x " + ("%9s" % fields.money(item[2])) + " = " + ("%11s" % fields.money(inv.value(code)))) => line
        if inv.short(code) > 0:
            line + "  LOW" => line
        line ^0
    ("  " + ("%-50s" % "total value") + ("%11s" % fields.money(inv.total()))) ^0

def show_low(inv):
    inv.low() => codes
    "" ^0
    ("-- below safety level: " + str(len(codes)) + " --") ^0
    if len(codes) == 0:
        "  Nothing is below its safety level." ^0
    for code in codes:
        inv.items[code] => item
        ("  " + ("%-10s" % code) + str(item[1]) + " on hand, safety " + str(item[3]) + ": order at least " + str(inv.short(code))) ^0

def show_log(inv):
    "" ^0
    ("-- movements: " + str(len(inv.log)) + " --") ^0
    if len(inv.log) == 0:
        "  (nothing here)" ^0
    for m in inv.log:
        ("  " + ("%3d" % m[0]) + "  " + ("%-4s" % m[1]) + ("%-10s" % m[2]) + ("%7d" % m[3]) + "   on hand " + str(m[4])) ^0

stock.Stock() => inv
True => running
while running:
    "" ^0
    ("== Inventory: " + plural(len(inv.items), "item") + ", value " + fields.money(inv.total()) + " ==") ^0
    "1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit" ^0
    fields.trim(input("choice> ")) => choice
    if choice == "1":
        ask_code(inv, False) => code
        if code != None:
            fields.trim(input("name> ")) => name
            if name == "" or len(name) > 18:
                "A name has 1 to 18 characters." ^0
            else:
                fields.parse_cents(fields.trim(input("unit cost> "))) => cost
                if cost < 1 or cost > 100000000:
                    "A unit cost looks like 4 or 4.50, from 0.01 to 1,000,000.00." ^0
                else:
                    fields.number(fields.trim(input("safety level> "))) => safety
                    if safety < 0 or safety > 100000:
                        "A safety level is a whole number from 0 to 100,000." ^0
                    else:
                        inv.add(code, name, cost, safety)
                        ("Added " + code + ": " + name + ", " + fields.money(cost) + " each, safety " + str(safety) + ".") ^0
    elif choice == "2":
        ask_code(inv, True) => code
        if code != None:
            ask_quantity() => q
            if q > 0:
                inv.receive(code, q)
                ("Received " + str(q) + " " + code + "; on hand " + str(inv.items[code][1]) + ".") ^0
    elif choice == "3":
        ask_code(inv, True) => code
        if code != None:
            ask_quantity() => q
            if q > 0:
                try:
                    inv.ship(code, q)
                    ("Shipped " + str(q) + " " + code + "; on hand " + str(inv.items[code][1]) + ".") ^0
                    if inv.short(code) > 0:
                        ("Warning: " + code + " is below its safety level of " + str(inv.items[code][3]) + ".") ^0
                except ValueError as e:
                    ("Cannot ship " + str(q) + " " + code + ": " + str(e)) ^0
    elif choice == "4":
        show_items(inv)
    elif choice == "5":
        show_low(inv)
    elif choice == "6":
        show_log(inv)
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import stock
import fields

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def ask_code(inv, must_exist):
    code = fields.upper(fields.trim(input("item code> ")))
    problem = fields.code_problem(code)
    if problem != "":
        print(problem)
        return None
    if must_exist and not code in inv.items:
        print("No item " + code + ".")
        return None
    if not must_exist and code in inv.items:
        print("There is already an item " + code + ".")
        return None
    return code

def ask_quantity():
    q = fields.number(fields.trim(input("quantity> ")))
    if q < 1 or q > 100000:
        print("A quantity is a whole number from 1 to 100,000.")
        return -1
    return q

def show_items(inv):
    print("")
    print("-- items: " + str(len(inv.items)) + " --")
    if len(inv.items) == 0:
        print("  (nothing here)")
    for code in inv.items:
        item = inv.items[code]
        line = "  " + "%-10s" % code + "%-18s" % item[0] + "%7d" % item[1] + " x " + "%9s" % fields.money(item[2]) + " = " + "%11s" % fields.money(inv.value(code))
        if inv.short(code) > 0:
            line = line + "  LOW"
        print(line)
    print("  " + "%-50s" % "total value" + "%11s" % fields.money(inv.total()))

def show_low(inv):
    codes = inv.low()
    print("")
    print("-- below safety level: " + str(len(codes)) + " --")
    if len(codes) == 0:
        print("  Nothing is below its safety level.")
    for code in codes:
        item = inv.items[code]
        print("  " + "%-10s" % code + str(item[1]) + " on hand, safety " + str(item[3]) + ": order at least " + str(inv.short(code)))

def show_log(inv):
    print("")
    print("-- movements: " + str(len(inv.log)) + " --")
    if len(inv.log) == 0:
        print("  (nothing here)")
    for m in inv.log:
        print("  " + "%3d" % m[0] + "  " + "%-4s" % m[1] + "%-10s" % m[2] + "%7d" % m[3] + "   on hand " + str(m[4]))

inv = stock.Stock()
running = True
while running:
    print("")
    print("== Inventory: " + plural(len(inv.items), "item") + ", value " + fields.money(inv.total()) + " ==")
    print("1) add item  2) receive  3) ship  4) list  5) low stock  6) movements  7) quit")
    choice = fields.trim(input("choice> "))
    if choice == "1":
        code = ask_code(inv, False)
        if code != None:
            name = fields.trim(input("name> "))
            if name == "" or len(name) > 18:
                print("A name has 1 to 18 characters.")
            else:
                cost = fields.parse_cents(fields.trim(input("unit cost> ")))
                if cost < 1 or cost > 100000000:
                    print("A unit cost looks like 4 or 4.50, from 0.01 to 1,000,000.00.")
                else:
                    safety = fields.number(fields.trim(input("safety level> ")))
                    if safety < 0 or safety > 100000:
                        print("A safety level is a whole number from 0 to 100,000.")
                    else:
                        inv.add(code, name, cost, safety)
                        print("Added " + code + ": " + name + ", " + fields.money(cost) + " each, safety " + str(safety) + ".")
    elif choice == "2":
        code = ask_code(inv, True)
        if code != None:
            q = ask_quantity()
            if q > 0:
                inv.receive(code, q)
                print("Received " + str(q) + " " + code + "; on hand " + str(inv.items[code][1]) + ".")
    elif choice == "3":
        code = ask_code(inv, True)
        if code != None:
            q = ask_quantity()
            if q > 0:
                try:
                    inv.ship(code, q)
                    print("Shipped " + str(q) + " " + code + "; on hand " + str(inv.items[code][1]) + ".")
                    if inv.short(code) > 0:
                        print("Warning: " + code + " is below its safety level of " + str(inv.items[code][3]) + ".")
                except ValueError as e:
                    print("Cannot ship " + str(q) + " " + code + ": " + str(e))
    elif choice == "4":
        show_items(inv)
    elif choice == "5":
        show_low(inv)
    elif choice == "6":
        show_log(inv)
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")
```

### stock.eml

```eml
# P006 inventory - the stock. Items live in a dict keyed by item code, which
# iterates in the order the items were added, so every report comes out in
# that order. An item is a list [name, on_hand, unit_cost_cents, safety].
# Every receipt and shipment is logged as [n, kind, code, quantity,
# on_hand_after]; a shipment larger than what is on hand is refused with a
# ValueError and changes nothing.

class Stock:
    def __init__(self):
        {} => self.items
        [] => self.log

    def add(self, code, name, cost, safety):
        [name, 0, cost, safety] => self.items[code]

    def receive(self, code, quantity):
        self.items[code] => item
        item[1] + quantity => item[1]
        self.record("in", code, quantity)

    def ship(self, code, quantity):
        self.items[code] => item
        if quantity > item[1]:
            raise ValueError("Only " + str(item[1]) + " on hand.")
        item[1] - quantity => item[1]
        self.record("out", code, quantity)

    def record(self, kind, code, quantity):
        self.log + [[len(self.log) + 1, kind, code, quantity, self.items[code][1]]] => self.log

    def value(self, code):
        # What the item's stock cost, in cents.
        self.items[code] => item
        return item[1] * item[2]

    def total(self):
        0 => cents
        for code in self.items:
            cents + self.value(code) => cents
        return cents

    def short(self, code):
        # How many the item is below its safety level; 0 if it is not.
        self.items[code] => item
        if item[1] < item[3]:
            return item[3] - item[1]
        return 0

    def low(self):
        # The codes of the items below their safety level, in the order added.
        return [code for code in self.items if self.short(code) > 0]
```

Python projection of stock.eml:

```python
class Stock:
    def __init__(self):
        self.items = {}
        self.log = []
    def add(self, code, name, cost, safety):
        self.items[code] = [name, 0, cost, safety]
    def receive(self, code, quantity):
        item = self.items[code]
        item[1] = item[1] + quantity
        self.record("in", code, quantity)
    def ship(self, code, quantity):
        item = self.items[code]
        if quantity > item[1]:
            raise ValueError("Only " + str(item[1]) + " on hand.")
        item[1] = item[1] - quantity
        self.record("out", code, quantity)
    def record(self, kind, code, quantity):
        self.log = self.log + [[len(self.log) + 1, kind, code, quantity, self.items[code][1]]]
    def value(self, code):
        item = self.items[code]
        return item[1] * item[2]
    def total(self):
        cents = 0
        for code in self.items:
            cents = cents + self.value(code)
        return cents
    def short(self, code):
        item = self.items[code]
        if item[1] < item[3]:
            return item[3] - item[1]
        return 0
    def low(self):
        return [code for code in self.items if self.short(code) > 0]
```

### fields.eml

```eml
# P006 inventory - reading and writing what is typed: item codes, whole
# numbers and money. Money is whole cents from the moment it is typed: "4.50"
# is read digit by digit into 450 and never passes through a float. The
# interpreter that checks every session does not run string methods yet, so
# the text handling is written out here.

def trim(s):
    # s without the spaces at either end.
    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 upper(s):
    # s with a-z turned into A-Z; everything else as it was.
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if lower_letters[i] == c:
                upper_letters[i] => d
        out + d => out
    return out

def code_problem(code):
    # What is wrong with code as an item code, or "" if nothing is: 2 to 12
    # letters, digits and dashes.
    if len(code) < 2 or len(code) > 12:
        return "An item code has 2 to 12 characters."
    for c in code:
        if not (c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"):
            return "An item code has only letters, digits and dashes."
    return ""

def number(s):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return -1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return -1
        n * 10 + int(c) => n
    return n

def parse_cents(s):
    # The amount in s in whole cents, or -1 if s is not written like 4, 4.5
    # or 4.50: digits, then a point and one or two digits if any.
    0 => whole
    0 => whole_digits
    0 => frac
    0 => frac_digits
    False => point
    for c in s:
        if c == ".":
            if point:
                return -1
            True => point
        elif c in "0123456789":
            if point:
                frac * 10 + int(c) => frac
                frac_digits + 1 => frac_digits
            else:
                whole * 10 + int(c) => whole
                whole_digits + 1 => whole_digits
        else:
            return -1
    if whole_digits == 0:
        return -1
    if point and (frac_digits == 0 or frac_digits > 2):
        return -1
    if frac_digits == 1:
        frac * 10 => frac
    return whole * 100 + frac

def money(c):
    # Whole cents as 1,234.50, built from the digits of the integer.
    str(c) => s
    while len(s) < 3:
        "0" + s => s
    s[0:len(s) - 2] => whole
    s[len(s) - 2:len(s)] => cents
    "" => grouped
    len(whole) => i
    while i > 3:
        "," + whole[i - 3:i] + grouped => grouped
        i - 3 => i
    return whole[0:i] + grouped + "." + cents
```

Python projection of fields.eml:

```python
def trim(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 upper(s):
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if lower_letters[i] == c:
                d = upper_letters[i]
        out = out + d
    return out

def code_problem(code):
    if len(code) < 2 or len(code) > 12:
        return "An item code has 2 to 12 characters."
    for c in code:
        if not c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-":
            return "An item code has only letters, digits and dashes."
    return ""

def number(s):
    if s == "":
        return -1
    n = 0
    for c in s:
        if not c in "0123456789":
            return -1
        n = n * 10 + int(c)
    return n

def parse_cents(s):
    whole = 0
    whole_digits = 0
    frac = 0
    frac_digits = 0
    point = False
    for c in s:
        if c == ".":
            if point:
                return -1
            point = True
        elif c in "0123456789":
            if point:
                frac = frac * 10 + int(c)
                frac_digits = frac_digits + 1
            else:
                whole = whole * 10 + int(c)
                whole_digits = whole_digits + 1
        else:
            return -1
    if whole_digits == 0:
        return -1
    if point and (frac_digits == 0 or frac_digits > 2):
        return -1
    if frac_digits == 1:
        frac = frac * 10
    return whole * 100 + frac

def money(c):
    s = str(c)
    while len(s) < 3:
        s = "0" + s
    whole = s[0:len(s) - 2]
    cents = s[len(s) - 2:len(s)]
    grouped = ""
    i = len(whole)
    while i > 3:
        grouped = "," + whole[i - 3:i] + grouped
        i = i - 3
    return whole[0:i] + grouped + "." + cents
```

## README

# P006 - Inventory

Keeps items, each with a code, a name, a unit cost and a safety level;
receives and ships stock, logging every movement; warns when an item falls
below its safety level; and shows the value of the whole stock on every menu.
A text menu; the stock lives while the program runs.

- `main.eml` - the menu loop, the questions asked with their checks, and the
  item list, the low-stock report and the movement log as they appear on screen
- `stock.eml` - the stock, a class: items in a dict keyed by code, which
  iterates in the order the items were added, so every report keeps that
  order; receiving, shipping (more than is on hand is refused with a
  ValueError and changes nothing), the log, values and shortfalls
- `fields.eml` - trimming, upper case, item codes, whole numbers, and money
  read digit by digit into whole cents and written as 1,234.50

Money is whole cents throughout: a unit cost is read without passing through a
float, and a value is quantity times cents.

What is checked: an item code has 2 to 12 letters, digits and dashes, is read
in upper case, and is new when adding and known when receiving or shipping; a
name has 1 to 18 characters; a unit cost is written like 4 or 4.50, from 0.01
to 1,000,000.00; a safety level is a whole number from 0 to 100,000, and a
quantity from 1 to 100,000. An item below its safety level is marked LOW in
the list, named after every shipment that leaves it there, and listed with how
many to order in the low-stock report.

Sessions: `sessions/basic.in` adds three items (codes typed in lower case
too), receives stock, ships two items below their safety levels, shows the
list, the low-stock report and the log, then restocks one item and shows the
report again; `sessions/bad-input.in` shows every report while empty, types
codes that are empty, contain a space or are too long, an empty name, unit
costs that are words, have three decimals or are zero, safety levels that are
words or negative, a code that is already there in lower case, a code that is
not there, quantities of zero, words and too many, and shipments of more than
is on hand, which are refused and never reach the log.

Built on the verified corpus cases `inventory-tracker` (stock as a class that
refuses to remove more than it has) and `inventory-ledger` (a dict of stock
levels reported in the order the items were added, with what to reorder).
