Project P123

Inventory diff

Two stock counts compared in both directions: added, gone, changed and renamed items, an item very likely moved to a new code, a units balance, and what a one-way check never sees.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-17

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

Two stock counts, last month's and this month's, each item a code, a name and a quantity, compared in both directions.

  • main.eml - the menu, both counts side by side, the differences, the one-way checks, and changes to the new count
  • stock.eml - the two counts, codes in order, the differences, items very likely moved to a new code, the units balance, and the changes

How each part works:

  • As in the corpus case inventory-diff-report, a difference has two directions: what is new now, and what is gone since. Checking each item now against before finds the added and the changed, and never the gone; checking before against now never finds the added.
  • Items on both counts are changed (another quantity), renamed (another name under the same code), or the same. A quantity of 0 is still on the list: counted and empty, not gone.
  • The same item counted under a new code shows up as one gone and one added. When a gone item and an added one have the same name and quantity, the program says it is very likely one item.
  • Units now - units before = added - gone + the change on the items in both: 537 - 561 = 42 - 17 - 49.
  • As in the corpus case dict-inventory-report, the counts are dicts keyed by code, always reported in sorted order.

What is checked: menu choices 1 to 5; a code of a capital letter and 3 digits; a quantity from 0 to 99999, or remove for an item on the new count; a name of up to 20 lowercase letters and spaces for a new one. An empty answer cancels.

Sessions: sessions/basic.in gives: - both counts, the differences and the one-way checks; - B205 taken off and B201 added back, after which brackets are the same item again; - glue from 0 to 4, the differences and the one-way checks.

sessions/bad-input.in gives: - menu choices 0 and x; - codes: a cancel, d400, D40 and DD400; - quantities for D400: a cancel, x, 100000 and -3; - a new code Z999: a cancel, Tape, a name too long, then tape with a cancel and remove, tape 2, and masking tape 7; - the differences.

Built on the verified corpus cases inventory-diff-report (a difference taken in both directions) and dict-inventory-report (counts in a dict, reported in a fixed order).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Inventory diff ==
Two stock counts, last month's and this month's, compared in both directions.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 0
Pick a number from 1 to 5.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> x
Pick a number from 1 to 5.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> 
Cancelled.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> d400
Type a capital letter and 3 digits, such as D400.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> D40
Type a capital letter and 3 digits, such as D400.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> DD400
Type a capital letter and 3 digits, such as D400.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> D400
quantity (0 to 99999, or remove)> 
Cancelled.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> D400
quantity (0 to 99999, or remove)> x
Type a whole number from 0 to 99999, or remove.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> D400
quantity (0 to 99999, or remove)> 100000
Type a whole number from 0 to 99999, or remove.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> D400
quantity (0 to 99999, or remove)> -3
Type a whole number from 0 to 99999, or remove.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> 
Cancelled.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> Tape
Type up to 20 lowercase letters and spaces.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> a very long name indeed yes
Type up to 20 lowercase letters and spaces.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> tape
quantity (0 to 99999, or remove)> 
Cancelled.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> tape
quantity (0 to 99999, or remove)> remove
Z999 is not on the new count.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> tape 2
Type up to 20 lowercase letters and spaces.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> Z999
name (up to 20 lowercase letters and spaces)> masking tape
quantity (0 to 99999, or remove)> 7
Z999 masking tape added to the new count, 7.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 2
Differences, before to now:
  added:   B205 brackets 12
  added:   D400 tape 30
  added:   Z999 masking tape 7
  gone:    B201 brackets 12
  gone:    C301 sealant 5
  changed: A101 nuts 300 -> 260 (-40)
  changed: C300 glue 9 -> 0 (-9)
           C300 is still on the list, at 0: counted and empty, not gone.
  renamed: B200 hinges -> door hinges
  the same: 2 items
Very likely one item under a new code: B201 brackets is gone and B205 brackets added, with the same name and quantity (12).
Units: 561 before, 544 now: +49 added, -17 gone, -49 on the items in both.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 5
Bye.
What was typed (48 lines)
0
x
4

