<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P017-flashcards/ | updated: 2026-10-03 -->

# P017 Flashcards

Practise decks of flashcards with Leitner boxes: a card answered right the first time moves up a box, a wrong one goes back to box 1 and comes again at the end of the practice; box k is due every 2^(k-1) practices. Two decks to start, new decks and cards, and a progress chart per deck - from a text menu.

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

## Sessions

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

Input:

```text
7
2
0
x

5

5
A deck name far too long
capitals
Empty
2
3
3
3
4
9
1

4
1
france
A front that is far too long for any card here
Italy

2
1
Paris
q
3
1
2
1
q
1
6
```

Screen:

```text

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 7
Pick a number from 1 to 6.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 0
Type a deck number from 1 to 2, or nothing to cancel.
deck> x
Type a deck number from 1 to 2, or nothing to cancel.
deck> 
Cancelled.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 5
deck name> 
Cancelled.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 5
deck name> A deck name far too long
At most 20 characters.
deck name> capitals
There is already a deck called Capitals.
deck name> Empty
Deck 3: Empty.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 3
No cards in this deck yet.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 3
deck> 3

-- Empty: 0 cards, no practice yet --
  box 1   0
  box 2   0
  box 3   0
  box 4   0
  box 5   0
  next practice (1): box 1 - 0 cards

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 4
deck> 9
Type a deck number from 1 to 3, or nothing to cancel.
deck> 1
front> 
Cancelled.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 4
deck> 1
front> france
There is already a card France in Capitals.
front> A front that is far too long for any card here
At most 40 characters.
front> Italy
back> 
Cancelled.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 1
Practice 1 of Capitals: box 1 - 8 cards.
Type the answer, or q to stop.

France   (box 1)
answer> Paris
Right - now in box 2.

Japan   (box 1)
answer> q
Stopped: 1 of 1 right the first time; the other cards stay where they were.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 3
deck> 1

-- Capitals: 8 cards, 1 practice done --
  box 1   7  #######
  box 2   1  #
  box 3   0
  box 4   0
  box 5   0
  next practice (2): boxes 1 and 2 - 8 cards

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 1
Practice 2 of Capitals: boxes 1 and 2 - 8 cards.
Type the answer, or q to stop.

Japan   (box 1)
answer> q
Stopped before any answer; this practice does not count.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 1

-- decks --
  1) Capitals             8 cards   1 practice done
  2) Spanish              8 cards   no practice yet
  3) Empty                0 cards   no practice yet

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 6
Bye.
```

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

Input:

```text
1
2
1
Paris
Tokyo
Mombasa
Lima
Toronto
Cairo
Oslo
Santiago
Nairobi
ottawa
3
1
2
1
Nairobi
Ottawa
Paris
Tokyo
Lima
Cairo
Bergen
Santiago
Oslo
2
1
Oslo
3
1
4
2
book
libro
5
Science
4
3
H2O
water
1
6
```

Screen:

