branch: externals/org-mathsheet
commit 4cfa9040d2a1bd9c178f94e0547547cf98d1e255
Author: Ian Martins <ia...@jhu.edu>
Commit: Ian Martins <ia...@jhu.edu>

    Added math funcs, multi range sequences, worksheet instruction
---
 example.org   |  88 +++++++-------
 mathsheet.org | 364 +++++++++++++++++++++++++++++++++++++++-------------------
 2 files changed, 286 insertions(+), 166 deletions(-)

diff --git a/example.org b/example.org
index 3abbe060bd..19ea901393 100644
--- a/example.org
+++ b/example.org
@@ -9,58 +9,52 @@
 |      1 |     3 | [a=2..10] - [1..$a]   | small number subtraction, positive 
result |
 
 #+name: add-sub-2
-| weight | order | template                      | descr                       
            |
-|--------+-------+-------------------------------+-----------------------------------------|
-|      3 |     1 | [1..10] + [0..10]             | simple                      
            |
-|      2 |     2 | [1..10] + [8..15]             | second number bigger        
            |
-|      1 |     2 | [a=3..10] - [0..$a]           | subtraction                 
            |
-|      1 |     3 | [1..10] + [1..7] + [1..5]     | three numbers               
            |
-|      1 |     4 | [a=1..10] + [0..10] - [0..$a] | three with subtraction, 
positive result |
-|      0 |     0 | [$a*[1..5]] / [a=1..10]       | division                    
            |
+| weight | order | template                             | descr                
                   |
+|--------+-------+--------------------------------------+-----------------------------------------|
+|      3 |     1 | [1..10] + [0..10]                    | simple               
                   |
+|      2 |     2 | [1..10] + [8..15]                    | second number bigger 
                   |
+|      1 |     2 | [a=3..10] - [0..$a]                  | subtraction          
                   |
+|      1 |     3 | [1..10] + [1..7] + [1..5]            | three numbers        
                   |
+|      1 |     4 | [a=1..10] + [0..10] - [0..$a]        | three with 
subtraction, positive result |
+|      0 |     0 | [$a*[1..5]] / [a=1..10]              | division             
                   |
+|      5 |     5 | [a=1..10] + [b=0..10] - [0..($a+$b)] | three with 
subtraction, positive result |
 
-#+BEGIN: problem-set :templates "add-sub-1" :count 21 :instruction "Compute 
the answer"
-| problem | answer |
-|---------+--------|
-| 6 + 3   |      9 |
-| 1 + 6   |      7 |
-| 5 + 6   |     11 |
-| 2 + 0   |      2 |
-| 1 + 4   |      5 |
-| 8 + 7   |     15 |
-| 6 + 5   |     11 |
-| 2 + 3   |      5 |
-| 9 + 9   |     18 |
-| 3 + 8   |     11 |
-| 1 + 11  |     12 |
-| 4 + 2   |      6 |
-| 2 + 9   |     11 |
-| 1 + 4   |      5 |
-| 11 + 1  |     12 |
-| 6 + 0   |      6 |
-| 4 + 9   |     13 |
-| 7 + 10  |     17 |
-| 3 - 1   |      2 |
-| 9 - 8   |      1 |
-| 2 - 1   |      1 |
+#+BEGIN: problem-set :templates "add-sub-2" :count 12 :instruction "Compute 
the answer"
+| problem   | answer |
+|-----------+--------|
+| 2 + 6     |      8 |
+| 6 + 4     |     10 |
+| 9 + 5     |     14 |
+| 4 + 9     |     13 |
+| 6 - 4     |      2 |
+| 6 + 12    |     18 |
+| 5 + 3 + 1 |      9 |
+| 4 + 1 - 3 |      2 |
+| 2 + 7 - 5 |      4 |
+| 1 + 7 - 5 |      3 |
+| 9 + 6 - 7 |      8 |
+| 2 + 0 - 0 |      2 |
 #+END:
 
 * algebra
 #+name: algebra-1
-| weight | order | template                           | descr  |
-|--------+-------+------------------------------------+--------|
-|      3 |     1 | x / ([2..4] + [a=0..5]) = [$a..10] | simple |
-|      3 |     2 | [$a*[2..10]] / x = [a=1,2,4]       | simple |
-|      1 |     3 | x^2 = sqrt([16 - $a] + [a=1..5])   | simple |
+| weight | order | template                              | descr               
  |