4
d400
4
D40
4
DD400
4
D400

4
D400
x
4
D400
100000
4
D400
-3
4
Z999

4
Z999
Tape
4
Z999
a very long name indeed yes
4
Z999
tape

4
Z999
tape
remove
4
Z999
tape 2
4
Z999
masking tape
7
2
5

basic

interpreter: byte-equal
== Inventory diff ==
Two stock counts, last month's and this month's, compared in both directions.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 1
  code  before                now
  A100  bolts 120             bolts 120
  A101  nuts 300              nuts 260
  A102  washers 75            washers 75
  B200  hinges 40             door hinges 40
  B201  brackets 12           -
  B205  -                     brackets 12
  C300  glue 9                glue 0
  C301  sealant 5             -
  D400  -                     tape 30
  7 items before, 7 now.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 2
Differences, before to now:
  added:   B205 brackets 12
  added:   D400 tape 30
  gone:    B201 brackets 12
  gone:    C301 sealant 5
  changed: A101 nuts 300 -> 260 (-40)
  changed: C300 glue 9 -> 0 (-9)
           C300 is still on the list, at 0: counted and empty, not gone.
  renamed: B200 hinges -> door hinges
  the same: 2 items
Very likely one item under a new code: B201 brackets is gone and B205 brackets added, with the same name and quantity (12).
Units: 561 before, 537 now: +42 added, -17 gone, -49 on the items in both.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 3
Checking each item now against before finds B205, D400, A101 and C300 - and never what is gone: B201 and C301.
Checking each item before against now finds B201, C301, A101 and C300 - and never what is new: B205 and D400.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> B205
quantity (0 to 99999, or remove)> remove
B205 brackets taken off the new count.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> B201
name (up to 20 lowercase letters and spaces)> brackets
quantity (0 to 99999, or remove)> 12
B201 brackets added to the new count, 12.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 2
Differences, before to now:
  added:   D400 tape 30
  gone:    C301 sealant 5
  changed: A101 nuts 300 -> 260 (-40)
  changed: C300 glue 9 -> 0 (-9)
           C300 is still on the list, at 0: counted and empty, not gone.
  renamed: B200 hinges -> door hinges
  the same: 3 items
Units: 561 before, 537 now: +30 added, -5 gone, -49 on the items in both.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 4
code (a capital letter and 3 digits, such as D400)> C300
quantity (0 to 99999, or remove)> 4
C300 glue: 0 -> 4.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 2
Differences, before to now:
  added:   D400 tape 30
  gone:    C301 sealant 5
  changed: A101 nuts 300 -> 260 (-40)
  changed: C300 glue 9 -> 4 (-5)
  renamed: B200 hinges -> door hinges
  the same: 3 items
Units: 561 before, 541 now: +30 added, -5 gone, -45 on the items in both.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 3
Checking each item now against before finds D400, A101 and C300 - and never what is gone: C301.
Checking each item before against now finds C301, A101 and C300 - and never what is new: D400.

1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit
choice> 5
Bye.
What was typed (17 lines)
1
2
3
4
B205
remove
4
B201
brackets
12
2
4
C300
4
2
3
5

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P123 inventory diff: two stock counts side by side, the differences in
# both directions, what a one-way check misses, and changes to the new
# count.
import stock

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 names_text(ns):
    "" => out
    for i in [0:len(ns) - 1]:
        if i > 0 and i == len(ns) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + ns[i] => out
    return out

def signed(n):
    if n > 0:
        return "+" + str(n)
    return str(n)

def item_text(count, code):
    return code + " " + count[code][0] + " " + str(count[code][1])

def show(before, now):
    "  code  before                now" ^0
    for code in all_codes(before, now):
        "-" => a
        "-" => b
        if code in before:
            before[code][0] + " " + str(before[code][1]) => a
        if code in now:
            now[code][0] + " " + str(now[code][1]) => b
        ("  " + code + "  " + pad(a, 22) + b) ^0
    ("  " + str(len(before)) + " items before, " + str(len(now)) + " now.") ^0

