<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P176-locker-rental/ | updated: 2026-10-19 -->

# P176 Locker rental

A gym's lockers rented through a year, each freed number handed to the next tenant: notes written against a locker number now name another tenant or nobody, and random card ids of four hex characters collide among the first thousand members.

EML-P project `projects/locker-rental` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/locker-rental` runs it and `eml project verify projects/locker-rental` 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: the-id-was-reused-after-the-thing-it-identified-was-gone (https://efficientnewlanguage.org/cases/559-the-id-was-reused-after-the-thing-it-identified-was-gone/), the-id-space-was-smaller-than-the-records (https://efficientnewlanguage.org/cases/873-the-id-space-was-smaller-than-the-records/).

## Sessions

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

Input:

```text
0
x
4

4
1
4
100001
4
x
4
2
4
65537
5
```

Screen:

```text
== Locker rental ==
Locker numbers handed out again, and card ids drawn from too few.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 0
Pick a number from 1 to 5.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> x
Pick a number from 1 to 5.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 
Cancelled.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 1
Type a whole number from 2 to 100000.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 100001
Type a whole number from 2 to 100000.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> x
Type a whole number from 2 to 100000.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 2
2 members: 0 share their id with another, 0 pairs drawn here, 0.0 pairs expected.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 65537
65,537 members: 41,485 share their id with another, 33,136 pairs drawn here, 32,768.5 pairs expected.
At least 1 of them gets an id another member already has, however the ids are drawn.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 5
Bye.
```

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

Input:

```text
1
2
3
4
1000
4
70000
5
```

Screen:

```text
== Locker rental ==
Locker numbers handed out again, and card ids drawn from too few.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 1
40 lockers, rented a month at a time through 2027. A locker given up goes back to the free list, and a new tenant gets the lowest number free.
At the end of the year 20 are taken. The first ten:
  locker  1  T154
  locker  2  T102
  locker  3  T146
  locker  4  T104
  locker  5  T105
  locker  6  T155
  locker  7  T156
  locker  8  T147
  locker  9  T157
  locker 10  T151

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 2
Notes the front desk wrote in 2027, each naming a locker - 21 of them. The first ten, and who the locker names today:
  month  locker  written about  note                       names today
  Jan    17      T117           deposit paid               T145, another tenant
  Jan    5       T105           complaint about the noise  the same tenant
  Feb    17      T117           deposit paid               T145, another tenant
  Feb    29      T129           complaint about the noise  nobody: it is free
  Mar    17      T117           deposit paid               T145, another tenant
  Apr    27      T127           item found inside          nobody: it is free
  May    18      T138           deposit paid               the same tenant
  May    18      T138           deposit paid               the same tenant
  Jun    6       T135           complaint about the noise  T155, another tenant
  Jul    6       T135           complaint about the noise  T155, another tenant
Of the 21 notes, 12 still name the tenant they were written about, 6 name another tenant who has the locker now, and 3 a locker that is free.
Every note was right when it was written, and every one still finds a locker. Written against the tenancy - the locker and its tenant - each would keep its tenant.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 3
Every member card gets a random id of four hex characters, such as 8d35 - 65,536 possible ids.
Among the first 1,000 members, 20 share their id with another member: 10 pairs, where 7.6 are expected.
At 100,000 members, at least 34,464 get an id another member already has, however the ids are drawn: there are only 65,536 of them.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 1000
1,000 members: 20 share their id with another, 10 pairs drawn here, 7.6 pairs expected.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 4
members (2 to 100000)> 70000
70,000 members: 46,017 share their id with another, 37,761 pairs drawn here, 37,383.5 pairs expected.
At least 4,464 of them get an id another member already has, however the ids are drawn.

1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P176 locker rental: a gym's lockers through a year - the lockers, the
# notes written against locker numbers, member card ids of four hex
# characters, and a member count of your own.
import lockers

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 digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def commas(n):
    str(n) => s
    "" => out
    0 => k
    len(s) => i
    while i > 0:
        if k == 3:
            "," + out => out
            0 => k
        s[i - 1] + out => out
        k + 1 => k
        i - 1 => i
    return out

def tenths(t):
    return commas(int((t - t % 10) / 10)) + "." + str(t % 10)

def months():
    return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

