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