def all_codes(before, now):
    {} => both
    for c in before:
        1 => both[c]
    for c in now:
        1 => both[c]
    return stock.sorted_codes(both)

def differences(before, now):
    stock.diff(before, now) => d
    "Differences, before to now:" ^0
    if len(d["added"]) == 0:
        "  added:   none" ^0
    for code in d["added"]:
        ("  added:   " + item_text(now, code)) ^0
    if len(d["gone"]) == 0:
        "  gone:    none" ^0
    for code in d["gone"]:
        ("  gone:    " + item_text(before, code)) ^0
    for code in d["changed"]:
        now[code][1] - before[code][1] => delta
        ("  changed: " + code + " " + now[code][0] + " " + str(before[code][1]) + " -> " + str(now[code][1]) + " (" + signed(delta) + ")") ^0
        if now[code][1] == 0:
            ("           " + code + " is still on the list, at 0: counted and empty, not gone.") ^0
    for code in d["renamed"]:
        ("  renamed: " + code + " " + before[code][0] + " -> " + now[code][0]) ^0
    ("  the same: " + str(len(d["same"])) + " items") ^0
    for p in stock.moved(before, now, d):
        ("Very likely one item under a new code: " + p[0] + " " + before[p[0]][0] + " is gone and " + p[1] + " " + now[p[1]][0] + " added, with the same name and quantity (" + str(now[p[1]][1]) + ").") ^0
    stock.balance(before, now, d) => b
    ("Units: " + str(b[0]) + " before, " + str(b[1]) + " now: " + signed(b[2]) + " added, " + signed(0 - b[3]) + " gone, " + signed(b[4]) + " on the items in both.") ^0

def one_way(before, now):
    stock.diff(before, now) => d
    [] => found
    for code in d["added"]:
        found + [code] => found
    for code in d["changed"]:
        found + [code] => found
    ("Checking each item now against before finds " + list_or_none(found) + " - and never what is gone: " + list_or_none(d["gone"]) + ".") ^0
    [] => found2
    for code in d["gone"]:
        found2 + [code] => found2
    for code in d["changed"]:
        found2 + [code] => found2
    ("Checking each item before against now finds " + list_or_none(found2) + " - and never what is new: " + list_or_none(d["added"]) + ".") ^0

def list_or_none(xs):
    if len(xs) == 0:
        return "nothing"
    return names_text(xs)

def good_code(c):
    if len(c) != 4 or c[0] < "A" or c[0] > "Z":
        return False
    for k in [1:3]:
        if c[k] < "0" or c[k] > "9":
            return False
    return True

def ask_qty():
    # the quantity, -1 to cancel or refuse, -2 to remove
    plain(input("quantity (0 to 99999, or remove)> ")) => t
    if t == "":
        "Cancelled." ^0
        return 0 - 1
    if t == "remove":
        return 0 - 2
    if len(t) == 0 or len(t) > 5:
        "Type a whole number from 0 to 99999, or remove." ^0
        return 0 - 1
    for c in t:
        if c < "0" or c > "9":
            "Type a whole number from 0 to 99999, or remove." ^0
            return 0 - 1
    return int(t)

def change(now):
    plain(input("code (a capital letter and 3 digits, such as D400)> ")) => code
    if code == "":
        "Cancelled." ^0
        return now
    if not good_code(code):
        "Type a capital letter and 3 digits, such as D400." ^0
        return now
    if code in now:
        ask_qty() => q
        if q == 0 - 1:
            return now
        if q == 0 - 2:
            (code + " " + now[code][0] + " taken off the new count.") ^0
            return stock.remove_item(now, code)
        (code + " " + now[code][0] + ": " + str(now[code][1]) + " -> " + str(q) + ".") ^0
        return stock.set_item(now, code, now[code][0], q)
    plain(input("name (up to 20 lowercase letters and spaces)> ")) => name
    if name == "":
        "Cancelled." ^0
        return now
    if len(name) > 20:
        "Type up to 20 lowercase letters and spaces." ^0
        return now
    for c in name:
        if not ((c >= "a" and c <= "z") or c == " "):
            "Type up to 20 lowercase letters and spaces." ^0
            return now
    ask_qty() => q
    if q == 0 - 1:
        return now
    if q == 0 - 2:
        (code + " is not on the new count.") ^0
        return now
    (code + " " + name + " added to the new count, " + str(q) + ".") ^0
    return stock.set_item(now, code, name, q)