```text

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 1

-- decks --
  1) Capitals             8 cards   no practice yet
  2) Spanish              8 cards   no practice yet

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 1
Practice 1 of Capitals: box 1 - 8 cards.
Type the answer, or q to stop.

France   (box 1)
answer> Paris
Right - now in box 2.

Japan   (box 1)
answer> Tokyo
Right - now in box 2.

Kenya   (box 1)
answer> Mombasa
No - it is Nairobi. Back to box 1; it comes again at the end.

Peru   (box 1)
answer> Lima
Right - now in box 2.

Canada   (box 1)
answer> Toronto
No - it is Ottawa. Back to box 1; it comes again at the end.

Egypt   (box 1)
answer> Cairo
Right - now in box 2.

Norway   (box 1)
answer> Oslo
Right - now in box 2.

Chile   (box 1)
answer> Santiago
Right - now in box 2.

Kenya   (box 1)
answer> Nairobi
Right.

Canada   (box 1)
answer> ottawa
Right.

Practice 1 done: 6 of 8 right the first time.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 3
deck> 1

-- Capitals: 8 cards, 1 practice done --
  box 1   2  ##
  box 2   6  ######
  box 3   0
  box 4   0
  box 5   0
  next practice (2): boxes 1 and 2 - 8 cards

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 1
Practice 2 of Capitals: boxes 1 and 2 - 8 cards.
Type the answer, or q to stop.

Kenya   (box 1)
answer> Nairobi
Right - now in box 2.

Canada   (box 1)
answer> Ottawa
Right - now in box 2.

France   (box 2)
answer> Paris
Right - now in box 3.

Japan   (box 2)
answer> Tokyo
Right - now in box 3.

Peru   (box 2)
answer> Lima
Right - now in box 3.

Egypt   (box 2)
answer> Cairo
Right - now in box 3.

Norway   (box 2)
answer> Bergen
No - it is Oslo. Back to box 1; it comes again at the end.

Chile   (box 2)
answer> Santiago
Right - now in box 3.

Norway   (box 1)
answer> Oslo
Right.

Practice 2 done: 7 of 8 right the first time.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 2
deck> 1
Practice 3 of Capitals: box 1 - 1 card.
Type the answer, or q to stop.

Norway   (box 1)
answer> Oslo
Right - now in box 2.

Practice 3 done: 1 of 1 right the first time.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 3
deck> 1

-- Capitals: 8 cards, 3 practices done --
  box 1   0
  box 2   3  ###
  box 3   5  #####
  box 4   0
  box 5   0
  next practice (4): boxes 1, 2 and 3 - 8 cards

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 4
deck> 2
front> book
back> libro
Added to Spanish: book = libro (box 1).

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 5
deck name> Science
Deck 3: Science.

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 4
deck> 3
front> H2O
back> water
Added to Science: H2O = water (box 1).

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 1

-- decks --
  1) Capitals             8 cards   3 practices done
  2) Spanish              9 cards   no practice yet
  3) Science              1 card    no practice yet

== Flashcards ==
1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P017 flashcards: practise decks of cards with Leitner boxes. A practice
# asks the cards that are due, in a queue; a card answered wrongly goes to
# the back of the queue and comes again until it is answered right, but only
# the first answer of a practice moves a card between boxes.
import leitner
import text

class Queue:
    # First in, first out.
    def __init__(self):
        [] => self.items

    def put(self, item):
        self.items + [item] => self.items

    def take(self):
        self.items[0] => front
        self.items[1:len(self.items)] => self.items
        return front

    def is_empty(self):
        return len(self.items) == 0

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def boxes_text(bs):
    # "box 1", "boxes 1 and 2", "boxes 1, 2 and 3".
    if len(bs) == 1:
        return "box " + str(bs[0])
    "boxes " => out
    for i in [0:len(bs) - 2]:
        if i > 0:
            out + ", " => out
        out + str(bs[i]) => out
    return out + " and " + str(bs[len(bs) - 1])

def practices_text(deck):
    if deck[2] == 0:
        return "no practice yet"
    return plural(deck[2], "practice") + " done"

def ask_deck(decks):
    # A deck, asked again until a good number is typed; None cancels.
    while True:
        text.trim(input("deck> ")) => answer
        if answer == "":
            return None
        text.number(answer) => n
        if n >= 1 and n <= len(decks):
            return decks[n - 1]
        ("Type a deck number from 1 to " + str(len(decks)) + ", or nothing to cancel.") ^0

def ask_text(prompt, longest):
    # Text of at most longest characters, asked again until it fits; "" cancels.
    while True:
        text.trim(input(prompt)) => answer
        if len(answer) <= longest:
            return answer
        ("At most " + str(longest) + " characters.") ^0

def show_decks(decks):
    "" ^0
    "-- decks --" ^0
    for i in [0:len(decks) - 1]:
        ("  " + str(i + 1) + ") " + ("%-21s" % decks[i][0]) + ("%-10s" % plural(len(decks[i][1]), "card")) + practices_text(decks[i])) ^0

def bar(n):
    # Two spaces and one # per card, or nothing for an empty box.
    if n == 0:
        return ""
    return "  " + ("#" * n)

def show_progress(deck):
    leitner.box_counts(deck) => counts
    deck[2] + 1 => coming
    "" ^0
    ("-- " + deck[0] + ": " + plural(len(deck[1]), "card") + ", " + practices_text(deck) + " --") ^0
    for b in [1:leitner.boxes]:
        ("  box " + str(b) + ("%4d" % counts[b - 1]) + bar(counts[b - 1])) ^0
    ("  next practice (" + str(coming) + "): " + boxes_text(leitner.due_boxes(coming)) + " - " + plural(len(leitner.due_cards(deck, coming)), "card")) ^0

def practice(deck):
    deck[2] + 1 => n
    leitner.due_cards(deck, n) => due
    if len(deck[1]) == 0:
        "No cards in this deck yet." ^0
        return
    ("Practice " + str(n) + " of " + deck[0] + ": " + boxes_text(leitner.due_boxes(n)) + " - " + plural(len(due), "card") + ".") ^0
    if len(due) == 0:
        "Nothing is due this time." ^0
        n => deck[2]
        return
    "Type the answer, or q to stop." ^0
    Queue() => queue
    for c in due:
        queue.put([c, True])
    0 => answered
    0 => right
    while not queue.is_empty():
        queue.take() => item
        item[0] => card
        "" ^0
        (card[0] + "   (box " + str(card[2]) + ")") ^0
        text.trim(input("answer> ")) => answer
        if answer == "q" or answer == "Q":
            if answered == 0:
                "Stopped before any answer; this practice does not count." ^0
                return
            n => deck[2]
            ("Stopped: " + str(right) + " of " + str(answered) + " right the first time; the other cards stay where they were.") ^0
            return
        if text.same(answer, card[1]):
            if item[1]:
                leitner.promote(card)
                answered + 1 => answered
                right + 1 => right
                ("Right - now in box " + str(card[2]) + ".") ^0
            else:
                "Right." ^0
        else:
            if item[1]:
                leitner.demote(card)
                answered + 1 => answered
                ("No - it is " + card[1] + ". Back to box 1; it comes again at the end.") ^0
            else:
                ("No - it is " + card[1] + ". It comes again at the end.") ^0
            queue.put([card, False])
    n => deck[2]
    "" ^0
    ("Practice " + str(n) + " done: " + str(right) + " of " + str(len(due)) + " right the first time.") ^0

[
    ["Capitals", [["France", "Paris", 1], ["Japan", "Tokyo", 1], ["Kenya", "Nairobi", 1], ["Peru", "Lima", 1],
                  ["Canada", "Ottawa", 1], ["Egypt", "Cairo", 1], ["Norway", "Oslo", 1], ["Chile", "Santiago", 1]], 0],
    ["Spanish", [["dog", "perro", 1], ["cat", "gato", 1], ["house", "casa", 1], ["water", "agua", 1],
                 ["red", "rojo", 1], ["bread", "pan", 1], ["friend", "amigo", 1], ["green", "verde", 1]], 0],
] => decks
True => running
while running:
    "" ^0
    "== Flashcards ==" ^0
    "1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        show_decks(decks)
    elif choice == "2":
        ask_deck(decks) => deck
        if deck == None:
            "Cancelled." ^0
        else:
            practice(deck)
    elif choice == "3":
        ask_deck(decks) => deck
        if deck == None:
            "Cancelled." ^0
        else:
            show_progress(deck)
    elif choice == "4":
        ask_deck(decks) => deck
        "" => front
        if deck != None:
            while True:
                ask_text("front> ", 40) => front
                None => clash
                for c in deck[1]:
                    if front != "" and text.same(c[0], front):
                        c => clash
                if clash == None:
                    break
                ("There is already a card " + clash[0] + " in " + deck[0] + ".") ^0
        "" => back
        if front != "":
            ask_text("back> ", 40) => back
        if back == "":
            "Cancelled." ^0
        else:
            deck[1] + [[front, back, 1]] => deck[1]
            ("Added to " + deck[0] + ": " + front + " = " + back + " (box 1).") ^0
    elif choice == "5":
        while True:
            ask_text("deck name> ", 20) => name
            None => clash
            for d in decks:
                if name != "" and text.same(d[0], name):
                    d => clash
            if clash == None:
                break
            ("There is already a deck called " + clash[0] + ".") ^0
        if name == "":
            "Cancelled." ^0
        else:
            decks + [[name, [], 0]] => decks
            ("Deck " + str(len(decks)) + ": " + name + ".") ^0
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import leitner
import text

class Queue:
    def __init__(self):
        self.items = []
    def put(self, item):
        self.items = self.items + [item]
    def take(self):
        front = self.items[0]
        self.items = self.items[1:len(self.items)]
        return front
    def is_empty(self):
        return len(self.items) == 0

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def boxes_text(bs):
    if len(bs) == 1:
        return "box " + str(bs[0])
    out = "boxes "
    for i in range(0, len(bs) - 2+1):
        if i > 0:
            out = out + ", "
        out = out + str(bs[i])
    return out + " and " + str(bs[len(bs) - 1])

def practices_text(deck):
    if deck[2] == 0:
        return "no practice yet"
    return plural(deck[2], "practice") + " done"

def ask_deck(decks):
    while True:
        answer = text.trim(input("deck> "))
        if answer == "":
            return None
        n = text.number(answer)
        if n >= 1 and n <= len(decks):
            return decks[n - 1]
        print("Type a deck number from 1 to " + str(len(decks)) + ", or nothing to cancel.")

def ask_text(prompt, longest):
    while True:
        answer = text.trim(input(prompt))
        if len(answer) <= longest:
            return answer
        print("At most " + str(longest) + " characters.")

def show_decks(decks):
    print("")
    print("-- decks --")
    for i in range(0, len(decks)):
        print("  " + str(i + 1) + ") " + "%-21s" % decks[i][0] + "%-10s" % plural(len(decks[i][1]), "card") + practices_text(decks[i]))

def bar(n):
    if n == 0:
        return ""
    return "  " + "#" * n

def show_progress(deck):
    counts = leitner.box_counts(deck)
    coming = deck[2] + 1
    print("")
    print("-- " + deck[0] + ": " + plural(len(deck[1]), "card") + ", " + practices_text(deck) + " --")
    for b in range(1, leitner.boxes+1):
        print("  box " + str(b) + "%4d" % counts[b - 1] + bar(counts[b - 1]))
    print("  next practice (" + str(coming) + "): " + boxes_text(leitner.due_boxes(coming)) + " - " + plural(len(leitner.due_cards(deck, coming)), "card"))

def practice(deck):
    n = deck[2] + 1
    due = leitner.due_cards(deck, n)
    if len(deck[1]) == 0:
        print("No cards in this deck yet.")
        return
    print("Practice " + str(n) + " of " + deck[0] + ": " + boxes_text(leitner.due_boxes(n)) + " - " + plural(len(due), "card") + ".")
    if len(due) == 0:
        print("Nothing is due this time.")
        deck[2] = n
        return
    print("Type the answer, or q to stop.")
    queue = Queue()
    for c in due:
        queue.put([c, True])
    answered = 0
    right = 0
    while not queue.is_empty():
        item = queue.take()
        card = item[0]
        print("")
        print(card[0] + "   (box " + str(card[2]) + ")")
        answer = text.trim(input("answer> "))
        if answer == "q" or answer == "Q":
            if answered == 0:
                print("Stopped before any answer; this practice does not count.")
                return
            deck[2] = n
            print("Stopped: " + str(right) + " of " + str(answered) + " right the first time; the other cards stay where they were.")
            return
        if text.same(answer, card[1]):
            if item[1]:
                leitner.promote(card)
                answered = answered + 1
                right = right + 1
                print("Right - now in box " + str(card[2]) + ".")
            else:
                print("Right.")
        else:
            if item[1]:
                leitner.demote(card)
                answered = answered + 1
                print("No - it is " + card[1] + ". Back to box 1; it comes again at the end.")
            else:
                print("No - it is " + card[1] + ". It comes again at the end.")
            queue.put([card, False])
    deck[2] = n
    print("")
    print("Practice " + str(n) + " done: " + str(right) + " of " + str(len(due)) + " right the first time.")

decks = [["Capitals", [["France", "Paris", 1], ["Japan", "Tokyo", 1], ["Kenya", "Nairobi", 1], ["Peru", "Lima", 1], ["Canada", "Ottawa", 1], ["Egypt", "Cairo", 1], ["Norway", "Oslo", 1], ["Chile", "Santiago", 1]], 0], ["Spanish", [["dog", "perro", 1], ["cat", "gato", 1], ["house", "casa", 1], ["water", "agua", 1], ["red", "rojo", 1], ["bread", "pan", 1], ["friend", "amigo", 1], ["green", "verde", 1]], 0]]
running = True
while running:
    print("")
    print("== Flashcards ==")
    print("1) decks  2) practice  3) progress  4) add card  5) new deck  6) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        show_decks(decks)
    elif choice == "2":
        deck = ask_deck(decks)
        if deck == None:
            print("Cancelled.")
        else:
            practice(deck)
    elif choice == "3":
        deck = ask_deck(decks)
        if deck == None:
            print("Cancelled.")
        else:
            show_progress(deck)
    elif choice == "4":
        deck = ask_deck(decks)
        front = ""
        if deck != None:
            while True:
                front = ask_text("front> ", 40)
                clash = None
                for c in deck[1]:
                    if front != "" and text.same(c[0], front):
                        clash = c
                if clash == None:
                    break
                print("There is already a card " + clash[0] + " in " + deck[0] + ".")
        back = ""
        if front != "":
            back = ask_text("back> ", 40)
        if back == "":
            print("Cancelled.")
        else:
            deck[1] = deck[1] + [[front, back, 1]]
            print("Added to " + deck[0] + ": " + front + " = " + back + " (box 1).")
    elif choice == "5":
        while True:
            name = ask_text("deck name> ", 20)
            clash = None
            for d in decks:
                if name != "" and text.same(d[0], name):
                    clash = d
            if clash == None:
                break
            print("There is already a deck called " + clash[0] + ".")
        if name == "":
            print("Cancelled.")
        else:
            decks = decks + [[name, [], 0]]
            print("Deck " + str(len(decks)) + ": " + name + ".")
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### leitner.eml

```eml
# P017 flashcards - the Leitner system. A deck is [name, cards, practices
# done] and a card is [front, back, box]. There are five boxes: new cards
# start in box 1, a card answered right the first time in a practice moves up
# one box (box 5 is the top), and a wrong one goes back to box 1. Box k is
# due in every practice whose number is a multiple of 2^(k-1): box 1 every
# time, box 2 every second time, box 3 every fourth, and so on.