+|--------+-------+---------------------------------------+-----------------------|
+|      3 |     1 | x / ([2..4] + [a=0..5]) = [$a..10]    | simple              
  |
+|      3 |     2 | [$a*[2..10]] / x = [a=1,2,4]          | simple              
  |
+|      1 |     3 | x^2 = sqrt([16 - $a] + [a=1..5])      | sqrt                
  |
+|      3 |     4 | [-5..-1,1..5] x + [0..10] = [-10..10] | double range 
sequence |
+|      3 |     4 | x = 2/x + sin([1..10])                | trig, two vars      
  |
 
 #+BEGIN: problem-set :templates "algebra-1" :count 8 :instruction "Solve for x"
-| problem            | answer |
-|--------------------+--------|
-| x / (3 + 0) = 5    | x = 15 |
-| x / (3 + 4) = 9    | x = 63 |
-| x / (3 + 0) = 4    | x = 12 |
-| 12 / x = 2         | x = 6  |
-| 8 / x = 2          | x = 4  |
-| 36 / x = 4         | x = 9  |
-| 6 / x = 2          | x = 3  |
-| x^2 = sqrt(13 + 3) | x = 2  |
+| problem            | answer            |
+|--------------------+-------------------|
+| x / (2 + 3) = 9    | x = 45            |
+| x / (3 + 3) = 9    | x = 54            |
+| 8 / x = 2          | x = 4             |
+| 36 / x = 4         | x = 9             |
+| x^2 = sqrt(12 + 4) | x = 2             |
+| -5 x + 8 = 0       | x = 8:5           |
+| -2 x + 3 = 0       | x = 3:2           |
+| x = 2/x + sin(2)   | x = 1.43177096141 |
 #+END:
diff --git a/mathsheet.org b/mathsheet.org
index 3f18868811..73279900a5 100644
--- a/mathsheet.org
+++ b/mathsheet.org
@@ -1,12 +1,87 @@
-* Goal
-The goal is to generate a math practice sheet made up of dynamic
-problems that are defined based on a set of flexible templates. The
-problem distribution and order should also be configurable.
-* Problem Templates
-** Overview
-This section contains some example templates. Each table defines a
-worksheet. Each time the worksheet is generated the problems are
-re-randomized.
+* TODO rename to org-mathsheet.el
+* Overview
+** Description
+This is a math worksheet generator. The worksheets are randomly
+generated based on templates that define what kinds of problems to
+include along with the order and relative frequency that each type of
+problem should appear on the worksheet.
+** Audience
+This could be useful for anyone that wants to provide math practice to
+someone else. It could be useful for a teacher, tutor, homeschool
+parent, or any parent.
+** Examples
+Here are some example worksheets generated by this tool:
+1. arithmatic
+2. algebra
+** Parts
+There are two main components involved in generating a worksheet:
+1. the problem templates
+2. the problem-set block
+*** Problem Templates
+**** Expression Templates
+The worksheet is made of a set of math problems. Each problem is
+defined by a template that lays out an equation or expression and
+shows where variables or numbers should be. For example, consider this
+template:
+#+begin_example
+[0..15] + [1..10]
+#+end_example
+The parts within the brackets are fields. When a template is made into
+a problem and added to a worksheet, each field is replaced by a number
+based on a set of rules. The supported rules are described in more
+detail below, but ~[0..15]~ means pick a random number between 0 and 15,
+inclusive, so the above template could result in problems like these:
+#+begin_example
+1 + 2
+15 + 10
+5 + 1
+#+end_example
+**** Equation Templates
+In additon to expressions where the answer is a number, templates can
+be equations where the solution is found by solving for the
+variable. For example, consider this template:
+#+begin_example
+[1..5] x + [0..10] = [-10..10]
+#+end_example
+This can produce the following problems:
+#+begin_example
+3 x + 6 = -1
+4 x + 2 = 2
+1 x + 8 = -3
+#+end_example
+**** Field Rules
+These are the field rules:
+- [-2..8] :: choose a random number from -2 to 8, inclusive
+- [1,3,5] :: choose randomly from 1, 3 or 5
+- [-3..-1,1..3] :: choose a random number from -3 to -1 or 1 to 3
+- [10/(2-1)] :: evaluate the expression
+- [a=...] :: assign the variable a to the number chosen for this field
+- [-2..$a] :: any number from -2 to the value assigned to ~a~ in another
+  field
+- [0..[$a/2]] :: any number from 0 to half the value assigned to ~a~.
+
+The ability to keep track of the random number chosen in one field and
+use it to influence another allows the template to be written to avoid
+answers that are negative or don't divide evenly.
+**** Template Examples
+Here are a few more examples:
+
+Division problem that divides evenly
+#+begin_example
+[$a*[1..5]] / [a=1..10]
+#+end_example
+
+Addition and subtraction, still with a positive result
+#+begin_example
+[a=1..10] + [b=0..10] - [0..($a+$b)]
+#+end_example
+
+*** The Problem Template Table
+
+You may want to have more than one type of problem on a worksheet, so
+
+Each table defines a worksheet. Each time the worksheet is generated
+the problems are re-randomized.
 
 The table contains the following columns:
 - weight :: the relative number of this type of problem to include on
