aboutsummaryrefslogtreecommitdiff
path: root/org-drill.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-27 20:47:35 -0500
committerCraig Jennings <c@cjennings.net>2026-05-27 20:47:35 -0500
commit39d01b75ac679410821ce5e16c09ec6b7799f791 (patch)
treed825389532e98a9bcfc3fddd87b787c7879c5dcc /org-drill.el
parent198e772efbb17cc8dbd514b0c0487d780c5e3eaa (diff)
downloadorg-drill-39d01b75ac679410821ce5e16c09ec6b7799f791.tar.gz
org-drill-39d01b75ac679410821ce5e16c09ec6b7799f791.zip
refactor: thread card-state struct through the item-data round-trip
Second step of #147. get-item-data now returns an org-drill-card-state and store-item-data takes one, so the six recall fields move as named slots instead of a positional list. The three call sites (smart-reschedule, hypothetical-next-review-date, copy-scheduling-to-marker) read scheduler inputs through accessors and build a struct for the store, which removes the hand re-ordering between the get-shape and the store-shape. Behavior is unchanged. The legacy LEARN_DATA read path and the virgin-item sentinel are preserved field-for-field, and store takes just the struct because its last-interval slot already holds the interval to persist. The schedulers still take positional args; they adopt the struct in the following commits. I updated the round-trip, integration, and setup-helper tests to build and read the struct via a small list-view helper, so the existing expected-value assertions stay readable.
Diffstat (limited to 'org-drill.el')
-rw-r--r--org-drill.el222
1 files changed, 129 insertions, 93 deletions
diff --git a/org-drill.el b/org-drill.el
index 270a19f..9e38666 100644
--- a/org-drill.el
+++ b/org-drill.el
@@ -1145,64 +1145,77 @@ Returns the parsed list or nil if invalid or unsafe."
last-interval repetitions ease failures meanq total-repeats)
(defun org-drill-get-item-data ()
- "Return alist of 6 items, containing all the stored recall
- data for the item at point:
+ "Return an `org-drill-card-state' with the stored recall data for the item at point.
+
+Slots:
- LAST-INTERVAL is the interval in days that was used to schedule the item's
current review date.
-- REPEATS is the number of items the item has been successfully recalled without
+- REPETITIONS is the number of times the item has been successfully recalled
without any failures. It is reset to 0 upon failure to recall the item.
- FAILURES is the total number of times the user has failed to recall the item.
- TOTAL-REPEATS includes both successful and unsuccessful repetitions.
-- AVERAGE-QUALITY is the mean quality of recall of the item over
- all its repetitions, successful and unsuccessful.
-- EASE is a number reflecting how easy the item is to learn. Higher is easier.
-"
+- MEANQ is the mean quality of recall of the item over all its repetitions,
+ successful and unsuccessful.
+- EASE is a number reflecting how easy the item is to learn. Higher is easier."
(let ((learn-str (org-entry-get (point) "LEARN_DATA"))
(repeats (org-drill-entry-total-repeats :missing)))
(cond
(learn-str
(let ((learn-data (org-drill--safe-read-learn-data learn-str)))
(if learn-data
- (list (nth 0 learn-data) ; last interval
- (nth 1 learn-data) ; repetitions
- (org-drill-entry-failure-count)
- (nth 1 learn-data)
- (org-drill-entry-last-quality)
- (nth 2 learn-data)) ; EF
+ (make-org-drill-card-state
+ :last-interval (nth 0 learn-data)
+ :repetitions (nth 1 learn-data)
+ :failures (org-drill-entry-failure-count)
+ :total-repeats (nth 1 learn-data)
+ :meanq (org-drill-entry-last-quality)
+ :ease (nth 2 learn-data)) ; EF
;; If LEARN_DATA is invalid/unsafe, fall through to next case
(if (not (eql :missing repeats))
- (list (org-drill-entry-last-interval)
- (org-drill-entry-repeats-since-fail)
- (org-drill-entry-failure-count)
- (org-drill-entry-total-repeats)
- (org-drill-entry-average-quality)
- (org-drill-entry-ease))
+ (make-org-drill-card-state
+ :last-interval (org-drill-entry-last-interval)
+ :repetitions (org-drill-entry-repeats-since-fail)
+ :failures (org-drill-entry-failure-count)
+ :total-repeats (org-drill-entry-total-repeats)
+ :meanq (org-drill-entry-average-quality)
+ :ease (org-drill-entry-ease))
;; Virgin item
- (list 0 0 0 0 nil nil)))))
+ (make-org-drill-card-state
+ :last-interval 0 :repetitions 0 :failures 0
+ :total-repeats 0 :meanq nil :ease nil)))))
((not (eql :missing repeats))
- (list (org-drill-entry-last-interval)
- (org-drill-entry-repeats-since-fail)
- (org-drill-entry-failure-count)
- (org-drill-entry-total-repeats)
- (org-drill-entry-average-quality)
- (org-drill-entry-ease)))
+ (make-org-drill-card-state
+ :last-interval (org-drill-entry-last-interval)
+ :repetitions (org-drill-entry-repeats-since-fail)
+ :failures (org-drill-entry-failure-count)
+ :total-repeats (org-drill-entry-total-repeats)
+ :meanq (org-drill-entry-average-quality)
+ :ease (org-drill-entry-ease)))
(t ; virgin item
- (list 0 0 0 0 nil nil)))))
-
-(defun org-drill-store-item-data (last-interval repeats failures
- total-repeats meanq
- ease)
- "Stores the given data in the item at point."
+ (make-org-drill-card-state
+ :last-interval 0 :repetitions 0 :failures 0
+ :total-repeats 0 :meanq nil :ease nil)))))
+
+(defun org-drill-store-item-data (state)
+ "Store the recall data in STATE, an `org-drill-card-state', into the item at point.
+STATE's LAST-INTERVAL slot holds the interval to schedule going forward (the
+caller passes the scheduler's next-interval there)."
(org-entry-delete (point) "LEARN_DATA")
(org-set-property "DRILL_LAST_INTERVAL"
- (number-to-string (org-drill-round-float last-interval 4)))
- (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats))
- (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats))
- (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures))
+ (number-to-string
+ (org-drill-round-float (org-drill-card-state-last-interval state) 4)))
+ (org-set-property "DRILL_REPEATS_SINCE_FAIL"
+ (number-to-string (org-drill-card-state-repetitions state)))
+ (org-set-property "DRILL_TOTAL_REPEATS"
+ (number-to-string (org-drill-card-state-total-repeats state)))
+ (org-set-property "DRILL_FAILURE_COUNT"
+ (number-to-string (org-drill-card-state-failures state)))
(org-set-property "DRILL_AVERAGE_QUALITY"
- (number-to-string (org-drill-round-float meanq 3)))
+ (number-to-string
+ (org-drill-round-float (org-drill-card-state-meanq state) 3)))
(org-set-property "DRILL_EASE"
- (number-to-string (org-drill-round-float ease 3))))
+ (number-to-string
+ (org-drill-round-float (org-drill-card-state-ease state) 3))))
;;; SM2 Algorithm =============================================================
(defun org-drill-determine-next-interval-sm2 (last-interval n ef quality
@@ -1481,23 +1494,33 @@ item will be scheduled exactly this many days into the future."
(weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
(if (stringp weight)
(setq weight (string-to-number weight)))
- (cl-destructuring-bind (last-interval repetitions failures
- total-repeats meanq ease)
- (org-drill-get-item-data)
+ (let ((state (org-drill-get-item-data)))
(cl-destructuring-bind (next-interval repetitions ease
failures meanq total-repeats
&optional new-ofmatrix)
(cl-case org-drill-spaced-repetition-algorithm
- (sm5 (org-drill-determine-next-interval-sm5 last-interval repetitions
- ease quality failures
- meanq total-repeats ofmatrix))
- (sm2 (org-drill-determine-next-interval-sm2 last-interval repetitions
- ease quality failures
- meanq total-repeats))
- (simple8 (org-drill-determine-next-interval-simple8 last-interval repetitions
- quality failures meanq
- total-repeats
- delta-days)))
+ (sm5 (org-drill-determine-next-interval-sm5
+ (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ (org-drill-card-state-ease state) quality
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-total-repeats state) ofmatrix))
+ (sm2 (org-drill-determine-next-interval-sm2
+ (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ (org-drill-card-state-ease state) quality
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-total-repeats state)))
+ (simple8 (org-drill-determine-next-interval-simple8
+ (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ quality
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-total-repeats state)
+ delta-days)))
(if (numberp days-ahead)
(setq next-interval days-ahead))
@@ -1505,11 +1528,15 @@ item will be scheduled exactly this many days into the future."
(numberp weight) (cl-plusp weight)
(not (cl-minusp next-interval)))
(setq next-interval
- (max 1.0 (+ last-interval
- (/ (- next-interval last-interval) weight)))))
+ (max 1.0 (+ (org-drill-card-state-last-interval state)
+ (/ (- next-interval
+ (org-drill-card-state-last-interval state))
+ weight)))))
- (org-drill-store-item-data next-interval repetitions failures
- total-repeats meanq ease)
+ (org-drill-store-item-data
+ (make-org-drill-card-state
+ :last-interval next-interval :repetitions repetitions :ease ease
+ :failures failures :meanq meanq :total-repeats total-repeats))
(if (eql 'sm5 org-drill-spaced-repetition-algorithm)
(setq org-drill-sm5-optimal-factor-matrix new-ofmatrix))
@@ -1533,34 +1560,46 @@ item will be scheduled exactly this many days into the future."
"Return aninteger representing the number of days into the future
that the current item would be scheduled, based on a recall quality
of QUALITY."
- (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
- (cl-destructuring-bind (last-interval repetitions failures
- total-repeats meanq ease)
- (org-drill-get-item-data)
+ (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT"))
+ (state (org-drill-get-item-data)))
(if (stringp weight)
(setq weight (string-to-number weight)))
(cl-destructuring-bind (next-interval _repetitions _ease
_failures _meanq _total-repeats
&optional _ofmatrix)
(cl-case org-drill-spaced-repetition-algorithm
- (sm5 (org-drill-determine-next-interval-sm5 last-interval repetitions
- ease quality failures
- meanq total-repeats
- org-drill-sm5-optimal-factor-matrix))
- (sm2 (org-drill-determine-next-interval-sm2 last-interval repetitions
- ease quality failures
- meanq total-repeats))
- (simple8 (org-drill-determine-next-interval-simple8 last-interval repetitions
- quality failures meanq
- total-repeats)))
+ (sm5 (org-drill-determine-next-interval-sm5
+ (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ (org-drill-card-state-ease state) quality
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-total-repeats state)
+ org-drill-sm5-optimal-factor-matrix))
+ (sm2 (org-drill-determine-next-interval-sm2
+ (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ (org-drill-card-state-ease state) quality
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-total-repeats state)))
+ (simple8 (org-drill-determine-next-interval-simple8
+ (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ quality
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-total-repeats state))))
(cond
((not (cl-plusp next-interval))
0)
((and (numberp weight) (cl-plusp weight))
- (+ last-interval
- (max 1.0 (/ (- next-interval last-interval) weight))))
+ (+ (org-drill-card-state-last-interval state)
+ (max 1.0 (/ (- next-interval
+ (org-drill-card-state-last-interval state))
+ weight))))
(t
- next-interval))))))
+ next-interval)))))
(defun org-drill-hypothetical-next-review-dates ()
"Return hypothetical next review dates."
@@ -3649,27 +3688,24 @@ the tag \\='imported\\='."
The destination's previous data is stripped first. Skips zero-
total-repeats items (i.e., never-rated cards) — those have no
scheduling information worth migrating."
- (cl-destructuring-bind (last-interval repetitions failures
- total-repeats meanq ease)
- (org-drill-get-item-data)
- (let ((last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED"))
- (last-quality (org-entry-get (point) "DRILL_LAST_QUALITY"))
- (scheduled-time (org-get-scheduled-time (point))))
- (with-current-buffer (marker-buffer marker)
- (save-excursion
- (goto-char marker)
- (org-drill-strip-entry-data)
- (unless (zerop total-repeats)
- (org-drill-store-item-data last-interval repetitions failures
- total-repeats meanq ease)
- (if last-quality
- (org-set-property "DRILL_LAST_QUALITY" last-quality)
- (org-delete-property "DRILL_LAST_QUALITY"))
- (if last-reviewed
- (org-set-property "DRILL_LAST_REVIEWED" last-reviewed)
- (org-delete-property "DRILL_LAST_REVIEWED"))
- (when scheduled-time
- (org-schedule nil scheduled-time))))))))
+ (let ((state (org-drill-get-item-data))
+ (last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED"))
+ (last-quality (org-entry-get (point) "DRILL_LAST_QUALITY"))
+ (scheduled-time (org-get-scheduled-time (point))))
+ (with-current-buffer (marker-buffer marker)
+ (save-excursion
+ (goto-char marker)
+ (org-drill-strip-entry-data)
+ (unless (zerop (org-drill-card-state-total-repeats state))
+ (org-drill-store-item-data state)
+ (if last-quality
+ (org-set-property "DRILL_LAST_QUALITY" last-quality)
+ (org-delete-property "DRILL_LAST_QUALITY"))
+ (if last-reviewed
+ (org-set-property "DRILL_LAST_REVIEWED" last-reviewed)
+ (org-delete-property "DRILL_LAST_REVIEWED"))
+ (when scheduled-time
+ (org-schedule nil scheduled-time)))))))
(defun org-drill--migrate-from-source (src dest ignore-new-items-p)
"Walk SRC's entries, migrating scheduling data into matching DEST entries.