Updated org-drill to latest version (2.3).
[org-mode.git] / contrib / lisp / org-drill.el
blob98642cbf08f772b23a11e45d1af7010dac52510e
1 ;;; org-drill.el - Self-testing using spaced repetition
2 ;;;
3 ;;; Author: Paul Sexton <eeeickythump@gmail.com>
4 ;;; Version: 2.3
5 ;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
6 ;;;
7 ;;;
8 ;;; Synopsis
9 ;;; ========
10 ;;;
11 ;;; Uses the SuperMemo spaced repetition algorithms to conduct interactive
12 ;;; "drill sessions", where the material to be remembered is presented to the
13 ;;; student in random order. The student rates his or her recall of each item,
14 ;;; and this information is used to schedule the item for later revision.
15 ;;;
16 ;;; Each drill session can be restricted to topics in the current buffer
17 ;;; (default), one or several files, all agenda files, or a subtree. A single
18 ;;; topic can also be drilled.
19 ;;;
20 ;;; Different "card types" can be defined, which present their information to
21 ;;; the student in different ways.
22 ;;;
23 ;;; See the file README.org for more detailed documentation.
26 (eval-when-compile (require 'cl))
27 (eval-when-compile (require 'hi-lock))
28 (require 'org)
29 (require 'org-id)
30 (require 'org-learn)
33 (defgroup org-drill nil
34 "Options concerning interactive drill sessions in Org mode (org-drill)."
35 :tag "Org-Drill"
36 :group 'org-link)
40 (defcustom org-drill-question-tag
41 "drill"
42 "Tag which topics must possess in order to be identified as review topics
43 by `org-drill'."
44 :group 'org-drill
45 :type 'string)
48 (defcustom org-drill-maximum-items-per-session
50 "Each drill session will present at most this many topics for review.
51 Nil means unlimited."
52 :group 'org-drill
53 :type '(choice integer (const nil)))
57 (defcustom org-drill-maximum-duration
59 "Maximum duration of a drill session, in minutes.
60 Nil means unlimited."
61 :group 'org-drill
62 :type '(choice integer (const nil)))
65 (defcustom org-drill-failure-quality
67 "If the quality of recall for an item is this number or lower,
68 it is regarded as an unambiguous failure, and the repetition
69 interval for the card is reset to 0 days. If the quality is higher
70 than this number, it is regarded as successfully recalled, but the
71 time interval to the next repetition will be lowered if the quality
72 was near to a fail.
74 By default this is 2, for SuperMemo-like behaviour. For
75 Mnemosyne-like behaviour, set it to 1. Other values are not
76 really sensible."
77 :group 'org-drill
78 :type '(choice (const 2) (const 1)))
81 (defcustom org-drill-forgetting-index
83 "What percentage of items do you consider it is 'acceptable' to
84 forget each drill session? The default is 10%. A warning message
85 is displayed at the end of the session if the percentage forgotten
86 climbs above this number."
87 :group 'org-drill
88 :type 'integer)
91 (defcustom org-drill-leech-failure-threshold
93 "If an item is forgotten more than this many times, it is tagged
94 as a 'leech' item."
95 :group 'org-drill
96 :type '(choice integer (const nil)))
99 (defcustom org-drill-leech-method
100 'skip
101 "How should 'leech items' be handled during drill sessions?
102 Possible values:
103 - nil :: Leech items are treated the same as normal items.
104 - skip :: Leech items are not included in drill sessions.
105 - warn :: Leech items are still included in drill sessions,
106 but a warning message is printed when each leech item is
107 presented."
108 :group 'org-drill
109 :type '(choice (const 'warn) (const 'skip) (const nil)))
112 (defface org-drill-visible-cloze-face
113 '((t (:foreground "darkseagreen")))
114 "The face used to hide the contents of cloze phrases."
115 :group 'org-drill)
118 (defface org-drill-visible-cloze-hint-face
119 '((t (:foreground "dark slate blue")))
120 "The face used to hide the contents of cloze phrases."
121 :group 'org-drill)
124 (defface org-drill-hidden-cloze-face
125 '((t (:foreground "deep sky blue" :background "blue")))
126 "The face used to hide the contents of cloze phrases."
127 :group 'org-drill)
130 (defcustom org-drill-use-visible-cloze-face-p
132 "Use a special face to highlight cloze-deleted text in org mode
133 buffers?"
134 :group 'org-drill
135 :type 'boolean)
138 (defcustom org-drill-hide-item-headings-p
140 "Conceal the contents of the main heading of each item during drill
141 sessions? You may want to enable this behaviour if item headings or tags
142 contain information that could 'give away' the answer."
143 :group 'org-drill
144 :type 'boolean)
147 (defcustom org-drill-new-count-color
148 "royal blue"
149 "Foreground colour used to display the count of remaining new items
150 during a drill session."
151 :group 'org-drill
152 :type 'color)
154 (defcustom org-drill-mature-count-color
155 "green"
156 "Foreground colour used to display the count of remaining mature items
157 during a drill session. Mature items are due for review, but are not new."
158 :group 'org-drill
159 :type 'color)
161 (defcustom org-drill-failed-count-color
162 "red"
163 "Foreground colour used to display the count of remaining failed items
164 during a drill session."
165 :group 'org-drill
166 :type 'color)
168 (defcustom org-drill-done-count-color
169 "sienna"
170 "Foreground colour used to display the count of reviewed items
171 during a drill session."
172 :group 'org-drill
173 :type 'color)
176 (setplist 'org-drill-cloze-overlay-defaults
177 '(display "[...]"
178 face org-drill-hidden-cloze-face
179 window t))
181 (setplist 'org-drill-hidden-text-overlay
182 '(invisible t))
184 (setplist 'org-drill-replaced-text-overlay
185 '(display "Replaced text"
186 face default
187 window t))
190 (defvar org-drill-cloze-regexp
191 ;; ver 1 "[^][]\\(\\[[^][][^]]*\\]\\)"
192 ;; ver 2 "\\(\\[.*?\\]\\|^[^[[:cntrl:]]*?\\]\\|\\[.*?$\\)"
193 ;; ver 3! "\\(\\[.*?\\]\\|\\[.*?[[:cntrl:]]+.*?\\]\\)"
194 "\\(\\[[[:cntrl:][:graph:][:space:]]*?\\)\\(\\||.+?\\)\\(\\]\\)")
197 (defvar org-drill-cloze-keywords
198 `((,org-drill-cloze-regexp
199 (1 'org-drill-visible-cloze-face nil)
200 (2 'org-drill-visible-cloze-hint-face t)
201 (3 'org-drill-visible-cloze-face nil)
205 (defcustom org-drill-card-type-alist
206 '((nil . org-drill-present-simple-card)
207 ("simple" . org-drill-present-simple-card)
208 ("twosided" . org-drill-present-two-sided-card)
209 ("multisided" . org-drill-present-multi-sided-card)
210 ("hide1cloze" . org-drill-present-multicloze-hide1)
211 ("hide2cloze" . org-drill-present-multicloze-hide2)
212 ("show1cloze" . org-drill-present-multicloze-show1)
213 ("multicloze" . org-drill-present-multicloze-hide1)
214 ("conjugate" org-drill-present-verb-conjugation
215 org-drill-show-answer-verb-conjugation)
216 ("spanish_verb" . org-drill-present-spanish-verb)
217 ("translate_number" org-drill-present-translate-number
218 org-drill-show-answer-translate-number))
219 "Alist associating card types with presentation functions. Each entry in the
220 alist takes one of two forms:
221 1. (CARDTYPE . QUESTION-FN), where CARDTYPE is a string or nil (for default),
222 and QUESTION-FN is a function which takes no arguments and returns a boolean
223 value.
224 2. (CARDTYPE QUESTION-FN ANSWER-FN), where ANSWER-FN is a function that takes
225 one argument -- the argument is a function that itself takes no arguments.
226 ANSWER-FN is called with the point on the active item's
227 heading, just prior to displaying the item's 'answer'. It can therefore be
228 used to modify the appearance of the answer. ANSWER-FN must call its argument
229 before returning. (Its argument is a function that prompts the user and
230 performs rescheduling)."
231 :group 'org-drill
232 :type '(alist :key-type (choice string (const nil)) :value-type function))
235 (defcustom org-drill-spaced-repetition-algorithm
236 'sm5
237 "Which SuperMemo spaced repetition algorithm to use for scheduling items.
238 Available choices are:
239 - SM2 :: the SM2 algorithm, used in SuperMemo 2.0
240 - SM5 :: the SM5 algorithm, used in SuperMemo 5.0
241 - Simple8 :: a modified version of the SM8 algorithm. SM8 is used in
242 SuperMemo 98. The version implemented here is simplified in that while it
243 'learns' the difficulty of each item using quality grades and number of
244 failures, it does not modify the matrix of values that
245 governs how fast the inter-repetition intervals increase. A method for
246 adjusting intervals when items are reviewed early or late has been taken
247 from SM11, a later version of the algorithm, and included in Simple8."
248 :group 'org-drill
249 :type '(choice (const 'sm2) (const 'sm5) (const 'simple8)))
253 (defcustom org-drill-optimal-factor-matrix
255 "DO NOT CHANGE THE VALUE OF THIS VARIABLE.
257 Persistent matrix of optimal factors, used by the SuperMemo SM5 algorithm.
258 The matrix is saved (using the 'customize' facility) at the end of each
259 drill session.
261 Over time, values in the matrix will adapt to the individual user's
262 pace of learning."
263 :group 'org-drill
264 :type 'sexp)
267 (defcustom org-drill-add-random-noise-to-intervals-p
269 "If true, the number of days until an item's next repetition
270 will vary slightly from the interval calculated by the SM2
271 algorithm. The variation is very small when the interval is
272 small, but scales up with the interval."
273 :group 'org-drill
274 :type 'boolean)
277 (defcustom org-drill-adjust-intervals-for-early-and-late-repetitions-p
279 "If true, when the student successfully reviews an item 1 or more days
280 before or after the scheduled review date, this will affect that date of
281 the item's next scheduled review, according to the algorithm presented at
282 [[http://www.supermemo.com/english/algsm11.htm#Advanced%20repetitions]].
284 Items that were reviewed early will have their next review date brought
285 forward. Those that were reviewed late will have their next review
286 date postponed further.
288 Note that this option currently has no effect if the SM2 algorithm
289 is used."
290 :group 'org-drill
291 :type 'boolean)
294 (defcustom org-drill-cram-hours
296 "When in cram mode, items are considered due for review if
297 they were reviewed at least this many hours ago."
298 :group 'org-drill
299 :type 'integer)
302 ;;; NEW items have never been presented in a drill session before.
303 ;;; MATURE items HAVE been presented at least once before.
304 ;;; - YOUNG mature items were scheduled no more than
305 ;;; ORG-DRILL-DAYS-BEFORE-OLD days after their last
306 ;;; repetition. These items will have been learned 'recently' and will have a
307 ;;; low repetition count.
308 ;;; - OLD mature items have intervals greater than
309 ;;; ORG-DRILL-DAYS-BEFORE-OLD.
310 ;;; - OVERDUE items are past their scheduled review date by more than
311 ;;; LAST-INTERVAL * (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) days,
312 ;;; regardless of young/old status.
315 (defcustom org-drill-days-before-old
317 "When an item's inter-repetition interval rises above this value in days,
318 it is no longer considered a 'young' (recently learned) item."
319 :group 'org-drill
320 :type 'integer)
323 (defcustom org-drill-overdue-interval-factor
325 "An item is considered overdue if its scheduled review date is
326 more than (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) * LAST-INTERVAL
327 days in the past. For example, a value of 1.2 means an additional
328 20% of the last scheduled interval is allowed to elapse before
329 the item is overdue. A value of 1.0 means no extra time is
330 allowed at all - items are immediately considered overdue if
331 there is even one day's delay in reviewing them. This variable
332 should never be less than 1.0."
333 :group 'org-drill
334 :type 'float)
337 (defcustom org-drill-learn-fraction
339 "Fraction between 0 and 1 that governs how quickly the spaces
340 between successive repetitions increase, for all items. The
341 default value is 0.5. Higher values make spaces increase more
342 quickly with each successful repetition. You should only change
343 this in small increments (for example 0.05-0.1) as it has an
344 exponential effect on inter-repetition spacing."
345 :group 'org-drill
346 :type 'float)
349 (defvar *org-drill-session-qualities* nil)
350 (defvar *org-drill-start-time* 0)
351 (defvar *org-drill-new-entries* nil)
352 (defvar *org-drill-dormant-entry-count* 0)
353 (defvar *org-drill-due-entry-count* 0)
354 (defvar *org-drill-overdue-entry-count* 0)
355 (defvar *org-drill-due-tomorrow-count* 0)
356 (defvar *org-drill-current-entry-schedule-type* nil)
357 (defvar *org-drill-overdue-entries* nil
358 "List of markers for items that are considered 'overdue', based on
359 the value of ORG-DRILL-OVERDUE-INTERVAL-FACTOR.")
360 (defvar *org-drill-young-mature-entries* nil
361 "List of markers for mature entries whose last inter-repetition
362 interval was <= ORG-DRILL-DAYS-BEFORE-OLD days.")
363 (defvar *org-drill-old-mature-entries* nil
364 "List of markers for mature entries whose last inter-repetition
365 interval was greater than ORG-DRILL-DAYS-BEFORE-OLD days.")
366 (defvar *org-drill-failed-entries* nil)
367 (defvar *org-drill-again-entries* nil)
368 (defvar *org-drill-done-entries* nil)
369 (defvar *org-drill-current-item* nil
370 "Set to the marker for the item currently being tested.")
371 (defvar *org-drill-cram-mode* nil
372 "Are we in 'cram mode', where all items are considered due
373 for review unless they were already reviewed in the recent past?")
374 (defvar org-drill-scheduling-properties
375 '("LEARN_DATA" "DRILL_LAST_INTERVAL" "DRILL_REPEATS_SINCE_FAIL"
376 "DRILL_TOTAL_REPEATS" "DRILL_FAILURE_COUNT" "DRILL_AVERAGE_QUALITY"
377 "DRILL_EASE" "DRILL_LAST_QUALITY" "DRILL_LAST_REVIEWED"))
380 ;;; Make the above settings safe as file-local variables.
383 (put 'org-drill-question-tag 'safe-local-variable 'stringp)
384 (put 'org-drill-maximum-items-per-session 'safe-local-variable
385 '(lambda (val) (or (integerp val) (null val))))
386 (put 'org-drill-maximum-duration 'safe-local-variable
387 '(lambda (val) (or (integerp val) (null val))))
388 (put 'org-drill-failure-quality 'safe-local-variable 'integerp)
389 (put 'org-drill-forgetting-index 'safe-local-variable 'integerp)
390 (put 'org-drill-leech-failure-threshold 'safe-local-variable 'integerp)
391 (put 'org-drill-leech-method 'safe-local-variable
392 '(lambda (val) (memq val '(nil skip warn))))
393 (put 'org-drill-use-visible-cloze-face-p 'safe-local-variable 'booleanp)
394 (put 'org-drill-hide-item-headings-p 'safe-local-variable 'booleanp)
395 (put 'org-drill-spaced-repetition-algorithm 'safe-local-variable
396 '(lambda (val) (memq val '(simple8 sm5 sm2))))
397 (put 'org-drill-add-random-noise-to-intervals-p 'safe-local-variable 'booleanp)
398 (put 'org-drill-adjust-intervals-for-early-and-late-repetitions-p
399 'safe-local-variable 'booleanp)
400 (put 'org-drill-cram-hours 'safe-local-variable 'integerp)
401 (put 'org-drill-learn-fraction 'safe-local-variable 'floatp)
402 (put 'org-drill-days-before-old 'safe-local-variable 'integerp)
403 (put 'org-drill-overdue-interval-factor 'safe-local-variable 'floatp)
406 ;;;; Utilities ================================================================
409 (defun free-marker (m)
410 (set-marker m nil))
413 (defmacro pop-random (place)
414 (let ((idx (gensym)))
415 `(if (null ,place)
417 (let ((,idx (random* (length ,place))))
418 (prog1 (nth ,idx ,place)
419 (setq ,place (append (subseq ,place 0 ,idx)
420 (subseq ,place (1+ ,idx)))))))))
423 (defmacro push-end (val place)
424 "Add VAL to the end of the sequence stored in PLACE. Return the new
425 value."
426 `(setq ,place (append ,place (list ,val))))
429 (defun shuffle-list (list)
430 "Randomly permute the elements of LIST (all permutations equally likely)."
431 ;; Adapted from 'shuffle-vector' in cookie1.el
432 (let ((i 0)
434 temp
435 (len (length list)))
436 (while (< i len)
437 (setq j (+ i (random* (- len i))))
438 (setq temp (nth i list))
439 (setf (nth i list) (nth j list))
440 (setf (nth j list) temp)
441 (setq i (1+ i))))
442 list)
445 (defun round-float (floatnum fix)
446 "Round the floating point number FLOATNUM to FIX decimal places.
447 Example: (round-float 3.56755765 3) -> 3.568"
448 (let ((n (expt 10 fix)))
449 (/ (float (round (* floatnum n))) n)))
451 (defun time-to-inactive-org-timestamp (time)
452 (format-time-string
453 (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")
454 time))
457 (defun org-map-drill-entries (func scope &rest skip)
458 "Like `org-map-entries', but only drill entries are processed."
459 (apply 'org-map-entries func
460 (concat "+" org-drill-question-tag) scope skip))
463 (defmacro with-hidden-cloze-text (&rest body)
464 `(progn
465 (org-drill-hide-clozed-text)
466 (unwind-protect
467 (progn
468 ,@body)
469 (org-drill-unhide-clozed-text))))
472 (defmacro with-hidden-cloze-hints (&rest body)
473 `(progn
474 (org-drill-hide-cloze-hints)
475 (unwind-protect
476 (progn
477 ,@body)
478 (org-drill-unhide-text))))
481 (defmacro with-hidden-comments (&rest body)
482 `(progn
483 (if org-drill-hide-item-headings-p
484 (org-drill-hide-heading-at-point))
485 (org-drill-hide-comments)
486 (unwind-protect
487 (progn
488 ,@body)
489 (org-drill-unhide-text))))
492 (defun org-drill-days-since-last-review ()
493 "Nil means a last review date has not yet been stored for
494 the item.
495 Zero means it was reviewed today.
496 A positive number means it was reviewed that many days ago.
497 A negative number means the date of last review is in the future --
498 this should never happen."
499 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
500 (when datestr
501 (- (time-to-days (current-time))
502 (time-to-days (apply 'encode-time
503 (org-parse-time-string datestr)))))))
506 (defun org-drill-hours-since-last-review ()
507 "Like `org-drill-days-since-last-review', but return value is
508 in hours rather than days."
509 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
510 (when datestr
511 (floor
512 (/ (- (time-to-seconds (current-time))
513 (time-to-seconds (apply 'encode-time
514 (org-parse-time-string datestr))))
515 (* 60 60))))))
518 (defun org-drill-entry-p (&optional marker)
519 "Is MARKER, or the point, in a 'drill item'? This will return nil if
520 the point is inside a subheading of a drill item -- to handle that
521 situation use `org-part-of-drill-entry-p'."
522 (save-excursion
523 (when marker
524 (org-drill-goto-entry marker))
525 (member org-drill-question-tag (org-get-local-tags))))
528 (defun org-drill-goto-entry (marker)
529 (switch-to-buffer (marker-buffer marker))
530 (goto-char marker))
533 (defun org-part-of-drill-entry-p ()
534 "Is the current entry either the main heading of a 'drill item',
535 or a subheading within a drill item?"
536 (or (org-drill-entry-p)
537 ;; Does this heading INHERIT the drill tag
538 (member org-drill-question-tag (org-get-tags-at))))
541 (defun org-drill-goto-drill-entry-heading ()
542 "Move the point to the heading which holds the :drill: tag for this
543 drill entry."
544 (unless (org-at-heading-p)
545 (org-back-to-heading))
546 (unless (org-part-of-drill-entry-p)
547 (error "Point is not inside a drill entry"))
548 (while (not (org-drill-entry-p))
549 (unless (org-up-heading-safe)
550 (error "Cannot find a parent heading that is marked as a drill entry"))))
554 (defun org-drill-entry-leech-p ()
555 "Is the current entry a 'leech item'?"
556 (and (org-drill-entry-p)
557 (member "leech" (org-get-local-tags))))
560 ;; (defun org-drill-entry-due-p ()
561 ;; (cond
562 ;; (*org-drill-cram-mode*
563 ;; (let ((hours (org-drill-hours-since-last-review)))
564 ;; (and (org-drill-entry-p)
565 ;; (or (null hours)
566 ;; (>= hours org-drill-cram-hours)))))
567 ;; (t
568 ;; (let ((item-time (org-get-scheduled-time (point))))
569 ;; (and (org-drill-entry-p)
570 ;; (or (not (eql 'skip org-drill-leech-method))
571 ;; (not (org-drill-entry-leech-p)))
572 ;; (or (null item-time) ; not scheduled
573 ;; (not (minusp ; scheduled for today/in past
574 ;; (- (time-to-days (current-time))
575 ;; (time-to-days item-time))))))))))
578 (defun org-drill-entry-days-overdue ()
579 "Returns:
580 - NIL if the item is not to be regarded as scheduled for review at all.
581 This is the case if it is not a drill item, or if it is a leech item
582 that we wish to skip, or if we are in cram mode and have already reviewed
583 the item within the last few hours.
584 - 0 if the item is new, or if it scheduled for review today.
585 - A negative integer - item is scheduled that many days in the future.
586 - A positive integer - item is scheduled that many days in the past."
587 (cond
588 (*org-drill-cram-mode*
589 (let ((hours (org-drill-hours-since-last-review)))
590 (and (org-drill-entry-p)
591 (or (null hours)
592 (>= hours org-drill-cram-hours))
593 0)))
595 (let ((item-time (org-get-scheduled-time (point))))
596 (cond
597 ((or (not (org-drill-entry-p))
598 (and (eql 'skip org-drill-leech-method)
599 (org-drill-entry-leech-p)))
600 nil)
601 ((null item-time) ; not scheduled -> due now
604 (- (time-to-days (current-time))
605 (time-to-days item-time))))))))
608 (defun org-drill-entry-overdue-p (&optional days-overdue last-interval)
609 "Returns true if entry that is scheduled DAYS-OVERDUE dasy in the past,
610 and whose last inter-repetition interval was LAST-INTERVAL, should be
611 considered 'overdue'. If the arguments are not given they are extracted
612 from the entry at point."
613 (unless days-overdue
614 (setq days-overdue (org-drill-entry-days-overdue)))
615 (unless last-interval
616 (setq last-interval (org-drill-entry-last-interval 1)))
617 (and (numberp days-overdue)
618 (> days-overdue 1) ; enforce a sane minimum 'overdue' gap
619 ;;(> due org-drill-days-before-overdue)
620 (> (/ (+ days-overdue last-interval 1.0) last-interval)
621 org-drill-overdue-interval-factor)))
625 (defun org-drill-entry-due-p ()
626 (let ((due (org-drill-entry-days-overdue)))
627 (and (not (null due))
628 (not (minusp due)))))
631 (defun org-drill-entry-new-p ()
632 (and (org-drill-entry-p)
633 (let ((item-time (org-get-scheduled-time (point))))
634 (null item-time))))
637 (defun org-drill-entry-last-quality (&optional default)
638 (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
639 (if quality
640 (string-to-number quality)
641 default)))
644 (defun org-drill-entry-failure-count ()
645 (let ((quality (org-entry-get (point) "DRILL_FAILURE_COUNT")))
646 (if quality
647 (string-to-number quality)
648 0)))
651 (defun org-drill-entry-average-quality (&optional default)
652 (let ((val (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
653 (if val
654 (string-to-number val)
655 (or default nil))))
657 (defun org-drill-entry-last-interval (&optional default)
658 (let ((val (org-entry-get (point) "DRILL_LAST_INTERVAL")))
659 (if val
660 (string-to-number val)
661 (or default 0))))
663 (defun org-drill-entry-repeats-since-fail (&optional default)
664 (let ((val (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
665 (if val
666 (string-to-number val)
667 (or default 0))))
669 (defun org-drill-entry-total-repeats (&optional default)
670 (let ((val (org-entry-get (point) "DRILL_TOTAL_REPEATS")))
671 (if val
672 (string-to-number val)
673 (or default 0))))
675 (defun org-drill-entry-ease (&optional default)
676 (let ((val (org-entry-get (point) "DRILL_EASE")))
677 (if val
678 (string-to-number val)
679 default)))
682 ;;; From http://www.supermemo.com/english/ol/sm5.htm
683 (defun org-drill-random-dispersal-factor ()
684 "Returns a random number between 0.5 and 1.5."
685 (let ((a 0.047)
686 (b 0.092)
687 (p (- (random* 1.0) 0.5)))
688 (flet ((sign (n)
689 (cond ((zerop n) 0)
690 ((plusp n) 1)
691 (t -1))))
692 (/ (+ 100 (* (* (/ -1 b) (log (- 1 (* (/ b a ) (abs p)))))
693 (sign p)))
694 100.0))))
696 (defun pseudonormal (mean variation)
697 "Random numbers in a pseudo-normal distribution with mean MEAN, range
698 MEAN-VARIATION to MEAN+VARIATION"
699 (+ (random* variation)
700 (random* variation)
701 (- variation)
702 mean))
705 (defun org-drill-early-interval-factor (optimal-factor
706 optimal-interval
707 days-ahead)
708 "Arguments:
709 - OPTIMAL-FACTOR: interval-factor if the item had been tested
710 exactly when it was supposed to be.
711 - OPTIMAL-INTERVAL: interval for next repetition (days) if the item had been
712 tested exactly when it was supposed to be.
713 - DAYS-AHEAD: how many days ahead of time the item was reviewed.
715 Returns an adjusted optimal factor which should be used to
716 calculate the next interval, instead of the optimal factor found
717 in the matrix."
718 (let ((delta-ofmax (* (1- optimal-factor)
719 (/ (+ optimal-interval
720 (* 0.6 optimal-interval) -1) (1- optimal-interval)))))
721 (- optimal-factor
722 (* delta-ofmax (/ days-ahead (+ days-ahead (* 0.6 optimal-interval)))))))
725 (defun org-drill-get-item-data ()
726 "Returns a list of 6 items, containing all the stored recall
727 data for the item at point:
728 - LAST-INTERVAL is the interval in days that was used to schedule the item's
729 current review date.
730 - REPEATS is the number of items the item has been successfully recalled without
731 without any failures. It is reset to 0 upon failure to recall the item.
732 - FAILURES is the total number of times the user has failed to recall the item.
733 - TOTAL-REPEATS includes both successful and unsuccessful repetitions.
734 - AVERAGE-QUALITY is the mean quality of recall of the item over
735 all its repetitions, successful and unsuccessful.
736 - EASE is a number reflecting how easy the item is to learn. Higher is easier.
738 (let ((learn-str (org-entry-get (point) "LEARN_DATA"))
739 (repeats (org-drill-entry-total-repeats :missing)))
740 (cond
741 (learn-str
742 (let ((learn-data (or (and learn-str
743 (read learn-str))
744 (copy-list initial-repetition-state))))
745 (list (nth 0 learn-data) ; last interval
746 (nth 1 learn-data) ; repetitions
747 (org-drill-entry-failure-count)
748 (nth 1 learn-data)
749 (org-drill-entry-last-quality)
750 (nth 2 learn-data) ; EF
752 ((not (eql :missing repeats))
753 (list (org-drill-entry-last-interval)
754 (org-drill-entry-repeats-since-fail)
755 (org-drill-entry-failure-count)
756 (org-drill-entry-total-repeats)
757 (org-drill-entry-average-quality)
758 (org-drill-entry-ease)))
759 (t ; virgin item
760 (list 0 0 0 0 nil nil)))))
763 (defun org-drill-store-item-data (last-interval repeats failures
764 total-repeats meanq
765 ease)
766 "Stores the given data in the item at point."
767 (org-entry-delete (point) "LEARN_DATA")
768 (org-set-property "DRILL_LAST_INTERVAL"
769 (number-to-string (round-float last-interval 4)))
770 (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats))
771 (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats))
772 (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures))
773 (org-set-property "DRILL_AVERAGE_QUALITY"
774 (number-to-string (round-float meanq 3)))
775 (org-set-property "DRILL_EASE"
776 (number-to-string (round-float ease 3))))
780 ;;; SM2 Algorithm =============================================================
783 (defun determine-next-interval-sm2 (last-interval n ef quality
784 failures meanq total-repeats)
785 "Arguments:
786 - LAST-INTERVAL -- the number of days since the item was last reviewed.
787 - REPEATS -- the number of times the item has been successfully reviewed
788 - EF -- the 'easiness factor'
789 - QUALITY -- 0 to 5
791 Returns a list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX), where:
792 - INTERVAL is the number of days until the item should next be reviewed
793 - REPEATS is incremented by 1.
794 - EF is modified based on the recall quality for the item.
795 - OF-MATRIX is not modified."
796 (assert (> n 0))
797 (assert (and (>= quality 0) (<= quality 5)))
798 (if (<= quality org-drill-failure-quality)
799 ;; When an item is failed, its interval is reset to 0,
800 ;; but its EF is unchanged
801 (list -1 1 ef (1+ failures) meanq (1+ total-repeats)
802 org-drill-optimal-factor-matrix)
803 ;; else:
804 (let* ((next-ef (modify-e-factor ef quality))
805 (interval
806 (cond
807 ((<= n 1) 1)
808 ((= n 2)
809 (cond
810 (org-drill-add-random-noise-to-intervals-p
811 (case quality
812 (5 6)
813 (4 4)
814 (3 3)
815 (2 1)
816 (t -1)))
817 (t 6)))
818 (t (* last-interval next-ef)))))
819 (list (if org-drill-add-random-noise-to-intervals-p
820 (+ last-interval (* (- interval last-interval)
821 (org-drill-random-dispersal-factor)))
822 interval)
823 (1+ n)
824 next-ef
825 failures meanq (1+ total-repeats)
826 org-drill-optimal-factor-matrix))))
829 ;;; SM5 Algorithm =============================================================
832 (defun inter-repetition-interval-sm5 (last-interval n ef &optional of-matrix)
833 (let ((of (get-optimal-factor n ef (or of-matrix
834 org-drill-optimal-factor-matrix))))
835 (if (= 1 n)
837 (* of last-interval))))
840 (defun determine-next-interval-sm5 (last-interval n ef quality
841 failures meanq total-repeats
842 of-matrix &optional delta-days)
843 (if (zerop n) (setq n 1))
844 (if (null ef) (setq ef 2.5))
845 (assert (> n 0))
846 (assert (and (>= quality 0) (<= quality 5)))
847 (unless of-matrix
848 (setq of-matrix org-drill-optimal-factor-matrix))
849 (setq of-matrix (cl-copy-tree of-matrix))
851 (setq meanq (if meanq
852 (/ (+ quality (* meanq total-repeats 1.0))
853 (1+ total-repeats))
854 quality))
856 (let ((next-ef (modify-e-factor ef quality))
857 (old-ef ef)
858 (new-of (modify-of (get-optimal-factor n ef of-matrix)
859 quality org-drill-learn-fraction))
860 (interval nil))
861 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
862 delta-days (minusp delta-days))
863 (setq new-of (org-drill-early-interval-factor
864 (get-optimal-factor n ef of-matrix)
865 (inter-repetition-interval-sm5
866 last-interval n ef of-matrix)
867 delta-days)))
869 (setq of-matrix
870 (set-optimal-factor n next-ef of-matrix
871 (round-float new-of 3))) ; round OF to 3 d.p.
873 (setq ef next-ef)
875 (cond
876 ;; "Failed" -- reset repetitions to 0,
877 ((<= quality org-drill-failure-quality)
878 (list -1 1 old-ef (1+ failures) meanq (1+ total-repeats)
879 of-matrix)) ; Not clear if OF matrix is supposed to be
880 ; preserved
881 ;; For a zero-based quality of 4 or 5, don't repeat
882 ;; ((and (>= quality 4)
883 ;; (not org-learn-always-reschedule))
884 ;; (list 0 (1+ n) ef failures meanq
885 ;; (1+ total-repeats) of-matrix)) ; 0 interval = unschedule
887 (setq interval (inter-repetition-interval-sm5
888 last-interval n ef of-matrix))
889 (if org-drill-add-random-noise-to-intervals-p
890 (setq interval (* interval (org-drill-random-dispersal-factor))))
891 (list interval
892 (1+ n)
894 failures
895 meanq
896 (1+ total-repeats)
897 of-matrix)))))
900 ;;; Simple8 Algorithm =========================================================
903 (defun org-drill-simple8-first-interval (failures)
904 "Arguments:
905 - FAILURES: integer >= 0. The total number of times the item has
906 been forgotten, ever.
908 Returns the optimal FIRST interval for an item which has previously been
909 forgotten on FAILURES occasions."
910 (* 2.4849 (exp (* -0.057 failures))))
913 (defun org-drill-simple8-interval-factor (ease repetition)
914 "Arguments:
915 - EASE: floating point number >= 1.2. Corresponds to `AF' in SM8 algorithm.
916 - REPETITION: the number of times the item has been tested.
917 1 is the first repetition (ie the second trial).
918 Returns:
919 The factor by which the last interval should be
920 multiplied to give the next interval. Corresponds to `RF' or `OF'."
921 (+ 1.2 (* (- ease 1.2) (expt org-drill-learn-fraction (log repetition 2)))))
924 (defun org-drill-simple8-quality->ease (quality)
925 "Returns the ease (`AF' in the SM8 algorithm) which corresponds
926 to a mean item quality of QUALITY."
927 (+ (* 0.0542 (expt quality 4))
928 (* -0.4848 (expt quality 3))
929 (* 1.4916 (expt quality 2))
930 (* -1.2403 quality)
931 1.4515))
934 (defun determine-next-interval-simple8 (last-interval repeats quality
935 failures meanq totaln
936 &optional delta-days)
937 "Arguments:
938 - LAST-INTERVAL -- the number of days since the item was last reviewed.
939 - REPEATS -- the number of times the item has been successfully reviewed
940 - EASE -- the 'easiness factor'
941 - QUALITY -- 0 to 5
942 - DELTA-DAYS -- how many days overdue was the item when it was reviewed.
943 0 = reviewed on the scheduled day. +N = N days overdue.
944 -N = reviewed N days early.
946 Returns the new item data, as a list of 6 values:
947 - NEXT-INTERVAL
948 - REPEATS
949 - EASE
950 - FAILURES
951 - AVERAGE-QUALITY
952 - TOTAL-REPEATS.
953 See the documentation for `org-drill-get-item-data' for a description of these."
954 (assert (>= repeats 0))
955 (assert (and (>= quality 0) (<= quality 5)))
956 (assert (or (null meanq) (and (>= meanq 0) (<= meanq 5))))
957 (let ((next-interval nil))
958 (setf meanq (if meanq
959 (/ (+ quality (* meanq totaln 1.0)) (1+ totaln))
960 quality))
961 (cond
962 ((<= quality org-drill-failure-quality)
963 (incf failures)
964 (setf repeats 0
965 next-interval -1))
966 ((or (zerop repeats)
967 (zerop last-interval))
968 (setf next-interval (org-drill-simple8-first-interval failures))
969 (incf repeats)
970 (incf totaln))
972 (let* ((use-n
973 (if (and
974 org-drill-adjust-intervals-for-early-and-late-repetitions-p
975 (numberp delta-days) (plusp delta-days)
976 (plusp last-interval))
977 (+ repeats (min 1 (/ delta-days last-interval 1.0)))
978 repeats))
979 (factor (org-drill-simple8-interval-factor
980 (org-drill-simple8-quality->ease meanq) use-n))
981 (next-int (* last-interval factor)))
982 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
983 (numberp delta-days) (minusp delta-days))
984 ;; The item was reviewed earlier than scheduled.
985 (setf factor (org-drill-early-interval-factor
986 factor next-int (abs delta-days))
987 next-int (* last-interval factor)))
988 (setf next-interval next-int)
989 (incf repeats)
990 (incf totaln))))
991 (list
992 (if (and org-drill-add-random-noise-to-intervals-p
993 (plusp next-interval))
994 (* next-interval (org-drill-random-dispersal-factor))
995 next-interval)
996 repeats
997 (org-drill-simple8-quality->ease meanq)
998 failures
999 meanq
1000 totaln
1006 ;;; Essentially copied from `org-learn.el', but modified to
1007 ;;; optionally call the SM2 or simple8 functions.
1008 (defun org-drill-smart-reschedule (quality &optional days-ahead)
1009 "If DAYS-AHEAD is supplied it must be a positive integer. The
1010 item will be scheduled exactly this many days into the future."
1011 (let ((delta-days (- (time-to-days (current-time))
1012 (time-to-days (or (org-get-scheduled-time (point))
1013 (current-time)))))
1014 (ofmatrix org-drill-optimal-factor-matrix)
1015 ;; Entries can have weights, 1 by default. Intervals are divided by the
1016 ;; item's weight, so an item with a weight of 2 will have all intervals
1017 ;; halved, meaning you will end up reviewing it twice as often.
1018 ;; Useful for entries which randomly present any of several facts.
1019 (weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1020 (if (stringp weight)
1021 (setq weight (read weight)))
1022 (destructuring-bind (last-interval repetitions failures
1023 total-repeats meanq ease)
1024 (org-drill-get-item-data)
1025 (destructuring-bind (next-interval repetitions ease
1026 failures meanq total-repeats
1027 &optional new-ofmatrix)
1028 (case org-drill-spaced-repetition-algorithm
1029 (sm5 (determine-next-interval-sm5 last-interval repetitions
1030 ease quality failures
1031 meanq total-repeats ofmatrix))
1032 (sm2 (determine-next-interval-sm2 last-interval repetitions
1033 ease quality failures
1034 meanq total-repeats))
1035 (simple8 (determine-next-interval-simple8 last-interval repetitions
1036 quality failures meanq
1037 total-repeats
1038 delta-days)))
1039 (if (numberp days-ahead)
1040 (setq next-interval days-ahead))
1042 (org-drill-store-item-data next-interval repetitions failures
1043 total-repeats meanq ease)
1044 (if (and (null days-ahead)
1045 (numberp weight) (plusp weight)
1046 (not (minusp next-interval)))
1047 (setq next-interval (max 1.0 (/ next-interval weight))))
1049 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
1050 (setq org-drill-optimal-factor-matrix new-ofmatrix))
1052 (cond
1053 ((= 0 days-ahead)
1054 (org-schedule t))
1055 ((minusp days-ahead)
1056 (org-schedule nil (current-time)))
1058 (org-schedule nil (time-add (current-time)
1059 (days-to-time
1060 (round next-interval))))))))))
1063 (defun org-drill-hypothetical-next-review-date (quality)
1064 "Returns an integer representing the number of days into the future
1065 that the current item would be scheduled, based on a recall quality
1066 of QUALITY."
1067 (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1068 (destructuring-bind (last-interval repetitions failures
1069 total-repeats meanq ease)
1070 (org-drill-get-item-data)
1071 (if (stringp weight)
1072 (setq weight (read weight)))
1073 (destructuring-bind (next-interval repetitions ease
1074 failures meanq total-repeats
1075 &optional ofmatrix)
1076 (case org-drill-spaced-repetition-algorithm
1077 (sm5 (determine-next-interval-sm5 last-interval repetitions
1078 ease quality failures
1079 meanq total-repeats
1080 org-drill-optimal-factor-matrix))
1081 (sm2 (determine-next-interval-sm2 last-interval repetitions
1082 ease quality failures
1083 meanq total-repeats))
1084 (simple8 (determine-next-interval-simple8 last-interval repetitions
1085 quality failures meanq
1086 total-repeats)))
1087 (cond
1088 ((not (plusp next-interval))
1090 ((and (numberp weight) (plusp weight))
1091 (max 1.0 (/ next-interval weight)))
1093 next-interval))))))
1096 (defun org-drill-hypothetical-next-review-dates ()
1097 (let ((intervals nil))
1098 (dotimes (q 6)
1099 (push (max (or (car intervals) 0)
1100 (org-drill-hypothetical-next-review-date q))
1101 intervals))
1102 (reverse intervals)))
1105 (defun org-drill-reschedule ()
1106 "Returns quality rating (0-5), or nil if the user quit."
1107 (let ((ch nil)
1108 (input nil)
1109 (next-review-dates (org-drill-hypothetical-next-review-dates)))
1110 (save-excursion
1111 (while (not (memq ch '(?q ?e ?0 ?1 ?2 ?3 ?4 ?5)))
1112 (setq input (read-key-sequence
1113 (if (eq ch ??)
1114 (format "0-2 Means you have forgotten the item.
1115 3-5 Means you have remembered the item.
1117 0 - Completely forgot.
1118 1 - Even after seeing the answer, it still took a bit to sink in.
1119 2 - After seeing the answer, you remembered it.
1120 3 - It took you awhile, but you finally remembered. (+%s days)
1121 4 - After a little bit of thought you remembered. (+%s days)
1122 5 - You remembered the item really easily. (+%s days)
1124 How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)"
1125 (round (nth 3 next-review-dates))
1126 (round (nth 4 next-review-dates))
1127 (round (nth 5 next-review-dates)))
1128 "How well did you do? (0-5, ?=help, e=edit, q=quit)")))
1129 (cond
1130 ((stringp input)
1131 (setq ch (elt input 0)))
1132 ((and (vectorp input) (symbolp (elt input 0)))
1133 (case (elt input 0)
1134 (up (ignore-errors (forward-line -1)))
1135 (down (ignore-errors (forward-line 1)))
1136 (left (ignore-errors (backward-char)))
1137 (right (ignore-errors (forward-char)))
1138 (prior (ignore-errors (scroll-down))) ; pgup
1139 (next (ignore-errors (scroll-up))))) ; pgdn
1140 ((and (vectorp input) (listp (elt input 0))
1141 (eventp (elt input 0)))
1142 (case (car (elt input 0))
1143 (wheel-up (ignore-errors (mwheel-scroll (elt input 0))))
1144 (wheel-down (ignore-errors (mwheel-scroll (elt input 0)))))))
1145 (if (eql ch ?t)
1146 (org-set-tags-command))))
1147 (cond
1148 ((and (>= ch ?0) (<= ch ?5))
1149 (let ((quality (- ch ?0))
1150 (failures (org-drill-entry-failure-count)))
1151 (save-excursion
1152 (org-drill-smart-reschedule quality
1153 (nth quality next-review-dates)))
1154 (push quality *org-drill-session-qualities*)
1155 (cond
1156 ((<= quality org-drill-failure-quality)
1157 (when org-drill-leech-failure-threshold
1158 ;;(setq failures (if failures (string-to-number failures) 0))
1159 ;; (org-set-property "DRILL_FAILURE_COUNT"
1160 ;; (format "%d" (1+ failures)))
1161 (if (> (1+ failures) org-drill-leech-failure-threshold)
1162 (org-toggle-tag "leech" 'on))))
1164 (let ((scheduled-time (org-get-scheduled-time (point))))
1165 (when scheduled-time
1166 (message "Next review in %d days"
1167 (- (time-to-days scheduled-time)
1168 (time-to-days (current-time))))
1169 (sit-for 0.5)))))
1170 (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality))
1171 (org-set-property "DRILL_LAST_REVIEWED"
1172 (time-to-inactive-org-timestamp (current-time)))
1173 quality))
1174 ((= ch ?e)
1175 'edit)
1177 nil))))
1180 ;; (defun org-drill-hide-all-subheadings-except (heading-list)
1181 ;; "Returns a list containing the position of each immediate subheading of
1182 ;; the current topic."
1183 ;; (let ((drill-entry-level (org-current-level))
1184 ;; (drill-sections nil)
1185 ;; (drill-heading nil))
1186 ;; (org-show-subtree)
1187 ;; (save-excursion
1188 ;; (org-map-entries
1189 ;; (lambda ()
1190 ;; (when (and (not (outline-invisible-p))
1191 ;; (> (org-current-level) drill-entry-level))
1192 ;; (setq drill-heading (org-get-heading t))
1193 ;; (unless (and (= (org-current-level) (1+ drill-entry-level))
1194 ;; (member drill-heading heading-list))
1195 ;; (hide-subtree))
1196 ;; (push (point) drill-sections)))
1197 ;; "" 'tree))
1198 ;; (reverse drill-sections)))
1202 (defun org-drill-hide-subheadings-if (test)
1203 "TEST is a function taking no arguments. TEST will be called for each
1204 of the immediate subheadings of the current drill item, with the point
1205 on the relevant subheading. TEST should return nil if the subheading is
1206 to be revealed, non-nil if it is to be hidden.
1207 Returns a list containing the position of each immediate subheading of
1208 the current topic."
1209 (let ((drill-entry-level (org-current-level))
1210 (drill-sections nil))
1211 (org-show-subtree)
1212 (save-excursion
1213 (org-map-entries
1214 (lambda ()
1215 (when (and (not (outline-invisible-p))
1216 (> (org-current-level) drill-entry-level))
1217 (when (or (/= (org-current-level) (1+ drill-entry-level))
1218 (funcall test))
1219 (hide-subtree))
1220 (push (point) drill-sections)))
1221 "" 'tree))
1222 (reverse drill-sections)))
1225 (defun org-drill-hide-all-subheadings-except (heading-list)
1226 (org-drill-hide-subheadings-if
1227 (lambda () (let ((drill-heading (org-get-heading t)))
1228 (not (member drill-heading heading-list))))))
1231 (defun org-drill-presentation-prompt (&rest fmt-and-args)
1232 (let* ((item-start-time (current-time))
1233 (input nil)
1234 (ch nil)
1235 (last-second 0)
1236 (mature-entry-count (+ (length *org-drill-young-mature-entries*)
1237 (length *org-drill-old-mature-entries*)
1238 (length *org-drill-overdue-entries*)))
1239 (prompt
1240 (if fmt-and-args
1241 (apply 'format
1242 (first fmt-and-args)
1243 (rest fmt-and-args))
1244 (concat "Press key for answer, "
1245 "e=edit, t=tags, s=skip, q=quit."))))
1246 (setq prompt
1247 (format "%s %s %s %s %s %s"
1248 (propertize
1249 (char-to-string
1250 (case *org-drill-current-entry-schedule-type*
1251 (new ?N) (young ?Y) (old ?o) (overdue ?!) (failed ?F) (t ??)))
1252 'face `(:foreground
1253 ,(case *org-drill-current-entry-schedule-type*
1254 (new org-drill-new-count-color)
1255 ((young old) org-drill-mature-count-color)
1256 ((overdue failed) org-drill-failed-count-color)
1257 (t org-drill-done-count-color))))
1258 (propertize
1259 (number-to-string (length *org-drill-done-entries*))
1260 'face `(:foreground ,org-drill-done-count-color)
1261 'help-echo "The number of items you have reviewed this session.")
1262 (propertize
1263 (number-to-string (+ (length *org-drill-again-entries*)
1264 (length *org-drill-failed-entries*)))
1265 'face `(:foreground ,org-drill-failed-count-color)
1266 'help-echo (concat "The number of items that you failed, "
1267 "and need to review again."))
1268 (propertize
1269 (number-to-string mature-entry-count)
1270 'face `(:foreground ,org-drill-mature-count-color)
1271 'help-echo "The number of old items due for review.")
1272 (propertize
1273 (number-to-string (length *org-drill-new-entries*))
1274 'face `(:foreground ,org-drill-new-count-color)
1275 'help-echo (concat "The number of new items that you "
1276 "have never reviewed."))
1277 prompt))
1278 (if (and (eql 'warn org-drill-leech-method)
1279 (org-drill-entry-leech-p))
1280 (setq prompt (concat
1281 (propertize "!!! LEECH ITEM !!!
1282 You seem to be having a lot of trouble memorising this item.
1283 Consider reformulating the item to make it easier to remember.\n"
1284 'face '(:foreground "red"))
1285 prompt)))
1286 (while (memq ch '(nil ?t))
1287 (setq ch nil)
1288 (while (not (input-pending-p))
1289 (let ((elapsed (time-subtract (current-time) item-start-time)))
1290 (message (concat (if (>= (time-to-seconds elapsed) (* 60 60))
1291 "++:++ "
1292 (format-time-string "%M:%S " elapsed))
1293 prompt))
1294 (sit-for 1)))
1295 (setq input (read-key-sequence nil))
1296 (if (stringp input) (setq ch (elt input 0)))
1297 (if (eql ch ?t)
1298 (org-set-tags-command)))
1299 (case ch
1300 (?q nil)
1301 (?e 'edit)
1302 (?s 'skip)
1303 (otherwise t))))
1306 (defun org-pos-in-regexp (pos regexp &optional nlines)
1307 (save-excursion
1308 (goto-char pos)
1309 (org-in-regexp regexp nlines)))
1312 (defun org-drill-hide-region (beg end &optional text)
1313 "Hide the buffer region between BEG and END with an 'invisible text'
1314 visual overlay, or with the string TEXT if it is supplied."
1315 (let ((ovl (make-overlay beg end)))
1316 (overlay-put ovl 'category
1317 'org-drill-hidden-text-overlay)
1318 (when (stringp text)
1319 (overlay-put ovl 'invisible nil)
1320 (overlay-put ovl 'face 'default)
1321 (overlay-put ovl 'display text))))
1324 (defun org-drill-hide-heading-at-point (&optional text)
1325 (unless (org-at-heading-p)
1326 (error "Point is not on a heading."))
1327 (save-excursion
1328 (let ((beg (point)))
1329 (end-of-line)
1330 (org-drill-hide-region beg (point) text))))
1333 (defun org-drill-hide-comments ()
1334 (save-excursion
1335 (while (re-search-forward "^#.*$" nil t)
1336 (org-drill-hide-region (match-beginning 0) (match-end 0)))))
1339 (defun org-drill-unhide-text ()
1340 ;; This will also unhide the item's heading.
1341 (save-excursion
1342 (dolist (ovl (overlays-in (point-min) (point-max)))
1343 (when (eql 'org-drill-hidden-text-overlay (overlay-get ovl 'category))
1344 (delete-overlay ovl)))))
1347 (defun org-drill-hide-clozed-text ()
1348 (save-excursion
1349 (while (re-search-forward org-drill-cloze-regexp nil t)
1350 ;; Don't hide org links, partly because they might contain inline
1351 ;; images which we want to keep visible
1352 (unless (org-pos-in-regexp (match-beginning 0)
1353 org-bracket-link-regexp 1)
1354 (org-drill-hide-matched-cloze-text)))))
1357 (defun org-drill-hide-matched-cloze-text ()
1358 "Hide the current match with a 'cloze' visual overlay."
1359 (let ((ovl (make-overlay (match-beginning 0) (match-end 0))))
1360 (overlay-put ovl 'category
1361 'org-drill-cloze-overlay-defaults)
1362 (when (find ?| (match-string 0))
1363 (let ((hint (substring-no-properties
1364 (match-string 0)
1365 (1+ (position ?| (match-string 0)))
1366 (1- (length (match-string 0))))))
1367 (overlay-put
1368 ovl 'display
1369 ;; If hint is like `X...' then display [X...]
1370 ;; otherwise display [...X]
1371 (format (if (string-match-p "\\.\\.\\." hint) "[%s]" "[%s...]")
1372 hint))))))
1375 (defun org-drill-hide-cloze-hints ()
1376 (save-excursion
1377 (while (re-search-forward org-drill-cloze-regexp nil t)
1378 (unless (or (org-pos-in-regexp (match-beginning 0)
1379 org-bracket-link-regexp 1)
1380 (null (match-beginning 2))) ; hint subexpression matched
1381 (org-drill-hide-region (match-beginning 2) (match-end 2))))))
1384 (defmacro with-replaced-entry-text (text &rest body)
1385 "During the execution of BODY, the entire text of the current entry is
1386 concealed by an overlay that displays the string TEXT."
1387 `(progn
1388 (org-drill-replace-entry-text ,text)
1389 (unwind-protect
1390 (progn
1391 ,@body)
1392 (org-drill-unreplace-entry-text))))
1395 (defun org-drill-replace-entry-text (text)
1396 "Make an overlay that conceals the entire text of the item, not
1397 including properties or the contents of subheadings. The overlay shows
1398 the string TEXT.
1399 Note: does not actually alter the item."
1400 (let ((ovl (make-overlay (point-min)
1401 (save-excursion
1402 (outline-next-heading)
1403 (point)))))
1404 (overlay-put ovl 'category
1405 'org-drill-replaced-text-overlay)
1406 (overlay-put ovl 'display text)))
1409 (defun org-drill-unreplace-entry-text ()
1410 (save-excursion
1411 (dolist (ovl (overlays-in (point-min) (point-max)))
1412 (when (eql 'org-drill-replaced-text-overlay (overlay-get ovl 'category))
1413 (delete-overlay ovl)))))
1416 (defmacro with-replaced-entry-heading (heading &rest body)
1417 `(progn
1418 (org-drill-replace-entry-heading ,heading)
1419 (unwind-protect
1420 (progn
1421 ,@body)
1422 (org-drill-unhide-text))))
1425 (defun org-drill-replace-entry-heading (heading)
1426 "Make an overlay that conceals the heading of the item. The overlay shows
1427 the string TEXT.
1428 Note: does not actually alter the item."
1429 (org-drill-hide-heading-at-point heading))
1432 (defun org-drill-unhide-clozed-text ()
1433 (save-excursion
1434 (dolist (ovl (overlays-in (point-min) (point-max)))
1435 (when (eql 'org-drill-cloze-overlay-defaults (overlay-get ovl 'category))
1436 (delete-overlay ovl)))))
1439 (defun org-drill-get-entry-text (&optional keep-properties-p)
1440 (let ((text (org-agenda-get-some-entry-text (point-marker) 100)))
1441 (if keep-properties-p
1442 text
1443 (substring-no-properties text))))
1446 (defun org-drill-entry-empty-p ()
1447 (zerop (length (org-drill-get-entry-text))))
1451 ;;; Presentation functions ====================================================
1453 ;; Each of these is called with point on topic heading. Each needs to show the
1454 ;; topic in the form of a 'question' or with some information 'hidden', as
1455 ;; appropriate for the card type. The user should then be prompted to press a
1456 ;; key. The function should then reveal either the 'answer' or the entire
1457 ;; topic, and should return t if the user chose to see the answer and rate their
1458 ;; recall, nil if they chose to quit.
1460 (defun org-drill-present-simple-card ()
1461 (with-hidden-comments
1462 (with-hidden-cloze-hints
1463 (with-hidden-cloze-text
1464 (org-drill-hide-all-subheadings-except nil)
1465 (org-display-inline-images t)
1466 (org-cycle-hide-drawers 'all)
1467 (prog1 (org-drill-presentation-prompt)
1468 (org-drill-hide-subheadings-if 'org-drill-entry-p))))))
1471 (defun org-drill-present-default-answer (reschedule-fn)
1472 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1473 (org-drill-unhide-clozed-text)
1474 (with-hidden-cloze-hints
1475 (funcall reschedule-fn)))
1478 (defun org-drill-present-two-sided-card ()
1479 (with-hidden-comments
1480 (with-hidden-cloze-hints
1481 (with-hidden-cloze-text
1482 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1483 (when drill-sections
1484 (save-excursion
1485 (goto-char (nth (random* (min 2 (length drill-sections)))
1486 drill-sections))
1487 (org-show-subtree)))
1488 (org-display-inline-images t)
1489 (org-cycle-hide-drawers 'all)
1490 (prog1 (org-drill-presentation-prompt)
1491 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1495 (defun org-drill-present-multi-sided-card ()
1496 (with-hidden-comments
1497 (with-hidden-cloze-hints
1498 (with-hidden-cloze-text
1499 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1500 (when drill-sections
1501 (save-excursion
1502 (goto-char (nth (random* (length drill-sections)) drill-sections))
1503 (org-show-subtree)))
1504 (org-display-inline-images t)
1505 (org-cycle-hide-drawers 'all)
1506 (prog1 (org-drill-presentation-prompt)
1507 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1510 (defun org-drill-present-multicloze-hide-n (number-to-hide)
1511 "Hides NUMBER-TO-HIDE pieces of text that are marked for cloze deletion,
1512 chosen at random."
1513 (with-hidden-comments
1514 (with-hidden-cloze-hints
1515 (let ((item-end nil)
1516 (match-count 0)
1517 (body-start (or (cdr (org-get-property-block))
1518 (point))))
1519 (org-drill-hide-all-subheadings-except nil)
1520 (save-excursion
1521 (outline-next-heading)
1522 (setq item-end (point)))
1523 (save-excursion
1524 (goto-char body-start)
1525 (while (re-search-forward org-drill-cloze-regexp item-end t)
1526 (incf match-count)))
1527 (when (plusp match-count)
1528 (let ((match-nums (subseq (shuffle-list (loop for i from 1 to match-count
1529 collect i))
1530 0 number-to-hide)))
1531 (dolist (pos-to-hide match-nums)
1532 (save-excursion
1533 (goto-char body-start)
1534 (re-search-forward org-drill-cloze-regexp
1535 item-end t pos-to-hide)
1536 (org-drill-hide-matched-cloze-text)))))
1537 (org-display-inline-images t)
1538 (org-cycle-hide-drawers 'all)
1539 (prog1 (org-drill-presentation-prompt)
1540 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1541 (org-drill-unhide-clozed-text))))))
1544 (defun org-drill-present-multicloze-hide1 ()
1545 "Hides one of the pieces of text that are marked for cloze deletion,
1546 chosen at random."
1547 (org-drill-present-multicloze-hide-n 1))
1550 (defun org-drill-present-multicloze-hide2 ()
1551 "Hides two of the pieces of text that are marked for cloze deletion,
1552 chosen at random."
1553 (org-drill-present-multicloze-hide-n 2))
1556 ;; (defun org-drill-present-multicloze-hide1 ()
1557 ;; "Hides one of the pieces of text that are marked for cloze deletion,
1558 ;; chosen at random."
1559 ;; (with-hidden-comments
1560 ;; (let ((item-end nil)
1561 ;; (match-count 0)
1562 ;; (body-start (or (cdr (org-get-property-block))
1563 ;; (point))))
1564 ;; (org-drill-hide-all-subheadings-except nil)
1565 ;; (save-excursion
1566 ;; (outline-next-heading)
1567 ;; (setq item-end (point)))
1568 ;; (save-excursion
1569 ;; (goto-char body-start)
1570 ;; (while (re-search-forward org-drill-cloze-regexp item-end t)
1571 ;; (incf match-count)))
1572 ;; (when (plusp match-count)
1573 ;; (save-excursion
1574 ;; (goto-char body-start)
1575 ;; (re-search-forward org-drill-cloze-regexp
1576 ;; item-end t (1+ (random match-count)))
1577 ;; (org-drill-hide-matched-cloze-text)))
1578 ;; (org-display-inline-images t)
1579 ;; (org-cycle-hide-drawers 'all)
1580 ;; (prog1 (org-drill-presentation-prompt)
1581 ;; (org-drill-hide-subheadings-if 'org-drill-entry-p)
1582 ;; (org-drill-unhide-clozed-text)))))
1585 (defun org-drill-present-multicloze-show1 ()
1586 "Similar to `org-drill-present-multicloze-hide1', but hides all
1587 the pieces of text that are marked for cloze deletion, except for one
1588 piece which is chosen at random."
1589 (with-hidden-comments
1590 (with-hidden-cloze-hints
1591 (let ((item-end nil)
1592 (match-count 0)
1593 (body-start (or (cdr (org-get-property-block))
1594 (point))))
1595 (org-drill-hide-all-subheadings-except nil)
1596 (save-excursion
1597 (outline-next-heading)
1598 (setq item-end (point)))
1599 (save-excursion
1600 (goto-char body-start)
1601 (while (re-search-forward org-drill-cloze-regexp item-end t)
1602 (incf match-count)))
1603 (when (plusp match-count)
1604 (let ((match-to-hide (random* match-count)))
1605 (save-excursion
1606 (goto-char body-start)
1607 (dotimes (n match-count)
1608 (re-search-forward org-drill-cloze-regexp
1609 item-end t)
1610 (unless (= n match-to-hide)
1611 (org-drill-hide-matched-cloze-text))))))
1612 (org-display-inline-images t)
1613 (org-cycle-hide-drawers 'all)
1614 (prog1 (org-drill-presentation-prompt)
1615 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1616 (org-drill-unhide-clozed-text))))))
1619 (defun org-drill-present-card-using-text (question &optional answer)
1620 "Present the string QUESTION as the only visible content of the card."
1621 (with-hidden-comments
1622 (with-replaced-entry-text
1623 question
1624 (org-drill-hide-all-subheadings-except nil)
1625 (org-cycle-hide-drawers 'all)
1626 (prog1 (org-drill-presentation-prompt)
1627 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
1630 ;;; The following macro is necessary because `org-save-outline-visibility'
1631 ;;; currently discards the value returned by its body and returns a garbage
1632 ;;; value instead. (as at org mode v 7.5)
1634 (defmacro org-drill-save-visibility (&rest body)
1635 "Store the current visibility state of the org buffer, and restore it
1636 after executing BODY. Return the value of the last expression
1637 in BODY."
1638 (let ((retval (gensym)))
1639 `(let ((,retval nil))
1640 (org-save-outline-visibility t
1641 (setq ,retval
1642 (progn
1643 ,@body)))
1644 ,retval)))
1647 (defun org-drill-entry ()
1648 "Present the current topic for interactive review, as in `org-drill'.
1649 Review will occur regardless of whether the topic is due for review or whether
1650 it meets the definition of a 'review topic' used by `org-drill'.
1652 Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
1653 EDIT if the user chose to exit the drill and edit the current item. Choosing
1654 the latter option leaves the drill session suspended; it can be resumed
1655 later using `org-drill-resume'.
1657 See `org-drill' for more details."
1658 (interactive)
1659 (org-drill-goto-drill-entry-heading)
1660 ;;(unless (org-part-of-drill-entry-p)
1661 ;; (error "Point is not inside a drill entry"))
1662 ;;(unless (org-at-heading-p)
1663 ;; (org-back-to-heading))
1664 (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE"))
1665 (answer-fn 'org-drill-present-default-answer)
1666 (cont nil))
1667 (org-drill-save-visibility
1668 (save-restriction
1669 (org-narrow-to-subtree)
1670 (org-show-subtree)
1671 (org-cycle-hide-drawers 'all)
1673 (let ((presentation-fn (cdr (assoc card-type org-drill-card-type-alist))))
1674 (if (listp presentation-fn)
1675 (psetq answer-fn (or (second presentation-fn)
1676 'org-drill-present-default-answer)
1677 presentation-fn (first presentation-fn)))
1678 (cond
1679 (presentation-fn
1680 (setq cont (funcall presentation-fn)))
1682 (error "Unknown card type: '%s'" card-type))))
1684 (cond
1685 ((not cont)
1686 (message "Quit")
1687 nil)
1688 ((eql cont 'edit)
1689 'edit)
1690 ((eql cont 'skip)
1691 'skip)
1693 (save-excursion
1694 (funcall answer-fn
1695 (lambda () (org-drill-reschedule))))))))))
1698 (defun org-drill-entries-pending-p ()
1699 (or *org-drill-again-entries*
1700 (and (not (org-drill-maximum-item-count-reached-p))
1701 (not (org-drill-maximum-duration-reached-p))
1702 (or *org-drill-new-entries*
1703 *org-drill-failed-entries*
1704 *org-drill-young-mature-entries*
1705 *org-drill-old-mature-entries*
1706 *org-drill-overdue-entries*
1707 *org-drill-again-entries*))))
1710 (defun org-drill-pending-entry-count ()
1711 (+ (length *org-drill-new-entries*)
1712 (length *org-drill-failed-entries*)
1713 (length *org-drill-young-mature-entries*)
1714 (length *org-drill-old-mature-entries*)
1715 (length *org-drill-overdue-entries*)
1716 (length *org-drill-again-entries*)))
1719 (defun org-drill-maximum-duration-reached-p ()
1720 "Returns true if the current drill session has continued past its
1721 maximum duration."
1722 (and org-drill-maximum-duration
1723 *org-drill-start-time*
1724 (> (- (float-time (current-time)) *org-drill-start-time*)
1725 (* org-drill-maximum-duration 60))))
1728 (defun org-drill-maximum-item-count-reached-p ()
1729 "Returns true if the current drill session has reached the
1730 maximum number of items."
1731 (and org-drill-maximum-items-per-session
1732 (>= (length *org-drill-done-entries*)
1733 org-drill-maximum-items-per-session)))
1736 (defun org-drill-pop-next-pending-entry ()
1737 (block org-drill-pop-next-pending-entry
1738 (let ((m nil))
1739 (while (or (null m)
1740 (not (org-drill-entry-p m)))
1741 (setq
1743 (cond
1744 ;; First priority is items we failed in a prior session.
1745 ((and *org-drill-failed-entries*
1746 (not (org-drill-maximum-item-count-reached-p))
1747 (not (org-drill-maximum-duration-reached-p)))
1748 (setq *org-drill-current-entry-schedule-type* 'failed)
1749 (pop-random *org-drill-failed-entries*))
1750 ;; Next priority is overdue items.
1751 ((and *org-drill-overdue-entries*
1752 (not (org-drill-maximum-item-count-reached-p))
1753 (not (org-drill-maximum-duration-reached-p)))
1754 ;; We use `pop', not `pop-random', because we have already
1755 ;; sorted overdue items into a random order which takes
1756 ;; number of days overdue into account.
1757 (setq *org-drill-current-entry-schedule-type* 'overdue)
1758 (pop *org-drill-overdue-entries*))
1759 ;; Next priority is 'young' items.
1760 ((and *org-drill-young-mature-entries*
1761 (not (org-drill-maximum-item-count-reached-p))
1762 (not (org-drill-maximum-duration-reached-p)))
1763 (setq *org-drill-current-entry-schedule-type* 'young)
1764 (pop-random *org-drill-young-mature-entries*))
1765 ;; Next priority is newly added items, and older entries.
1766 ;; We pool these into a single group.
1767 ((and (or *org-drill-new-entries*
1768 *org-drill-old-mature-entries*)
1769 (not (org-drill-maximum-item-count-reached-p))
1770 (not (org-drill-maximum-duration-reached-p)))
1771 (cond
1772 ((< (random* (+ (length *org-drill-new-entries*)
1773 (length *org-drill-old-mature-entries*)))
1774 (length *org-drill-new-entries*))
1775 (setq *org-drill-current-entry-schedule-type* 'new)
1776 (pop-random *org-drill-new-entries*))
1778 (setq *org-drill-current-entry-schedule-type* 'old)
1779 (pop-random *org-drill-old-mature-entries*))))
1780 ;; After all the above are done, last priority is items
1781 ;; that were failed earlier THIS SESSION.
1782 (*org-drill-again-entries*
1783 (setq *org-drill-current-entry-schedule-type* 'failed)
1784 (pop *org-drill-again-entries*))
1785 (t ; nothing left -- return nil
1786 (return-from org-drill-pop-next-pending-entry nil)))))
1787 m)))
1790 (defun org-drill-entries (&optional resuming-p)
1791 "Returns nil, t, or a list of markers representing entries that were
1792 'failed' and need to be presented again before the session ends.
1794 RESUMING-P is true if we are resuming a suspended drill session."
1795 (block org-drill-entries
1796 (while (org-drill-entries-pending-p)
1797 (let ((m (cond
1798 ((or (not resuming-p)
1799 (null *org-drill-current-item*)
1800 (not (org-drill-entry-p *org-drill-current-item*)))
1801 (org-drill-pop-next-pending-entry))
1802 (t ; resuming a suspended session.
1803 (setq resuming-p nil)
1804 *org-drill-current-item*))))
1805 (setq *org-drill-current-item* m)
1806 (unless m
1807 (error "Unexpectedly ran out of pending drill items"))
1808 (save-excursion
1809 (org-drill-goto-entry m)
1810 (setq result (org-drill-entry))
1811 (cond
1812 ((null result)
1813 (message "Quit")
1814 (setq end-pos :quit)
1815 (return-from org-drill-entries nil))
1816 ((eql result 'edit)
1817 (setq end-pos (point-marker))
1818 (return-from org-drill-entries nil))
1819 ((eql result 'skip)
1820 nil) ; skip this item
1822 (cond
1823 ((<= result org-drill-failure-quality)
1824 (if *org-drill-again-entries*
1825 (setq *org-drill-again-entries*
1826 (shuffle-list *org-drill-again-entries*)))
1827 (push-end m *org-drill-again-entries*))
1829 (push m *org-drill-done-entries*))))))))))
1833 (defun org-drill-final-report ()
1834 (let ((pass-percent
1835 (round (* 100 (count-if (lambda (qual)
1836 (> qual org-drill-failure-quality))
1837 *org-drill-session-qualities*))
1838 (max 1 (length *org-drill-session-qualities*))))
1839 (prompt nil))
1840 (setq prompt
1841 (format
1842 "%d items reviewed. Session duration %s.
1843 %d/%d items awaiting review (%s, %s, %s, %s, %s).
1845 Recall of reviewed items:
1846 Excellent (5): %3d%% | Near miss (2): %3d%%
1847 Good (4): %3d%% | Failure (1): %3d%%
1848 Hard (3): %3d%% | Abject failure (0): %3d%%
1850 You successfully recalled %d%% of reviewed items (quality > %s)
1851 Tomorrow, %d more items will become due for review.
1852 Session finished. Press a key to continue..."
1853 (length *org-drill-done-entries*)
1854 (format-seconds "%h:%.2m:%.2s"
1855 (- (float-time (current-time)) *org-drill-start-time*))
1856 (org-drill-pending-entry-count)
1857 (+ (org-drill-pending-entry-count)
1858 *org-drill-dormant-entry-count*)
1859 (propertize
1860 (format "%d failed"
1861 (+ (length *org-drill-failed-entries*)
1862 (length *org-drill-again-entries*)))
1863 'face `(:foreground ,org-drill-failed-count-color))
1864 (propertize
1865 (format "%d overdue"
1866 (length *org-drill-overdue-entries*))
1867 'face `(:foreground ,org-drill-failed-count-color))
1868 (propertize
1869 (format "%d new"
1870 (length *org-drill-new-entries*))
1871 'face `(:foreground ,org-drill-new-count-color))
1872 (propertize
1873 (format "%d young"
1874 (length *org-drill-young-mature-entries*))
1875 'face `(:foreground ,org-drill-mature-count-color))
1876 (propertize
1877 (format "%d old"
1878 (length *org-drill-old-mature-entries*))
1879 'face `(:foreground ,org-drill-mature-count-color))
1880 (round (* 100 (count 5 *org-drill-session-qualities*))
1881 (max 1 (length *org-drill-session-qualities*)))
1882 (round (* 100 (count 2 *org-drill-session-qualities*))
1883 (max 1 (length *org-drill-session-qualities*)))
1884 (round (* 100 (count 4 *org-drill-session-qualities*))
1885 (max 1 (length *org-drill-session-qualities*)))
1886 (round (* 100 (count 1 *org-drill-session-qualities*))
1887 (max 1 (length *org-drill-session-qualities*)))
1888 (round (* 100 (count 3 *org-drill-session-qualities*))
1889 (max 1 (length *org-drill-session-qualities*)))
1890 (round (* 100 (count 0 *org-drill-session-qualities*))
1891 (max 1 (length *org-drill-session-qualities*)))
1892 pass-percent
1893 org-drill-failure-quality
1894 *org-drill-due-tomorrow-count*
1897 (while (not (input-pending-p))
1898 (message "%s" prompt)
1899 (sit-for 0.5))
1900 (read-char-exclusive)
1902 (if (< pass-percent (- 100 org-drill-forgetting-index))
1903 (read-char-exclusive
1904 (format
1906 You failed %d%% of the items you reviewed during this session.
1907 %d (%d%%) of all items scanned were overdue.
1909 Are you keeping up with your items, and reviewing them
1910 when they are scheduled? If so, you may want to consider
1911 lowering the value of `org-drill-learn-fraction' slightly in
1912 order to make items appear more frequently over time."
1913 (propertize "WARNING!" 'face 'org-warning)
1914 (- 100 pass-percent)
1915 *org-drill-overdue-entry-count*
1916 (round (* 100 *org-drill-overdue-entry-count*)
1917 (+ *org-drill-dormant-entry-count*
1918 *org-drill-due-entry-count*)))
1919 ))))
1923 (defun org-drill-free-all-markers ()
1924 (dolist (m (append *org-drill-done-entries*
1925 *org-drill-new-entries*
1926 *org-drill-failed-entries*
1927 *org-drill-again-entries*
1928 *org-drill-overdue-entries*
1929 *org-drill-young-mature-entries*
1930 *org-drill-old-mature-entries*))
1931 (free-marker m)))
1934 (defun org-drill-order-overdue-entries (overdue-data)
1935 (setq *org-drill-overdue-entries*
1936 (mapcar 'car
1937 (sort (shuffle-list overdue-data)
1938 (lambda (a b) (> (cdr a) (cdr b)))))))
1941 (defun org-drill (&optional scope resume-p)
1942 "Begin an interactive 'drill session'. The user is asked to
1943 review a series of topics (headers). Each topic is initially
1944 presented as a 'question', often with part of the topic content
1945 hidden. The user attempts to recall the hidden information or
1946 answer the question, then presses a key to reveal the answer. The
1947 user then rates his or her recall or performance on that
1948 topic. This rating information is used to reschedule the topic
1949 for future review.
1951 Org-drill proceeds by:
1953 - Finding all topics (headings) in SCOPE which have either been
1954 used and rescheduled before, or which have a tag that matches
1955 `org-drill-question-tag'.
1957 - All matching topics which are either unscheduled, or are
1958 scheduled for the current date or a date in the past, are
1959 considered to be candidates for the drill session.
1961 - If `org-drill-maximum-items-per-session' is set, a random
1962 subset of these topics is presented. Otherwise, all of the
1963 eligible topics will be presented.
1965 SCOPE determines the scope in which to search for
1966 questions. It is passed to `org-map-entries', and can be any of:
1968 nil The current buffer, respecting the restriction if any.
1969 This is the default.
1970 tree The subtree started with the entry at point
1971 file The current buffer, without restriction
1972 file-with-archives
1973 The current buffer, and any archives associated with it
1974 agenda All agenda files
1975 agenda-with-archives
1976 All agenda files with any archive files associated with them
1977 (file1 file2 ...)
1978 If this is a list, all files in the list will be scanned.
1980 If RESUME-P is non-nil, resume a suspended drill session rather
1981 than starting a new one."
1983 (interactive)
1984 (let ((end-pos nil)
1985 (overdue-data nil)
1986 (cnt 0))
1987 (block org-drill
1988 (unless resume-p
1989 (org-drill-free-all-markers)
1990 (setq *org-drill-current-item* nil
1991 *org-drill-done-entries* nil
1992 *org-drill-dormant-entry-count* 0
1993 *org-drill-due-entry-count* 0
1994 *org-drill-due-tomorrow-count* 0
1995 *org-drill-overdue-entry-count* 0
1996 *org-drill-new-entries* nil
1997 *org-drill-overdue-entries* nil
1998 *org-drill-young-mature-entries* nil
1999 *org-drill-old-mature-entries* nil
2000 *org-drill-failed-entries* nil
2001 *org-drill-again-entries* nil)
2002 (setq *org-drill-session-qualities* nil)
2003 (setq *org-drill-start-time* (float-time (current-time))))
2004 (setq *random-state* (make-random-state t)) ; reseed RNG
2005 (unwind-protect
2006 (save-excursion
2007 (unless resume-p
2008 (let ((org-trust-scanner-tags t)
2009 (warned-about-id-creation nil))
2010 (org-map-drill-entries
2011 (lambda ()
2012 (when (zerop (% (incf cnt) 50))
2013 (message "Processing drill items: %4d%s"
2014 (+ (length *org-drill-new-entries*)
2015 (length *org-drill-overdue-entries*)
2016 (length *org-drill-young-mature-entries*)
2017 (length *org-drill-old-mature-entries*)
2018 (length *org-drill-failed-entries*))
2019 (make-string (ceiling cnt 50) ?.)))
2020 (cond
2021 ((not (org-drill-entry-p))
2022 nil) ; skip
2024 (when (and (not warned-about-id-creation)
2025 (null (org-id-get)))
2026 (message (concat "Creating unique IDs for items "
2027 "(slow, but only happens once)"))
2028 (sit-for 0.5)
2029 (setq warned-about-id-creation t))
2030 (org-id-get-create) ; ensure drill entry has unique ID
2031 (let ((due (org-drill-entry-days-overdue))
2032 (last-int (org-drill-entry-last-interval 1)))
2033 (cond
2034 ((org-drill-entry-empty-p)
2035 nil) ; skip -- item body is empty
2036 ((or (null due) ; unscheduled - usually a skipped leech
2037 (minusp due)) ; scheduled in the future
2038 (incf *org-drill-dormant-entry-count*)
2039 (if (eq -1 due)
2040 (incf *org-drill-due-tomorrow-count*)))
2041 ((org-drill-entry-new-p)
2042 (push (point-marker) *org-drill-new-entries*))
2043 ((<= (org-drill-entry-last-quality 9999)
2044 org-drill-failure-quality)
2045 ;; Mature entries that were failed last time are
2046 ;; FAILED, regardless of how young, old or overdue
2047 ;; they are.
2048 (push (point-marker) *org-drill-failed-entries*))
2049 ((org-drill-entry-overdue-p due last-int)
2050 ;; Overdue status overrides young versus old
2051 ;; distinction.
2052 ;; Store marker + due, for sorting of overdue entries
2053 (push (cons (point-marker) due) overdue-data))
2054 ((<= (org-drill-entry-last-interval 9999)
2055 org-drill-days-before-old)
2056 ;; Item is 'young'.
2057 (push (point-marker)
2058 *org-drill-young-mature-entries*))
2060 (push (point-marker)
2061 *org-drill-old-mature-entries*)))))))
2062 scope)
2063 ;; Order 'overdue' items so that the most overdue will tend to
2064 ;; come up for review first, while keeping exact order random
2065 (org-drill-order-overdue-entries overdue-data)
2066 (setq *org-drill-overdue-entry-count*
2067 (length *org-drill-overdue-entries*))))
2068 (setq *org-drill-due-entry-count* (org-drill-pending-entry-count))
2069 (cond
2070 ((and (null *org-drill-new-entries*)
2071 (null *org-drill-failed-entries*)
2072 (null *org-drill-overdue-entries*)
2073 (null *org-drill-young-mature-entries*)
2074 (null *org-drill-old-mature-entries*))
2075 (message "I did not find any pending drill items."))
2077 (org-drill-entries resume-p)
2078 (message "Drill session finished!"))))
2079 (progn
2080 (unless end-pos
2081 (org-drill-free-all-markers)))))
2082 (cond
2083 (end-pos
2084 (when (markerp end-pos)
2085 (org-drill-goto-entry end-pos))
2086 (message
2087 "You can continue the drill session with `M-x org-drill-resume'."))
2089 (org-drill-final-report)
2090 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
2091 (org-drill-save-optimal-factor-matrix))
2092 ))))
2096 (defun org-drill-save-optimal-factor-matrix ()
2097 (message "Saving optimal factor matrix...")
2098 (customize-save-variable 'org-drill-optimal-factor-matrix
2099 org-drill-optimal-factor-matrix))
2102 (defun org-drill-cram (&optional scope)
2103 "Run an interactive drill session in 'cram mode'. In cram mode,
2104 all drill items are considered to be due for review, unless they
2105 have been reviewed within the last `org-drill-cram-hours'
2106 hours."
2107 (interactive)
2108 (let ((*org-drill-cram-mode* t))
2109 (org-drill scope)))
2112 (defun org-drill-tree ()
2113 "Run an interactive drill session using drill items within the
2114 subtree at point."
2115 (interactive)
2116 (org-drill 'tree))
2119 (defun org-drill-resume ()
2120 "Resume a suspended drill session. Sessions are suspended by
2121 exiting them with the `edit' option."
2122 (interactive)
2123 (org-drill nil t))
2126 (defun org-drill-strip-entry-data ()
2127 (dolist (prop org-drill-scheduling-properties)
2128 (org-delete-property prop))
2129 (org-schedule t))
2132 (defun org-drill-strip-all-data (&optional scope)
2133 "Delete scheduling data from every drill entry in scope. This
2134 function may be useful if you want to give your collection of
2135 entries to someone else. Scope defaults to the current buffer,
2136 and is specified by the argument SCOPE, which accepts the same
2137 values as `org-drill'."
2138 (interactive)
2139 (when (yes-or-no-p
2140 "Delete scheduling data from ALL items in scope: are you sure?")
2141 (cond
2142 ((null scope)
2143 ;; Scope is the current buffer. This means we can use
2144 ;; `org-delete-property-globally', which is faster.
2145 (dolist (prop org-drill-scheduling-properties)
2146 (org-delete-property-globally prop))
2147 (org-map-drill-entries (lambda () (org-schedule t)) scope))
2149 (org-map-drill-entries 'org-drill-strip-entry-data scope)))
2150 (message "Done.")))
2154 (add-hook 'org-mode-hook
2155 (lambda ()
2156 (when org-drill-use-visible-cloze-face-p
2157 (font-lock-add-keywords 'org-mode
2158 org-drill-cloze-keywords
2159 nil))))
2162 ;;; Synching card collections =================================================
2165 (defvar *org-drill-dest-id-table* (make-hash-table :test 'equal))
2168 (defun org-drill-copy-entry-to-other-buffer (dest &optional path)
2169 "Copy the subtree at point to the buffer DEST. The copy will receive
2170 the tag 'imported'."
2171 (block org-drill-copy-entry-to-other-buffer
2172 (save-excursion
2173 (let ((src (current-buffer))
2174 (m nil))
2175 (flet ((paste-tree-here (&optional level)
2176 (org-paste-subtree level)
2177 (org-drill-strip-entry-data)
2178 (org-toggle-tag "imported" 'on)
2179 (org-map-drill-entries
2180 (lambda ()
2181 (let ((id (org-id-get)))
2182 (org-drill-strip-entry-data)
2183 (unless (gethash id *org-drill-dest-id-table*)
2184 (puthash id (point-marker)
2185 *org-drill-dest-id-table*))))
2186 'tree)))
2187 (unless path
2188 (setq path (org-get-outline-path)))
2189 (org-copy-subtree)
2190 (switch-to-buffer dest)
2191 (setq m
2192 (condition-case nil
2193 (org-find-olp path t)
2194 (error ; path does not exist in DEST
2195 (return-from org-drill-copy-entry-to-other-buffer
2196 (cond
2197 ((cdr path)
2198 (org-drill-copy-entry-to-other-buffer
2199 dest (butlast path)))
2201 ;; We've looked all the way up the path
2202 ;; Default to appending to the end of DEST
2203 (goto-char (point-max))
2204 (newline)
2205 (paste-tree-here)))))))
2206 (goto-char m)
2207 (outline-next-heading)
2208 (newline)
2209 (forward-line -1)
2210 (paste-tree-here (1+ (or (org-current-level) 0)))
2211 )))))
2215 (defun org-drill-merge-buffers (src &optional dest)
2216 "SRC and DEST are two org mode buffers containing drill items.
2217 For each drill item in DEST that shares an ID with an item in SRC,
2218 overwrite scheduling data in DEST with data taken from the item in SRC.
2219 This is intended for use when two people are sharing a set of drill items,
2220 one person has made some updates to the item set, and the other person
2221 wants to migrate to the updated set without losing their scheduling data."
2222 ;; In future could look at what to do if we find an item in SRC whose ID
2223 ;; is not present in DEST -- copy the whole item to DEST?
2224 ;; org-copy-subtree --> org-paste-subtree
2225 ;; could try to put it "near" the closest marker
2226 (interactive "bImport scheduling info from which buffer?")
2227 (unless dest
2228 (setq dest (current-buffer)))
2229 (setq src (get-buffer src)
2230 dest (get-buffer dest))
2231 (when (yes-or-no-p
2232 (format
2233 (concat "About to overwrite all scheduling data for drill items in `%s' "
2234 "with information taken from matching items in `%s'. Proceed? ")
2235 (buffer-name dest) (buffer-name src)))
2236 ;; Compile list of all IDs in the destination buffer.
2237 (clrhash *org-drill-dest-id-table*)
2238 (with-current-buffer dest
2239 (org-map-drill-entries
2240 (lambda ()
2241 (let ((this-id (org-id-get)))
2242 (when this-id
2243 (puthash this-id (point-marker) *org-drill-dest-id-table*))))))
2244 ;; Look through all entries in source buffer.
2245 (with-current-buffer src
2246 (org-map-drill-entries
2247 (lambda ()
2248 (let ((id (org-id-get))
2249 (last-quality nil) (last-reviewed nil)
2250 (scheduled-time nil))
2251 (cond
2252 ((or (null id)
2253 (not (org-drill-entry-p)))
2254 nil)
2255 ((gethash id *org-drill-dest-id-table*)
2256 ;; This entry matches an entry in dest. Retrieve all its
2257 ;; scheduling data, then go to the matching location in dest
2258 ;; and write the data.
2259 (let ((marker (gethash id *org-drill-dest-id-table*)))
2260 (destructuring-bind (last-interval repetitions failures
2261 total-repeats meanq ease)
2262 (org-drill-get-item-data)
2263 (setq last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")
2264 last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")
2265 scheduled-time (org-get-scheduled-time (point)))
2266 (save-excursion
2267 ;; go to matching entry in destination buffer
2268 (switch-to-buffer (marker-buffer marker))
2269 (goto-char marker)
2270 (org-drill-strip-entry-data)
2271 (unless (zerop total-repeats)
2272 (org-drill-store-item-data last-interval repetitions failures
2273 total-repeats meanq ease)
2274 (org-set-property "LAST_QUALITY" last-quality)
2275 (org-set-property "LAST_REVIEWED" last-reviewed)
2276 (if scheduled-time
2277 (org-schedule nil scheduled-time)))))
2278 (free-marker marker)))
2280 ;; item in SRC has ID, but no matching ID in DEST.
2281 ;; It must be a new item that does not exist in DEST.
2282 ;; Copy the entire item to the *end* of DEST.
2283 (org-drill-copy-entry-to-other-buffer dest)))))))))
2287 ;;; Card types for learning languages =========================================
2289 ;;; Get spell-number.el from:
2290 ;;; http://www.emacswiki.org/emacs/spell-number.el
2291 (autoload 'spelln-integer-in-words "spell-number")
2294 ;;; `conjugate' card type =====================================================
2295 ;;; See spanish.org for usage
2297 (defvar org-drill-verb-tense-alist
2298 '(("present" "tomato")
2299 ("simple present" "tomato")
2300 ("present indicative" "tomato")
2301 ;; past tenses
2302 ("past" "purple")
2303 ("simple past" "purple")
2304 ("preterite" "purple")
2305 ("imperfect" "darkturquoise")
2306 ("present perfect" "royalblue")
2307 ;; future tenses
2308 ("future" "green"))
2309 "Alist where each entry has the form (TENSE COLOUR), where
2310 TENSE is a string naming a tense in which verbs can be
2311 conjugated, and COLOUR is a string specifying a foreground colour
2312 which will be used by `org-drill-present-verb-conjugation' and
2313 `org-drill-show-answer-verb-conjugation' to fontify the verb and
2314 the name of the tense.")
2317 (defun org-drill-get-verb-conjugation-info ()
2318 "Auxiliary function used by `org-drill-present-verb-conjugation' and
2319 `org-drill-show-answer-verb-conjugation'."
2320 (let ((infinitive (org-entry-get (point) "VERB_INFINITIVE" t))
2321 (translation (org-entry-get (point) "VERB_TRANSLATION" t))
2322 (tense (org-entry-get (point) "VERB_TENSE" nil))
2323 (highlight-face nil))
2324 (unless (and infinitive translation tense)
2325 (error "Missing information for verb conjugation card (%s, %s, %s) at %s"
2326 infinitive translation tense (point)))
2327 (setq tense (downcase (car (read-from-string tense)))
2328 infinitive (car (read-from-string infinitive))
2329 translation (car (read-from-string translation)))
2330 (setq highlight-face
2331 (list :foreground
2332 (or (second (assoc-string tense org-drill-verb-tense-alist t))
2333 "red")))
2334 (setq infinitive (propertize infinitive 'face highlight-face))
2335 (setq translation (propertize translation 'face highlight-face))
2336 (setq tense (propertize tense 'face highlight-face))
2337 (list infinitive translation tense)))
2340 (defun org-drill-present-verb-conjugation ()
2341 "Present a drill entry whose card type is 'conjugate'."
2342 (destructuring-bind (infinitive translation tense)
2343 (org-drill-get-verb-conjugation-info)
2344 (org-drill-present-card-using-text
2345 (cond
2346 ((zerop (random* 2))
2347 (format "\nTranslate the verb\n\n%s\n\nand conjugate for the %s tense.\n\n"
2348 infinitive tense))
2350 (format "\nGive the verb that means\n\n%s\n\nand conjugate for the %s tense.\n\n"
2351 translation tense))))))
2354 (defun org-drill-show-answer-verb-conjugation (reschedule-fn)
2355 "Show the answer for a drill item whose card type is 'conjugate'.
2356 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
2357 returns its return value."
2358 (destructuring-bind (infinitive translation tense)
2359 (org-drill-get-verb-conjugation-info)
2360 (with-replaced-entry-heading
2361 (format "%s tense of %s ==> %s\n\n"
2362 (capitalize tense)
2363 infinitive translation)
2364 (funcall reschedule-fn))))
2367 ;;; `translate_number' card type ==============================================
2368 ;;; See spanish.org for usage
2370 (defvar *drilled-number* 0)
2371 (defvar *drilled-number-direction* 'to-english)
2373 (defun org-drill-present-translate-number ()
2374 (let ((num-min (read (org-entry-get (point) "DRILL_NUMBER_MIN")))
2375 (num-max (read (org-entry-get (point) "DRILL_NUMBER_MAX")))
2376 (language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
2377 (highlight-face 'font-lock-warning-face))
2378 (cond
2379 ((not (fboundp 'spelln-integer-in-words))
2380 (message "`spell-number.el' not loaded, skipping 'translate_number' card...")
2381 (sit-for 0.5)
2382 'skip)
2383 ((not (and (numberp num-min) (numberp num-max) language))
2384 (error "Missing language or minimum or maximum numbers for number card"))
2386 (if (> num-min num-max)
2387 (psetf num-min num-max
2388 num-max num-min))
2389 (setq *drilled-number*
2390 (+ num-min (random* (abs (1+ (- num-max num-min))))))
2391 (setq *drilled-number-direction*
2392 (if (zerop (random* 2)) 'from-english 'to-english))
2393 (org-drill-present-card-using-text
2394 (if (eql 'to-english *drilled-number-direction*)
2395 (format "\nTranslate into English:\n\n%s\n"
2396 (let ((spelln-language language))
2397 (propertize
2398 (spelln-integer-in-words *drilled-number*)
2399 'face highlight-face)))
2400 (format "\nTranslate into %s:\n\n%s\n"
2401 (capitalize (format "%s" language))
2402 (let ((spelln-language 'english-gb))
2403 (propertize
2404 (spelln-integer-in-words *drilled-number*)
2405 'face highlight-face)))))))))
2408 (defun org-drill-show-answer-translate-number (reschedule-fn)
2409 (let* ((language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
2410 (highlight-face 'font-lock-warning-face)
2411 (non-english
2412 (let ((spelln-language language))
2413 (propertize (spelln-integer-in-words *drilled-number*)
2414 'face highlight-face)))
2415 (english
2416 (let ((spelln-language 'english-gb))
2417 (propertize (spelln-integer-in-words *drilled-number*)
2418 'face 'highlight-face))))
2419 (with-replaced-entry-text
2420 (cond
2421 ((eql 'to-english *drilled-number-direction*)
2422 (format "\nThe English translation of %s is:\n\n%s\n"
2423 non-english english))
2425 (format "\nThe %s translation of %s is:\n\n%s\n"
2426 (capitalize (format "%s" language))
2427 english non-english)))
2428 (funcall reschedule-fn))))
2431 ;;; `spanish_verb' card type ==================================================
2432 ;;; Not very interesting, but included to demonstrate how a presentation
2433 ;;; function can manipulate which subheading are hidden versus shown.
2436 (defun org-drill-present-spanish-verb ()
2437 (let ((prompt nil)
2438 (reveal-headings nil))
2439 (with-hidden-comments
2440 (with-hidden-cloze-hints
2441 (with-hidden-cloze-text
2442 (case (random* 6)
2444 (org-drill-hide-all-subheadings-except '("Infinitive"))
2445 (setq prompt
2446 (concat "Translate this Spanish verb, and conjugate it "
2447 "for the *present* tense.")
2448 reveal-headings '("English" "Present Tense" "Notes")))
2450 (org-drill-hide-all-subheadings-except '("English"))
2451 (setq prompt (concat "For the *present* tense, conjugate the "
2452 "Spanish translation of this English verb.")
2453 reveal-headings '("Infinitive" "Present Tense" "Notes")))
2455 (org-drill-hide-all-subheadings-except '("Infinitive"))
2456 (setq prompt (concat "Translate this Spanish verb, and "
2457 "conjugate it for the *past* tense.")
2458 reveal-headings '("English" "Past Tense" "Notes")))
2460 (org-drill-hide-all-subheadings-except '("English"))
2461 (setq prompt (concat "For the *past* tense, conjugate the "
2462 "Spanish translation of this English verb.")
2463 reveal-headings '("Infinitive" "Past Tense" "Notes")))
2465 (org-drill-hide-all-subheadings-except '("Infinitive"))
2466 (setq prompt (concat "Translate this Spanish verb, and "
2467 "conjugate it for the *future perfect* tense.")
2468 reveal-headings '("English" "Future Perfect Tense" "Notes")))
2470 (org-drill-hide-all-subheadings-except '("English"))
2471 (setq prompt (concat "For the *future perfect* tense, conjugate the "
2472 "Spanish translation of this English verb.")
2473 reveal-headings '("Infinitive" "Future Perfect Tense" "Notes"))))
2474 (org-cycle-hide-drawers 'all)
2475 (prog1 (org-drill-presentation-prompt)
2476 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
2479 (provide 'org-drill)