<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P110-pagination/ | updated: 2026-10-16 -->

# P110 Pagination

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.

EML-P project `projects/pagination` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/pagination` runs it and `eml project verify projects/pagination` 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: pagination-shifting-data (https://efficientnewlanguage.org/cases/255-pagination-shifting-data/).

## Sessions

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

Input:

```text
0
x
2

2
jump
2
page del
2
page del x
2
del 3 new
2
page del 1234
2
new del 21 del 21 page
1
3

3
1
3
11
4

4
4
4
61
4
7
1
5
```

Screen:

```text
== Pagination ==
A feed read a page at a time while items are added and deleted: by offset and by key, side by side.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 0
Pick a number from 1 to 5.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> x
Pick a number from 1 to 5.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> 
Cancelled.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> jump
Type words page, new and del followed by a number, separated by spaces.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page del
Type words page, new and del followed by a number, separated by spaces.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page del x
Type words page, new and del followed by a number, separated by spaces.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> del 3 new
Ask for at least one page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page del 1234
Type words page, new and del followed by a number, separated by spaces.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> new del 21 del 21 page
Events: new del 21 del 21 page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 20 items, newest first; 5 to a page; events: new del 21 del 21 page.
           by offset          by key
  new: item 21 added at the top
  del 21: item 21 deleted
  del 21: there is no item 21 now
  page 1   20 19 18 17 16     20 19 18 17 16
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: every one reached was delivered exactly once; 15 older not reached yet
  by key: every one reached was delivered exactly once; 15 older not reached yet

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 3
rows to a page (2 to 10)> 
Cancelled.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 3
rows to a page (2 to 10)> 1
Type a whole number from 2 to 10.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 3
rows to a page (2 to 10)> 11
Type a whole number from 2 to 10.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 4
items in the feed (5 to 60)> 
Cancelled.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 4
items in the feed (5 to 60)> 4
Type a whole number from 5 to 60.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 4
items in the feed (5 to 60)> 61
Type a whole number from 5 to 60.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 4
items in the feed (5 to 60)> 7
A feed of 7 items, 7 down to 1.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 7 items, newest first; 5 to a page; events: new del 21 del 21 page.
           by offset          by key
  new: item 8 added at the top
  del 21: there is no item 21 now
  del 21: there is no item 21 now
  page 1   8 7 6 5 4          8 7 6 5 4
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: every one reached was delivered exactly once; 3 older not reached yet
  by key: every one reached was delivered exactly once; 3 older not reached yet

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 5
Bye.
```

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

Input:

```text
1
2
page page page page
1
2
page new new page page
1
2
page page del 3 page
1
3
4
2
page del 20 page del 19 page page page page
1
5
```

Screen:

```text
== Pagination ==
A feed read a page at a time while items are added and deleted: by offset and by key, side by side.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 20 items, newest first; 5 to a page; events: page new page del 17 page page page.
           by offset          by key
  page 1   20 19 18 17 16     20 19 18 17 16
  new: item 21 added at the top
  page 2   16 15 14 13 12     15 14 13 12 11
  del 17: item 17 deleted
  page 3   10 9 8 7 6         10 9 8 7 6
  page 4   5 4 3 2 1          5 4 3 2 1
  page 5   (empty)            (empty)
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: 16 delivered twice; 11 never delivered
    page 3 went past 11 and still came back with all 5 rows: nothing in it shows a row is missing
  by key: every one reached was delivered exactly once

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page page page page
Events: page page page page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 20 items, newest first; 5 to a page; events: page page page page.
           by offset          by key
  page 1   20 19 18 17 16     20 19 18 17 16
  page 2   15 14 13 12 11     15 14 13 12 11
  page 3   10 9 8 7 6         10 9 8 7 6
  page 4   5 4 3 2 1          5 4 3 2 1
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: every one reached was delivered exactly once
  by key: every one reached was delivered exactly once

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page new new page page
Events: page new new page page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 20 items, newest first; 5 to a page; events: page new new page page.
           by offset          by key
  page 1   20 19 18 17 16     20 19 18 17 16
  new: item 21 added at the top
  new: item 22 added at the top
  page 2   17 16 15 14 13     15 14 13 12 11
  page 3   12 11 10 9 8       10 9 8 7 6
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: 17, 16 delivered twice; 7 older not reached yet
  by key: every one reached was delivered exactly once; 5 older not reached yet

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page page del 3 page
Events: page page del 3 page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 20 items, newest first; 5 to a page; events: page page del 3 page.
           by offset          by key
  page 1   20 19 18 17 16     20 19 18 17 16
  page 2   15 14 13 12 11     15 14 13 12 11
  del 3: item 3 deleted
  page 3   10 9 8 7 6         10 9 8 7 6
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: every one reached was delivered exactly once; 4 older not reached yet
  by key: every one reached was delivered exactly once; 4 older not reached yet

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 3
rows to a page (2 to 10)> 4
4 rows to a page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 2
Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.
events> page del 20 page del 19 page page page page
Events: page del 20 page del 19 page page page page.

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 1
Feed: 20 items, newest first; 4 to a page; events: page del 20 page del 19 page page page page.
           by offset       by key
  page 1   20 19 18 17     20 19 18 17
  del 20: item 20 deleted
  page 2   15 14 13 12     16 15 14 13
  del 19: item 19 deleted
  page 3   10 9 8 7        12 11 10 9
  page 4   6 5 4 3         8 7 6 5
  page 5   2 1             4 3 2 1
  page 6   (empty)         (empty)
