Python CLI program to learn words using Wordnik API and SuperMemo2 algorithm

Multi tool use
Multi tool use

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
2
down vote

favorite
1












Motivation



I wanted to write a simple console application to improve my vocabulary by using the Wordnik API to get words and definitions and using the SuperMemo2 spaced-repetition algorithm to schedule review sessions.



Features



  • Correctly implement the SM2 (SuperMemo2) spaced-repetition algorithm to schedule practice


  • Automatically compute the "easiness factor" for the SM2 algorithm based on the amount of time taken to answer each question


  • Automatically add new words using the Wordnik API


Implementation



Here is a small Python module that I wrote which prompts the user with a definition and asks for them to supply the correct word.



NOTE: This requires a (free) Wordnik API key in order to run.



#!/usr/bin/env python3
# -*- encoding: utf8 -*-
"""learnwords.py

Learn words from the Wordnik API using
the SuperMemo2 spaced-repetition algorithm.
"""

from datetime import datetime
from datetime import date
from datetime import timedelta
import json
import os
import requests
import sys
import time

# Define module-level variable for Wordnik API base URL
WORDNIK_API_BASEURL = "https://api.wordnik.com/v4"

def get_updated_sm2(
repetition_iteration,
easiness_factor,
repetition_interval,
response_quality,
):
"""SuperMemo2 update function"""

# Rename the parameters for convenience
n = repetition_iteration
EF = easiness_factor
q = response_quality

# Update the iteration number
# If the response quality was too low, restart the
# repetitions without changing the easiness factor
if q<3:
new_repetition_iteration = 0
else:
new_repetition_iteration = n + 1

# Compute the new easiness factor
new_easiness_factor = max(
EF + (0.1-(5-q)*(0.08+(5-q)*0.02)),
1.3
)

# Compute the new inter-repetition interval
if new_repetition_iteration == 1:
new_repetition_interval = 1
elif new_repetition_iteration == 2:
new_repetition_interval = 6
else:
new_repetition_interval =
int(new_repetition_iteration * new_easiness_factor)

# Return the updated SM2 values
return (
new_repetition_iteration,
new_easiness_factor,
new_repetition_interval,
)

def get_random_word(api_key):
"""Returns a random word as a string."""

# Construct the API URL for a random-word query
api_url = "baseurl/words.json/randomWord".format(
baseurl=WORDNIK_API_BASEURL
)

# Set the parameters for the query to
# limit the list of results to a single word
parameters =
'hasDictionaryDef': 'true',
'includePartOfSpeech': 'noun',
'minCorpusCount': 0,
'maxCorpusCount': -1,
'minDictionaryCount': 1,
'maxDictionaryCount': -1,
'minLength': 5,
'maxLength': 15,
'api_key': api_key,


# Perform the query and store the HTTP response object
response = requests.get(api_url, params=parameters)

# Convert the response content to a list
# NOTE: The content is initially returned as a byte string
word_object = json.loads(response.content)

# Get the word itself
word = word_object.get("canonicalForm", word_object["word"])

# Return the word as a string
return word

def get_definition(api_key, word):
"""Returns the definition of a word."""

# Construct the API URL for a word-definition query
api_url = "baseurl/word.json/word/definitions".format(
baseurl=WORDNIK_API_BASEURL,
word=word,
)

# Set the parameters for the query to limit the
# list of results to a single definition
parameters =
'limit': 1,
'partOfSpeech': 'noun',
'includeRelated': False,
'sourceDictionaries': 'webster',
'useCanonical': True,
'includeTags': False,
'api_key': api_key,


# Perform the query and store the HTTP response object
response = requests.get(api_url, params=parameters)

# Convert the response content to a list
# NOTE: The content is initially returned as a byte string
definition_list = json.loads(response.content)

# Return the first definition as a string, else return None
if definition_list:
return definition_list[0]["text"]

def teach_new_word(word_dict):

# Prompt the user with the new word
# Repeat until they enter it correctly
answer = None
while answer != word_dict["word"]:

# Clear the screen
os.system('cls' if os.name == 'nt' else 'clear')

# Prompt the user and get their response
answer = input(
"New word: nnDefinition:ntnnRepeat the word: ".format(
word_dict["word"],
word_dict["definition"],
)
).strip()

# Tell the user whether or not they entered the answer correctly
if answer == word_dict["word"]:
print("nCorrect!")
else:
print("nIncorrect!")

# Pause for a second to give the user time to read
time.sleep(1)

def review_word(word_dict, deck, update):

# Clear the screen
os.system('cls' if os.name == 'nt' else 'clear')

# Prompt with the definition and time the response
start = time.time()
answer = input(
"nDefinition: nnWord: ".format(word_dict["definition"])
).strip()
stop = time.time()
duration = stop - start

# Check to see if the answer is correct,
# compute a reponse quality,
# and display the correct answer
if answer == word_dict["word"]:

# Compute the response quality from the duration
# NOTE: This was totally arbitrary on my part.
if duration < 5:
response_quality = 5
elif duration < 10:
response_quality = 4
elif duration < 15:
response_quality = 3
elif duration < 20:
response_quality = 2
elif duration < 25:
response_quality = 1
else:
response_quality = 0

# Display feedback for the user
print("nCorrect!nnTime taken: :6.2f secondsnnResponse quality: ".format(
duration, response_quality)
)

else:

# If the question was answered incorrectly
# then the response quality is zero
response_quality = 0

# Display feedback for the user
print(
(
"nIncorrect!"
"nnAnswer: "
"nnTime taken: :6.2f seconds"
"nnResponse quality: "
).format(
word_dict["word"], duration, response_quality
)
)

