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

    Tangle instead of executing source blocks in place
---
 example.org   |  38 ++++++++
 mathsheet.org | 303 +++++++++++++++++++++++++++++++---------------------------
 2 files changed, 202 insertions(+), 139 deletions(-)

diff --git a/example.org b/example.org
new file mode 100644
index 0000000000..53315935e0
--- /dev/null
+++ b/example.org
@@ -0,0 +1,38 @@
+* add and subtract
+
+#+name: first-set
+| 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 |
+|      0 |     0 | [$a*[1..5]] / [a=1..10]       | division               |
+
+#+BEGIN: problem-set :templates "first-set" :count 21 :name "Noble"
+| problem   | answer |
+|-----------+--------|
+| 2 + 7     |      9 |
+| 1 + 3     |      4 |
+| 4 + 6     |     10 |
+| 7 + 0     |      7 |
+| 9 + 1     |     10 |
+| 6 + 7     |     13 |
+| 6 + 8     |     14 |
+| 3 + 13    |     16 |
+| 9 + 11    |     20 |
+| 9 - 2     |      7 |
+| 4 - 1     |      3 |
+| 5 + 14    |     19 |
+| 5 + 11    |     16 |
+| 1 + 10    |     11 |
+| 8 - 3     |      5 |
+| 7 + 5 + 2 |     14 |
+| 2 + 5 + 4 |     11 |
+| 9 + 4 + 2 |     15 |
+| 5 + 8 - 4 |      9 |
+| 5 + 8 - 3 |     10 |
+| 7 + 9 - 2 |     14 |
+#+END:
+* bigger addition and multiplications
diff --git a/mathsheet.org b/mathsheet.org
index 854493acfe..d30683cc33 100644
--- a/mathsheet.org
+++ b/mathsheet.org
@@ -1,18 +1,14 @@
-* goal
-The goal is to generate a math practice sheet made up of dynamic problems that 
are defined in flexible templates.
+* Goal
+The goal is to generate a math practice sheet made up of dynamic
+problems that are defined based on flexible templates. The problem
+distribution and order is also configurable.
 
 Similar to https://www.math-aids.com.
-* script
-** vars
-This sets the name at the top of the page as well as the number of
-problems on the worksheet.
-
-#+property: header-args+ :var student="Noble" problem-count=26
-
-** problem set examples
+* Problem Templates
+** Description
 This section contains some example templates. Each table defines a
-worksheet. Each time the worksheet is created the problems are
-generated randomly.
+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
@@ -33,7 +29,7 @@ in square brackets.
   placeholder
 - [0..[$a/2]] :: placeholders can be embedded within placeholders
 
-*** add and subtract
+** Examples
 
 #+name: firstset
 | weight | order | template                      | descr                  |
@@ -45,50 +41,54 @@ in square brackets.
 |      1 |     4 | [a=1..10] + [0..10] - [0..$a] | three with subtraction |
 |      0 |     0 | [$a*[1..5]] / [a=1..10]       | division               |
 
+* script
 ** problem generation
+*** variables
+Need ~var-list~ to keep track of the variables between fields.
 
-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
+~worksheet-template~ is the LaTeX template for the worksheet.
 
-*** var-list
-need ~var-list~ to keep track of the variables between fields.
+#+begin_src elisp :tangle mathsheet.el :var page=page
+  (defvar ianxm/var-list '()
+    "List of variables used in a problem")
 
-#+name: var-list
-#+begin_src elisp
-(defvar ianxm/var-list '()
-"List of variables used in a problem")
+  (defconst ianxm/worksheet-template page
+    "LaTeX template for worksheet")
 #+end_src
-*** scan field
+*** scan problem
 
-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:
+must call with point at the start of a problem. moves the point to the
+end of the problem. 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.
+~var~ is a variable name if there is an assignment, otherwise it is a
+placeholder like ~_0~, ~_1~, etc.
 
 ~start-marker~ and ~end-marker~ are markers in the (temp) buffer.
 
+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 later.
+
 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