@@ -15,19 +90,9 @@ The table contains the following columns:
   problems with the same order will be intermingled.
 - template :: this is the template used to generate problems of this
   type. Templates are described in more detail below.
-- descr :: just notes, not used in worksheet generation.
+- descr :: just your notes, not used in worksheet generation.
 
-Templates are problems but the numbers are replaced with placeholders
-in square brackets.
-- [0..10] :: any number from 0 to 10
-- [a=...] :: assign the variable a to the number chosen for this field
-- [1,3,5] :: choose 1 or 3 or 5
-- [10/(2-1)] :: evaluate the expression
-- [-2..$a] :: any number from -2 to the value assigned to a in another
-  placeholder
-- [0..[$a/2]] :: placeholders can be embedded within placeholders
-
-** Examples
+**** Examples
 
 We label the table so that we can refer to it from the dynamic block
 that generates the worksheet. Only the first three columns are used.
@@ -41,7 +106,9 @@ that generates the worksheet. Only the first three columns 
are used.
 |      1 |     3 | [1..10] + [1..7] + [1..5]     | three terms                 
 |
 |      1 |     4 | [a=1..10] + [0..10] - [0..$a] | three terms with 
subtraction |
 |      0 |     0 | [$a*[1..5]] / [a=1..10]       | division                    
 |
-
+*** Problem-Set Block
+**** Overview
+**** Examples
 * Code walkthrough
 ** Problem generation
 *** Header
@@ -57,17 +124,17 @@ This package needs 
[[https://elpa.gnu.org/packages/peg.html][peg]].
 #+end_src
 
 *** Variables
-Need ~ianxm/var-list~ to keep track of the variables between fields.
+Need ~mathsheet--var-list~ to keep track of the variables between fields.
 
 ~worksheet-template~ is the LaTeX template for the worksheet.
 
 #+name: variables
 #+begin_src elisp :tangle mathsheet.el :var page=page
-  (defvar ianxm/var-list '()
+  (defvar mathsheet--var-list '()
     "List of variables used in a problem")
 
-  (defconst ianxm/worksheet-template page
-    "LaTeX template for worksheet")
+  (defconst mathsheet--worksheet-template page
+    "LaTeX template for the worksheet")
 #+end_src
 *** Scan problem
 
@@ -98,6 +165,7 @@ The last entry is ~nil~ for "not visited." It is used by 
~dfs-visit~.
 for example:
 #+begin_example
 [$a + 2 + [a=1..5]] => '((nil (a) m1 m19 nil) (a nil m11 m18 nil))
+                       '((:fields (_0 (a a) (marker . marker) nil) (a nil 
(marker . marker) nil)) (:alg-vars))
 #+end_example
 
 This uses the peg package to parse the problem. Instead of using the
