SiriusStarr/elm-spaced-repetition/1.1.0

NOTE: the latest version is 2.1.0

SpacedRepetition.SMTwoAnki

This package provides everything necessary to create spaced repetition software using the algorithm used by the popular F/OSS program Anki. Anki's algorithm is a heavily-modified version of the SM-2 algorithm, which has been released for free public use when accompanied by the following notice:

Algorithm SM-2, (C) Copyright SuperMemo World, 1991.

For details about Anki's algorithm, please refer to the following section of its manual.

The above description details how Anki's algorithm differs from the SM-2 algorithm, but briefly, the following differences may be noted:

  • SM-2 defines an initial interval of 1 day then 6 days, whereas Anki's algorithm allows for customization of these initial intervals.

  • Anki's algorithm uses 4 choices (3 during learning) for answering cards, not 6.

  • Answering cards later than scheduled will be factored into the next interval calculation.

  • Failing a review card may cause behavior besides fully resetting it (if desired).

  • "Easy" answer choice increases the next interval in addition to ease/E-factor.

  • Successive failures while cards are in learning do not result in further decreases to the card’s ease.

Settings

This algorithm requires certain settings be provided when functions are called to specify the behavior of the system. A builder pattern is available for creating settings with defaults.

type alias AnkiSettings =
{ newSteps : List (TimeInterval Minutes)
, graduatingInterval : TimeInterval Days
, easyInterval : TimeInterval Days
, startingEase : Float
, easyBonus : Float
, intervalModifier : Float
, maximumInterval : TimeInterval Days
, hardInterval : Float
, lapseSteps : List (TimeInterval Minutes)
, lapseNewInterval : Float
, lapseMinimumInterval : TimeInterval Days
, leechThreshold : Int
}

AnkiSettings customizes the behavior of this algorithm. Refer to this section of the Anki manual for more details. It may be created with the createSettings function and its values may be changed form default by piping to the following functions:

New Cards

  • setNewSteps : List Int -> AnkiSettings -> AnkiSettings -- Anki default: [1, 10] Sets the interval steps (in minutes) that a card must go through while in the learning process. Answering Again or Hard will return a card to step 0. Answering Easy will instantly graduate a card. Answering Good will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer. Note that an interval must be at least 1 minute long; any value less than 1 minute will result in an interval of 1 minute.

  • setGraduatingInterval : Int -> AnkiSettings -> AnkiSettings -- Anki default: 1 Sets the initial interval (in days) a card will be scheduled for upon graduating from the learning phase. If newSteps is empty, all new cards will be scheduled for this interval when first encountered, regardless of answer. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.

  • setEasyInterval : Int -> AnkiSettings -> AnkiSettings -- Anki default: 4 Sets the initial interval (in days) a card will be scheduled for upon instantly graduating from the learning phase with an answer of Easy. This value is not used if newSteps is empty. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.`

  • setStartingEase : Float -> AnkiSettings -> AnkiSettings -- Anki default: 2.5 Sets the initial ease that a card graduating from learning will have. Values less than 1.3 are ignored and result in 1.3 being used instead (as that is the minimum ease a card may have).

Reviews

  • setEasyBonus : Float -> AnkiSettings -> AnkiSettings -- Anki default 1.3 Sets the multiplier for the next interval of a card that was answered Easy (i.e. additional interval length over merely answering Good. Values less than 1.0 are ignored.

  • setIntervalModifier : Float -> AnkiSettings -> AnkiSettings -- Anki default 1.0 Set the multiplier for all intervals, e.g. setting it to 0.8 will result in all review intervals being 80% as long as they would normally be. This multiplier may be used to increase/decrease retention at the cost of increasing/decreasing study time, respectively. Values less than or equal to zero are ignored.

  • setMaximumInterval : Int -> AnkiSettings -> AnkiSettings -- Anki default: 36500 Set the upper limit on the time the algorithm will schedule a card for (in days). Small values may be used to cap time between seeing a card (to increase retention). Values less than 1 are ignored. Note that, due to the limitations of integers, an absolute maximum interval of 1491308 days (4085 years) exists. Since you almost certainly won't be alive in 4000 years to study a card, this shouldn't be a problem.

  • setHardInterval : Float -> AnkiSettings -> AnkiSettings -- Anki default: 1.2 Set the multiplier applied to the previous interval when Hard is answered to a card. Note that intervals are forced to be at least 1 day longer than the previous one (before fuzzing). As such, values <= 1 will have no effect. Additionally, values of hardInterval that would result in a longer interval than that from answering Good (i.e. that are larger than the ease of the card) are ignored, with ease being used instead. As such, it is probably good practice to keep this value <= 1.3, as that is the lower limit on ease.

Lapses

  • lapseSteps : List Int -> AnkiSettings -> AnkiSettings -- Anki default: [10] Set the interval steps (in minutes) that a card must go through while relearning after a lapse. Answering Again or Hard will return a card to step 0. Answering Easy will instantly graduate a card back to review. Answering Good will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer.
  • lapseNewInterval : Float -> AnkiSettings -> AnkiSettings -- Anki default: 0.0 Set a multiplier for the pre-lapse interval to determine the new interval after graduating a card back from re-learning. Values less than 0.0 are ignored Note that this interval is not fuzzed, per Anki's source.
  • lapseMinimumInterval : Int -> AnkiSettings -> AnkiSettings -- Anki default: 1 Set a minimum bound on the previous option, with lapsed cards getting a new interval of at least this value. Answers less than 1 are ignored.
  • leechThreshold : Int -> AnkiSettings -> AnkiSettings -- Anki default: 8 Set the number of lapses before a card is considered a "leech." getDueCardIndices will return the leech status of each card in the deck. Additionally, getLeeches will return all leeches in a deck (regardless of due status). Setting this value to less than or equal to 0 turns off leeches entirely.
createSettings : AnkiSettings

createSettings creates an AnkiSettings with the same default settings as the Anki program itself. It may be piped into the following functions to update from default:

  • setNewSteps
  • setGraduatingInterval
  • setEasyInterval
  • setStartingEase
  • setEasyBonus
  • setIntervalModifier
  • setMaximumInterval
  • setHardInterval
  • setLapseSteps
  • setLapseNewInterval
  • setLapseMinimumInterval
  • setLeechThreshold

An example follows:

createSettings
    |> setNewSteps [ 1, 2, 3 ]
    |> setLapseSteps [ 4, 5, 6 ]
    |> setLeechThreshold 0
setNewSteps : List Int -> AnkiSettings -> AnkiSettings

setNewSteps sets the the interval steps (in minutes) that a card must go through while in the learning process. Answering Again or Hard will return a card to step 0. Answering Easy will instantly graduate a card. Answering Good will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer. Note that an interval must be at least 1 minute long; any value less than 1 minute will result in an interval of 1 minute.

setGraduatingInterval : Int -> AnkiSettings -> AnkiSettings

setGraduatingInterval sets the initial interval (in days) a card will be scheduled for upon graduating from the learning phase. If newSteps is empty, all new cards will be scheduled for this interval when first encountered, regardless of answer. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.

setEasyInterval : Int -> AnkiSettings -> AnkiSettings

setEasyInterval sets the initial interval (in days) a card will be scheduled for upon instantly graduating from the learning phase with an answer of Easy. This value is not used if newSteps is empty. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.

setStartingEase : Float -> AnkiSettings -> AnkiSettings

setStartingEase sets the initial ease that a card graduating from learning will have. Values less than 1.3 are ignored and result in 1.3 being used instead (as that is the minimum ease a card may have).

setEasyBonus : Float -> AnkiSettings -> AnkiSettings

setEasyBonus sets the multiplier for the next interval of a card that was answered Easy (i.e. additional interval length over merely answering Good. Values less than 1.0 are ignored.

setIntervalModifier : Float -> AnkiSettings -> AnkiSettings

setIntervalModifier sets the multiplier for all intervals, e.g. setting it to 0.8 will result in all review intervals being 80% as long as they would normally be. This multiplier may be used to increase/decrease retention at the cost of increasing/decreasing study time, respectively. Values less than or equal to zero are ignored.

setMaximumInterval : Int -> AnkiSettings -> AnkiSettings

setMaximumInterval sets the upper limit on the time the algorithm will schedule a card for (in days). Small values may be used to cap time between seeing a card (to increase retention). Values less than 1 are ignored. Note that, due to the limitations of integers, an absolute maximum interval of 1491308 days (4085 years) exists. Since you almost certainly won't be alive in 4000 years to study a card, this shouldn't be a problem.

setHardInterval : Float -> AnkiSettings -> AnkiSettings

setHardInterval sets the multiplier applied to the previous interval when Hard is answered to a card. Note that intervals are forced to be at least 1 day longer than the previous one. As such, values <= 1 will have no effect. Additionally, values of hardInterval that would result in a longer interval than that from answering Good (i.e. that are larger than the ease of the card) are ignored, with ease being used instead. As such, it is probably good practice to keep this value <= 1.3, as that is the lower limit on ease.

setLapseSteps : List Int -> AnkiSettings -> AnkiSettings

setLapseSteps sets the interval steps (in minutes) that a card must go through while relearning after a lapse. Answering Again or Hard will return a card to step 0. Answering Easy will instantly graduate a card back to review. Answering Good will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer.

setLapseNewInterval : Float -> AnkiSettings -> AnkiSettings

setLapseNewInterval sets a multiplier for the pre-lapse interval to determine the new interval after graduating a card back from re-learning. Values less than 0.0 are ignored Note that this interval is not fuzzed, per Anki's source.

setLapseMinimumInterval : Int -> AnkiSettings -> AnkiSettings

setLapseMinimumInterval sets a minimum bound on the previous option, with lapsed cards getting a new interval of at least this value. Answers less than 1 are ignored.

setLeechThreshold : Int -> AnkiSettings -> AnkiSettings

setLeechThreshold sets the number of lapses before a card is considered a "leech." getDueCardIndices will return the leech status of each card in the deck. Additionally, getLeeches will return all leeches in a deck (regardless of due status). Setting this value to less than or equal to 0 turns off leeches entirely.

Cards and Decks

The building blocks of this package are Cards and Decks. In simple terms, a Card may be thought of as a single flashcard and a Deck as a list or collection of Cards.

type alias Card a =
{ a | srsData : SRSData }

A Card represents a single question or unit of knowledge the user will review. In general terms, each would represent a single flashcard. Card is defined as an extensible record; as such, whatever necessary custom fields for a use case may simply be included in the record, e.g.:

type alias MyFlashcard =
    Card { prompt : String, answer : String }

A Card contains only the information necessary for scheduling and nothing else; all other information should be added as in the above example.

type alias Deck a b =
{ a | cards : Array (Card b), settings : AnkiSettings }

A Deck represents a list of cards to be studied (this might be called a "collection" in other software). It is a record with field cards, an Array of Card and field settings of AnkiSettings. Maintaining the state of a Deck may be handled by the user of the module or by this module itself. In general, it is probably best not to add a massive quantity of new (unstudied) cards to a deck at once.

Card Data

A Card may be created by use of the newSRSData function, as in the following example:

type alias MyFlashcard =
    Card { prompt : String, answer : String }


myFlashcard : MyFlashcard
myFlashcard =
    { prompt = "SYN"
    , answer = "SYN-ACK"
    , srsData = newSRSData
    }
type alias SRSData =
QueueStatus

SRSData contains all data necessary for the Anki system and may be created with the newSRSData function. It may additionally be saved/loaded using the Json encoder/decoder in this package

newSRSData : SRSData

newSRSData creates a new SRSData for inclusion in a Card.

Json Encoders/Decoders

Since Card data must necessarily be preserved between sessions, a Json encoder/decoder is provided for SRSData. It may be utilized as follows:

import Json.Decode as Decode
import Json.Encode as Encode


type alias MyFlashcard =
    Card { prompt : String, answer : String }


myFlashcardConstructor : SRSData -> String -> String -> MyFlashcard
myFlashcardConstructor srsData prompt answer =
    { prompt = prompt
    , answer = answer
    , srsData = srsData
    }


myFlashcardToJson : MyFlashcard -> String
myFlashcardToJson myCard =
    Encode.encode 0 <|
        Encode.object
            [ ( "srsData", encoderSRSData myCard.srsData )
            , ( "prompt", Encode.string myCard.prompt )
            , ( "answer", Encode.string myCard.answer )
            ]


myFlashcardDecoder : Decode.Decoder MyFlashcard
myFlashcardDecoder =
    Decode.map3 myFlashcardConstructor
        (Decode.field "srsData" decoderSRSData)
        (Decode.field "prompt" Decode.string)
        (Decode.field "answer" Decode.string)


jsonToMyFlashcard : String -> Result Decode.Error MyFlashcard
jsonToMyFlashcard str =
    Decode.decodeString myFlashcardDecoder str

A Json encoder/decoder is also provided for AnkiSettings, since a Deck's settings must be preserved between sessions.

encoderSRSData : SRSData -> Value

encoderSRSData provides a Json encoder for encoding SRSData from a Card.

decoderSRSData : Decoder SRSData

decoderSRSData provides a Json decoder for decoding SRSData for a Card.

encoderAnkiSettings : AnkiSettings -> Value

encoderAnkiSettings provides a Json encoder for encoding AnkiSettings from a Deck.

decoderAnkiSettings : Decoder AnkiSettings

decoderAnkiSettings provides a Json decoder for decoding AnkiSettings for a Deck.

Answering Cards

The SM-2 Anki algorithm depends on grading answers on a scale with one incorrect and three correct responses, except during the learning phase, in which case only 1 incorrect and two correct responses should be used. For some use cases, it may be able to programmatically determine the Answer of a user's response. In other cases, however, the user may need to self-report.

type Answer
= Again
| Hard
| Good
| Easy

The Answer type represents how accurate/certain a user's response was to a card and must be passed to answerCard whenever a Card is reviewed. This package uses the same names as Anki, as presented below:

  • Again -- An incorrect response.

  • Hard -- A correct response that was challenging to produce. Should not be presented as an option when answering cards in the Learning queue. In the Learning queue, Hard is merely a synonym for Again.

  • Good -- A correct response of appropriate difficulty.

  • Easy -- A correct response that was excessively easy to produce (will increase ease and interval faster)

answerCardInDeck : Posix -> Answer -> Int -> Deck a b -> Deck a b

answerCardInDeck functions analogously to answerCard but handles maintenance of the Deck, which is typically what one would desire. When a card is presented to the user and answered, answerCardInDeck should be called with the current time (in the Time.Posix format returned by the now task of the core Time module), an Answer, the index of the card in the Deck, and the Deck itself. It returns the updated Deck. Use this function if you simply want to store a Deck and not worry about updating it manually (which is most likely what you want). Otherwise, use answerCard to handle updating the Deck manually. Handling the presentation of a card is the responsibility of the implementing program, as various behaviors might be desirable in different cases. Note that if an invalid (out of bounds) index is passed, the Deck is returned unaltered.

answerCard : Posix -> Answer -> AnkiSettings -> Card a -> Card a

When a card is presented to the user and answered, answerCard should be called with the current time (in the Time.Posix format returned by the now task of the core Time module) and an Answer. It returns the updated card, which should replace the card in the Deck. Use this function if you want to handle updating the Deck manually; otherwise, use answerCardInDeck. Handling the presentation of a card is the responsibility of the implementing program, as various behaviors might be desirable in different cases.

Due Cards

Besides answering cards, this package handles determining which cards in a Deck are due and require answering.

getDueCardIndices : Posix -> Deck a b -> List ( Int, Bool )

getDueCardIndices takes the current time (in the Time.Posix format returned by the now task of the core Time module) and a Deck and returns the indices of the subset of the Deck that is due for review, as well as whether or not they are leeches (as List (Int, Bool)). The returned indices will be sorted in the following order:

  1. Lapsed cards overdue for review
    1. Cards more overdue (by proportion of interval)
    2. Cards less overdue (by proportion of interval)
  2. Review cards overdue for review
    1. Cards more overdue (by proportion of interval)
    2. Cards less overdue (by proportion of interval)
  3. Learning cards overdue for review
    1. Cards more overdue (by proportion of interval)
    2. Cards less overdue (by proportion of interval)
  4. Any new cards in the deck (never having been studied before).

getDueCardIndices will show cards up to 20 minutes early, as per Anki.

getLeeches : Deck a b -> List Int

getLeeches takes a Deck and returns the indices of the subset of the Deck that are leeches (as List Int). The returned indices will be sorted in the following order:

  1. Cards with more lapses

  2. Cards with fewer lapses