def show_lockers():
    lockers.year() => y
    0 => taken
    for h in y[0]:
        if h != "":
            taken + 1 => taken
    ("40 lockers, rented a month at a time through 2027. A locker given up goes back to the free list, and a new tenant gets the lowest number free.") ^0
    ("At the end of the year " + str(taken) + " are taken. The first ten:") ^0
    0 => k
    while k < 10:
        "free" => h
        if y[0][k] != "":
            y[0][k] => h
        ("  locker " + lpad(str(k + 1), 2) + "  " + h) ^0
        k + 1 => k

def verdict(note, holder):
    holder[note[1] - 1] => now
    if now == note[2]:
        return "the same tenant"
    if now == "":
        return "nobody: it is free"
    return now + ", another tenant"

def show_notes():
    lockers.year() => y
    y[1] => notes
    ("Notes the front desk wrote in 2027, each naming a locker - " + str(len(notes)) + " of them. The first ten, and who the locker names today:") ^0
    ("  " + pad("month", 7) + pad("locker", 8) + pad("written about", 15) + pad("note", 27) + "names today") ^0
    0 => i
    while i < 10:
        notes[i] => n
        ("  " + pad(months()[n[0] - 1], 7) + pad(str(n[1]), 8) + pad(n[2], 15) + pad(n[3], 27) + verdict(n, y[0])) ^0
        i + 1 => i
    0 => same
    0 => other
    0 => free
    for n in notes:
        y[0][n[1] - 1] => now
        if now == n[2]:
            same + 1 => same
        elif now == "":
            free + 1 => free
        else:
            other + 1 => other
    ("Of the " + str(len(notes)) + " notes, " + str(same) + " still name the tenant they were written about, " + str(other) + " name another tenant who has the locker now, and " + str(free) + " a locker that is free.") ^0
    "Every note was right when it was written, and every one still finds a locker. Written against the tenancy - the locker and its tenant - each would keep its tenant." ^0

def show_cards():
    lockers.cards(1000) => ids
    lockers.shared(ids) => s
    "Every member card gets a random id of four hex characters, such as " + lockers.hex4(ids[0]) + " - 65,536 possible ids." => line
    line ^0
    ("Among the first 1,000 members, " + str(s[0]) + " share their id with another member: " + str(s[1]) + " pairs, where " + tenths(int((1000 * 999 * 10 + 65536) / 131072)) + " are expected.") ^0
    "At 100,000 members, at least 34,464 get an id another member already has, however the ids are drawn: there are only 65,536 of them." ^0