# We update after the first attempt, but not on subsequent attempts
if update:

# Compute the new SuperMemo2 parameter values
repetition_iteration, easiness_factor, repetition_interval =
get_updated_sm2(
repetition_iteration=word_dict["repetition_iteration"],
easiness_factor=word_dict["easiness_factor"],
repetition_interval=word_dict["repetition_interval"],
response_quality=response_quality,
)

# Set the date for the next practice for this word
next_practice =
date.today() +
timedelta(days=repetition_interval)

# Update the SuperMemo2 parameter values in the deck
deck[word_dict["word"]].update(
"repetition_iteration": repetition_iteration,
"easiness_factor": easiness_factor,
"repetition_interval": repetition_interval,
"next_practice": next_practice
)

# Pause for a second to give the user time to read
time.sleep(1)

# Return the response quality (determines whether or not to review again)
return response_quality

def learn_words(api_key, deck):

# Store the current time (to determine which words to review)
current_date = date.today()

# Get all the words that need to be reviewed
words_to_review = [
v for k, v in deck.items()
if datetime.strptime(v["next_practice"], "%Y-%m-%d").date()
<= current_date
]

# If no words are due, then get a new word
if not words_to_review:

# Get a random word and make sure it has a definition
# NOTE: Why isn't this always the case? I'm setting the
# "hasDictionaryDef" parameter to 'true'.
definition = None
while not definition:

# Get a random word
word = get_random_word(api_key)

# Get the definition of the word
definition = get_definition(api_key, word)

# Construct the dictionary representation for the word
word_dict =
"word": word,
"definition": definition,
"easiness_factor": 0,
"repetition_interval": 0,
"repetition_iteration": 0,
"last_practiced": None,
"next_practice": None,


# Teach the user the new word
teach_new_word(word_dict)

# Update the practice times for this word
word_dict.update(
"last_practiced": date.today(),
"next_practice": date.today(),
)

# Add the word object to the deck
deck[word] = word_dict

# Add the word to the list of words to review
words_to_review.append(word_dict)

# Enter the main review loop
while words_to_review:

# Get the next word
word_dict = words_to_review.pop()

# Review the next word
response_quality = review_word(
word_dict=word_dict,
deck=deck,
update=True,
)

# If the response quality is too low, review again but don't update
while response_quality < 4:
response_quality = review_word(
word_dict=word_dict,
deck=deck,
update=False,
)

def run():

# Get the Wordnik API key from a config file
with open(os.path.expanduser('~/.learnwords/wordnik.conf'), 'r') as config_file:

# Load the configuration parameters from a JSON file
config_json = json.load(config_file)

# Get the Wordnik API key
api_key = config_json["api_key"]

# Load the deck from a file if it exists or initialize it otherwise
try:

# Load the deck (local library of stored words)
with open(os.path.expanduser('~/.learnwords/deck.json'), 'r') as deck_file:

# Convert the JSON data to a Python dictionary
deck = json.load(deck_file)

except FileNotFoundError as e:

# Create an empty deck file
with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
deck_file.write(r'')
deck =

# Learn the words in the deck and update the deck file (allow the user to interrupt)

# Learn the words in the deck
try:
learn_words(api_key, deck)

# Allow the user to interrupt
except KeyboardInterrupt as e:
print("nnExiting program...")
sys.exit(0)

# Update the deck file
finally:

# Update the local deck file
with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
json.dump(deck, deck_file, indent=4, sort_keys=True, default=str)

# Print an exit message
print("nAll done for today!")

# Run the application
if __name__ == "__main__":
run()


Usage



The Wordnik API key is stored in a JSON-format configuration file (~/.learnwords/wordnik.conf), e.g.




"__comment__": "Wordnik API Key",
"api_key": "idah8AeV1Shai7oosah9ayefahy1yiequa2yi0uyo2eeshaej"