+This uses the peg package to parse the problem. Instead of using the
+peg return value we build the list of fields outside of the peg stack.
+
+~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
+~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
+#+begin_src elisp :tangle mathsheet.el
   (defun ianxm/scan-problem ()
-    (interactive)
+    "Scan problem"
     (let ((field-index 0)
           open-fields ; stack (open close (vars) deps)
           closed-fields) ; list (open close (vars) deps)
@@ -135,14 +135,16 @@ new field to the list when we close the current field.
 
 *** 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.
+This must be called with point at the start of a field. This moves the
+point to the end of the field. This returns a list containing the
+value to which the field reduces.
+
+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.
 
-#+name: reduce-field
-#+begin_src elisp
+#+begin_src elisp :tangle mathsheet.el
   (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))
@@ -163,8 +165,7 @@ reduces.
                    `(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")))
+         (parenthetical "(" expression ")")
          (var-lhs (substring letter)) ; var for assignment
          (var-rhs "$" (substring letter) ; var for use
                   `(v -- (let ((val (alist-get (intern v) var-list)))
@@ -180,10 +181,10 @@ reduces.
 
 *** replace field
 
-replace a field with the value returned from processing it.
+Replace a field with the value returned from processing it.
 
 #+name: replace-field
-#+begin_src elisp
+#+begin_src elisp :tangle mathsheet.el
   (defun ianxm/replace-field (node)
     (let ((start (caddr node))
           (end (1+ (cadddr node)))
@@ -201,7 +202,7 @@ replace a field with the value returned from processing it.
 check dependencies then visit the node
 
 #+name: dfs-visit
-#+begin_src elisp
+#+begin_src elisp :tangle mathsheet.el
   (defun ianxm/dfs-visit (node fields)
     (pcase (nth 4 node)
       (1 (error "cycle detected")) ; cycle
@@ -225,7 +226,7 @@ processes all fields in a problem.
 #+end_example
 
 #+name: fill-problem
-#+begin_src elisp :var full-problem="[$a + 2 + [a=1..5]]"
+#+begin_src elisp :tangle mathsheet.el
   (defun ianxm/fill-problem (full-problem)
       (interactive)
       (let (fields)
@@ -278,27 +279,6 @@ other examples
   [0..[$a..$b]]
 #+end_example
 
-*** 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>>
-
-  <<generate-problems>>
-#+end_src
 ** generate problem set from templates
 
 1. load table
@@ -313,9 +293,18 @@ tangles everything needed to convert a template to a 
problem
 4. loop through list, replacing entry with '(problem . solution)
 
 #+name: generate-problems
-#+begin_src elisp :results table :var templates=firstset
-  (defun ianxm/generate-problems ()
-    (let (total-weight problems)
+#+begin_src elisp :tangle mathsheet.el
+  (defun ianxm/generate-problems (template-name count)
+    (let (total-weight templates problems)
+      (save-excursion
+        (goto-char (point-min))
+        (search-forward-regexp (org-babel-named-data-regexp-for-name 
template-name) nil t)
+        ;; read table from buffer, drop header, convert fields to numbers or 
strings
+        (setq templates (mapcar
+                         (lambda (row) (list (string-to-number (nth 0 row))
+                                             (string-to-number (nth 1 row))
+                                             (substring-no-properties (nth 2 
row))))
+                         (seq-drop (org-table-to-lisp) 2))))
       ;; sort by weight (low to high)
       (setq templates (sort templates (lambda (a b) (< (car a) (car b))))
             ;; calc total weight
@@ -325,26 +314,23 @@ tangles everything needed to convert a template to a 
problem
                                       0)))
       ;; calculate number for each row
       (dotimes (ii (length templates) problems)
-        (let* (problem answer
-                       (item (nth ii templates))
-                       (weight (car item))
-                       (needed (cond
-                                ((= weight 0)
-                                 0)
-                                ((= ii (1- (length templates)))
-                                 (- problem-count (length problems)))
-                                (t
-                                 (max (round (* (/ weight total-weight) 
problem-count) ) 1)))))
-
-          ;; add just problems to list?
-          ;; dedup each one
-          ;; add until "needed" are kept
+        (let* ((item (nth ii templates))
+               (weight (car item))
+               (needed (cond ; number of problems to add for this template
+                        ((= weight 0)
+                         0)
+                        ((= ii (1- (length templates)))
+                         (- count (length problems)))
+                        (t
+                         (max (round (* (/ weight total-weight) count) ) 1))))
+               problem answer)
+
           (let ((added 0)
                 problem-set
                 problem)
-            (while (< added needed)
+            (while (< added needed) ; add until "needed" are kept
               (setq problem (ianxm/fill-problem (caddr item)))
-              (when (not (member problem problem-set))
+              (when (not (member problem problem-set)) ; dedup problems
                 (push problem problem-set)
                 (push (list problem (calc-eval problem) (cadr item)) problems)
                 (setq added (1+ added)))))))
@@ -358,69 +344,93 @@ tangles everything needed to convert a template to a 
problem
       ;; sort by order
       (sort problems (lambda (a b) (< (caddr a) (caddr b))))
 
-      ;; return
-      problems))
+      ;; return problems and answers, drop header
+      (mapcar
+       (lambda (x) (seq-take x 2))
+       problems)))
 #+end_src
 
 test with this
 
 #+name: problem-set
-#+begin_src elisp :results table :noweb yes :var templates=firstset
+#+begin_src elisp :results table :noweb yes
   <<full>>
 
-  (ianxm/generate-problems)
+  (ianxm/generate-problems "firstset" 20)
 #+end_src
 
 #+RESULTS: problem-set
-| 9 + 9     | 18 | 1 |
-| 4 + 3     |  7 | 1 |
-| 6 + 9     | 15 | 1 |
-| 5 + 4     |  9 | 1 |
-| 1 + 4     |  5 | 1 |
-| 3 + 7     | 10 | 1 |
-| 4 + 7     | 11 | 1 |
-| 8 + 3     | 11 | 1 |
-| 2 + 0     |  2 | 1 |
-| 5 + 5     | 10 | 1 |
-| 9 + 3     | 12 | 1 |
-| 8 + 10    | 18 | 2 |
-| 6 + 10    | 16 | 2 |
-| 6 - 4     |  2 | 2 |
-| 7 + 10    | 17 | 2 |
-| 4 + 13    | 17 | 2 |
-| 6 + 14    | 20 | 2 |
-| 7 - 3     |  4 | 2 |
-| 8 - 6     |  2 | 2 |
-| 2 + 11    | 13 | 2 |
-| 9 + 1 + 4 | 14 | 3 |
-| 3 + 1 + 4 |  8 | 3 |
-| 1 + 1 + 4 |  6 | 3 |
-| 2 + 7 - 1 |  8 | 4 |
-| 9 + 1 - 1 |  9 | 4 |
-| 9 + 1 - 4 |  6 | 4 |
-
-** lay out problems and answers
-this generates a problem set.
-
-#+name: layout-problems-answers
-#+begin_src elisp :results silent :noweb yes :var problem-set=problem-set 
problemsp='t
-      (with-temp-buffer
-          (dolist (row problem-set)
-            (if problemsp
-                (insert (format"\\CircledItem %s = 
\\rule[-.2\\baselineskip]{2cm}{0.4pt}\n\n"
-                               (car row)))
-              (insert (format "\\CircledItem %s\n\n"
-                              (cadr row)))))
-        (buffer-string))
+| 1 + 6     |  7 |
+| 4 + 0     |  4 |
+| 1 + 3     |  4 |
+| 3 + 9     | 12 |
+| 2 + 8     | 10 |
+| 9 + 5     | 14 |
+| 2 + 5     |  7 |
+| 3 + 5     |  8 |
+| 7 + 8     | 15 |
+| 6 + 13    | 19 |
+| 4 + 13    | 17 |
+| 8 + 14    | 22 |
+| 9 + 11    | 20 |
+| 5 + 13    | 18 |
+| 6 - 0     |  6 |
+| 5 - 1     |  4 |
+| 9 + 6 + 2 | 17 |
+| 4 + 5 + 2 | 11 |
+| 6 + 6 - 2 | 10 |
+| 2 + 8 - 0 | 10 |
+
+** update problem-set block
+
+This generates a problem set and writes it to the dynamic block. This
+is triggered by C-c C-c on the dynamic block header.
+
+~params~ is a property list of params on the block header line
+I need to extract the values
+
+- :templates :: templates
+- :count :: 10
+
+#+begin_src elisp :tangle mathsheet.el
+  (defun org-dblock-write:problem-set (params)
+    "Update problem-set block and optionally write a worksheet."
+
+    ;; write the table header
+    (insert "| problem | answer |\n")
+    (insert "|-\n")
+
+    ;; generate problem set
+    (let ((problems (ianxm/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|"
+                                  (car problem)
+                                  (cadr problem)))
+        problems
+        "\n"))
+
+      ;; align table
+      (org-table-align)
+
+      ;; should we generate the sheet?
+      (when (y-or-n-p "Write worksheet? ")
+        (ianxm/gen-worksheet
+         problems))))
 #+end_src
 
+* create worksheet
 ** lay out page
 this wraps the problems with a tex header and footer.
 
 solution for how to enumerate with circled numbers from 
[[https://latex.org/forum/viewtopic.php?p=40006&sid=d202f756313add2391c3140fbeafe2ff#p40006][here]]
 
 #+name: page
-#+begin_src latex :results value silent :noweb yes
+#+begin_src latex :results value silent :var student="Noble"
   \documentclass[12pt]{article}
   \usepackage[top=1in, bottom=0.8in, left=0.8in, right=0.8in]{geometry}
   \usepackage{fancyhdr}
@@ -436,7 +446,7 @@ solution for how to enumerate with circled numbers from 
[[https://latex.org/foru
     \stepcounter{enumi}\item[\circled{\theenumi}]}
 
   \pagestyle{fancy}
-  \lhead{\textmd{\textsf{Name: student}}}
+  \lhead{\textmd{\textsf{Name: }}}
   \rhead{\textmd{\textsf{Date: \today}}}
   \cfoot{}
 
@@ -446,10 +456,12 @@ solution for how to enumerate with circled numbers from 
[[https://latex.org/foru
 
     \begin{multicols}{2}
       \begin{enumerate}[itemsep=0.5cm]
-        <<layout-problems-answers(problemsp='t)>>
+        <<problems>>
       \end{enumerate}
     \end{multicols}
 
+    \vspace*{\fill}
+
     \vspace*{0.1cm}
     \noindent\rule{\linewidth}{0.4pt}
     \vspace*{0.1cm}
@@ -460,7 +472,7 @@ solution for how to enumerate with circled numbers from 
[[https://latex.org/foru
         \footnotesize
         \begin{multicols}{4}
           \begin{enumerate}
-            <<layout-problems-answers(problemsp='nil)>>
+            <<answers>>
           \end{enumerate}
         \end{multicols}
       \end{minipage}
@@ -469,13 +481,26 @@ solution for how to enumerate with circled numbers from 
[[https://latex.org/foru
   \end{document}
 #+end_src
 
-* generate pdf
+** generate pdf
 this writes the generated into a local file and runs ~texi2pdf~ to
 convert it to a pdf.
 
-#+begin_src elisp :results silent :var tex-content=page
-  (with-temp-file "worksheet.tex"
-    (insert tex-content))
-  (shell-command "texi2pdf worksheet.tex"
-                 (get-buffer-create "*Standard output*"))))
+#+begin_src elisp :results silent :tangle mathsheet.el
+  (defun ianxm/gen-worksheet (problems)
+    (with-temp-file "worksheet.tex"
+      (insert ianxm/worksheet-template)
+      (goto-char (point-min))
+      (search-forward "<<problems>>")
+      (replace-match "")
+      (dolist (row problems)
+        (insert (format"\\CircledItem %s = 
\\rule[-.2\\baselineskip]{2cm}{0.4pt}\n\n"
+                       (car row))))
+      (goto-char (point-min))
+      (search-forward "<<answers>>")
+      (replace-match "")
+      (dolist (row problems)
+        (insert (format "\\CircledItem %s\n\n"
+                        (cadr row)))))
+    (shell-command "texi2pdf worksheet.tex"
+                   (get-buffer-create "*Standard output*")))
 #+end_src

Reply via email to