def own():
    plain(input("members (2 to 100000)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 6 or not digits(t) or int(t) < 2 or int(t) > 100000:
        "Type a whole number from 2 to 100000." ^0
        return
    int(t) => n
    lockers.shared(lockers.cards(n)) => s
    int((n * (n - 1) * 10 + 65536) / 131072) => expect
    (commas(n) + " members: " + commas(s[0]) + " share their id with another, " + commas(s[1]) + " pairs drawn here, " + tenths(expect) + " pairs expected.") ^0
    if n > 65536:
        " get" => verb
        if n - 65536 == 1:
            " gets" => verb
        ("At least " + commas(n - 65536) + " of them" + verb + " an id another member already has, however the ids are drawn.") ^0

"== Locker rental ==" ^0
"Locker numbers handed out again, and card ids drawn from too few." ^0
True => running
while running:
    "" ^0
    "1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_lockers()
    elif choice == "2":
        show_notes()
    elif choice == "3":
        show_cards()
    elif choice == "4":
        own()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import lockers

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 digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def commas(n):
    s = str(n)
    out = ""
    k = 0
    i = len(s)
    while i > 0:
        if k == 3:
            out = "," + out
            k = 0
        out = s[i - 1] + out
        k = k + 1
        i = i - 1
    return out

def tenths(t):
    return commas(int((t - t % 10) / 10)) + "." + str(t % 10)

def months():
    return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

def show_lockers():
    y = lockers.year()
    taken = 0
    for h in y[0]:
        if h != "":
            taken = taken + 1
    print("40 lockers, rented a month at a time through 2027. A locker given up goes back to the free list, and a new tenant gets the lowest number free.")
    print("At the end of the year " + str(taken) + " are taken. The first ten:")
    k = 0
    while k < 10:
        h = "free"
        if y[0][k] != "":
            h = y[0][k]
        print("  locker " + lpad(str(k + 1), 2) + "  " + h)
        k = k + 1

def verdict(note, holder):
    now = holder[note[1] - 1]
    if now == note[2]:
        return "the same tenant"
    if now == "":
        return "nobody: it is free"
    return now + ", another tenant"

def show_notes():
    y = lockers.year()
    notes = y[1]
    print("Notes the front desk wrote in 2027, each naming a locker - " + str(len(notes)) + " of them. The first ten, and who the locker names today:")
    print("  " + pad("month", 7) + pad("locker", 8) + pad("written about", 15) + pad("note", 27) + "names today")
    i = 0
    while i < 10:
        n = notes[i]
        print("  " + pad(months()[n[0] - 1], 7) + pad(str(n[1]), 8) + pad(n[2], 15) + pad(n[3], 27) + verdict(n, y[0]))
        i = i + 1
    same = 0
    other = 0
    free = 0
    for n in notes:
        now = y[0][n[1] - 1]
        if now == n[2]:
            same = same + 1
        elif now == "":
            free = free + 1
        else:
            other = other + 1
    print("Of the " + str(len(notes)) + " notes, " + str(same) + " still name the tenant they were written about, " + str(other) + " name another tenant who has the locker now, and " + str(free) + " a locker that is free.")
    print("Every note was right when it was written, and every one still finds a locker. Written against the tenancy - the locker and its tenant - each would keep its tenant.")

def show_cards():
    ids = lockers.cards(1000)
    s = lockers.shared(ids)
    line = "Every member card gets a random id of four hex characters, such as " + lockers.hex4(ids[0]) + " - 65,536 possible ids."
    print(line)
    print("Among the first 1,000 members, " + str(s[0]) + " share their id with another member: " + str(s[1]) + " pairs, where " + tenths(int((1000 * 999 * 10 + 65536) / 131072)) + " are expected.")
    print("At 100,000 members, at least 34,464 get an id another member already has, however the ids are drawn: there are only 65,536 of them.")

def own():
    t = plain(input("members (2 to 100000)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 6 or not digits(t) or int(t) < 2 or int(t) > 100000:
        print("Type a whole number from 2 to 100000.")
        return
    n = int(t)
    s = lockers.shared(lockers.cards(n))
    expect = int((n * (n - 1) * 10 + 65536) / 131072)
    print(commas(n) + " members: " + commas(s[0]) + " share their id with another, " + commas(s[1]) + " pairs drawn here, " + tenths(expect) + " pairs expected.")
    if n > 65536:
        verb = " get"
        if n - 65536 == 1:
            verb = " gets"
        print("At least " + commas(n - 65536) + " of them" + verb + " an id another member already has, however the ids are drawn.")

print("== Locker rental ==")
print("Locker numbers handed out again, and card ids drawn from too few.")
running = True
while running:
    print("")
    print("1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_lockers()
    elif choice == "2":
        show_notes()
    elif choice == "3":
        show_cards()
    elif choice == "4":
        own()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### lockers.eml

```eml
# P176 locker rental - a gym's 40 lockers rented month by month for a year.
# As in the corpus case the-id-was-reused-after-the-thing-it-identified-was-
# gone, a locker number freed by one tenant is the next tenant's, and a note
# that names "locker 12" resolves today to whoever has it now. And as in
# the-id-space-was-smaller-than-the-records, member cards with a random id of
# four hex characters can tell apart 65536 members at most, and collide long
# before that.

def lockers():
    return 40

def step(x):
    return (x * 48271) % 2147483647

def year():
    # [the tenant of each locker at the end ("" if free), the notes]
    # A note is [month, locker, tenant when written, what it says].
    [""] * lockers() => holder
    [] => notes
    0 => made
    20271 => x
    1 => m
    while m <= 12:
        0 => k
        while k < lockers():
            if holder[k] != "":
                step(x) => x
                if x % 100 < 15:
                    "" => holder[k]
            k + 1 => k
        step(x) => x
        1 + x % 4 => joining
        if m == 1:
            32 => joining
        0 => k
        while k < lockers() and joining > 0:
            if holder[k] == "":
                made + 1 => made
                "T" + str(100 + made) => holder[k]
                joining - 1 => joining
            k + 1 => k
        0 => n
        while n < 3:
            step(x) => x
            x % lockers() => k
            if holder[k] != "":
                step(x) => x
                ["deposit paid", "key replaced", "complaint about the noise", "item found inside"][x % 4] => what
                notes + [[m, k + 1, holder[k], what]] => notes
            n + 1 => n
        m + 1 => m
    return [holder, notes]

def hex4(n):
    "0123456789abcdef" => h
    "" => out
    0 => i
    while i < 4:
        h[n % 16] + out => out
        int((n - n % 16) / 16) => n
        i + 1 => i
    return out

def cards(n):
    # n members' card ids, each drawn at random from the 65536 four-hex values
    [0] * n => out
    1019 => x
    0 => i
    while i < n:
        step(x) => x
        x % 65536 => out[i]
        i + 1 => i
    return out

def shared(ids):
    # [members whose id another member also has, pairs of members sharing one]
    {} => count
    for v in ids:
        if v in count:
            count[v] + 1 => count[v]
        else:
            1 => count[v]
    0 => members
    0 => pairs
    for v in count:
        if count[v] > 1:
            members + count[v] => members
            pairs + int(count[v] * (count[v] - 1) / 2) => pairs
    return [members, pairs]
```

Python projection of lockers.eml:

```python
def lockers():
    return 40

def step(x):
    return x * 48271 % 2147483647

def year():
    holder = [""] * lockers()
    notes = []
    made = 0
    x = 20271
    m = 1
    while m <= 12:
        k = 0
        while k < lockers():
            if holder[k] != "":
                x = step(x)
                if x % 100 < 15:
                    holder[k] = ""
            k = k + 1
        x = step(x)
        joining = 1 + x % 4
        if m == 1:
            joining = 32
        k = 0
        while k < lockers() and joining > 0:
            if holder[k] == "":
                made = made + 1
                holder[k] = "T" + str(100 + made)
                joining = joining - 1
            k = k + 1
        n = 0
        while n < 3:
            x = step(x)
            k = x % lockers()
            if holder[k] != "":
                x = step(x)
                what = ["deposit paid", "key replaced", "complaint about the noise", "item found inside"][x % 4]
                notes = notes + [[m, k + 1, holder[k], what]]
            n = n + 1
        m = m + 1
    return [holder, notes]

def hex4(n):
    h = "0123456789abcdef"
    out = ""
    i = 0
    while i < 4:
        out = h[n % 16] + out
        n = int((n - n % 16) / 16)
        i = i + 1
    return out

def cards(n):
    out = [0] * n
    x = 1019
    i = 0
    while i < n:
        x = step(x)
        out[i] = x % 65536
        i = i + 1
    return out

def shared(ids):
    count = {}
    for v in ids:
        if v in count:
            count[v] = count[v] + 1
        else:
            count[v] = 1
    members = 0
    pairs = 0
    for v in count:
        if count[v] > 1:
            members = members + count[v]
            pairs = pairs + int(count[v] * (count[v] - 1) / 2)
    return [members, pairs]
```

## README

# P176 - Locker rental

A gym's 40 lockers rented month by month through 2027, the notes written
against locker numbers, and member card ids of four hex characters.

- `main.eml` - the menu, the lockers, the notes and who they name today,
  the member cards, and a member count of your own
- `lockers.eml` - the year of rentals and notes, drawn from a fixed seed;
  card ids; and counting the members who share one

How each part works:

- Each month some tenants give their locker up; it goes back to the free
  list, and a new tenant gets the lowest number free. The front desk
  writes notes - a deposit, a key, a complaint, an item found - naming the
  locker.
- As in the corpus case `the-id-was-reused-after-the-thing-it-identified-was-gone`,
  every note was right when it was written and every one still finds a
  locker. Of the year's 21 notes, 12 still name the tenant they were
  written about, 6 name another tenant who has the locker now, and 3 a
  locker that is free. Written against the tenancy, the locker and its
  tenant, each would keep its tenant.
- As in `the-id-space-was-smaller-than-the-records`, a card id of four hex
  characters has 65,536 values. Among the first 1,000 members, 10 pairs
  share an id, where 7.6 are expected; at 100,000 members at least 34,464
  get an id another member already has, however the ids are drawn.

What is checked: menu choices 1 to 5; a member count from 2 to 100000. An
empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the lockers, the notes and the member cards;
- member counts of 1,000 and 70,000.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- member counts: a cancel, 1, 100001 and x;
- 2 members, and 65,537, one more than there are ids.

Built on the verified corpus cases `the-id-was-reused-after-the-thing-it-identified-was-gone`
(an audit log of ids that were recycled) and
`the-id-space-was-smaller-than-the-records` (random four-character hex
identifiers for a growing set of records).