After running the program for the first time (e.g. python learnwords.py, a local file (~/.learnwords/deck.json) should be created and a new random word should be added from the Wordnik online dictionary. The user is shown the definition of the new word and prompted to enter the word:



New word: vidding

Definition:
Command; order; a proclamation or notifying.

Repeat the word:


After entering the word correctly, the user is again prompted with the definition and asked for the word:



Definition:
Command; order; a proclamation or notifying.

Word:


After answering incorrectly (or too slowly), the user is given feedback and asked the question again:



Definition: Command; order; a proclamation or notifying.

Word: not vidding

Incorrect!

Answer: vidding

Time taken: 11.37 seconds

Response quality: 0


After answering correctly, the user is asked another word or the program exits if there are no more words to review:



Definition:
Command; order; a proclamation or notifying.

Word: vidding

Correct!

Time taken: 7.11 seconds

Response quality: 4

All done for today!






share|improve this question



























    up vote
    2
    down vote

    favorite
    1












    Motivation



    I wanted to write a simple console application to improve my vocabulary by using the Wordnik API to get words and definitions and using the SuperMemo2 spaced-repetition algorithm to schedule review sessions.



    Features



    • Correctly implement the SM2 (SuperMemo2) spaced-repetition algorithm to schedule practice


    • Automatically compute the "easiness factor" for the SM2 algorithm based on the amount of time taken to answer each question


    • Automatically add new words using the Wordnik API


    Implementation



    Here is a small Python module that I wrote which prompts the user with a definition and asks for them to supply the correct word.



    NOTE: This requires a (free) Wordnik API key in order to run.



    #!/usr/bin/env python3
    # -*- encoding: utf8 -*-
    """learnwords.py

    Learn words from the Wordnik API using
    the SuperMemo2 spaced-repetition algorithm.
    """

    from datetime import datetime
    from datetime import date
    from datetime import timedelta
    import json
    import os
    import requests
    import sys
    import time

    # Define module-level variable for Wordnik API base URL
    WORDNIK_API_BASEURL = "https://api.wordnik.com/v4"

    def get_updated_sm2(
    repetition_iteration,
    easiness_factor,
    repetition_interval,
    response_quality,
    ):
    """SuperMemo2 update function"""

    # Rename the parameters for convenience
    n = repetition_iteration
    EF = easiness_factor
    q = response_quality

    # Update the iteration number
    # If the response quality was too low, restart the
    # repetitions without changing the easiness factor
    if q<3:
    new_repetition_iteration = 0
    else:
    new_repetition_iteration = n + 1

    # Compute the new easiness factor
    new_easiness_factor = max(
    EF + (0.1-(5-q)*(0.08+(5-q)*0.02)),
    1.3
    )

    # Compute the new inter-repetition interval
    if new_repetition_iteration == 1:
    new_repetition_interval = 1
    elif new_repetition_iteration == 2:
    new_repetition_interval = 6
    else:
    new_repetition_interval =
    int(new_repetition_iteration * new_easiness_factor)

    # Return the updated SM2 values
    return (
    new_repetition_iteration,
    new_easiness_factor,
    new_repetition_interval,
    )

    def get_random_word(api_key):
    """Returns a random word as a string."""

    # Construct the API URL for a random-word query
    api_url = "baseurl/words.json/randomWord".format(
    baseurl=WORDNIK_API_BASEURL
    )

    # Set the parameters for the query to
    # limit the list of results to a single word
    parameters =
    'hasDictionaryDef': 'true',
    'includePartOfSpeech': 'noun',
    'minCorpusCount': 0,
    'maxCorpusCount': -1,
    'minDictionaryCount': 1,
    'maxDictionaryCount': -1,
    'minLength': 5,
    'maxLength': 15,
    'api_key': api_key,


    # Perform the query and store the HTTP response object
    response = requests.get(api_url, params=parameters)

    # Convert the response content to a list
    # NOTE: The content is initially returned as a byte string
    word_object = json.loads(response.content)

    # Get the word itself
    word = word_object.get("canonicalForm", word_object["word"])

    # Return the word as a string
    return word

    def get_definition(api_key, word):
    """Returns the definition of a word."""

    # Construct the API URL for a word-definition query
    api_url = "baseurl/word.json/word/definitions".format(
    baseurl=WORDNIK_API_BASEURL,
    word=word,
    )

    # Set the parameters for the query to limit the
    # list of results to a single definition
    parameters =
    'limit': 1,
    'partOfSpeech': 'noun',
    'includeRelated': False,
    'sourceDictionaries': 'webster',
    'useCanonical': True,
    'includeTags': False,
    'api_key': api_key,


    # Perform the query and store the HTTP response object
    response = requests.get(api_url, params=parameters)

    # Convert the response content to a list
    # NOTE: The content is initially returned as a byte string
    definition_list = json.loads(response.content)

    # Return the first definition as a string, else return None
    if definition_list:
    return definition_list[0]["text"]

    def teach_new_word(word_dict):

    # Prompt the user with the new word
    # Repeat until they enter it correctly
    answer = None
    while answer != word_dict["word"]:

    # Clear the screen
    os.system('cls' if os.name == 'nt' else 'clear')

    # Prompt the user and get their response
    answer = input(
    "New word: nnDefinition:ntnnRepeat the word: ".format(
    word_dict["word"],
    word_dict["definition"],
    )
    ).strip()

    # Tell the user whether or not they entered the answer correctly
    if answer == word_dict["word"]:
    print("nCorrect!")
    else:
    print("nIncorrect!")

    # Pause for a second to give the user time to read
    time.sleep(1)

    def review_word(word_dict, deck, update):

    # Clear the screen
    os.system('cls' if os.name == 'nt' else 'clear')

    # Prompt with the definition and time the response
    start = time.time()
    answer = input(
    "nDefinition: nnWord: ".format(word_dict["definition"])
    ).strip()
    stop = time.time()
    duration = stop - start

    # Check to see if the answer is correct,
    # compute a reponse quality,
    # and display the correct answer
    if answer == word_dict["word"]:

    # Compute the response quality from the duration
    # NOTE: This was totally arbitrary on my part.
    if duration < 5:
    response_quality = 5
    elif duration < 10:
    response_quality = 4
    elif duration < 15:
    response_quality = 3
    elif duration < 20:
    response_quality = 2
    elif duration < 25:
    response_quality = 1
    else:
    response_quality = 0

    # Display feedback for the user
    print("nCorrect!nnTime taken: :6.2f secondsnnResponse quality: ".format(
    duration, response_quality)
    )

    else:

    # If the question was answered incorrectly
    # then the response quality is zero
    response_quality = 0

    # Display feedback for the user
    print(
    (
    "nIncorrect!"
    "nnAnswer: "
    "nnTime taken: :6.2f seconds"
    "nnResponse quality: "
    ).format(
    word_dict["word"], duration, response_quality
    )
    )

    # We update after the first attempt, but not on subsequent attempts
    if update:

    # Compute the new SuperMemo2 parameter values
    repetition_iteration, easiness_factor, repetition_interval =
    get_updated_sm2(
    repetition_iteration=word_dict["repetition_iteration"],
    easiness_factor=word_dict["easiness_factor"],
    repetition_interval=word_dict["repetition_interval"],
    response_quality=response_quality,
    )

    # Set the date for the next practice for this word
    next_practice =
    date.today() +
    timedelta(days=repetition_interval)

    # Update the SuperMemo2 parameter values in the deck
    deck[word_dict["word"]].update(
    "repetition_iteration": repetition_iteration,
    "easiness_factor": easiness_factor,
    "repetition_interval": repetition_interval,
    "next_practice": next_practice
    )

    # Pause for a second to give the user time to read
    time.sleep(1)

    # Return the response quality (determines whether or not to review again)
    return response_quality

    def learn_words(api_key, deck):

    # Store the current time (to determine which words to review)
    current_date = date.today()

    # Get all the words that need to be reviewed
    words_to_review = [
    v for k, v in deck.items()
    if datetime.strptime(v["next_practice"], "%Y-%m-%d").date()
    <= current_date
    ]

    # If no words are due, then get a new word
    if not words_to_review:

    # Get a random word and make sure it has a definition
    # NOTE: Why isn't this always the case? I'm setting the
    # "hasDictionaryDef" parameter to 'true'.
    definition = None
    while not definition:

    # Get a random word
    word = get_random_word(api_key)

    # Get the definition of the word
    definition = get_definition(api_key, word)

    # Construct the dictionary representation for the word
    word_dict =
    "word": word,
    "definition": definition,
    "easiness_factor": 0,
    "repetition_interval": 0,
    "repetition_iteration": 0,
    "last_practiced": None,
    "next_practice": None,


    # Teach the user the new word
    teach_new_word(word_dict)

    # Update the practice times for this word
    word_dict.update(
    "last_practiced": date.today(),
    "next_practice": date.today(),
    )

    # Add the word object to the deck
    deck[word] = word_dict

    # Add the word to the list of words to review
    words_to_review.append(word_dict)

    # Enter the main review loop
    while words_to_review:

    # Get the next word
    word_dict = words_to_review.pop()

    # Review the next word
    response_quality = review_word(
    word_dict=word_dict,
    deck=deck,
    update=True,
    )

    # If the response quality is too low, review again but don't update
    while response_quality < 4:
    response_quality = review_word(
    word_dict=word_dict,
    deck=deck,
    update=False,
    )

    def run():

    # Get the Wordnik API key from a config file
    with open(os.path.expanduser('~/.learnwords/wordnik.conf'), 'r') as config_file:

    # Load the configuration parameters from a JSON file
    config_json = json.load(config_file)

    # Get the Wordnik API key
    api_key = config_json["api_key"]

    # Load the deck from a file if it exists or initialize it otherwise
    try:

    # Load the deck (local library of stored words)
    with open(os.path.expanduser('~/.learnwords/deck.json'), 'r') as deck_file:

    # Convert the JSON data to a Python dictionary
    deck = json.load(deck_file)

    except FileNotFoundError as e:

    # Create an empty deck file
    with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
    deck_file.write(r'')
    deck =

    # Learn the words in the deck and update the deck file (allow the user to interrupt)

    # Learn the words in the deck
    try:
    learn_words(api_key, deck)

    # Allow the user to interrupt
    except KeyboardInterrupt as e:
    print("nnExiting program...")
    sys.exit(0)

    # Update the deck file
    finally:

    # Update the local deck file
    with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
    json.dump(deck, deck_file, indent=4, sort_keys=True, default=str)

    # Print an exit message
    print("nAll done for today!")

    # Run the application
    if __name__ == "__main__":
    run()


    Usage



    The Wordnik API key is stored in a JSON-format configuration file (~/.learnwords/wordnik.conf), e.g.




    "__comment__": "Wordnik API Key",
    "api_key": "idah8AeV1Shai7oosah9ayefahy1yiequa2yi0uyo2eeshaej"



    After running the program for the first time (e.g. python learnwords.py, a local file (~/.learnwords/deck.json) should be created and a new random word should be added from the Wordnik online dictionary. The user is shown the definition of the new word and prompted to enter the word:



    New word: vidding

    Definition:
    Command; order; a proclamation or notifying.

    Repeat the word:


    After entering the word correctly, the user is again prompted with the definition and asked for the word:



    Definition:
    Command; order; a proclamation or notifying.

    Word:


    After answering incorrectly (or too slowly), the user is given feedback and asked the question again:



    Definition: Command; order; a proclamation or notifying.

    Word: not vidding

    Incorrect!

    Answer: vidding

    Time taken: 11.37 seconds

    Response quality: 0


    After answering correctly, the user is asked another word or the program exits if there are no more words to review:



    Definition:
    Command; order; a proclamation or notifying.

    Word: vidding

    Correct!

    Time taken: 7.11 seconds

    Response quality: 4

    All done for today!






    share|improve this question























      up vote
      2
      down vote

      favorite
      1









      up vote
      2
      down vote

      favorite
      1






      1





      Motivation



      I wanted to write a simple console application to improve my vocabulary by using the Wordnik API to get words and definitions and using the SuperMemo2 spaced-repetition algorithm to schedule review sessions.



      Features



      • Correctly implement the SM2 (SuperMemo2) spaced-repetition algorithm to schedule practice


      • Automatically compute the "easiness factor" for the SM2 algorithm based on the amount of time taken to answer each question


      • Automatically add new words using the Wordnik API


      Implementation



      Here is a small Python module that I wrote which prompts the user with a definition and asks for them to supply the correct word.



      NOTE: This requires a (free) Wordnik API key in order to run.



      #!/usr/bin/env python3
      # -*- encoding: utf8 -*-
      """learnwords.py

      Learn words from the Wordnik API using
      the SuperMemo2 spaced-repetition algorithm.
      """

      from datetime import datetime
      from datetime import date
      from datetime import timedelta
      import json
      import os
      import requests
      import sys
      import time

      # Define module-level variable for Wordnik API base URL
      WORDNIK_API_BASEURL = "https://api.wordnik.com/v4"

      def get_updated_sm2(
      repetition_iteration,
      easiness_factor,
      repetition_interval,
      response_quality,
      ):
      """SuperMemo2 update function"""

      # Rename the parameters for convenience
      n = repetition_iteration
      EF = easiness_factor
      q = response_quality

      # Update the iteration number
      # If the response quality was too low, restart the
      # repetitions without changing the easiness factor
      if q<3:
      new_repetition_iteration = 0
      else:
      new_repetition_iteration = n + 1

      # Compute the new easiness factor
      new_easiness_factor = max(
      EF + (0.1-(5-q)*(0.08+(5-q)*0.02)),
      1.3
      )

      # Compute the new inter-repetition interval
      if new_repetition_iteration == 1:
      new_repetition_interval = 1
      elif new_repetition_iteration == 2:
      new_repetition_interval = 6
      else:
      new_repetition_interval =
      int(new_repetition_iteration * new_easiness_factor)

      # Return the updated SM2 values
      return (
      new_repetition_iteration,
      new_easiness_factor,
      new_repetition_interval,
      )

      def get_random_word(api_key):
      """Returns a random word as a string."""

      # Construct the API URL for a random-word query
      api_url = "baseurl/words.json/randomWord".format(
      baseurl=WORDNIK_API_BASEURL
      )

      # Set the parameters for the query to
      # limit the list of results to a single word
      parameters =
      'hasDictionaryDef': 'true',
      'includePartOfSpeech': 'noun',
      'minCorpusCount': 0,
      'maxCorpusCount': -1,
      'minDictionaryCount': 1,
      'maxDictionaryCount': -1,
      'minLength': 5,
      'maxLength': 15,
      'api_key': api_key,


      # Perform the query and store the HTTP response object
      response = requests.get(api_url, params=parameters)

      # Convert the response content to a list
      # NOTE: The content is initially returned as a byte string
      word_object = json.loads(response.content)

      # Get the word itself
      word = word_object.get("canonicalForm", word_object["word"])

      # Return the word as a string
      return word

      def get_definition(api_key, word):
      """Returns the definition of a word."""

      # Construct the API URL for a word-definition query
      api_url = "baseurl/word.json/word/definitions".format(
      baseurl=WORDNIK_API_BASEURL,
      word=word,
      )

      # Set the parameters for the query to limit the
      # list of results to a single definition
      parameters =
      'limit': 1,
      'partOfSpeech': 'noun',
      'includeRelated': False,
      'sourceDictionaries': 'webster',
      'useCanonical': True,
      'includeTags': False,
      'api_key': api_key,


      # Perform the query and store the HTTP response object
      response = requests.get(api_url, params=parameters)

      # Convert the response content to a list
      # NOTE: The content is initially returned as a byte string
      definition_list = json.loads(response.content)

      # Return the first definition as a string, else return None
      if definition_list:
      return definition_list[0]["text"]

      def teach_new_word(word_dict):

      # Prompt the user with the new word
      # Repeat until they enter it correctly
      answer = None
      while answer != word_dict["word"]:

      # Clear the screen
      os.system('cls' if os.name == 'nt' else 'clear')

      # Prompt the user and get their response
      answer = input(
      "New word: nnDefinition:ntnnRepeat the word: ".format(
      word_dict["word"],
      word_dict["definition"],
      )
      ).strip()

      # Tell the user whether or not they entered the answer correctly
      if answer == word_dict["word"]:
      print("nCorrect!")
      else:
      print("nIncorrect!")

      # Pause for a second to give the user time to read
      time.sleep(1)

      def review_word(word_dict, deck, update):

      # Clear the screen
      os.system('cls' if os.name == 'nt' else 'clear')

      # Prompt with the definition and time the response
      start = time.time()
      answer = input(
      "nDefinition: nnWord: ".format(word_dict["definition"])
      ).strip()
      stop = time.time()
      duration = stop - start

      # Check to see if the answer is correct,
      # compute a reponse quality,
      # and display the correct answer
      if answer == word_dict["word"]:

      # Compute the response quality from the duration
      # NOTE: This was totally arbitrary on my part.
      if duration < 5:
      response_quality = 5
      elif duration < 10:
      response_quality = 4
      elif duration < 15:
      response_quality = 3
      elif duration < 20:
      response_quality = 2
      elif duration < 25:
      response_quality = 1
      else:
      response_quality = 0

      # Display feedback for the user
      print("nCorrect!nnTime taken: :6.2f secondsnnResponse quality: ".format(
      duration, response_quality)
      )

      else:

      # If the question was answered incorrectly
      # then the response quality is zero
      response_quality = 0

      # Display feedback for the user
      print(
      (
      "nIncorrect!"
      "nnAnswer: "
      "nnTime taken: :6.2f seconds"
      "nnResponse quality: "
      ).format(
      word_dict["word"], duration, response_quality
      )
      )

      # We update after the first attempt, but not on subsequent attempts
      if update:

      # Compute the new SuperMemo2 parameter values
      repetition_iteration, easiness_factor, repetition_interval =
      get_updated_sm2(
      repetition_iteration=word_dict["repetition_iteration"],
      easiness_factor=word_dict["easiness_factor"],
      repetition_interval=word_dict["repetition_interval"],
      response_quality=response_quality,
      )

      # Set the date for the next practice for this word
      next_practice =
      date.today() +
      timedelta(days=repetition_interval)

      # Update the SuperMemo2 parameter values in the deck
      deck[word_dict["word"]].update(
      "repetition_iteration": repetition_iteration,
      "easiness_factor": easiness_factor,
      "repetition_interval": repetition_interval,
      "next_practice": next_practice
      )

      # Pause for a second to give the user time to read
      time.sleep(1)

      # Return the response quality (determines whether or not to review again)
      return response_quality

      def learn_words(api_key, deck):

      # Store the current time (to determine which words to review)
      current_date = date.today()

      # Get all the words that need to be reviewed
      words_to_review = [
      v for k, v in deck.items()
      if datetime.strptime(v["next_practice"], "%Y-%m-%d").date()
      <= current_date
      ]

      # If no words are due, then get a new word
      if not words_to_review:

      # Get a random word and make sure it has a definition
      # NOTE: Why isn't this always the case? I'm setting the
      # "hasDictionaryDef" parameter to 'true'.
      definition = None
      while not definition:

      # Get a random word
      word = get_random_word(api_key)

      # Get the definition of the word
      definition = get_definition(api_key, word)

      # Construct the dictionary representation for the word
      word_dict =
      "word": word,
      "definition": definition,
      "easiness_factor": 0,
      "repetition_interval": 0,
      "repetition_iteration": 0,
      "last_practiced": None,
      "next_practice": None,


      # Teach the user the new word
      teach_new_word(word_dict)

      # Update the practice times for this word
      word_dict.update(
      "last_practiced": date.today(),
      "next_practice": date.today(),
      )

      # Add the word object to the deck
      deck[word] = word_dict

      # Add the word to the list of words to review
      words_to_review.append(word_dict)

      # Enter the main review loop
      while words_to_review:

      # Get the next word
      word_dict = words_to_review.pop()

      # Review the next word
      response_quality = review_word(
      word_dict=word_dict,
      deck=deck,
      update=True,
      )

      # If the response quality is too low, review again but don't update
      while response_quality < 4:
      response_quality = review_word(
      word_dict=word_dict,
      deck=deck,
      update=False,
      )

      def run():

      # Get the Wordnik API key from a config file
      with open(os.path.expanduser('~/.learnwords/wordnik.conf'), 'r') as config_file:

      # Load the configuration parameters from a JSON file
      config_json = json.load(config_file)

      # Get the Wordnik API key
      api_key = config_json["api_key"]

      # Load the deck from a file if it exists or initialize it otherwise
      try:

      # Load the deck (local library of stored words)
      with open(os.path.expanduser('~/.learnwords/deck.json'), 'r') as deck_file:

      # Convert the JSON data to a Python dictionary
      deck = json.load(deck_file)

      except FileNotFoundError as e:

      # Create an empty deck file
      with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
      deck_file.write(r'')
      deck =

      # Learn the words in the deck and update the deck file (allow the user to interrupt)

      # Learn the words in the deck
      try:
      learn_words(api_key, deck)

      # Allow the user to interrupt
      except KeyboardInterrupt as e:
      print("nnExiting program...")
      sys.exit(0)

      # Update the deck file
      finally:

      # Update the local deck file
      with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
      json.dump(deck, deck_file, indent=4, sort_keys=True, default=str)

      # Print an exit message
      print("nAll done for today!")

      # Run the application
      if __name__ == "__main__":
      run()


      Usage



      The Wordnik API key is stored in a JSON-format configuration file (~/.learnwords/wordnik.conf), e.g.




      "__comment__": "Wordnik API Key",
      "api_key": "idah8AeV1Shai7oosah9ayefahy1yiequa2yi0uyo2eeshaej"



      After running the program for the first time (e.g. python learnwords.py, a local file (~/.learnwords/deck.json) should be created and a new random word should be added from the Wordnik online dictionary. The user is shown the definition of the new word and prompted to enter the word:



      New word: vidding

      Definition:
      Command; order; a proclamation or notifying.

      Repeat the word:


      After entering the word correctly, the user is again prompted with the definition and asked for the word:



      Definition:
      Command; order; a proclamation or notifying.

      Word:


      After answering incorrectly (or too slowly), the user is given feedback and asked the question again:



      Definition: Command; order; a proclamation or notifying.

      Word: not vidding

      Incorrect!

      Answer: vidding

      Time taken: 11.37 seconds

      Response quality: 0


      After answering correctly, the user is asked another word or the program exits if there are no more words to review:



      Definition:
      Command; order; a proclamation or notifying.

      Word: vidding

      Correct!

      Time taken: 7.11 seconds

      Response quality: 4

      All done for today!






      share|improve this question













      Motivation



      I wanted to write a simple console application to improve my vocabulary by using the Wordnik API to get words and definitions and using the SuperMemo2 spaced-repetition algorithm to schedule review sessions.



      Features



      • Correctly implement the SM2 (SuperMemo2) spaced-repetition algorithm to schedule practice


      • Automatically compute the "easiness factor" for the SM2 algorithm based on the amount of time taken to answer each question


      • Automatically add new words using the Wordnik API


      Implementation



      Here is a small Python module that I wrote which prompts the user with a definition and asks for them to supply the correct word.



      NOTE: This requires a (free) Wordnik API key in order to run.



      #!/usr/bin/env python3
      # -*- encoding: utf8 -*-
      """learnwords.py

      Learn words from the Wordnik API using
      the SuperMemo2 spaced-repetition algorithm.
      """

      from datetime import datetime
      from datetime import date
      from datetime import timedelta
      import json
      import os
      import requests
      import sys
      import time

      # Define module-level variable for Wordnik API base URL
      WORDNIK_API_BASEURL = "https://api.wordnik.com/v4"

      def get_updated_sm2(
      repetition_iteration,
      easiness_factor,
      repetition_interval,
      response_quality,
      ):
      """SuperMemo2 update function"""

      # Rename the parameters for convenience
      n = repetition_iteration
      EF = easiness_factor
      q = response_quality

      # Update the iteration number
      # If the response quality was too low, restart the
      # repetitions without changing the easiness factor
      if q<3:
      new_repetition_iteration = 0
      else:
      new_repetition_iteration = n + 1

      # Compute the new easiness factor
      new_easiness_factor = max(
      EF + (0.1-(5-q)*(0.08+(5-q)*0.02)),
      1.3
      )

      # Compute the new inter-repetition interval
      if new_repetition_iteration == 1:
      new_repetition_interval = 1
      elif new_repetition_iteration == 2:
      new_repetition_interval = 6
      else:
      new_repetition_interval =
      int(new_repetition_iteration * new_easiness_factor)

      # Return the updated SM2 values
      return (
      new_repetition_iteration,
      new_easiness_factor,
      new_repetition_interval,
      )

      def get_random_word(api_key):
      """Returns a random word as a string."""

      # Construct the API URL for a random-word query
      api_url = "baseurl/words.json/randomWord".format(
      baseurl=WORDNIK_API_BASEURL
      )

      # Set the parameters for the query to
      # limit the list of results to a single word
      parameters =
      'hasDictionaryDef': 'true',
      'includePartOfSpeech': 'noun',
      'minCorpusCount': 0,
      'maxCorpusCount': -1,
      'minDictionaryCount': 1,
      'maxDictionaryCount': -1,
      'minLength': 5,
      'maxLength': 15,
      'api_key': api_key,


      # Perform the query and store the HTTP response object
      response = requests.get(api_url, params=parameters)

      # Convert the response content to a list
      # NOTE: The content is initially returned as a byte string
      word_object = json.loads(response.content)

      # Get the word itself
      word = word_object.get("canonicalForm", word_object["word"])

      # Return the word as a string
      return word

      def get_definition(api_key, word):
      """Returns the definition of a word."""

      # Construct the API URL for a word-definition query
      api_url = "baseurl/word.json/word/definitions".format(
      baseurl=WORDNIK_API_BASEURL,
      word=word,
      )

      # Set the parameters for the query to limit the
      # list of results to a single definition
      parameters =
      'limit': 1,
      'partOfSpeech': 'noun',
      'includeRelated': False,
      'sourceDictionaries': 'webster',
      'useCanonical': True,
      'includeTags': False,
      'api_key': api_key,


      # Perform the query and store the HTTP response object
      response = requests.get(api_url, params=parameters)

      # Convert the response content to a list
      # NOTE: The content is initially returned as a byte string
      definition_list = json.loads(response.content)

      # Return the first definition as a string, else return None
      if definition_list:
      return definition_list[0]["text"]

      def teach_new_word(word_dict):

      # Prompt the user with the new word
      # Repeat until they enter it correctly
      answer = None
      while answer != word_dict["word"]:

      # Clear the screen
      os.system('cls' if os.name == 'nt' else 'clear')

      # Prompt the user and get their response
      answer = input(
      "New word: nnDefinition:ntnnRepeat the word: ".format(
      word_dict["word"],
      word_dict["definition"],
      )
      ).strip()

      # Tell the user whether or not they entered the answer correctly
      if answer == word_dict["word"]:
      print("nCorrect!")
      else:
      print("nIncorrect!")

      # Pause for a second to give the user time to read
      time.sleep(1)

      def review_word(word_dict, deck, update):

      # Clear the screen
      os.system('cls' if os.name == 'nt' else 'clear')

      # Prompt with the definition and time the response
      start = time.time()
      answer = input(
      "nDefinition: nnWord: ".format(word_dict["definition"])
      ).strip()
      stop = time.time()
      duration = stop - start

      # Check to see if the answer is correct,
      # compute a reponse quality,
      # and display the correct answer
      if answer == word_dict["word"]:

      # Compute the response quality from the duration
      # NOTE: This was totally arbitrary on my part.
      if duration < 5:
      response_quality = 5
      elif duration < 10:
      response_quality = 4
      elif duration < 15:
      response_quality = 3
      elif duration < 20:
      response_quality = 2
      elif duration < 25:
      response_quality = 1
      else:
      response_quality = 0

      # Display feedback for the user
      print("nCorrect!nnTime taken: :6.2f secondsnnResponse quality: ".format(
      duration, response_quality)
      )

      else:

      # If the question was answered incorrectly
      # then the response quality is zero
      response_quality = 0

      # Display feedback for the user
      print(
      (
      "nIncorrect!"
      "nnAnswer: "
      "nnTime taken: :6.2f seconds"
      "nnResponse quality: "
      ).format(
      word_dict["word"], duration, response_quality
      )
      )

      # We update after the first attempt, but not on subsequent attempts
      if update:

      # Compute the new SuperMemo2 parameter values
      repetition_iteration, easiness_factor, repetition_interval =
      get_updated_sm2(
      repetition_iteration=word_dict["repetition_iteration"],
      easiness_factor=word_dict["easiness_factor"],
      repetition_interval=word_dict["repetition_interval"],
      response_quality=response_quality,
      )

      # Set the date for the next practice for this word
      next_practice =
      date.today() +
      timedelta(days=repetition_interval)

      # Update the SuperMemo2 parameter values in the deck
      deck[word_dict["word"]].update(
      "repetition_iteration": repetition_iteration,
      "easiness_factor": easiness_factor,
      "repetition_interval": repetition_interval,
      "next_practice": next_practice
      )

      # Pause for a second to give the user time to read
      time.sleep(1)

      # Return the response quality (determines whether or not to review again)
      return response_quality

      def learn_words(api_key, deck):

      # Store the current time (to determine which words to review)
      current_date = date.today()

      # Get all the words that need to be reviewed
      words_to_review = [
      v for k, v in deck.items()
      if datetime.strptime(v["next_practice"], "%Y-%m-%d").date()
      <= current_date
      ]

      # If no words are due, then get a new word
      if not words_to_review:

      # Get a random word and make sure it has a definition
      # NOTE: Why isn't this always the case? I'm setting the
      # "hasDictionaryDef" parameter to 'true'.
      definition = None
      while not definition:

      # Get a random word
      word = get_random_word(api_key)

      # Get the definition of the word
      definition = get_definition(api_key, word)

      # Construct the dictionary representation for the word
      word_dict =
      "word": word,
      "definition": definition,
      "easiness_factor": 0,
      "repetition_interval": 0,
      "repetition_iteration": 0,
      "last_practiced": None,
      "next_practice": None,


      # Teach the user the new word
      teach_new_word(word_dict)

      # Update the practice times for this word
      word_dict.update(
      "last_practiced": date.today(),
      "next_practice": date.today(),
      )

      # Add the word object to the deck
      deck[word] = word_dict

      # Add the word to the list of words to review
      words_to_review.append(word_dict)

      # Enter the main review loop
      while words_to_review:

      # Get the next word
      word_dict = words_to_review.pop()

      # Review the next word
      response_quality = review_word(
      word_dict=word_dict,
      deck=deck,
      update=True,
      )

      # If the response quality is too low, review again but don't update
      while response_quality < 4:
      response_quality = review_word(
      word_dict=word_dict,
      deck=deck,
      update=False,
      )

      def run():

      # Get the Wordnik API key from a config file
      with open(os.path.expanduser('~/.learnwords/wordnik.conf'), 'r') as config_file:

      # Load the configuration parameters from a JSON file
      config_json = json.load(config_file)

      # Get the Wordnik API key
      api_key = config_json["api_key"]

      # Load the deck from a file if it exists or initialize it otherwise
      try:

      # Load the deck (local library of stored words)
      with open(os.path.expanduser('~/.learnwords/deck.json'), 'r') as deck_file:

      # Convert the JSON data to a Python dictionary
      deck = json.load(deck_file)

      except FileNotFoundError as e:

      # Create an empty deck file
      with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
      deck_file.write(r'')
      deck =

      # Learn the words in the deck and update the deck file (allow the user to interrupt)

      # Learn the words in the deck
      try:
      learn_words(api_key, deck)

      # Allow the user to interrupt
      except KeyboardInterrupt as e:
      print("nnExiting program...")
      sys.exit(0)

      # Update the deck file
      finally:

      # Update the local deck file
      with open(os.path.expanduser('~/.learnwords/deck.json'), 'w') as deck_file:
      json.dump(deck, deck_file, indent=4, sort_keys=True, default=str)

      # Print an exit message
      print("nAll done for today!")

      # Run the application
      if __name__ == "__main__":
      run()


      Usage



      The Wordnik API key is stored in a JSON-format configuration file (~/.learnwords/wordnik.conf), e.g.




      "__comment__": "Wordnik API Key",
      "api_key": "idah8AeV1Shai7oosah9ayefahy1yiequa2yi0uyo2eeshaej"



      After running the program for the first time (e.g. python learnwords.py, a local file (~/.learnwords/deck.json) should be created and a new random word should be added from the Wordnik online dictionary. The user is shown the definition of the new word and prompted to enter the word:



      New word: vidding

      Definition:
      Command; order; a proclamation or notifying.

      Repeat the word:


      After entering the word correctly, the user is again prompted with the definition and asked for the word:



      Definition:
      Command; order; a proclamation or notifying.

      Word:


      After answering incorrectly (or too slowly), the user is given feedback and asked the question again:



      Definition: Command; order; a proclamation or notifying.

      Word: not vidding

      Incorrect!

      Answer: vidding

      Time taken: 11.37 seconds

      Response quality: 0


      After answering correctly, the user is asked another word or the program exits if there are no more words to review:



      Definition:
      Command; order; a proclamation or notifying.

      Word: vidding

      Correct!

      Time taken: 7.11 seconds

      Response quality: 4

      All done for today!








      share|improve this question












      share|improve this question




      share|improve this question








      edited Apr 3 at 20:58
























      asked Apr 3 at 12:05









      igal

      1647




      1647

























          active

          oldest

          votes











          Your Answer




          StackExchange.ifUsing("editor", function ()
          return StackExchange.using("mathjaxEditing", function ()
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          );
          );
          , "mathjax-editing");

          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "196"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );








           

          draft saved


          draft discarded


















          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f191151%2fpython-cli-program-to-learn-words-using-wordnik-api-and-supermemo2-algorithm%23new-answer', 'question_page');

          );

          Post as a guest



































          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes










           

          draft saved


          draft discarded


























           


          draft saved


          draft discarded














          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f191151%2fpython-cli-program-to-learn-words-using-wordnik-api-and-supermemo2-algorithm%23new-answer', 'question_page');

          );

          Post as a guest













































































          R NJCA2ANPc5O 3h9r,bnmZJyV0b0 51,Ghinmm VOgP5x j 7pWbVXUoT
          Le9ObROYtl NyGjk l Oxd

          Popular posts from this blog

          Chat program with C++ and SFML

          Function to Return a JSON Like Objects Using VBA Collections and Arrays

          Read an image with ADNS2610 optical sensor and Arduino Uno