Move `org-key' and `org-defkey' into "org-macs.el"
[org-mode.git] / contrib / lisp / org-drill.el
blob7c4a29930d6baaa83cbab08a1a257694b2e1b384
1 ;; -*- coding: utf-8-unix -*-
2 ;;; org-drill.el - Self-testing using spaced repetition
3 ;;;
4 ;;; Copyright (C) 2010-2015 Paul Sexton
5 ;;;
6 ;;; Author: Paul Sexton <eeeickythump@gmail.com>
7 ;;; Version: 2.4.7
8 ;;; Keywords: flashcards, memory, learning, memorization
9 ;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
10 ;;;
11 ;;; This file is not part of GNU Emacs.
12 ;;;
13 ;;; This program is free software; you can redistribute it and/or modify
14 ;;; it under the terms of the GNU General Public License as published by
15 ;;; the Free Software Foundation, either version 3 of the License, or
16 ;;; (at your option) any later version.
17 ;;;
18 ;;; This program is distaributed in the hope that it will be useful,
19 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;;; GNU General Public License for more details.
22 ;;;
23 ;;; You should have received a copy of the GNU General Public License
24 ;;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ;;;
26 ;;;
27 ;;; Synopsis
28 ;;; ========
29 ;;;
30 ;;; Uses the SuperMemo spaced repetition algorithms to conduct interactive
31 ;;; "drill sessions", where the material to be remembered is presented to the
32 ;;; student in random order. The student rates his or her recall of each item,
33 ;;; and this information is used to schedule the item for later revision.
34 ;;;
35 ;;; Each drill session can be restricted to topics in the current buffer
36 ;;; (default), one or several files, all agenda files, or a subtree. A single
37 ;;; topic can also be drilled.
38 ;;;
39 ;;; Different "card types" can be defined, which present their information to
40 ;;; the student in different ways.
41 ;;;
42 ;;; See the file README.org for more detailed documentation.
45 (eval-when-compile (require 'cl))
46 (eval-when-compile (require 'hi-lock))
47 (require 'cl-lib)
48 (require 'hi-lock)
49 (require 'org)
50 (require 'org-id)
51 (require 'org-learn)
52 (require 'savehist)
55 (defgroup org-drill nil
56 "Options concerning interactive drill sessions in Org mode (org-drill)."
57 :tag "Org-Drill"
58 :group 'org-link)
62 (defcustom org-drill-question-tag
63 "drill"
64 "Tag which topics must possess in order to be identified as review topics
65 by `org-drill'."
66 :group 'org-drill
67 :type 'string)
70 (defcustom org-drill-maximum-items-per-session
72 "Each drill session will present at most this many topics for review.
73 Nil means unlimited."
74 :group 'org-drill
75 :type '(choice integer (const nil)))
79 (defcustom org-drill-maximum-duration
81 "Maximum duration of a drill session, in minutes.
82 Nil means unlimited."
83 :group 'org-drill
84 :type '(choice integer (const nil)))
87 (defcustom org-drill-failure-quality
89 "If the quality of recall for an item is this number or lower,
90 it is regarded as an unambiguous failure, and the repetition
91 interval for the card is reset to 0 days. If the quality is higher
92 than this number, it is regarded as successfully recalled, but the
93 time interval to the next repetition will be lowered if the quality
94 was near to a fail.
96 By default this is 2, for SuperMemo-like behaviour. For
97 Mnemosyne-like behaviour, set it to 1. Other values are not
98 really sensible."
99 :group 'org-drill
100 :type '(choice (const 2) (const 1)))
103 (defcustom org-drill-forgetting-index
105 "What percentage of items do you consider it is 'acceptable' to
106 forget each drill session? The default is 10%. A warning message
107 is displayed at the end of the session if the percentage forgotten
108 climbs above this number."
109 :group 'org-drill
110 :type 'integer)
113 (defcustom org-drill-leech-failure-threshold
115 "If an item is forgotten more than this many times, it is tagged
116 as a 'leech' item."
117 :group 'org-drill
118 :type '(choice integer (const nil)))
121 (defcustom org-drill-leech-method
122 'skip
123 "How should 'leech items' be handled during drill sessions?
124 Possible values:
125 - nil :: Leech items are treated the same as normal items.
126 - skip :: Leech items are not included in drill sessions.
127 - warn :: Leech items are still included in drill sessions,
128 but a warning message is printed when each leech item is
129 presented."
130 :group 'org-drill
131 :type '(choice (const warn) (const skip) (const nil)))
134 (defface org-drill-visible-cloze-face
135 '((t (:foreground "darkseagreen")))
136 "The face used to hide the contents of cloze phrases."
137 :group 'org-drill)
140 (defface org-drill-visible-cloze-hint-face
141 '((t (:foreground "dark slate blue")))
142 "The face used to hide the contents of cloze phrases."
143 :group 'org-drill)
146 (defface org-drill-hidden-cloze-face
147 '((t (:foreground "deep sky blue" :background "blue")))
148 "The face used to hide the contents of cloze phrases."
149 :group 'org-drill)
152 (defcustom org-drill-use-visible-cloze-face-p
154 "Use a special face to highlight cloze-deleted text in org mode
155 buffers?"
156 :group 'org-drill
157 :type 'boolean)
160 (defcustom org-drill-hide-item-headings-p
162 "Conceal the contents of the main heading of each item during drill
163 sessions? You may want to enable this behaviour if item headings or tags
164 contain information that could 'give away' the answer."
165 :group 'org-drill
166 :type 'boolean)
169 (defcustom org-drill-new-count-color
170 "royal blue"
171 "Foreground colour used to display the count of remaining new items
172 during a drill session."
173 :group 'org-drill
174 :type 'color)
176 (defcustom org-drill-mature-count-color
177 "green"
178 "Foreground colour used to display the count of remaining mature items
179 during a drill session. Mature items are due for review, but are not new."
180 :group 'org-drill
181 :type 'color)
183 (defcustom org-drill-failed-count-color
184 "red"
185 "Foreground colour used to display the count of remaining failed items
186 during a drill session."
187 :group 'org-drill
188 :type 'color)
190 (defcustom org-drill-done-count-color
191 "sienna"
192 "Foreground colour used to display the count of reviewed items
193 during a drill session."
194 :group 'org-drill
195 :type 'color)
197 (defcustom org-drill-left-cloze-delimiter
199 "String used within org buffers to delimit cloze deletions."
200 :group 'org-drill
201 :type 'string)
203 (defcustom org-drill-right-cloze-delimiter
205 "String used within org buffers to delimit cloze deletions."
206 :group 'org-drill
207 :type 'string)
210 (setplist 'org-drill-cloze-overlay-defaults
211 `(display ,(format "%s...%s"
212 org-drill-left-cloze-delimiter
213 org-drill-right-cloze-delimiter)
214 face org-drill-hidden-cloze-face
215 window t))
217 (setplist 'org-drill-hidden-text-overlay
218 '(invisible t))
220 (setplist 'org-drill-replaced-text-overlay
221 '(display "Replaced text"
222 face default
223 window t))
225 (add-hook 'org-font-lock-set-keywords-hook 'org-drill-add-cloze-fontification)
228 (defvar org-drill-hint-separator "||"
229 "String which, if it occurs within a cloze expression, signifies that the
230 rest of the expression after the string is a `hint', to be displayed instead of
231 the hidden cloze during a test.")
233 (defun org-drill--compute-cloze-regexp ()
234 (concat "\\("
235 (regexp-quote org-drill-left-cloze-delimiter)
236 "[[:cntrl:][:graph:][:space:]]+?\\)\\(\\|"
237 (regexp-quote org-drill-hint-separator)
238 ".+?\\)\\("
239 (regexp-quote org-drill-right-cloze-delimiter)
240 "\\)"))
242 (defun org-drill--compute-cloze-keywords ()
243 (list (list (org-drill--compute-cloze-regexp)
244 (copy-list '(1 'org-drill-visible-cloze-face nil))
245 (copy-list '(2 'org-drill-visible-cloze-hint-face t))
246 (copy-list '(3 'org-drill-visible-cloze-face nil))
249 (defvar-local org-drill-cloze-regexp
250 (org-drill--compute-cloze-regexp))
253 (defvar-local org-drill-cloze-keywords
254 (org-drill--compute-cloze-keywords))
257 ;; Variables defining what keys can be pressed during drill sessions to quit the
258 ;; session, edit the item, etc.
259 (defvar org-drill--quit-key ?q
260 "If this character is pressed during a drill session, quit the session.")
261 (defvar org-drill--edit-key ?e
262 "If this character is pressed during a drill session, suspend the session
263 with the cursor at the current item..")
264 (defvar org-drill--help-key ??
265 "If this character is pressed during a drill session, show help.")
266 (defvar org-drill--skip-key ?s
267 "If this character is pressed during a drill session, skip to the next
268 item.")
269 (defvar org-drill--tags-key ?t
270 "If this character is pressed during a drill session, edit the tags for
271 the current item.")
274 (defcustom org-drill-card-type-alist
275 '((nil org-drill-present-simple-card)
276 ("simple" org-drill-present-simple-card)
277 ("twosided" org-drill-present-two-sided-card nil t)
278 ("multisided" org-drill-present-multi-sided-card nil t)
279 ("hide1cloze" org-drill-present-multicloze-hide1)
280 ("hide2cloze" org-drill-present-multicloze-hide2)
281 ("show1cloze" org-drill-present-multicloze-show1)
282 ("show2cloze" org-drill-present-multicloze-show2)
283 ("multicloze" org-drill-present-multicloze-hide1)
284 ("hidefirst" org-drill-present-multicloze-hide-first)
285 ("hidelast" org-drill-present-multicloze-hide-last)
286 ("hide1_firstmore" org-drill-present-multicloze-hide1-firstmore)
287 ("show1_lastmore" org-drill-present-multicloze-show1-lastmore)
288 ("show1_firstless" org-drill-present-multicloze-show1-firstless)
289 ("conjugate"
290 org-drill-present-verb-conjugation
291 org-drill-show-answer-verb-conjugation)
292 ("decline_noun"
293 org-drill-present-noun-declension
294 org-drill-show-answer-noun-declension)
295 ("spanish_verb" org-drill-present-spanish-verb)
296 ("translate_number" org-drill-present-translate-number))
297 "Alist associating card types with presentation functions. Each
298 entry in the alist takes the form:
300 ;;; (CARDTYPE QUESTION-FN [ANSWER-FN DRILL-EMPTY-P])
302 Where CARDTYPE is a string or nil (for default), and QUESTION-FN
303 is a function which takes no arguments and returns a boolean
304 value.
306 When supplied, ANSWER-FN is a function that takes one argument --
307 that argument is a function of no arguments, which when called,
308 prompts the user to rate their recall and performs rescheduling
309 of the drill item. ANSWER-FN is called with the point on the
310 active item's heading, just prior to displaying the item's
311 'answer'. It can therefore be used to modify the appearance of
312 the answer. ANSWER-FN must call its argument before returning.
314 When supplied, DRILL-EMPTY-P is a boolean value, default nil.
315 When non-nil, cards of this type will be presented during tests
316 even if their bodies are empty."
317 :group 'org-drill
318 :type '(alist :key-type (choice string (const nil))
319 :value-type function))
322 (defcustom org-drill-scope
323 'file
324 "The scope in which to search for drill items when conducting a
325 drill session. This can be any of:
327 file The current buffer, respecting the restriction if any.
328 This is the default.
329 tree The subtree started with the entry at point
330 file-no-restriction The current buffer, without restriction
331 file-with-archives The current buffer, and any archives associated with it.
332 agenda All agenda files
333 agenda-with-archives All agenda files with any archive files associated
334 with them.
335 directory All files with the extension '.org' in the same
336 directory as the current file (includes the current
337 file if it is an .org file.)
338 (FILE1 FILE2 ...) If this is a list, all files in the list will be scanned.
340 ;; Note -- meanings differ slightly from the argument to org-map-entries:
341 ;; 'file' means current file/buffer, respecting any restriction
342 ;; 'file-no-restriction' means current file/buffer, ignoring restrictions
343 ;; 'directory' means all *.org files in current directory
344 :group 'org-drill
345 :type '(choice (const :tag "The current buffer, respecting the restriction if any." file)
346 (const :tag "The subtree started with the entry at point" tree)
347 (const :tag "The current buffer, without restriction" file-no-restriction)
348 (const :tag "The current buffer, and any archives associated with it." file-with-archives)
349 (const :tag "All agenda files" agenda)
350 (const :tag "All agenda files with any archive files associated with them." agenda-with-archives)
351 (const :tag "All files with the extension '.org' in the same directory as the current file (includes the current file if it is an .org file.)" directory)
352 (repeat :tag "List of files to scan for drill items." file)))
355 (defcustom org-drill-match
357 "If non-nil, a string specifying a tags/property/TODO query. During
358 drill sessions, only items that match this query will be considered."
359 :group 'org-drill
360 :type '(choice (const nil) string))
363 (defcustom org-drill-save-buffers-after-drill-sessions-p
365 "If non-nil, prompt to save all modified buffers after a drill session
366 finishes."
367 :group 'org-drill
368 :type 'boolean)
371 (defcustom org-drill-spaced-repetition-algorithm
372 'sm5
373 "Which SuperMemo spaced repetition algorithm to use for scheduling items.
374 Available choices are:
375 - SM2 :: the SM2 algorithm, used in SuperMemo 2.0
376 - SM5 :: the SM5 algorithm, used in SuperMemo 5.0
377 - Simple8 :: a modified version of the SM8 algorithm. SM8 is used in
378 SuperMemo 98. The version implemented here is simplified in that while it
379 'learns' the difficulty of each item using quality grades and number of
380 failures, it does not modify the matrix of values that
381 governs how fast the inter-repetition intervals increase. A method for
382 adjusting intervals when items are reviewed early or late has been taken
383 from SM11, a later version of the algorithm, and included in Simple8."
384 :group 'org-drill
385 :type '(choice (const sm2) (const sm5) (const simple8)))
388 (defcustom org-drill-optimal-factor-matrix
390 "Obsolete and will be removed in future. The SM5 optimal factor
391 matrix data is now stored in the variable
392 `org-drill-sm5-optimal-factor-matrix'."
393 :group 'org-drill
394 :type 'sexp)
397 (defvar org-drill-sm5-optimal-factor-matrix
399 "DO NOT CHANGE THE VALUE OF THIS VARIABLE.
401 Persistent matrix of optimal factors, used by the SuperMemo SM5
402 algorithm. The matrix is saved at the end of each drill session.
404 Over time, values in the matrix will adapt to the individual user's
405 pace of learning.")
408 (add-to-list 'savehist-additional-variables
409 'org-drill-sm5-optimal-factor-matrix)
410 (unless savehist-mode
411 (savehist-mode 1))
414 (defun org-drill--transfer-optimal-factor-matrix ()
415 (if (and org-drill-optimal-factor-matrix
416 (null org-drill-sm5-optimal-factor-matrix))
417 (setq org-drill-sm5-optimal-factor-matrix
418 org-drill-optimal-factor-matrix)))
420 (add-hook 'after-init-hook 'org-drill--transfer-optimal-factor-matrix)
423 (defcustom org-drill-sm5-initial-interval
425 "In the SM5 algorithm, the initial interval after the first
426 successful presentation of an item is always 4 days. If you wish to change
427 this, you can do so here."
428 :group 'org-drill
429 :type 'float)
432 (defcustom org-drill-add-random-noise-to-intervals-p
434 "If true, the number of days until an item's next repetition
435 will vary slightly from the interval calculated by the SM2
436 algorithm. The variation is very small when the interval is
437 small, but scales up with the interval."
438 :group 'org-drill
439 :type 'boolean)
442 (defcustom org-drill-adjust-intervals-for-early-and-late-repetitions-p
444 "If true, when the student successfully reviews an item 1 or more days
445 before or after the scheduled review date, this will affect that date of
446 the item's next scheduled review, according to the algorithm presented at
447 [[http://www.supermemo.com/english/algsm11.htm#Advanced%20repetitions]].
449 Items that were reviewed early will have their next review date brought
450 forward. Those that were reviewed late will have their next review
451 date postponed further.
453 Note that this option currently has no effect if the SM2 algorithm
454 is used."
455 :group 'org-drill
456 :type 'boolean)
459 (defcustom org-drill-cloze-text-weight
461 "For card types 'hide1_firstmore', 'show1_lastmore' and 'show1_firstless',
462 this number determines how often the 'less favoured' situation
463 should arise. It will occur 1 in every N trials, where N is the
464 value of the variable.
466 For example, with the hide1_firstmore card type, the first piece
467 of clozed text should be hidden more often than the other
468 pieces. If this variable is set to 4 (default), the first item
469 will only be shown 25% of the time (1 in 4 trials). Similarly for
470 show1_lastmore, the last item will be shown 75% of the time, and
471 for show1_firstless, the first item would only be shown 25% of the
472 time.
474 If the value of this variable is NIL, then weighting is disabled, and
475 all weighted card types are treated as their unweighted equivalents."
476 :group 'org-drill
477 :type '(choice integer (const nil)))
480 (defcustom org-drill-cram-hours
482 "When in cram mode, items are considered due for review if
483 they were reviewed at least this many hours ago."
484 :group 'org-drill
485 :type 'integer)
488 ;;; NEW items have never been presented in a drill session before.
489 ;;; MATURE items HAVE been presented at least once before.
490 ;;; - YOUNG mature items were scheduled no more than
491 ;;; ORG-DRILL-DAYS-BEFORE-OLD days after their last
492 ;;; repetition. These items will have been learned 'recently' and will have a
493 ;;; low repetition count.
494 ;;; - OLD mature items have intervals greater than
495 ;;; ORG-DRILL-DAYS-BEFORE-OLD.
496 ;;; - OVERDUE items are past their scheduled review date by more than
497 ;;; LAST-INTERVAL * (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) days,
498 ;;; regardless of young/old status.
501 (defcustom org-drill-days-before-old
503 "When an item's inter-repetition interval rises above this value in days,
504 it is no longer considered a 'young' (recently learned) item."
505 :group 'org-drill
506 :type 'integer)
509 (defcustom org-drill-overdue-interval-factor
511 "An item is considered overdue if its scheduled review date is
512 more than (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) * LAST-INTERVAL
513 days in the past. For example, a value of 1.2 means an additional
514 20% of the last scheduled interval is allowed to elapse before
515 the item is overdue. A value of 1.0 means no extra time is
516 allowed at all - items are immediately considered overdue if
517 there is even one day's delay in reviewing them. This variable
518 should never be less than 1.0."
519 :group 'org-drill
520 :type 'float)
523 (defcustom org-drill-learn-fraction
525 "Fraction between 0 and 1 that governs how quickly the spaces
526 between successive repetitions increase, for all items. The
527 default value is 0.5. Higher values make spaces increase more
528 quickly with each successful repetition. You should only change
529 this in small increments (for example 0.05-0.1) as it has an
530 exponential effect on inter-repetition spacing."
531 :group 'org-drill
532 :type 'float)
535 (defvar drill-answer nil
536 "Global variable that can be bound to a correct answer when an
537 item is being presented. If this variable is non-nil, the default
538 presentation function will show its value instead of the default
539 behaviour of revealing the contents of the drilled item.
541 This variable is useful for card types that compute their answers
542 -- for example, a card type that asks the student to translate a
543 random number to another language. ")
546 (defvar *org-drill-session-qualities* nil)
547 (defvar *org-drill-start-time* 0)
548 (defvar *org-drill-new-entries* nil)
549 (defvar *org-drill-dormant-entry-count* 0)
550 (defvar *org-drill-due-entry-count* 0)
551 (defvar *org-drill-overdue-entry-count* 0)
552 (defvar *org-drill-due-tomorrow-count* 0)
553 (defvar *org-drill-overdue-entries* nil
554 "List of markers for items that are considered 'overdue', based on
555 the value of ORG-DRILL-OVERDUE-INTERVAL-FACTOR.")
556 (defvar *org-drill-young-mature-entries* nil
557 "List of markers for mature entries whose last inter-repetition
558 interval was <= ORG-DRILL-DAYS-BEFORE-OLD days.")
559 (defvar *org-drill-old-mature-entries* nil
560 "List of markers for mature entries whose last inter-repetition
561 interval was greater than ORG-DRILL-DAYS-BEFORE-OLD days.")
562 (defvar *org-drill-failed-entries* nil)
563 (defvar *org-drill-again-entries* nil)
564 (defvar *org-drill-done-entries* nil)
565 (defvar *org-drill-current-item* nil
566 "Set to the marker for the item currently being tested.")
567 (defvar *org-drill-cram-mode* nil
568 "Are we in 'cram mode', where all items are considered due
569 for review unless they were already reviewed in the recent past?")
570 (defvar org-drill-scheduling-properties
571 '("LEARN_DATA" "DRILL_LAST_INTERVAL" "DRILL_REPEATS_SINCE_FAIL"
572 "DRILL_TOTAL_REPEATS" "DRILL_FAILURE_COUNT" "DRILL_AVERAGE_QUALITY"
573 "DRILL_EASE" "DRILL_LAST_QUALITY" "DRILL_LAST_REVIEWED"))
574 (defvar org-drill--lapse-very-overdue-entries-p nil
575 "If non-nil, entries more than 90 days overdue are regarded as 'lapsed'.
576 This means that when the item is eventually re-tested it will be
577 treated as 'failed' (quality 2) for rescheduling purposes,
578 regardless of whether the test was successful.")
581 ;;; Make the above settings safe as file-local variables.
584 (put 'org-drill-question-tag 'safe-local-variable 'stringp)
585 (put 'org-drill-maximum-items-per-session 'safe-local-variable
586 '(lambda (val) (or (integerp val) (null val))))
587 (put 'org-drill-maximum-duration 'safe-local-variable
588 '(lambda (val) (or (integerp val) (null val))))
589 (put 'org-drill-failure-quality 'safe-local-variable 'integerp)
590 (put 'org-drill-forgetting-index 'safe-local-variable 'integerp)
591 (put 'org-drill-leech-failure-threshold 'safe-local-variable 'integerp)
592 (put 'org-drill-leech-method 'safe-local-variable
593 '(lambda (val) (memq val '(nil skip warn))))
594 (put 'org-drill-use-visible-cloze-face-p 'safe-local-variable 'booleanp)
595 (put 'org-drill-hide-item-headings-p 'safe-local-variable 'booleanp)
596 (put 'org-drill-spaced-repetition-algorithm 'safe-local-variable
597 '(lambda (val) (memq val '(simple8 sm5 sm2))))
598 (put 'org-drill-sm5-initial-interval 'safe-local-variable 'floatp)
599 (put 'org-drill-add-random-noise-to-intervals-p 'safe-local-variable 'booleanp)
600 (put 'org-drill-adjust-intervals-for-early-and-late-repetitions-p
601 'safe-local-variable 'booleanp)
602 (put 'org-drill-cram-hours 'safe-local-variable 'integerp)
603 (put 'org-drill-learn-fraction 'safe-local-variable 'floatp)
604 (put 'org-drill-days-before-old 'safe-local-variable 'integerp)
605 (put 'org-drill-overdue-interval-factor 'safe-local-variable 'floatp)
606 (put 'org-drill-scope 'safe-local-variable
607 '(lambda (val) (or (symbolp val) (listp val))))
608 (put 'org-drill-match 'safe-local-variable
609 '(lambda (val) (or (stringp val) (null val))))
610 (put 'org-drill-save-buffers-after-drill-sessions-p 'safe-local-variable 'booleanp)
611 (put 'org-drill-cloze-text-weight 'safe-local-variable
612 '(lambda (val) (or (null val) (integerp val))))
613 (put 'org-drill-left-cloze-delimiter 'safe-local-variable 'stringp)
614 (put 'org-drill-right-cloze-delimiter 'safe-local-variable 'stringp)
617 ;;;; Utilities ================================================================
620 (defun free-marker (m)
621 (set-marker m nil))
624 (defmacro pop-random (place)
625 (let ((idx (cl-gensym)))
626 `(if (null ,place)
628 (let ((,idx (random* (length ,place))))
629 (prog1 (nth ,idx ,place)
630 (setq ,place (append (subseq ,place 0 ,idx)
631 (subseq ,place (1+ ,idx)))))))))
634 (defmacro push-end (val place)
635 "Add VAL to the end of the sequence stored in PLACE. Return the new
636 value."
637 `(setq ,place (append ,place (list ,val))))
640 (defun shuffle-list (list)
641 "Randomly permute the elements of LIST (all permutations equally likely)."
642 ;; Adapted from 'shuffle-vector' in cookie1.el
643 (let ((i 0)
645 temp
646 (len (length list)))
647 (while (< i len)
648 (setq j (+ i (random* (- len i))))
649 (setq temp (nth i list))
650 (setf (nth i list) (nth j list))
651 (setf (nth j list) temp)
652 (setq i (1+ i))))
653 list)
656 (defun round-float (floatnum fix)
657 "Round the floating point number FLOATNUM to FIX decimal places.
658 Example: (round-float 3.56755765 3) -> 3.568"
659 (let ((n (expt 10 fix)))
660 (/ (float (round (* floatnum n))) n)))
663 (defun command-keybinding-to-string (cmd)
664 "Return a human-readable description of the key/keys to which the command
665 CMD is bound, or nil if it is not bound to a key."
666 (let ((key (where-is-internal cmd overriding-local-map t)))
667 (if key (key-description key))))
670 (defun time-to-inactive-org-timestamp (time)
671 (format-time-string
672 (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")
673 time))
676 (defun time-to-active-org-timestamp (time)
677 (format-time-string
678 (concat "<" (substring (cdr org-time-stamp-formats) 1 -1) ">")
679 time))
682 (defun org-map-drill-entries (func &optional scope drill-match &rest skip)
683 "Like `org-map-entries', but only drill entries are processed."
684 (let ((org-drill-scope (or scope org-drill-scope))
685 (org-drill-match (or drill-match org-drill-match)))
686 (apply 'org-map-entries func
687 (concat "+" org-drill-question-tag
688 (if (and (stringp org-drill-match)
689 (not (member '(?+ ?- ?|) (elt org-drill-match 0))))
690 "+" "")
691 (or org-drill-match ""))
692 (case org-drill-scope
693 (file nil)
694 (file-no-restriction 'file)
695 (directory
696 (directory-files (file-name-directory (buffer-file-name))
697 t "\\.org$"))
698 (t org-drill-scope))
699 skip)))
702 (defmacro with-hidden-cloze-text (&rest body)
703 `(progn
704 (org-drill-hide-clozed-text)
705 (unwind-protect
706 (progn
707 ,@body)
708 (org-drill-unhide-clozed-text))))
711 (defmacro with-hidden-cloze-hints (&rest body)
712 `(progn
713 (org-drill-hide-cloze-hints)
714 (unwind-protect
715 (progn
716 ,@body)
717 (org-drill-unhide-text))))
720 (defmacro with-hidden-comments (&rest body)
721 `(progn
722 (if org-drill-hide-item-headings-p
723 (org-drill-hide-heading-at-point))
724 (org-drill-hide-comments)
725 (unwind-protect
726 (progn
727 ,@body)
728 (org-drill-unhide-text))))
731 (defun org-drill-days-since-last-review ()
732 "Nil means a last review date has not yet been stored for
733 the item.
734 Zero means it was reviewed today.
735 A positive number means it was reviewed that many days ago.
736 A negative number means the date of last review is in the future --
737 this should never happen."
738 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
739 (when datestr
740 (- (time-to-days (current-time))
741 (time-to-days (apply 'encode-time
742 (org-parse-time-string datestr)))))))
745 (defun org-drill-hours-since-last-review ()
746 "Like `org-drill-days-since-last-review', but return value is
747 in hours rather than days."
748 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
749 (when datestr
750 (floor
751 (/ (- (time-to-seconds (current-time))
752 (time-to-seconds (apply 'encode-time
753 (org-parse-time-string datestr))))
754 (* 60 60))))))
757 (defun org-drill-entry-p (&optional marker)
758 "Is MARKER, or the point, in a 'drill item'? This will return nil if
759 the point is inside a subheading of a drill item -- to handle that
760 situation use `org-part-of-drill-entry-p'."
761 (save-excursion
762 (when marker
763 (org-drill-goto-entry marker))
764 (member org-drill-question-tag (org-get-local-tags))))
767 (defun org-drill-goto-entry (marker)
768 (switch-to-buffer (marker-buffer marker))
769 (goto-char marker))
772 (defun org-part-of-drill-entry-p ()
773 "Is the current entry either the main heading of a 'drill item',
774 or a subheading within a drill item?"
775 (or (org-drill-entry-p)
776 ;; Does this heading INHERIT the drill tag
777 (member org-drill-question-tag (org-get-tags-at))))
780 (defun org-drill-goto-drill-entry-heading ()
781 "Move the point to the heading which holds the :drill: tag for this
782 drill entry."
783 (unless (org-at-heading-p)
784 (org-back-to-heading))
785 (unless (org-part-of-drill-entry-p)
786 (error "Point is not inside a drill entry"))
787 (while (not (org-drill-entry-p))
788 (unless (org-up-heading-safe)
789 (error "Cannot find a parent heading that is marked as a drill entry"))))
793 (defun org-drill-entry-leech-p ()
794 "Is the current entry a 'leech item'?"
795 (and (org-drill-entry-p)
796 (member "leech" (org-get-local-tags))))
799 ;; (defun org-drill-entry-due-p ()
800 ;; (cond
801 ;; (*org-drill-cram-mode*
802 ;; (let ((hours (org-drill-hours-since-last-review)))
803 ;; (and (org-drill-entry-p)
804 ;; (or (null hours)
805 ;; (>= hours org-drill-cram-hours)))))
806 ;; (t
807 ;; (let ((item-time (org-get-scheduled-time (point))))
808 ;; (and (org-drill-entry-p)
809 ;; (or (not (eql 'skip org-drill-leech-method))
810 ;; (not (org-drill-entry-leech-p)))
811 ;; (or (null item-time) ; not scheduled
812 ;; (not (minusp ; scheduled for today/in past
813 ;; (- (time-to-days (current-time))
814 ;; (time-to-days item-time))))))))))
817 (defun org-drill-entry-days-overdue ()
818 "Returns:
819 - NIL if the item is not to be regarded as scheduled for review at all.
820 This is the case if it is not a drill item, or if it is a leech item
821 that we wish to skip, or if we are in cram mode and have already reviewed
822 the item within the last few hours.
823 - 0 if the item is new, or if it scheduled for review today.
824 - A negative integer - item is scheduled that many days in the future.
825 - A positive integer - item is scheduled that many days in the past."
826 (cond
827 (*org-drill-cram-mode*
828 (let ((hours (org-drill-hours-since-last-review)))
829 (and (org-drill-entry-p)
830 (or (null hours)
831 (>= hours org-drill-cram-hours))
832 0)))
834 (let ((item-time (org-get-scheduled-time (point))))
835 (cond
836 ((or (not (org-drill-entry-p))
837 (and (eql 'skip org-drill-leech-method)
838 (org-drill-entry-leech-p)))
839 nil)
840 ((null item-time) ; not scheduled -> due now
843 (- (time-to-days (current-time))
844 (time-to-days item-time))))))))
847 (defun org-drill-entry-overdue-p (&optional days-overdue last-interval)
848 "Returns true if entry that is scheduled DAYS-OVERDUE dasy in the past,
849 and whose last inter-repetition interval was LAST-INTERVAL, should be
850 considered 'overdue'. If the arguments are not given they are extracted
851 from the entry at point."
852 (unless days-overdue
853 (setq days-overdue (org-drill-entry-days-overdue)))
854 (unless last-interval
855 (setq last-interval (org-drill-entry-last-interval 1)))
856 (and (numberp days-overdue)
857 (> days-overdue 1) ; enforce a sane minimum 'overdue' gap
858 ;;(> due org-drill-days-before-overdue)
859 (> (/ (+ days-overdue last-interval 1.0) last-interval)
860 org-drill-overdue-interval-factor)))
864 (defun org-drill-entry-due-p ()
865 (let ((due (org-drill-entry-days-overdue)))
866 (and (not (null due))
867 (not (minusp due)))))
870 (defun org-drill-entry-new-p ()
871 (and (org-drill-entry-p)
872 (let ((item-time (org-get-scheduled-time (point))))
873 (null item-time))))
876 (defun org-drill-entry-last-quality (&optional default)
877 (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
878 (if quality
879 (string-to-number quality)
880 default)))
883 (defun org-drill-entry-failure-count ()
884 (let ((quality (org-entry-get (point) "DRILL_FAILURE_COUNT")))
885 (if quality
886 (string-to-number quality)
887 0)))
890 (defun org-drill-entry-average-quality (&optional default)
891 (let ((val (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
892 (if val
893 (string-to-number val)
894 (or default nil))))
896 (defun org-drill-entry-last-interval (&optional default)
897 (let ((val (org-entry-get (point) "DRILL_LAST_INTERVAL")))
898 (if val
899 (string-to-number val)
900 (or default 0))))
902 (defun org-drill-entry-repeats-since-fail (&optional default)
903 (let ((val (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
904 (if val
905 (string-to-number val)
906 (or default 0))))
908 (defun org-drill-entry-total-repeats (&optional default)
909 (let ((val (org-entry-get (point) "DRILL_TOTAL_REPEATS")))
910 (if val
911 (string-to-number val)
912 (or default 0))))
914 (defun org-drill-entry-ease (&optional default)
915 (let ((val (org-entry-get (point) "DRILL_EASE")))
916 (if val
917 (string-to-number val)
918 default)))
921 ;;; From http://www.supermemo.com/english/ol/sm5.htm
922 (defun org-drill-random-dispersal-factor ()
923 "Returns a random number between 0.5 and 1.5."
924 (let ((a 0.047)
925 (b 0.092)
926 (p (- (random* 1.0) 0.5)))
927 (cl-flet ((sign (n)
928 (cond ((zerop n) 0)
929 ((plusp n) 1)
930 (t -1))))
931 (/ (+ 100 (* (* (/ -1 b) (log (- 1 (* (/ b a ) (abs p)))))
932 (sign p)))
933 100.0))))
935 (defun pseudonormal (mean variation)
936 "Random numbers in a pseudo-normal distribution with mean MEAN, range
937 MEAN-VARIATION to MEAN+VARIATION"
938 (+ (random* variation)
939 (random* variation)
940 (- variation)
941 mean))
944 (defun org-drill-early-interval-factor (optimal-factor
945 optimal-interval
946 days-ahead)
947 "Arguments:
948 - OPTIMAL-FACTOR: interval-factor if the item had been tested
949 exactly when it was supposed to be.
950 - OPTIMAL-INTERVAL: interval for next repetition (days) if the item had been
951 tested exactly when it was supposed to be.
952 - DAYS-AHEAD: how many days ahead of time the item was reviewed.
954 Returns an adjusted optimal factor which should be used to
955 calculate the next interval, instead of the optimal factor found
956 in the matrix."
957 (let ((delta-ofmax (* (1- optimal-factor)
958 (/ (+ optimal-interval
959 (* 0.6 optimal-interval) -1) (1- optimal-interval)))))
960 (- optimal-factor
961 (* delta-ofmax (/ days-ahead (+ days-ahead (* 0.6 optimal-interval)))))))
964 (defun org-drill-get-item-data ()
965 "Returns a list of 6 items, containing all the stored recall
966 data for the item at point:
967 - LAST-INTERVAL is the interval in days that was used to schedule the item's
968 current review date.
969 - REPEATS is the number of items the item has been successfully recalled without
970 without any failures. It is reset to 0 upon failure to recall the item.
971 - FAILURES is the total number of times the user has failed to recall the item.
972 - TOTAL-REPEATS includes both successful and unsuccessful repetitions.
973 - AVERAGE-QUALITY is the mean quality of recall of the item over
974 all its repetitions, successful and unsuccessful.
975 - EASE is a number reflecting how easy the item is to learn. Higher is easier.
977 (let ((learn-str (org-entry-get (point) "LEARN_DATA"))
978 (repeats (org-drill-entry-total-repeats :missing)))
979 (cond
980 (learn-str
981 (let ((learn-data (or (and learn-str
982 (read learn-str))
983 (copy-list initial-repetition-state))))
984 (list (nth 0 learn-data) ; last interval
985 (nth 1 learn-data) ; repetitions
986 (org-drill-entry-failure-count)
987 (nth 1 learn-data)
988 (org-drill-entry-last-quality)
989 (nth 2 learn-data) ; EF
991 ((not (eql :missing repeats))
992 (list (org-drill-entry-last-interval)
993 (org-drill-entry-repeats-since-fail)
994 (org-drill-entry-failure-count)
995 (org-drill-entry-total-repeats)
996 (org-drill-entry-average-quality)
997 (org-drill-entry-ease)))
998 (t ; virgin item
999 (list 0 0 0 0 nil nil)))))
1002 (defun org-drill-store-item-data (last-interval repeats failures
1003 total-repeats meanq
1004 ease)
1005 "Stores the given data in the item at point."
1006 (org-entry-delete (point) "LEARN_DATA")
1007 (org-set-property "DRILL_LAST_INTERVAL"
1008 (number-to-string (round-float last-interval 4)))
1009 (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats))
1010 (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats))
1011 (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures))
1012 (org-set-property "DRILL_AVERAGE_QUALITY"
1013 (number-to-string (round-float meanq 3)))
1014 (org-set-property "DRILL_EASE"
1015 (number-to-string (round-float ease 3))))
1019 ;;; SM2 Algorithm =============================================================
1022 (defun determine-next-interval-sm2 (last-interval n ef quality
1023 failures meanq total-repeats)
1024 "Arguments:
1025 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1026 - REPEATS -- the number of times the item has been successfully reviewed
1027 - EF -- the 'easiness factor'
1028 - QUALITY -- 0 to 5
1030 Returns a list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX), where:
1031 - INTERVAL is the number of days until the item should next be reviewed
1032 - REPEATS is incremented by 1.
1033 - EF is modified based on the recall quality for the item.
1034 - OF-MATRIX is not modified."
1035 (assert (> n 0))
1036 (assert (and (>= quality 0) (<= quality 5)))
1037 (if (<= quality org-drill-failure-quality)
1038 ;; When an item is failed, its interval is reset to 0,
1039 ;; but its EF is unchanged
1040 (list -1 1 ef (1+ failures) meanq (1+ total-repeats)
1041 org-drill-sm5-optimal-factor-matrix)
1042 ;; else:
1043 (let* ((next-ef (modify-e-factor ef quality))
1044 (interval
1045 (cond
1046 ((<= n 1) 1)
1047 ((= n 2)
1048 (cond
1049 (org-drill-add-random-noise-to-intervals-p
1050 (case quality
1051 (5 6)
1052 (4 4)
1053 (3 3)
1054 (2 1)
1055 (t -1)))
1056 (t 6)))
1057 (t (* last-interval next-ef)))))
1058 (list (if org-drill-add-random-noise-to-intervals-p
1059 (+ last-interval (* (- interval last-interval)
1060 (org-drill-random-dispersal-factor)))
1061 interval)
1062 (1+ n)
1063 next-ef
1064 failures meanq (1+ total-repeats)
1065 org-drill-sm5-optimal-factor-matrix))))
1068 ;;; SM5 Algorithm =============================================================
1072 (defun initial-optimal-factor-sm5 (n ef)
1073 (if (= 1 n)
1074 org-drill-sm5-initial-interval
1075 ef))
1077 (defun get-optimal-factor-sm5 (n ef of-matrix)
1078 (let ((factors (assoc n of-matrix)))
1079 (or (and factors
1080 (let ((ef-of (assoc ef (cdr factors))))
1081 (and ef-of (cdr ef-of))))
1082 (initial-optimal-factor-sm5 n ef))))
1085 (defun inter-repetition-interval-sm5 (last-interval n ef &optional of-matrix)
1086 (let ((of (get-optimal-factor-sm5 n ef (or of-matrix
1087 org-drill-sm5-optimal-factor-matrix))))
1088 (if (= 1 n)
1090 (* of last-interval))))
1093 (defun determine-next-interval-sm5 (last-interval n ef quality
1094 failures meanq total-repeats
1095 of-matrix &optional delta-days)
1096 (if (zerop n) (setq n 1))
1097 (if (null ef) (setq ef 2.5))
1098 (assert (> n 0))
1099 (assert (and (>= quality 0) (<= quality 5)))
1100 (unless of-matrix
1101 (setq of-matrix org-drill-sm5-optimal-factor-matrix))
1102 (setq of-matrix (cl-copy-tree of-matrix))
1104 (setq meanq (if meanq
1105 (/ (+ quality (* meanq total-repeats 1.0))
1106 (1+ total-repeats))
1107 quality))
1109 (let ((next-ef (modify-e-factor ef quality))
1110 (old-ef ef)
1111 (new-of (modify-of (get-optimal-factor-sm5 n ef of-matrix)
1112 quality org-drill-learn-fraction))
1113 (interval nil))
1114 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1115 delta-days (minusp delta-days))
1116 (setq new-of (org-drill-early-interval-factor
1117 (get-optimal-factor-sm5 n ef of-matrix)
1118 (inter-repetition-interval-sm5
1119 last-interval n ef of-matrix)
1120 delta-days)))
1122 (setq of-matrix
1123 (set-optimal-factor n next-ef of-matrix
1124 (round-float new-of 3))) ; round OF to 3 d.p.
1126 (setq ef next-ef)
1128 (cond
1129 ;; "Failed" -- reset repetitions to 0,
1130 ((<= quality org-drill-failure-quality)
1131 (list -1 1 old-ef (1+ failures) meanq (1+ total-repeats)
1132 of-matrix)) ; Not clear if OF matrix is supposed to be
1133 ; preserved
1134 ;; For a zero-based quality of 4 or 5, don't repeat
1135 ;; ((and (>= quality 4)
1136 ;; (not org-learn-always-reschedule))
1137 ;; (list 0 (1+ n) ef failures meanq
1138 ;; (1+ total-repeats) of-matrix)) ; 0 interval = unschedule
1140 (setq interval (inter-repetition-interval-sm5
1141 last-interval n ef of-matrix))
1142 (if org-drill-add-random-noise-to-intervals-p
1143 (setq interval (* interval (org-drill-random-dispersal-factor))))
1144 (list interval
1145 (1+ n)
1147 failures
1148 meanq
1149 (1+ total-repeats)
1150 of-matrix)))))
1153 ;;; Simple8 Algorithm =========================================================
1156 (defun org-drill-simple8-first-interval (failures)
1157 "Arguments:
1158 - FAILURES: integer >= 0. The total number of times the item has
1159 been forgotten, ever.
1161 Returns the optimal FIRST interval for an item which has previously been
1162 forgotten on FAILURES occasions."
1163 (* 2.4849 (exp (* -0.057 failures))))
1166 (defun org-drill-simple8-interval-factor (ease repetition)
1167 "Arguments:
1168 - EASE: floating point number >= 1.2. Corresponds to `AF' in SM8 algorithm.
1169 - REPETITION: the number of times the item has been tested.
1170 1 is the first repetition (ie the second trial).
1171 Returns:
1172 The factor by which the last interval should be
1173 multiplied to give the next interval. Corresponds to `RF' or `OF'."
1174 (+ 1.2 (* (- ease 1.2) (expt org-drill-learn-fraction (log repetition 2)))))
1177 (defun org-drill-simple8-quality->ease (quality)
1178 "Returns the ease (`AF' in the SM8 algorithm) which corresponds
1179 to a mean item quality of QUALITY."
1180 (+ (* 0.0542 (expt quality 4))
1181 (* -0.4848 (expt quality 3))
1182 (* 1.4916 (expt quality 2))
1183 (* -1.2403 quality)
1184 1.4515))
1187 (defun determine-next-interval-simple8 (last-interval repeats quality
1188 failures meanq totaln
1189 &optional delta-days)
1190 "Arguments:
1191 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1192 - REPEATS -- the number of times the item has been successfully reviewed
1193 - EASE -- the 'easiness factor'
1194 - QUALITY -- 0 to 5
1195 - DELTA-DAYS -- how many days overdue was the item when it was reviewed.
1196 0 = reviewed on the scheduled day. +N = N days overdue.
1197 -N = reviewed N days early.
1199 Returns the new item data, as a list of 6 values:
1200 - NEXT-INTERVAL
1201 - REPEATS
1202 - EASE
1203 - FAILURES
1204 - AVERAGE-QUALITY
1205 - TOTAL-REPEATS.
1206 See the documentation for `org-drill-get-item-data' for a description of these."
1207 (assert (>= repeats 0))
1208 (assert (and (>= quality 0) (<= quality 5)))
1209 (assert (or (null meanq) (and (>= meanq 0) (<= meanq 5))))
1210 (let ((next-interval nil))
1211 (setf meanq (if meanq
1212 (/ (+ quality (* meanq totaln 1.0)) (1+ totaln))
1213 quality))
1214 (cond
1215 ((<= quality org-drill-failure-quality)
1216 (incf failures)
1217 (setf repeats 0
1218 next-interval -1))
1219 ((or (zerop repeats)
1220 (zerop last-interval))
1221 (setf next-interval (org-drill-simple8-first-interval failures))
1222 (incf repeats)
1223 (incf totaln))
1225 (let* ((use-n
1226 (if (and
1227 org-drill-adjust-intervals-for-early-and-late-repetitions-p
1228 (numberp delta-days) (plusp delta-days)
1229 (plusp last-interval))
1230 (+ repeats (min 1 (/ delta-days last-interval 1.0)))
1231 repeats))
1232 (factor (org-drill-simple8-interval-factor
1233 (org-drill-simple8-quality->ease meanq) use-n))
1234 (next-int (* last-interval factor)))
1235 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1236 (numberp delta-days) (minusp delta-days))
1237 ;; The item was reviewed earlier than scheduled.
1238 (setf factor (org-drill-early-interval-factor
1239 factor next-int (abs delta-days))
1240 next-int (* last-interval factor)))
1241 (setf next-interval next-int)
1242 (incf repeats)
1243 (incf totaln))))
1244 (list
1245 (if (and org-drill-add-random-noise-to-intervals-p
1246 (plusp next-interval))
1247 (* next-interval (org-drill-random-dispersal-factor))
1248 next-interval)
1249 repeats
1250 (org-drill-simple8-quality->ease meanq)
1251 failures
1252 meanq
1253 totaln
1259 ;;; Essentially copied from `org-learn.el', but modified to
1260 ;;; optionally call the SM2 or simple8 functions.
1261 (defun org-drill-smart-reschedule (quality &optional days-ahead)
1262 "If DAYS-AHEAD is supplied it must be a positive integer. The
1263 item will be scheduled exactly this many days into the future."
1264 (let ((delta-days (- (time-to-days (current-time))
1265 (time-to-days (or (org-get-scheduled-time (point))
1266 (current-time)))))
1267 (ofmatrix org-drill-sm5-optimal-factor-matrix)
1268 ;; Entries can have weights, 1 by default. Intervals are divided by the
1269 ;; item's weight, so an item with a weight of 2 will have all intervals
1270 ;; halved, meaning you will end up reviewing it twice as often.
1271 ;; Useful for entries which randomly present any of several facts.
1272 (weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1273 (if (stringp weight)
1274 (setq weight (read weight)))
1275 (destructuring-bind (last-interval repetitions failures
1276 total-repeats meanq ease)
1277 (org-drill-get-item-data)
1278 (destructuring-bind (next-interval repetitions ease
1279 failures meanq total-repeats
1280 &optional new-ofmatrix)
1281 (case org-drill-spaced-repetition-algorithm
1282 (sm5 (determine-next-interval-sm5 last-interval repetitions
1283 ease quality failures
1284 meanq total-repeats ofmatrix))
1285 (sm2 (determine-next-interval-sm2 last-interval repetitions
1286 ease quality failures
1287 meanq total-repeats))
1288 (simple8 (determine-next-interval-simple8 last-interval repetitions
1289 quality failures meanq
1290 total-repeats
1291 delta-days)))
1292 (if (numberp days-ahead)
1293 (setq next-interval days-ahead))
1295 (if (and (null days-ahead)
1296 (numberp weight) (plusp weight)
1297 (not (minusp next-interval)))
1298 (setq next-interval
1299 (max 1.0 (+ last-interval
1300 (/ (- next-interval last-interval) weight)))))
1302 (org-drill-store-item-data next-interval repetitions failures
1303 total-repeats meanq ease)
1305 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
1306 (setq org-drill-sm5-optimal-factor-matrix new-ofmatrix))
1308 (cond
1309 ((= 0 days-ahead)
1310 (org-schedule '(4)))
1311 ((minusp days-ahead)
1312 (org-schedule nil (current-time)))
1314 (org-schedule nil (time-add (current-time)
1315 (days-to-time
1316 (round next-interval))))))))))
1319 (defun org-drill-hypothetical-next-review-date (quality)
1320 "Returns an integer representing the number of days into the future
1321 that the current item would be scheduled, based on a recall quality
1322 of QUALITY."
1323 (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1324 (destructuring-bind (last-interval repetitions failures
1325 total-repeats meanq ease)
1326 (org-drill-get-item-data)
1327 (if (stringp weight)
1328 (setq weight (read weight)))
1329 (destructuring-bind (next-interval repetitions ease
1330 failures meanq total-repeats
1331 &optional ofmatrix)
1332 (case org-drill-spaced-repetition-algorithm
1333 (sm5 (determine-next-interval-sm5 last-interval repetitions
1334 ease quality failures
1335 meanq total-repeats
1336 org-drill-sm5-optimal-factor-matrix))
1337 (sm2 (determine-next-interval-sm2 last-interval repetitions
1338 ease quality failures
1339 meanq total-repeats))
1340 (simple8 (determine-next-interval-simple8 last-interval repetitions
1341 quality failures meanq
1342 total-repeats)))
1343 (cond
1344 ((not (plusp next-interval))
1346 ((and (numberp weight) (plusp weight))
1347 (+ last-interval
1348 (max 1.0 (/ (- next-interval last-interval) weight))))
1350 next-interval))))))
1353 (defun org-drill-hypothetical-next-review-dates ()
1354 (let ((intervals nil))
1355 (dotimes (q 6)
1356 (push (max (or (car intervals) 0)
1357 (org-drill-hypothetical-next-review-date q))
1358 intervals))
1359 (reverse intervals)))
1362 (defun org-drill-reschedule ()
1363 "Returns quality rating (0-5), or nil if the user quit."
1364 (let ((ch nil)
1365 (input nil)
1366 (next-review-dates (org-drill-hypothetical-next-review-dates))
1367 (key-prompt (format "(0-5, %c=help, %c=edit, %c=tags, %c=quit)"
1368 org-drill--help-key
1369 org-drill--edit-key
1370 org-drill--tags-key
1371 org-drill--quit-key)))
1372 (save-excursion
1373 (while (not (memq ch (list org-drill--quit-key
1374 org-drill--edit-key
1375 7 ; C-g
1376 ?0 ?1 ?2 ?3 ?4 ?5)))
1377 (setq input (read-key-sequence
1378 (if (eq ch org-drill--help-key)
1379 (format "0-2 Means you have forgotten the item.
1380 3-5 Means you have remembered the item.
1382 0 - Completely forgot.
1383 1 - Even after seeing the answer, it still took a bit to sink in.
1384 2 - After seeing the answer, you remembered it.
1385 3 - It took you awhile, but you finally remembered. (+%s days)
1386 4 - After a little bit of thought you remembered. (+%s days)
1387 5 - You remembered the item really easily. (+%s days)
1389 How well did you do? %s"
1390 (round (nth 3 next-review-dates))
1391 (round (nth 4 next-review-dates))
1392 (round (nth 5 next-review-dates))
1393 key-prompt)
1394 (format "How well did you do? %s" key-prompt))))
1395 (cond
1396 ((stringp input)
1397 (setq ch (elt input 0)))
1398 ((and (vectorp input) (symbolp (elt input 0)))
1399 (case (elt input 0)
1400 (up (ignore-errors (forward-line -1)))
1401 (down (ignore-errors (forward-line 1)))
1402 (left (ignore-errors (backward-char)))
1403 (right (ignore-errors (forward-char)))
1404 (prior (ignore-errors (scroll-down))) ; pgup
1405 (next (ignore-errors (scroll-up))))) ; pgdn
1406 ((and (vectorp input) (listp (elt input 0))
1407 (eventp (elt input 0)))
1408 (case (car (elt input 0))
1409 (wheel-up (ignore-errors (mwheel-scroll (elt input 0))))
1410 (wheel-down (ignore-errors (mwheel-scroll (elt input 0)))))))
1411 (if (eql ch org-drill--tags-key)
1412 (org-set-tags-command))))
1413 (cond
1414 ((and (>= ch ?0) (<= ch ?5))
1415 (let ((quality (- ch ?0))
1416 (failures (org-drill-entry-failure-count)))
1417 (unless *org-drill-cram-mode*
1418 (save-excursion
1419 (let ((quality (if (org-drill--entry-lapsed-p) 2 quality)))
1420 (org-drill-smart-reschedule quality
1421 (nth quality next-review-dates))))
1422 (push quality *org-drill-session-qualities*)
1423 (cond
1424 ((<= quality org-drill-failure-quality)
1425 (when org-drill-leech-failure-threshold
1426 ;;(setq failures (if failures (string-to-number failures) 0))
1427 ;; (org-set-property "DRILL_FAILURE_COUNT"
1428 ;; (format "%d" (1+ failures)))
1429 (if (> (1+ failures) org-drill-leech-failure-threshold)
1430 (org-toggle-tag "leech" 'on))))
1432 (let ((scheduled-time (org-get-scheduled-time (point))))
1433 (when scheduled-time
1434 (message "Next review in %d days"
1435 (- (time-to-days scheduled-time)
1436 (time-to-days (current-time))))
1437 (sit-for 0.5)))))
1438 (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality))
1439 (org-set-property "DRILL_LAST_REVIEWED"
1440 (time-to-inactive-org-timestamp (current-time))))
1441 quality))
1442 ((= ch org-drill--edit-key)
1443 'edit)
1445 nil))))
1448 ;; (defun org-drill-hide-all-subheadings-except (heading-list)
1449 ;; "Returns a list containing the position of each immediate subheading of
1450 ;; the current topic."
1451 ;; (let ((drill-entry-level (org-current-level))
1452 ;; (drill-sections nil)
1453 ;; (drill-heading nil))
1454 ;; (org-show-subtree)
1455 ;; (save-excursion
1456 ;; (org-map-entries
1457 ;; (lambda ()
1458 ;; (when (and (not (org-invisible-p))
1459 ;; (> (org-current-level) drill-entry-level))
1460 ;; (setq drill-heading (org-get-heading t))
1461 ;; (unless (and (= (org-current-level) (1+ drill-entry-level))
1462 ;; (member drill-heading heading-list))
1463 ;; (hide-subtree))
1464 ;; (push (point) drill-sections)))
1465 ;; "" 'tree))
1466 ;; (reverse drill-sections)))
1470 (defun org-drill-hide-subheadings-if (test)
1471 "TEST is a function taking no arguments. TEST will be called for each
1472 of the immediate subheadings of the current drill item, with the point
1473 on the relevant subheading. TEST should return nil if the subheading is
1474 to be revealed, non-nil if it is to be hidden.
1475 Returns a list containing the position of each immediate subheading of
1476 the current topic."
1477 (let ((drill-entry-level (org-current-level))
1478 (drill-sections nil))
1479 (org-show-subtree)
1480 (save-excursion
1481 (org-map-entries
1482 (lambda ()
1483 (when (and (not (org-invisible-p))
1484 (> (org-current-level) drill-entry-level))
1485 (when (or (/= (org-current-level) (1+ drill-entry-level))
1486 (funcall test))
1487 (hide-subtree))
1488 (push (point) drill-sections)))
1489 "" 'tree))
1490 (reverse drill-sections)))
1493 (defun org-drill-hide-all-subheadings-except (heading-list)
1494 (org-drill-hide-subheadings-if
1495 (lambda () (let ((drill-heading (org-get-heading t)))
1496 (not (member drill-heading heading-list))))))
1499 (defun org-drill-presentation-prompt (&rest fmt-and-args)
1500 (let* ((item-start-time (current-time))
1501 (input nil)
1502 (ch nil)
1503 (last-second 0)
1504 (mature-entry-count (+ (length *org-drill-young-mature-entries*)
1505 (length *org-drill-old-mature-entries*)
1506 (length *org-drill-overdue-entries*)))
1507 (status (first (org-drill-entry-status)))
1508 (prompt
1509 (if fmt-and-args
1510 (apply 'format
1511 (first fmt-and-args)
1512 (rest fmt-and-args))
1513 (format (concat "Press key for answer, "
1514 "%c=edit, %c=tags, %c=skip, %c=quit.")
1515 org-drill--edit-key
1516 org-drill--tags-key
1517 org-drill--skip-key
1518 org-drill--quit-key))))
1519 (setq prompt
1520 (format "%s %s %s %s %s %s"
1521 (propertize
1522 (char-to-string
1523 (cond
1524 ((eql status :failed) ?F)
1525 (*org-drill-cram-mode* ?C)
1527 (case status
1528 (:new ?N) (:young ?Y) (:old ?o) (:overdue ?!)
1529 (t ??)))))
1530 'face `(:foreground
1531 ,(case status
1532 (:new org-drill-new-count-color)
1533 ((:young :old) org-drill-mature-count-color)
1534 ((:overdue :failed) org-drill-failed-count-color)
1535 (t org-drill-done-count-color))))
1536 (propertize
1537 (number-to-string (length *org-drill-done-entries*))
1538 'face `(:foreground ,org-drill-done-count-color)
1539 'help-echo "The number of items you have reviewed this session.")
1540 (propertize
1541 (number-to-string (+ (length *org-drill-again-entries*)
1542 (length *org-drill-failed-entries*)))
1543 'face `(:foreground ,org-drill-failed-count-color)
1544 'help-echo (concat "The number of items that you failed, "
1545 "and need to review again."))
1546 (propertize
1547 (number-to-string mature-entry-count)
1548 'face `(:foreground ,org-drill-mature-count-color)
1549 'help-echo "The number of old items due for review.")
1550 (propertize
1551 (number-to-string (length *org-drill-new-entries*))
1552 'face `(:foreground ,org-drill-new-count-color)
1553 'help-echo (concat "The number of new items that you "
1554 "have never reviewed."))
1555 prompt))
1556 (if (and (eql 'warn org-drill-leech-method)
1557 (org-drill-entry-leech-p))
1558 (setq prompt (concat
1559 (propertize "!!! LEECH ITEM !!!
1560 You seem to be having a lot of trouble memorising this item.
1561 Consider reformulating the item to make it easier to remember.\n"
1562 'face '(:foreground "red"))
1563 prompt)))
1564 (while (memq ch '(nil org-drill--tags-key))
1565 (setq ch nil)
1566 (while (not (input-pending-p))
1567 (let ((elapsed (time-subtract (current-time) item-start-time)))
1568 (message (concat (if (>= (time-to-seconds elapsed) (* 60 60))
1569 "++:++ "
1570 (format-time-string "%M:%S " elapsed))
1571 prompt))
1572 (sit-for 1)))
1573 (setq input (read-key-sequence nil))
1574 (if (stringp input) (setq ch (elt input 0)))
1575 (if (eql ch org-drill--tags-key)
1576 (org-set-tags-command)))
1577 (case ch
1578 (org-drill--quit-key nil)
1579 (org-drill--edit-key 'edit)
1580 (org-drill--skip-key 'skip)
1581 (otherwise t))))
1584 (defun org-pos-in-regexp (pos regexp &optional nlines)
1585 (save-excursion
1586 (goto-char pos)
1587 (org-in-regexp regexp nlines)))
1590 (defun org-drill-hide-region (beg end &optional text)
1591 "Hide the buffer region between BEG and END with an 'invisible text'
1592 visual overlay, or with the string TEXT if it is supplied."
1593 (let ((ovl (make-overlay beg end)))
1594 (overlay-put ovl 'category
1595 'org-drill-hidden-text-overlay)
1596 (overlay-put ovl 'priority 9999)
1597 (when (stringp text)
1598 (overlay-put ovl 'invisible nil)
1599 (overlay-put ovl 'face 'default)
1600 (overlay-put ovl 'display text))))
1603 (defun org-drill-hide-heading-at-point (&optional text)
1604 (unless (org-at-heading-p)
1605 (error "Point is not on a heading."))
1606 (save-excursion
1607 (let ((beg (point)))
1608 (end-of-line)
1609 (org-drill-hide-region beg (point) text))))
1612 (defun org-drill-hide-comments ()
1613 (save-excursion
1614 (while (re-search-forward "^#.*$" nil t)
1615 (org-drill-hide-region (match-beginning 0) (match-end 0)))))
1618 (defun org-drill-unhide-text ()
1619 ;; This will also unhide the item's heading.
1620 (save-excursion
1621 (dolist (ovl (overlays-in (point-min) (point-max)))
1622 (when (eql 'org-drill-hidden-text-overlay (overlay-get ovl 'category))
1623 (delete-overlay ovl)))))
1626 (defun org-drill-hide-clozed-text ()
1627 (save-excursion
1628 (while (re-search-forward org-drill-cloze-regexp nil t)
1629 ;; Don't hide:
1630 ;; - org links, partly because they might contain inline
1631 ;; images which we want to keep visible.
1632 ;; - LaTeX math fragments
1633 ;; - the contents of SRC blocks
1634 (unless (save-match-data
1635 (or (org-pos-in-regexp (match-beginning 0)
1636 org-bracket-link-regexp 1)
1637 (org-in-src-block-p)
1638 (org-inside-LaTeX-fragment-p)))
1639 (org-drill-hide-matched-cloze-text)))))
1642 (defun org-drill-hide-matched-cloze-text ()
1643 "Hide the current match with a 'cloze' visual overlay."
1644 (let ((ovl (make-overlay (match-beginning 0) (match-end 0)))
1645 (hint-sep-pos (string-match-p (regexp-quote org-drill-hint-separator)
1646 (match-string 0))))
1647 (overlay-put ovl 'category
1648 'org-drill-cloze-overlay-defaults)
1649 (overlay-put ovl 'priority 9999)
1650 (when (and hint-sep-pos
1651 (> hint-sep-pos 1))
1652 (let ((hint (substring-no-properties
1653 (match-string 0)
1654 (+ hint-sep-pos (length org-drill-hint-separator))
1655 (1- (length (match-string 0))))))
1656 (overlay-put
1657 ovl 'display
1658 ;; If hint is like `X...' then display [X...]
1659 ;; otherwise display [...X]
1660 (format (if (string-match-p (regexp-quote "...") hint) "[%s]" "[%s...]")
1661 hint))))))
1664 (defun org-drill-hide-cloze-hints ()
1665 (save-excursion
1666 (while (re-search-forward org-drill-cloze-regexp nil t)
1667 (unless (or (save-match-data
1668 (org-pos-in-regexp (match-beginning 0)
1669 org-bracket-link-regexp 1))
1670 (null (match-beginning 2))) ; hint subexpression matched
1671 (org-drill-hide-region (match-beginning 2) (match-end 2))))))
1674 (defmacro with-replaced-entry-text (text &rest body)
1675 "During the execution of BODY, the entire text of the current entry is
1676 concealed by an overlay that displays the string TEXT."
1677 `(progn
1678 (org-drill-replace-entry-text ,text)
1679 (unwind-protect
1680 (progn
1681 ,@body)
1682 (org-drill-unreplace-entry-text))))
1685 (defmacro with-replaced-entry-text-multi (replacements &rest body)
1686 "During the execution of BODY, the entire text of the current entry is
1687 concealed by an overlay that displays the overlays in REPLACEMENTS."
1688 `(progn
1689 (org-drill-replace-entry-text ,replacements t)
1690 (unwind-protect
1691 (progn
1692 ,@body)
1693 (org-drill-unreplace-entry-text))))
1696 (defun org-drill-replace-entry-text (text &optional multi-p)
1697 "Make an overlay that conceals the entire text of the item, not
1698 including properties or the contents of subheadings. The overlay shows
1699 the string TEXT.
1700 If MULTI-P is non-nil, TEXT must be a list of values which are legal
1701 for the `display' text property. The text of the item will be temporarily
1702 replaced by all of these items, in the order in which they appear in
1703 the list.
1704 Note: does not actually alter the item."
1705 (cond
1706 ((and multi-p
1707 (listp text))
1708 (org-drill-replace-entry-text-multi text))
1710 (let ((ovl (make-overlay (point-min)
1711 (save-excursion
1712 (outline-next-heading)
1713 (point)))))
1714 (overlay-put ovl 'priority 9999)
1715 (overlay-put ovl 'category
1716 'org-drill-replaced-text-overlay)
1717 (overlay-put ovl 'display text)))))
1720 (defun org-drill-unreplace-entry-text ()
1721 (save-excursion
1722 (dolist (ovl (overlays-in (point-min) (point-max)))
1723 (when (eql 'org-drill-replaced-text-overlay (overlay-get ovl 'category))
1724 (delete-overlay ovl)))))
1727 (defun org-drill-replace-entry-text-multi (replacements)
1728 "Make overlays that conceal the entire text of the item, not
1729 including properties or the contents of subheadings. The overlay shows
1730 the string TEXT.
1731 Note: does not actually alter the item."
1732 (let ((ovl nil)
1733 (p-min (point-min))
1734 (p-max (save-excursion
1735 (outline-next-heading)
1736 (point))))
1737 (assert (>= (- p-max p-min) (length replacements)))
1738 (dotimes (i (length replacements))
1739 (setq ovl (make-overlay (+ p-min (* 2 i))
1740 (if (= i (1- (length replacements)))
1741 p-max
1742 (+ p-min (* 2 i) 1))))
1743 (overlay-put ovl 'priority 9999)
1744 (overlay-put ovl 'category
1745 'org-drill-replaced-text-overlay)
1746 (overlay-put ovl 'display (nth i replacements)))))
1749 (defmacro with-replaced-entry-heading (heading &rest body)
1750 `(progn
1751 (org-drill-replace-entry-heading ,heading)
1752 (unwind-protect
1753 (progn
1754 ,@body)
1755 (org-drill-unhide-text))))
1758 (defun org-drill-replace-entry-heading (heading)
1759 "Make an overlay that conceals the heading of the item. The overlay shows
1760 the string TEXT.
1761 Note: does not actually alter the item."
1762 (org-drill-hide-heading-at-point heading))
1765 (defun org-drill-unhide-clozed-text ()
1766 (save-excursion
1767 (dolist (ovl (overlays-in (point-min) (point-max)))
1768 (when (eql 'org-drill-cloze-overlay-defaults (overlay-get ovl 'category))
1769 (delete-overlay ovl)))))
1772 (defun org-drill-get-entry-text (&optional keep-properties-p)
1773 (let ((text (org-agenda-get-some-entry-text (point-marker) 100)))
1774 (if keep-properties-p
1775 text
1776 (substring-no-properties text))))
1779 ;; (defun org-entry-empty-p ()
1780 ;; (zerop (length (org-drill-get-entry-text))))
1782 ;; This version is about 5x faster than the old version, above.
1783 (defun org-entry-empty-p ()
1784 (save-excursion
1785 (org-back-to-heading t)
1786 (let ((lim (save-excursion
1787 (outline-next-heading) (point))))
1788 (if (fboundp 'org-end-of-meta-data-and-drawers)
1789 (org-end-of-meta-data-and-drawers) ; function removed Feb 2015
1790 (org-end-of-meta-data t))
1791 (or (>= (point) lim)
1792 (null (re-search-forward "[[:graph:]]" lim t))))))
1794 (defun org-drill-entry-empty-p () (org-entry-empty-p))
1797 ;;; Presentation functions ====================================================
1799 ;; Each of these is called with point on topic heading. Each needs to show the
1800 ;; topic in the form of a 'question' or with some information 'hidden', as
1801 ;; appropriate for the card type. The user should then be prompted to press a
1802 ;; key. The function should then reveal either the 'answer' or the entire
1803 ;; topic, and should return t if the user chose to see the answer and rate their
1804 ;; recall, nil if they chose to quit.
1807 (defun org-drill-present-simple-card ()
1808 (with-hidden-comments
1809 (with-hidden-cloze-hints
1810 (with-hidden-cloze-text
1811 (org-drill-hide-all-subheadings-except nil)
1812 (org-drill--show-latex-fragments) ; overlay all LaTeX fragments with images
1813 (ignore-errors
1814 (org-display-inline-images t))
1815 (org-cycle-hide-drawers 'all)
1816 (prog1 (org-drill-presentation-prompt)
1817 (org-drill-hide-subheadings-if 'org-drill-entry-p))))))
1820 (defun org-drill-present-default-answer (reschedule-fn)
1821 (cond
1822 (drill-answer
1823 (with-replaced-entry-text
1824 (format "\nAnswer:\n\n %s\n" drill-answer)
1825 (prog1
1826 (funcall reschedule-fn)
1827 (setq drill-answer nil))))
1829 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1830 (org-drill-unhide-clozed-text)
1831 (org-drill--show-latex-fragments)
1832 (ignore-errors
1833 (org-display-inline-images t))
1834 (org-cycle-hide-drawers 'all)
1835 (with-hidden-cloze-hints
1836 (funcall reschedule-fn)))))
1839 (defun org-drill--show-latex-fragments ()
1840 (org-remove-latex-fragment-image-overlays)
1841 (if (fboundp 'org-toggle-latex-fragment)
1842 (org-toggle-latex-fragment '(4))
1843 (org-preview-latex-fragment '(4))))
1846 (defun org-drill-present-two-sided-card ()
1847 (with-hidden-comments
1848 (with-hidden-cloze-hints
1849 (with-hidden-cloze-text
1850 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1851 (when drill-sections
1852 (save-excursion
1853 (goto-char (nth (random* (min 2 (length drill-sections)))
1854 drill-sections))
1855 (org-show-subtree)))
1856 (org-drill--show-latex-fragments)
1857 (ignore-errors
1858 (org-display-inline-images t))
1859 (org-cycle-hide-drawers 'all)
1860 (prog1 (org-drill-presentation-prompt)
1861 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1865 (defun org-drill-present-multi-sided-card ()
1866 (with-hidden-comments
1867 (with-hidden-cloze-hints
1868 (with-hidden-cloze-text
1869 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1870 (when drill-sections
1871 (save-excursion
1872 (goto-char (nth (random* (length drill-sections)) drill-sections))
1873 (org-show-subtree)))
1874 (org-drill--show-latex-fragments)
1875 (ignore-errors
1876 (org-display-inline-images t))
1877 (org-cycle-hide-drawers 'all)
1878 (prog1 (org-drill-presentation-prompt)
1879 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1882 (defun org-drill-present-multicloze-hide-n (number-to-hide
1883 &optional
1884 force-show-first
1885 force-show-last
1886 force-hide-first)
1887 "Hides NUMBER-TO-HIDE pieces of text that are marked for cloze deletion,
1888 chosen at random.
1889 If NUMBER-TO-HIDE is negative, show only (ABS NUMBER-TO-HIDE) pieces,
1890 hiding all the rest.
1891 If FORCE-HIDE-FIRST is non-nil, force the first piece of text to be one of
1892 the hidden items.
1893 If FORCE-SHOW-FIRST is non-nil, never hide the first piece of text.
1894 If FORCE-SHOW-LAST is non-nil, never hide the last piece of text.
1895 If the number of text pieces in the item is less than
1896 NUMBER-TO-HIDE, then all text pieces will be hidden (except the first or last
1897 items if FORCE-SHOW-FIRST or FORCE-SHOW-LAST is non-nil)."
1898 (with-hidden-comments
1899 (with-hidden-cloze-hints
1900 (let ((item-end nil)
1901 (match-count 0)
1902 (body-start (or (cdr (org-get-property-block))
1903 (point))))
1904 (if (and force-hide-first force-show-first)
1905 (error "FORCE-HIDE-FIRST and FORCE-SHOW-FIRST are mutually exclusive"))
1906 (org-drill-hide-all-subheadings-except nil)
1907 (save-excursion
1908 (outline-next-heading)
1909 (setq item-end (point)))
1910 (save-excursion
1911 (goto-char body-start)
1912 (while (re-search-forward org-drill-cloze-regexp item-end t)
1913 (let ((in-regexp? (save-match-data
1914 (org-pos-in-regexp (match-beginning 0)
1915 org-bracket-link-regexp 1))))
1916 (unless (or in-regexp?
1917 (org-inside-LaTeX-fragment-p))
1918 (incf match-count)))))
1919 (if (minusp number-to-hide)
1920 (setq number-to-hide (+ match-count number-to-hide)))
1921 (when (plusp match-count)
1922 (let* ((positions (shuffle-list (loop for i from 1
1923 to match-count
1924 collect i)))
1925 (match-nums nil)
1926 (cnt nil))
1927 (if force-hide-first
1928 ;; Force '1' to be in the list, and to be the first item
1929 ;; in the list.
1930 (setq positions (cons 1 (remove 1 positions))))
1931 (if force-show-first
1932 (setq positions (remove 1 positions)))
1933 (if force-show-last
1934 (setq positions (remove match-count positions)))
1935 (setq match-nums
1936 (subseq positions
1937 0 (min number-to-hide (length positions))))
1938 ;; (dolist (pos-to-hide match-nums)
1939 (save-excursion
1940 (goto-char body-start)
1941 (setq cnt 0)
1942 (while (re-search-forward org-drill-cloze-regexp item-end t)
1943 (unless (save-match-data
1944 (or (org-pos-in-regexp (match-beginning 0)
1945 org-bracket-link-regexp 1)
1946 (org-inside-LaTeX-fragment-p)))
1947 (incf cnt)
1948 (if (memq cnt match-nums)
1949 (org-drill-hide-matched-cloze-text)))))))
1950 ;; (loop
1951 ;; do (re-search-forward org-drill-cloze-regexp
1952 ;; item-end t pos-to-hide)
1953 ;; while (org-pos-in-regexp (match-beginning 0)
1954 ;; org-bracket-link-regexp 1))
1955 ;; (org-drill-hide-matched-cloze-text)))))
1956 (org-drill--show-latex-fragments)
1957 (ignore-errors
1958 (org-display-inline-images t))
1959 (org-cycle-hide-drawers 'all)
1960 (prog1 (org-drill-presentation-prompt)
1961 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1962 (org-drill-unhide-clozed-text))))))
1965 (defun org-drill-present-multicloze-hide-nth (to-hide)
1966 "Hide the TO-HIDE'th piece of clozed text. 1 is the first piece. If
1967 TO-HIDE is negative, count backwards, so -1 means the last item, -2
1968 the second to last, etc."
1969 (with-hidden-comments
1970 (with-hidden-cloze-hints
1971 (let ((item-end nil)
1972 (match-count 0)
1973 (body-start (or (cdr (org-get-property-block))
1974 (point)))
1975 (cnt 0))
1976 (org-drill-hide-all-subheadings-except nil)
1977 (save-excursion
1978 (outline-next-heading)
1979 (setq item-end (point)))
1980 (save-excursion
1981 (goto-char body-start)
1982 (while (re-search-forward org-drill-cloze-regexp item-end t)
1983 (let ((in-regexp? (save-match-data
1984 (org-pos-in-regexp (match-beginning 0)
1985 org-bracket-link-regexp 1))))
1986 (unless (or in-regexp?
1987 (org-inside-LaTeX-fragment-p))
1988 (incf match-count)))))
1989 (if (minusp to-hide)
1990 (setq to-hide (+ 1 to-hide match-count)))
1991 (cond
1992 ((or (not (plusp match-count))
1993 (> to-hide match-count))
1994 nil)
1996 (save-excursion
1997 (goto-char body-start)
1998 (setq cnt 0)
1999 (while (re-search-forward org-drill-cloze-regexp item-end t)
2000 (unless (save-match-data
2001 ;; Don't consider this a cloze region if it is part of an
2002 ;; org link, or if it occurs inside a LaTeX math
2003 ;; fragment
2004 (or (org-pos-in-regexp (match-beginning 0)
2005 org-bracket-link-regexp 1)
2006 (org-inside-LaTeX-fragment-p)))
2007 (incf cnt)
2008 (if (= cnt to-hide)
2009 (org-drill-hide-matched-cloze-text)))))))
2010 (org-drill--show-latex-fragments)
2011 (ignore-errors
2012 (org-display-inline-images t))
2013 (org-cycle-hide-drawers 'all)
2014 (prog1 (org-drill-presentation-prompt)
2015 (org-drill-hide-subheadings-if 'org-drill-entry-p)
2016 (org-drill-unhide-clozed-text))))))
2019 (defun org-drill-present-multicloze-hide1 ()
2020 "Hides one of the pieces of text that are marked for cloze deletion,
2021 chosen at random."
2022 (org-drill-present-multicloze-hide-n 1))
2025 (defun org-drill-present-multicloze-hide2 ()
2026 "Hides two of the pieces of text that are marked for cloze deletion,
2027 chosen at random."
2028 (org-drill-present-multicloze-hide-n 2))
2031 (defun org-drill-present-multicloze-hide-first ()
2032 "Hides the first piece of text that is marked for cloze deletion."
2033 (org-drill-present-multicloze-hide-nth 1))
2036 (defun org-drill-present-multicloze-hide-last ()
2037 "Hides the last piece of text that is marked for cloze deletion."
2038 (org-drill-present-multicloze-hide-nth -1))
2041 (defun org-drill-present-multicloze-hide1-firstmore ()
2042 "Commonly, hides the FIRST piece of text that is marked for
2043 cloze deletion. Uncommonly, hide one of the other pieces of text,
2044 chosen at random.
2046 The definitions of 'commonly' and 'uncommonly' are determined by
2047 the value of `org-drill-cloze-text-weight'."
2048 ;; The 'firstmore' and 'lastmore' functions used to randomly choose whether
2049 ;; to hide the 'favoured' piece of text. However even when the chance of
2050 ;; hiding it was set quite high (80%), the outcome was too unpredictable over
2051 ;; the small number of repetitions where most learning takes place for each
2052 ;; item. In other words, the actual frequency during the first 10 repetitions
2053 ;; was often very different from 80%. Hence we use modulo instead.
2054 (cond
2055 ((null org-drill-cloze-text-weight)
2056 ;; Behave as hide1cloze
2057 (org-drill-present-multicloze-hide1))
2058 ((not (and (integerp org-drill-cloze-text-weight)
2059 (plusp org-drill-cloze-text-weight)))
2060 (error "Illegal value for org-drill-cloze-text-weight: %S"
2061 org-drill-cloze-text-weight))
2062 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
2063 org-drill-cloze-text-weight))
2064 ;; Uncommonly, hide any item except the first
2065 (org-drill-present-multicloze-hide-n 1 t))
2067 ;; Commonly, hide first item
2068 (org-drill-present-multicloze-hide-first))))
2071 (defun org-drill-present-multicloze-show1-lastmore ()
2072 "Commonly, hides all pieces except the last. Uncommonly, shows
2073 any random piece. The effect is similar to 'show1cloze' except
2074 that the last item is much less likely to be the item that is
2075 visible.
2077 The definitions of 'commonly' and 'uncommonly' are determined by
2078 the value of `org-drill-cloze-text-weight'."
2079 (cond
2080 ((null org-drill-cloze-text-weight)
2081 ;; Behave as show1cloze
2082 (org-drill-present-multicloze-show1))
2083 ((not (and (integerp org-drill-cloze-text-weight)
2084 (plusp org-drill-cloze-text-weight)))
2085 (error "Illegal value for org-drill-cloze-text-weight: %S"
2086 org-drill-cloze-text-weight))
2087 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
2088 org-drill-cloze-text-weight))
2089 ;; Uncommonly, show any item except the last
2090 (org-drill-present-multicloze-hide-n -1 nil nil t))
2092 ;; Commonly, show the LAST item
2093 (org-drill-present-multicloze-hide-n -1 nil t))))
2096 (defun org-drill-present-multicloze-show1-firstless ()
2097 "Commonly, hides all pieces except one, where the shown piece
2098 is guaranteed NOT to be the first piece. Uncommonly, shows any
2099 random piece. The effect is similar to 'show1cloze' except that
2100 the first item is much less likely to be the item that is
2101 visible.
2103 The definitions of 'commonly' and 'uncommonly' are determined by
2104 the value of `org-drill-cloze-text-weight'."
2105 (cond
2106 ((null org-drill-cloze-text-weight)
2107 ;; Behave as show1cloze
2108 (org-drill-present-multicloze-show1))
2109 ((not (and (integerp org-drill-cloze-text-weight)
2110 (plusp org-drill-cloze-text-weight)))
2111 (error "Illegal value for org-drill-cloze-text-weight: %S"
2112 org-drill-cloze-text-weight))
2113 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
2114 org-drill-cloze-text-weight))
2115 ;; Uncommonly, show the first item
2116 (org-drill-present-multicloze-hide-n -1 t))
2118 ;; Commonly, show any item, except the first
2119 (org-drill-present-multicloze-hide-n -1 nil nil t))))
2122 (defun org-drill-present-multicloze-show1 ()
2123 "Similar to `org-drill-present-multicloze-hide1', but hides all
2124 the pieces of text that are marked for cloze deletion, except for one
2125 piece which is chosen at random."
2126 (org-drill-present-multicloze-hide-n -1))
2129 (defun org-drill-present-multicloze-show2 ()
2130 "Similar to `org-drill-present-multicloze-show1', but reveals two
2131 pieces rather than one."
2132 (org-drill-present-multicloze-hide-n -2))
2135 (defun org-drill-present-card-using-text (question &optional answer)
2136 "Present the string QUESTION as the only visible content of the card.
2137 If ANSWER is supplied, set the global variable `drill-answer' to its value."
2138 (if answer (setq drill-answer answer))
2139 (with-hidden-comments
2140 (with-replaced-entry-text
2141 (concat "\n" question)
2142 (org-drill-hide-all-subheadings-except nil)
2143 (org-cycle-hide-drawers 'all)
2144 (ignore-errors
2145 (org-display-inline-images t))
2146 (prog1 (org-drill-presentation-prompt)
2147 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
2150 (defun org-drill-present-card-using-multiple-overlays (replacements &optional answer)
2151 "TEXTS is a list of valid values for the 'display' text property.
2152 Present these overlays, in sequence, as the only
2153 visible content of the card.
2154 If ANSWER is supplied, set the global variable `drill-answer' to its value."
2155 (if answer (setq drill-answer answer))
2156 (with-hidden-comments
2157 (with-replaced-entry-text-multi
2158 replacements
2159 (org-drill-hide-all-subheadings-except nil)
2160 (org-cycle-hide-drawers 'all)
2161 (ignore-errors
2162 (org-display-inline-images t))
2163 (prog1 (org-drill-presentation-prompt)
2164 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
2167 (defun org-drill-entry ()
2168 "Present the current topic for interactive review, as in `org-drill'.
2169 Review will occur regardless of whether the topic is due for review or whether
2170 it meets the definition of a 'review topic' used by `org-drill'.
2172 Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
2173 EDIT if the user chose to exit the drill and edit the current item. Choosing
2174 the latter option leaves the drill session suspended; it can be resumed
2175 later using `org-drill-resume'.
2177 See `org-drill' for more details."
2178 (interactive)
2179 (org-drill-goto-drill-entry-heading)
2180 ;;(unless (org-part-of-drill-entry-p)
2181 ;; (error "Point is not inside a drill entry"))
2182 ;;(unless (org-at-heading-p)
2183 ;; (org-back-to-heading))
2184 (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE" t))
2185 (answer-fn 'org-drill-present-default-answer)
2186 (present-empty-cards nil)
2187 (cont nil)
2188 ;; fontification functions in `outline-view-change-hook' can cause big
2189 ;; slowdowns, so we temporarily bind this variable to nil here.
2190 (outline-view-change-hook nil))
2191 (setq drill-answer nil)
2192 (org-save-outline-visibility t
2193 (save-restriction
2194 (org-narrow-to-subtree)
2195 (org-show-subtree)
2196 (org-cycle-hide-drawers 'all)
2198 (let ((presentation-fn
2199 (cdr (assoc card-type org-drill-card-type-alist))))
2200 (if (listp presentation-fn)
2201 (psetq answer-fn (or (second presentation-fn)
2202 'org-drill-present-default-answer)
2203 present-empty-cards (third presentation-fn)
2204 presentation-fn (first presentation-fn)))
2205 (prog1
2206 (cond
2207 ((null presentation-fn)
2208 (message "%s:%d: Unrecognised card type '%s', skipping..."
2209 (buffer-name) (point) card-type)
2210 (sit-for 0.5)
2211 'skip)
2213 (setq cont (funcall presentation-fn))
2214 (cond
2215 ((not cont)
2216 (message "Quit")
2217 nil)
2218 ((eql cont 'edit)
2219 'edit)
2220 ((eql cont 'skip)
2221 'skip)
2223 (save-excursion
2224 (funcall answer-fn
2225 (lambda () (org-drill-reschedule))))))))
2226 (org-remove-latex-fragment-image-overlays)))))))
2229 (defun org-drill-entries-pending-p ()
2230 (or *org-drill-again-entries*
2231 *org-drill-current-item*
2232 (and (not (org-drill-maximum-item-count-reached-p))
2233 (not (org-drill-maximum-duration-reached-p))
2234 (or *org-drill-new-entries*
2235 *org-drill-failed-entries*
2236 *org-drill-young-mature-entries*
2237 *org-drill-old-mature-entries*
2238 *org-drill-overdue-entries*
2239 *org-drill-again-entries*))))
2242 (defun org-drill-pending-entry-count ()
2243 (+ (if (markerp *org-drill-current-item*) 1 0)
2244 (length *org-drill-new-entries*)
2245 (length *org-drill-failed-entries*)
2246 (length *org-drill-young-mature-entries*)
2247 (length *org-drill-old-mature-entries*)
2248 (length *org-drill-overdue-entries*)
2249 (length *org-drill-again-entries*)))
2252 (defun org-drill-maximum-duration-reached-p ()
2253 "Returns true if the current drill session has continued past its
2254 maximum duration."
2255 (and org-drill-maximum-duration
2256 (not *org-drill-cram-mode*)
2257 *org-drill-start-time*
2258 (> (- (float-time (current-time)) *org-drill-start-time*)
2259 (* org-drill-maximum-duration 60))))
2262 (defun org-drill-maximum-item-count-reached-p ()
2263 "Returns true if the current drill session has reached the
2264 maximum number of items."
2265 (and org-drill-maximum-items-per-session
2266 (not *org-drill-cram-mode*)
2267 (>= (length *org-drill-done-entries*)
2268 org-drill-maximum-items-per-session)))
2271 (defun org-drill-pop-next-pending-entry ()
2272 (block org-drill-pop-next-pending-entry
2273 (let ((m nil))
2274 (while (or (null m)
2275 (not (org-drill-entry-p m)))
2276 (setq
2278 (cond
2279 ;; First priority is items we failed in a prior session.
2280 ((and *org-drill-failed-entries*
2281 (not (org-drill-maximum-item-count-reached-p))
2282 (not (org-drill-maximum-duration-reached-p)))
2283 (pop-random *org-drill-failed-entries*))
2284 ;; Next priority is overdue items.
2285 ((and *org-drill-overdue-entries*
2286 (not (org-drill-maximum-item-count-reached-p))
2287 (not (org-drill-maximum-duration-reached-p)))
2288 ;; We use `pop', not `pop-random', because we have already
2289 ;; sorted overdue items into a random order which takes
2290 ;; number of days overdue into account.
2291 (pop *org-drill-overdue-entries*))
2292 ;; Next priority is 'young' items.
2293 ((and *org-drill-young-mature-entries*
2294 (not (org-drill-maximum-item-count-reached-p))
2295 (not (org-drill-maximum-duration-reached-p)))
2296 (pop-random *org-drill-young-mature-entries*))
2297 ;; Next priority is newly added items, and older entries.
2298 ;; We pool these into a single group.
2299 ((and (or *org-drill-new-entries*
2300 *org-drill-old-mature-entries*)
2301 (not (org-drill-maximum-item-count-reached-p))
2302 (not (org-drill-maximum-duration-reached-p)))
2303 (cond
2304 ((< (random* (+ (length *org-drill-new-entries*)
2305 (length *org-drill-old-mature-entries*)))
2306 (length *org-drill-new-entries*))
2307 (pop-random *org-drill-new-entries*))
2309 (pop-random *org-drill-old-mature-entries*))))
2310 ;; After all the above are done, last priority is items
2311 ;; that were failed earlier THIS SESSION.
2312 (*org-drill-again-entries*
2313 (pop *org-drill-again-entries*))
2314 (t ; nothing left -- return nil
2315 (return-from org-drill-pop-next-pending-entry nil)))))
2316 m)))
2319 (defun org-drill-entries (&optional resuming-p)
2320 "Returns nil, t, or a list of markers representing entries that were
2321 'failed' and need to be presented again before the session ends.
2323 RESUMING-P is true if we are resuming a suspended drill session."
2324 (block org-drill-entries
2325 (while (org-drill-entries-pending-p)
2326 (let ((m (cond
2327 ((or (not resuming-p)
2328 (null *org-drill-current-item*)
2329 (not (org-drill-entry-p *org-drill-current-item*)))
2330 (org-drill-pop-next-pending-entry))
2331 (t ; resuming a suspended session.
2332 (setq resuming-p nil)
2333 *org-drill-current-item*))))
2334 (setq *org-drill-current-item* m)
2335 (unless m
2336 (error "Unexpectedly ran out of pending drill items"))
2337 (save-excursion
2338 (org-drill-goto-entry m)
2339 (cond
2340 ((not (org-drill-entry-due-p))
2341 ;; The entry is not due anymore. This could arise if the user
2342 ;; suspends a drill session, then drills an individual entry,
2343 ;; then resumes the session.
2344 (message "Entry no longer due, skipping...")
2345 (sit-for 0.3)
2346 nil)
2348 (setq result (org-drill-entry))
2349 (cond
2350 ((null result)
2351 (message "Quit")
2352 (setq end-pos :quit)
2353 (return-from org-drill-entries nil))
2354 ((eql result 'edit)
2355 (setq end-pos (point-marker))
2356 (return-from org-drill-entries nil))
2357 ((eql result 'skip)
2358 (setq *org-drill-current-item* nil)
2359 nil) ; skip this item
2361 (cond
2362 ((<= result org-drill-failure-quality)
2363 (if *org-drill-again-entries*
2364 (setq *org-drill-again-entries*
2365 (shuffle-list *org-drill-again-entries*)))
2366 (push-end m *org-drill-again-entries*))
2368 (push m *org-drill-done-entries*)))
2369 (setq *org-drill-current-item* nil))))))))))
2373 (defun org-drill-final-report ()
2374 (let ((pass-percent
2375 (round (* 100 (count-if (lambda (qual)
2376 (> qual org-drill-failure-quality))
2377 *org-drill-session-qualities*))
2378 (max 1 (length *org-drill-session-qualities*))))
2379 (prompt nil)
2380 (max-mini-window-height 0.6))
2381 (setq prompt
2382 (format
2383 "%d items reviewed. Session duration %s.
2384 Recall of reviewed items:
2385 Excellent (5): %3d%% | Near miss (2): %3d%%
2386 Good (4): %3d%% | Failure (1): %3d%%
2387 Hard (3): %3d%% | Abject failure (0): %3d%%
2389 You successfully recalled %d%% of reviewed items (quality > %s)
2390 %d/%d items still await review (%s, %s, %s, %s, %s).
2391 Tomorrow, %d more items will become due for review.
2392 Session finished. Press a key to continue..."
2393 (length *org-drill-done-entries*)
2394 (format-seconds "%h:%.2m:%.2s"
2395 (- (float-time (current-time)) *org-drill-start-time*))
2396 (round (* 100 (count 5 *org-drill-session-qualities*))
2397 (max 1 (length *org-drill-session-qualities*)))
2398 (round (* 100 (count 2 *org-drill-session-qualities*))
2399 (max 1 (length *org-drill-session-qualities*)))
2400 (round (* 100 (count 4 *org-drill-session-qualities*))
2401 (max 1 (length *org-drill-session-qualities*)))
2402 (round (* 100 (count 1 *org-drill-session-qualities*))
2403 (max 1 (length *org-drill-session-qualities*)))
2404 (round (* 100 (count 3 *org-drill-session-qualities*))
2405 (max 1 (length *org-drill-session-qualities*)))
2406 (round (* 100 (count 0 *org-drill-session-qualities*))
2407 (max 1 (length *org-drill-session-qualities*)))
2408 pass-percent
2409 org-drill-failure-quality
2410 (org-drill-pending-entry-count)
2411 (+ (org-drill-pending-entry-count)
2412 *org-drill-dormant-entry-count*)
2413 (propertize
2414 (format "%d failed"
2415 (+ (length *org-drill-failed-entries*)
2416 (length *org-drill-again-entries*)))
2417 'face `(:foreground ,org-drill-failed-count-color))
2418 (propertize
2419 (format "%d overdue"
2420 (length *org-drill-overdue-entries*))
2421 'face `(:foreground ,org-drill-failed-count-color))
2422 (propertize
2423 (format "%d new"
2424 (length *org-drill-new-entries*))
2425 'face `(:foreground ,org-drill-new-count-color))
2426 (propertize
2427 (format "%d young"
2428 (length *org-drill-young-mature-entries*))
2429 'face `(:foreground ,org-drill-mature-count-color))
2430 (propertize
2431 (format "%d old"
2432 (length *org-drill-old-mature-entries*))
2433 'face `(:foreground ,org-drill-mature-count-color))
2434 *org-drill-due-tomorrow-count*
2437 (while (not (input-pending-p))
2438 (message "%s" prompt)
2439 (sit-for 0.5))
2440 (read-char-exclusive)
2442 (if (and *org-drill-session-qualities*
2443 (< pass-percent (- 100 org-drill-forgetting-index)))
2444 (read-char-exclusive
2445 (format
2447 You failed %d%% of the items you reviewed during this session.
2448 %d (%d%%) of all items scanned were overdue.
2450 Are you keeping up with your items, and reviewing them
2451 when they are scheduled? If so, you may want to consider
2452 lowering the value of `org-drill-learn-fraction' slightly in
2453 order to make items appear more frequently over time."
2454 (propertize "WARNING!" 'face 'org-warning)
2455 (- 100 pass-percent)
2456 *org-drill-overdue-entry-count*
2457 (round (* 100 *org-drill-overdue-entry-count*)
2458 (+ *org-drill-dormant-entry-count*
2459 *org-drill-due-entry-count*)))
2460 ))))
2464 (defun org-drill-free-markers (markers)
2465 "MARKERS is a list of markers, all of which will be freed (set to
2466 point nowhere). Alternatively, MARKERS can be 't', in which case
2467 all the markers used by Org-Drill will be freed."
2468 (dolist (m (if (eql t markers)
2469 (append *org-drill-done-entries*
2470 *org-drill-new-entries*
2471 *org-drill-failed-entries*
2472 *org-drill-again-entries*
2473 *org-drill-overdue-entries*
2474 *org-drill-young-mature-entries*
2475 *org-drill-old-mature-entries*)
2476 markers))
2477 (free-marker m)))
2480 ;;; overdue-data is a list of entries, each entry has the form (POS DUE AGE)
2481 ;;; where POS is a marker pointing to the start of the entry, and
2482 ;;; DUE is a number indicating how many days ago the entry was due.
2483 ;;; AGE is the number of days elapsed since item creation (nil if unknown).
2484 ;;; if age > lapse threshold (default 90), sort by age (oldest first)
2485 ;;; if age < lapse threshold, sort by due (biggest first)
2488 (defun org-drill-order-overdue-entries (overdue-data)
2489 (let* ((lapsed-days (if org-drill--lapse-very-overdue-entries-p
2490 90 most-positive-fixnum))
2491 (not-lapsed (remove-if (lambda (a) (> (or (second a) 0) lapsed-days))
2492 overdue-data))
2493 (lapsed (remove-if-not (lambda (a) (> (or (second a) 0)
2494 lapsed-days)) overdue-data)))
2495 (setq *org-drill-overdue-entries*
2496 (mapcar 'first
2497 (append
2498 (sort (shuffle-list not-lapsed)
2499 (lambda (a b) (> (second a) (second b))))
2500 (sort lapsed
2501 (lambda (a b) (> (third a) (third b)))))))))
2504 (defun org-drill--entry-lapsed-p ()
2505 (let ((lapsed-days 90))
2506 (and org-drill--lapse-very-overdue-entries-p
2507 (> (or (org-drill-entry-days-overdue) 0) lapsed-days))))
2512 (defun org-drill-entry-days-since-creation (&optional use-last-interval-p)
2513 "If USE-LAST-INTERVAL-P is non-nil, and DATE_ADDED is missing, use the
2514 value of DRILL_LAST_INTERVAL instead (as the item's age must be at least
2515 that many days)."
2516 (let ((timestamp (org-entry-get (point) "DATE_ADDED")))
2517 (cond
2518 (timestamp
2519 (- (org-time-stamp-to-now timestamp)))
2520 (use-last-interval-p
2521 (+ (or (org-drill-entry-days-overdue) 0)
2522 (read (or (org-entry-get (point) "DRILL_LAST_INTERVAL") "0"))))
2523 (t nil))))
2526 (defun org-drill-entry-status ()
2527 "Returns a list (STATUS DUE AGE) where DUE is the number of days overdue,
2528 zero being due today, -1 being scheduled 1 day in the future.
2529 AGE is the number of days elapsed since the item was created (nil if unknown).
2530 STATUS is one of the following values:
2531 - nil, if the item is not a drill entry, or has an empty body
2532 - :unscheduled
2533 - :future
2534 - :new
2535 - :failed
2536 - :overdue
2537 - :young
2538 - :old
2540 (save-excursion
2541 (unless (org-at-heading-p)
2542 (org-back-to-heading))
2543 (let ((due (org-drill-entry-days-overdue))
2544 (age (org-drill-entry-days-since-creation t))
2545 (last-int (org-drill-entry-last-interval 1)))
2546 (list
2547 (cond
2548 ((not (org-drill-entry-p))
2549 nil)
2550 ((and (org-entry-empty-p)
2551 (let* ((card-type (org-entry-get (point) "DRILL_CARD_TYPE" nil))
2552 (dat (cdr (assoc card-type org-drill-card-type-alist))))
2553 (or (null card-type)
2554 (not (third dat)))))
2555 ;; body is empty, and this is not a card type where empty bodies are
2556 ;; meaningful, so skip it.
2557 nil)
2558 ((null due) ; unscheduled - usually a skipped leech
2559 :unscheduled)
2560 ;; ((eql -1 due)
2561 ;; :tomorrow)
2562 ((minusp due) ; scheduled in the future
2563 :future)
2564 ;; The rest of the stati all denote 'due' items ==========================
2565 ((<= (org-drill-entry-last-quality 9999)
2566 org-drill-failure-quality)
2567 ;; Mature entries that were failed last time are
2568 ;; FAILED, regardless of how young, old or overdue
2569 ;; they are.
2570 :failed)
2571 ((org-drill-entry-new-p)
2572 :new)
2573 ((org-drill-entry-overdue-p due last-int)
2574 ;; Overdue status overrides young versus old
2575 ;; distinction.
2576 ;; Store marker + due, for sorting of overdue entries
2577 :overdue)
2578 ((<= (org-drill-entry-last-interval 9999)
2579 org-drill-days-before-old)
2580 :young)
2582 :old))
2583 due age))))
2586 (defun org-drill-progress-message (collected scanned)
2587 (when (zerop (% scanned 50))
2588 (let* ((meter-width 40)
2589 (sym1 (if (oddp (floor scanned (* 50 meter-width))) ?| ?.))
2590 (sym2 (if (eql sym1 ?.) ?| ?.)))
2591 (message "Collecting due drill items:%4d %s%s"
2592 collected
2593 (make-string (% (ceiling scanned 50) meter-width)
2594 sym2)
2595 (make-string (- meter-width (% (ceiling scanned 50) meter-width))
2596 sym1)))))
2599 (defun org-map-drill-entry-function ()
2600 (org-drill-progress-message
2601 (+ (length *org-drill-new-entries*)
2602 (length *org-drill-overdue-entries*)
2603 (length *org-drill-young-mature-entries*)
2604 (length *org-drill-old-mature-entries*)
2605 (length *org-drill-failed-entries*))
2606 (incf cnt))
2607 (cond
2608 ((not (org-drill-entry-p))
2609 nil) ; skip
2611 (when (and (not warned-about-id-creation)
2612 (null (org-id-get)))
2613 (message (concat "Creating unique IDs for items "
2614 "(slow, but only happens once)"))
2615 (sit-for 0.5)
2616 (setq warned-about-id-creation t))
2617 (org-id-get-create) ; ensure drill entry has unique ID
2618 (destructuring-bind (status due age)
2619 (org-drill-entry-status)
2620 (case status
2621 (:unscheduled
2622 (incf *org-drill-dormant-entry-count*))
2623 ;; (:tomorrow
2624 ;; (incf *org-drill-dormant-entry-count*)
2625 ;; (incf *org-drill-due-tomorrow-count*))
2626 (:future
2627 (incf *org-drill-dormant-entry-count*)
2628 (if (eq -1 due)
2629 (incf *org-drill-due-tomorrow-count*)))
2630 (:new
2631 (push (point-marker) *org-drill-new-entries*))
2632 (:failed
2633 (push (point-marker) *org-drill-failed-entries*))
2634 (:young
2635 (push (point-marker) *org-drill-young-mature-entries*))
2636 (:overdue
2637 (push (list (point-marker) due age) overdue-data))
2638 (:old
2639 (push (point-marker) *org-drill-old-mature-entries*))
2640 )))))
2643 (defun org-drill (&optional scope drill-match resume-p)
2644 "Begin an interactive 'drill session'. The user is asked to
2645 review a series of topics (headers). Each topic is initially
2646 presented as a 'question', often with part of the topic content
2647 hidden. The user attempts to recall the hidden information or
2648 answer the question, then presses a key to reveal the answer. The
2649 user then rates his or her recall or performance on that
2650 topic. This rating information is used to reschedule the topic
2651 for future review.
2653 Org-drill proceeds by:
2655 - Finding all topics (headings) in SCOPE which have either been
2656 used and rescheduled before, or which have a tag that matches
2657 `org-drill-question-tag'.
2659 - All matching topics which are either unscheduled, or are
2660 scheduled for the current date or a date in the past, are
2661 considered to be candidates for the drill session.
2663 - If `org-drill-maximum-items-per-session' is set, a random
2664 subset of these topics is presented. Otherwise, all of the
2665 eligible topics will be presented.
2667 SCOPE determines the scope in which to search for
2668 questions. It accepts the same values as `org-drill-scope',
2669 which see.
2671 DRILL-MATCH, if supplied, is a string specifying a tags/property/
2672 todo query. Only items matching the query will be considered.
2673 It accepts the same values as `org-drill-match', which see.
2675 If RESUME-P is non-nil, resume a suspended drill session rather
2676 than starting a new one."
2678 (interactive)
2679 ;; Check org version. Org 7.9.3f introduced a backwards-incompatible change
2680 ;; to the arguments accepted by `org-schedule'. At the time of writing there
2681 ;; are still lots of people using versions of org older than this.
2682 (let ((majorv (first (mapcar 'string-to-number (split-string (org-release) "[.]")))))
2683 (if (and (< majorv 8)
2684 (not (string-match-p "universal prefix argument" (documentation 'org-schedule))))
2685 (read-char-exclusive
2686 (format "Warning: org-drill requires org mode 7.9.3f or newer. Scheduling of failed cards will not
2687 work correctly with older versions of org mode. Your org mode version (%s) appears to be older than
2688 7.9.3f. Please consider installing a more recent version of org mode." (org-release)))))
2689 (let ((end-pos nil)
2690 (overdue-data nil)
2691 (cnt 0))
2692 (block org-drill
2693 (unless resume-p
2694 (org-drill-free-markers t)
2695 (setq *org-drill-current-item* nil
2696 *org-drill-done-entries* nil
2697 *org-drill-dormant-entry-count* 0
2698 *org-drill-due-entry-count* 0
2699 *org-drill-due-tomorrow-count* 0
2700 *org-drill-overdue-entry-count* 0
2701 *org-drill-new-entries* nil
2702 *org-drill-overdue-entries* nil
2703 *org-drill-young-mature-entries* nil
2704 *org-drill-old-mature-entries* nil
2705 *org-drill-failed-entries* nil
2706 *org-drill-again-entries* nil)
2707 (setq *org-drill-session-qualities* nil)
2708 (setq *org-drill-start-time* (float-time (current-time))))
2709 (setq *random-state* (make-random-state t)) ; reseed RNG
2710 (unwind-protect
2711 (save-excursion
2712 (unless resume-p
2713 (let ((org-trust-scanner-tags t)
2714 (warned-about-id-creation nil))
2715 (org-map-drill-entries
2716 'org-map-drill-entry-function
2717 scope drill-match)
2718 (org-drill-order-overdue-entries overdue-data)
2719 (setq *org-drill-overdue-entry-count*
2720 (length *org-drill-overdue-entries*))))
2721 (setq *org-drill-due-entry-count* (org-drill-pending-entry-count))
2722 (cond
2723 ((and (null *org-drill-current-item*)
2724 (null *org-drill-new-entries*)
2725 (null *org-drill-failed-entries*)
2726 (null *org-drill-overdue-entries*)
2727 (null *org-drill-young-mature-entries*)
2728 (null *org-drill-old-mature-entries*))
2729 (message "I did not find any pending drill items."))
2731 (org-drill-entries resume-p)
2732 (message "Drill session finished!"))))
2733 (progn
2734 (unless end-pos
2735 (setq *org-drill-cram-mode* nil)
2736 (org-drill-free-markers *org-drill-done-entries*)))))
2737 (cond
2738 (end-pos
2739 (when (markerp end-pos)
2740 (org-drill-goto-entry end-pos)
2741 (org-reveal)
2742 (org-show-entry))
2743 (let ((keystr (command-keybinding-to-string 'org-drill-resume)))
2744 (message
2745 "You can continue the drill session with the command `org-drill-resume'.%s"
2746 (if keystr (format "\nYou can run this command by pressing %s." keystr)
2747 ""))))
2749 (org-drill-final-report)
2750 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
2751 (org-drill-save-optimal-factor-matrix))
2752 (if org-drill-save-buffers-after-drill-sessions-p
2753 (save-some-buffers))
2754 (message "Drill session finished!")
2755 ))))
2758 (defun org-drill-save-optimal-factor-matrix ()
2759 (savehist-autosave))
2762 (defun org-drill-cram (&optional scope drill-match)
2763 "Run an interactive drill session in 'cram mode'. In cram mode,
2764 all drill items are considered to be due for review, unless they
2765 have been reviewed within the last `org-drill-cram-hours'
2766 hours."
2767 (interactive)
2768 (setq *org-drill-cram-mode* t)
2769 (org-drill scope drill-match))
2772 (defun org-drill-tree ()
2773 "Run an interactive drill session using drill items within the
2774 subtree at point."
2775 (interactive)
2776 (org-drill 'tree))
2779 (defun org-drill-directory ()
2780 "Run an interactive drill session using drill items from all org
2781 files in the same directory as the current file."
2782 (interactive)
2783 (org-drill 'directory))
2786 (defun org-drill-again (&optional scope drill-match)
2787 "Run a new drill session, but try to use leftover due items that
2788 were not reviewed during the last session, rather than scanning for
2789 unreviewed items. If there are no leftover items in memory, a full
2790 scan will be performed."
2791 (interactive)
2792 (setq *org-drill-cram-mode* nil)
2793 (cond
2794 ((plusp (org-drill-pending-entry-count))
2795 (org-drill-free-markers *org-drill-done-entries*)
2796 (if (markerp *org-drill-current-item*)
2797 (free-marker *org-drill-current-item*))
2798 (setq *org-drill-start-time* (float-time (current-time))
2799 *org-drill-done-entries* nil
2800 *org-drill-current-item* nil)
2801 (org-drill scope drill-match t))
2803 (org-drill scope drill-match))))
2807 (defun org-drill-resume ()
2808 "Resume a suspended drill session. Sessions are suspended by
2809 exiting them with the `edit' or `quit' options."
2810 (interactive)
2811 (cond
2812 ((org-drill-entries-pending-p)
2813 (org-drill nil nil t))
2814 ((and (plusp (org-drill-pending-entry-count))
2815 ;; Current drill session is finished, but there are still
2816 ;; more items which need to be reviewed.
2817 (y-or-n-p (format
2818 "You have finished the drill session. However, %d items still
2819 need reviewing. Start a new drill session? "
2820 (org-drill-pending-entry-count))))
2821 (org-drill-again))
2823 (message "You have finished the drill session."))))
2826 (defun org-drill-relearn-item ()
2827 "Make the current item due for revision, and set its last interval to 0.
2828 Makes the item behave as if it has been failed, without actually recording a
2829 failure. This command can be used to 'reset' repetitions for an item."
2830 (interactive)
2831 (org-drill-smart-reschedule 4 0))
2834 (defun org-drill-strip-entry-data ()
2835 (dolist (prop org-drill-scheduling-properties)
2836 (org-delete-property prop))
2837 (org-schedule '(4)))
2840 (defun org-drill-strip-all-data (&optional scope)
2841 "Delete scheduling data from every drill entry in scope. This
2842 function may be useful if you want to give your collection of
2843 entries to someone else. Scope defaults to the current buffer,
2844 and is specified by the argument SCOPE, which accepts the same
2845 values as `org-drill-scope'."
2846 (interactive)
2847 (when (yes-or-no-p
2848 "Delete scheduling data from ALL items in scope: are you sure?")
2849 (cond
2850 ((null scope)
2851 ;; Scope is the current buffer. This means we can use
2852 ;; `org-delete-property-globally', which is faster.
2853 (dolist (prop org-drill-scheduling-properties)
2854 (org-delete-property-globally prop))
2855 (org-map-drill-entries (lambda () (org-schedule '(4))) scope))
2857 (org-map-drill-entries 'org-drill-strip-entry-data scope)))
2858 (message "Done.")))
2861 (defun org-drill-add-cloze-fontification ()
2862 ;; Compute local versions of the regexp for cloze deletions, in case
2863 ;; the left and right delimiters are redefined locally.
2864 (setq-local org-drill-cloze-regexp (org-drill--compute-cloze-regexp))
2865 (setq-local org-drill-cloze-keywords (org-drill--compute-cloze-keywords))
2866 (when org-drill-use-visible-cloze-face-p
2867 (add-to-list 'org-font-lock-extra-keywords
2868 (first org-drill-cloze-keywords))))
2871 ;; Can't add to org-mode-hook, because local variables won't have been loaded
2872 ;; yet.
2874 ;; (defun org-drill-add-cloze-fontification ()
2875 ;; (when (eql major-mode 'org-mode)
2876 ;; ;; Compute local versions of the regexp for cloze deletions, in case
2877 ;; ;; the left and right delimiters are redefined locally.
2878 ;; (setq-local org-drill-cloze-regexp (org-drill--compute-cloze-regexp))
2879 ;; (setq-local org-drill-cloze-keywords (org-drill--compute-cloze-keywords))
2880 ;; (when org-drill-use-visible-cloze-face-p
2881 ;; (font-lock-add-keywords nil ;'org-mode
2882 ;; org-drill-cloze-keywords
2883 ;; nil))))
2885 ;; XXX
2886 ;; (add-hook 'hack-local-variables-hook
2887 ;; 'org-drill-add-cloze-fontification)
2889 ;; (org-drill-add-cloze-fontification)
2892 ;;; Synching card collections =================================================
2895 (defvar *org-drill-dest-id-table* (make-hash-table :test 'equal))
2898 (defun org-drill-copy-entry-to-other-buffer (dest &optional path)
2899 "Copy the subtree at point to the buffer DEST. The copy will receive
2900 the tag 'imported'."
2901 (block org-drill-copy-entry-to-other-buffer
2902 (save-excursion
2903 (let ((src (current-buffer))
2904 (m nil))
2905 (cl-flet ((paste-tree-here (&optional level)
2906 (org-paste-subtree level)
2907 (org-drill-strip-entry-data)
2908 (org-toggle-tag "imported" 'on)
2909 (org-map-drill-entries
2910 (lambda ()
2911 (let ((id (org-id-get)))
2912 (org-drill-strip-entry-data)
2913 (unless (gethash id *org-drill-dest-id-table*)
2914 (puthash id (point-marker)
2915 *org-drill-dest-id-table*))))
2916 'tree)))
2917 (unless path
2918 (setq path (org-get-outline-path)))
2919 (org-copy-subtree)
2920 (switch-to-buffer dest)
2921 (setq m
2922 (condition-case nil
2923 (org-find-olp path t)
2924 (error ; path does not exist in DEST
2925 (return-from org-drill-copy-entry-to-other-buffer
2926 (cond
2927 ((cdr path)
2928 (org-drill-copy-entry-to-other-buffer
2929 dest (butlast path)))
2931 ;; We've looked all the way up the path
2932 ;; Default to appending to the end of DEST
2933 (goto-char (point-max))
2934 (newline)
2935 (paste-tree-here)))))))
2936 (goto-char m)
2937 (outline-next-heading)
2938 (newline)
2939 (forward-line -1)
2940 (paste-tree-here (1+ (or (org-current-level) 0)))
2941 )))))
2945 (defun org-drill-merge-buffers (src &optional dest ignore-new-items-p)
2946 "SRC and DEST are two org mode buffers containing drill items.
2947 For each drill item in DEST that shares an ID with an item in SRC,
2948 overwrite scheduling data in DEST with data taken from the item in SRC.
2949 This is intended for use when two people are sharing a set of drill items,
2950 one person has made some updates to the item set, and the other person
2951 wants to migrate to the updated set without losing their scheduling data.
2953 By default, any drill items in SRC which do not exist in DEST are
2954 copied into DEST. We attempt to place the copied item in the
2955 equivalent location in DEST to its location in SRC, by matching
2956 the heading hierarchy. However if IGNORE-NEW-ITEMS-P is non-nil,
2957 we simply ignore any items that do not exist in DEST, and do not
2958 copy them across."
2959 (interactive "bImport scheduling info from which buffer?")
2960 (unless dest
2961 (setq dest (current-buffer)))
2962 (setq src (get-buffer src)
2963 dest (get-buffer dest))
2964 (when (yes-or-no-p
2965 (format
2966 (concat "About to overwrite all scheduling data for drill items in `%s' "
2967 "with information taken from matching items in `%s'. Proceed? ")
2968 (buffer-name dest) (buffer-name src)))
2969 ;; Compile list of all IDs in the destination buffer.
2970 (clrhash *org-drill-dest-id-table*)
2971 (with-current-buffer dest
2972 (org-map-drill-entries
2973 (lambda ()
2974 (let ((this-id (org-id-get)))
2975 (when this-id
2976 (puthash this-id (point-marker) *org-drill-dest-id-table*))))
2977 'file))
2978 ;; Look through all entries in source buffer.
2979 (with-current-buffer src
2980 (org-map-drill-entries
2981 (lambda ()
2982 (let ((id (org-id-get))
2983 (last-quality nil) (last-reviewed nil)
2984 (scheduled-time nil))
2985 (cond
2986 ((or (null id)
2987 (not (org-drill-entry-p)))
2988 nil)
2989 ((gethash id *org-drill-dest-id-table*)
2990 ;; This entry matches an entry in dest. Retrieve all its
2991 ;; scheduling data, then go to the matching location in dest
2992 ;; and write the data.
2993 (let ((marker (gethash id *org-drill-dest-id-table*)))
2994 (destructuring-bind (last-interval repetitions failures
2995 total-repeats meanq ease)
2996 (org-drill-get-item-data)
2997 (setq last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")
2998 last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")
2999 scheduled-time (org-get-scheduled-time (point)))
3000 (save-excursion
3001 ;; go to matching entry in destination buffer
3002 (switch-to-buffer (marker-buffer marker))
3003 (goto-char marker)
3004 (org-drill-strip-entry-data)
3005 (unless (zerop total-repeats)
3006 (org-drill-store-item-data last-interval repetitions failures
3007 total-repeats meanq ease)
3008 (if last-quality
3009 (org-set-property "LAST_QUALITY" last-quality)
3010 (org-delete-property "LAST_QUALITY"))
3011 (if last-reviewed
3012 (org-set-property "LAST_REVIEWED" last-reviewed)
3013 (org-delete-property "LAST_REVIEWED"))
3014 (if scheduled-time
3015 (org-schedule nil scheduled-time)))))
3016 (remhash id *org-drill-dest-id-table*)
3017 (free-marker marker)))
3019 ;; item in SRC has ID, but no matching ID in DEST.
3020 ;; It must be a new item that does not exist in DEST.
3021 ;; Copy the entire item to the *end* of DEST.
3022 (unless ignore-new-items-p
3023 (org-drill-copy-entry-to-other-buffer dest))))))
3024 'file))
3025 ;; Finally: there may be some items in DEST which are not in SRC, and
3026 ;; which have been scheduled by another user of DEST. Clear out the
3027 ;; scheduling info from all the unmatched items in DEST.
3028 (with-current-buffer dest
3029 (maphash (lambda (id m)
3030 (goto-char m)
3031 (org-drill-strip-entry-data)
3032 (free-marker m))
3033 *org-drill-dest-id-table*))))
3037 ;;; Card types for learning languages =========================================
3039 ;;; Get spell-number.el from:
3040 ;;; http://www.emacswiki.org/emacs/spell-number.el
3041 (autoload 'spelln-integer-in-words "spell-number")
3044 ;;; `conjugate' card type =====================================================
3045 ;;; See spanish.org for usage
3047 (defvar org-drill-verb-tense-alist
3048 '(("present" "tomato")
3049 ("simple present" "tomato")
3050 ("present indicative" "tomato")
3051 ;; past tenses
3052 ("past" "purple")
3053 ("simple past" "purple")
3054 ("preterite" "purple")
3055 ("imperfect" "darkturquoise")
3056 ("present perfect" "royalblue")
3057 ;; future tenses
3058 ("future" "green")
3059 ;; moods (backgrounds).
3060 ("indicative" nil) ; default
3061 ("subjunctive" "medium blue")
3062 ("conditional" "grey30")
3063 ("negative imperative" "red4")
3064 ("positive imperative" "darkgreen")
3066 "Alist where each entry has the form (TENSE COLOUR), where
3067 TENSE is a string naming a tense in which verbs can be
3068 conjugated, and COLOUR is a string specifying a foreground colour
3069 which will be used by `org-drill-present-verb-conjugation' and
3070 `org-drill-show-answer-verb-conjugation' to fontify the verb and
3071 the name of the tense.")
3074 (defun org-drill-get-verb-conjugation-info ()
3075 "Auxiliary function used by `org-drill-present-verb-conjugation' and
3076 `org-drill-show-answer-verb-conjugation'."
3077 (let ((infinitive (org-entry-get (point) "VERB_INFINITIVE" t))
3078 (inf-hint (org-entry-get (point) "VERB_INFINITIVE_HINT" t))
3079 (translation (org-entry-get (point) "VERB_TRANSLATION" t))
3080 (tense (org-entry-get (point) "VERB_TENSE" nil))
3081 (mood (org-entry-get (point) "VERB_MOOD" nil))
3082 (highlight-face nil))
3083 (unless (and infinitive translation (or tense mood))
3084 (error "Missing information for verb conjugation card (%s, %s, %s, %s) at %s"
3085 infinitive translation tense mood (point)))
3086 (setq tense (if tense (downcase (car (read-from-string tense))))
3087 mood (if mood (downcase (car (read-from-string mood))))
3088 infinitive (car (read-from-string infinitive))
3089 inf-hint (if inf-hint (car (read-from-string inf-hint)))
3090 translation (car (read-from-string translation)))
3091 (setq highlight-face
3092 (list :foreground
3093 (or (second (assoc-string tense org-drill-verb-tense-alist t))
3094 "hotpink")
3095 :background
3096 (second (assoc-string mood org-drill-verb-tense-alist t))))
3097 (setq infinitive (propertize infinitive 'face highlight-face))
3098 (setq translation (propertize translation 'face highlight-face))
3099 (if tense (setq tense (propertize tense 'face highlight-face)))
3100 (if mood (setq mood (propertize mood 'face highlight-face)))
3101 (list infinitive inf-hint translation tense mood)))
3104 (defun org-drill-present-verb-conjugation ()
3105 "Present a drill entry whose card type is 'conjugate'."
3106 (cl-flet ((tense-and-mood-to-string
3107 (tense mood)
3108 (cond
3109 ((and tense mood)
3110 (format "%s tense, %s mood" tense mood))
3111 (tense
3112 (format "%s tense" tense))
3113 (mood
3114 (format "%s mood" mood)))))
3115 (destructuring-bind (infinitive inf-hint translation tense mood)
3116 (org-drill-get-verb-conjugation-info)
3117 (org-drill-present-card-using-text
3118 (cond
3119 ((zerop (random* 2))
3120 (format "\nTranslate the verb\n\n%s\n\nand conjugate for the %s.\n\n"
3121 infinitive (tense-and-mood-to-string tense mood)))
3124 (format "\nGive the verb that means\n\n%s %s\n
3125 and conjugate for the %s.\n\n"
3126 translation
3127 (if inf-hint (format " [HINT: %s]" inf-hint) "")
3128 (tense-and-mood-to-string tense mood))))))))
3131 (defun org-drill-show-answer-verb-conjugation (reschedule-fn)
3132 "Show the answer for a drill item whose card type is 'conjugate'.
3133 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
3134 returns its return value."
3135 (destructuring-bind (infinitive inf-hint translation tense mood)
3136 (org-drill-get-verb-conjugation-info)
3137 (with-replaced-entry-heading
3138 (format "%s of %s ==> %s\n\n"
3139 (capitalize
3140 (cond
3141 ((and tense mood)
3142 (format "%s tense, %s mood" tense mood))
3143 (tense
3144 (format "%s tense" tense))
3145 (mood
3146 (format "%s mood" mood))))
3147 infinitive translation)
3148 (org-cycle-hide-drawers 'all)
3149 (funcall reschedule-fn))))
3152 ;;; `decline_noun' card type ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
3155 (defvar org-drill-noun-gender-alist
3156 '(("masculine" "dodgerblue")
3157 ("masc" "dodgerblue")
3158 ("male" "dodgerblue")
3159 ("m" "dodgerblue")
3160 ("feminine" "orchid")
3161 ("fem" "orchid")
3162 ("female" "orchid")
3163 ("f" "orchid")
3164 ("neuter" "green")
3165 ("neutral" "green")
3166 ("neut" "green")
3167 ("n" "green")
3171 (defun org-drill-get-noun-info ()
3172 "Auxiliary function used by `org-drill-present-noun-declension' and
3173 `org-drill-show-answer-noun-declension'."
3174 (let ((noun (org-entry-get (point) "NOUN" t))
3175 (noun-hint (org-entry-get (point) "NOUN_HINT" t))
3176 (noun-root (org-entry-get (point) "NOUN_ROOT" t))
3177 (noun-gender (org-entry-get (point) "NOUN_GENDER" t))
3178 (translation (org-entry-get (point) "NOUN_TRANSLATION" t))
3179 (highlight-face nil))
3180 (unless (and noun translation)
3181 (error "Missing information for `decline_noun' card (%s, %s, %s, %s) at %s"
3182 noun translation noun-hint noun-root (point)))
3183 (setq noun-root (if noun-root (car (read-from-string noun-root)))
3184 noun (car (read-from-string noun))
3185 noun-gender (downcase (car (read-from-string noun-gender)))
3186 noun-hint (if noun-hint (car (read-from-string noun-hint)))
3187 translation (car (read-from-string translation)))
3188 (setq highlight-face
3189 (list :foreground
3190 (or (second (assoc-string noun-gender
3191 org-drill-noun-gender-alist t))
3192 "red")))
3193 (setq noun (propertize noun 'face highlight-face))
3194 (setq translation (propertize translation 'face highlight-face))
3195 (list noun noun-root noun-gender noun-hint translation)))
3198 (defun org-drill-present-noun-declension ()
3199 "Present a drill entry whose card type is 'decline_noun'."
3200 (destructuring-bind (noun noun-root noun-gender noun-hint translation)
3201 (org-drill-get-noun-info)
3202 (let* ((props (org-entry-properties (point)))
3203 (definite
3204 (cond
3205 ((assoc "DECLINE_DEFINITE" props)
3206 (propertize (if (org-entry-get (point) "DECLINE_DEFINITE")
3207 "definite" "indefinite")
3208 'face 'warning))
3209 (t nil)))
3210 (plural
3211 (cond
3212 ((assoc "DECLINE_PLURAL" props)
3213 (propertize (if (org-entry-get (point) "DECLINE_PLURAL")
3214 "plural" "singular")
3215 'face 'warning))
3216 (t nil))))
3217 (org-drill-present-card-using-text
3218 (cond
3219 ((zerop (random* 2))
3220 (format "\nTranslate the noun\n\n%s (%s)\n\nand list its declensions%s.\n\n"
3221 noun noun-gender
3222 (if (or plural definite)
3223 (format " for the %s %s form" definite plural)
3224 "")))
3226 (format "\nGive the noun that means\n\n%s %s\n
3227 and list its declensions%s.\n\n"
3228 translation
3229 (if noun-hint (format " [HINT: %s]" noun-hint) "")
3230 (if (or plural definite)
3231 (format " for the %s %s form" definite plural)
3232 ""))))))))
3235 (defun org-drill-show-answer-noun-declension (reschedule-fn)
3236 "Show the answer for a drill item whose card type is 'decline_noun'.
3237 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
3238 returns its return value."
3239 (destructuring-bind (noun noun-root noun-gender noun-hint translation)
3240 (org-drill-get-noun-info)
3241 (with-replaced-entry-heading
3242 (format "Declensions of %s (%s) ==> %s\n\n"
3243 noun noun-gender translation)
3244 (org-cycle-hide-drawers 'all)
3245 (funcall reschedule-fn))))
3248 ;;; `translate_number' card type ==============================================
3249 ;;; See spanish.org for usage
3252 (defun spelln-integer-in-language (n lang)
3253 (let ((spelln-language lang))
3254 (spelln-integer-in-words n)))
3256 (defun org-drill-present-translate-number ()
3257 (let ((num-min (read (org-entry-get (point) "DRILL_NUMBER_MIN")))
3258 (num-max (read (org-entry-get (point) "DRILL_NUMBER_MAX")))
3259 (language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
3260 (drilled-number 0)
3261 (drilled-number-direction 'to-english)
3262 (highlight-face 'font-lock-warning-face))
3263 (cond
3264 ((not (fboundp 'spelln-integer-in-words))
3265 (message "`spell-number.el' not loaded, skipping 'translate_number' card...")
3266 (sit-for 0.5)
3267 'skip)
3268 ((not (and (numberp num-min) (numberp num-max) language))
3269 (error "Missing language or minimum or maximum numbers for number card"))
3271 (if (> num-min num-max)
3272 (psetf num-min num-max
3273 num-max num-min))
3274 (setq drilled-number
3275 (+ num-min (random* (abs (1+ (- num-max num-min))))))
3276 (setq drilled-number-direction
3277 (if (zerop (random* 2)) 'from-english 'to-english))
3278 (cond
3279 ((eql 'to-english drilled-number-direction)
3280 (org-drill-present-card-using-text
3281 (format "\nTranslate into English:\n\n%s\n"
3282 (propertize
3283 (spelln-integer-in-language drilled-number language)
3284 'face highlight-face))
3285 (spelln-integer-in-language drilled-number 'english-gb)))
3287 (org-drill-present-card-using-text
3288 (format "\nTranslate into %s:\n\n%s\n"
3289 (capitalize (format "%s" language))
3290 (propertize
3291 (spelln-integer-in-language drilled-number 'english-gb)
3292 'face highlight-face))
3293 (spelln-integer-in-language drilled-number language))))))))
3296 ;; (defun org-drill-show-answer-translate-number (reschedule-fn)
3297 ;; (let* ((language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
3298 ;; (highlight-face 'font-lock-warning-face)
3299 ;; (non-english
3300 ;; (let ((spelln-language language))
3301 ;; (propertize (spelln-integer-in-words *drilled-number*)
3302 ;; 'face highlight-face)))
3303 ;; (english
3304 ;; (let ((spelln-language 'english-gb))
3305 ;; (propertize (spelln-integer-in-words *drilled-number*)
3306 ;; 'face 'highlight-face))))
3307 ;; (with-replaced-entry-text
3308 ;; (cond
3309 ;; ((eql 'to-english *drilled-number-direction*)
3310 ;; (format "\nThe English translation of %s is:\n\n%s\n"
3311 ;; non-english english))
3312 ;; (t
3313 ;; (format "\nThe %s translation of %s is:\n\n%s\n"
3314 ;; (capitalize (format "%s" language))
3315 ;; english non-english)))
3316 ;; (funcall reschedule-fn))))
3319 ;;; `spanish_verb' card type ==================================================
3320 ;;; Not very interesting, but included to demonstrate how a presentation
3321 ;;; function can manipulate which subheading are hidden versus shown.
3324 (defun org-drill-present-spanish-verb ()
3325 (let ((prompt nil)
3326 (reveal-headings nil))
3327 (with-hidden-comments
3328 (with-hidden-cloze-hints
3329 (with-hidden-cloze-text
3330 (case (random* 6)
3332 (org-drill-hide-all-subheadings-except '("Infinitive"))
3333 (setq prompt
3334 (concat "Translate this Spanish verb, and conjugate it "
3335 "for the *present* tense.")
3336 reveal-headings '("English" "Present Tense" "Notes")))
3338 (org-drill-hide-all-subheadings-except '("English"))
3339 (setq prompt (concat "For the *present* tense, conjugate the "
3340 "Spanish translation of this English verb.")
3341 reveal-headings '("Infinitive" "Present Tense" "Notes")))
3343 (org-drill-hide-all-subheadings-except '("Infinitive"))
3344 (setq prompt (concat "Translate this Spanish verb, and "
3345 "conjugate it for the *past* tense.")
3346 reveal-headings '("English" "Past Tense" "Notes")))
3348 (org-drill-hide-all-subheadings-except '("English"))
3349 (setq prompt (concat "For the *past* tense, conjugate the "
3350 "Spanish translation of this English verb.")
3351 reveal-headings '("Infinitive" "Past Tense" "Notes")))
3353 (org-drill-hide-all-subheadings-except '("Infinitive"))
3354 (setq prompt (concat "Translate this Spanish verb, and "
3355 "conjugate it for the *future perfect* tense.")
3356 reveal-headings '("English" "Future Perfect Tense" "Notes")))
3358 (org-drill-hide-all-subheadings-except '("English"))
3359 (setq prompt (concat "For the *future perfect* tense, conjugate the "
3360 "Spanish translation of this English verb.")
3361 reveal-headings '("Infinitive" "Future Perfect Tense" "Notes"))))
3362 (org-cycle-hide-drawers 'all)
3363 (prog1 (org-drill-presentation-prompt)
3364 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
3367 (provide 'org-drill)