Blog 2019/7/24
-
-
Save cellularmitosis/bab425e7dd99046e79ca7d8e32f830dd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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