5 => boxes

def every(box):
    # How often box is due: 2^(box - 1) practices.
    1 => n
    for i in [2:box]:
        n * 2 => n
    return n

def due_boxes(practice):
    # The boxes due in practice number practice, lowest first.
    [] => out
    for b in [1:boxes]:
        if practice % every(b) == 0:
            out + [b] => out
    return out

def due_cards(deck, practice):
    # The cards due in practice number practice: box by box, lowest first,
    # and in the deck's order inside a box.
    [] => out
    for b in due_boxes(practice):
        for c in deck[1]:
            if c[2] == b:
                out + [c] => out
    return out

def box_counts(deck):
    # How many cards are in each box, box 1 first.
    [] => out
    for b in [1:boxes]:
        0 => n
        for c in deck[1]:
            if c[2] == b:
                n + 1 => n
        out + [n] => out
    return out

def promote(card):
    # A first-time right answer: up one box, no higher than the top.
    if card[2] < boxes:
        card[2] + 1 => card[2]

def demote(card):
    # A first-time wrong answer: back to box 1.
    1 => card[2]
```

Python projection of leitner.eml:

```python
boxes = 5

def every(box):
    n = 1
    for i in range(2, box+1):
        n = n * 2
    return n

def due_boxes(practice):
    out = []
    for b in range(1, boxes+1):
        if practice % every(b) == 0:
            out = out + [b]
    return out

