SiriusStarr / elm-spaced-repetition / 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:

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 : Basics.Float
, easyBonus : Basics.Float
, intervalModifier : Basics.Float
, maximumInterval : TimeInterval Days
, hardInterval : Basics.Float
, lapseSteps : List (TimeInterval Minutes)
, lapseNewInterval : Basics.Float
, lapseMinimumInterval : TimeInterval Days
, leechThreshold : Basics.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

Reviews

Lapses

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:

An example follows:

createSettings
    |> setNewSteps [ 1, 2, 3 ]
    |> setLapseSteps [ 4, 5, 6 ]
    |> setLeechThreshold 0

setNewSteps : List Basics.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 : Basics.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 : Basics.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 : Basics.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 : Basics.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 : Basics.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 : Basics.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 : Basics.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 (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.

setLapseSteps : List Basics.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 : Basics.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 : Basics.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 : Basics.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 =
SpacedRepetition.Internal.SMTwoAnki.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 -> Json.Encode.Value

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

decoderSRSData : Json.Decode.Decoder SRSData

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

encoderAnkiSettings : AnkiSettings -> Json.Encode.Value

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

decoderAnkiSettings : Json.Decode.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:

answerCardInDeck : Time.Posix -> Answer -> Basics.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 : Time.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 : Time.Posix -> Deck a b -> List Basics.Int

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. 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).

Equally due cards are presented in random order.

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

getDueCardIndicesWithDetails : Time.Posix -> Deck a b -> List { index : Basics.Int, isLeech : Basics.Bool, queueDetails : QueueDetails }

getDueCardIndicesWithDetails 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 subset of the Deck that is due for review as a list of records, providing their index, which queue they are currently in (e.g. whether they are being learned or reviewed) along with any relevant queue details, and whether or not they are leeches. 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).

Equally due cards are presented in random order.

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

Card Details

If you require specific details for a single card, you may use the provided functionality here. If you need details for all due cards, just use getDueCardIndicesWithDetails. You can also get all leeches using getLeeches.


type QueueDetails
    = NewCard
    | LearningQueue ({ lastSeen : Time.Posix, intervalInMinutes : Basics.Int })
    | ReviewQueue ({ lastSeen : Time.Posix, intervalInDays : Basics.Int, lapses : Basics.Int })
    | LapsedQueue ({ lastSeen : Time.Posix, formerIntervalInDays : Basics.Int, intervalInMinutes : Basics.Int, lapses : Basics.Int })

QueueDetails represents the current status of a card.

getCardDetails : AnkiSettings -> Card a -> { isLeech : Basics.Bool, queueDetails : QueueDetails }

getCardDetails returns the current queue status for a given card and whether or not it is a leech. If you require this for every due card, simply use getDueCardIndicesWithDetails.

getLeeches : Deck a b -> List Basics.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

Cards with the same number of lapses will be ordered by their appearance in the original input deck.

Opaque Types

The following are exposed only so that they may be used in type annotations and may be created via their respective functions.


type alias TimeInterval a =
SpacedRepetition.Internal.SMTwoAnki.TimeInterval a

Opaque type. You don't need it, except maybe in type signatures.


type alias Days =
SpacedRepetition.Internal.SMTwoAnki.Days

Opaque type. You don't need it, except maybe in type signatures.


type alias Minutes =
SpacedRepetition.Internal.SMTwoAnki.Minutes

Opaque type. You don't need it, except maybe in type signatures.