summaryrefslogtreecommitdiff
path: root/gnosis-algorithm.el
diff options
context:
space:
mode:
Diffstat (limited to 'gnosis-algorithm.el')
-rw-r--r--gnosis-algorithm.el186
1 files changed, 115 insertions, 71 deletions
diff --git a/gnosis-algorithm.el b/gnosis-algorithm.el
index e850c9f..cee1d13 100644
--- a/gnosis-algorithm.el
+++ b/gnosis-algorithm.el
@@ -1,12 +1,14 @@
;;; gnosis-algorithm.el --- Spaced Repetition Algorithm for Gnosis -*- lexical-binding: t; -*-
-;; Copyright (C) 2023 Thanos Apollo
+;; Copyright (C) 2023-2024 Thanos Apollo
;; Author: Thanos Apollo <[email protected]>
;; Keywords: extensions
;; URL: https://git.thanosapollo.org/gnosis
;; Version: 0.0.1
+;; Package-Requires: ((emacs "29.1"))
+
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
@@ -22,7 +24,26 @@
;;; Commentary:
-;; Work in progress
+;; Handles date calculation as well as ef & interval calculations.
+
+;; Gnosis implements a highly customizable algorithm, inspired by SM-2.
+;; Gnosis algorithm does not use user's subjective rating of a note to
+;; determine the next review interval, but instead uses the user's
+;; success or failure in recalling the answer of a note.
+
+;; Each gnosis note has an ef (easiness factor), which is a list of 3
+;; values. The last value is the total ef for a note, which will be
+;; used to determine the next interval upon a successful answer recall,
+;; the second value is the ef-decrease value, this value will be
+;; subtracted from the the total ef upon failure to recall the answer of
+;; a note, the first value is the ef increase, will be added to the
+;; total ef upon a successful recall.
+
+;; Each gnosis deck has a gnosis-algorithm-ef-threshold, it's an
+;; integer value that refers to the consecutive success or failures to
+;; recall an answer. Upon reaching the threshold, gnosis-algorithm-ef-decrease
+;; or gnosis-algorithm-ef-increase will be applied to the ef-increase or
+;; ef-decrease of note.
;;; Code:
@@ -30,7 +51,7 @@
(require 'calendar)
(defcustom gnosis-algorithm-interval '(1 3)
- "Gnosis initial interval for successful reviews.
+ "Gnosis initial interval for initial successful reviews.
First item: First interval,
Second item: Second interval."
@@ -40,11 +61,9 @@ Second item: Second interval."
(defcustom gnosis-algorithm-ef '(0.35 0.30 1.3)
"Gnosis easiness factor.
-First item : Increase factor
-Second item: Decrease factor
-Third item : Starting total ef
-
-Note: Starting total ef should not be above 3.0"
+First item : Increase value
+Second item: Decrease value
+Third item : Total ef"
:group 'gnosis
:type '(list float))
@@ -53,10 +72,43 @@ Note: Starting total ef should not be above 3.0"
Used to calcuate new interval for failed questions.
-NOTE: Do not change this value above 1"
+NOTE: This value should be less than 1.0."
+ :group 'gnosis
+ :type 'float)
+
+(defcustom gnosis-algorithm-ef-increase 0.1
+ "Value to increase ef increase value with.
+
+Increase ef-increase value by this amount for every
+`gnosis-algorithm-ef-threshold' number of successful reviews."
:group 'gnosis
:type 'float)
+(defcustom gnosis-algorithm-ef-decrease 0.2
+ "Value to decrease ef decrease value with.
+
+Decrease ef decrease value by this amount for every
+`gnosis-algorithm-ef-threshold' number of failed reviews."
+ :group 'gnosis
+ :type 'float)
+
+(defcustom gnosis-algorithm-ef-threshold 3
+ "Threshold for updating ef increase/decrease values.
+
+Refers to the number of consecutive successful or failed reviews."
+ :group 'gnosis
+ :type 'integer)
+
+(defun gnosis-algorithm-replace-at-index (index new-item list)
+ "Replace item at INDEX with NEW-ITEM in LIST."
+ (cl-loop for item in list
+ for i from 0
+ collect (if (= i index) new-item item)))
+
+(defun gnosis-algorithm-round-items (list)
+ "Round all items in LIST to 2 decimal places."
+ (cl-loop for item in list
+ collect (/ (round (* item 100)) 100.0)))
(defun gnosis-algorithm-date (&optional offset)
"Return the current date in a list (year month day).
@@ -74,76 +126,68 @@ Optional integer OFFSET is a number of days from the current date."
(defun gnosis-algorithm-date-diff (date)
"Find the difference between the current date and the given DATE.
-DATE format must be given as (yyyy mm dd)
-The structure of the given date is (YEAR MONTH DAY)."
+DATE format must be given as (year month day)."
(let ((given-date (encode-time 0 0 0 (caddr date) (cadr date) (car date))))
(- (time-to-days (current-time))
(time-to-days given-date))))
+(cl-defun gnosis-algorithm-next-ef (&key ef success increase decrease threshold
+ c-successes c-failures)
+ "Return the new EF, (increase-value decrease-value total-value)
+
+Calculate the new e-factor given existing EF and SUCCESS, either t or nil.
-(defun gnosis-algorithm-e-factor (ef success)
- "Calculate the new e-factor given existing EF and SUCCESS, either t or nil."
- (pcase success
- (`t (+ ef (car gnosis-algorithm-ef)))
- (`nil (max 1.3 (- ef (cadr gnosis-algorithm-ef))))))
+Next EF is calculated as follows:
+Upon a successful review, increase total ef value (nth 2) by
+ef-increase value (nth 0).
-(cl-defun gnosis-algorithm-next-interval (&key last-interval review-num ef success failure-factor successful-reviews successful-reviews-c fails-c fails-t initial-interval)
+Upon a failed review, decrease total ef by ef-decrease value (nth 1).
+
+For every THRESHOLD of C-SUCCESSES (consecutive successful reviews)
+reviews, increase ef-increase by INCREASE.
+
+For every THRESHOLD of C-FAILURES reviews, decrease ef-decrease value
+by DECREASE."
+ (cl-assert (listp ef) nil "Assertion failed: ef must be a list")
+ (cl-assert (booleanp success) nil "Assertion failed: success must be a boolean value")
+ (cl-assert (numberp increase) nil "Assertion failed: increase must be a number")
+ (cl-assert (numberp decrease) nil "Assertion failed: decrease must be a number")
+ (cl-assert (numberp threshold) nil "Assertion failed: threshold must be a number")
+ (let ((threshold-p (= (% (max 1 (if success c-successes c-failures)) threshold) 0))
+ (new-ef (if success (gnosis-algorithm-replace-at-index 2 (+ (nth 2 ef) (nth 0 ef)) ef)
+ (gnosis-algorithm-replace-at-index 2 (max 1.3 (- (nth 2 ef) (nth 1 ef))) ef))))
+ (cond ((and success threshold-p)
+ (setf new-ef (gnosis-algorithm-replace-at-index 0 (+ (nth 0 ef) increase) new-ef)))
+ ((and (not success) threshold-p
+ (setf new-ef (gnosis-algorithm-replace-at-index 1 (+ (nth 1 ef) decrease) new-ef)))))
+ (gnosis-algorithm-round-items new-ef)))
+
+(cl-defun gnosis-algorithm-next-interval (&key last-interval ef success successful-reviews
+ failure-factor initial-interval)
"Calculate next interval.
-- LAST-INTERVAL : The number of days since the item was last reviewed.
--review-num: Number of times the item has been reviewed.
-- EF : Easiness Factor.
-- SUCCESS : Success of the recall, ranges from 0 (unsuccessful) to 1
- (successful).
-- FF: Failure factor
-- SUCCESSFUL-REVIEWS : Number of successful reviews.
-- SUCCESSFULL-REVIEWS-C: Successful reviews in a row.
-- FAILS-C: Failed reviews in a row.
-- FAILS-T: Total failed reviews.
-- INITIAL-INTERVAL: Initial intervals for successful reviews.
-
-Returns a list of: (INTERVAL N EF) where,
-- Next review date in (yyyy mm dd) format.
-- REVIEW-NUM: Incremented by 1.
-- EF : Modified based on the recall success for the item."
- ;; Check if gnosis-algorithm-ff is lower than 1 & is total-ef above 1.3
- (cond ((>= gnosis-algorithm-ff 1)
- (error "Value of `gnosis-algorithm-ff' must be lower than 1"))
- ((< (nth 2 gnosis-algorithm-ef) 1.3)
- (error "Value of total-ef from `gnosis-algorithm-ef' must be above 1.3")))
- ;; Calculate the next easiness factor.
- (let* ((next-ef (gnosis-algorithm-e-factor ef success))
- (interval
- (cond
- ;; TODO: Rewrite this!
- ;; First successful review -> first interval
- ((and (= successful-reviews 0) success
- (car initial-interval)))
- ;; Second successful review -> second interval
- ((and (= successful-reviews 1) success)
- (cadr initial-interval))
- ;; When successful-reviews-c is above 3, use 150% or 180%
- ;; of ef depending on the value of successful-reviews
- ((and success
- (>= successful-reviews-c 3)
- (>= review-num 5)
- (> last-interval 1))
- (* (* ef (if (>= successful-reviews 10) 1.8 1.5)) last-interval))
- ((and (equal success nil)
- (> fails-c 3)
- (>= review-num 5)
- (> last-interval 1))
- ;; When fails-c is above 3, use 150% or 180% of
- ;; failure-factor depending on the value of total failed
- ;; reviews.
- (* (max (min 0.8 (* failure-factor (if (>= fails-t 10) 1.8 1.5)))
- failure-factor)
- last-interval))
- ;; For everything else
- (t (if success
- (* ef last-interval)
- (* failure-factor last-interval))))))
- (list (gnosis-algorithm-date (round interval)) next-ef)))
+
+LAST-INTERVAL: number of days since last review
+EF: Easiness factor
+SUCCESS: t if review was successful, nil otherwise
+SUCCESSFUL-REVIEWS: number of successful reviews
+FAILURE-FACTOR: factor to multiply last interval by if review was unsuccessful
+INITIAL-INTERVAL: list of initial intervals for initial successful
+reviews. Will be used to determine the next interval for the first 2
+successful reviews."
+ (cl-assert (< gnosis-algorithm-ff 1) "Value of `gnosis-algorithm-ff' must be lower than 1")
+ ;; This should only occur in testing env or when the user has made breaking changes.
+ (cl-assert (> (nth 2 ef) 1) "Total ef value must be above 1")
+ (let* ((ef (nth 2 gnosis-algorithm-ef))
+ (interval (cond ((and (= successful-reviews 0) success)
+ (car initial-interval))
+ ((and (= successful-reviews 1) success)
+ (cadr initial-interval))
+ (t (if success
+ (* ef last-interval)
+ (* failure-factor last-interval))))))
+ (gnosis-algorithm-date (round interval))))
+
(provide 'gnosis-algorithm)
;;; gnosis-algorithm.el ends here