Revert "Backwards compatibility: don't defalias special forms"
[org-mode.git] / contrib / lisp / org-drill.el
blob2ffc201940cc45ec3e99499443964e2b8288bada
1 ;;; -*- coding: utf-8-unix -*-
2 ;;; org-drill.el - Self-testing using spaced repetition
3 ;;;
4 ;;; Author: Paul Sexton <eeeickythump@gmail.com>
5 ;;; Version: 2.3.6
6 ;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
7 ;;;
8 ;;;
9 ;;; Synopsis
10 ;;; ========
11 ;;;
12 ;;; Uses the SuperMemo spaced repetition algorithms to conduct interactive
13 ;;; "drill sessions", where the material to be remembered is presented to the
14 ;;; student in random order. The student rates his or her recall of each item,
15 ;;; and this information is used to schedule the item for later revision.
16 ;;;
17 ;;; Each drill session can be restricted to topics in the current buffer
18 ;;; (default), one or several files, all agenda files, or a subtree. A single
19 ;;; topic can also be drilled.
20 ;;;
21 ;;; Different "card types" can be defined, which present their information to
22 ;;; the student in different ways.
23 ;;;
24 ;;; See the file README.org for more detailed documentation.
27 (eval-when-compile (require 'cl))
28 (eval-when-compile (require 'hi-lock))
29 (require 'org)
30 (require 'org-id)
31 (require 'org-learn)
34 (defgroup org-drill nil
35 "Options concerning interactive drill sessions in Org mode (org-drill)."
36 :tag "Org-Drill"
37 :group 'org-link)
41 (defcustom org-drill-question-tag
42 "drill"
43 "Tag which topics must possess in order to be identified as review topics
44 by `org-drill'."
45 :group 'org-drill
46 :type 'string)
49 (defcustom org-drill-maximum-items-per-session
51 "Each drill session will present at most this many topics for review.
52 Nil means unlimited."
53 :group 'org-drill
54 :type '(choice integer (const nil)))
58 (defcustom org-drill-maximum-duration
60 "Maximum duration of a drill session, in minutes.
61 Nil means unlimited."
62 :group 'org-drill
63 :type '(choice integer (const nil)))
66 (defcustom org-drill-failure-quality
68 "If the quality of recall for an item is this number or lower,
69 it is regarded as an unambiguous failure, and the repetition
70 interval for the card is reset to 0 days. If the quality is higher
71 than this number, it is regarded as successfully recalled, but the
72 time interval to the next repetition will be lowered if the quality
73 was near to a fail.
75 By default this is 2, for SuperMemo-like behaviour. For
76 Mnemosyne-like behaviour, set it to 1. Other values are not
77 really sensible."
78 :group 'org-drill
79 :type '(choice (const 2) (const 1)))
82 (defcustom org-drill-forgetting-index
84 "What percentage of items do you consider it is 'acceptable' to
85 forget each drill session? The default is 10%. A warning message
86 is displayed at the end of the session if the percentage forgotten
87 climbs above this number."
88 :group 'org-drill
89 :type 'integer)
92 (defcustom org-drill-leech-failure-threshold
94 "If an item is forgotten more than this many times, it is tagged
95 as a 'leech' item."
96 :group 'org-drill
97 :type '(choice integer (const nil)))
100 (defcustom org-drill-leech-method
101 'skip
102 "How should 'leech items' be handled during drill sessions?
103 Possible values:
104 - nil :: Leech items are treated the same as normal items.
105 - skip :: Leech items are not included in drill sessions.
106 - warn :: Leech items are still included in drill sessions,
107 but a warning message is printed when each leech item is
108 presented."
109 :group 'org-drill
110 :type '(choice (const 'warn) (const 'skip) (const nil)))
113 (defface org-drill-visible-cloze-face
114 '((t (:foreground "darkseagreen")))
115 "The face used to hide the contents of cloze phrases."
116 :group 'org-drill)
119 (defface org-drill-visible-cloze-hint-face
120 '((t (:foreground "dark slate blue")))
121 "The face used to hide the contents of cloze phrases."
122 :group 'org-drill)
125 (defface org-drill-hidden-cloze-face
126 '((t (:foreground "deep sky blue" :background "blue")))
127 "The face used to hide the contents of cloze phrases."
128 :group 'org-drill)
131 (defcustom org-drill-use-visible-cloze-face-p
133 "Use a special face to highlight cloze-deleted text in org mode
134 buffers?"
135 :group 'org-drill
136 :type 'boolean)
139 (defcustom org-drill-hide-item-headings-p
141 "Conceal the contents of the main heading of each item during drill
142 sessions? You may want to enable this behaviour if item headings or tags
143 contain information that could 'give away' the answer."
144 :group 'org-drill
145 :type 'boolean)
148 (defcustom org-drill-new-count-color
149 "royal blue"
150 "Foreground colour used to display the count of remaining new items
151 during a drill session."
152 :group 'org-drill
153 :type 'color)
155 (defcustom org-drill-mature-count-color
156 "green"
157 "Foreground colour used to display the count of remaining mature items
158 during a drill session. Mature items are due for review, but are not new."
159 :group 'org-drill
160 :type 'color)
162 (defcustom org-drill-failed-count-color
163 "red"
164 "Foreground colour used to display the count of remaining failed items
165 during a drill session."
166 :group 'org-drill
167 :type 'color)
169 (defcustom org-drill-done-count-color
170 "sienna"
171 "Foreground colour used to display the count of reviewed items
172 during a drill session."
173 :group 'org-drill
174 :type 'color)
177 (setplist 'org-drill-cloze-overlay-defaults
178 '(display "[...]"
179 face org-drill-hidden-cloze-face
180 window t))
182 (setplist 'org-drill-hidden-text-overlay
183 '(invisible t))
185 (setplist 'org-drill-replaced-text-overlay
186 '(display "Replaced text"
187 face default
188 window t))
191 (defvar org-drill-cloze-regexp
192 ;; ver 1 "[^][]\\(\\[[^][][^]]*\\]\\)"
193 ;; ver 2 "\\(\\[.*?\\]\\|^[^[[:cntrl:]]*?\\]\\|\\[.*?$\\)"
194 ;; ver 3! "\\(\\[.*?\\]\\|\\[.*?[[:cntrl:]]+.*?\\]\\)"
195 "\\(\\[[[:cntrl:][:graph:][:space:]]*?\\)\\(\\||.+?\\)\\(\\]\\)")
198 (defvar org-drill-cloze-keywords
199 `((,org-drill-cloze-regexp
200 (1 'org-drill-visible-cloze-face nil)
201 (2 'org-drill-visible-cloze-hint-face t)
202 (3 'org-drill-visible-cloze-face nil)
206 (defcustom org-drill-card-type-alist
207 '((nil . org-drill-present-simple-card)
208 ("simple" . org-drill-present-simple-card)
209 ("twosided" . org-drill-present-two-sided-card)
210 ("multisided" . org-drill-present-multi-sided-card)
211 ("hide1cloze" . org-drill-present-multicloze-hide1)
212 ("hide2cloze" . org-drill-present-multicloze-hide2)
213 ("show1cloze" . org-drill-present-multicloze-show1)
214 ("show2cloze" . org-drill-present-multicloze-show2)
215 ("multicloze" . org-drill-present-multicloze-hide1)
216 ("hidefirst" . org-drill-present-multicloze-hide-first)
217 ("hidelast" . org-drill-present-multicloze-hide-last)
218 ("hide1_firstmore" . org-drill-present-multicloze-hide1-firstmore)
219 ("show1_lastmore" . org-drill-present-multicloze-show1-lastmore)
220 ("show1_firstless" . org-drill-present-multicloze-show1-firstless)
221 ("conjugate" org-drill-present-verb-conjugation
222 org-drill-show-answer-verb-conjugation)
223 ("spanish_verb" . org-drill-present-spanish-verb)
224 ("translate_number" org-drill-present-translate-number
225 org-drill-show-answer-translate-number))
226 "Alist associating card types with presentation functions. Each entry in the
227 alist takes one of two forms:
228 1. (CARDTYPE . QUESTION-FN), where CARDTYPE is a string or nil (for default),
229 and QUESTION-FN is a function which takes no arguments and returns a boolean
230 value.
231 2. (CARDTYPE QUESTION-FN ANSWER-FN), where ANSWER-FN is a function that takes
232 one argument -- the argument is a function that itself takes no arguments.
233 ANSWER-FN is called with the point on the active item's
234 heading, just prior to displaying the item's 'answer'. It can therefore be
235 used to modify the appearance of the answer. ANSWER-FN must call its argument
236 before returning. (Its argument is a function that prompts the user and
237 performs rescheduling)."
238 :group 'org-drill
239 :type '(alist :key-type (choice string (const nil)) :value-type function))
242 (defcustom org-drill-scope
243 'file
244 "The scope in which to search for drill items when conducting a
245 drill session. This can be any of:
247 file The current buffer, respecting the restriction if any.
248 This is the default.
249 tree The subtree started with the entry at point
250 file-no-restriction The current buffer, without restriction
251 file-with-archives The current buffer, and any archives associated with it.
252 agenda All agenda files
253 agenda-with-archives All agenda files with any archive files associated
254 with them.
255 directory All files with the extension '.org' in the same
256 directory as the current file (includes the current
257 file if it is an .org file.)
258 (FILE1 FILE2 ...) If this is a list, all files in the list will be scanned.
260 ;; Note -- meanings differ slightly from the argument to org-map-entries:
261 ;; 'file' means current file/buffer, respecting any restriction
262 ;; 'file-no-restriction' means current file/buffer, ignoring restrictions
263 ;; 'directory' means all *.org files in current directory
264 :group 'org-drill
265 :type '(choice (const 'file) (const 'tree) (const 'file-no-restriction)
266 (const 'file-with-archives) (const 'agenda)
267 (const 'agenda-with-archives) (const 'directory)
268 list))
271 (defcustom org-drill-save-buffers-after-drill-sessions-p
273 "If non-nil, prompt to save all modified buffers after a drill session
274 finishes."
275 :group 'org-drill
276 :type 'boolean)
279 (defcustom org-drill-spaced-repetition-algorithm
280 'sm5
281 "Which SuperMemo spaced repetition algorithm to use for scheduling items.
282 Available choices are:
283 - SM2 :: the SM2 algorithm, used in SuperMemo 2.0
284 - SM5 :: the SM5 algorithm, used in SuperMemo 5.0
285 - Simple8 :: a modified version of the SM8 algorithm. SM8 is used in
286 SuperMemo 98. The version implemented here is simplified in that while it
287 'learns' the difficulty of each item using quality grades and number of
288 failures, it does not modify the matrix of values that
289 governs how fast the inter-repetition intervals increase. A method for
290 adjusting intervals when items are reviewed early or late has been taken
291 from SM11, a later version of the algorithm, and included in Simple8."
292 :group 'org-drill
293 :type '(choice (const 'sm2) (const 'sm5) (const 'simple8)))
296 (defcustom org-drill-optimal-factor-matrix
298 "DO NOT CHANGE THE VALUE OF THIS VARIABLE.
300 Persistent matrix of optimal factors, used by the SuperMemo SM5 algorithm.
301 The matrix is saved (using the 'customize' facility) at the end of each
302 drill session.
304 Over time, values in the matrix will adapt to the individual user's
305 pace of learning."
306 :group 'org-drill
307 :type 'sexp)
310 (defcustom org-drill-sm5-initial-interval
312 "In the SM5 algorithm, the initial interval after the first
313 successful presentation of an item is always 4 days. If you wish to change
314 this, you can do so here."
315 :group 'org-drill
316 :type 'float)
319 (defcustom org-drill-add-random-noise-to-intervals-p
321 "If true, the number of days until an item's next repetition
322 will vary slightly from the interval calculated by the SM2
323 algorithm. The variation is very small when the interval is
324 small, but scales up with the interval."
325 :group 'org-drill
326 :type 'boolean)
329 (defcustom org-drill-adjust-intervals-for-early-and-late-repetitions-p
331 "If true, when the student successfully reviews an item 1 or more days
332 before or after the scheduled review date, this will affect that date of
333 the item's next scheduled review, according to the algorithm presented at
334 [[http://www.supermemo.com/english/algsm11.htm#Advanced%20repetitions]].
336 Items that were reviewed early will have their next review date brought
337 forward. Those that were reviewed late will have their next review
338 date postponed further.
340 Note that this option currently has no effect if the SM2 algorithm
341 is used."
342 :group 'org-drill
343 :type 'boolean)
346 (defcustom org-drill-cloze-text-weight
348 "For card types 'hide1_firstmore', 'show1_lastmore' and 'show1_firstless',
349 this number determines how often the 'less favoured' situation
350 should arise. It will occur 1 in every N trials, where N is the
351 value of the variable.
353 For example, with the hide1_firstmore card type, the first piece
354 of clozed text should be hidden more often than the other
355 pieces. If this variable is set to 4 (default), the first item
356 will only be shown 25% of the time (1 in 4 trials). Similarly for
357 show1_lastmore, the last item will be shown 75% of the time, and
358 for show1_firstless, the first item would only be shown 25% of the
359 time.
361 If the value of this variable is NIL, then weighting is disabled, and
362 all weighted card types are treated as their unweighted equivalents."
363 :group 'org-drill
364 :type '(choice integer (const nil)))
367 (defcustom org-drill-cram-hours
369 "When in cram mode, items are considered due for review if
370 they were reviewed at least this many hours ago."
371 :group 'org-drill
372 :type 'integer)
375 ;;; NEW items have never been presented in a drill session before.
376 ;;; MATURE items HAVE been presented at least once before.
377 ;;; - YOUNG mature items were scheduled no more than
378 ;;; ORG-DRILL-DAYS-BEFORE-OLD days after their last
379 ;;; repetition. These items will have been learned 'recently' and will have a
380 ;;; low repetition count.
381 ;;; - OLD mature items have intervals greater than
382 ;;; ORG-DRILL-DAYS-BEFORE-OLD.
383 ;;; - OVERDUE items are past their scheduled review date by more than
384 ;;; LAST-INTERVAL * (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) days,
385 ;;; regardless of young/old status.
388 (defcustom org-drill-days-before-old
390 "When an item's inter-repetition interval rises above this value in days,
391 it is no longer considered a 'young' (recently learned) item."
392 :group 'org-drill
393 :type 'integer)
396 (defcustom org-drill-overdue-interval-factor
398 "An item is considered overdue if its scheduled review date is
399 more than (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) * LAST-INTERVAL
400 days in the past. For example, a value of 1.2 means an additional
401 20% of the last scheduled interval is allowed to elapse before
402 the item is overdue. A value of 1.0 means no extra time is
403 allowed at all - items are immediately considered overdue if
404 there is even one day's delay in reviewing them. This variable
405 should never be less than 1.0."
406 :group 'org-drill
407 :type 'float)
410 (defcustom org-drill-learn-fraction
412 "Fraction between 0 and 1 that governs how quickly the spaces
413 between successive repetitions increase, for all items. The
414 default value is 0.5. Higher values make spaces increase more
415 quickly with each successful repetition. You should only change
416 this in small increments (for example 0.05-0.1) as it has an
417 exponential effect on inter-repetition spacing."
418 :group 'org-drill
419 :type 'float)
422 (defvar *org-drill-session-qualities* nil)
423 (defvar *org-drill-start-time* 0)
424 (defvar *org-drill-new-entries* nil)
425 (defvar *org-drill-dormant-entry-count* 0)
426 (defvar *org-drill-due-entry-count* 0)
427 (defvar *org-drill-overdue-entry-count* 0)
428 (defvar *org-drill-due-tomorrow-count* 0)
429 (defvar *org-drill-overdue-entries* nil
430 "List of markers for items that are considered 'overdue', based on
431 the value of ORG-DRILL-OVERDUE-INTERVAL-FACTOR.")
432 (defvar *org-drill-young-mature-entries* nil
433 "List of markers for mature entries whose last inter-repetition
434 interval was <= ORG-DRILL-DAYS-BEFORE-OLD days.")
435 (defvar *org-drill-old-mature-entries* nil
436 "List of markers for mature entries whose last inter-repetition
437 interval was greater than ORG-DRILL-DAYS-BEFORE-OLD days.")
438 (defvar *org-drill-failed-entries* nil)
439 (defvar *org-drill-again-entries* nil)
440 (defvar *org-drill-done-entries* nil)
441 (defvar *org-drill-current-item* nil
442 "Set to the marker for the item currently being tested.")
443 (defvar *org-drill-cram-mode* nil
444 "Are we in 'cram mode', where all items are considered due
445 for review unless they were already reviewed in the recent past?")
446 (defvar org-drill-scheduling-properties
447 '("LEARN_DATA" "DRILL_LAST_INTERVAL" "DRILL_REPEATS_SINCE_FAIL"
448 "DRILL_TOTAL_REPEATS" "DRILL_FAILURE_COUNT" "DRILL_AVERAGE_QUALITY"
449 "DRILL_EASE" "DRILL_LAST_QUALITY" "DRILL_LAST_REVIEWED"))
452 ;;; Make the above settings safe as file-local variables.
455 (put 'org-drill-question-tag 'safe-local-variable 'stringp)
456 (put 'org-drill-maximum-items-per-session 'safe-local-variable
457 '(lambda (val) (or (integerp val) (null val))))
458 (put 'org-drill-maximum-duration 'safe-local-variable
459 '(lambda (val) (or (integerp val) (null val))))
460 (put 'org-drill-failure-quality 'safe-local-variable 'integerp)
461 (put 'org-drill-forgetting-index 'safe-local-variable 'integerp)
462 (put 'org-drill-leech-failure-threshold 'safe-local-variable 'integerp)
463 (put 'org-drill-leech-method 'safe-local-variable
464 '(lambda (val) (memq val '(nil skip warn))))
465 (put 'org-drill-use-visible-cloze-face-p 'safe-local-variable 'booleanp)
466 (put 'org-drill-hide-item-headings-p 'safe-local-variable 'booleanp)
467 (put 'org-drill-spaced-repetition-algorithm 'safe-local-variable
468 '(lambda (val) (memq val '(simple8 sm5 sm2))))
469 (put 'org-drill-sm5-initial-interval 'safe-local-variable 'floatp)
470 (put 'org-drill-add-random-noise-to-intervals-p 'safe-local-variable 'booleanp)
471 (put 'org-drill-adjust-intervals-for-early-and-late-repetitions-p
472 'safe-local-variable 'booleanp)
473 (put 'org-drill-cram-hours 'safe-local-variable 'integerp)
474 (put 'org-drill-learn-fraction 'safe-local-variable 'floatp)
475 (put 'org-drill-days-before-old 'safe-local-variable 'integerp)
476 (put 'org-drill-overdue-interval-factor 'safe-local-variable 'floatp)
477 (put 'org-drill-scope 'safe-local-variable
478 '(lambda (val) (or (symbolp val) (listp val))))
479 (put 'org-drill-save-buffers-after-drill-sessions-p 'safe-local-variable 'booleanp)
480 (put 'org-drill-cloze-text-weight 'safe-local-variable
481 '(lambda (val) (or (null val) (integerp val))))
484 ;;;; Utilities ================================================================
487 (defun free-marker (m)
488 (set-marker m nil))
491 (defmacro pop-random (place)
492 (let ((idx (gensym)))
493 `(if (null ,place)
495 (let ((,idx (random* (length ,place))))
496 (prog1 (nth ,idx ,place)
497 (setq ,place (append (subseq ,place 0 ,idx)
498 (subseq ,place (1+ ,idx)))))))))
501 (defmacro push-end (val place)
502 "Add VAL to the end of the sequence stored in PLACE. Return the new
503 value."
504 `(setq ,place (append ,place (list ,val))))
507 (defun shuffle-list (list)
508 "Randomly permute the elements of LIST (all permutations equally likely)."
509 ;; Adapted from 'shuffle-vector' in cookie1.el
510 (let ((i 0)
512 temp
513 (len (length list)))
514 (while (< i len)
515 (setq j (+ i (random* (- len i))))
516 (setq temp (nth i list))
517 (setf (nth i list) (nth j list))
518 (setf (nth j list) temp)
519 (setq i (1+ i))))
520 list)
523 (defun round-float (floatnum fix)
524 "Round the floating point number FLOATNUM to FIX decimal places.
525 Example: (round-float 3.56755765 3) -> 3.568"
526 (let ((n (expt 10 fix)))
527 (/ (float (round (* floatnum n))) n)))
530 (defun command-keybinding-to-string (cmd)
531 "Return a human-readable description of the key/keys to which the command
532 CMD is bound, or nil if it is not bound to a key."
533 (let ((key (where-is-internal cmd overriding-local-map t)))
534 (if key (key-description key))))
537 (defun time-to-inactive-org-timestamp (time)
538 (format-time-string
539 (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")
540 time))
543 (defun org-map-drill-entries (func &optional scope &rest skip)
544 "Like `org-map-entries', but only drill entries are processed."
545 (let ((org-drill-scope (or scope org-drill-scope)))
546 (apply 'org-map-entries func
547 (concat "+" org-drill-question-tag)
548 (case org-drill-scope
549 (file nil)
550 (file-no-restriction 'file)
551 (directory
552 (directory-files (file-name-directory (buffer-file-name))
553 t "\\.org$"))
554 (t org-drill-scope))
555 skip)))
558 (defmacro with-hidden-cloze-text (&rest body)
559 `(progn
560 (org-drill-hide-clozed-text)
561 (unwind-protect
562 (progn
563 ,@body)
564 (org-drill-unhide-clozed-text))))
567 (defmacro with-hidden-cloze-hints (&rest body)
568 `(progn
569 (org-drill-hide-cloze-hints)
570 (unwind-protect
571 (progn
572 ,@body)
573 (org-drill-unhide-text))))
576 (defmacro with-hidden-comments (&rest body)
577 `(progn
578 (if org-drill-hide-item-headings-p
579 (org-drill-hide-heading-at-point))
580 (org-drill-hide-comments)
581 (unwind-protect
582 (progn
583 ,@body)
584 (org-drill-unhide-text))))
587 (defun org-drill-days-since-last-review ()
588 "Nil means a last review date has not yet been stored for
589 the item.
590 Zero means it was reviewed today.
591 A positive number means it was reviewed that many days ago.
592 A negative number means the date of last review is in the future --
593 this should never happen."
594 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
595 (when datestr
596 (- (time-to-days (current-time))
597 (time-to-days (apply 'encode-time
598 (org-parse-time-string datestr)))))))
601 (defun org-drill-hours-since-last-review ()
602 "Like `org-drill-days-since-last-review', but return value is
603 in hours rather than days."
604 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
605 (when datestr
606 (floor
607 (/ (- (time-to-seconds (current-time))
608 (time-to-seconds (apply 'encode-time
609 (org-parse-time-string datestr))))
610 (* 60 60))))))
613 (defun org-drill-entry-p (&optional marker)
614 "Is MARKER, or the point, in a 'drill item'? This will return nil if
615 the point is inside a subheading of a drill item -- to handle that
616 situation use `org-part-of-drill-entry-p'."
617 (save-excursion
618 (when marker
619 (org-drill-goto-entry marker))
620 (member org-drill-question-tag (org-get-local-tags))))
623 (defun org-drill-goto-entry (marker)
624 (switch-to-buffer (marker-buffer marker))
625 (goto-char marker))
628 (defun org-part-of-drill-entry-p ()
629 "Is the current entry either the main heading of a 'drill item',
630 or a subheading within a drill item?"
631 (or (org-drill-entry-p)
632 ;; Does this heading INHERIT the drill tag
633 (member org-drill-question-tag (org-get-tags-at))))
636 (defun org-drill-goto-drill-entry-heading ()
637 "Move the point to the heading which holds the :drill: tag for this
638 drill entry."
639 (unless (org-at-heading-p)
640 (org-back-to-heading))
641 (unless (org-part-of-drill-entry-p)
642 (error "Point is not inside a drill entry"))
643 (while (not (org-drill-entry-p))
644 (unless (org-up-heading-safe)
645 (error "Cannot find a parent heading that is marked as a drill entry"))))
649 (defun org-drill-entry-leech-p ()
650 "Is the current entry a 'leech item'?"
651 (and (org-drill-entry-p)
652 (member "leech" (org-get-local-tags))))
655 ;; (defun org-drill-entry-due-p ()
656 ;; (cond
657 ;; (*org-drill-cram-mode*
658 ;; (let ((hours (org-drill-hours-since-last-review)))
659 ;; (and (org-drill-entry-p)
660 ;; (or (null hours)
661 ;; (>= hours org-drill-cram-hours)))))
662 ;; (t
663 ;; (let ((item-time (org-get-scheduled-time (point))))
664 ;; (and (org-drill-entry-p)
665 ;; (or (not (eql 'skip org-drill-leech-method))
666 ;; (not (org-drill-entry-leech-p)))
667 ;; (or (null item-time) ; not scheduled
668 ;; (not (minusp ; scheduled for today/in past
669 ;; (- (time-to-days (current-time))
670 ;; (time-to-days item-time))))))))))
673 (defun org-drill-entry-days-overdue ()
674 "Returns:
675 - NIL if the item is not to be regarded as scheduled for review at all.
676 This is the case if it is not a drill item, or if it is a leech item
677 that we wish to skip, or if we are in cram mode and have already reviewed
678 the item within the last few hours.
679 - 0 if the item is new, or if it scheduled for review today.
680 - A negative integer - item is scheduled that many days in the future.
681 - A positive integer - item is scheduled that many days in the past."
682 (cond
683 (*org-drill-cram-mode*
684 (let ((hours (org-drill-hours-since-last-review)))
685 (and (org-drill-entry-p)
686 (or (null hours)
687 (>= hours org-drill-cram-hours))
688 0)))
690 (let ((item-time (org-get-scheduled-time (point))))
691 (cond
692 ((or (not (org-drill-entry-p))
693 (and (eql 'skip org-drill-leech-method)
694 (org-drill-entry-leech-p)))
695 nil)
696 ((null item-time) ; not scheduled -> due now
699 (- (time-to-days (current-time))
700 (time-to-days item-time))))))))
703 (defun org-drill-entry-overdue-p (&optional days-overdue last-interval)
704 "Returns true if entry that is scheduled DAYS-OVERDUE dasy in the past,
705 and whose last inter-repetition interval was LAST-INTERVAL, should be
706 considered 'overdue'. If the arguments are not given they are extracted
707 from the entry at point."
708 (unless days-overdue
709 (setq days-overdue (org-drill-entry-days-overdue)))
710 (unless last-interval
711 (setq last-interval (org-drill-entry-last-interval 1)))
712 (and (numberp days-overdue)
713 (> days-overdue 1) ; enforce a sane minimum 'overdue' gap
714 ;;(> due org-drill-days-before-overdue)
715 (> (/ (+ days-overdue last-interval 1.0) last-interval)
716 org-drill-overdue-interval-factor)))
720 (defun org-drill-entry-due-p ()
721 (let ((due (org-drill-entry-days-overdue)))
722 (and (not (null due))
723 (not (minusp due)))))
726 (defun org-drill-entry-new-p ()
727 (and (org-drill-entry-p)
728 (let ((item-time (org-get-scheduled-time (point))))
729 (null item-time))))
732 (defun org-drill-entry-last-quality (&optional default)
733 (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
734 (if quality
735 (string-to-number quality)
736 default)))
739 (defun org-drill-entry-failure-count ()
740 (let ((quality (org-entry-get (point) "DRILL_FAILURE_COUNT")))
741 (if quality
742 (string-to-number quality)
743 0)))
746 (defun org-drill-entry-average-quality (&optional default)
747 (let ((val (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
748 (if val
749 (string-to-number val)
750 (or default nil))))
752 (defun org-drill-entry-last-interval (&optional default)
753 (let ((val (org-entry-get (point) "DRILL_LAST_INTERVAL")))
754 (if val
755 (string-to-number val)
756 (or default 0))))
758 (defun org-drill-entry-repeats-since-fail (&optional default)
759 (let ((val (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
760 (if val
761 (string-to-number val)
762 (or default 0))))
764 (defun org-drill-entry-total-repeats (&optional default)
765 (let ((val (org-entry-get (point) "DRILL_TOTAL_REPEATS")))
766 (if val
767 (string-to-number val)
768 (or default 0))))
770 (defun org-drill-entry-ease (&optional default)
771 (let ((val (org-entry-get (point) "DRILL_EASE")))
772 (if val
773 (string-to-number val)
774 default)))
777 ;;; From http://www.supermemo.com/english/ol/sm5.htm
778 (defun org-drill-random-dispersal-factor ()
779 "Returns a random number between 0.5 and 1.5."
780 (let ((a 0.047)
781 (b 0.092)
782 (p (- (random* 1.0) 0.5)))
783 (flet ((sign (n)
784 (cond ((zerop n) 0)
785 ((plusp n) 1)
786 (t -1))))
787 (/ (+ 100 (* (* (/ -1 b) (log (- 1 (* (/ b a ) (abs p)))))
788 (sign p)))
789 100.0))))
791 (defun pseudonormal (mean variation)
792 "Random numbers in a pseudo-normal distribution with mean MEAN, range
793 MEAN-VARIATION to MEAN+VARIATION"
794 (+ (random* variation)
795 (random* variation)
796 (- variation)
797 mean))
800 (defun org-drill-early-interval-factor (optimal-factor
801 optimal-interval
802 days-ahead)
803 "Arguments:
804 - OPTIMAL-FACTOR: interval-factor if the item had been tested
805 exactly when it was supposed to be.
806 - OPTIMAL-INTERVAL: interval for next repetition (days) if the item had been
807 tested exactly when it was supposed to be.
808 - DAYS-AHEAD: how many days ahead of time the item was reviewed.
810 Returns an adjusted optimal factor which should be used to
811 calculate the next interval, instead of the optimal factor found
812 in the matrix."
813 (let ((delta-ofmax (* (1- optimal-factor)
814 (/ (+ optimal-interval
815 (* 0.6 optimal-interval) -1) (1- optimal-interval)))))
816 (- optimal-factor
817 (* delta-ofmax (/ days-ahead (+ days-ahead (* 0.6 optimal-interval)))))))
820 (defun org-drill-get-item-data ()
821 "Returns a list of 6 items, containing all the stored recall
822 data for the item at point:
823 - LAST-INTERVAL is the interval in days that was used to schedule the item's
824 current review date.
825 - REPEATS is the number of items the item has been successfully recalled without
826 without any failures. It is reset to 0 upon failure to recall the item.
827 - FAILURES is the total number of times the user has failed to recall the item.
828 - TOTAL-REPEATS includes both successful and unsuccessful repetitions.
829 - AVERAGE-QUALITY is the mean quality of recall of the item over
830 all its repetitions, successful and unsuccessful.
831 - EASE is a number reflecting how easy the item is to learn. Higher is easier.
833 (let ((learn-str (org-entry-get (point) "LEARN_DATA"))
834 (repeats (org-drill-entry-total-repeats :missing)))
835 (cond
836 (learn-str
837 (let ((learn-data (or (and learn-str
838 (read learn-str))
839 (copy-list initial-repetition-state))))
840 (list (nth 0 learn-data) ; last interval
841 (nth 1 learn-data) ; repetitions
842 (org-drill-entry-failure-count)
843 (nth 1 learn-data)
844 (org-drill-entry-last-quality)
845 (nth 2 learn-data) ; EF
847 ((not (eql :missing repeats))
848 (list (org-drill-entry-last-interval)
849 (org-drill-entry-repeats-since-fail)
850 (org-drill-entry-failure-count)
851 (org-drill-entry-total-repeats)
852 (org-drill-entry-average-quality)
853 (org-drill-entry-ease)))
854 (t ; virgin item
855 (list 0 0 0 0 nil nil)))))
858 (defun org-drill-store-item-data (last-interval repeats failures
859 total-repeats meanq
860 ease)
861 "Stores the given data in the item at point."
862 (org-entry-delete (point) "LEARN_DATA")
863 (org-set-property "DRILL_LAST_INTERVAL"
864 (number-to-string (round-float last-interval 4)))
865 (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats))
866 (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats))
867 (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures))
868 (org-set-property "DRILL_AVERAGE_QUALITY"
869 (number-to-string (round-float meanq 3)))
870 (org-set-property "DRILL_EASE"
871 (number-to-string (round-float ease 3))))
875 ;;; SM2 Algorithm =============================================================
878 (defun determine-next-interval-sm2 (last-interval n ef quality
879 failures meanq total-repeats)
880 "Arguments:
881 - LAST-INTERVAL -- the number of days since the item was last reviewed.
882 - REPEATS -- the number of times the item has been successfully reviewed
883 - EF -- the 'easiness factor'
884 - QUALITY -- 0 to 5
886 Returns a list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX), where:
887 - INTERVAL is the number of days until the item should next be reviewed
888 - REPEATS is incremented by 1.
889 - EF is modified based on the recall quality for the item.
890 - OF-MATRIX is not modified."
891 (assert (> n 0))
892 (assert (and (>= quality 0) (<= quality 5)))
893 (if (<= quality org-drill-failure-quality)
894 ;; When an item is failed, its interval is reset to 0,
895 ;; but its EF is unchanged
896 (list -1 1 ef (1+ failures) meanq (1+ total-repeats)
897 org-drill-optimal-factor-matrix)
898 ;; else:
899 (let* ((next-ef (modify-e-factor ef quality))
900 (interval
901 (cond
902 ((<= n 1) 1)
903 ((= n 2)
904 (cond
905 (org-drill-add-random-noise-to-intervals-p
906 (case quality
907 (5 6)
908 (4 4)
909 (3 3)
910 (2 1)
911 (t -1)))
912 (t 6)))
913 (t (* last-interval next-ef)))))
914 (list (if org-drill-add-random-noise-to-intervals-p
915 (+ last-interval (* (- interval last-interval)
916 (org-drill-random-dispersal-factor)))
917 interval)
918 (1+ n)
919 next-ef
920 failures meanq (1+ total-repeats)
921 org-drill-optimal-factor-matrix))))
924 ;;; SM5 Algorithm =============================================================
928 (defun initial-optimal-factor-sm5 (n ef)
929 (if (= 1 n)
930 org-drill-sm5-initial-interval
931 ef))
933 (defun get-optimal-factor-sm5 (n ef of-matrix)
934 (let ((factors (assoc n of-matrix)))
935 (or (and factors
936 (let ((ef-of (assoc ef (cdr factors))))
937 (and ef-of (cdr ef-of))))
938 (initial-optimal-factor-sm5 n ef))))
941 (defun inter-repetition-interval-sm5 (last-interval n ef &optional of-matrix)
942 (let ((of (get-optimal-factor-sm5 n ef (or of-matrix
943 org-drill-optimal-factor-matrix))))
944 (if (= 1 n)
946 (* of last-interval))))
949 (defun determine-next-interval-sm5 (last-interval n ef quality
950 failures meanq total-repeats
951 of-matrix &optional delta-days)
952 (if (zerop n) (setq n 1))
953 (if (null ef) (setq ef 2.5))
954 (assert (> n 0))
955 (assert (and (>= quality 0) (<= quality 5)))
956 (unless of-matrix
957 (setq of-matrix org-drill-optimal-factor-matrix))
958 (setq of-matrix (cl-copy-tree of-matrix))
960 (setq meanq (if meanq
961 (/ (+ quality (* meanq total-repeats 1.0))
962 (1+ total-repeats))
963 quality))
965 (let ((next-ef (modify-e-factor ef quality))
966 (old-ef ef)
967 (new-of (modify-of (get-optimal-factor-sm5 n ef of-matrix)
968 quality org-drill-learn-fraction))
969 (interval nil))
970 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
971 delta-days (minusp delta-days))
972 (setq new-of (org-drill-early-interval-factor
973 (get-optimal-factor-sm5 n ef of-matrix)
974 (inter-repetition-interval-sm5
975 last-interval n ef of-matrix)
976 delta-days)))
978 (setq of-matrix
979 (set-optimal-factor n next-ef of-matrix
980 (round-float new-of 3))) ; round OF to 3 d.p.
982 (setq ef next-ef)
984 (cond
985 ;; "Failed" -- reset repetitions to 0,
986 ((<= quality org-drill-failure-quality)
987 (list -1 1 old-ef (1+ failures) meanq (1+ total-repeats)
988 of-matrix)) ; Not clear if OF matrix is supposed to be
989 ; preserved
990 ;; For a zero-based quality of 4 or 5, don't repeat
991 ;; ((and (>= quality 4)
992 ;; (not org-learn-always-reschedule))
993 ;; (list 0 (1+ n) ef failures meanq
994 ;; (1+ total-repeats) of-matrix)) ; 0 interval = unschedule
996 (setq interval (inter-repetition-interval-sm5
997 last-interval n ef of-matrix))
998 (if org-drill-add-random-noise-to-intervals-p
999 (setq interval (* interval (org-drill-random-dispersal-factor))))
1000 (list interval
1001 (1+ n)
1003 failures
1004 meanq
1005 (1+ total-repeats)
1006 of-matrix)))))
1009 ;;; Simple8 Algorithm =========================================================
1012 (defun org-drill-simple8-first-interval (failures)
1013 "Arguments:
1014 - FAILURES: integer >= 0. The total number of times the item has
1015 been forgotten, ever.
1017 Returns the optimal FIRST interval for an item which has previously been
1018 forgotten on FAILURES occasions."
1019 (* 2.4849 (exp (* -0.057 failures))))
1022 (defun org-drill-simple8-interval-factor (ease repetition)
1023 "Arguments:
1024 - EASE: floating point number >= 1.2. Corresponds to `AF' in SM8 algorithm.
1025 - REPETITION: the number of times the item has been tested.
1026 1 is the first repetition (ie the second trial).
1027 Returns:
1028 The factor by which the last interval should be
1029 multiplied to give the next interval. Corresponds to `RF' or `OF'."
1030 (+ 1.2 (* (- ease 1.2) (expt org-drill-learn-fraction (log repetition 2)))))
1033 (defun org-drill-simple8-quality->ease (quality)
1034 "Returns the ease (`AF' in the SM8 algorithm) which corresponds
1035 to a mean item quality of QUALITY."
1036 (+ (* 0.0542 (expt quality 4))
1037 (* -0.4848 (expt quality 3))
1038 (* 1.4916 (expt quality 2))
1039 (* -1.2403 quality)
1040 1.4515))
1043 (defun determine-next-interval-simple8 (last-interval repeats quality
1044 failures meanq totaln
1045 &optional delta-days)
1046 "Arguments:
1047 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1048 - REPEATS -- the number of times the item has been successfully reviewed
1049 - EASE -- the 'easiness factor'
1050 - QUALITY -- 0 to 5
1051 - DELTA-DAYS -- how many days overdue was the item when it was reviewed.
1052 0 = reviewed on the scheduled day. +N = N days overdue.
1053 -N = reviewed N days early.
1055 Returns the new item data, as a list of 6 values:
1056 - NEXT-INTERVAL
1057 - REPEATS
1058 - EASE
1059 - FAILURES
1060 - AVERAGE-QUALITY
1061 - TOTAL-REPEATS.
1062 See the documentation for `org-drill-get-item-data' for a description of these."
1063 (assert (>= repeats 0))
1064 (assert (and (>= quality 0) (<= quality 5)))
1065 (assert (or (null meanq) (and (>= meanq 0) (<= meanq 5))))
1066 (let ((next-interval nil))
1067 (setf meanq (if meanq
1068 (/ (+ quality (* meanq totaln 1.0)) (1+ totaln))
1069 quality))
1070 (cond
1071 ((<= quality org-drill-failure-quality)
1072 (incf failures)
1073 (setf repeats 0
1074 next-interval -1))
1075 ((or (zerop repeats)
1076 (zerop last-interval))
1077 (setf next-interval (org-drill-simple8-first-interval failures))
1078 (incf repeats)
1079 (incf totaln))
1081 (let* ((use-n
1082 (if (and
1083 org-drill-adjust-intervals-for-early-and-late-repetitions-p
1084 (numberp delta-days) (plusp delta-days)
1085 (plusp last-interval))
1086 (+ repeats (min 1 (/ delta-days last-interval 1.0)))
1087 repeats))
1088 (factor (org-drill-simple8-interval-factor
1089 (org-drill-simple8-quality->ease meanq) use-n))
1090 (next-int (* last-interval factor)))
1091 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1092 (numberp delta-days) (minusp delta-days))
1093 ;; The item was reviewed earlier than scheduled.
1094 (setf factor (org-drill-early-interval-factor
1095 factor next-int (abs delta-days))
1096 next-int (* last-interval factor)))
1097 (setf next-interval next-int)
1098 (incf repeats)
1099 (incf totaln))))
1100 (list
1101 (if (and org-drill-add-random-noise-to-intervals-p
1102 (plusp next-interval))
1103 (* next-interval (org-drill-random-dispersal-factor))
1104 next-interval)
1105 repeats
1106 (org-drill-simple8-quality->ease meanq)
1107 failures
1108 meanq
1109 totaln
1115 ;;; Essentially copied from `org-learn.el', but modified to
1116 ;;; optionally call the SM2 or simple8 functions.
1117 (defun org-drill-smart-reschedule (quality &optional days-ahead)
1118 "If DAYS-AHEAD is supplied it must be a positive integer. The
1119 item will be scheduled exactly this many days into the future."
1120 (let ((delta-days (- (time-to-days (current-time))
1121 (time-to-days (or (org-get-scheduled-time (point))
1122 (current-time)))))
1123 (ofmatrix org-drill-optimal-factor-matrix)
1124 ;; Entries can have weights, 1 by default. Intervals are divided by the
1125 ;; item's weight, so an item with a weight of 2 will have all intervals
1126 ;; halved, meaning you will end up reviewing it twice as often.
1127 ;; Useful for entries which randomly present any of several facts.
1128 (weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1129 (if (stringp weight)
1130 (setq weight (read weight)))
1131 (destructuring-bind (last-interval repetitions failures
1132 total-repeats meanq ease)
1133 (org-drill-get-item-data)
1134 (destructuring-bind (next-interval repetitions ease
1135 failures meanq total-repeats
1136 &optional new-ofmatrix)
1137 (case org-drill-spaced-repetition-algorithm
1138 (sm5 (determine-next-interval-sm5 last-interval repetitions
1139 ease quality failures
1140 meanq total-repeats ofmatrix))
1141 (sm2 (determine-next-interval-sm2 last-interval repetitions
1142 ease quality failures
1143 meanq total-repeats))
1144 (simple8 (determine-next-interval-simple8 last-interval repetitions
1145 quality failures meanq
1146 total-repeats
1147 delta-days)))
1148 (if (numberp days-ahead)
1149 (setq next-interval days-ahead))
1151 (if (and (null days-ahead)
1152 (numberp weight) (plusp weight)
1153 (not (minusp next-interval)))
1154 (setq next-interval
1155 (max 1.0 (+ last-interval
1156 (/ (- next-interval last-interval) weight)))))
1158 (org-drill-store-item-data next-interval repetitions failures
1159 total-repeats meanq ease)
1161 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
1162 (setq org-drill-optimal-factor-matrix new-ofmatrix))
1164 (cond
1165 ((= 0 days-ahead)
1166 (org-schedule t))
1167 ((minusp days-ahead)
1168 (org-schedule nil (current-time)))
1170 (org-schedule nil (time-add (current-time)
1171 (days-to-time
1172 (round next-interval))))))))))
1175 (defun org-drill-hypothetical-next-review-date (quality)
1176 "Returns an integer representing the number of days into the future
1177 that the current item would be scheduled, based on a recall quality
1178 of QUALITY."
1179 (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1180 (destructuring-bind (last-interval repetitions failures
1181 total-repeats meanq ease)
1182 (org-drill-get-item-data)
1183 (if (stringp weight)
1184 (setq weight (read weight)))
1185 (destructuring-bind (next-interval repetitions ease
1186 failures meanq total-repeats
1187 &optional ofmatrix)
1188 (case org-drill-spaced-repetition-algorithm
1189 (sm5 (determine-next-interval-sm5 last-interval repetitions
1190 ease quality failures
1191 meanq total-repeats
1192 org-drill-optimal-factor-matrix))
1193 (sm2 (determine-next-interval-sm2 last-interval repetitions
1194 ease quality failures
1195 meanq total-repeats))
1196 (simple8 (determine-next-interval-simple8 last-interval repetitions
1197 quality failures meanq
1198 total-repeats)))
1199 (cond
1200 ((not (plusp next-interval))
1202 ((and (numberp weight) (plusp weight))
1203 (+ last-interval
1204 (max 1.0 (/ (- next-interval last-interval) weight))))
1206 next-interval))))))
1209 (defun org-drill-hypothetical-next-review-dates ()
1210 (let ((intervals nil))
1211 (dotimes (q 6)
1212 (push (max (or (car intervals) 0)
1213 (org-drill-hypothetical-next-review-date q))
1214 intervals))
1215 (reverse intervals)))
1218 (defun org-drill-reschedule ()
1219 "Returns quality rating (0-5), or nil if the user quit."
1220 (let ((ch nil)
1221 (input nil)
1222 (next-review-dates (org-drill-hypothetical-next-review-dates)))
1223 (save-excursion
1224 (while (not (memq ch '(?q ?e ?0 ?1 ?2 ?3 ?4 ?5)))
1225 (setq input (read-key-sequence
1226 (if (eq ch ??)
1227 (format "0-2 Means you have forgotten the item.
1228 3-5 Means you have remembered the item.
1230 0 - Completely forgot.
1231 1 - Even after seeing the answer, it still took a bit to sink in.
1232 2 - After seeing the answer, you remembered it.
1233 3 - It took you awhile, but you finally remembered. (+%s days)
1234 4 - After a little bit of thought you remembered. (+%s days)
1235 5 - You remembered the item really easily. (+%s days)
1237 How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)"
1238 (round (nth 3 next-review-dates))
1239 (round (nth 4 next-review-dates))
1240 (round (nth 5 next-review-dates)))
1241 "How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)")))
1242 (cond
1243 ((stringp input)
1244 (setq ch (elt input 0)))
1245 ((and (vectorp input) (symbolp (elt input 0)))
1246 (case (elt input 0)
1247 (up (ignore-errors (forward-line -1)))
1248 (down (ignore-errors (forward-line 1)))
1249 (left (ignore-errors (backward-char)))
1250 (right (ignore-errors (forward-char)))
1251 (prior (ignore-errors (scroll-down))) ; pgup
1252 (next (ignore-errors (scroll-up))))) ; pgdn
1253 ((and (vectorp input) (listp (elt input 0))
1254 (eventp (elt input 0)))
1255 (case (car (elt input 0))
1256 (wheel-up (ignore-errors (mwheel-scroll (elt input 0))))
1257 (wheel-down (ignore-errors (mwheel-scroll (elt input 0)))))))
1258 (if (eql ch ?t)
1259 (org-set-tags-command))))
1260 (cond
1261 ((and (>= ch ?0) (<= ch ?5))
1262 (let ((quality (- ch ?0))
1263 (failures (org-drill-entry-failure-count)))
1264 (save-excursion
1265 (org-drill-smart-reschedule quality
1266 (nth quality next-review-dates)))
1267 (push quality *org-drill-session-qualities*)
1268 (cond
1269 ((<= quality org-drill-failure-quality)
1270 (when org-drill-leech-failure-threshold
1271 ;;(setq failures (if failures (string-to-number failures) 0))
1272 ;; (org-set-property "DRILL_FAILURE_COUNT"
1273 ;; (format "%d" (1+ failures)))
1274 (if (> (1+ failures) org-drill-leech-failure-threshold)
1275 (org-toggle-tag "leech" 'on))))
1277 (let ((scheduled-time (org-get-scheduled-time (point))))
1278 (when scheduled-time
1279 (message "Next review in %d days"
1280 (- (time-to-days scheduled-time)
1281 (time-to-days (current-time))))
1282 (sit-for 0.5)))))
1283 (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality))
1284 (org-set-property "DRILL_LAST_REVIEWED"
1285 (time-to-inactive-org-timestamp (current-time)))
1286 quality))
1287 ((= ch ?e)
1288 'edit)
1290 nil))))
1293 ;; (defun org-drill-hide-all-subheadings-except (heading-list)
1294 ;; "Returns a list containing the position of each immediate subheading of
1295 ;; the current topic."
1296 ;; (let ((drill-entry-level (org-current-level))
1297 ;; (drill-sections nil)
1298 ;; (drill-heading nil))
1299 ;; (org-show-subtree)
1300 ;; (save-excursion
1301 ;; (org-map-entries
1302 ;; (lambda ()
1303 ;; (when (and (not (outline-invisible-p))
1304 ;; (> (org-current-level) drill-entry-level))
1305 ;; (setq drill-heading (org-get-heading t))
1306 ;; (unless (and (= (org-current-level) (1+ drill-entry-level))
1307 ;; (member drill-heading heading-list))
1308 ;; (hide-subtree))
1309 ;; (push (point) drill-sections)))
1310 ;; "" 'tree))
1311 ;; (reverse drill-sections)))
1315 (defun org-drill-hide-subheadings-if (test)
1316 "TEST is a function taking no arguments. TEST will be called for each
1317 of the immediate subheadings of the current drill item, with the point
1318 on the relevant subheading. TEST should return nil if the subheading is
1319 to be revealed, non-nil if it is to be hidden.
1320 Returns a list containing the position of each immediate subheading of
1321 the current topic."
1322 (let ((drill-entry-level (org-current-level))
1323 (drill-sections nil))
1324 (org-show-subtree)
1325 (save-excursion
1326 (org-map-entries
1327 (lambda ()
1328 (when (and (not (outline-invisible-p))
1329 (> (org-current-level) drill-entry-level))
1330 (when (or (/= (org-current-level) (1+ drill-entry-level))
1331 (funcall test))
1332 (hide-subtree))
1333 (push (point) drill-sections)))
1334 "" 'tree))
1335 (reverse drill-sections)))
1338 (defun org-drill-hide-all-subheadings-except (heading-list)
1339 (org-drill-hide-subheadings-if
1340 (lambda () (let ((drill-heading (org-get-heading t)))
1341 (not (member drill-heading heading-list))))))
1344 (defun org-drill-presentation-prompt (&rest fmt-and-args)
1345 (let* ((item-start-time (current-time))
1346 (input nil)
1347 (ch nil)
1348 (last-second 0)
1349 (mature-entry-count (+ (length *org-drill-young-mature-entries*)
1350 (length *org-drill-old-mature-entries*)
1351 (length *org-drill-overdue-entries*)))
1352 (status (first (org-drill-entry-status)))
1353 (prompt
1354 (if fmt-and-args
1355 (apply 'format
1356 (first fmt-and-args)
1357 (rest fmt-and-args))
1358 (concat "Press key for answer, "
1359 "e=edit, t=tags, s=skip, q=quit."))))
1360 (setq prompt
1361 (format "%s %s %s %s %s %s"
1362 (propertize
1363 (char-to-string
1364 (case status
1365 (:new ?N) (:young ?Y) (:old ?o) (:overdue ?!)
1366 (:failed ?F) (t ??)))
1367 'face `(:foreground
1368 ,(case status
1369 (:new org-drill-new-count-color)
1370 ((:young :old) org-drill-mature-count-color)
1371 ((:overdue :failed) org-drill-failed-count-color)
1372 (t org-drill-done-count-color))))
1373 (propertize
1374 (number-to-string (length *org-drill-done-entries*))
1375 'face `(:foreground ,org-drill-done-count-color)
1376 'help-echo "The number of items you have reviewed this session.")
1377 (propertize
1378 (number-to-string (+ (length *org-drill-again-entries*)
1379 (length *org-drill-failed-entries*)))
1380 'face `(:foreground ,org-drill-failed-count-color)
1381 'help-echo (concat "The number of items that you failed, "
1382 "and need to review again."))
1383 (propertize
1384 (number-to-string mature-entry-count)
1385 'face `(:foreground ,org-drill-mature-count-color)
1386 'help-echo "The number of old items due for review.")
1387 (propertize
1388 (number-to-string (length *org-drill-new-entries*))
1389 'face `(:foreground ,org-drill-new-count-color)
1390 'help-echo (concat "The number of new items that you "
1391 "have never reviewed."))
1392 prompt))
1393 (if (and (eql 'warn org-drill-leech-method)
1394 (org-drill-entry-leech-p))
1395 (setq prompt (concat
1396 (propertize "!!! LEECH ITEM !!!
1397 You seem to be having a lot of trouble memorising this item.
1398 Consider reformulating the item to make it easier to remember.\n"
1399 'face '(:foreground "red"))
1400 prompt)))
1401 (while (memq ch '(nil ?t))
1402 (setq ch nil)
1403 (while (not (input-pending-p))
1404 (let ((elapsed (time-subtract (current-time) item-start-time)))
1405 (message (concat (if (>= (time-to-seconds elapsed) (* 60 60))
1406 "++:++ "
1407 (format-time-string "%M:%S " elapsed))
1408 prompt))
1409 (sit-for 1)))
1410 (setq input (read-key-sequence nil))
1411 (if (stringp input) (setq ch (elt input 0)))
1412 (if (eql ch ?t)
1413 (org-set-tags-command)))
1414 (case ch
1415 (?q nil)
1416 (?e 'edit)
1417 (?s 'skip)
1418 (otherwise t))))
1421 (defun org-pos-in-regexp (pos regexp &optional nlines)
1422 (save-excursion
1423 (goto-char pos)
1424 (org-in-regexp regexp nlines)))
1427 (defun org-drill-hide-region (beg end &optional text)
1428 "Hide the buffer region between BEG and END with an 'invisible text'
1429 visual overlay, or with the string TEXT if it is supplied."
1430 (let ((ovl (make-overlay beg end)))
1431 (overlay-put ovl 'category
1432 'org-drill-hidden-text-overlay)
1433 (when (stringp text)
1434 (overlay-put ovl 'invisible nil)
1435 (overlay-put ovl 'face 'default)
1436 (overlay-put ovl 'display text))))
1439 (defun org-drill-hide-heading-at-point (&optional text)
1440 (unless (org-at-heading-p)
1441 (error "Point is not on a heading"))
1442 (save-excursion
1443 (let ((beg (point)))
1444 (end-of-line)
1445 (org-drill-hide-region beg (point) text))))
1448 (defun org-drill-hide-comments ()
1449 (save-excursion
1450 (while (re-search-forward "^#.*$" nil t)
1451 (org-drill-hide-region (match-beginning 0) (match-end 0)))))
1454 (defun org-drill-unhide-text ()
1455 ;; This will also unhide the item's heading.
1456 (save-excursion
1457 (dolist (ovl (overlays-in (point-min) (point-max)))
1458 (when (eql 'org-drill-hidden-text-overlay (overlay-get ovl 'category))
1459 (delete-overlay ovl)))))
1462 (defun org-drill-hide-clozed-text ()
1463 (save-excursion
1464 (while (re-search-forward org-drill-cloze-regexp nil t)
1465 ;; Don't hide org links, partly because they might contain inline
1466 ;; images which we want to keep visible
1467 (unless (save-match-data
1468 (org-pos-in-regexp (match-beginning 0)
1469 org-bracket-link-regexp 1))
1470 (org-drill-hide-matched-cloze-text)))))
1473 (defun org-drill-hide-matched-cloze-text ()
1474 "Hide the current match with a 'cloze' visual overlay."
1475 (let ((ovl (make-overlay (match-beginning 0) (match-end 0))))
1476 (overlay-put ovl 'category
1477 'org-drill-cloze-overlay-defaults)
1478 (when (find ?| (match-string 0))
1479 (let ((hint (substring-no-properties
1480 (match-string 0)
1481 (1+ (position ?| (match-string 0)))
1482 (1- (length (match-string 0))))))
1483 (overlay-put
1484 ovl 'display
1485 ;; If hint is like `X...' then display [X...]
1486 ;; otherwise display [...X]
1487 (format (if (string-match-p "\\.\\.\\." hint) "[%s]" "[%s...]")
1488 hint))))))
1491 (defun org-drill-hide-cloze-hints ()
1492 (save-excursion
1493 (while (re-search-forward org-drill-cloze-regexp nil t)
1494 (unless (or (save-match-data
1495 (org-pos-in-regexp (match-beginning 0)
1496 org-bracket-link-regexp 1))
1497 (null (match-beginning 2))) ; hint subexpression matched
1498 (org-drill-hide-region (match-beginning 2) (match-end 2))))))
1501 (defmacro with-replaced-entry-text (text &rest body)
1502 "During the execution of BODY, the entire text of the current entry is
1503 concealed by an overlay that displays the string TEXT."
1504 `(progn
1505 (org-drill-replace-entry-text ,text)
1506 (unwind-protect
1507 (progn
1508 ,@body)
1509 (org-drill-unreplace-entry-text))))
1512 (defmacro with-replaced-entry-text-multi (replacements &rest body)
1513 "During the execution of BODY, the entire text of the current entry is
1514 concealed by an overlay that displays the overlays in REPLACEMENTS."
1515 `(progn
1516 (org-drill-replace-entry-text ,replacements t)
1517 (unwind-protect
1518 (progn
1519 ,@body)
1520 (org-drill-unreplace-entry-text))))
1523 (defun org-drill-replace-entry-text (text &optional multi-p)
1524 "Make an overlay that conceals the entire text of the item, not
1525 including properties or the contents of subheadings. The overlay shows
1526 the string TEXT.
1527 If MULTI-P is non-nil, TEXT must be a list of values which are legal
1528 for the `display' text property. The text of the item will be temporarily
1529 replaced by all of these items, in the order in which they appear in
1530 the list.
1531 Note: does not actually alter the item."
1532 (cond
1533 ((and multi-p
1534 (listp text))
1535 (org-drill-replace-entry-text-multi text))
1537 (let ((ovl (make-overlay (point-min)
1538 (save-excursion
1539 (outline-next-heading)
1540 (point)))))
1541 (overlay-put ovl 'category
1542 'org-drill-replaced-text-overlay)
1543 (overlay-put ovl 'display text)))))
1546 (defun org-drill-unreplace-entry-text ()
1547 (save-excursion
1548 (dolist (ovl (overlays-in (point-min) (point-max)))
1549 (when (eql 'org-drill-replaced-text-overlay (overlay-get ovl 'category))
1550 (delete-overlay ovl)))))
1553 (defun org-drill-replace-entry-text-multi (replacements)
1554 "Make overlays that conceal the entire text of the item, not
1555 including properties or the contents of subheadings. The overlay shows
1556 the string TEXT.
1557 Note: does not actually alter the item."
1558 (let ((ovl nil)
1559 (p-min (point-min))
1560 (p-max (save-excursion
1561 (outline-next-heading)
1562 (point))))
1563 (assert (>= (- p-max p-min) (length replacements)))
1564 (dotimes (i (length replacements))
1565 (setq ovl (make-overlay (+ p-min (* 2 i))
1566 (if (= i (1- (length replacements)))
1567 p-max
1568 (+ p-min (* 2 i) 1))))
1569 (overlay-put ovl 'category
1570 'org-drill-replaced-text-overlay)
1571 (overlay-put ovl 'display (nth i replacements)))))
1574 (defmacro with-replaced-entry-heading (heading &rest body)
1575 `(progn
1576 (org-drill-replace-entry-heading ,heading)
1577 (unwind-protect
1578 (progn
1579 ,@body)
1580 (org-drill-unhide-text))))
1583 (defun org-drill-replace-entry-heading (heading)
1584 "Make an overlay that conceals the heading of the item. The overlay shows
1585 the string TEXT.
1586 Note: does not actually alter the item."
1587 (org-drill-hide-heading-at-point heading))
1590 (defun org-drill-unhide-clozed-text ()
1591 (save-excursion
1592 (dolist (ovl (overlays-in (point-min) (point-max)))
1593 (when (eql 'org-drill-cloze-overlay-defaults (overlay-get ovl 'category))
1594 (delete-overlay ovl)))))
1597 (defun org-drill-get-entry-text (&optional keep-properties-p)
1598 (let ((text (org-agenda-get-some-entry-text (point-marker) 100)))
1599 (if keep-properties-p
1600 text
1601 (substring-no-properties text))))
1604 (defun org-drill-entry-empty-p ()
1605 (zerop (length (org-drill-get-entry-text))))
1609 ;;; Presentation functions ====================================================
1611 ;; Each of these is called with point on topic heading. Each needs to show the
1612 ;; topic in the form of a 'question' or with some information 'hidden', as
1613 ;; appropriate for the card type. The user should then be prompted to press a
1614 ;; key. The function should then reveal either the 'answer' or the entire
1615 ;; topic, and should return t if the user chose to see the answer and rate their
1616 ;; recall, nil if they chose to quit.
1618 (defun org-drill-present-simple-card ()
1619 (with-hidden-comments
1620 (with-hidden-cloze-hints
1621 (with-hidden-cloze-text
1622 (org-drill-hide-all-subheadings-except nil)
1623 (ignore-errors
1624 (org-display-inline-images t))
1625 (org-cycle-hide-drawers 'all)
1626 (prog1 (org-drill-presentation-prompt)
1627 (org-drill-hide-subheadings-if 'org-drill-entry-p))))))
1630 (defun org-drill-present-default-answer (reschedule-fn)
1631 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1632 (org-drill-unhide-clozed-text)
1633 (ignore-errors
1634 (org-display-inline-images t))
1635 (with-hidden-cloze-hints
1636 (funcall reschedule-fn)))
1639 (defun org-drill-present-two-sided-card ()
1640 (with-hidden-comments
1641 (with-hidden-cloze-hints
1642 (with-hidden-cloze-text
1643 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1644 (when drill-sections
1645 (save-excursion
1646 (goto-char (nth (random* (min 2 (length drill-sections)))
1647 drill-sections))
1648 (org-show-subtree)))
1649 (ignore-errors
1650 (org-display-inline-images t))
1651 (org-cycle-hide-drawers 'all)
1652 (prog1 (org-drill-presentation-prompt)
1653 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1657 (defun org-drill-present-multi-sided-card ()
1658 (with-hidden-comments
1659 (with-hidden-cloze-hints
1660 (with-hidden-cloze-text
1661 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1662 (when drill-sections
1663 (save-excursion
1664 (goto-char (nth (random* (length drill-sections)) drill-sections))
1665 (org-show-subtree)))
1666 (ignore-errors
1667 (org-display-inline-images t))
1668 (org-cycle-hide-drawers 'all)
1669 (prog1 (org-drill-presentation-prompt)
1670 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1673 (defun org-drill-present-multicloze-hide-n (number-to-hide
1674 &optional
1675 force-show-first
1676 force-show-last
1677 force-hide-first)
1678 "Hides NUMBER-TO-HIDE pieces of text that are marked for cloze deletion,
1679 chosen at random.
1680 If NUMBER-TO-HIDE is negative, show only (ABS NUMBER-TO-HIDE) pieces,
1681 hiding all the rest.
1682 If FORCE-HIDE-FIRST is non-nil, force the first piece of text to be one of
1683 the hidden items.
1684 If FORCE-SHOW-FIRST is non-nil, never hide the first piece of text.
1685 If FORCE-SHOW-LAST is non-nil, never hide the last piece of text.
1686 If the number of text pieces in the item is less than
1687 NUMBER-TO-HIDE, then all text pieces will be hidden (except the first or last
1688 items if FORCE-SHOW-FIRST or FORCE-SHOW-LAST is non-nil)."
1689 (with-hidden-comments
1690 (with-hidden-cloze-hints
1691 (let ((item-end nil)
1692 (match-count 0)
1693 (body-start (or (cdr (org-get-property-block))
1694 (point))))
1695 (if (and force-hide-first force-show-first)
1696 (error "FORCE-HIDE-FIRST and FORCE-SHOW-FIRST are mutually exclusive"))
1697 (org-drill-hide-all-subheadings-except nil)
1698 (save-excursion
1699 (outline-next-heading)
1700 (setq item-end (point)))
1701 (save-excursion
1702 (goto-char body-start)
1703 (while (re-search-forward org-drill-cloze-regexp item-end t)
1704 (let ((in-regexp? (save-match-data
1705 (org-pos-in-regexp (match-beginning 0)
1706 org-bracket-link-regexp 1))))
1707 (unless in-regexp?
1708 (incf match-count)))))
1709 (if (minusp number-to-hide)
1710 (setq number-to-hide (+ match-count number-to-hide)))
1711 (when (plusp match-count)
1712 (let* ((positions (shuffle-list (loop for i from 1
1713 to match-count
1714 collect i)))
1715 (match-nums nil)
1716 (cnt nil))
1717 (if force-hide-first
1718 ;; Force '1' to be in the list, and to be the first item
1719 ;; in the list.
1720 (setq positions (cons 1 (remove 1 positions))))
1721 (if force-show-first
1722 (setq positions (remove 1 positions)))
1723 (if force-show-last
1724 (setq positions (remove match-count positions)))
1725 (setq match-nums
1726 (subseq positions
1727 0 (min number-to-hide (length positions))))
1728 ;; (dolist (pos-to-hide match-nums)
1729 (save-excursion
1730 (goto-char body-start)
1731 (setq cnt 0)
1732 (while (re-search-forward org-drill-cloze-regexp item-end t)
1733 (unless (save-match-data
1734 (org-pos-in-regexp (match-beginning 0)
1735 org-bracket-link-regexp 1))
1736 (incf cnt)
1737 (if (memq cnt match-nums)
1738 (org-drill-hide-matched-cloze-text)))))))
1739 ;; (loop
1740 ;; do (re-search-forward org-drill-cloze-regexp
1741 ;; item-end t pos-to-hide)
1742 ;; while (org-pos-in-regexp (match-beginning 0)
1743 ;; org-bracket-link-regexp 1))
1744 ;; (org-drill-hide-matched-cloze-text)))))
1745 (ignore-errors
1746 (org-display-inline-images t))
1747 (org-cycle-hide-drawers 'all)
1748 (prog1 (org-drill-presentation-prompt)
1749 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1750 (org-drill-unhide-clozed-text))))))
1753 (defun org-drill-present-multicloze-hide-nth (to-hide)
1754 "Hide the TO-HIDE'th piece of clozed text. 1 is the first piece. If
1755 TO-HIDE is negative, count backwards, so -1 means the last item, -2
1756 the second to last, etc."
1757 (with-hidden-comments
1758 (with-hidden-cloze-hints
1759 (let ((item-end nil)
1760 (match-count 0)
1761 (body-start (or (cdr (org-get-property-block))
1762 (point)))
1763 (cnt 0))
1764 (org-drill-hide-all-subheadings-except nil)
1765 (save-excursion
1766 (outline-next-heading)
1767 (setq item-end (point)))
1768 (save-excursion
1769 (goto-char body-start)
1770 (while (re-search-forward org-drill-cloze-regexp item-end t)
1771 (let ((in-regexp? (save-match-data
1772 (org-pos-in-regexp (match-beginning 0)
1773 org-bracket-link-regexp 1))))
1774 (unless in-regexp?
1775 (incf match-count)))))
1776 (if (minusp to-hide)
1777 (setq to-hide (+ 1 to-hide match-count)))
1778 (cond
1779 ((or (not (plusp match-count))
1780 (> to-hide match-count))
1781 nil)
1783 (save-excursion
1784 (goto-char body-start)
1785 (setq cnt 0)
1786 (while (re-search-forward org-drill-cloze-regexp item-end t)
1787 (unless (save-match-data
1788 (org-pos-in-regexp (match-beginning 0)
1789 org-bracket-link-regexp 1))
1790 (incf cnt)
1791 (if (= cnt to-hide)
1792 (org-drill-hide-matched-cloze-text)))))))
1793 (ignore-errors
1794 (org-display-inline-images t))
1795 (org-cycle-hide-drawers 'all)
1796 (prog1 (org-drill-presentation-prompt)
1797 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1798 (org-drill-unhide-clozed-text))))))
1801 (defun org-drill-present-multicloze-hide1 ()
1802 "Hides one of the pieces of text that are marked for cloze deletion,
1803 chosen at random."
1804 (org-drill-present-multicloze-hide-n 1))
1807 (defun org-drill-present-multicloze-hide2 ()
1808 "Hides two of the pieces of text that are marked for cloze deletion,
1809 chosen at random."
1810 (org-drill-present-multicloze-hide-n 2))
1813 (defun org-drill-present-multicloze-hide-first ()
1814 "Hides the first piece of text that is marked for cloze deletion."
1815 (org-drill-present-multicloze-hide-nth 1))
1818 (defun org-drill-present-multicloze-hide-last ()
1819 "Hides the last piece of text that is marked for cloze deletion."
1820 (org-drill-present-multicloze-hide-nth -1))
1823 (defun org-drill-present-multicloze-hide1-firstmore ()
1824 "Commonly, hides the FIRST piece of text that is marked for
1825 cloze deletion. Uncommonly, hide one of the other pieces of text,
1826 chosen at random.
1828 The definitions of 'commonly' and 'uncommonly' are determined by
1829 the value of `org-drill-cloze-text-weight'."
1830 ;; The 'firstmore' and 'lastmore' functions used to randomly choose whether
1831 ;; to hide the 'favoured' piece of text. However even when the chance of
1832 ;; hiding it was set quite high (80%), the outcome was too unpredictable over
1833 ;; the small number of repetitions where most learning takes place for each
1834 ;; item. In other words, the actual frequency during the first 10 repetitions
1835 ;; was often very different from 80%. Hence we use modulo instead.
1836 (cond
1837 ((null org-drill-cloze-text-weight)
1838 ;; Behave as hide1cloze
1839 (org-drill-present-multicloze-hide1))
1840 ((not (and (integerp org-drill-cloze-text-weight)
1841 (plusp org-drill-cloze-text-weight)))
1842 (error "Illegal value for org-drill-cloze-text-weight: %S"
1843 org-drill-cloze-text-weight))
1844 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1845 org-drill-cloze-text-weight))
1846 ;; Uncommonly, hide any item except the first
1847 (org-drill-present-multicloze-hide-n 1 t))
1849 ;; Commonly, hide first item
1850 (org-drill-present-multicloze-hide-first))))
1853 (defun org-drill-present-multicloze-show1-lastmore ()
1854 "Commonly, hides all pieces except the last. Uncommonly, shows
1855 any random piece. The effect is similar to 'show1cloze' except
1856 that the last item is much less likely to be the item that is
1857 visible.
1859 The definitions of 'commonly' and 'uncommonly' are determined by
1860 the value of `org-drill-cloze-text-weight'."
1861 (cond
1862 ((null org-drill-cloze-text-weight)
1863 ;; Behave as show1cloze
1864 (org-drill-present-multicloze-show1))
1865 ((not (and (integerp org-drill-cloze-text-weight)
1866 (plusp org-drill-cloze-text-weight)))
1867 (error "Illegal value for org-drill-cloze-text-weight: %S"
1868 org-drill-cloze-text-weight))
1869 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1870 org-drill-cloze-text-weight))
1871 ;; Uncommonly, show any item except the last
1872 (org-drill-present-multicloze-hide-n -1 nil nil t))
1874 ;; Commonly, show the LAST item
1875 (org-drill-present-multicloze-hide-n -1 nil t))))
1878 (defun org-drill-present-multicloze-show1-firstless ()
1879 "Commonly, hides all pieces except one, where the shown piece
1880 is guaranteed NOT to be the first piece. Uncommonly, shows any
1881 random piece. The effect is similar to 'show1cloze' except that
1882 the first item is much less likely to be the item that is
1883 visible.
1885 The definitions of 'commonly' and 'uncommonly' are determined by
1886 the value of `org-drill-cloze-text-weight'."
1887 (cond
1888 ((null org-drill-cloze-text-weight)
1889 ;; Behave as show1cloze
1890 (org-drill-present-multicloze-show1))
1891 ((not (and (integerp org-drill-cloze-text-weight)
1892 (plusp org-drill-cloze-text-weight)))
1893 (error "Illegal value for org-drill-cloze-text-weight: %S"
1894 org-drill-cloze-text-weight))
1895 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1896 org-drill-cloze-text-weight))
1897 ;; Uncommonly, show the first item
1898 (org-drill-present-multicloze-hide-n -1 t))
1900 ;; Commonly, show any item, except the first
1901 (org-drill-present-multicloze-hide-n -1 nil nil t))))
1904 (defun org-drill-present-multicloze-show1 ()
1905 "Similar to `org-drill-present-multicloze-hide1', but hides all
1906 the pieces of text that are marked for cloze deletion, except for one
1907 piece which is chosen at random."
1908 (org-drill-present-multicloze-hide-n -1))
1911 (defun org-drill-present-multicloze-show2 ()
1912 "Similar to `org-drill-present-multicloze-show1', but reveals two
1913 pieces rather than one."
1914 (org-drill-present-multicloze-hide-n -2))
1917 ;; (defun org-drill-present-multicloze-show1 ()
1918 ;; "Similar to `org-drill-present-multicloze-hide1', but hides all
1919 ;; the pieces of text that are marked for cloze deletion, except for one
1920 ;; piece which is chosen at random."
1921 ;; (with-hidden-comments
1922 ;; (with-hidden-cloze-hints
1923 ;; (let ((item-end nil)
1924 ;; (match-count 0)
1925 ;; (body-start (or (cdr (org-get-property-block))
1926 ;; (point))))
1927 ;; (org-drill-hide-all-subheadings-except nil)
1928 ;; (save-excursion
1929 ;; (outline-next-heading)
1930 ;; (setq item-end (point)))
1931 ;; (save-excursion
1932 ;; (goto-char body-start)
1933 ;; (while (re-search-forward org-drill-cloze-regexp item-end t)
1934 ;; (incf match-count)))
1935 ;; (when (plusp match-count)
1936 ;; (let ((match-to-hide (random* match-count)))
1937 ;; (save-excursion
1938 ;; (goto-char body-start)
1939 ;; (dotimes (n match-count)
1940 ;; (re-search-forward org-drill-cloze-regexp
1941 ;; item-end t)
1942 ;; (unless (= n match-to-hide)
1943 ;; (org-drill-hide-matched-cloze-text))))))
1944 ;; (org-display-inline-images t)
1945 ;; (org-cycle-hide-drawers 'all)
1946 ;; (prog1 (org-drill-presentation-prompt)
1947 ;; (org-drill-hide-subheadings-if 'org-drill-entry-p)
1948 ;; (org-drill-unhide-clozed-text))))))
1951 (defun org-drill-present-card-using-text (question &optional answer)
1952 "Present the string QUESTION as the only visible content of the card."
1953 (with-hidden-comments
1954 (with-replaced-entry-text
1955 question
1956 (org-drill-hide-all-subheadings-except nil)
1957 (org-cycle-hide-drawers 'all)
1958 (ignore-errors
1959 (org-display-inline-images t))
1960 (prog1 (org-drill-presentation-prompt)
1961 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
1964 (defun org-drill-present-card-using-multiple-overlays (replacements &optional answer)
1965 "TEXTS is a list of valid values for the 'display' text property.
1966 Present these overlays, in sequence, as the only
1967 visible content of the card."
1968 (with-hidden-comments
1969 (with-replaced-entry-text-multi
1970 replacements
1971 (org-drill-hide-all-subheadings-except nil)
1972 (org-cycle-hide-drawers 'all)
1973 (ignore-errors
1974 (org-display-inline-images t))
1975 (prog1 (org-drill-presentation-prompt)
1976 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
1979 (defun org-drill-entry ()
1980 "Present the current topic for interactive review, as in `org-drill'.
1981 Review will occur regardless of whether the topic is due for review or whether
1982 it meets the definition of a 'review topic' used by `org-drill'.
1984 Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
1985 EDIT if the user chose to exit the drill and edit the current item. Choosing
1986 the latter option leaves the drill session suspended; it can be resumed
1987 later using `org-drill-resume'.
1989 See `org-drill' for more details."
1990 (interactive)
1991 (org-drill-goto-drill-entry-heading)
1992 ;;(unless (org-part-of-drill-entry-p)
1993 ;; (error "Point is not inside a drill entry"))
1994 ;;(unless (org-at-heading-p)
1995 ;; (org-back-to-heading))
1996 (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE"))
1997 (answer-fn 'org-drill-present-default-answer)
1998 (cont nil)
1999 ;; fontification functions in `outline-view-change-hook' can cause big
2000 ;; slowdowns, so we temporarily bind this variable to nil here.
2001 (outline-view-change-hook nil))
2002 (org-save-outline-visibility t
2003 (save-restriction
2004 (org-narrow-to-subtree)
2005 (org-show-subtree)
2006 (org-cycle-hide-drawers 'all)
2008 (let ((presentation-fn (cdr (assoc card-type org-drill-card-type-alist))))
2009 (if (listp presentation-fn)
2010 (psetq answer-fn (or (second presentation-fn)
2011 'org-drill-present-default-answer)
2012 presentation-fn (first presentation-fn)))
2013 (cond
2014 ((null presentation-fn)
2015 (message "%s:%d: Unrecognised card type '%s', skipping..."
2016 (buffer-name) (point) card-type)
2017 (sit-for 0.5)
2018 'skip)
2020 (setq cont (funcall presentation-fn))
2021 (cond
2022 ((not cont)
2023 (message "Quit")
2024 nil)
2025 ((eql cont 'edit)
2026 'edit)
2027 ((eql cont 'skip)
2028 'skip)
2030 (save-excursion
2031 (funcall answer-fn
2032 (lambda () (org-drill-reschedule)))))))))))))
2035 (defun org-drill-entries-pending-p ()
2036 (or *org-drill-again-entries*
2037 (and (not (org-drill-maximum-item-count-reached-p))
2038 (not (org-drill-maximum-duration-reached-p))
2039 (or *org-drill-new-entries*
2040 *org-drill-failed-entries*
2041 *org-drill-young-mature-entries*
2042 *org-drill-old-mature-entries*
2043 *org-drill-overdue-entries*
2044 *org-drill-again-entries*))))
2047 (defun org-drill-pending-entry-count ()
2048 (+ (length *org-drill-new-entries*)
2049 (length *org-drill-failed-entries*)
2050 (length *org-drill-young-mature-entries*)
2051 (length *org-drill-old-mature-entries*)
2052 (length *org-drill-overdue-entries*)
2053 (length *org-drill-again-entries*)))
2056 (defun org-drill-maximum-duration-reached-p ()
2057 "Returns true if the current drill session has continued past its
2058 maximum duration."
2059 (and org-drill-maximum-duration
2060 *org-drill-start-time*
2061 (> (- (float-time (current-time)) *org-drill-start-time*)
2062 (* org-drill-maximum-duration 60))))
2065 (defun org-drill-maximum-item-count-reached-p ()
2066 "Returns true if the current drill session has reached the
2067 maximum number of items."
2068 (and org-drill-maximum-items-per-session
2069 (>= (length *org-drill-done-entries*)
2070 org-drill-maximum-items-per-session)))
2073 (defun org-drill-pop-next-pending-entry ()
2074 (block org-drill-pop-next-pending-entry
2075 (let ((m nil))
2076 (while (or (null m)
2077 (not (org-drill-entry-p m)))
2078 (setq
2080 (cond
2081 ;; First priority is items we failed in a prior session.
2082 ((and *org-drill-failed-entries*
2083 (not (org-drill-maximum-item-count-reached-p))
2084 (not (org-drill-maximum-duration-reached-p)))
2085 (pop-random *org-drill-failed-entries*))
2086 ;; Next priority is overdue items.
2087 ((and *org-drill-overdue-entries*
2088 (not (org-drill-maximum-item-count-reached-p))
2089 (not (org-drill-maximum-duration-reached-p)))
2090 ;; We use `pop', not `pop-random', because we have already
2091 ;; sorted overdue items into a random order which takes
2092 ;; number of days overdue into account.
2093 (pop *org-drill-overdue-entries*))
2094 ;; Next priority is 'young' items.
2095 ((and *org-drill-young-mature-entries*
2096 (not (org-drill-maximum-item-count-reached-p))
2097 (not (org-drill-maximum-duration-reached-p)))
2098 (pop-random *org-drill-young-mature-entries*))
2099 ;; Next priority is newly added items, and older entries.
2100 ;; We pool these into a single group.
2101 ((and (or *org-drill-new-entries*
2102 *org-drill-old-mature-entries*)
2103 (not (org-drill-maximum-item-count-reached-p))
2104 (not (org-drill-maximum-duration-reached-p)))
2105 (cond
2106 ((< (random* (+ (length *org-drill-new-entries*)
2107 (length *org-drill-old-mature-entries*)))
2108 (length *org-drill-new-entries*))
2109 (pop-random *org-drill-new-entries*))
2111 (pop-random *org-drill-old-mature-entries*))))
2112 ;; After all the above are done, last priority is items
2113 ;; that were failed earlier THIS SESSION.
2114 (*org-drill-again-entries*
2115 (pop *org-drill-again-entries*))
2116 (t ; nothing left -- return nil
2117 (return-from org-drill-pop-next-pending-entry nil)))))
2118 m)))
2121 (defun org-drill-entries (&optional resuming-p)
2122 "Returns nil, t, or a list of markers representing entries that were
2123 'failed' and need to be presented again before the session ends.
2125 RESUMING-P is true if we are resuming a suspended drill session."
2126 (block org-drill-entries
2127 (while (org-drill-entries-pending-p)
2128 (let ((m (cond
2129 ((or (not resuming-p)
2130 (null *org-drill-current-item*)
2131 (not (org-drill-entry-p *org-drill-current-item*)))
2132 (org-drill-pop-next-pending-entry))
2133 (t ; resuming a suspended session.
2134 (setq resuming-p nil)
2135 *org-drill-current-item*))))
2136 (setq *org-drill-current-item* m)
2137 (unless m
2138 (error "Unexpectedly ran out of pending drill items"))
2139 (save-excursion
2140 (org-drill-goto-entry m)
2141 (cond
2142 ((not (org-drill-entry-due-p))
2143 ;; The entry is not due anymore. This could arise if the user
2144 ;; suspends a drill session, then drills an individual entry,
2145 ;; then resumes the session.
2146 (message "Entry no longer due, skipping...")
2147 (sit-for 0.3)
2148 nil)
2150 (setq result (org-drill-entry))
2151 (cond
2152 ((null result)
2153 (message "Quit")
2154 (setq end-pos :quit)
2155 (return-from org-drill-entries nil))
2156 ((eql result 'edit)
2157 (setq end-pos (point-marker))
2158 (return-from org-drill-entries nil))
2159 ((eql result 'skip)
2160 nil) ; skip this item
2162 (cond
2163 ((<= result org-drill-failure-quality)
2164 (if *org-drill-again-entries*
2165 (setq *org-drill-again-entries*
2166 (shuffle-list *org-drill-again-entries*)))
2167 (push-end m *org-drill-again-entries*))
2169 (push m *org-drill-done-entries*))))))))))))
2173 (defun org-drill-final-report ()
2174 (let ((pass-percent
2175 (round (* 100 (count-if (lambda (qual)
2176 (> qual org-drill-failure-quality))
2177 *org-drill-session-qualities*))
2178 (max 1 (length *org-drill-session-qualities*))))
2179 (prompt nil))
2180 (setq prompt
2181 (format
2182 "%d items reviewed. Session duration %s.
2183 Recall of reviewed items:
2184 Excellent (5): %3d%% | Near miss (2): %3d%%
2185 Good (4): %3d%% | Failure (1): %3d%%
2186 Hard (3): %3d%% | Abject failure (0): %3d%%
2188 You successfully recalled %d%% of reviewed items (quality > %s)
2189 %d/%d items still await review (%s, %s, %s, %s, %s).
2190 Tomorrow, %d more items will become due for review.
2191 Session finished. Press a key to continue..."
2192 (length *org-drill-done-entries*)
2193 (format-seconds "%h:%.2m:%.2s"
2194 (- (float-time (current-time)) *org-drill-start-time*))
2195 (round (* 100 (count 5 *org-drill-session-qualities*))
2196 (max 1 (length *org-drill-session-qualities*)))
2197 (round (* 100 (count 2 *org-drill-session-qualities*))
2198 (max 1 (length *org-drill-session-qualities*)))
2199 (round (* 100 (count 4 *org-drill-session-qualities*))
2200 (max 1 (length *org-drill-session-qualities*)))
2201 (round (* 100 (count 1 *org-drill-session-qualities*))
2202 (max 1 (length *org-drill-session-qualities*)))
2203 (round (* 100 (count 3 *org-drill-session-qualities*))
2204 (max 1 (length *org-drill-session-qualities*)))
2205 (round (* 100 (count 0 *org-drill-session-qualities*))
2206 (max 1 (length *org-drill-session-qualities*)))
2207 pass-percent
2208 org-drill-failure-quality
2209 (org-drill-pending-entry-count)
2210 (+ (org-drill-pending-entry-count)
2211 *org-drill-dormant-entry-count*)
2212 (propertize
2213 (format "%d failed"
2214 (+ (length *org-drill-failed-entries*)
2215 (length *org-drill-again-entries*)))
2216 'face `(:foreground ,org-drill-failed-count-color))
2217 (propertize
2218 (format "%d overdue"
2219 (length *org-drill-overdue-entries*))
2220 'face `(:foreground ,org-drill-failed-count-color))
2221 (propertize
2222 (format "%d new"
2223 (length *org-drill-new-entries*))
2224 'face `(:foreground ,org-drill-new-count-color))
2225 (propertize
2226 (format "%d young"
2227 (length *org-drill-young-mature-entries*))
2228 'face `(:foreground ,org-drill-mature-count-color))
2229 (propertize
2230 (format "%d old"
2231 (length *org-drill-old-mature-entries*))
2232 'face `(:foreground ,org-drill-mature-count-color))
2233 *org-drill-due-tomorrow-count*
2236 (while (not (input-pending-p))
2237 (message "%s" prompt)
2238 (sit-for 0.5))
2239 (read-char-exclusive)
2241 (if (and *org-drill-session-qualities*
2242 (< pass-percent (- 100 org-drill-forgetting-index)))
2243 (read-char-exclusive
2244 (format
2246 You failed %d%% of the items you reviewed during this session.
2247 %d (%d%%) of all items scanned were overdue.
2249 Are you keeping up with your items, and reviewing them
2250 when they are scheduled? If so, you may want to consider
2251 lowering the value of `org-drill-learn-fraction' slightly in
2252 order to make items appear more frequently over time."
2253 (propertize "WARNING!" 'face 'org-warning)
2254 (- 100 pass-percent)
2255 *org-drill-overdue-entry-count*
2256 (round (* 100 *org-drill-overdue-entry-count*)
2257 (+ *org-drill-dormant-entry-count*
2258 *org-drill-due-entry-count*)))
2259 ))))
2263 (defun org-drill-free-markers (markers)
2264 "MARKERS is a list of markers, all of which will be freed (set to
2265 point nowhere). Alternatively, MARKERS can be 't', in which case
2266 all the markers used by Org-Drill will be freed."
2267 (dolist (m (if (eql t markers)
2268 (append *org-drill-done-entries*
2269 *org-drill-new-entries*
2270 *org-drill-failed-entries*
2271 *org-drill-again-entries*
2272 *org-drill-overdue-entries*
2273 *org-drill-young-mature-entries*
2274 *org-drill-old-mature-entries*)
2275 markers))
2276 (free-marker m)))
2279 (defun org-drill-order-overdue-entries (overdue-data)
2280 (setq *org-drill-overdue-entries*
2281 (mapcar 'car
2282 (sort (shuffle-list overdue-data)
2283 (lambda (a b) (> (cdr a) (cdr b)))))))
2286 (defun org-drill-entry-status ()
2287 "Returns a list (STATUS DUE) where DUE is the number of days overdue,
2288 zero being due today, -1 being scheduled 1 day in the future. STATUS is
2289 one of the following values:
2290 - nil, if the item is not a drill entry, or has an empty body
2291 - :unscheduled
2292 - :future
2293 - :new
2294 - :failed
2295 - :overdue
2296 - :young
2297 - :old
2299 (save-excursion
2300 (unless (org-at-heading-p)
2301 (org-back-to-heading))
2302 (let ((due (org-drill-entry-days-overdue))
2303 (last-int (org-drill-entry-last-interval 1)))
2304 (list
2305 (cond
2306 ((not (org-drill-entry-p))
2307 nil)
2308 ((org-drill-entry-empty-p)
2309 nil) ; skip -- item body is empty
2310 ((null due) ; unscheduled - usually a skipped leech
2311 :unscheduled)
2312 ;; ((eql -1 due)
2313 ;; :tomorrow)
2314 ((minusp due) ; scheduled in the future
2315 :future)
2316 ;; The rest of the stati all denote 'due' items ==========================
2317 ((<= (org-drill-entry-last-quality 9999)
2318 org-drill-failure-quality)
2319 ;; Mature entries that were failed last time are
2320 ;; FAILED, regardless of how young, old or overdue
2321 ;; they are.
2322 :failed)
2323 ((org-drill-entry-new-p)
2324 :new)
2325 ((org-drill-entry-overdue-p due last-int)
2326 ;; Overdue status overrides young versus old
2327 ;; distinction.
2328 ;; Store marker + due, for sorting of overdue entries
2329 :overdue)
2330 ((<= (org-drill-entry-last-interval 9999)
2331 org-drill-days-before-old)
2332 :young)
2334 :old))
2335 due))))
2338 (defun org-drill-progress-message (collected scanned)
2339 (when (zerop (% scanned 50))
2340 (let* ((meter-width 40)
2341 (sym1 (if (oddp (floor scanned (* 50 meter-width))) ?| ?.))
2342 (sym2 (if (eql sym1 ?.) ?| ?.)))
2343 (message "Collecting due drill items:%4d %s%s"
2344 collected
2345 (make-string (% (ceiling scanned 50) meter-width)
2346 sym2)
2347 (make-string (- meter-width (% (ceiling scanned 50) meter-width))
2348 sym1)))))
2351 (defun org-drill (&optional scope resume-p)
2352 "Begin an interactive 'drill session'. The user is asked to
2353 review a series of topics (headers). Each topic is initially
2354 presented as a 'question', often with part of the topic content
2355 hidden. The user attempts to recall the hidden information or
2356 answer the question, then presses a key to reveal the answer. The
2357 user then rates his or her recall or performance on that
2358 topic. This rating information is used to reschedule the topic
2359 for future review.
2361 Org-drill proceeds by:
2363 - Finding all topics (headings) in SCOPE which have either been
2364 used and rescheduled before, or which have a tag that matches
2365 `org-drill-question-tag'.
2367 - All matching topics which are either unscheduled, or are
2368 scheduled for the current date or a date in the past, are
2369 considered to be candidates for the drill session.
2371 - If `org-drill-maximum-items-per-session' is set, a random
2372 subset of these topics is presented. Otherwise, all of the
2373 eligible topics will be presented.
2375 SCOPE determines the scope in which to search for
2376 questions. It accepts the same values as `org-drill-scope',
2377 which see.
2379 If RESUME-P is non-nil, resume a suspended drill session rather
2380 than starting a new one."
2382 (interactive)
2383 (let ((end-pos nil)
2384 (overdue-data nil)
2385 (cnt 0))
2386 (block org-drill
2387 (unless resume-p
2388 (org-drill-free-markers t)
2389 (setq *org-drill-current-item* nil
2390 *org-drill-done-entries* nil
2391 *org-drill-dormant-entry-count* 0
2392 *org-drill-due-entry-count* 0
2393 *org-drill-due-tomorrow-count* 0
2394 *org-drill-overdue-entry-count* 0
2395 *org-drill-new-entries* nil
2396 *org-drill-overdue-entries* nil
2397 *org-drill-young-mature-entries* nil
2398 *org-drill-old-mature-entries* nil
2399 *org-drill-failed-entries* nil
2400 *org-drill-again-entries* nil)
2401 (setq *org-drill-session-qualities* nil)
2402 (setq *org-drill-start-time* (float-time (current-time))))
2403 (setq *random-state* (make-random-state t)) ; reseed RNG
2404 (unwind-protect
2405 (save-excursion
2406 (unless resume-p
2407 (let ((org-trust-scanner-tags t)
2408 (warned-about-id-creation nil))
2409 (org-map-drill-entries
2410 (lambda ()
2411 (org-drill-progress-message
2412 (+ (length *org-drill-new-entries*)
2413 (length *org-drill-overdue-entries*)
2414 (length *org-drill-young-mature-entries*)
2415 (length *org-drill-old-mature-entries*)
2416 (length *org-drill-failed-entries*))
2417 (incf cnt))
2418 (cond
2419 ((not (org-drill-entry-p))
2420 nil) ; skip
2422 (when (and (not warned-about-id-creation)
2423 (null (org-id-get)))
2424 (message (concat "Creating unique IDs for items "
2425 "(slow, but only happens once)"))
2426 (sit-for 0.5)
2427 (setq warned-about-id-creation t))
2428 (org-id-get-create) ; ensure drill entry has unique ID
2429 (destructuring-bind (status due) (org-drill-entry-status)
2430 (case status
2431 (:unscheduled
2432 (incf *org-drill-dormant-entry-count*))
2433 ;; (:tomorrow
2434 ;; (incf *org-drill-dormant-entry-count*)
2435 ;; (incf *org-drill-due-tomorrow-count*))
2436 (:future
2437 (incf *org-drill-dormant-entry-count*)
2438 (if (eq -1 due)
2439 (incf *org-drill-due-tomorrow-count*)))
2440 (:new
2441 (push (point-marker) *org-drill-new-entries*))
2442 (:failed
2443 (push (point-marker) *org-drill-failed-entries*))
2444 (:young
2445 (push (point-marker) *org-drill-young-mature-entries*))
2446 (:overdue
2447 (push (cons (point-marker) due) overdue-data))
2448 (:old
2449 (push (point-marker) *org-drill-old-mature-entries*)))))))
2450 scope)
2451 ;; (let ((due (org-drill-entry-days-overdue))
2452 ;; (last-int (org-drill-entry-last-interval 1)))
2453 ;; (cond
2454 ;; ((org-drill-entry-empty-p)
2455 ;; nil) ; skip -- item body is empty
2456 ;; ((or (null due) ; unscheduled - usually a skipped leech
2457 ;; (minusp due)) ; scheduled in the future
2458 ;; (incf *org-drill-dormant-entry-count*)
2459 ;; (if (eq -1 due)
2460 ;; (incf *org-drill-due-tomorrow-count*)))
2461 ;; ((org-drill-entry-new-p)
2462 ;; (push (point-marker) *org-drill-new-entries*))
2463 ;; ((<= (org-drill-entry-last-quality 9999)
2464 ;; org-drill-failure-quality)
2465 ;; ;; Mature entries that were failed last time are
2466 ;; ;; FAILED, regardless of how young, old or overdue
2467 ;; ;; they are.
2468 ;; (push (point-marker) *org-drill-failed-entries*))
2469 ;; ((org-drill-entry-overdue-p due last-int)
2470 ;; ;; Overdue status overrides young versus old
2471 ;; ;; distinction.
2472 ;; ;; Store marker + due, for sorting of overdue entries
2473 ;; (push (cons (point-marker) due) overdue-data))
2474 ;; ((<= (org-drill-entry-last-interval 9999)
2475 ;; org-drill-days-before-old)
2476 ;; ;; Item is 'young'.
2477 ;; (push (point-marker)
2478 ;; *org-drill-young-mature-entries*))
2479 ;; (t
2480 ;; (push (point-marker)
2481 ;; *org-drill-old-mature-entries*))))
2482 ;; Order 'overdue' items so that the most overdue will tend to
2483 ;; come up for review first, while keeping exact order random
2484 (org-drill-order-overdue-entries overdue-data)
2485 (setq *org-drill-overdue-entry-count*
2486 (length *org-drill-overdue-entries*))))
2487 (setq *org-drill-due-entry-count* (org-drill-pending-entry-count))
2488 (cond
2489 ((and (null *org-drill-new-entries*)
2490 (null *org-drill-failed-entries*)
2491 (null *org-drill-overdue-entries*)
2492 (null *org-drill-young-mature-entries*)
2493 (null *org-drill-old-mature-entries*))
2494 (message "I did not find any pending drill items."))
2496 (org-drill-entries resume-p)
2497 (message "Drill session finished!"))))
2498 (progn
2499 (unless end-pos
2500 (org-drill-free-markers *org-drill-done-entries*)))))
2501 (cond
2502 (end-pos
2503 (when (markerp end-pos)
2504 (org-drill-goto-entry end-pos)
2505 (org-reveal)
2506 (org-show-entry))
2507 (let ((keystr (command-keybinding-to-string 'org-drill-resume)))
2508 (message
2509 "You can continue the drill session with the command `org-drill-resume'.%s"
2510 (if keystr (format "\nYou can run this command by pressing %s." keystr)
2511 ""))))
2513 (org-drill-final-report)
2514 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
2515 (org-drill-save-optimal-factor-matrix))
2516 (if org-drill-save-buffers-after-drill-sessions-p
2517 (save-some-buffers))
2518 (message "Drill session finished!")
2519 ))))
2522 (defun org-drill-save-optimal-factor-matrix ()
2523 (message "Saving optimal factor matrix...")
2524 (customize-save-variable 'org-drill-optimal-factor-matrix
2525 org-drill-optimal-factor-matrix))
2528 (defun org-drill-cram (&optional scope)
2529 "Run an interactive drill session in 'cram mode'. In cram mode,
2530 all drill items are considered to be due for review, unless they
2531 have been reviewed within the last `org-drill-cram-hours'
2532 hours."
2533 (interactive)
2534 (let ((*org-drill-cram-mode* t))
2535 (org-drill scope)))
2538 (defun org-drill-tree ()
2539 "Run an interactive drill session using drill items within the
2540 subtree at point."
2541 (interactive)
2542 (org-drill 'tree))
2545 (defun org-drill-directory ()
2546 "Run an interactive drill session using drill items from all org
2547 files in the same directory as the current file."
2548 (interactive)
2549 (org-drill 'directory))
2552 (defun org-drill-again (&optional scope)
2553 "Run a new drill session, but try to use leftover due items that
2554 were not reviewed during the last session, rather than scanning for
2555 unreviewed items. If there are no leftover items in memory, a full
2556 scan will be performed."
2557 (interactive)
2558 (cond
2559 ((plusp (org-drill-pending-entry-count))
2560 (org-drill-free-markers *org-drill-done-entries*)
2561 (if (markerp *org-drill-current-item*)
2562 (free-marker *org-drill-current-item*))
2563 (setq *org-drill-start-time* (float-time (current-time))
2564 *org-drill-done-entries* nil
2565 *org-drill-current-item* nil)
2566 (org-drill scope t))
2568 (org-drill scope))))
2572 (defun org-drill-resume ()
2573 "Resume a suspended drill session. Sessions are suspended by
2574 exiting them with the `edit' or `quit' options."
2575 (interactive)
2576 (cond
2577 ((org-drill-entries-pending-p)
2578 (org-drill nil t))
2579 ((and (plusp (org-drill-pending-entry-count))
2580 ;; Current drill session is finished, but there are still
2581 ;; more items which need to be reviewed.
2582 (y-or-n-p (format
2583 "You have finished the drill session. However, %d items still
2584 need reviewing. Start a new drill session? "
2585 (org-drill-pending-entry-count))))
2586 (org-drill-again))
2588 (message "You have finished the drill session."))))
2591 (defun org-drill-strip-entry-data ()
2592 (dolist (prop org-drill-scheduling-properties)
2593 (org-delete-property prop))
2594 (org-schedule t))
2597 (defun org-drill-strip-all-data (&optional scope)
2598 "Delete scheduling data from every drill entry in scope. This
2599 function may be useful if you want to give your collection of
2600 entries to someone else. Scope defaults to the current buffer,
2601 and is specified by the argument SCOPE, which accepts the same
2602 values as `org-drill-scope'."
2603 (interactive)
2604 (when (yes-or-no-p
2605 "Delete scheduling data from ALL items in scope: are you sure?")
2606 (cond
2607 ((null scope)
2608 ;; Scope is the current buffer. This means we can use
2609 ;; `org-delete-property-globally', which is faster.
2610 (dolist (prop org-drill-scheduling-properties)
2611 (org-delete-property-globally prop))
2612 (org-map-drill-entries (lambda () (org-schedule t)) scope))
2614 (org-map-drill-entries 'org-drill-strip-entry-data scope)))
2615 (message "Done.")))
2619 (defun org-drill-add-cloze-fontification ()
2620 (when org-drill-use-visible-cloze-face-p
2621 (font-lock-add-keywords 'org-mode
2622 org-drill-cloze-keywords
2623 nil)))
2625 (add-hook 'org-mode-hook 'org-drill-add-cloze-fontification)
2627 (org-drill-add-cloze-fontification)
2630 ;;; Synching card collections =================================================
2633 (defvar *org-drill-dest-id-table* (make-hash-table :test 'equal))
2636 (defun org-drill-copy-entry-to-other-buffer (dest &optional path)
2637 "Copy the subtree at point to the buffer DEST. The copy will receive
2638 the tag 'imported'."
2639 (block org-drill-copy-entry-to-other-buffer
2640 (save-excursion
2641 (let ((src (current-buffer))
2642 (m nil))
2643 (flet ((paste-tree-here (&optional level)
2644 (org-paste-subtree level)
2645 (org-drill-strip-entry-data)
2646 (org-toggle-tag "imported" 'on)
2647 (org-map-drill-entries
2648 (lambda ()
2649 (let ((id (org-id-get)))
2650 (org-drill-strip-entry-data)
2651 (unless (gethash id *org-drill-dest-id-table*)
2652 (puthash id (point-marker)
2653 *org-drill-dest-id-table*))))
2654 'tree)))
2655 (unless path
2656 (setq path (org-get-outline-path)))
2657 (org-copy-subtree)
2658 (switch-to-buffer dest)
2659 (setq m
2660 (condition-case nil
2661 (org-find-olp path t)
2662 (error ; path does not exist in DEST
2663 (return-from org-drill-copy-entry-to-other-buffer
2664 (cond
2665 ((cdr path)
2666 (org-drill-copy-entry-to-other-buffer
2667 dest (butlast path)))
2669 ;; We've looked all the way up the path
2670 ;; Default to appending to the end of DEST
2671 (goto-char (point-max))
2672 (newline)
2673 (paste-tree-here)))))))
2674 (goto-char m)
2675 (outline-next-heading)
2676 (newline)
2677 (forward-line -1)
2678 (paste-tree-here (1+ (or (org-current-level) 0)))
2679 )))))
2683 (defun org-drill-merge-buffers (src &optional dest ignore-new-items-p)
2684 "SRC and DEST are two org mode buffers containing drill items.
2685 For each drill item in DEST that shares an ID with an item in SRC,
2686 overwrite scheduling data in DEST with data taken from the item in SRC.
2687 This is intended for use when two people are sharing a set of drill items,
2688 one person has made some updates to the item set, and the other person
2689 wants to migrate to the updated set without losing their scheduling data.
2691 By default, any drill items in SRC which do not exist in DEST are
2692 copied into DEST. We attempt to place the copied item in the
2693 equivalent location in DEST to its location in SRC, by matching
2694 the heading hierarchy. However if IGNORE-NEW-ITEMS-P is non-nil,
2695 we simply ignore any items that do not exist in DEST, and do not
2696 copy them across."
2697 (interactive "bImport scheduling info from which buffer?")
2698 (unless dest
2699 (setq dest (current-buffer)))
2700 (setq src (get-buffer src)
2701 dest (get-buffer dest))
2702 (when (yes-or-no-p
2703 (format
2704 (concat "About to overwrite all scheduling data for drill items in `%s' "
2705 "with information taken from matching items in `%s'. Proceed? ")
2706 (buffer-name dest) (buffer-name src)))
2707 ;; Compile list of all IDs in the destination buffer.
2708 (clrhash *org-drill-dest-id-table*)
2709 (with-current-buffer dest
2710 (org-map-drill-entries
2711 (lambda ()
2712 (let ((this-id (org-id-get)))
2713 (when this-id
2714 (puthash this-id (point-marker) *org-drill-dest-id-table*))))
2715 'file))
2716 ;; Look through all entries in source buffer.
2717 (with-current-buffer src
2718 (org-map-drill-entries
2719 (lambda ()
2720 (let ((id (org-id-get))
2721 (last-quality nil) (last-reviewed nil)
2722 (scheduled-time nil))
2723 (cond
2724 ((or (null id)
2725 (not (org-drill-entry-p)))
2726 nil)
2727 ((gethash id *org-drill-dest-id-table*)
2728 ;; This entry matches an entry in dest. Retrieve all its
2729 ;; scheduling data, then go to the matching location in dest
2730 ;; and write the data.
2731 (let ((marker (gethash id *org-drill-dest-id-table*)))
2732 (destructuring-bind (last-interval repetitions failures
2733 total-repeats meanq ease)
2734 (org-drill-get-item-data)
2735 (setq last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")
2736 last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")
2737 scheduled-time (org-get-scheduled-time (point)))
2738 (save-excursion
2739 ;; go to matching entry in destination buffer
2740 (switch-to-buffer (marker-buffer marker))
2741 (goto-char marker)
2742 (org-drill-strip-entry-data)
2743 (unless (zerop total-repeats)
2744 (org-drill-store-item-data last-interval repetitions failures
2745 total-repeats meanq ease)
2746 (if last-quality
2747 (org-set-property "LAST_QUALITY" last-quality)
2748 (org-delete-property "LAST_QUALITY"))
2749 (if last-reviewed
2750 (org-set-property "LAST_REVIEWED" last-reviewed)
2751 (org-delete-property "LAST_REVIEWED"))
2752 (if scheduled-time
2753 (org-schedule nil scheduled-time)))))
2754 (remhash id *org-drill-dest-id-table*)
2755 (free-marker marker)))
2757 ;; item in SRC has ID, but no matching ID in DEST.
2758 ;; It must be a new item that does not exist in DEST.
2759 ;; Copy the entire item to the *end* of DEST.
2760 (unless ignore-new-items-p
2761 (org-drill-copy-entry-to-other-buffer dest))))))
2762 'file))
2763 ;; Finally: there may be some items in DEST which are not in SRC, and
2764 ;; which have been scheduled by another user of DEST. Clear out the
2765 ;; scheduling info from all the unmatched items in DEST.
2766 (with-current-buffer dest
2767 (maphash (lambda (id m)
2768 (goto-char m)
2769 (org-drill-strip-entry-data)
2770 (free-marker m))
2771 *org-drill-dest-id-table*))))
2775 ;;; Card types for learning languages =========================================
2777 ;;; Get spell-number.el from:
2778 ;;; http://www.emacswiki.org/emacs/spell-number.el
2779 (autoload 'spelln-integer-in-words "spell-number")
2782 ;;; `conjugate' card type =====================================================
2783 ;;; See spanish.org for usage
2785 (defvar org-drill-verb-tense-alist
2786 '(("present" "tomato")
2787 ("simple present" "tomato")
2788 ("present indicative" "tomato")
2789 ;; past tenses
2790 ("past" "purple")
2791 ("simple past" "purple")
2792 ("preterite" "purple")
2793 ("imperfect" "darkturquoise")
2794 ("present perfect" "royalblue")
2795 ;; future tenses
2796 ("future" "green")
2797 ;; moods (backgrounds).
2798 ("indicative" nil) ; default
2799 ("subjunctive" "medium blue")
2800 ("conditional" "grey30")
2801 ("negative imperative" "red4")
2802 ("positive imperative" "darkgreen")
2804 "Alist where each entry has the form (TENSE COLOUR), where
2805 TENSE is a string naming a tense in which verbs can be
2806 conjugated, and COLOUR is a string specifying a foreground colour
2807 which will be used by `org-drill-present-verb-conjugation' and
2808 `org-drill-show-answer-verb-conjugation' to fontify the verb and
2809 the name of the tense.")
2812 (defun org-drill-get-verb-conjugation-info ()
2813 "Auxiliary function used by `org-drill-present-verb-conjugation' and
2814 `org-drill-show-answer-verb-conjugation'."
2815 (let ((infinitive (org-entry-get (point) "VERB_INFINITIVE" t))
2816 (inf-hint (org-entry-get (point) "VERB_INFINITIVE_HINT" t))
2817 (translation (org-entry-get (point) "VERB_TRANSLATION" t))
2818 (tense (org-entry-get (point) "VERB_TENSE" nil))
2819 (mood (org-entry-get (point) "VERB_MOOD" nil))
2820 (highlight-face nil))
2821 (unless (and infinitive translation (or tense mood))
2822 (error "Missing information for verb conjugation card (%s, %s, %s, %s) at %s"
2823 infinitive translation tense mood (point)))
2824 (setq tense (if tense (downcase (car (read-from-string tense))))
2825 mood (if mood (downcase (car (read-from-string mood))))
2826 infinitive (car (read-from-string infinitive))
2827 inf-hint (if inf-hint (car (read-from-string inf-hint)))
2828 translation (car (read-from-string translation)))
2829 (setq highlight-face
2830 (list :foreground
2831 (or (second (assoc-string tense org-drill-verb-tense-alist t))
2832 "hotpink")
2833 :background
2834 (second (assoc-string mood org-drill-verb-tense-alist t))))
2835 (setq infinitive (propertize infinitive 'face highlight-face))
2836 (setq translation (propertize translation 'face highlight-face))
2837 (if tense (setq tense (propertize tense 'face highlight-face)))
2838 (if mood (setq mood (propertize mood 'face highlight-face)))
2839 (list infinitive inf-hint translation tense mood)))
2842 (defun org-drill-present-verb-conjugation ()
2843 "Present a drill entry whose card type is 'conjugate'."
2844 (flet ((tense-and-mood-to-string
2845 (tense mood)
2846 (cond
2847 ((and tense mood)
2848 (format "%s tense, %s mood" tense mood))
2849 (tense
2850 (format "%s tense" tense))
2851 (mood
2852 (format "%s mood" mood)))))
2853 (destructuring-bind (infinitive inf-hint translation tense mood)
2854 (org-drill-get-verb-conjugation-info)
2855 (org-drill-present-card-using-text
2856 (cond
2857 ((zerop (random* 2))
2858 (format "\nTranslate the verb\n\n%s\n\nand conjugate for the %s.\n\n"
2859 infinitive (tense-and-mood-to-string tense mood)))
2862 (format "\nGive the verb that means\n\n%s %s\n
2863 and conjugate for the %s.\n\n"
2864 translation
2865 (if inf-hint (format " [HINT: %s]" inf-hint) "")
2866 (tense-and-mood-to-string tense mood))))))))
2869 (defun org-drill-show-answer-verb-conjugation (reschedule-fn)
2870 "Show the answer for a drill item whose card type is 'conjugate'.
2871 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
2872 returns its return value."
2873 (destructuring-bind (infinitive inf-hint translation tense mood)
2874 (org-drill-get-verb-conjugation-info)
2875 (with-replaced-entry-heading
2876 (format "%s of %s ==> %s\n\n"
2877 (capitalize
2878 (cond
2879 ((and tense mood)
2880 (format "%s tense, %s mood" tense mood))
2881 (tense
2882 (format "%s tense" tense))
2883 (mood
2884 (format "%s mood" mood))))
2885 infinitive translation)
2886 (funcall reschedule-fn))))
2889 ;;; `translate_number' card type ==============================================
2890 ;;; See spanish.org for usage
2892 (defvar *drilled-number* 0)
2893 (defvar *drilled-number-direction* 'to-english)
2895 (defun org-drill-present-translate-number ()
2896 (let ((num-min (read (org-entry-get (point) "DRILL_NUMBER_MIN")))
2897 (num-max (read (org-entry-get (point) "DRILL_NUMBER_MAX")))
2898 (language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
2899 (highlight-face 'font-lock-warning-face))
2900 (cond
2901 ((not (fboundp 'spelln-integer-in-words))
2902 (message "`spell-number.el' not loaded, skipping 'translate_number' card...")
2903 (sit-for 0.5)
2904 'skip)
2905 ((not (and (numberp num-min) (numberp num-max) language))
2906 (error "Missing language or minimum or maximum numbers for number card"))
2908 (if (> num-min num-max)
2909 (psetf num-min num-max
2910 num-max num-min))
2911 (setq *drilled-number*
2912 (+ num-min (random* (abs (1+ (- num-max num-min))))))
2913 (setq *drilled-number-direction*
2914 (if (zerop (random* 2)) 'from-english 'to-english))
2915 (org-drill-present-card-using-text
2916 (if (eql 'to-english *drilled-number-direction*)
2917 (format "\nTranslate into English:\n\n%s\n"
2918 (let ((spelln-language language))
2919 (propertize
2920 (spelln-integer-in-words *drilled-number*)
2921 'face highlight-face)))
2922 (format "\nTranslate into %s:\n\n%s\n"
2923 (capitalize (format "%s" language))
2924 (let ((spelln-language 'english-gb))
2925 (propertize
2926 (spelln-integer-in-words *drilled-number*)
2927 'face highlight-face)))))))))
2930 (defun org-drill-show-answer-translate-number (reschedule-fn)
2931 (let* ((language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
2932 (highlight-face 'font-lock-warning-face)
2933 (non-english
2934 (let ((spelln-language language))
2935 (propertize (spelln-integer-in-words *drilled-number*)
2936 'face highlight-face)))
2937 (english
2938 (let ((spelln-language 'english-gb))
2939 (propertize (spelln-integer-in-words *drilled-number*)
2940 'face 'highlight-face))))
2941 (with-replaced-entry-text
2942 (cond
2943 ((eql 'to-english *drilled-number-direction*)
2944 (format "\nThe English translation of %s is:\n\n%s\n"
2945 non-english english))
2947 (format "\nThe %s translation of %s is:\n\n%s\n"
2948 (capitalize (format "%s" language))
2949 english non-english)))
2950 (funcall reschedule-fn))))
2953 ;;; `spanish_verb' card type ==================================================
2954 ;;; Not very interesting, but included to demonstrate how a presentation
2955 ;;; function can manipulate which subheading are hidden versus shown.
2958 (defun org-drill-present-spanish-verb ()
2959 (let ((prompt nil)
2960 (reveal-headings nil))
2961 (with-hidden-comments
2962 (with-hidden-cloze-hints
2963 (with-hidden-cloze-text
2964 (case (random* 6)
2966 (org-drill-hide-all-subheadings-except '("Infinitive"))
2967 (setq prompt
2968 (concat "Translate this Spanish verb, and conjugate it "
2969 "for the *present* tense.")
2970 reveal-headings '("English" "Present Tense" "Notes")))
2972 (org-drill-hide-all-subheadings-except '("English"))
2973 (setq prompt (concat "For the *present* tense, conjugate the "
2974 "Spanish translation of this English verb.")
2975 reveal-headings '("Infinitive" "Present Tense" "Notes")))
2977 (org-drill-hide-all-subheadings-except '("Infinitive"))
2978 (setq prompt (concat "Translate this Spanish verb, and "
2979 "conjugate it for the *past* tense.")
2980 reveal-headings '("English" "Past Tense" "Notes")))
2982 (org-drill-hide-all-subheadings-except '("English"))
2983 (setq prompt (concat "For the *past* tense, conjugate the "
2984 "Spanish translation of this English verb.")
2985 reveal-headings '("Infinitive" "Past Tense" "Notes")))
2987 (org-drill-hide-all-subheadings-except '("Infinitive"))
2988 (setq prompt (concat "Translate this Spanish verb, and "
2989 "conjugate it for the *future perfect* tense.")
2990 reveal-headings '("English" "Future Perfect Tense" "Notes")))
2992 (org-drill-hide-all-subheadings-except '("English"))
2993 (setq prompt (concat "For the *future perfect* tense, conjugate the "
2994 "Spanish translation of this English verb.")
2995 reveal-headings '("Infinitive" "Future Perfect Tense" "Notes"))))
2996 (org-cycle-hide-drawers 'all)
2997 (prog1 (org-drill-presentation-prompt)
2998 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
3001 (provide 'org-drill)