stock.before_count() => before
stock.now_count() => now

"== Inventory diff ==" ^0
"Two stock counts, last month's and this month's, compared in both directions." ^0
True => running
while running:
    "" ^0
    "1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show(before, now)
    elif choice == "2":
        differences(before, now)
    elif choice == "3":
        one_way(before, now)
    elif choice == "4":
        change(now) => now
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import stock

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 names_text(ns):
    out = ""
    for i in range(0, len(ns)):
        if i > 0 and i == len(ns) - 1:
            out = out + " and "
        elif i > 0:
            out = out + ", "
        out = out + ns[i]
    return out

def signed(n):
    if n > 0:
        return "+" + str(n)
    return str(n)

def item_text(count, code):
    return code + " " + count[code][0] + " " + str(count[code][1])

def show(before, now):
    print("  code  before                now")
    for code in all_codes(before, now):
        a = "-"
        b = "-"
        if code in before:
            a = before[code][0] + " " + str(before[code][1])
        if code in now:
            b = now[code][0] + " " + str(now[code][1])
        print("  " + code + "  " + pad(a, 22) + b)
    print("  " + str(len(before)) + " items before, " + str(len(now)) + " now.")

def all_codes(before, now):
    both = {}
    for c in before:
        both[c] = 1
    for c in now:
        both[c] = 1
    return stock.sorted_codes(both)

def differences(before, now):
    d = stock.diff(before, now)
    print("Differences, before to now:")
    if len(d["added"]) == 0:
        print("  added:   none")
    for code in d["added"]:
        print("  added:   " + item_text(now, code))
    if len(d["gone"]) == 0:
        print("  gone:    none")
    for code in d["gone"]:
        print("  gone:    " + item_text(before, code))
    for code in d["changed"]:
        delta = now[code][1] - before[code][1]
        print("  changed: " + code + " " + now[code][0] + " " + str(before[code][1]) + " -> " + str(now[code][1]) + " (" + signed(delta) + ")")
        if now[code][1] == 0:
            print("           " + code + " is still on the list, at 0: counted and empty, not gone.")
    for code in d["renamed"]:
        print("  renamed: " + code + " " + before[code][0] + " -> " + now[code][0])
    print("  the same: " + str(len(d["same"])) + " items")
    for p in stock.moved(before, now, d):
        print("Very likely one item under a new code: " + p[0] + " " + before[p[0]][0] + " is gone and " + p[1] + " " + now[p[1]][0] + " added, with the same name and quantity (" + str(now[p[1]][1]) + ").")
    b = stock.balance(before, now, d)
    print("Units: " + str(b[0]) + " before, " + str(b[1]) + " now: " + signed(b[2]) + " added, " + signed(0 - b[3]) + " gone, " + signed(b[4]) + " on the items in both.")

def one_way(before, now):
    d = stock.diff(before, now)
    found = []
    for code in d["added"]:
        found = found + [code]
    for code in d["changed"]:
        found = found + [code]
    print("Checking each item now against before finds " + list_or_none(found) + " - and never what is gone: " + list_or_none(d["gone"]) + ".")
    found2 = []
    for code in d["gone"]:
        found2 = found2 + [code]
    for code in d["changed"]:
        found2 = found2 + [code]
    print("Checking each item before against now finds " + list_or_none(found2) + " - and never what is new: " + list_or_none(d["added"]) + ".")

def list_or_none(xs):
    if len(xs) == 0:
        return "nothing"
    return names_text(xs)