Judged on the items that were there for the whole walk, not added and not deleted:
  by offset: 16, 11 never delivered
    page 2 went past 16 and still came back with all 4 rows: nothing in it shows a row is missing
    page 3 went past 11 and still came back with all 4 rows: nothing in it shows a row is missing
  by key: every one reached was delivered exactly once

1) walk the feed  2) events  3) page size  4) feed size  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P110 pagination: a feed read page by page while items come and go,
# by offset and by key side by side, and what each one delivered.
import pages

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 number(s, most):
    if len(s) == 0 or len(s) > most:
        return -1
    0 => n
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n * 10 + int(c) => n
    return n

def pad(s, n):
    while len(s) < n:
        s + " " => s
    return s

def ids(xs):
    if len(xs) == 0:
        return "(empty)"
    "" => out
    for x in xs:
        if out != "":
            out + " " => out
        out + str(x) => out
    return out

def listed(xs):
    "" => out
    for x in xs:
        if out != "":
            out + ", " => out
        out + str(x) => out
    return out

def events_text(events):
    "" => out
    for e in events:
        if out != "":
            out + " " => out
        if e[0] == "del":
            out + "del " + str(e[1]) => out
        else:
            out + e[0] => out
    return out

def show_walk(n, events, size):
    pages.feed(n) => start
    pages.walk(start, events, size) => w
    ("Feed: " + str(n) + " items, newest first; " + str(size) + " to a page; events: " + events_text(events) + ".") ^0
    ("  " + pad("", 9) + pad("by offset", 3 * size + 4) + "by key") ^0
    0 => k
    n => top
    start => present
    for e in events:
        if e[0] == "page":
            ("  " + pad("page " + str(k + 1), 9) + pad(ids(w[0][k]), 3 * size + 4) + ids(w[1][k])) ^0
            k + 1 => k
        elif e[0] == "new":
            top + 1 => top
            [top] + present => present
            ("  new: item " + str(top) + " added at the top") ^0
        elif pages.has(present, e[1]):
            pages.without(present, e[1]) => present
            ("  del " + str(e[1]) + ": item " + str(e[1]) + " deleted") ^0
        else:
            ("  del " + str(e[1]) + ": there is no item " + str(e[1]) + " now") ^0
    "Judged on the items that were there for the whole walk, not added and not deleted:" ^0
    ["by offset", "by key"] => names
    for m in [0:1]:
        pages.tally(start, w[m], w[2], w[3]) => t
        "" => tail
        if t[2] > 0:
            "; " + str(t[2]) + " older not reached yet" => tail
        if len(t[0]) == 0 and len(t[1]) == 0:
            ("  " + names[m] + ": every one reached was delivered exactly once" + tail) ^0
        else:
            "" => line
            if len(t[1]) > 0:
                listed(t[1]) + " delivered twice" => line
            if len(t[0]) > 0:
                if line != "":
                    line + "; " => line
                line + listed(t[0]) + " never delivered" => line
            ("  " + names[m] + ": " + line + tail) ^0
            for r in t[0]:
                # the page that should have held r: the first with an older item
                -1 => at
                for i in [0:len(w[m]) - 1]:
                    if at == -1:
                        for x in w[m][i]:
                            if x < r:
                                i => at
                if at >= 0:
                    if len(w[m][at]) == size:
                        ("    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
                    else:
                        ("    page " + str(at + 1) + " went past " + str(r)) ^0

def set_events():
    "Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30." ^0
    plain(input("events> ")) => t
    if t == "":
        "Cancelled." ^0
        return []
    pages.parse(t) => e
    if len(e) == 0:
        "Type words page, new and del followed by a number, separated by spaces." ^0
        return []
    0 => p
    for x in e:
        if x[0] == "page":
            p + 1 => p
    if p == 0:
        "Ask for at least one page." ^0
        return []
    ("Events: " + events_text(e) + ".") ^0
    return e

[["page", 0], ["new", 0], ["page", 0], ["del", 17], ["page", 0], ["page", 0], ["page", 0]] => events
20 => n
5 => size

"== Pagination ==" ^0
"A feed read a page at a time while items are added and deleted: by offset and by key, side by side." ^0
True => running
while running:
    "" ^0
    "1) walk the feed  2) events  3) page size  4) feed size  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_walk(n, events, size)
    elif choice == "2":
        set_events() => e
        if len(e) > 0:
            e => events
    elif choice == "3":
        plain(input("rows to a page (2 to 10)> ")) => t
        number(t, 2) => v
        if t == "":
            "Cancelled." ^0
        elif v < 2 or v > 10:
            "Type a whole number from 2 to 10." ^0
        else:
            v => size
            (str(v) + " rows to a page.") ^0
    elif choice == "4":
        plain(input("items in the feed (5 to 60)> ")) => t
        number(t, 2) => v
        if t == "":
            "Cancelled." ^0
        elif v < 5 or v > 60:
            "Type a whole number from 5 to 60." ^0
        else:
            v => n
            ("A feed of " + str(v) + " items, " + str(v) + " down to 1.") ^0
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import pages

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 number(s, most):
    if len(s) == 0 or len(s) > most:
        return -1
    n = 0
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n = n * 10 + int(c)
    return n

def pad(s, n):
    while len(s) < n:
        s = s + " "
    return s

def ids(xs):
    if len(xs) == 0:
        return "(empty)"
    out = ""
    for x in xs:
        if out != "":
            out = out + " "
        out = out + str(x)
    return out

def listed(xs):
    out = ""
    for x in xs:
        if out != "":
            out = out + ", "
        out = out + str(x)
    return out

def events_text(events):
    out = ""
    for e in events:
        if out != "":
            out = out + " "
        if e[0] == "del":
            out = out + "del " + str(e[1])
        else:
            out = out + e[0]
    return out

def show_walk(n, events, size):
    start = pages.feed(n)
    w = pages.walk(start, events, size)
    print("Feed: " + str(n) + " items, newest first; " + str(size) + " to a page; events: " + events_text(events) + ".")
    print("  " + pad("", 9) + pad("by offset", 3 * size + 4) + "by key")
    k = 0
    top = n
    present = start
    for e in events:
        if e[0] == "page":
            print("  " + pad("page " + str(k + 1), 9) + pad(ids(w[0][k]), 3 * size + 4) + ids(w[1][k]))
            k = k + 1
        elif e[0] == "new":
            top = top + 1
            present = [top] + present
            print("  new: item " + str(top) + " added at the top")
        elif pages.has(present, e[1]):
            present = pages.without(present, e[1])
            print("  del " + str(e[1]) + ": item " + str(e[1]) + " deleted")
        else:
            print("  del " + str(e[1]) + ": there is no item " + str(e[1]) + " now")
    print("Judged on the items that were there for the whole walk, not added and not deleted:")
    names = ["by offset", "by key"]
    for m in range(0, 2):
        t = pages.tally(start, w[m], w[2], w[3])
        tail = ""
        if t[2] > 0:
            tail = "; " + str(t[2]) + " older not reached yet"
        if len(t[0]) == 0 and len(t[1]) == 0:
            print("  " + names[m] + ": every one reached was delivered exactly once" + tail)
        else:
            line = ""
            if len(t[1]) > 0:
                line = listed(t[1]) + " delivered twice"
            if len(t[0]) > 0:
                if line != "":
                    line = line + "; "
                line = line + listed(t[0]) + " never delivered"
            print("  " + names[m] + ": " + line + tail)
            for r in t[0]:
                at = -1
                for i in range(0, len(w[m])):
                    if at == -1:
                        for x in w[m][i]:
                            if x < r:
                                at = i
                if at >= 0:
                    if len(w[m][at]) == size:
                        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")
                    else:
                        print("    page " + str(at + 1) + " went past " + str(r))

def set_events():
    print("Events: page (ask for the next page), new (an item added at the top), del N (item N deleted); up to 30.")
    t = plain(input("events> "))
    if t == "":
        print("Cancelled.")
        return []
    e = pages.parse(t)
    if len(e) == 0:
        print("Type words page, new and del followed by a number, separated by spaces.")
        return []
    p = 0
    for x in e:
        if x[0] == "page":
            p = p + 1
    if p == 0:
        print("Ask for at least one page.")
        return []
    print("Events: " + events_text(e) + ".")
    return e

events = [["page", 0], ["new", 0], ["page", 0], ["del", 17], ["page", 0], ["page", 0], ["page", 0]]
n = 20
size = 5
print("== Pagination ==")
print("A feed read a page at a time while items are added and deleted: by offset and by key, side by side.")
running = True
while running:
    print("")
    print("1) walk the feed  2) events  3) page size  4) feed size  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_walk(n, events, size)
    elif choice == "2":
        e = set_events()
        if len(e) > 0:
            events = e
    elif choice == "3":
        t = plain(input("rows to a page (2 to 10)> "))
        v = number(t, 2)
        if t == "":
            print("Cancelled.")
        elif v < 2 or v > 10:
            print("Type a whole number from 2 to 10.")
        else:
            size = v
            print(str(v) + " rows to a page.")
    elif choice == "4":
        t = plain(input("items in the feed (5 to 60)> "))
        v = number(t, 2)
        if t == "":
            print("Cancelled.")
        elif v < 5 or v > 60:
            print("Type a whole number from 5 to 60.")
        else:
            n = v
            print("A feed of " + str(v) + " items, " + str(v) + " down to 1.")
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### pages.eml

```eml
# P110 pagination - a feed, newest first, read page by page while items
# are added and deleted between the requests. Offset pagination asks for
# "the next 5 after the first 10"; keyset (cursor) pagination asks for "the
# next 5 older than the last one I got". As in the corpus case
# pagination-shifting-data, an item added before the window makes offset
# pagination deliver a row twice, and an item deleted before the window
# makes it skip one - while every page still comes back full. Keyset
# pagination does neither.
#
# Events are "page", "new" (an item added at the top) and "del N".

def feed(n):
    # ids n down to 1, newest first
    [] => out
    for i in [1:n]:
        out + [n + 1 - i] => out
    return out

def has(xs, x):
    for y in xs:
        if y == x:
            return True
    return False

def without(xs, x):
    [] => out
    for y in xs:
        if y != x:
            out + [y] => out
    return out

def walk(start, events, size):
    # [pages by offset, pages by key, ids added, ids deleted]; each page
    # is a list of ids. A walk's requests stop at the end of the events.
    start => items
    0 => offset
    -1 => cursor
    [] => by_offset
    [] => by_key
    [] => added
    [] => deleted
    0 => top
    for x in start:
        if x > top:
            x => top
    for e in events:
        if e[0] == "new":
            top + 1 => top
            [top] + items => items
            added + [top] => added
        elif e[0] == "del":
            if has(items, e[1]):
                without(items, e[1]) => items
                deleted + [e[1]] => deleted
        else:
            [] => page
            for k in [offset:offset + size - 1]:
                if k < len(items):
                    page + [items[k]] => page
            offset + size => offset
            by_offset + [page] => by_offset
            [] => kpage
            for x in items:
                if len(kpage) < size and (cursor == -1 or x < cursor):
                    kpage + [x] => kpage
            if len(kpage) > 0:
                kpage[len(kpage) - 1] => cursor
            by_key + [kpage] => by_key
    return [by_offset, by_key, added, deleted]

def tally(start, pages, added, deleted):
    # [rows never delivered, rows delivered twice or more, rows not reached
    # yet]: only rows that were there for the whole walk - not added, not
    # deleted - are judged, and a row older than every row delivered is
    # one the pages have not reached yet, not one they skipped
    {} => seen
    -1 => oldest
    for p in pages:
        for x in p:
            if x in seen:
                seen[x] + 1 => seen[x]
            else:
                1 => seen[x]
            if oldest == -1 or x < oldest:
                x => oldest
    [] => missing
    [] => twice
    0 => later
    for x in start:
        if not has(deleted, x):
            if not (x in seen):
                if oldest != -1 and x > oldest:
                    missing + [x] => missing
                else:
                    later + 1 => later
            elif seen[x] > 1:
                twice + [x] => twice
    return [missing, twice, later]

def parse(text):
    # "page new page del 17 page" -> [["page", 0], ["new", 0], ...] or []
    [] => words
    "" => w
    for c in text + " ":
        if c == " ":
            if w != "":
                words + [w] => words
            "" => w
        else:
            w + c => w
    [] => out
    0 => i
    while i < len(words):
        if words[i] == "page" or words[i] == "new":
            out + [[words[i], 0]] => out
            i + 1 => i
        elif words[i] == "del" and i + 1 < len(words):
            0 => n
            True => ok
            for c in words[i + 1]:
                if c >= "0" and c <= "9":
                    n * 10 + int(c) => n
                else:
                    False => ok
            if not ok or len(words[i + 1]) > 3:
                return []
            out + [["del", n]] => out
            i + 2 => i
        else:
            return []
    if len(out) == 0 or len(out) > 30:
        return []
    return out
```

Python projection of pages.eml:

```python
def feed(n):
    out = []
    for i in range(1, n+1):
        out = out + [n + 1 - i]
    return out

def has(xs, x):
    for y in xs:
        if y == x:
            return True
    return False

def without(xs, x):
    out = []
    for y in xs:
        if y != x:
            out = out + [y]
    return out

def walk(start, events, size):
    items = start
    offset = 0
    cursor = -1
    by_offset = []
    by_key = []
    added = []
    deleted = []
    top = 0
    for x in start:
        if x > top:
            top = x
    for e in events:
        if e[0] == "new":
            top = top + 1
            items = [top] + items
            added = added + [top]
        elif e[0] == "del":
            if has(items, e[1]):
                items = without(items, e[1])
                deleted = deleted + [e[1]]
        else:
            page = []
            for k in range(offset, offset + size):
                if k < len(items):
                    page = page + [items[k]]
            offset = offset + size
            by_offset = by_offset + [page]
            kpage = []
            for x in items:
                if len(kpage) < size and (cursor == -1 or x < cursor):
                    kpage = kpage + [x]
            if len(kpage) > 0:
                cursor = kpage[len(kpage) - 1]
            by_key = by_key + [kpage]
    return [by_offset, by_key, added, deleted]

def tally(start, pages, added, deleted):
    seen = {}
    oldest = -1
    for p in pages:
        for x in p:
            if x in seen:
                seen[x] = seen[x] + 1
            else:
                seen[x] = 1
            if oldest == -1 or x < oldest:
                oldest = x
    missing = []
    twice = []
    later = 0
    for x in start:
        if not has(deleted, x):
            if not x in seen:
                if oldest != -1 and x > oldest:
                    missing = missing + [x]
                else:
                    later = later + 1
            elif seen[x] > 1:
                twice = twice + [x]
    return [missing, twice, later]

def parse(text):
    words = []
    w = ""
    for c in text + " ":
        if c == " ":
            if w != "":
                words = words + [w]
            w = ""
        else:
            w = w + c
    out = []
    i = 0
    while i < len(words):
        if words[i] == "page" or words[i] == "new":
            out = out + [[words[i], 0]]
            i = i + 1
        elif words[i] == "del" and i + 1 < len(words):
            n = 0
            ok = True
            for c in words[i + 1]:
                if c >= "0" and c <= "9":
                    n = n * 10 + int(c)
                else:
                    ok = False
            if not ok or len(words[i + 1]) > 3:
                return []
            out = out + [["del", n]]
            i = i + 2
        else:
            return []
    if len(out) == 0 or len(out) > 30:
        return []
    return out
```

## README

# P110 - Pagination

A feed, newest first, read one page at a time while items are added and
deleted between the requests. Offset pagination and keyset (cursor)
pagination run side by side, and the program reports what each one
delivered.

- `main.eml` - the menu, the side-by-side walk and its verdict, the
  events, the page size and the feed size
- `pages.eml` - the feed, the walk under both kinds of pagination, the
  tally of what was delivered, and reading the events

How each part works:

- Offset pagination asks for "the next 5 after the first 10". Keyset
  pagination asks for "the next 5 older than the last one I got".
- Events run in order:
  - page asks both for the next page;
  - new adds an item at the top;
  - del N deletes item N.
- As in the corpus case `pagination-shifting-data`:
  - an item added before the window makes offset pagination deliver a row
    a second time;
  - an item deleted before the window makes it skip one, and the page that
    goes past the skipped row still comes back full, so nothing in it shows
    that a row is missing;
  - keyset pagination does neither.
- The verdict judges only the items that were there for the whole walk,
  not added and not deleted. An item older than everything delivered so
  far has not been reached yet, rather than skipped.

What is checked: menu choices 1 to 5; up to 30 events, made of page, new
and del followed by a number of up to three digits, with at least one
page; 2 to 10 rows to a page; a feed of 5 to 60 items. An empty answer
cancels.

Sessions: `sessions/basic.in` gives:
- the starting walk: an item added after page 1 makes offset repeat 16,
  and 17 deleted after page 2 makes it skip 11 on a full page;
- four pages and nothing else: both clean;
- two items added after page 1: offset repeats 17 and 16;
- an item deleted after page 2, ahead of both;
- 4 to a page, with 20 and 19 deleted after pages 1 and 2: offset skips 16
  and 11, each on a full page; keyset delivers every one.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- events: a cancel, an unknown word, del with no number, del with a
  letter, no page at all, a four-digit number; then a script that deletes
  21 twice, and its walk;
- page sizes: a cancel, 1 and 11; feed sizes: a cancel, 4 and 61, then 7,
  and the walk again.

Built on the verified corpus case `pagination-shifting-data` (offset
pagination repeats a row on an insert and skips one on a delete, the
skip invisible behind a full page).
