Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis cellularmitosis/README.md Secret
Last active Oct 14, 2019

Embed
What would you like to do?

Blog 2019/7/24

srs.py: a work-in-progress command-line spaced-repetition learning system

#!/usr/bin/env python
"""srs.py: a simple command-line spaced-repetition learning system."""
# see also https://github.com/pepasflo/whodis
# Example deck file (~/srs/jokes.md):
'''
# Jokes
A deck of jokes
# q
Why did the chicken cross the road?
---
To get to the other side!
# q
What do you call a cow with no legs?
---
Ground beef!
'''
# A vocabulary deck:
'''
# Spanish
A deck of Spanish vocabulary.
# q
Biblioteca
---
Library
# q
Ventana
---
Window
'''
# A more complex example:
'''
# Misc
A deck of random things.
# q
Who said
> We have nothing to fear but fear itself
---
FDR
# q
What might a linked-list node look like in Python?
---
```
class ListNode:
value = None
next = None
```
# q
Who painted this?
[https://upload.wikimedia.org/wikipedia/en/4/4a/No._5%2C_1948.jpg]()
---
Jackson Pollock
'''
# A sqlite database is used to track statistics about questions / answers.
# Each question will be assigned a uuid the first time it is encountered,
# (the markdown file will be edited to insert the UUID)
# and will be referred to in the database by that key.
# (The deck will also be assigned a UUID).
# i.e. after running srs.py, the above deck file might look like:
'''
# Jokes 9DF8162E-9767-4363-BF24-5BED99D8DC39
...
## Q 7302CD07-B3ED-41CB-8327-C52E29C5FC9D
What do you call a cow with no legs?
## A
Ground beef!
'''
# Algorithm for choosing the next card to show the user:
# We us something similar to the Leitner algorithm.
# For each card, keep track of a score and a last-seen timestamp.
# A score of 0 means (and a NULL timestamp) "this card has never been seen".
# A correct answer increments the score by 1.
# An incorrect answer decrements the score by 1.
# The score "truncates" when switching sign.
# That is, an incorrect answer causes any positive score to "jump" to -1
# and vice-versa (a correct answer jumps any negative score to +1).
# An answer sequence of "(never seen), correct, correct, correct, incorrect"
# would result in a score history of "0, 1, 2, 3, -1".
# The last-seen timestamp also affects card selection.
# - Cards which haven't been seen during this session are prioritized
# - Among these cards, the most negative score are prioritized
# - Then, unseen cards are prioritized
# - Finally, cards with a positive score are prioritized
# - After all cards which haven't been seen during this session are exhausted,
# the user will be shown cards which they have already seen today,
# - starting with the most negative score
# - in order of least-recently seen (among cards with the same score)
# - Additionally, the same card is never shown twice in a row, no matter what.
import sys
import os
import sqlite3
import time
import uuid
import re
dbpath = os.path.join(os.path.expanduser("~"), ".srs", "db.sqlite3")
def get_db():
"""Return a connection to the SRS stats database (creating it if needed)."""
if os.path.exists(dbpath):
return sqlite3.connect(dbpath)
else:
sql = '''
CREATE TABLE IF NOT EXISTS srs_stats_v1 (
card_id TEXT NOT NULL UNIQUE PRIMARY KEY,
deck_id TEXT,
score INTEGER NOT NULL, -- +1 per correct, -1 per incorrect answer
last_seen INTEGER -- unix timestamp, null means "never seen"
);
'''
conn = sqlite3.connect(dbpath)
cur = conn.cursor()
cur.execute(sql)
cur.close()
return conn
def next_card(conn, session_start, deck_id=None):
"""Determine which card would be best to show the user next."""
if deck_id is not None:
sql = '''
-- Get the cards (from deck_id) which have'nt been seen in this session, lowest score first.
SELECT card_id, score
FROM srs_stats_v1
WHERE deck_id = :deck_id
AND last_seen < :session_start
ORDER BY score ASC, last_seen ASC, card_id ASC
LIMIT 1;
'''
else:
sql = '''
-- Get the cards which have'nt been seen in this session, lowest score first.
SELECT card_id, score
FROM srs_stats_v1
WHERE last_seen < :session_start
ORDER BY score ASC, last_seen ASC, card_id ASC
LIMIT 1;
'''
cur = conn.cursor()
result = cur.execute(sql)
return result
def find_decks():
"""Return a list of paths to all deck files"""
deckspath = os.path.join(os.path.expanduser("~"), "srs")
deckfiles = [f for f in os.listdir(deckspath) if f.endswith(".md")]
return [os.path.join(deckspath, d) for d in deckfiles]
uuid_regex = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')
def line_contains_uuid(line):
return uuid_regex.search(line) != None
def ensure_deck_uuid(lines):
"""If the deck doesn't have a deck UUID, add one."""
for i, line in enumerate(lines):
if line.startswith("# "):
if not line_contains_uuid(line):
lines[i] += " %s" % uuid.uuid1()
break
def ensure_card_uuids(lines):
"""If any card in the deck doesn't have a UUID, add one."""
for i, line in enumerate(lines):
if line.startswith("## Q") or line.startswith("# q"):
if not line_contains_uuid(line):
lines[i] += " %s" % uuid.uuid1()
break
def parse_deck(deckfpath):
with open(deckfpath) as fd:
lines = fd.readlines()
def make_uuid():
return uuid.uuid1().hex
if __name__ == "__main__":
session_start = int(time.time())
print find_decks()
sys.exit(0)
if len(sys.argv) > 1:
decks = sys.argv[1:]
else:
pass
# TODO:
# - open all decks and scan for items without UUIDs
# - generate UUIDs and edit the decks
# - start the loop of asking the user questions
# - update the DB for each answer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.