diff options
Diffstat (limited to 'gnosis-algorithm.el')
-rw-r--r-- | gnosis-algorithm.el | 186 |
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 |