{
  "id": "P176",
  "slug": "locker-rental",
  "key": "P176-locker-rental",
  "title": "Locker rental",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P176 - Locker rental\n\nA gym's 40 lockers rented month by month through 2027, the notes written\nagainst locker numbers, and member card ids of four hex characters.\n\n- `main.eml` - the menu, the lockers, the notes and who they name today,\n  the member cards, and a member count of your own\n- `lockers.eml` - the year of rentals and notes, drawn from a fixed seed;\n  card ids; and counting the members who share one\n\nHow each part works:\n\n- Each month some tenants give their locker up; it goes back to the free\n  list, and a new tenant gets the lowest number free. The front desk\n  writes notes - a deposit, a key, a complaint, an item found - naming the\n  locker.\n- As in the corpus case `the-id-was-reused-after-the-thing-it-identified-was-gone`,\n  every note was right when it was written and every one still finds a\n  locker. Of the year's 21 notes, 12 still name the tenant they were\n  written about, 6 name another tenant who has the locker now, and 3 a\n  locker that is free. Written against the tenancy, the locker and its\n  tenant, each would keep its tenant.\n- As in `the-id-space-was-smaller-than-the-records`, a card id of four hex\n  characters has 65,536 values. Among the first 1,000 members, 10 pairs\n  share an id, where 7.6 are expected; at 100,000 members at least 34,464\n  get an id another member already has, however the ids are drawn.\n\nWhat is checked: menu choices 1 to 5; a member count from 2 to 100000. An\nempty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the lockers, the notes and the member cards;\n- member counts of 1,000 and 70,000.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- member counts: a cancel, 1, 100001 and x;\n- 2 members, and 65,537, one more than there are ids.\n\nBuilt on the verified corpus cases `the-id-was-reused-after-the-thing-it-identified-was-gone`\n(an audit log of ids that were recycled) and\n`the-id-space-was-smaller-than-the-records` (random four-character hex\nidentifiers for a growing set of records).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P176 locker rental: a gym's lockers through a year - the lockers, the\n# notes written against locker numbers, member card ids of four hex\n# characters, and a member count of your own.\nimport lockers\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef commas(n):\n    str(n) => s\n    \"\" => out\n    0 => k\n    len(s) => i\n    while i > 0:\n        if k == 3:\n            \",\" + out => out\n            0 => k\n        s[i - 1] + out => out\n        k + 1 => k\n        i - 1 => i\n    return out\n\ndef tenths(t):\n    return commas(int((t - t % 10) / 10)) + \".\" + str(t % 10)\n\ndef months():\n    return [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\ndef show_lockers():\n    lockers.year() => y\n    0 => taken\n    for h in y[0]:\n        if h != \"\":\n            taken + 1 => taken\n    (\"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\n    (\"At the end of the year \" + str(taken) + \" are taken. The first ten:\") ^0\n    0 => k\n    while k < 10:\n        \"free\" => h\n        if y[0][k] != \"\":\n            y[0][k] => h\n        (\"  locker \" + lpad(str(k + 1), 2) + \"  \" + h) ^0\n        k + 1 => k\n\ndef verdict(note, holder):\n    holder[note[1] - 1] => now\n    if now == note[2]:\n        return \"the same tenant\"\n    if now == \"\":\n        return \"nobody: it is free\"\n    return now + \", another tenant\"\n\ndef show_notes():\n    lockers.year() => y\n    y[1] => notes\n    (\"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\n    (\"  \" + pad(\"month\", 7) + pad(\"locker\", 8) + pad(\"written about\", 15) + pad(\"note\", 27) + \"names today\") ^0\n    0 => i\n    while i < 10:\n        notes[i] => n\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\n        i + 1 => i\n    0 => same\n    0 => other\n    0 => free\n    for n in notes:\n        y[0][n[1] - 1] => now\n        if now == n[2]:\n            same + 1 => same\n        elif now == \"\":\n            free + 1 => free\n        else:\n            other + 1 => other\n    (\"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\n    \"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\n\ndef show_cards():\n    lockers.cards(1000) => ids\n    lockers.shared(ids) => s\n    \"Every member card gets a random id of four hex characters, such as \" + lockers.hex4(ids[0]) + \" - 65,536 possible ids.\" => line\n    line ^0\n    (\"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\n    \"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\n\ndef own():\n    plain(input(\"members (2 to 100000)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(t) > 6 or not digits(t) or int(t) < 2 or int(t) > 100000:\n        \"Type a whole number from 2 to 100000.\" ^0\n        return\n    int(t) => n\n    lockers.shared(lockers.cards(n)) => s\n    int((n * (n - 1) * 10 + 65536) / 131072) => expect\n    (commas(n) + \" members: \" + commas(s[0]) + \" share their id with another, \" + commas(s[1]) + \" pairs drawn here, \" + tenths(expect) + \" pairs expected.\") ^0\n    if n > 65536:\n        \" get\" => verb\n        if n - 65536 == 1:\n            \" gets\" => verb\n        (\"At least \" + commas(n - 65536) + \" of them\" + verb + \" an id another member already has, however the ids are drawn.\") ^0\n\n\"== Locker rental ==\" ^0\n\"Locker numbers handed out again, and card ids drawn from too few.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_lockers()\n    elif choice == \"2\":\n        show_notes()\n    elif choice == \"3\":\n        show_cards()\n    elif choice == \"4\":\n        own()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import lockers\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef commas(n):\n    s = str(n)\n    out = \"\"\n    k = 0\n    i = len(s)\n    while i > 0:\n        if k == 3:\n            out = \",\" + out\n            k = 0\n        out = s[i - 1] + out\n        k = k + 1\n        i = i - 1\n    return out\n\ndef tenths(t):\n    return commas(int((t - t % 10) / 10)) + \".\" + str(t % 10)\n\ndef months():\n    return [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\ndef show_lockers():\n    y = lockers.year()\n    taken = 0\n    for h in y[0]:\n        if h != \"\":\n            taken = taken + 1\n    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.\")\n    print(\"At the end of the year \" + str(taken) + \" are taken. The first ten:\")\n    k = 0\n    while k < 10:\n        h = \"free\"\n        if y[0][k] != \"\":\n            h = y[0][k]\n        print(\"  locker \" + lpad(str(k + 1), 2) + \"  \" + h)\n        k = k + 1\n\ndef verdict(note, holder):\n    now = holder[note[1] - 1]\n    if now == note[2]:\n        return \"the same tenant\"\n    if now == \"\":\n        return \"nobody: it is free\"\n    return now + \", another tenant\"\n\ndef show_notes():\n    y = lockers.year()\n    notes = y[1]\n    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:\")\n    print(\"  \" + pad(\"month\", 7) + pad(\"locker\", 8) + pad(\"written about\", 15) + pad(\"note\", 27) + \"names today\")\n    i = 0\n    while i < 10:\n        n = notes[i]\n        print(\"  \" + pad(months()[n[0] - 1], 7) + pad(str(n[1]), 8) + pad(n[2], 15) + pad(n[3], 27) + verdict(n, y[0]))\n        i = i + 1\n    same = 0\n    other = 0\n    free = 0\n    for n in notes:\n        now = y[0][n[1] - 1]\n        if now == n[2]:\n            same = same + 1\n        elif now == \"\":\n            free = free + 1\n        else:\n            other = other + 1\n    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.\")\n    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.\")\n\ndef show_cards():\n    ids = lockers.cards(1000)\n    s = lockers.shared(ids)\n    line = \"Every member card gets a random id of four hex characters, such as \" + lockers.hex4(ids[0]) + \" - 65,536 possible ids.\"\n    print(line)\n    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.\")\n    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.\")\n\ndef own():\n    t = plain(input(\"members (2 to 100000)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(t) > 6 or not digits(t) or int(t) < 2 or int(t) > 100000:\n        print(\"Type a whole number from 2 to 100000.\")\n        return\n    n = int(t)\n    s = lockers.shared(lockers.cards(n))\n    expect = int((n * (n - 1) * 10 + 65536) / 131072)\n    print(commas(n) + \" members: \" + commas(s[0]) + \" share their id with another, \" + commas(s[1]) + \" pairs drawn here, \" + tenths(expect) + \" pairs expected.\")\n    if n > 65536:\n        verb = \" get\"\n        if n - 65536 == 1:\n            verb = \" gets\"\n        print(\"At least \" + commas(n - 65536) + \" of them\" + verb + \" an id another member already has, however the ids are drawn.\")\n\nprint(\"== Locker rental ==\")\nprint(\"Locker numbers handed out again, and card ids drawn from too few.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_lockers()\n    elif choice == \"2\":\n        show_notes()\n    elif choice == \"3\":\n        show_cards()\n    elif choice == \"4\":\n        own()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "lockers.eml",
      "eml": "# P176 locker rental - a gym's 40 lockers rented month by month for a year.\n# As in the corpus case the-id-was-reused-after-the-thing-it-identified-was-\n# gone, a locker number freed by one tenant is the next tenant's, and a note\n# that names \"locker 12\" resolves today to whoever has it now. And as in\n# the-id-space-was-smaller-than-the-records, member cards with a random id of\n# four hex characters can tell apart 65536 members at most, and collide long\n# before that.\n\ndef lockers():\n    return 40\n\ndef step(x):\n    return (x * 48271) % 2147483647\n\ndef year():\n    # [the tenant of each locker at the end (\"\" if free), the notes]\n    # A note is [month, locker, tenant when written, what it says].\n    [\"\"] * lockers() => holder\n    [] => notes\n    0 => made\n    20271 => x\n    1 => m\n    while m <= 12:\n        0 => k\n        while k < lockers():\n            if holder[k] != \"\":\n                step(x) => x\n                if x % 100 < 15:\n                    \"\" => holder[k]\n            k + 1 => k\n        step(x) => x\n        1 + x % 4 => joining\n        if m == 1:\n            32 => joining\n        0 => k\n        while k < lockers() and joining > 0:\n            if holder[k] == \"\":\n                made + 1 => made\n                \"T\" + str(100 + made) => holder[k]\n                joining - 1 => joining\n            k + 1 => k\n        0 => n\n        while n < 3:\n            step(x) => x\n            x % lockers() => k\n            if holder[k] != \"\":\n                step(x) => x\n                [\"deposit paid\", \"key replaced\", \"complaint about the noise\", \"item found inside\"][x % 4] => what\n                notes + [[m, k + 1, holder[k], what]] => notes\n            n + 1 => n\n        m + 1 => m\n    return [holder, notes]\n\ndef hex4(n):\n    \"0123456789abcdef\" => h\n    \"\" => out\n    0 => i\n    while i < 4:\n        h[n % 16] + out => out\n        int((n - n % 16) / 16) => n\n        i + 1 => i\n    return out\n\ndef cards(n):\n    # n members' card ids, each drawn at random from the 65536 four-hex values\n    [0] * n => out\n    1019 => x\n    0 => i\n    while i < n:\n        step(x) => x\n        x % 65536 => out[i]\n        i + 1 => i\n    return out\n\ndef shared(ids):\n    # [members whose id another member also has, pairs of members sharing one]\n    {} => count\n    for v in ids:\n        if v in count:\n            count[v] + 1 => count[v]\n        else:\n            1 => count[v]\n    0 => members\n    0 => pairs\n    for v in count:\n        if count[v] > 1:\n            members + count[v] => members\n            pairs + int(count[v] * (count[v] - 1) / 2) => pairs\n    return [members, pairs]\n",
      "python": "def lockers():\n    return 40\n\ndef step(x):\n    return x * 48271 % 2147483647\n\ndef year():\n    holder = [\"\"] * lockers()\n    notes = []\n    made = 0\n    x = 20271\n    m = 1\n    while m <= 12:\n        k = 0\n        while k < lockers():\n            if holder[k] != \"\":\n                x = step(x)\n                if x % 100 < 15:\n                    holder[k] = \"\"\n            k = k + 1\n        x = step(x)\n        joining = 1 + x % 4\n        if m == 1:\n            joining = 32\n        k = 0\n        while k < lockers() and joining > 0:\n            if holder[k] == \"\":\n                made = made + 1\n                holder[k] = \"T\" + str(100 + made)\n                joining = joining - 1\n            k = k + 1\n        n = 0\n        while n < 3:\n            x = step(x)\n            k = x % lockers()\n            if holder[k] != \"\":\n                x = step(x)\n                what = [\"deposit paid\", \"key replaced\", \"complaint about the noise\", \"item found inside\"][x % 4]\n                notes = notes + [[m, k + 1, holder[k], what]]\n            n = n + 1\n        m = m + 1\n    return [holder, notes]\n\ndef hex4(n):\n    h = \"0123456789abcdef\"\n    out = \"\"\n    i = 0\n    while i < 4:\n        out = h[n % 16] + out\n        n = int((n - n % 16) / 16)\n        i = i + 1\n    return out\n\ndef cards(n):\n    out = [0] * n\n    x = 1019\n    i = 0\n    while i < n:\n        x = step(x)\n        out[i] = x % 65536\n        i = i + 1\n    return out\n\ndef shared(ids):\n    count = {}\n    for v in ids:\n        if v in count:\n            count[v] = count[v] + 1\n        else:\n            count[v] = 1\n    members = 0\n    pairs = 0\n    for v in count:\n        if count[v] > 1:\n            members = members + count[v]\n            pairs = pairs + int(count[v] * (count[v] - 1) / 2)\n    return [members, pairs]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\n1\n4\n100001\n4\nx\n4\n2\n4\n65537\n5\n",
      "screen": "== Locker rental ==\nLocker numbers handed out again, and card ids drawn from too few.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> \nCancelled.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> 1\nType a whole number from 2 to 100000.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> 100001\nType a whole number from 2 to 100000.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> x\nType a whole number from 2 to 100000.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> 2\n2 members: 0 share their id with another, 0 pairs drawn here, 0.0 pairs expected.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> 65537\n65,537 members: 41,485 share their id with another, 33,136 pairs drawn here, 32,768.5 pairs expected.\nAt least 1 of them gets an id another member already has, however the ids are drawn.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n1000\n4\n70000\n5\n",
      "screen": "== Locker rental ==\nLocker numbers handed out again, and card ids drawn from too few.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 1\n40 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.\nAt the end of the year 20 are taken. The first ten:\n  locker  1  T154\n  locker  2  T102\n  locker  3  T146\n  locker  4  T104\n  locker  5  T105\n  locker  6  T155\n  locker  7  T156\n  locker  8  T147\n  locker  9  T157\n  locker 10  T151\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 2\nNotes the front desk wrote in 2027, each naming a locker - 21 of them. The first ten, and who the locker names today:\n  month  locker  written about  note                       names today\n  Jan    17      T117           deposit paid               T145, another tenant\n  Jan    5       T105           complaint about the noise  the same tenant\n  Feb    17      T117           deposit paid               T145, another tenant\n  Feb    29      T129           complaint about the noise  nobody: it is free\n  Mar    17      T117           deposit paid               T145, another tenant\n  Apr    27      T127           item found inside          nobody: it is free\n  May    18      T138           deposit paid               the same tenant\n  May    18      T138           deposit paid               the same tenant\n  Jun    6       T135           complaint about the noise  T155, another tenant\n  Jul    6       T135           complaint about the noise  T155, another tenant\nOf 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.\nEvery 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.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 3\nEvery member card gets a random id of four hex characters, such as 8d35 - 65,536 possible ids.\nAmong the first 1,000 members, 20 share their id with another member: 10 pairs, where 7.6 are expected.\nAt 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.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> 1000\n1,000 members: 20 share their id with another, 10 pairs drawn here, 7.6 pairs expected.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 4\nmembers (2 to 100000)> 70000\n70,000 members: 46,017 share their id with another, 37,761 pairs drawn here, 37,383.5 pairs expected.\nAt least 4,464 of them get an id another member already has, however the ids are drawn.\n\n1) the lockers  2) the notes  3) member cards  4) a member count of your own  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "the-id-was-reused-after-the-thing-it-identified-was-gone",
      "caseId": "559-the-id-was-reused-after-the-thing-it-identified-was-gone",
      "title": "The id was reused after the thing it identified was gone"
    },
    {
      "slug": "the-id-space-was-smaller-than-the-records",
      "caseId": "873-the-id-space-was-smaller-than-the-records",
      "title": "The id space was smaller than the records"
    }
  ],
  "updated": "2026-10-19"
}
