{
  "id": "P110",
  "slug": "pagination",
  "key": "P110-pagination",
  "title": "Pagination",
  "summary": "A feed read one page at a time while items are added and deleted between the requests: offset and keyset pagination side by side, and which rows each delivered twice or skipped - the skip hidden behind a full page.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P110 - Pagination\n\nA feed, newest first, read one page at a time while items are added and\ndeleted between the requests. Offset pagination and keyset (cursor)\npagination run side by side, and the program reports what each one\ndelivered.\n\n- `main.eml` - the menu, the side-by-side walk and its verdict, the\n  events, the page size and the feed size\n- `pages.eml` - the feed, the walk under both kinds of pagination, the\n  tally of what was delivered, and reading the events\n\nHow each part works:\n\n- Offset pagination asks for \"the next 5 after the first 10\". Keyset\n  pagination asks for \"the next 5 older than the last one I got\".\n- Events run in order:\n  - page asks both for the next page;\n  - new adds an item at the top;\n  - del N deletes item N.\n- As in the corpus case `pagination-shifting-data`:\n  - an item added before the window makes offset pagination deliver a row\n    a second time;\n  - an item deleted before the window makes it skip one, and the page that\n    goes past the skipped row still comes back full, so nothing in it shows\n    that a row is missing;\n  - keyset pagination does neither.\n- The verdict judges only the items that were there for the whole walk,\n  not added and not deleted. An item older than everything delivered so\n  far has not been reached yet, rather than skipped.\n\nWhat is checked: menu choices 1 to 5; up to 30 events, made of page, new\nand del followed by a number of up to three digits, with at least one\npage; 2 to 10 rows to a page; a feed of 5 to 60 items. An empty answer\ncancels.\n\nSessions: `sessions/basic.in` gives:\n- the starting walk: an item added after page 1 makes offset repeat 16,\n  and 17 deleted after page 2 makes it skip 11 on a full page;\n- four pages and nothing else: both clean;\n- two items added after page 1: offset repeats 17 and 16;\n- an item deleted after page 2, ahead of both;\n- 4 to a page, with 20 and 19 deleted after pages 1 and 2: offset skips 16\n  and 11, each on a full page; keyset delivers every one.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- events: a cancel, an unknown word, del with no number, del with a\n  letter, no page at all, a four-digit number; then a script that deletes\n  21 twice, and its walk;\n- page sizes: a cancel, 1 and 11; feed sizes: a cancel, 4 and 61, then 7,\n  and the walk again.\n\nBuilt on the verified corpus case `pagination-shifting-data` (offset\npagination repeats a row on an insert and skips one on a delete, the\nskip invisible behind a full page).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P110 pagination: a feed read page by page while items come and go,\n# by offset and by key side by side, and what each one delivered.\nimport pages\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 number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef ids(xs):\n    if len(xs) == 0:\n        return \"(empty)\"\n    \"\" => out\n    for x in xs:\n        if out != \"\":\n            out + \" \" => out\n        out + str(x) => out\n    return out\n\ndef listed(xs):\n    \"\" => out\n    for x in xs:\n        if out != \"\":\n            out + \", \" => out\n        out + str(x) => out\n    return out\n\ndef events_text(events):\n    \"\" => out\n    for e in events:\n        if out != \"\":\n            out + \" \" => out\n        if e[0] == \"del\":\n            out + \"del \" + str(e[1]) => out\n        else:\n            out + e[0] => out\n    return out\n\ndef show_walk(n, events, size):\n    pages.feed(n) => start\n    pages.walk(start, events, size) => w\n    (\"Feed: \" + str(n) + \" items, newest first; \" + str(size) + \" to a page; events: \" + events_text(events) + \".\") ^0\n    (\"  \" + pad(\"\", 9) + pad(\"by offset\", 3 * size + 4) + \"by key\") ^0\n    0 => k\n    n => top\n    start => present\n    for e in events:\n        if e[0] == \"page\":\n            (\"  \" + pad(\"page \" + str(k + 1), 9) + pad(ids(w[0][k]), 3 * size + 4) + ids(w[1][k])) ^0\n            k + 1 => k\n        elif e[0] == \"new\":\n            top + 1 => top\n            [top] + present => present\n            (\"  new: item \" + str(top) + \" added at the top\") ^0\n        elif pages.has(present, e[1]):\n            pages.without(present, e[1]) => present\n            (\"  del \" + str(e[1]) + \": item \" + str(e[1]) + \" deleted\") ^0\n        else:\n            (\"  del \" + str(e[1]) + \": there is no item \" + str(e[1]) + \" now\") ^0\n    \"Judged on the items that were there for the whole walk, not added and not deleted:\" ^0\n    [\"by offset\", \"by key\"] => names\n    for m in [0:1]:\n        pages.tally(start, w[m], w[2], w[3]) => t\n        \"\" => tail\n        if t[2] > 0:\n            \"; \" + str(t[2]) + \" older not reached yet\" => tail\n        if len(t[0]) == 0 and len(t[1]) == 0:\n            (\"  \" + names[m] + \": every one reached was delivered exactly once\" + tail) ^0\n        else:\n            \"\" => line\n            if len(t[1]) > 0:\n                listed(t[1]) + \" delivered twice\" => line\n            if len(t[0]) > 0:\n                if line != \"\":\n                    line + \"; \" => line\n                line + listed(t[0]) + \" never delivered\" => line\n            (\"  \" + names[m] + \": \" + line + tail) ^0\n            for r in t[0]:\n                # the page that should have held r: the first with an older item\n                -1 => at\n                for i in [0:len(w[m]) - 1]:\n                    if at == -1:\n                        for x in w[m][i]:\n                            if x < r:\n                                i => at\n                if at >= 0:\n                    if len(w[m][at]) == size:\n                        (\"    page \" + str(at + 1) + \" went past \" + str(r) + \" and still came back with all \" + str(size) + \" rows: nothing in it shows a row is missing\") ^0\n                    else:\n                        (\"    page \" + str(at + 1) + \" went past \" + str(r)) ^0\n\ndef set_events():\n    \"Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\" ^0\n    plain(input(\"events> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return []\n    pages.parse(t) => e\n    if len(e) == 0:\n        \"Type words page, new and del followed by a number, separated by spaces.\" ^0\n        return []\n    0 => p\n    for x in e:\n        if x[0] == \"page\":\n            p + 1 => p\n    if p == 0:\n        \"Ask for at least one page.\" ^0\n        return []\n    (\"Events: \" + events_text(e) + \".\") ^0\n    return e\n\n[[\"page\", 0], [\"new\", 0], [\"page\", 0], [\"del\", 17], [\"page\", 0], [\"page\", 0], [\"page\", 0]] => events\n20 => n\n5 => size\n\n\"== Pagination ==\" ^0\n\"A feed read a page at a time while items are added and deleted: by offset and by key, side by side.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) walk the feed  2) events  3) page size  4) feed size  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_walk(n, events, size)\n    elif choice == \"2\":\n        set_events() => e\n        if len(e) > 0:\n            e => events\n    elif choice == \"3\":\n        plain(input(\"rows to a page (2 to 10)> \")) => t\n        number(t, 2) => v\n        if t == \"\":\n            \"Cancelled.\" ^0\n        elif v < 2 or v > 10:\n            \"Type a whole number from 2 to 10.\" ^0\n        else:\n            v => size\n            (str(v) + \" rows to a page.\") ^0\n    elif choice == \"4\":\n        plain(input(\"items in the feed (5 to 60)> \")) => t\n        number(t, 2) => v\n        if t == \"\":\n            \"Cancelled.\" ^0\n        elif v < 5 or v > 60:\n            \"Type a whole number from 5 to 60.\" ^0\n        else:\n            v => n\n            (\"A feed of \" + str(v) + \" items, \" + str(v) + \" down to 1.\") ^0\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import pages\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 number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef ids(xs):\n    if len(xs) == 0:\n        return \"(empty)\"\n    out = \"\"\n    for x in xs:\n        if out != \"\":\n            out = out + \" \"\n        out = out + str(x)\n    return out\n\ndef listed(xs):\n    out = \"\"\n    for x in xs:\n        if out != \"\":\n            out = out + \", \"\n        out = out + str(x)\n    return out\n\ndef events_text(events):\n    out = \"\"\n    for e in events:\n        if out != \"\":\n            out = out + \" \"\n        if e[0] == \"del\":\n            out = out + \"del \" + str(e[1])\n        else:\n            out = out + e[0]\n    return out\n\ndef show_walk(n, events, size):\n    start = pages.feed(n)\n    w = pages.walk(start, events, size)\n    print(\"Feed: \" + str(n) + \" items, newest first; \" + str(size) + \" to a page; events: \" + events_text(events) + \".\")\n    print(\"  \" + pad(\"\", 9) + pad(\"by offset\", 3 * size + 4) + \"by key\")\n    k = 0\n    top = n\n    present = start\n    for e in events:\n        if e[0] == \"page\":\n            print(\"  \" + pad(\"page \" + str(k + 1), 9) + pad(ids(w[0][k]), 3 * size + 4) + ids(w[1][k]))\n            k = k + 1\n        elif e[0] == \"new\":\n            top = top + 1\n            present = [top] + present\n            print(\"  new: item \" + str(top) + \" added at the top\")\n        elif pages.has(present, e[1]):\n            present = pages.without(present, e[1])\n            print(\"  del \" + str(e[1]) + \": item \" + str(e[1]) + \" deleted\")\n        else:\n            print(\"  del \" + str(e[1]) + \": there is no item \" + str(e[1]) + \" now\")\n    print(\"Judged on the items that were there for the whole walk, not added and not deleted:\")\n    names = [\"by offset\", \"by key\"]\n    for m in range(0, 2):\n        t = pages.tally(start, w[m], w[2], w[3])\n        tail = \"\"\n        if t[2] > 0:\n            tail = \"; \" + str(t[2]) + \" older not reached yet\"\n        if len(t[0]) == 0 and len(t[1]) == 0:\n            print(\"  \" + names[m] + \": every one reached was delivered exactly once\" + tail)\n        else:\n            line = \"\"\n            if len(t[1]) > 0:\n                line = listed(t[1]) + \" delivered twice\"\n            if len(t[0]) > 0:\n                if line != \"\":\n                    line = line + \"; \"\n                line = line + listed(t[0]) + \" never delivered\"\n            print(\"  \" + names[m] + \": \" + line + tail)\n            for r in t[0]:\n                at = -1\n                for i in range(0, len(w[m])):\n                    if at == -1:\n                        for x in w[m][i]:\n                            if x < r:\n                                at = i\n                if at >= 0:\n                    if len(w[m][at]) == size:\n                        print(\"    page \" + str(at + 1) + \" went past \" + str(r) + \" and still came back with all \" + str(size) + \" rows: nothing in it shows a row is missing\")\n                    else:\n                        print(\"    page \" + str(at + 1) + \" went past \" + str(r))\n\ndef set_events():\n    print(\"Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\")\n    t = plain(input(\"events> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return []\n    e = pages.parse(t)\n    if len(e) == 0:\n        print(\"Type words page, new and del followed by a number, separated by spaces.\")\n        return []\n    p = 0\n    for x in e:\n        if x[0] == \"page\":\n            p = p + 1\n    if p == 0:\n        print(\"Ask for at least one page.\")\n        return []\n    print(\"Events: \" + events_text(e) + \".\")\n    return e\n\nevents = [[\"page\", 0], [\"new\", 0], [\"page\", 0], [\"del\", 17], [\"page\", 0], [\"page\", 0], [\"page\", 0]]\nn = 20\nsize = 5\nprint(\"== Pagination ==\")\nprint(\"A feed read a page at a time while items are added and deleted: by offset and by key, side by side.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) walk the feed  2) events  3) page size  4) feed size  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_walk(n, events, size)\n    elif choice == \"2\":\n        e = set_events()\n        if len(e) > 0:\n            events = e\n    elif choice == \"3\":\n        t = plain(input(\"rows to a page (2 to 10)> \"))\n        v = number(t, 2)\n        if t == \"\":\n            print(\"Cancelled.\")\n        elif v < 2 or v > 10:\n            print(\"Type a whole number from 2 to 10.\")\n        else:\n            size = v\n            print(str(v) + \" rows to a page.\")\n    elif choice == \"4\":\n        t = plain(input(\"items in the feed (5 to 60)> \"))\n        v = number(t, 2)\n        if t == \"\":\n            print(\"Cancelled.\")\n        elif v < 5 or v > 60:\n            print(\"Type a whole number from 5 to 60.\")\n        else:\n            n = v\n            print(\"A feed of \" + str(v) + \" items, \" + str(v) + \" down to 1.\")\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "pages.eml",
      "eml": "# P110 pagination - a feed, newest first, read page by page while items\n# are added and deleted between the requests. Offset pagination asks for\n# \"the next 5 after the first 10\"; keyset (cursor) pagination asks for \"the\n# next 5 older than the last one I got\". As in the corpus case\n# pagination-shifting-data, an item added before the window makes offset\n# pagination deliver a row twice, and an item deleted before the window\n# makes it skip one - while every page still comes back full. Keyset\n# pagination does neither.\n#\n# Events are \"page\", \"new\" (an item added at the top) and \"del N\".\n\ndef feed(n):\n    # ids n down to 1, newest first\n    [] => out\n    for i in [1:n]:\n        out + [n + 1 - i] => out\n    return out\n\ndef has(xs, x):\n    for y in xs:\n        if y == x:\n            return True\n    return False\n\ndef without(xs, x):\n    [] => out\n    for y in xs:\n        if y != x:\n            out + [y] => out\n    return out\n\ndef walk(start, events, size):\n    # [pages by offset, pages by key, ids added, ids deleted]; each page\n    # is a list of ids. A walk's requests stop at the end of the events.\n    start => items\n    0 => offset\n    -1 => cursor\n    [] => by_offset\n    [] => by_key\n    [] => added\n    [] => deleted\n    0 => top\n    for x in start:\n        if x > top:\n            x => top\n    for e in events:\n        if e[0] == \"new\":\n            top + 1 => top\n            [top] + items => items\n            added + [top] => added\n        elif e[0] == \"del\":\n            if has(items, e[1]):\n                without(items, e[1]) => items\n                deleted + [e[1]] => deleted\n        else:\n            [] => page\n            for k in [offset:offset + size - 1]:\n                if k < len(items):\n                    page + [items[k]] => page\n            offset + size => offset\n            by_offset + [page] => by_offset\n            [] => kpage\n            for x in items:\n                if len(kpage) < size and (cursor == -1 or x < cursor):\n                    kpage + [x] => kpage\n            if len(kpage) > 0:\n                kpage[len(kpage) - 1] => cursor\n            by_key + [kpage] => by_key\n    return [by_offset, by_key, added, deleted]\n\ndef tally(start, pages, added, deleted):\n    # [rows never delivered, rows delivered twice or more, rows not reached\n    # yet]: only rows that were there for the whole walk - not added, not\n    # deleted - are judged, and a row older than every row delivered is\n    # one the pages have not reached yet, not one they skipped\n    {} => seen\n    -1 => oldest\n    for p in pages:\n        for x in p:\n            if x in seen:\n                seen[x] + 1 => seen[x]\n            else:\n                1 => seen[x]\n            if oldest == -1 or x < oldest:\n                x => oldest\n    [] => missing\n    [] => twice\n    0 => later\n    for x in start:\n        if not has(deleted, x):\n            if not (x in seen):\n                if oldest != -1 and x > oldest:\n                    missing + [x] => missing\n                else:\n                    later + 1 => later\n            elif seen[x] > 1:\n                twice + [x] => twice\n    return [missing, twice, later]\n\ndef parse(text):\n    # \"page new page del 17 page\" -> [[\"page\", 0], [\"new\", 0], ...] or []\n    [] => words\n    \"\" => w\n    for c in text + \" \":\n        if c == \" \":\n            if w != \"\":\n                words + [w] => words\n            \"\" => w\n        else:\n            w + c => w\n    [] => out\n    0 => i\n    while i < len(words):\n        if words[i] == \"page\" or words[i] == \"new\":\n            out + [[words[i], 0]] => out\n            i + 1 => i\n        elif words[i] == \"del\" and i + 1 < len(words):\n            0 => n\n            True => ok\n            for c in words[i + 1]:\n                if c >= \"0\" and c <= \"9\":\n                    n * 10 + int(c) => n\n                else:\n                    False => ok\n            if not ok or len(words[i + 1]) > 3:\n                return []\n            out + [[\"del\", n]] => out\n            i + 2 => i\n        else:\n            return []\n    if len(out) == 0 or len(out) > 30:\n        return []\n    return out\n",
      "python": "def feed(n):\n    out = []\n    for i in range(1, n+1):\n        out = out + [n + 1 - i]\n    return out\n\ndef has(xs, x):\n    for y in xs:\n        if y == x:\n            return True\n    return False\n\ndef without(xs, x):\n    out = []\n    for y in xs:\n        if y != x:\n            out = out + [y]\n    return out\n\ndef walk(start, events, size):\n    items = start\n    offset = 0\n    cursor = -1\n    by_offset = []\n    by_key = []\n    added = []\n    deleted = []\n    top = 0\n    for x in start:\n        if x > top:\n            top = x\n    for e in events:\n        if e[0] == \"new\":\n            top = top + 1\n            items = [top] + items\n            added = added + [top]\n        elif e[0] == \"del\":\n            if has(items, e[1]):\n                items = without(items, e[1])\n                deleted = deleted + [e[1]]\n        else:\n            page = []\n            for k in range(offset, offset + size):\n                if k < len(items):\n                    page = page + [items[k]]\n            offset = offset + size\n            by_offset = by_offset + [page]\n            kpage = []\n            for x in items:\n                if len(kpage) < size and (cursor == -1 or x < cursor):\n                    kpage = kpage + [x]\n            if len(kpage) > 0:\n                cursor = kpage[len(kpage) - 1]\n            by_key = by_key + [kpage]\n    return [by_offset, by_key, added, deleted]\n\ndef tally(start, pages, added, deleted):\n    seen = {}\n    oldest = -1\n    for p in pages:\n        for x in p:\n            if x in seen:\n                seen[x] = seen[x] + 1\n            else:\n                seen[x] = 1\n            if oldest == -1 or x < oldest:\n                oldest = x\n    missing = []\n    twice = []\n    later = 0\n    for x in start:\n        if not has(deleted, x):\n            if not x in seen:\n                if oldest != -1 and x > oldest:\n                    missing = missing + [x]\n                else:\n                    later = later + 1\n            elif seen[x] > 1:\n                twice = twice + [x]\n    return [missing, twice, later]\n\ndef parse(text):\n    words = []\n    w = \"\"\n    for c in text + \" \":\n        if c == \" \":\n            if w != \"\":\n                words = words + [w]\n            w = \"\"\n        else:\n            w = w + c\n    out = []\n    i = 0\n    while i < len(words):\n        if words[i] == \"page\" or words[i] == \"new\":\n            out = out + [[words[i], 0]]\n            i = i + 1\n        elif words[i] == \"del\" and i + 1 < len(words):\n            n = 0\n            ok = True\n            for c in words[i + 1]:\n                if c >= \"0\" and c <= \"9\":\n                    n = n * 10 + int(c)\n                else:\n                    ok = False\n            if not ok or len(words[i + 1]) > 3:\n                return []\n            out = out + [[\"del\", n]]\n            i = i + 2\n        else:\n            return []\n    if len(out) == 0 or len(out) > 30:\n        return []\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n\n2\njump\n2\npage del\n2\npage del x\n2\ndel 3 new\n2\npage del 1234\n2\nnew del 21 del 21 page\n1\n3\n\n3\n1\n3\n11\n4\n\n4\n4\n4\n61\n4\n7\n1\n5\n",
      "screen": "== Pagination ==\nA feed read a page at a time while items are added and deleted: by offset and by key, side by side.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> \nCancelled.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> jump\nType words page, new and del followed by a number, separated by spaces.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page del\nType words page, new and del followed by a number, separated by spaces.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page del x\nType words page, new and del followed by a number, separated by spaces.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> del 3 new\nAsk for at least one page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page del 1234\nType words page, new and del followed by a number, separated by spaces.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> new del 21 del 21 page\nEvents: new del 21 del 21 page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 20 items, newest first; 5 to a page; events: new del 21 del 21 page.\n           by offset          by key\n  new: item 21 added at the top\n  del 21: item 21 deleted\n  del 21: there is no item 21 now\n  page 1   20 19 18 17 16     20 19 18 17 16\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: every one reached was delivered exactly once; 15 older not reached yet\n  by key: every one reached was delivered exactly once; 15 older not reached yet\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 3\nrows to a page (2 to 10)> \nCancelled.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 3\nrows to a page (2 to 10)> 1\nType a whole number from 2 to 10.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 3\nrows to a page (2 to 10)> 11\nType a whole number from 2 to 10.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 4\nitems in the feed (5 to 60)> \nCancelled.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 4\nitems in the feed (5 to 60)> 4\nType a whole number from 5 to 60.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 4\nitems in the feed (5 to 60)> 61\nType a whole number from 5 to 60.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 4\nitems in the feed (5 to 60)> 7\nA feed of 7 items, 7 down to 1.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 7 items, newest first; 5 to a page; events: new del 21 del 21 page.\n           by offset          by key\n  new: item 8 added at the top\n  del 21: there is no item 21 now\n  del 21: there is no item 21 now\n  page 1   8 7 6 5 4          8 7 6 5 4\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: every one reached was delivered exactly once; 3 older not reached yet\n  by key: every one reached was delivered exactly once; 3 older not reached yet\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\npage page page page\n1\n2\npage new new page page\n1\n2\npage page del 3 page\n1\n3\n4\n2\npage del 20 page del 19 page page page page\n1\n5\n",
      "screen": "== Pagination ==\nA feed read a page at a time while items are added and deleted: by offset and by key, side by side.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 20 items, newest first; 5 to a page; events: page new page del 17 page page page.\n           by offset          by key\n  page 1   20 19 18 17 16     20 19 18 17 16\n  new: item 21 added at the top\n  page 2   16 15 14 13 12     15 14 13 12 11\n  del 17: item 17 deleted\n  page 3   10 9 8 7 6         10 9 8 7 6\n  page 4   5 4 3 2 1          5 4 3 2 1\n  page 5   (empty)            (empty)\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: 16 delivered twice; 11 never delivered\n    page 3 went past 11 and still came back with all 5 rows: nothing in it shows a row is missing\n  by key: every one reached was delivered exactly once\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page page page page\nEvents: page page page page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 20 items, newest first; 5 to a page; events: page page page page.\n           by offset          by key\n  page 1   20 19 18 17 16     20 19 18 17 16\n  page 2   15 14 13 12 11     15 14 13 12 11\n  page 3   10 9 8 7 6         10 9 8 7 6\n  page 4   5 4 3 2 1          5 4 3 2 1\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: every one reached was delivered exactly once\n  by key: every one reached was delivered exactly once\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page new new page page\nEvents: page new new page page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 20 items, newest first; 5 to a page; events: page new new page page.\n           by offset          by key\n  page 1   20 19 18 17 16     20 19 18 17 16\n  new: item 21 added at the top\n  new: item 22 added at the top\n  page 2   17 16 15 14 13     15 14 13 12 11\n  page 3   12 11 10 9 8       10 9 8 7 6\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: 17, 16 delivered twice; 7 older not reached yet\n  by key: every one reached was delivered exactly once; 5 older not reached yet\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page page del 3 page\nEvents: page page del 3 page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 20 items, newest first; 5 to a page; events: page page del 3 page.\n           by offset          by key\n  page 1   20 19 18 17 16     20 19 18 17 16\n  page 2   15 14 13 12 11     15 14 13 12 11\n  del 3: item 3 deleted\n  page 3   10 9 8 7 6         10 9 8 7 6\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: every one reached was delivered exactly once; 4 older not reached yet\n  by key: every one reached was delivered exactly once; 4 older not reached yet\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 3\nrows to a page (2 to 10)> 4\n4 rows to a page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 2\nEvents: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.\nevents> page del 20 page del 19 page page page page\nEvents: page del 20 page del 19 page page page page.\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 1\nFeed: 20 items, newest first; 4 to a page; events: page del 20 page del 19 page page page page.\n           by offset       by key\n  page 1   20 19 18 17     20 19 18 17\n  del 20: item 20 deleted\n  page 2   15 14 13 12     16 15 14 13\n  del 19: item 19 deleted\n  page 3   10 9 8 7        12 11 10 9\n  page 4   6 5 4 3         8 7 6 5\n  page 5   2 1             4 3 2 1\n  page 6   (empty)         (empty)\nJudged on the items that were there for the whole walk, not added and not deleted:\n  by offset: 16, 11 never delivered\n    page 2 went past 16 and still came back with all 4 rows: nothing in it shows a row is missing\n    page 3 went past 11 and still came back with all 4 rows: nothing in it shows a row is missing\n  by key: every one reached was delivered exactly once\n\n1) walk the feed  2) events  3) page size  4) feed size  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "pagination-shifting-data",
      "caseId": "255-pagination-shifting-data",
      "title": "Pagination over shifting data — the skip nobody notices"
    }
  ],
  "updated": "2026-10-16"
}