def good_code(c):
    if len(c) != 4 or c[0] < "A" or c[0] > "Z":
        return False
    for k in range(1, 4):
        if c[k] < "0" or c[k] > "9":
            return False
    return True

def ask_qty():
    t = plain(input("quantity (0 to 99999, or remove)> "))
    if t == "":
        print("Cancelled.")
        return 0 - 1
    if t == "remove":
        return 0 - 2
    if len(t) == 0 or len(t) > 5:
        print("Type a whole number from 0 to 99999, or remove.")
        return 0 - 1
    for c in t:
        if c < "0" or c > "9":
            print("Type a whole number from 0 to 99999, or remove.")
            return 0 - 1
    return int(t)

def change(now):
    code = plain(input("code (a capital letter and 3 digits, such as D400)> "))
    if code == "":
        print("Cancelled.")
        return now
    if not good_code(code):
        print("Type a capital letter and 3 digits, such as D400.")
        return now
    if code in now:
        q = ask_qty()
        if q == 0 - 1:
            return now
        if q == 0 - 2:
            print(code + " " + now[code][0] + " taken off the new count.")
            return stock.remove_item(now, code)
        print(code + " " + now[code][0] + ": " + str(now[code][1]) + " -> " + str(q) + ".")
        return stock.set_item(now, code, now[code][0], q)
    name = plain(input("name (up to 20 lowercase letters and spaces)> "))
    if name == "":
        print("Cancelled.")
        return now
    if len(name) > 20:
        print("Type up to 20 lowercase letters and spaces.")
        return now
    for c in name:
        if not (c >= "a" and c <= "z" or c == " "):
            print("Type up to 20 lowercase letters and spaces.")
            return now
    q = ask_qty()
    if q == 0 - 1:
        return now
    if q == 0 - 2:
        print(code + " is not on the new count.")
        return now
    print(code + " " + name + " added to the new count, " + str(q) + ".")
    return stock.set_item(now, code, name, q)

