branch: externals/org-mathsheet commit 688e66613b22d5f27c689036d5854a1b8d0b7424 Author: Ian Martins <ia...@jhu.edu> Commit: Ian Martins <ia...@jhu.edu>
Can produce problems from template --- mathsheet.org | 280 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 269 insertions(+), 11 deletions(-) diff --git a/mathsheet.org b/mathsheet.org index f290b37773..e3a13d57ae 100644 --- a/mathsheet.org +++ b/mathsheet.org @@ -1,25 +1,281 @@ * goal the goal is to generate a math practice sheet. + +similar to https://www.math-aids.com * script ** vars -#+property: header-args+ :var student="Noble" problem-count=24 +-#+property: header-args+ :var student="Noble" problem-count=24 ** problem sets *** add and subtract -TODO support "var c in [b 5]" -#+name: first-set +#+name: firstset +| weight | order | problem | descr | +|--------+-------+-------------------------------+------------------------| +| 3 | 1 | [1..10] + [0..10] | simple | +| 2 | 2 | [1..10] + [8..15] | second number bigger | +| 1 | 3 | [1..10] + [1..7] + [1..5] | three numbers | +| 1 | 4 | [a=3..10] - [0..$a] | subtraction | +| 1 | 4 | [a=1..10] + [0..10] - [1..$a] | three with subtraction | +| 0 | 0 | [$a*[1..5]] / [a=1..10] | division | + +** problem generation + +TODO create a package. if you C-c C-c on a table +1. if you are on the header, generate a worksheet +2. if you on on a row, generate a single example + +*** var-list +need ~var-list~ to keep track of the variables between fields. + +#+name: var-list +#+begin_src elisp +(defvar ianxm/var-list '() +"List of variables used in a problem") +#+end_src +*** scan field + +must call with point at the start of a field. moves the point to the +end of the field. returns a list of fields, formatted as: + +#+begin_example +'(var (deps) start-marker end-marker nil) +#+end_example + +~var~ is a variable name if there is an assignment, or it is a +placeholder like ~_0~, ~_1~, etc. the last entry is nil for "not +visited." ~var~ must be interned and must be the first index since we +use this as an alist. + +~start-marker~ and ~end-marker~ are markers in the (temp) buffer. + +for example: +#+begin_example +[$a + 2 + [a=1..5]] => '((nil (a) m1 m19 nil) (a nil m11 m18 nil)) +#+end_example + +~open-fields~ is a stack of fields with the current field on top. we +push a new field to the stack when we start a new field. +~closed-fields~ is a list of fields that have been completed. we push a +new field to the list when we close the current field. + +#+name: scan-problem +#+begin_src elisp + (defun ianxm/scan-problem () + (interactive) + (let ((field-index 0) + open-fields ; stack (open close (vars) deps) + closed-fields) ; list (open close (vars) deps) + + (with-peg-rules + ((stuff (* (or var letter digit symbol field space))) + (field open (opt assignment) stuff close) + (space (* [space])) + (open (region "[") + `(l r -- (progn + (setq + open-fields (push (list + (intern (concat "_" (number-to-string field-index))) + nil (copy-marker l) nil nil) + open-fields) + field-index (1+ field-index)) + "."))) + (assignment (region (substring letter)) "=" + `(l v r -- (progn + (setcar + (car open-fields) + (intern v)) + "."))) + (var "$" (substring letter) + `(v -- (progn + (setcar + (nthcdr 1 (car open-fields)) + (push (intern v) (nth 1 (car open-fields)))) + "."))) + (close (region "]") + `(l r -- (progn + (setcar (nthcdr 3 (car open-fields)) (copy-marker l t)) + (when (> (length open-fields) 1) + (setcar + (nthcdr 1 (nth 1 open-fields)) + (push (caar open-fields) (nth 1 (nth 1 open-fields))))) + (setq + closed-fields + (push (pop open-fields) closed-fields)) + "."))) + (letter [a-z]) + (digit [0-9]) + (symbol (or "." "+" "-" "*" "/" "(" ")"))) + + (peg-run (peg stuff) + (lambda (x) (message "failed %s" x)) + (lambda (x) + (funcall x) + closed-fields))))) +#+end_src + +*** reduce field + +must call with point at the start of a field. moves point to the end +of the field. returns a list containing the value to which the field +reduces. + +#+name: reduce-field +#+begin_src elisp + (defun ianxm/reduce-field () + (interactive) + (with-peg-rules + ((field "[" space (or range sequence assignment expression value) space "]") + (expression (list value space operation space value (* space operation space value)) + `(vals -- (string-to-number + (calc-eval + (mapconcat + (lambda (x) (if (numberp x) (number-to-string x) x)) + vals + " "))))) + (operation (substring (or "+" "-" "*" "/"))) + (assignment var-lhs space "=" space (or range sequence) + `(v r -- (progn + (setq var-list (push (cons (intern v) r) var-list)) + r))) + (range value ".." value + `(min max -- (+ (random (- max min)) min))) + (sequence (list value "," value (* "," value)) + `(vals -- (seq-random-elt vals))) + (value (or (substring (opt "-") (+ digit)) var-rhs parenthetical) + `(v -- (if (stringp v) (string-to-number v) v))) + (parenthetical "(" expression ")" + (action (message "paren"))) + (var-lhs (substring letter)) ; var for assignment + (var-rhs "$" (substring letter) ; var for use + `(v -- (let ((val (alist-get (intern v) var-list))) + (or val (error "var %s not set" v))))) + (space (* [space])) + (letter [a-z]) + (digit [0-9])) + + (peg-run (peg field) + (lambda (x) (message "failed %s" x)) + (lambda (x) (funcall x))))) +#+end_src + +#+begin_example + simple range + [10..11] + + complex range + [-10..[10..20]] + + complex with assignment + [a=1..[2..8]] + + complex with inner assignment + [-10..[b=10..20]] + + simple with variable + [0..[$a..$b]] +#+end_example + +*** replace field + +replace a field with the value returned from processing it. + +#+name: replace-field +#+begin_src elisp + (defun ianxm/replace-field (node) + (let ((start (nth 2 node)) + (end (1+ (nth 3 node))) + val) + (goto-char start) + (when (looking-at "\\[") + (setq val (car (ianxm/reduce-field))) + (goto-char start) + (delete-char (- end start) t) + (insert (number-to-string val))))) +#+end_src + +*** dfs visit + +check dependencies then visit the node + +#+name: dfs-visit +#+begin_src elisp + (defun ianxm/dfs-visit (node fields) + (pcase (nth 4 node) + (1 (error "cycle detected")) ; cycle + (2) ; skip + (_ ; process + (setcar (nthcdr 4 node) 1) ; started + (let ((deps (nth 1 node))) + (dolist (dep deps) + (ianxm/dfs-visit + (assq dep fields) + fields))) + (ianxm/replace-field node) ; visit + (setcar (nthcdr 4 node) 2)))) ; mark done +#+end_src +*** fill fields in problem + +processes all fields in a problem. + #+begin_example -var a in [1 10] -var b in [0 10] -var c in [1 5] -var d in [0 15] -problem weight 3 order 1 "a + b" -problem weight 2 order 1 "a + d" -problem weight 1 order 2 "a + b + c" -problem weight 1 order 3 "a + c - b" +(full-problem (buffer-substring (point-at-bol) (point-at-eol))) #+end_example +#+name: fill-problem +#+begin_src elisp :var full-problem="[$a + 2 + [a=1..5]]" + (defun ianxm/fill-problem (full-problem) + (interactive) + (let (fields) + (with-temp-buffer + ;; stage problem in temp buffer + (insert full-problem) + (beginning-of-buffer) + + ;; find fields, assignments, dependencies + (setq fields (ianxm/scan-problem)) + (message "fields %s" fields) + + ;; order fields according to dependencies + (dolist (node fields) + (ianxm/dfs-visit node fields)) + (setq var-list '()) + (buffer-string)))) +#+end_src + +test with this +#+begin_src elisp :noweb yes + <<full>> + + (ianxm/fill-problem "[1..12] + [1..10]") + ;;(ianxm/fill-problem "[1..[2..[10..100]]]") + ;;(ianxm/fill-problem "[$a*[1..10]] / [a=1..10]") + ;;(ianxm/fill-problem "[$a]/(3+[a=1..5])") + +#+end_src + +#+RESULTS: +: 11 + 8 + +*** full script +tangles everything needed to convert a template to a problem + +#+name: full +#+begin_src elisp :noweb yes :tangle mathsheet.el +<<var-list>> + +<<scan-problem>> + +<<reduce-field>> + +<<catalog-fields>> + +<<replace-field>> + +<<dfs-visit>> + +<<fill-problem>> +#+end_src ** generate problem set from template #+name: problem-set @@ -77,6 +333,8 @@ problem weight 1 order 3 "a + c - b" #+RESULTS: one-problem : (7 + 8 . 15) +*** TODO dedup + ** lay out problems and answers this generates a problem set.