def due_cards(deck, practice):
    out = []
    for b in due_boxes(practice):
        for c in deck[1]:
            if c[2] == b:
                out = out + [c]
    return out

def box_counts(deck):
    out = []
    for b in range(1, boxes+1):
        n = 0
        for c in deck[1]:
            if c[2] == b:
                n = n + 1
        out = out + [n]
    return out

def promote(card):
    if card[2] < boxes:
        card[2] = card[2] + 1

def demote(card):
    card[2] = 1
```

### text.eml

```eml
# P017 flashcards - reading what is typed. The interpreter that checks every
# session does not run string methods yet, so trimming and lower case are
# written out here.

def trim(s):
    # s without the spaces at either end.
    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 lower(s):
    # s with A-Z turned into a-z; everything else as it was.
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if upper_letters[i] == c:
                lower_letters[i] => d
        out + d => out
    return out

def number(s):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return 0 - 1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return 0 - 1
        n * 10 + int(c) => n
    return n

def same(a, b):
    # a and b equal once trimmed and in lower case.
    return lower(trim(a)) == lower(trim(b))
```

Python projection of text.eml:

```python
def trim(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 lower(s):
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if upper_letters[i] == c:
                d = lower_letters[i]
        out = out + d
    return out

def number(s):
    if s == "":
        return 0 - 1
    n = 0
    for c in s:
        if not c in "0123456789":
            return 0 - 1
        n = n * 10 + int(c)
    return n

def same(a, b):
    return lower(trim(a)) == lower(trim(b))
```

## README

# P017 - Flashcards

Practise decks of flashcards the Leitner way. Two decks come with the
program - eight capitals and eight Spanish words - and you can add decks and
cards. A practice asks the cards that are due; a progress chart shows how
many cards sit in each box and what the next practice will ask. Everything
lives while the program runs.

- `main.eml` - the menu and its questions, the practice itself (a queue of
  cards), and what the screen shows
- `leitner.eml` - the boxes: which are due in which practice, the cards due,
  the count in each box, moving a card up or back
- `text.eml` - trimming, lower case, whole numbers, and comparing answers

The Leitner rules: there are five boxes and new cards start in box 1. A card
answered right the first time in a practice moves up one box (box 5 is the
top); a card answered wrong goes back to box 1. Box k is due in every
practice whose number is a multiple of 2^(k-1) - box 1 every time, box 2
every second time, box 3 every fourth, box 4 every eighth, box 5 every
sixteenth - so a card that keeps being right is asked less and less often.

A practice puts the due cards in a queue, lowest box first and in the deck's
order inside a box. A card answered wrongly goes to the back of the queue
and comes again until it is answered right, but only the first answer of a
practice moves a card between boxes. Answers are compared without regard to
case or the spaces around them. Typing q stops a practice: the cards not yet
answered stay where they were, and a practice stopped before any answer does
not count.

What is checked: a deck number is one that exists; a deck name has at most
20 characters and is not already taken in any case; a card's front and back
have at most 40 characters each and the front is not already in the deck.
Anything else asks again; nothing cancels.

Sessions: `sessions/basic.in` lists the decks, practises the capitals three
times - two wrong answers in the first practice come back at the end; in the
second, boxes 1 and 2 are due and one card falls back to box 1; the third
asks box 1 alone - with the progress chart after the first and the third, then
adds a card to the Spanish deck, makes a new deck and adds a card to it;
`sessions/bad-input.in` types deck numbers that do not exist, deck names that
are empty, too long or taken, practises an empty deck, adds cards with an
empty, repeated or too long front and an empty back, stops one practice after
one answer and another before any.

Built on the verified corpus case `simple-queue` (a first-in, first-out queue
in a class: put at the back, take from the front).
