aboutsummaryrefslogtreecommitdiff
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
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.
-rw-r--r--org-drill.el222
-rw-r--r--tests/test-integration-drill-session-simple-workflow-integration-test.el61
-rw-r--r--tests/test-org-drill-additional-coverage.el4
-rw-r--r--tests/test-org-drill-cloze-and-scheduling-helpers.el4
-rw-r--r--tests/test-org-drill-item-data-roundtrip.el52
-rw-r--r--tests/test-org-drill-orchestration-helpers.el4
-rw-r--r--tests/test-org-drill-prompt-and-misc.el4
-rw-r--r--tests/test-org-drill-queue-and-misc.el4
8 files changed, 216 insertions, 139 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.
diff --git a/tests/test-integration-drill-session-simple-workflow-integration-test.el b/tests/test-integration-drill-session-simple-workflow-integration-test.el
index 7cc72c3..e6f0aae 100644
--- a/tests/test-integration-drill-session-simple-workflow-integration-test.el
+++ b/tests/test-integration-drill-session-simple-workflow-integration-test.el
@@ -6,7 +6,7 @@
;;
;; 1. Entry detection (org-drill-entry-p)
;; 2. Entry enumeration (org-drill-map-entries)
-;; 3. Data retrieval (org-drill-get-item-data)
+;; 3. Data retrieval (test-int--state-as-list (org-drill-get-item-data))
;; 4. Scheduling algorithm (org-drill-determine-next-interval-sm2)
;; 5. Data storage (org-drill-store-item-data)
;;
@@ -19,6 +19,19 @@
(require 'assess)
(require 'org-drill)
+;;; Helpers
+
+(defun test-int--state-as-list (state)
+ "View STATE (an `org-drill-card-state') as the canonical field list
+(LAST-INTERVAL REPETITIONS FAILURES TOTAL-REPEATS MEANQ EASE), so existing
+list-style assertions in this file keep working after the struct refactor."
+ (list (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-total-repeats state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-ease state)))
+
;;; Test Data
(defconst test-integration-simple-workflow-basic-entries
@@ -132,7 +145,7 @@ Should correctly parse all DRILL_* properties."
;; Find first drill entry
(re-search-forward "^\\* First Card :drill:")
(beginning-of-line)
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
;; Verify structure: (last-interval repeats failures total-repeats meanq ease)
(should (listp data))
(should (= (length data) 6))
@@ -154,7 +167,7 @@ Should return default values for new items."
;; Find first drill entry
(re-search-forward "^\\* Brand New Card :drill:")
(beginning-of-line)
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
;; Verify structure exists
(should (listp data))
(should (= (length data) 6))
@@ -179,7 +192,7 @@ Verifies integration between data retrieval and SM2 algorithm."
(re-search-forward "^\\* Second Card :drill:")
(beginning-of-line)
(cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease)
- (org-drill-get-item-data)
+ (test-int--state-as-list (org-drill-get-item-data))
;; Simulate quality rating of 4 (good recall)
(let* ((quality 4)
(result (org-drill-determine-next-interval-sm2
@@ -204,7 +217,7 @@ Verifies that failure handling works correctly in integrated workflow."
(re-search-forward "^\\* Third Card :drill:")
(beginning-of-line)
(cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease)
- (org-drill-get-item-data)
+ (test-int--state-as-list (org-drill-get-item-data))
;; Simulate complete failure (quality 0)
(let* ((quality 0)
(result (org-drill-determine-next-interval-sm2
@@ -232,7 +245,7 @@ Verifies org-drill-store-item-data updates properties correctly."
;; Get original data
(cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease)
- (org-drill-get-item-data)
+ (test-int--state-as-list (org-drill-get-item-data))
;; Calculate new scheduling data
(let* ((quality 5) ; Perfect recall
@@ -247,8 +260,11 @@ Verifies org-drill-store-item-data updates properties correctly."
(new-total (nth 5 result)))
;; Store new data
- (org-drill-store-item-data next-interval new-repeats new-failures
- new-total new-meanq new-ease)
+ (org-drill-store-item-data
+ (make-org-drill-card-state
+ :last-interval next-interval :repetitions new-repeats
+ :failures new-failures :total-repeats new-total
+ :meanq new-meanq :ease new-ease))
;; Verify data was stored (properties exist and are valid)
(should (org-entry-get (point) "DRILL_LAST_INTERVAL"))
@@ -317,7 +333,7 @@ Simulates reviewing a card and verifies all components work together."
;; Step 3: Retrieve current data
(cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease)
- (org-drill-get-item-data)
+ (test-int--state-as-list (org-drill-get-item-data))
(should (numberp last-interval))
(should (numberp repeats))
@@ -334,11 +350,14 @@ Simulates reviewing a card and verifies all components work together."
(new-total (nth 5 result)))
;; Step 5: Store results
- (org-drill-store-item-data next-interval new-repeats new-failures
- new-total new-meanq new-ease)
+ (org-drill-store-item-data
+ (make-org-drill-card-state
+ :last-interval next-interval :repetitions new-repeats
+ :failures new-failures :total-repeats new-total
+ :meanq new-meanq :ease new-ease))
;; Step 6: Verify data persisted
- (let ((retrieved-data (org-drill-get-item-data)))
+ (let ((retrieved-data (test-int--state-as-list (org-drill-get-item-data))))
(should (= (nth 0 retrieved-data) (floor next-interval)))
(should (= (nth 1 retrieved-data) new-repeats))
(should (= (nth 2 retrieved-data) new-failures))
@@ -367,7 +386,7 @@ Question content.
(re-search-forward "^\\* Corrupted Card :drill:")
(beginning-of-line)
;; Should not error when retrieving data with invalid values
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
(should (listp data))
;; Verify function handles corruption gracefully
(should (= (length data) 6)))))))
@@ -385,7 +404,7 @@ Question: Test question?
(re-search-forward "^\\* Card Without Properties :drill:")
(beginning-of-line)
;; Should not error, provide defaults
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
(should (listp data))
(should (= (length data) 6)))))))
@@ -409,7 +428,7 @@ Question content.
(lambda ()
(re-search-forward "^\\* Negative Values Card :drill:")
(beginning-of-line)
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
(should (listp data))
;; Verify system doesn't crash with negative values
(should (= (length data) 6)))))))
@@ -434,7 +453,7 @@ Question content.
(lambda ()
(re-search-forward "^\\* Large Values Card :drill:")
(beginning-of-line)
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
(should (listp data))
(should (= (length data) 6))
;; Verify scheduling can handle extreme values
@@ -457,7 +476,7 @@ Should handle gracefully without crashing."
(re-search-forward "Question: What is 2\\+2\\?")
(beginning-of-line)
;; Should not crash, may return nil or defaults
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
;; Just verify it doesn't crash
(should (or (null data) (listp data)))))))
@@ -472,7 +491,7 @@ Should handle non-drill headings gracefully."
(beginning-of-line)
(should-not (org-drill-entry-p))
;; Retrieving data from non-drill entry
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
;; Should not crash
(should (or (null data) (listp data)))))))
@@ -545,7 +564,7 @@ Question with Unicode: 日本語 Café.
(re-search-forward "^\\* Unicode Property Card :drill:")
(beginning-of-line)
;; Should handle Unicode in content without issues
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
(should (listp data))
(should (= (length data) 6)))))))
@@ -581,7 +600,7 @@ Question.
(lambda ()
(re-search-forward "^\\* Empty Drawer Card :drill:")
(beginning-of-line)
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
;; Should provide defaults for missing properties
(should (listp data))
(should (= (length data) 6)))))))
@@ -606,7 +625,7 @@ Question.
(lambda ()
(re-search-forward "^\\* Float Values Card :drill:")
(beginning-of-line)
- (let ((data (org-drill-get-item-data)))
+ (let ((data (test-int--state-as-list (org-drill-get-item-data))))
(should (listp data))
;; Verify system handles float->int conversion
(should (numberp (nth 0 data)))
diff --git a/tests/test-org-drill-additional-coverage.el b/tests/test-org-drill-additional-coverage.el
index f8f68dd..6d97614 100644
--- a/tests/test-org-drill-additional-coverage.el
+++ b/tests/test-org-drill-additional-coverage.el
@@ -137,7 +137,7 @@ new-entries."
"DRILL_CARD_WEIGHT > 1 stretches the next-interval delta — the resulting
SCHEDULED date is closer to today than without weight."
(with-fresh-drill-entry
- (org-drill-store-item-data 10 3 0 3 4.5 2.5)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 0 :total-repeats 3 :meanq 4.5 :ease 2.5))
(with-fixed-now
(let ((sched-no-weight nil)
(sched-with-weight nil))
@@ -145,7 +145,7 @@ SCHEDULED date is closer to today than without weight."
(org-drill-smart-reschedule 5)
(setq sched-no-weight (org-entry-get (point) "SCHEDULED"))
;; Reset and reschedule with weight=2.
- (org-drill-store-item-data 10 3 0 3 4.5 2.5)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 0 :total-repeats 3 :meanq 4.5 :ease 2.5))
(org-set-property "DRILL_CARD_WEIGHT" "2")
(org-drill-smart-reschedule 5)
(setq sched-with-weight (org-entry-get (point) "SCHEDULED"))
diff --git a/tests/test-org-drill-cloze-and-scheduling-helpers.el b/tests/test-org-drill-cloze-and-scheduling-helpers.el
index 1ee4c61..39573aa 100644
--- a/tests/test-org-drill-cloze-and-scheduling-helpers.el
+++ b/tests/test-org-drill-cloze-and-scheduling-helpers.el
@@ -148,7 +148,7 @@ roughly old + max(1, (next-old)/2) days."
(with-fresh-drill-entry
;; Set up a card that's been reviewed before, with weight = 2.
(org-set-property "DRILL_CARD_WEIGHT" "2")
- (org-drill-store-item-data 10 3 0 3 4.5 2.5)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 0 :total-repeats 3 :meanq 4.5 :ease 2.5))
(let ((q5-no-weight (progn
(org-delete-property "DRILL_CARD_WEIGHT")
(org-drill-hypothetical-next-review-date 5)))
@@ -180,7 +180,7 @@ This is what users see in the prompt: `1 / 2 / 3 / 4 / 6 / 9' style hints."
(ert-deftest test-org-drill-strip-entry-data-removes-scheduling-properties ()
"Stripping wipes every property listed in `org-drill-scheduling-properties'."
(with-fresh-drill-entry
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
;; sanity: the props are there
(should (org-entry-get (point) "DRILL_LAST_INTERVAL"))
(org-drill-strip-entry-data)
diff --git a/tests/test-org-drill-item-data-roundtrip.el b/tests/test-org-drill-item-data-roundtrip.el
index 126c8ac..dc445a1 100644
--- a/tests/test-org-drill-item-data-roundtrip.el
+++ b/tests/test-org-drill-item-data-roundtrip.el
@@ -9,6 +9,10 @@
;; "When I rate a card, my progress is saved. When I open the file
;; tomorrow, my progress is still there."
;;
+;; get-item-data returns an `org-drill-card-state' struct; store-item-data
+;; takes one. The tests below view the struct as a field list (via
+;; `test-roundtrip--state-as-list') so the expected values read at a glance.
+;;
;; The contract has three branches:
;;
;; 1. *Modern format*: store writes six DRILL_* properties; get reads
@@ -19,7 +23,7 @@
;; transparently reads either format. Backward compat — a 2015-era
;; deck should still drill.
;;
-;; 3. *Virgin item*: a card that's never been rated returns
+;; 3. *Virgin item*: a card that's never been rated returns a state of
;; `(0 0 0 0 nil nil)' so the scheduler knows to treat it as new.
;;; Code:
@@ -30,6 +34,17 @@
;;;; Helpers
+(defun test-roundtrip--state-as-list (state)
+ "Return STATE's fields as a list, in the canonical
+(LAST-INTERVAL REPETITIONS FAILURES TOTAL-REPEATS MEANQ EASE) order, so the
+struct can be compared against a literal expected list."
+ (list (org-drill-card-state-last-interval state)
+ (org-drill-card-state-repetitions state)
+ (org-drill-card-state-failures state)
+ (org-drill-card-state-total-repeats state)
+ (org-drill-card-state-meanq state)
+ (org-drill-card-state-ease state)))
+
(defmacro with-fresh-drill-entry (&rest body)
"Run BODY at point on a fresh drill entry with no DRILL_* or LEARN_DATA properties."
(declare (indent 0))
@@ -58,11 +73,12 @@ PROPS is a list of (NAME . STRING-VALUE) cons cells."
(ert-deftest test-org-drill-get-item-data-virgin-returns-zero-list ()
"A drill entry with no persisted state returns the virgin sentinel.
-Six elements: zero interval, zero repeats, zero failures, zero total,
+Six fields: zero interval, zero repeats, zero failures, zero total,
nil meanq, nil ease. This is the value the scheduler reads on a
never-rated card."
(with-fresh-drill-entry
- (should (equal '(0 0 0 0 nil nil) (org-drill-get-item-data)))))
+ (should (equal '(0 0 0 0 nil nil)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
;;;; Modern format — read-side
@@ -74,13 +90,14 @@ never-rated card."
("DRILL_TOTAL_REPEATS" . "5")
("DRILL_AVERAGE_QUALITY" . "3.8")
("DRILL_EASE" . "2.4"))
- (should (equal '(10 3 1 5 3.8 2.4) (org-drill-get-item-data)))))
+ (should (equal '(10 3 1 5 3.8 2.4)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
(ert-deftest test-org-drill-get-item-data-modern-partial-properties-fall-back-to-defaults ()
"Missing DRILL_* properties take their per-field defaults — but the
presence of any DRILL_* property still puts the function in modern mode."
(with-modern-drill-entry '(("DRILL_TOTAL_REPEATS" . "3"))
- (let ((result (org-drill-get-item-data)))
+ (let ((result (test-roundtrip--state-as-list (org-drill-get-item-data))))
;; total-repeats → 3 (set), others take their defaults
(should (equal 0 (nth 0 result))) ; last-interval default 0
(should (equal 0 (nth 1 result))) ; repeats default 0
@@ -94,7 +111,7 @@ presence of any DRILL_* property still puts the function in modern mode."
(ert-deftest test-org-drill-store-item-data-writes-all-six-properties ()
"store-item-data sets all six DRILL_* properties on the entry at point."
(with-fresh-drill-entry
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
(should (equal "10.0" (org-entry-get (point) "DRILL_LAST_INTERVAL")))
(should (equal "3" (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
(should (equal "1" (org-entry-get (point) "DRILL_FAILURE_COUNT")))
@@ -106,7 +123,7 @@ presence of any DRILL_* property still puts the function in modern mode."
"Floating-point fields are rounded — interval to 4dp, meanq/ease to 3dp.
Keeps the buffer tidy and avoids stray precision noise like 2.4999999998."
(with-fresh-drill-entry
- (org-drill-store-item-data 10.123456789 3 1 5 3.8765432 2.4567899)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10.123456789 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8765432 :ease 2.4567899))
(should (equal "10.1235" (org-entry-get (point) "DRILL_LAST_INTERVAL")))
(should (equal "3.877" (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
(should (equal "2.457" (org-entry-get (point) "DRILL_EASE")))))
@@ -115,7 +132,7 @@ Keeps the buffer tidy and avoids stray precision noise like 2.4999999998."
"Storing modern format wipes any legacy LEARN_DATA on the entry.
Otherwise get-item-data would still read the stale legacy value first."
(with-modern-drill-entry '(("LEARN_DATA" . "(2.5 1 0.5)"))
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
(should (null (org-entry-get (point) "LEARN_DATA")))))
;;;; Round-trip — the core user-facing assertion
@@ -130,16 +147,18 @@ floats because `org-drill-round-float' returns float; counters (REPEATS,
FAILURES, TOTAL-REPEATS) come back as ints. Numerically the round-trip
is lossless; the scheduler accepts both."
(with-fresh-drill-entry
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
- (should (equal '(10.0 3 1 5 3.8 2.4) (org-drill-get-item-data)))))
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
+ (should (equal '(10.0 3 1 5 3.8 2.4)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
(ert-deftest test-org-drill-item-data-roundtrip-preserves-zero-values ()
"A first-rating round-trip with mostly zeros survives intact.
Same type-mixing pattern as the non-zero round-trip — see that test's
note for why LAST-INTERVAL is 0.0 and the counters are integer 0."
(with-fresh-drill-entry
- (org-drill-store-item-data 0 0 0 1 5.0 2.5)
- (should (equal '(0.0 0 0 1 5.0 2.5) (org-drill-get-item-data)))))
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 0 :repetitions 0 :failures 0 :total-repeats 1 :meanq 5.0 :ease 2.5))
+ (should (equal '(0.0 0 0 1 5.0 2.5)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
;;;; Legacy LEARN_DATA — backward compat
@@ -151,7 +170,8 @@ from their separate DRILL_* properties for the legacy path."
("DRILL_FAILURE_COUNT" . "0")
("DRILL_LAST_QUALITY" . "4"))
;; Returned: (interval, repeats, failures, repeats-again, last-quality, ease)
- (should (equal '(7 2 0 2 4 2.6) (org-drill-get-item-data)))))
+ (should (equal '(7 2 0 2 4 2.6)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
(ert-deftest test-org-drill-get-item-data-invalid-LEARN_DATA-falls-through-to-modern ()
"If LEARN_DATA is malformed, fall through to the modern DRILL_* fields.
@@ -163,14 +183,16 @@ Defends against a corrupted legacy entry from breaking a session."
("DRILL_TOTAL_REPEATS" . "6")
("DRILL_AVERAGE_QUALITY" . "3.5")
("DRILL_EASE" . "2.1"))
- (should (equal '(12 4 2 6 3.5 2.1) (org-drill-get-item-data)))))
+ (should (equal '(12 4 2 6 3.5 2.1)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
(ert-deftest test-org-drill-get-item-data-invalid-LEARN_DATA-and-no-modern-returns-virgin ()
"Malformed LEARN_DATA on an entry with no DRILL_* fallback returns virgin sentinel.
This matters: a corrupted-only legacy entry shouldn't crash the session,
just be treated as never rated."
(with-modern-drill-entry '(("LEARN_DATA" . "garbage"))
- (should (equal '(0 0 0 0 nil nil) (org-drill-get-item-data)))))
+ (should (equal '(0 0 0 0 nil nil)
+ (test-roundtrip--state-as-list (org-drill-get-item-data))))))
(provide 'test-org-drill-item-data-roundtrip)
diff --git a/tests/test-org-drill-orchestration-helpers.el b/tests/test-org-drill-orchestration-helpers.el
index 5589fa7..774f664 100644
--- a/tests/test-org-drill-orchestration-helpers.el
+++ b/tests/test-org-drill-orchestration-helpers.el
@@ -211,7 +211,7 @@ the resume hint."
(insert "* Question :drill:\n")
(org-mode)
(goto-char (point-min))
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
(org-drill--copy-scheduling-to-marker dst-marker)))
(with-current-buffer (find-file-noselect dst-file)
(goto-char (point-min))
@@ -254,7 +254,7 @@ the resume hint."
(ert-deftest test-strip-unmatched-dest-entries-clears-properties ()
"Every entry left in the table has its scheduling props stripped."
(with-org-tempfile "* Question :drill:\n"
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
(clrhash org-drill-dest-id-table)
(puthash "stale-id" (point-marker) org-drill-dest-id-table)
(org-drill--strip-unmatched-dest-entries)
diff --git a/tests/test-org-drill-prompt-and-misc.el b/tests/test-org-drill-prompt-and-misc.el
index d684c9a..968ad57 100644
--- a/tests/test-org-drill-prompt-and-misc.el
+++ b/tests/test-org-drill-prompt-and-misc.el
@@ -95,7 +95,7 @@
(ert-deftest test-org-drill-relearn-item-resets-interval-to-zero ()
"Relearning sets DRILL_LAST_INTERVAL back to 0 — the card behaves as new."
(with-fresh-drill-entry
- (org-drill-store-item-data 30 5 0 5 4.0 2.5)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 30 :repetitions 5 :failures 0 :total-repeats 5 :meanq 4.0 :ease 2.5))
(with-fixed-now
(org-drill-relearn-item))
;; After relearn: last-interval should be 0
@@ -105,7 +105,7 @@
(ert-deftest test-org-drill-relearn-item-removes-scheduled-stamp ()
"Relearning unschedules the entry (days-ahead = 0 path)."
(with-fresh-drill-entry
- (org-drill-store-item-data 30 5 0 5 4.0 2.5)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 30 :repetitions 5 :failures 0 :total-repeats 5 :meanq 4.0 :ease 2.5))
(org-schedule nil "2026-06-01")
(with-fixed-now
(org-drill-relearn-item))
diff --git a/tests/test-org-drill-queue-and-misc.el b/tests/test-org-drill-queue-and-misc.el
index 81a7844..057e6f8 100644
--- a/tests/test-org-drill-queue-and-misc.el
+++ b/tests/test-org-drill-queue-and-misc.el
@@ -228,7 +228,7 @@ actions require explicit consent."
(insert "* Question :drill:\n")
(org-mode)
(goto-char (point-min))
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (_) nil)))
(org-drill-strip-all-data)
;; Properties survive — user said no.
@@ -241,7 +241,7 @@ actions require explicit consent."
(insert "* Question :drill:\n")
(org-mode)
(goto-char (point-min))
- (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ (org-drill-store-item-data (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 2.4))
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (_) t)))
(org-drill-strip-all-data)
(dolist (prop org-drill-scheduling-properties)