before = stock.before_count()
now = stock.now_count()
print("== Inventory diff ==")
print("Two stock counts, last month's and this month's, compared in both directions.")
running = True
while running:
    print("")
    print("1) both counts  2) differences  3) one-way checks  4) change the new count  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show(before, now)
    elif choice == "2":
        differences(before, now)
    elif choice == "3":
        one_way(before, now)
    elif choice == "4":
        now = change(now)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

stock.eml

eml
# P123 inventory diff - two stock counts, each item a code, a name and a
# quantity. As in the corpus case inventory-diff-report, the difference
# has two directions: what is new now and what is gone since, and a report
# that asks only one of them misses the other. As in dict-inventory-report,
# the counts are dicts keyed by code, reported in sorted order. A quantity
# of 0 is still on the list; an item off the list is gone.
#
# A count is {code: [name, quantity]}.

def before_count():
    return {"A100": ["bolts", 120], "A101": ["nuts", 300], "A102": ["washers", 75], "B200": ["hinges", 40], "B201": ["brackets", 12], "C300": ["glue", 9], "C301": ["sealant", 5]}

def now_count():
    return {"A100": ["bolts", 120], "A101": ["nuts", 260], "A102": ["washers", 75], "B200": ["door hinges", 40], "B205": ["brackets", 12], "C300": ["glue", 0], "D400": ["tape", 30]}

def sorted_codes(count):
    # the codes in order, by insertion into a growing sorted list
    [] => out
    for code in count:
        [] => nxt
        False => placed
        for c in out:
            if not placed and code < c:
                nxt + [code] => nxt
                True => placed
            nxt + [c] => nxt
        if not placed:
            nxt + [code] => nxt
        nxt => out
    return out

def only_in(a, b):
    # the codes in a and not in b, in order
    [] => out
    for code in sorted_codes(a):
        if not (code in b):
            out + [code] => out
    return out

def diff(before, now):
    # {"added", "gone", "changed", "renamed", "same"}: lists of codes
    [] => changed
    [] => renamed
    [] => same
    for code in sorted_codes(now):
        if code in before:
            if before[code][1] != now[code][1]:
                changed + [code] => changed
            if before[code][0] != now[code][0]:
                renamed + [code] => renamed
            if before[code][1] == now[code][1] and before[code][0] == now[code][0]:
                same + [code] => same
    return {"added": only_in(now, before), "gone": only_in(before, now), "changed": changed, "renamed": renamed, "same": same}

def moved(before, now, d):
    # pairs [gone code, added code] with the same name and quantity: very
    # likely one item counted under a new code
    [] => out
    for g in d["gone"]:
        for a in d["added"]:
            if before[g][0] == now[a][0] and before[g][1] == now[a][1]:
                out + [[g, a]] => out
    return out

def units(count):
    0 => n
    for code in count:
        n + count[code][1] => n
    return n

def balance(before, now, d):
    # [units before, now, added, gone, change in the items on both]
    0 => added
    for code in d["added"]:
        added + now[code][1] => added
    0 => gone
    for code in d["gone"]:
        gone + before[code][1] => gone
    0 => change
    for code in d["changed"]:
        change + now[code][1] - before[code][1] => change
    return [units(before), units(now), added, gone, change]

def copy_count(count):
    {} => out
    for code in count:
        [count[code][0], count[code][1]] => out[code]
    return out

def set_item(count, code, name, qty):
    copy_count(count) => out
    [name, qty] => out[code]
    return out

def remove_item(count, code):
    {} => out
    for c in count:
        if c != code:
            [count[c][0], count[c][1]] => out[c]
    return out
Python projection (stock.py)
def before_count():
    return {"A100": ["bolts", 120], "A101": ["nuts", 300], "A102": ["washers", 75], "B200": ["hinges", 40], "B201": ["brackets", 12], "C300": ["glue", 9], "C301": ["sealant", 5]}

def now_count():
    return {"A100": ["bolts", 120], "A101": ["nuts", 260], "A102": ["washers", 75], "B200": ["door hinges", 40], "B205": ["brackets", 12], "C300": ["glue", 0], "D400": ["tape", 30]}

def sorted_codes(count):
    out = []
    for code in count:
        nxt = []
        placed = False
        for c in out:
            if not placed and code < c:
                nxt = nxt + [code]
                placed = True
            nxt = nxt + [c]
        if not placed:
            nxt = nxt + [code]
        out = nxt
    return out

def only_in(a, b):
    out = []
    for code in sorted_codes(a):
        if not code in b:
            out = out + [code]
    return out

def diff(before, now):
    changed = []
    renamed = []
    same = []
    for code in sorted_codes(now):
        if code in before:
            if before[code][1] != now[code][1]:
                changed = changed + [code]
            if before[code][0] != now[code][0]:
                renamed = renamed + [code]
            if before[code][1] == now[code][1] and before[code][0] == now[code][0]:
                same = same + [code]
    return {"added": only_in(now, before), "gone": only_in(before, now), "changed": changed, "renamed": renamed, "same": same}

def moved(before, now, d):
    out = []
    for g in d["gone"]:
        for a in d["added"]:
            if before[g][0] == now[a][0] and before[g][1] == now[a][1]:
                out = out + [[g, a]]
    return out

def units(count):
    n = 0
    for code in count:
        n = n + count[code][1]
    return n

def balance(before, now, d):
    added = 0
    for code in d["added"]:
        added = added + now[code][1]
    gone = 0
    for code in d["gone"]:
        gone = gone + before[code][1]
    change = 0
    for code in d["changed"]:
        change = change + now[code][1] - before[code][1]
    return [units(before), units(now), added, gone, change]

def copy_count(count):
    out = {}
    for code in count:
        out[code] = [count[code][0], count[code][1]]
    return out

def set_item(count, code, name, qty):
    out = copy_count(count)
    out[code] = [name, qty]
    return out

def remove_item(count, code):
    out = {}
    for c in count:
        if c != code:
            out[c] = [count[c][0], count[c][1]]
    return out

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.