@@ -110,8 +178,15 @@ new field to the list when we close the current field.
 
 #+name: scan-problem
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/scan-problem ()
-    "Scan problem"
+  (defun mathsheet--scan-problem ()
+    "Scan a problem.
+
+  This parses the problem and produces a list containing info about
+  its fields. For each field it returns a list containing:
+  1. a symbol for the assigned variable or a unique placeholder
+  2. a list of variables this field depends on
+  3. a cons containing start and end markers for the field in the current 
buffer
+  4. `nil' which is used by `dfs-visit' later"
     (let ((field-index 0)
           open-fields ; stack
           closed-fields ; list
@@ -122,7 +197,7 @@ new field to the list when we close the current field.
            (field open (opt assignment) stuff close)
            (space (* [space]))
            (open (region "[")
-                 `(l r -- (progn
+                 `(l _ -- (progn
                             (push (list
                                    (intern (concat "_" (number-to-string 
field-index))) ; asn-var
                                    nil ; deps
@@ -131,28 +206,28 @@ new field to the list when we close the current field.
                                   open-fields)
                             (setq field-index (1+ field-index))
                             ".")))
-           (assignment (region (substring letter)) "="
-                       `(l v r -- (progn
-                                    (setcar
-                                     (car open-fields)
-                                     (intern v))
-                                    ".")))
+           (assignment (substring letter) "="
+                       `(v -- (progn
+                                (setcar
+                                 (car open-fields)
+                                 (intern v))
+                                ".")))
            (asn-var "$" (substring letter)
-                `(v -- (progn
-                         (push (intern v) (cadar open-fields))
-                         ".")))
+                    `(v -- (progn
+                             (push (intern v) (cadar open-fields))
+                             ".")))
            (alg-var (substring letter)
                     `(v -- (progn
                              (push v alg-vars)
                              ".")))
            (close (region "]")
-                  `(l r -- (progn
+                  `(l _ -- (progn
                              (setcdr (caddar open-fields) (copy-marker l t))
                              (when (> (length open-fields) 1) ; add parent to 
child dependency
                                (push (caar open-fields) (cadadr open-fields)))
                              (push (pop open-fields) closed-fields)
                              ".")))
-           (math-func (or "sqrt"))
+           (math-func (or "sqrt" "sin" "cos" "tan" "asin" "acos" "atan" 
"floor" "ceil" "round"))
            (letter [a-z])
            (digit [0-9])
            (symbol (or "." "," "+" "-" "*" "/" "^" "(" ")" "=")))
@@ -171,13 +246,13 @@ test scan
   <<scan-problem>>
 
   (with-temp-buffer
-    (insert "y = [1..4] + [5,7,9]")
+    (insert "[0..4,6-9,11] * x + [floor([-10..10]/3)] = [-10..10]")
     (goto-char (point-min))
-    (ianxm/scan-problem))
+    (mathsheet--scan-problem))
 #+end_src
 
 #+RESULTS:
-: ((:fields (_1 nil (#<marker in no buffer> . #<marker (moves after insertion) 
in no buffer>) nil) (_0 nil (#<marker in no buffer> . #<marker (moves after 
insertion) in no buffer>) nil)) (:alg-vars "y"))
+: ((:fields (_3 nil (#<marker in no buffer> . #<marker (moves after insertion) 
in no buffer>) nil) (_1 (_2) (#<marker in no buffer> . #<marker (moves after 
insertion) in no buffer>) nil) (_2 nil (#<marker in no buffer> . #<marker 
(moves after insertion) in no buffer>) nil) (_0 nil (#<marker in no buffer> . 
#<marker (moves after insertion) in no buffer>) nil)) (:alg-vars "x"))
 
 *** Reduce field
 
@@ -191,41 +266,58 @@ This uses the peg package to parse the field. This time 
there
 shouldn't be any fields embedded within the field. We should have
 already evaluated and replaced them.
 
+We use ~..~ insead of ~-~ for range because if we used ~-~ then this would
+be ambiguous:
+#+begin_example
+[1-5]
+#+end_example
+
 #+name: reduce-field
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/reduce-field ()
+  (defun mathsheet--reduce-field ()
+    "Reduce the field to a number.
+
+  Parse the field again, replacing spans with random numbers and
+  evaluating arithmetic operations. The field shouldn't have any
+  internal fields so this should result in a single number. Return
+  that number."
     (with-peg-rules
-        ((field "[" space (or range sequence assignment expression value) 
space "]")
+        ((field "[" space (or math-func expression sequence assignment 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
-                                  " ")))))
+                                 (list
+                                  (mapconcat
+                                   (lambda (x) (if (numberp x) 
(number-to-string x) x))
+                                   vals
+                                   " "))
+                                 calc-prefer-frac nil))))
          (operation (substring (or "+" "-" "*" "/")))
          (assignment var-lhs space "=" space (or range sequence)
                      `(v r -- (progn
-                                (push (cons (intern v) r) ianxm/var-list)
+                                (push (cons (intern v) r) mathsheet--var-list)
                                 r)))
+         (sequence (list (or range value) (* "," space (or range value)))
+                   `(vals -- (seq-random-elt vals)))
          (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 ")")
+         (parenthetical "(" (or expression value) ")")
          (var-lhs (substring letter)) ; var for assignment
          (var-rhs "$" (substring letter) ; var for use
-                  `(v -- (let ((val (alist-get (intern v) ianxm/var-list)))
+                  `(v -- (let ((val (alist-get (intern v) 
mathsheet--var-list)))
                            (or val (error "var %s not set" v)))))
+         (math-func (substring (or "sqrt" "sin" "cos" "tan" "asin" "acos" 
"atan" "floor" "ceil" "round"))
+                    parenthetical
+                    `(f v -- (string-to-number (calc-eval (format "%s(%s)" f 
v)))))
          (space (* [space]))
          (letter [a-z])
          (digit [0-9]))
 
       (peg-run (peg field)
                (lambda (x) (message "failed %s" x))
-               (lambda (x) (funcall x)))))
+               (lambda (x) (car (funcall x))))))
 #+end_src
 
 test with
@@ -235,13 +327,14 @@ test with
   <<reduce-field>>
 
   (with-temp-buffer
-      (insert "[1..4]")
+    ;(insert "[1..10,15..20,50]")
+    (insert "[1..10]")
       (goto-char (point-min))
-      (ianxm/reduce-field))
+      (mathsheet--reduce-field))
 #+end_src
 
 #+RESULTS:
-: (1)
+: 8
 
 *** Replace field
 
@@ -249,13 +342,18 @@ Replace a field with the value returned from reducing it.
 
 #+name: replace-field
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/replace-field (node)
+  (defun mathsheet--replace-field (node)
+    "Replace a field with the number to which it reduces
+
+  Update the current buffer by replacing the field at point in the
+  current buffer with the number it reduces to. NODE contains the
+  info for the current field."
     (let ((start (caaddr node))
           (end (1+ (cdaddr node)))
           val)
       (goto-char start)
       (when (looking-at "\\[")
-        (setq val (car (ianxm/reduce-field)))
+        (setq val (mathsheet--reduce-field))
         (goto-char start)
         (delete-char (- end start) t)
         (insert (number-to-string val)))))
@@ -269,7 +367,12 @@ the node.
 
 #+name: dfs-visit
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/dfs-visit (node fields)
+  (defun mathsheet--dfs-visit (node fields)
+    "Visit NODE as part of a DFS of the problem
+
+  Traverse the fields of a problem using depth first search to
+  ensure that field replacement happens in dependency order. FIELDS
+  is a list of all fields in the problem."
     (pcase (cadddr node)
       (1 (error "cycle detected")) ; cycle
       (2)                          ; skip
@@ -277,10 +380,10 @@ the node.
        (setcar (cdddr node) 1)     ; started
        (let ((deps (cadr node)))
          (dolist (dep deps)
-           (ianxm/dfs-visit
+           (mathsheet--dfs-visit
             (assq dep fields)
             fields)))
-       (ianxm/replace-field node) ; visit
+       (mathsheet--replace-field node) ; visit
        (setcar (cdddr node) 2)))) ; mark done
 #+end_src
 
@@ -293,21 +396,26 @@ processes all fields in a problem.
 #+end_example
 
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/fill-problem (full-problem)
+  (defun mathsheet--fill-problem (full-problem)
+    "Replace all fields in FULL-PROBLEM
+
+  Goes through all fields in the given problem in dependency order
+  and replaces fields with numbers. When this completes the problem
+  will be ready to solve."
       (with-temp-buffer
         ;; stage problem in temp buffer
         (insert full-problem)
-        (beginning-of-buffer)
+        (goto-char (point-min))
 
         ;; find fields, assignment variables, algebraic variables, dependencies
-        (let* ((scan-ret (ianxm/scan-problem))
+        (let* ((scan-ret (mathsheet--scan-problem))
                (fields (alist-get :fields scan-ret))
                (alg-vars (alist-get :alg-vars scan-ret)))
 
           ;; visit fields ordered according to dependencies
           (dolist (node fields)
-            (ianxm/dfs-visit node fields))
-          (setq ianxm/var-list '())
+            (mathsheet--dfs-visit node fields))
+          (setq mathsheet--var-list '())
 
           ;; return filled problem
           `((:problem . ,(buffer-string))
@@ -322,11 +430,11 @@ test with this
   <<replace-field>>
   <<dfs-visit>>
 
-  (ianxm/fill-problem "[1..12] + [1,4,6,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])")
-  ;; (ianxm/fill-problem "1/x + 2 = [-10..[10..20]]")
+  (mathsheet--fill-problem "[1..12] + [1,4,6,10]")
+  ;;(mathsheet--fill-problem "[1..[2..[10..100]]]")
+  ;;(mathsheet--fill-problem "[$a*[1..10]] / [a=1..10]")
+  ;;(mathsheet--fill-problem "[$a]/(3+[a=1..5])")
+  ;; (mathsheet--fill-problem "1/x + 2 = [-10..[10..20]]")
 
 #+end_src
 
@@ -366,7 +474,13 @@ other examples
 
 #+name: generate-problems
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/generate-problems (template-name count)
+  (defun mathsheet--generate-problems (template-name count)
+    "Generate COUNT problems based on TEMPLATE-NAME
+
+  Generate problems and answers based on what is defined in the
+  given template table. The template table defines problem
+  templates as well as relative weights and how they should be
+  ordered."
     (let (total-weight templates problems)
       (save-excursion
         (goto-char (point-min))
@@ -385,7 +499,7 @@ other examples
                                       templates
                                       0)))
       ;; calculate number for each row
-      (dotimes (ii (length templates) problems)
+      (dotimes (ii (length templates))
         (let* ((item (nth ii templates))
                (weight (car item))
                (needed (cond ; number of problems to add for this template
@@ -395,42 +509,39 @@ other examples
                          (- count (length problems)))
                         (t
                          (max (round (* (/ weight total-weight) count) ) 1))))
-               problem answer)
-
-          (let ((added 0)
-                (dup-count 0)
-                problem-set
-                fill-ret problem solution)
-            (while (< added needed) ; add until "needed" are kept
-              (let* ((fill-ret (ianxm/fill-problem (caddr item)))
-                     (problem (alist-get :problem fill-ret))
-                     (alg-vars (alist-get :alg-vars fill-ret))
-                     (calc-string (if (not alg-vars)
-                                      problem
-                                    (format "solve(%s,[%s])" problem 
(string-join alg-vars ","))))
-                     (solution
-                      (replace-regexp-in-string (rx (or "[" ".]" "]"))
-                                                ""
-                                                (calc-eval calc-string))))
-                (cond
-                 ((member problem problem-set) ; dedup problems
-                  (setq dup-count (1+ dup-count))
-                  (when (> dup-count 100)
-                    ;; high number of dups indicates a narrow problem space 
relative to problem count
-                    (error "Giving up, too many dups")))
-                 (t
-                  (push problem problem-set)
-                  (push (list problem ; problem
-                              solution ; solution
-                              (cadr item) ; order
-                              (not (null alg-vars))) ; true if algebraic 
variables exist
-                        problems)
-                  (setq added (1+ added)))))))))
+               (added 0)
+               (dup-count 0)
+               problem-set)
+          (while (< added needed) ; add until "needed" are kept
+            (let* ((fill-ret (mathsheet--fill-problem (caddr item)))
+                   (problem (alist-get :problem fill-ret))
+                   (alg-vars (alist-get :alg-vars fill-ret))
+                   (calc-string (if (not alg-vars)
+                                    problem
+                                  (format "solve(%s,[%s])" problem 
(string-join (seq-uniq alg-vars) ","))))
+                   (solution
+                    (replace-regexp-in-string (rx (or "[" ".]" "]"))
+                                              ""
+                                              (calc-eval calc-string))))
+              (cond
+               ((member problem problem-set) ; dedup problems
+                (setq dup-count (1+ dup-count))
+                (when (> dup-count 100)
+                  ;; high number of dups indicates a narrow problem space 
relative to problem count
+                  (error "Giving up, too many dups")))
+               (t
+                (push problem problem-set)
+                (push (list problem ; problem
+                            solution ; solution
+                            (cadr item) ; order
+                            (not (null alg-vars))) ; true if algebraic 
variables exist
+                      problems)
+                (setq added (1+ added))))))))
 
       ;; shuffle
       (dotimes (ii (- (length problems) 1))
         (let ((jj (+ (random (- (length problems) ii)) ii)))
-          (psetf (elt problems ii) (elt problems jj)
+          (cl-psetf (elt problems ii) (elt problems jj)
                  (elt problems jj) (elt problems ii))))
 
       ;; sort by order
@@ -450,24 +561,31 @@ I need to extract the values
 
 - :templates :: templates
 - :count :: 10
+- :instruction :: "Solve for x"
 
 #+begin_src elisp :tangle mathsheet.el
   (defun org-dblock-write:problem-set (params)
-    "Update problem-set block and optionally write a worksheet."
+    "Update problem-set block and optionally write a worksheet.
+
+  PARAMS is a plist with the properties set on the dynamic block
+  header, which includes `:tempates' which is the name of the
+  templates table, `:count' which is the number of problems to put
+  on the worksheet, and `:instruction' which is the content of the
+  instruction line at the top of the page"
 
     ;; write the table header
     (insert "| problem | answer |\n")
     (insert "|-\n")
 
     ;; generate problem set
-    (let ((problems (ianxm/generate-problems
+    (let ((problems (mathsheet--generate-problems
                      (plist-get params :templates)
                      (plist-get params :count))))
 
       ;; for each problem, write a row to the table
       (insert
        (mapconcat
-        (lambda (problem) (format "|%s|%s|"
+        (lambda (problem) (format "| %s | %s |"
                                   (car problem)
                                   (cadr problem)))
         problems
@@ -478,7 +596,7 @@ I need to extract the values
 
       ;; should we generate the sheet?
       (when (y-or-n-p "Write worksheet? ")
-        (ianxm/gen-worksheet
+        (mathsheet--gen-worksheet
          (plist-get params :templates)
          (plist-get params :instruction)
          problems))))
@@ -549,7 +667,11 @@ Convert a calc expression to latex format.
 
 #+name: convert-to-latex
 #+begin_src elisp :tangle mathsheet.el
-  (defun ianxm/convert-to-latex (expr)
+  (defun mathsheet--convert-to-latex (expr)
+    "Format the given calc expression EXPR for LaTeX
+
+  EXPR should be in normal calc format. The result is the same
+  expression (not simplified) but in LaTeX format."
     (let* ((calc-language 'latex)
            (calc-expr (math-read-expr expr))
            (latex-expr (math-format-stack-value (list calc-expr 1 nil)))
@@ -565,9 +687,13 @@ template name will overwrite the same file.
 
 
 #+begin_src elisp :results silent :tangle mathsheet.el
-  (defun ianxm/gen-worksheet (template-name instruction problems)
-    (with-temp-file (concat template-name ".tex")
-      (insert ianxm/worksheet-template)
+  (defun mathsheet--gen-worksheet (file-name instruction problems)
+    "Generate a worksheet with PROBLEMS.
+
+  Write a file named FILE-NAME. Include the INSTRUCTION line at the
+  top."
+    (with-temp-file (concat file-name ".tex")
+      (insert mathsheet--worksheet-template)
 
       (goto-char (point-min))
       (search-forward "<<instruction>>")
@@ -580,16 +706,16 @@ template name will overwrite the same file.
       (dolist (row problems)
         (if (cadddr row)
             (insert (format"\\CircledItem %s\\vspace{4cm}\n"
-                           (ianxm/convert-to-latex (car row))))
+                           (mathsheet--convert-to-latex (car row))))
           (insert (format"\\CircledItem %s = 
\\rule[-.2\\baselineskip]{2cm}{0.4pt}\n"
-                         (ianxm/convert-to-latex (car row))))))
+                         (mathsheet--convert-to-latex (car row))))))
 
       (goto-char (point-min))
       (search-forward "<<answers>>")
       (replace-match "")
       (dolist (row problems)
         (insert (format "\\CircledItem %s\n"
-                        (ianxm/convert-to-latex (cadr row))))))
-    (shell-command (concat "texi2pdf " template-name ".tex")
+                        (mathsheet--convert-to-latex (cadr row))))))
+    (shell-command (concat "texi2pdf " file-name ".tex")
                    (get-buffer-create "*Standard output*")))
 #+end_src

Reply via email to