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

    Wrote more documentation and added example worksheets
---
 .gitignore       |    5 +
 add-sub-1.pdf    |  Bin 0 -> 34623 bytes
 algebra-1.pdf    |  Bin 0 -> 75836 bytes
 example.org      |   16 +-
 mathsheet.org    |  263 ++++++++-----
 org-mathsheet.el |  423 ++++++++++++++++++++
 readme.md        | 1152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 1754 insertions(+), 105 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..f145f51ef0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.elc
+*.aux
+*.log
+*.pdf
+*.tex
diff --git a/add-sub-1.pdf b/add-sub-1.pdf
new file mode 100644
index 0000000000..0264fb0423
Binary files /dev/null and b/add-sub-1.pdf differ
diff --git a/algebra-1.pdf b/algebra-1.pdf
new file mode 100644
index 0000000000..e96f5acb7e
Binary files /dev/null and b/algebra-1.pdf differ
diff --git a/example.org b/example.org
index 5aae6f8a69..bdc48d12ef 100644
--- a/example.org
+++ b/example.org
@@ -53,12 +53,12 @@
 #+BEGIN: problem-set :templates "algebra-1" :count 8 :prob-cols 2 :instruction 
"Solve for x"
 | problem            | answer  |
 |--------------------+---------|
-| x / (3 + 0) = 4    | x = 12  |
-| x / (2 + 2) = 5    | x = 20  |
-| 6 / x = 2          | x = 3   |
-| 7 / x = 1          | x = 7   |
-| x = x/3 + 3        | x = 9/2 |
-| x/-2 + 1 = 7       | x = -12 |
-| x^2 = sqrt(15 + 1) | x = 2   |
-| x/-5 + 6 = 6       | x = 0   |
+| x / (3 + 4) = 5    | x = 35  |
+| x / (2 + 4) = 7    | x = 42  |
+| 9 / x = 1          | x = 9   |
+| 12 / x = 2         | x = 6   |
+| x^2 = sqrt(13 + 3) | x = 2   |
+| x/3 + 6 = -8       | x = -42 |
+| x = x/5 + 0        | x = 0   |
+| x/-2 + 9 = 4       | x = 10  |
 #+END:
diff --git a/mathsheet.org b/mathsheet.org
index 12634096c8..68d6ae2f93 100644
--- a/mathsheet.org
+++ b/mathsheet.org
@@ -1,4 +1,7 @@
-* TODO rename to org-mathsheet.el
+# -*- eval: (add-hook 'after-save-hook (lambda () (save-excursion 
(org-babel-tangle) (org-export-to-file 'md "readme.md"))) nil 'local); -*-
+#+title: Math Worksheet Generator
+#+author: Ian Martins
+#+email: ia...@jhu.edu
 * Overview
 ** Description
 This is a math worksheet generator. The worksheets are randomly
@@ -7,11 +10,11 @@ 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
+someone else. It could be useful for a teacher, tutor, homeschooling
 parent, or any parent.
 ** Examples
 Here are some example worksheets generated by this tool:
-1. [[file:add-sub-1.pdf][arithmatic]]
+1. [[file:add-sub-1.pdf][arithmetic]]
 2. [[file:algebra-1.pdf][algebra]]
 
 They were generated using [[file:example.org][this configuration]].
@@ -42,7 +45,7 @@ inclusive, so the above template could result in problems 
like these:
 5 + 1
 #+end_example
 **** Equation Templates
-In additon to expressions where the answer is a number, templates can
+In addition 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
@@ -55,15 +58,15 @@ This can produce the following problems:
 1 x + 8 = -3
 #+end_example
 **** Field Rules
-These are the field rules:
+These are the different ways fields can be defined:
 - [-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
 - [round(sin(0.3))] :: expressions can use math functions
-- [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
+- [a=...] :: assign the variable ~a~ to the number chosen for this field
+- [-2..$a] :: any number from -2 to the value another field assigned
+  to ~a~
 - [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
@@ -86,7 +89,7 @@ Addition and subtraction, but ensure a positive result
 [a=1..10] + [b=0..10] - [0..($a+$b)]
 #+end_example
 
-Division and ensure we don't divide by zero
+Division but ensure we don't divide by zero
 #+begin_example
 [-10..10] / [-5..-1,1..5]
 #+end_example
@@ -98,7 +101,7 @@ a worksheet, each worksheet is configured with a set of 
templates in a
 templates table. For example
 
 #+name: first-sheet
-| weight | order | template            | descr                  |
+| weight | order | template            | description            |
 |--------+-------+---------------------+------------------------|
 |      3 |     1 | [1..10] + [1..20]   | addition               |
 |      1 |     2 | [a=1..10] - [0..$a] | subtraction above zero |
@@ -108,21 +111,21 @@ The table contains the following columns:
   the worksheet. A weight of zero means the template will not be
   used. For ~first-sheet~ three out of four of the worksheet problems
   will be addition.
-- order :: Troblems are ordered on the sheet in ascending order. Two
+- order :: Problems are ordered on the sheet in ascending order. Two
   problems with the same order will be intermingled. For ~first-sheet~
   all of the addition problems will come first.
 - template :: this is the template used to generate problems of this
   type.
-- descr :: This column is just for your notes. It is not used in
+- description :: This column is just for your notes. It is not used in
   worksheet generation.
 
 Also notice that the table is assigned a name. That name will be used
 to refer to it later.
-**** Examples
+**** Example
 Here is another example template table.
 
 #+name: second-sheet
-| weight | order | template                      | descr                       
 |
+| weight | order | template                      | description                 
 |
 
|--------+-------+-------------------------------+------------------------------|
 |      3 |     1 | [1..10] + [0..10]             | simple                      
 |
 |      2 |     2 | [1..10] + [8..15]             | second number bigger        
 |
@@ -130,7 +133,7 @@ Here is another example template table.
 |      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
+*** The Problem-Set Block
 **** Overview
 The second thing needed to generate a mathsheet is an 
[[https://orgmode.org/manual/Dynamic-Blocks.html][org dynamic
 block]]. Here is an example:
@@ -146,64 +149,104 @@ The block name must be ~problem-set~ and it must specify 
the following parameter
   of the sheet to guide the student
 
 @@html:<kbd>@@C-c@@html:</kbd>@@ @@html:<kbd>@@C-c@@html:</kbd>@@ on
-the block ~BEGIN~ line or ~END~ line will trigger mathsheet to generate a
-new set of problems. The new problems and answers will be written to a
-table in the body of the dynamic block, and you will have the option
-(via a yes or no prompt in the mini bar) to write those problems to a
-PDF. On "yes", mathsheet will write a PDF to a file named by the
-template table name. If an existing file exists it will be
-overwritten. On "no", nothing will be written.
+the block ~BEGIN~ line or ~END~ line will trigger org-mathsheet to
+generate a new set of problems. The new problems and answers will be
+written to a table in the body of the dynamic block, and you will have
+the option (via a yes/no prompt in the mini bar) to write those
+problems to a PDF. On "yes", org-mathsheet will write a PDF to a file
+named by the template table name. If an existing file exists it will
+be overwritten. On "no", nothing will be written.
+**** Example
+This is an example problem-set block.
+
+#+BEGIN: problem-set :templates "algebra-1" :count 8 :prob-cols 2 :instruction 
"Solve for x"
+#+END:
 
 * Code walkthrough
 ** Problem generation
 *** Header
-#+begin_src elisp :tangle mathsheet.el
-;;; mathsheet.el --- Generate dynamic math worksheets  -*- lexical-binding:t 
-*-
+This is the standard emacs package header.
+
+#+begin_src elisp :tangle org-mathsheet.el
+  ;;; org-mathsheet.el --- Generate dynamic math worksheets  -*- 
lexical-binding:t -*-
+
+  ;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+  ;; Author: Ian Martins <ia...@jhu.edu>
+  ;; Keywords: tools, education, math
+  ;; Homepage: https://gitlab.com/ianxm/org-mathsheet
+  ;; Version: 1.0
+  ;; Package-Requires: ((peg "1.0"))
+
+  ;; This file is not part of GNU Emacs.
+
+  ;; GNU Emacs is free software: you can redistribute it and/or modify
+  ;; it under the terms of the GNU General Public License as published by
+  ;; the Free Software Foundation, either version 3 of the License, or
+  ;; (at your option) any later version.
+
+  ;; GNU Emacs is distributed in the hope that it will be useful,
+  ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ;; GNU General Public License for more details.
+
+  ;; You should have received a copy of the GNU General Public License
+  ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+  ;;; Commentary:
+
+  ;; This package generates dynamic math worksheets. The types and
+  ;; distribution of problems is highly customizable. Problem sets are
+  ;; defined in org tables, generated in dynamic blocks for review, and
+  ;; exported to PDF for printing.
+
+  ;;; Code:
 #+end_src
 
 *** Dependencies
 This package needs [[https://elpa.gnu.org/packages/peg.html][peg]].
 
-#+begin_src elisp :tangle mathsheet.el
+#+begin_src elisp :tangle org-mathsheet.el
   (require 'peg)
 #+end_src
 
 *** Variables
-Need ~mathsheet--var-list~ to keep track of the variables between fields.
+We need ~org-mathsheet--var-list~ to keep track of the variables between 
fields.
 
-~worksheet-template~ is the LaTeX template for the worksheet.
+~org-mathsheet--worksheet-template~ is the LaTeX template for the
+worksheet, which is defined in a LaTeX source block below. This
+assigns the constant directly to that named block.
 
 #+name: variables
-#+begin_src elisp :tangle mathsheet.el :var page=page
-  (defvar mathsheet--var-list '()
+#+begin_src elisp :tangle org-mathsheet.el :var page=page
+  (defvar org-mathsheet--var-list '()
     "List of variables used in a problem")
 
-  (defconst mathsheet--worksheet-template page
+  (defconst org-mathsheet--worksheet-template page
     "LaTeX template for the worksheet")
 #+end_src
 *** Scan problem
 
-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
-
-change to
+This scans a problem to find the locations of fields and dependencies
+between them. It must be called with point at the start of the
+problem. It moves the point to the end of the problem unless there's
+an error, in which case it stops at the place where the error
+occurred. This returns a list of fields, with each field formatted as:
 
 #+begin_example
 '(asn-var (deps) (start-marker . end-marker) nil)
 #+end_example
 
-~asn-var~ is a variable name if there is an assignment, otherwise it is a
-placeholder like ~_0~, ~_1~, etc. ~asn-var~ must be interned and must
-be the first index since we use this list as an alist later.
+~asn-var~ is a variable name if this field is being assigned to a
+variable, otherwise it is a placeholder like ~_0~, ~_1~, etc. ~asn-var~ must
+be interned and must be the first index since we use this list as an
+alist later.
 
-~alg-vars~ are algebraic variables if there are any in this problem,
-otherwise ~nil~.
+~deps~ is a list of are dependencies if this field has any, otherwise
+~nil~. Dependencies could be variables or placeholders.
 
-~start-marker~ and ~end-marker~ are markers in the (temp) buffer.
+~start-marker~ and ~end-marker~ are markers in the (temp) buffer. The
+~end-marker~ is configured to insert text before the marker.
 
 The last entry is ~nil~ for "not visited." It is used by ~dfs-visit~.
 
@@ -213,17 +256,19 @@ for example:
                        '((: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
-peg return value we build the list of fields outside of the peg stack.
+This uses peg 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
-new field to the list when we close the current field.
+new field to the list when we close the current field, taking it off
+of ~open-fields~.
 
 #+name: scan-problem
-#+begin_src elisp :tangle mathsheet.el
-  (defun mathsheet--scan-problem ()
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-mathsheet--scan-problem ()
     "Scan a problem.
 
   This parses the problem and produces a list containing info about
@@ -293,7 +338,7 @@ test scan
   (with-temp-buffer
     (insert "[0..4,6-9,11] * x + [floor([-10..10]/3)] = [-10..10]")
     (goto-char (point-min))
-    (mathsheet--scan-problem))
+    (org-mathsheet--scan-problem))
 #+end_src
 
 #+RESULTS:
@@ -302,24 +347,24 @@ test scan
 *** Reduce field
 
 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. It is wrapped in a list because
-~peg-run~ returns its stack and the value is the last thing remaining on
-the stack when peg completes.
+point to the end of the field. This returns the value to which the
+field reduces. ~peg-run~ returns its stack and the value is the last
+thing remaining on the stack when peg completes so peg returns a list
+with one value. We take the value out of the list and return it.
 
 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
+We use ~..~ instead 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 mathsheet--reduce-field ()
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-mathsheet--reduce-field ()
     "Reduce the field to a number.
 
   Parse the field again, replacing spans with random numbers and
@@ -340,7 +385,7 @@ be ambiguous:
          (operation (substring (or "+" "-" "*" "/")))
          (assignment var-lhs space "=" space (or range sequence)
                      `(v r -- (progn
-                                (push (cons (intern v) r) mathsheet--var-list)
+                                (push (cons (intern v) r) 
org-mathsheet--var-list)
                                 r)))
          (sequence (list (or range value) (* "," space (or range value)))
                    `(vals -- (seq-random-elt vals)))
@@ -353,7 +398,7 @@ be ambiguous:
          (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) 
mathsheet--var-list)))
+                  `(v -- (let ((val (alist-get (intern v) 
org-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
@@ -377,7 +422,7 @@ test with
     ;(insert "[1..10,15..20,50]")
     (insert "[1..10]")
       (goto-char (point-min))
-      (mathsheet--reduce-field))
+      (org-mathsheet--reduce-field))
 #+end_src
 
 #+RESULTS:
@@ -385,11 +430,13 @@ test with
 
 *** Replace field
 
-Replace a field with the value returned from reducing it.
+Replace a field with the value returned from reducing it. This uses
+~org-mathsheet--reduce-field~ to determine the value to use in place of
+the field.
 
 #+name: replace-field
-#+begin_src elisp :tangle mathsheet.el
-  (defun mathsheet--replace-field (node)
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-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
@@ -400,7 +447,7 @@ Replace a field with the value returned from reducing it.
           val)
       (goto-char start)
       (when (looking-at "\\[")
-        (setq val (mathsheet--reduce-field))
+        (setq val (org-mathsheet--reduce-field))
         (goto-char start)
         (delete-char (- end start) t)
         (insert (number-to-string val)))))
@@ -409,12 +456,13 @@ Replace a field with the value returned from reducing it.
 *** DFS visit
 
 This uses a depth first search to ensure that we visit (reduce and
-replace) the fields in dependency order. Check dependencies then visit
-the node.
+replace) the fields in dependency order. We check dependencies then
+visit the node. We use the last field in the field structure to keep
+track of which fields have been visited.
 
 #+name: dfs-visit
-#+begin_src elisp :tangle mathsheet.el
-  (defun mathsheet--dfs-visit (node fields)
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-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
@@ -427,10 +475,10 @@ the node.
        (setcar (cdddr node) 1)     ; started
        (let ((deps (cadr node)))
          (dolist (dep deps)
-           (mathsheet--dfs-visit
+           (org-mathsheet--dfs-visit
             (assq dep fields)
             fields)))
-       (mathsheet--replace-field node) ; visit
+       (org-mathsheet--replace-field node) ; visit
        (setcar (cdddr node) 2)))) ; mark done
 #+end_src
 
@@ -442,8 +490,8 @@ processes all fields in a problem.
 (full-problem (buffer-substring (point-at-bol) (point-at-eol)))
 #+end_example
 
-#+begin_src elisp :tangle mathsheet.el
-  (defun mathsheet--fill-problem (full-problem)
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-mathsheet--fill-problem (full-problem)
     "Replace all fields in FULL-PROBLEM
 
   Goes through all fields in the given problem in dependency order
@@ -455,14 +503,14 @@ processes all fields in a problem.
         (goto-char (point-min))
 
         ;; find fields, assignment variables, algebraic variables, dependencies
-        (let* ((scan-ret (mathsheet--scan-problem))
+        (let* ((scan-ret (org-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)
-            (mathsheet--dfs-visit node fields))
-          (setq mathsheet--var-list '())
+            (org-mathsheet--dfs-visit node fields))
+          (setq org-mathsheet--var-list '())
 
           ;; return filled problem
           `((:problem . ,(buffer-string))
@@ -477,11 +525,11 @@ test with this
   <<replace-field>>
   <<dfs-visit>>
 
-  (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]]")
+  (org-mathsheet--fill-problem "[1..12] + [1,4,6,10]")
+  ;;(org-mathsheet--fill-problem "[1..[2..[10..100]]]")
+  ;;(org-mathsheet--fill-problem "[$a*[1..10]] / [a=1..10]")
+  ;;(org-mathsheet--fill-problem "[$a]/(3+[a=1..5])")
+  ;; (org-mathsheet--fill-problem "1/x + 2 = [-10..[10..20]]")
 
 #+end_src
 
@@ -518,8 +566,8 @@ each template sequentially. In order to mix them up we 
shuffle the
 whole set and then reorder by ~order~.
 
 #+name: generate-problems
-#+begin_src elisp :tangle mathsheet.el
-  (defun mathsheet--generate-problems (template-name count)
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-mathsheet--generate-problems (template-name count)
     "Use templates from TEMPLATE-NAME to generate COUNT problems
 
   Generate problems and answers based on what is defined in the
@@ -561,7 +609,7 @@ whole set and then reorder by ~order~.
                (dup-count 0)
                problem-set)
           (while (< added needed) ; add until "needed" are kept
-            (let* ((fill-ret (mathsheet--fill-problem (caddr item)))
+            (let* ((fill-ret (org-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)
@@ -614,7 +662,12 @@ First we generate the problems and answers, then we write 
them out to
 a table in the dynamic block, finally, if the user wants it, we
 generate a PDF with these problems.
 
-#+begin_src elisp :tangle mathsheet.el
+The reason for the yes/no prompt is to allow you to see the problem
+set that was generated to decide if you want to use it or generate
+another.
+
+#+begin_src elisp :tangle org-mathsheet.el
+  ;;;###autoload
   (defun org-dblock-write:problem-set (params)
     "Update problem-set block and optionally write a worksheet.
 
@@ -630,7 +683,7 @@ generate a PDF with these problems.
     (insert "|-\n")
 
     ;; generate problem set
-    (let ((problems (mathsheet--generate-problems
+    (let ((problems (org-mathsheet--generate-problems
                      (plist-get params :templates)
                      (plist-get params :count))))
 
@@ -648,7 +701,7 @@ generate a PDF with these problems.
 
       ;; should we generate the sheet?
       (when (y-or-n-p "Write worksheet? ")
-        (mathsheet--gen-worksheet
+        (org-mathsheet--gen-worksheet
          (plist-get params :templates)
          (plist-get params :instruction)
          problems
@@ -659,9 +712,9 @@ generate a PDF with these problems.
 *** Lay out page
 This wraps the problems with a tex header and footer.
 
-This template doen't use noweb but it uses noweb syntax (~<<label>>~) to
-mark where mathsheet will insert content. It's not possible actually
-use noweb here since this template must be tangled to mathsheet.el as
+This template doesn't use noweb but it uses noweb syntax (~<<label>>~) to
+mark where org-mathsheet will insert content. It's not possible actually
+use noweb here since this template must be tangled to org-mathsheet.el as
 a template.
 
 I found the solution for how to enumerate with circled numbers 
[[https://latex.org/forum/viewtopic.php?p=40006&sid=d202f756313add2391c3140fbeafe2ff#p40006][here]].
@@ -710,8 +763,8 @@ written to a PDF we convert them to latex. emacs calc 
already knows
 how to convert between formats, so we let it do it.
 
 #+name: convert-to-latex
-#+begin_src elisp :tangle mathsheet.el
-  (defun mathsheet--convert-to-latex (expr)
+#+begin_src elisp :tangle org-mathsheet.el
+  (defun org-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
@@ -729,15 +782,15 @@ PDF. We save it as ~[template-name].tex~ and the final 
worksheet is
 named ~[template-name].pdf~. Each execution with the same template name
 will overwrite the same file.
 
-#+begin_src elisp :results silent :tangle mathsheet.el
-  (defun mathsheet--gen-worksheet (file-name instruction problems prob-cols)
+#+begin_src elisp :results silent :tangle org-mathsheet.el
+  (defun org-mathsheet--gen-worksheet (file-name instruction problems 
prob-cols)
     "Generate a worksheet with PROBLEMS.
 
   Write a file named FILE-NAME. Include the INSTRUCTION line at the
   top. The problems will be arranged in PROB-COLS columns. The
   answers will be in 4 columns."
     (with-temp-file (concat file-name ".tex")
-      (insert mathsheet--worksheet-template)
+      (insert org-mathsheet--worksheet-template)
 
       (goto-char (point-min))
       (search-forward "<<instruction>>")
@@ -753,9 +806,9 @@ will overwrite the same file.
           (dolist (row group)
             (if (cadddr row)
                 (insert (format"\\question %s\n"
-                               (mathsheet--convert-to-latex (car row))))
+                               (org-mathsheet--convert-to-latex (car row))))
               (insert (format"\\question %s = 
\\rule[-.2\\baselineskip]{2cm}{0.4pt}\n"
-                             (mathsheet--convert-to-latex (car row))))))
+                             (org-mathsheet--convert-to-latex (car row))))))
           (insert "\\end{multicols}\n")
           (insert "\\vspace{\\stretch{1}}\n"))
 
@@ -766,8 +819,24 @@ will overwrite the same file.
           (insert (format "\\begin{multicols}{%s}\n" answ-cols))
           (dolist (row group)
             (insert (format "\\question %s\n"
-                            (mathsheet--convert-to-latex (cadr row)))))
+                            (org-mathsheet--convert-to-latex (cadr row)))))
           (insert "\\end{multicols}\n"))))
     (shell-command (concat "texi2pdf " file-name ".tex")
                    (get-buffer-create "*Standard output*")))
 #+end_src
+*** Footer
+#+begin_src elisp :tangle org-mathsheet.el
+(provide 'org-mathsheet)
+
+;;; org-mathsheet.el ends here
+#+end_src
+
+* Literate Programming
+This is written as a 
[[https://en.wikipedia.org/wiki/Literate_programming][literate program]] using 
[[https://orgmode.org/][emacs org-mode]].  [[file:gimp-comic-book.org][The org
+file]] contains the code and documentation for the math worksheet
+generation script.  When this file is saved, the source code is
+generated using =org-babel-tangle= and the readme is generated using
+=org-md-export-to-file=.
+
+The first line of [[file:gimp-comic-book.org][the org file]] configures emacs 
to run those commands
+whenever this file is saved, which generates the scripts and readme.
diff --git a/org-mathsheet.el b/org-mathsheet.el
new file mode 100644
index 0000000000..67ed7633d8
--- /dev/null
+++ b/org-mathsheet.el
@@ -0,0 +1,423 @@
+;;; org-mathsheet.el --- Generate dynamic math worksheets  -*- 
lexical-binding:t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Ian Martins <ia...@jhu.edu>
+;; Keywords: tools, education, math
+;; Homepage: https://gitlab.com/ianxm/org-mathsheet
+;; Version: 1.0
+;; Package-Requires: ((peg "1.0"))
+
+;; This file is not part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package generates dynamic math worksheets. The types and
+;; distribution of problems is highly customizable. Problem sets are
+;; defined in org tables, generated in dynamic blocks for review, and
+;; exported to PDF for printing.
+
+;;; Code:
+
+(require 'peg)
+
+(let ((page '"\\documentclass[12pt]{exam}
+\\usepackage[top=1in, bottom=0.5in, left=0.8in, right=0.8in]{geometry}
+\\usepackage{multicol}
+\\usepackage{rotating}
+\\usepackage{xcolor}
+
+\\pagestyle{head}
+\\header{Name:\\enspace\\makebox[2.2in]{\\hrulefill}}{}{Date:\\enspace\\makebox[2.2in]{\\hrulefill}}
+
+\\begin{document}
+
+  \\noindent <<instruction>>
+
+  \\begin{questions}
+    <<problems>>
+  \\end{questions}
+
+  \\vspace*{\\fill}
+
+  \\vspace*{0.1cm}
+  \\noindent\\rule{\\linewidth}{0.4pt}
+  \\vspace*{0.1cm}
+
+  \\begin{turn}{180}
+    \\begin{minipage}{\\linewidth}
+      \\color{gray}
+      \\footnotesize
+      \\begin{questions}
+        <<answers>>
+      \\end{questions}
+    \\end{minipage}
+  \\end{turn}
+
+\\end{document}"))
+(defvar org-mathsheet--var-list '()
+  "List of variables used in a problem")
+
+(defconst org-mathsheet--worksheet-template page
+  "LaTeX template for the worksheet")
+)
+
+(defun org-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
+        alg-vars)
+
+    (with-peg-rules
+        ((stuff (* (or asn-var math-func alg-var digit symbol field space)))
+         (field open (opt assignment) stuff close)
+         (space (* [space]))
+         (open (region "[")
+               `(l _ -- (progn
+                          (push (list
+                                 (intern (concat "_" (number-to-string 
field-index))) ; asn-var
+                                 nil ; deps
+                                 (cons (copy-marker l) nil) ; start and end 
markers
+                                 nil) ; not visited
+                                open-fields)
+                          (setq field-index (1+ field-index))
+                          ".")))
+         (assignment (substring letter) "="
+                     `(v -- (progn
+                              (setcar
+                               (car open-fields)
+                               (intern v))
+                              ".")))
+         (asn-var "$" (substring letter)
+                  `(v -- (progn
+                           (push (intern v) (cadar open-fields))
+                           ".")))
+         (alg-var (substring letter)
+                  `(v -- (progn
+                           (push v alg-vars)
+                           ".")))
+         (close (region "]")
+                `(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" "sin" "cos" "tan" "asin" "acos" "atan" "floor" 
"ceil" "round"))
+         (letter [a-z])
+         (digit [0-9])
+         (symbol (or "." "," "+" "-" "*" "/" "^" "(" ")" "=")))
+
+      (peg-run (peg stuff)
+               (lambda (x) (message "failed %s" x))
+               (lambda (x)
+                 (funcall x)
+                 `((:fields . ,closed-fields)
+                   (:alg-vars . ,alg-vars)))))))
+
+(defun org-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 math-func expression sequence assignment value) 
space "]")
+       (expression (list value space operation space value (* space operation 
space value))
+                   `(vals -- (string-to-number
+                              (calc-eval
+                               (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) 
org-mathsheet--var-list)
+                              r)))
+       (sequence (list (or range value) (* "," space (or range value)))
+                 `(vals -- (seq-random-elt vals)))
+       (range value ".." value
+              `(min max -- (if (>= min max)
+                               (error "Range bounds must be increasing")
+                             (+ (random (- max min)) min))))
+       (value (or (substring (opt "-") (+ digit)) var-rhs parenthetical)
+              `(v -- (if (stringp v) (string-to-number v) v)))
+       (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) 
org-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) (car (funcall x))))))
+
+(defun org-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 (org-mathsheet--reduce-field))
+      (goto-char start)
+      (delete-char (- end start) t)
+      (insert (number-to-string val)))))
+
+(defun org-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
+    (_                           ; process
+     (setcar (cdddr node) 1)     ; started
+     (let ((deps (cadr node)))
+       (dolist (dep deps)
+         (org-mathsheet--dfs-visit
+          (assq dep fields)
+          fields)))
+     (org-mathsheet--replace-field node) ; visit
+     (setcar (cdddr node) 2)))) ; mark done
+
+(defun org-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)
+      (goto-char (point-min))
+
+      ;; find fields, assignment variables, algebraic variables, dependencies
+      (let* ((scan-ret (org-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)
+          (org-mathsheet--dfs-visit node fields))
+        (setq org-mathsheet--var-list '())
+
+        ;; return filled problem
+        `((:problem . ,(buffer-string))
+          (:alg-vars . ,alg-vars)))))
+
+(defun org-mathsheet--generate-problems (template-name count)
+  "Use templates from TEMPLATE-NAME to generate COUNT problems
+
+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))
+      (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)))) ; load the table, 
drop the header
+
+    ;; sort by weight (low to high)
+    (setq templates (sort templates (lambda (a b) (< (car a) (car b))))
+          ;; calc total weight
+          total-weight (float
+                        (seq-reduce (lambda (total item) (+ total (car item)))
+                                    templates
+                                    0)))
+
+    ;; calculate number for each row
+    (dotimes (ii (length templates))
+      (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))))
+             (added 0)
+             (dup-count 0)
+             problem-set)
+        (while (< added needed) ; add until "needed" are kept
+          (let* ((fill-ret (org-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
+                                                         calc-prefer-frac t
+                                                         calc-frac-format ("/" 
nil))))))
+            (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)))
+        (cl-psetf (elt problems ii) (elt problems jj)
+               (elt problems jj) (elt problems ii))))
+
+    ;; sort by order
+    (sort problems (lambda (a b) (< (caddr a) (caddr b))))
+
+    ;; return problems and answers, drop header
+    problems))
+
+;;;###autoload
+(defun org-dblock-write:problem-set (params)
+  "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, `:prob-cols' for the number of columns to use
+for problems, 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 (org-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 |"
+                                (car problem)
+                                (cadr problem)))
+      problems
+      "\n"))
+
+    ;; align table
+    (org-table-align)
+
+    ;; should we generate the sheet?
+    (when (y-or-n-p "Write worksheet? ")
+      (org-mathsheet--gen-worksheet
+       (plist-get params :templates)
+       (plist-get params :instruction)
+       problems
+       (plist-get params :prob-cols)))))
+
+(defun org-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)))
+         (latex-expr-cleaned (replace-regexp-in-string (rx "1:" (* space)) "" 
latex-expr)))
+    (concat "$" latex-expr-cleaned "$")))
+
+(defun org-mathsheet--gen-worksheet (file-name instruction problems prob-cols)
+  "Generate a worksheet with PROBLEMS.
+
+Write a file named FILE-NAME. Include the INSTRUCTION line at the
+top. The problems will be arranged in PROB-COLS columns. The
+answers will be in 4 columns."
+  (with-temp-file (concat file-name ".tex")
+    (insert org-mathsheet--worksheet-template)
+
+    (goto-char (point-min))
+    (search-forward "<<instruction>>")
+    (replace-match "")
+    (insert instruction)
+
+    (let ((answ-cols 5))
+      (goto-char (point-min))
+      (search-forward "<<problems>>")
+      (replace-match "")
+      (dolist (group (seq-partition problems prob-cols))
+        (insert (format "\\begin{multicols}{%d}\n" prob-cols))
+        (dolist (row group)
+          (if (cadddr row)
+              (insert (format"\\question %s\n"
+                             (org-mathsheet--convert-to-latex (car row))))
+            (insert (format"\\question %s = 
\\rule[-.2\\baselineskip]{2cm}{0.4pt}\n"
+                           (org-mathsheet--convert-to-latex (car row))))))
+        (insert "\\end{multicols}\n")
+        (insert "\\vspace{\\stretch{1}}\n"))
+
+      (goto-char (point-min))
+      (search-forward "<<answers>>")
+      (replace-match "")
+      (dolist (group (seq-partition problems answ-cols))
+        (insert (format "\\begin{multicols}{%s}\n" answ-cols))
+        (dolist (row group)
+          (insert (format "\\question %s\n"
+                          (org-mathsheet--convert-to-latex (cadr row)))))
+        (insert "\\end{multicols}\n"))))
+  (shell-command (concat "texi2pdf " file-name ".tex")
+                 (get-buffer-create "*Standard output*")))
+
+(provide 'org-mathsheet)
+
+;;; org-mathsheet.el ends here
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000000..c00d1b192e
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,1152 @@
+
+
+# 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, homeschooling
+parent, or any parent.
+
+
+## Examples
+
+Here are some example worksheets generated by this tool:
+
+1.  [arithmetic](add-sub-1.pdf)
+2.  [algebra](algebra-1.pdf)
+
+They were generated using [this configuration](example.md).
+
+
+## Requirements
+
+[texi2pdf](https://www.gnu.org/software/texinfo/manual/texinfo/html_node/Format-with-texi2dvi-or-texi2pdf.html)
 is required to generate the PDF worksheet. Without it you can
+still generate the table of problems and solutions.
+
+
+## Usage
+
+There are two main components involved in defining a worksheet:
+
+1.  the problem templates
+2.  the problem-set block
+
+
+### Problem Templates
+
+1.  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:
+    
+        [0..15] + [1..10]
+    
+    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:
+    
+        1 + 2
+        15 + 10
+        5 + 1
+
+2.  Equation Templates
+
+    In addition 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:
+    
+        [1..5] x + 3 = [-10..10]
+    
+    This can produce the following problems:
+    
+        3 x + 6 = -1
+        4 x + 2 = 2
+        1 x + 8 = -3
+
+3.  Field Rules
+
+    These are the different ways fields can be defined:
+    
+    -   **[-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
+    -   **[round(sin(0.3))]:** expressions can use math functions
+    -   **[a=&#x2026;]:** assign the variable `a` to the number chosen for 
this field
+    -   **[-2..$a]:** any number from -2 to the value another field assigned
+        to `a`
+    -   **[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.
+    
+    These math functions are allowed: sqrt, sin, cos, tan, asin, acos,
+    atan, floor, ceil, round. Find more details about each of these
+    functions in the emacs calc manual.
+
+4.  Template Examples
+
+    Here are a few more examples:
+    
+    Division problem that divides evenly
+    
+        [$a*[1..5]] / [a=1..10]
+    
+    Addition and subtraction, but ensure a positive result
+    
+        [a=1..10] + [b=0..10] - [0..($a+$b)]
+    
+    Division but ensure we don't divide by zero
+    
+        [-10..10] / [-5..-1,1..5]
+
+
+### The Problem Template Table
+
+1.  Overview
+
+    In order to make it possible to have more than one problem template on
+    a worksheet, each worksheet is configured with a set of templates in a
+    templates table. For example
+    
+    <table id="orgd1da49c" border="2" cellspacing="0" cellpadding="6" 
rules="groups" frame="hsides">
+    
+    
+    <colgroup>
+    <col  class="org-right" />
+    
+    <col  class="org-right" />
+    
+    <col  class="org-left" />
+    
+    <col  class="org-left" />
+    </colgroup>
+    <thead>
+    <tr>
+    <th scope="col" class="org-right">weight</th>
+    <th scope="col" class="org-right">order</th>
+    <th scope="col" class="org-left">template</th>
+    <th scope="col" class="org-left">description</th>
+    </tr>
+    </thead>
+    
+    <tbody>
+    <tr>
+    <td class="org-right">3</td>
+    <td class="org-right">1</td>
+    <td class="org-left">[1..10] + [1..20]</td>
+    <td class="org-left">addition</td>
+    </tr>
+    
+    
+    <tr>
+    <td class="org-right">1</td>
+    <td class="org-right">2</td>
+    <td class="org-left">[a=1..10] - [0..$a]</td>
+    <td class="org-left">subtraction above zero</td>
+    </tr>
+    </tbody>
+    </table>
+    
+    The table contains the following columns:
+    
+    -   **weight:** The relative number of this type of problem to include on
+        the worksheet. A weight of zero means the template will not be
+        used. For `first-sheet` three out of four of the worksheet problems
+        will be addition.
+    -   **order:** Problems are ordered on the sheet in ascending order. Two
+        problems with the same order will be intermingled. For `first-sheet`
+        all of the addition problems will come first.
+    -   **template:** this is the template used to generate problems of this
+        type.
+    -   **description:** This column is just for your notes. It is not used in
+        worksheet generation.
+    
+    Also notice that the table is assigned a name. That name will be used
+    to refer to it later.
+
+2.  Example
+
+    Here is another example template table.
+    
+    <table id="org346121c" border="2" cellspacing="0" cellpadding="6" 
rules="groups" frame="hsides">
+    
+    
+    <colgroup>
+    <col  class="org-right" />
+    
+    <col  class="org-right" />
+    
+    <col  class="org-left" />
+    
+    <col  class="org-left" />
+    </colgroup>
+    <thead>
+    <tr>
+    <th scope="col" class="org-right">weight</th>
+    <th scope="col" class="org-right">order</th>
+    <th scope="col" class="org-left">template</th>
+    <th scope="col" class="org-left">description</th>
+    </tr>
+    </thead>
+    
+    <tbody>
+    <tr>
+    <td class="org-right">3</td>
+    <td class="org-right">1</td>
+    <td class="org-left">[1..10] + [0..10]</td>
+    <td class="org-left">simple</td>
+    </tr>
+    
+    
+    <tr>
+    <td class="org-right">2</td>
+    <td class="org-right">2</td>
+    <td class="org-left">[1..10] + [8..15]</td>
+    <td class="org-left">second number bigger</td>
+    </tr>
+    
+    
+    <tr>
+    <td class="org-right">1</td>
+    <td class="org-right">2</td>
+    <td class="org-left">[a=3..10] - [0..$a]</td>
+    <td class="org-left">subtraction</td>
+    </tr>
+    
+    
+    <tr>
+    <td class="org-right">1</td>
+    <td class="org-right">3</td>
+    <td class="org-left">[1..10] + [1..7] + [1..5]</td>
+    <td class="org-left">three terms</td>
+    </tr>
+    
+    
+    <tr>
+    <td class="org-right">1</td>
+    <td class="org-right">4</td>
+    <td class="org-left">[a=1..10] + [0..10] - [0..$a]</td>
+    <td class="org-left">three terms with subtraction</td>
+    </tr>
+    
+    
+    <tr>
+    <td class="org-right">0</td>
+    <td class="org-right">0</td>
+    <td class="org-left">[$a*[1..5]] / [a=1..10]</td>
+    <td class="org-left">division</td>
+    </tr>
+    </tbody>
+    </table>
+
+
+### The Problem-Set Block
+
+1.  Overview
+
+    The second thing needed to generate a mathsheet is an [org dynamic
+    block](https://orgmode.org/manual/Dynamic-Blocks.html). Here is an example:
+    
+    The block name must be `problem-set` and it must specify the following 
parameters
+    
+    -   **`:templates`:** The name of the templates table to use
+    -   **`:count`:** the total number of problems to put on the sheet
+    -   **`:prob-cols`:** the number of columns in which to lay out the 
problems
+    -   **`:instruction`:** a brief instruction that will be included at the 
top
+        of the sheet to guide the student
+    
+    <kbd>C-c</kbd> <kbd>C-c</kbd> on
+    the block `BEGIN` line or `END` line will trigger org-mathsheet to
+    generate a new set of problems. The new problems and answers will be
+    written to a table in the body of the dynamic block, and you will have
+    the option (via a yes/no prompt in the mini bar) to write those
+    problems to a PDF. On "yes", org-mathsheet will write a PDF to a file
+    named by the template table name. If an existing file exists it will
+    be overwritten. On "no", nothing will be written.
+
+2.  Example
+
+    This is an example problem-set block.
+
+
+# Code walkthrough
+
+
+## Problem generation
+
+
+### Header
+
+This is the standard emacs package header.
+
+    ;;; org-mathsheet.el --- Generate dynamic math worksheets  -*- 
lexical-binding:t -*-
+    
+    ;; Copyright (C) 2025 Free Software Foundation, Inc.
+    
+    ;; Author: Ian Martins <ia...@jhu.edu>
+    ;; Keywords: tools, education, math
+    ;; Homepage: https://gitlab.com/ianxm/org-mathsheet
+    ;; Version: 1.0
+    ;; Package-Requires: ((peg "1.0"))
+    
+    ;; This file is not part of GNU Emacs.
+    
+    ;; GNU Emacs is free software: you can redistribute it and/or modify
+    ;; it under the terms of the GNU General Public License as published by
+    ;; the Free Software Foundation, either version 3 of the License, or
+    ;; (at your option) any later version.
+    
+    ;; GNU Emacs is distributed in the hope that it will be useful,
+    ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+    ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    ;; GNU General Public License for more details.
+    
+    ;; You should have received a copy of the GNU General Public License
+    ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+    
+    ;;; Commentary:
+    
+    ;; This package generates dynamic math worksheets. The types and
+    ;; distribution of problems is highly customizable. Problem sets are
+    ;; defined in org tables, generated in dynamic blocks for review, and
+    ;; exported to PDF for printing.
+    
+    ;;; Code:
+
+
+### Dependencies
+
+This package needs [peg](https://elpa.gnu.org/packages/peg.html).
+
+    (require 'peg)
+
+
+### Variables
+
+We need `org-mathsheet--var-list` to keep track of the variables between 
fields.
+
+`org-mathsheet--worksheet-template` is the LaTeX template for the
+worksheet, which is defined in a LaTeX source block below. This
+assigns the constant directly to that named block.
+
+    (defvar org-mathsheet--var-list '()
+      "List of variables used in a problem")
+    
+    (defconst org-mathsheet--worksheet-template page
+      "LaTeX template for the worksheet")
+
+
+### Scan problem
+
+This scans a problem to find the locations of fields and dependencies
+between them. It must be called with point at the start of the
+problem. It moves the point to the end of the problem unless there's
+an error, in which case it stops at the place where the error
+occurred. This returns a list of fields, with each field formatted as:
+
+    '(asn-var (deps) (start-marker . end-marker) nil)
+
+`asn-var` is a variable name if this field is being assigned to a
+variable, otherwise it is a placeholder like `_0`, `_1`, etc. `asn-var` must
+be interned and must be the first index since we use this list as an
+alist later.
+
+`deps` is a list of are dependencies if this field has any, otherwise
+`nil`. Dependencies could be variables or placeholders.
+
+`start-marker` and `end-marker` are markers in the (temp) buffer. The
+`end-marker` is configured to insert text before the marker.
+
+The last entry is `nil` for "not visited." It is used by `dfs-visit`.
+
+for 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))
+
+This uses peg 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
+new field to the list when we close the current field, taking it off
+of `open-fields`.
+
+    (defun org-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
+            alg-vars)
+    
+        (with-peg-rules
+            ((stuff (* (or asn-var math-func alg-var digit symbol field 
space)))
+             (field open (opt assignment) stuff close)
+             (space (* [space]))
+             (open (region "[")
+                   `(l _ -- (progn
+                              (push (list
+                                     (intern (concat "_" (number-to-string 
field-index))) ; asn-var
+                                     nil ; deps
+                                     (cons (copy-marker l) nil) ; start and 
end markers
+                                     nil) ; not visited
+                                    open-fields)
+                              (setq field-index (1+ field-index))
+                              ".")))
+             (assignment (substring letter) "="
+                         `(v -- (progn
+                                  (setcar
+                                   (car open-fields)
+                                   (intern v))
+                                  ".")))
+             (asn-var "$" (substring letter)
+                      `(v -- (progn
+                               (push (intern v) (cadar open-fields))
+                               ".")))
+             (alg-var (substring letter)
+                      `(v -- (progn
+                               (push v alg-vars)
+                               ".")))
+             (close (region "]")
+                    `(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" "sin" "cos" "tan" "asin" "acos" "atan" 
"floor" "ceil" "round"))
+             (letter [a-z])
+             (digit [0-9])
+             (symbol (or "." "," "+" "-" "*" "/" "^" "(" ")" "=")))
+    
+          (peg-run (peg stuff)
+                   (lambda (x) (message "failed %s" x))
+                   (lambda (x)
+                     (funcall x)
+                     `((:fields . ,closed-fields)
+                       (:alg-vars . ,alg-vars)))))))
+
+test scan
+
+    (defun org-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
+            alg-vars)
+    
+        (with-peg-rules
+            ((stuff (* (or asn-var math-func alg-var digit symbol field 
space)))
+             (field open (opt assignment) stuff close)
+             (space (* [space]))
+             (open (region "[")
+                   `(l _ -- (progn
+                              (push (list
+                                     (intern (concat "_" (number-to-string 
field-index))) ; asn-var
+                                     nil ; deps
+                                     (cons (copy-marker l) nil) ; start and 
end markers
+                                     nil) ; not visited
+                                    open-fields)
+                              (setq field-index (1+ field-index))
+                              ".")))
+             (assignment (substring letter) "="
+                         `(v -- (progn
+                                  (setcar
+                                   (car open-fields)
+                                   (intern v))
+                                  ".")))
+             (asn-var "$" (substring letter)
+                      `(v -- (progn
+                               (push (intern v) (cadar open-fields))
+                               ".")))
+             (alg-var (substring letter)
+                      `(v -- (progn
+                               (push v alg-vars)
+                               ".")))
+             (close (region "]")
+                    `(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" "sin" "cos" "tan" "asin" "acos" "atan" 
"floor" "ceil" "round"))
+             (letter [a-z])
+             (digit [0-9])
+             (symbol (or "." "," "+" "-" "*" "/" "^" "(" ")" "=")))
+    
+          (peg-run (peg stuff)
+                   (lambda (x) (message "failed %s" x))
+                   (lambda (x)
+                     (funcall x)
+                     `((:fields . ,closed-fields)
+                       (:alg-vars . ,alg-vars)))))))
+    
+    (with-temp-buffer
+      (insert "[0..4,6-9,11] * x + [floor([-10..10]/3)] = [-10..10]")
+      (goto-char (point-min))
+      (org-mathsheet--scan-problem))
+
+
+### Reduce field
+
+This must be called with point at the start of a field. This moves the
+point to the end of the field. This returns the value to which the
+field reduces. `peg-run` returns its stack and the value is the last
+thing remaining on the stack when peg completes so peg returns a list
+with one value. We take the value out of the list and return it.
+
+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 `..` instead of `-` for range because if we used `-` then this would
+be ambiguous:
+
+    [1-5]
+
+    (defun org-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 math-func expression sequence assignment 
value) space "]")
+           (expression (list value space operation space value (* space 
operation space value))
+                       `(vals -- (string-to-number
+                                  (calc-eval
+                                   (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) 
org-mathsheet--var-list)
+                                  r)))
+           (sequence (list (or range value) (* "," space (or range value)))
+                     `(vals -- (seq-random-elt vals)))
+           (range value ".." value
+                  `(min max -- (if (>= min max)
+                                   (error "Range bounds must be increasing")
+                                 (+ (random (- max min)) min))))
+           (value (or (substring (opt "-") (+ digit)) var-rhs parenthetical)
+                  `(v -- (if (stringp v) (string-to-number v) v)))
+           (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) 
org-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) (car (funcall x))))))
+
+test with
+
+    (defvar org-mathsheet--var-list '()
+      "List of variables used in a problem")
+    
+    (defconst org-mathsheet--worksheet-template page
+      "LaTeX template for the worksheet")
+    (defun org-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 math-func expression sequence assignment 
value) space "]")
+           (expression (list value space operation space value (* space 
operation space value))
+                       `(vals -- (string-to-number
+                                  (calc-eval
+                                   (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) 
org-mathsheet--var-list)
+                                  r)))
+           (sequence (list (or range value) (* "," space (or range value)))
+                     `(vals -- (seq-random-elt vals)))
+           (range value ".." value
+                  `(min max -- (if (>= min max)
+                                   (error "Range bounds must be increasing")
+                                 (+ (random (- max min)) min))))
+           (value (or (substring (opt "-") (+ digit)) var-rhs parenthetical)
+                  `(v -- (if (stringp v) (string-to-number v) v)))
+           (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) 
org-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) (car (funcall x))))))
+    
+    (with-temp-buffer
+      ;(insert "[1..10,15..20,50]")
+      (insert "[1..10]")
+        (goto-char (point-min))
+        (org-mathsheet--reduce-field))
+
+
+### Replace field
+
+Replace a field with the value returned from reducing it. This uses
+`org-mathsheet--reduce-field` to determine the value to use in place of
+the field.
+
+    (defun org-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 (org-mathsheet--reduce-field))
+          (goto-char start)
+          (delete-char (- end start) t)
+          (insert (number-to-string val)))))
+
+
+### DFS visit
+
+This uses a depth first search to ensure that we visit (reduce and
+replace) the fields in dependency order. We check dependencies then
+visit the node. We use the last field in the field structure to keep
+track of which fields have been visited.
+
+    (defun org-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
+        (_                           ; process
+         (setcar (cdddr node) 1)     ; started
+         (let ((deps (cadr node)))
+           (dolist (dep deps)
+             (org-mathsheet--dfs-visit
+              (assq dep fields)
+              fields)))
+         (org-mathsheet--replace-field node) ; visit
+         (setcar (cdddr node) 2)))) ; mark done
+
+
+### Fill fields in problem
+
+processes all fields in a problem.
+
+    (full-problem (buffer-substring (point-at-bol) (point-at-eol)))
+
+    (defun org-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)
+          (goto-char (point-min))
+    
+          ;; find fields, assignment variables, algebraic variables, 
dependencies
+          (let* ((scan-ret (org-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)
+              (org-mathsheet--dfs-visit node fields))
+            (setq org-mathsheet--var-list '())
+    
+            ;; return filled problem
+            `((:problem . ,(buffer-string))
+              (:alg-vars . ,alg-vars)))))
+
+test with this
+
+    (defvar org-mathsheet--var-list '()
+      "List of variables used in a problem")
+    
+    (defconst org-mathsheet--worksheet-template page
+      "LaTeX template for the worksheet")
+    (defun org-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
+            alg-vars)
+    
+        (with-peg-rules
+            ((stuff (* (or asn-var math-func alg-var digit symbol field 
space)))
+             (field open (opt assignment) stuff close)
+             (space (* [space]))
+             (open (region "[")
+                   `(l _ -- (progn
+                              (push (list
+                                     (intern (concat "_" (number-to-string 
field-index))) ; asn-var
+                                     nil ; deps
+                                     (cons (copy-marker l) nil) ; start and 
end markers
+                                     nil) ; not visited
+                                    open-fields)
+                              (setq field-index (1+ field-index))
+                              ".")))
+             (assignment (substring letter) "="
+                         `(v -- (progn
+                                  (setcar
+                                   (car open-fields)
+                                   (intern v))
+                                  ".")))
+             (asn-var "$" (substring letter)
+                      `(v -- (progn
+                               (push (intern v) (cadar open-fields))
+                               ".")))
+             (alg-var (substring letter)
+                      `(v -- (progn
+                               (push v alg-vars)
+                               ".")))
+             (close (region "]")
+                    `(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" "sin" "cos" "tan" "asin" "acos" "atan" 
"floor" "ceil" "round"))
+             (letter [a-z])
+             (digit [0-9])
+             (symbol (or "." "," "+" "-" "*" "/" "^" "(" ")" "=")))
+    
+          (peg-run (peg stuff)
+                   (lambda (x) (message "failed %s" x))
+                   (lambda (x)
+                     (funcall x)
+                     `((:fields . ,closed-fields)
+                       (:alg-vars . ,alg-vars)))))))
+    (defun org-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 math-func expression sequence assignment 
value) space "]")
+           (expression (list value space operation space value (* space 
operation space value))
+                       `(vals -- (string-to-number
+                                  (calc-eval
+                                   (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) 
org-mathsheet--var-list)
+                                  r)))
+           (sequence (list (or range value) (* "," space (or range value)))
+                     `(vals -- (seq-random-elt vals)))
+           (range value ".." value
+                  `(min max -- (if (>= min max)
+                                   (error "Range bounds must be increasing")
+                                 (+ (random (- max min)) min))))
+           (value (or (substring (opt "-") (+ digit)) var-rhs parenthetical)
+                  `(v -- (if (stringp v) (string-to-number v) v)))
+           (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) 
org-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) (car (funcall x))))))
+    (defun org-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 (org-mathsheet--reduce-field))
+          (goto-char start)
+          (delete-char (- end start) t)
+          (insert (number-to-string val)))))
+    (defun org-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
+        (_                           ; process
+         (setcar (cdddr node) 1)     ; started
+         (let ((deps (cadr node)))
+           (dolist (dep deps)
+             (org-mathsheet--dfs-visit
+              (assq dep fields)
+              fields)))
+         (org-mathsheet--replace-field node) ; visit
+         (setcar (cdddr node) 2)))) ; mark done
+    
+    (org-mathsheet--fill-problem "[1..12] + [1,4,6,10]")
+    ;;(org-mathsheet--fill-problem "[1..[2..[10..100]]]")
+    ;;(org-mathsheet--fill-problem "[$a*[1..10]] / [a=1..10]")
+    ;;(org-mathsheet--fill-problem "[$a]/(3+[a=1..5])")
+    ;; (org-mathsheet--fill-problem "1/x + 2 = [-10..[10..20]]")
+
+other examples
+
+    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]]
+
+
+### Generate problem set from templates
+
+This reads in the templates, figures out how many of each based on
+weights and the number of problems needed, generates the problem set,
+figures out the answers, then reorders.
+
+The reordering is done because if multiple templates are assigned the
+same `order`, they should be intermingled, but we add all problems for
+each template sequentially. In order to mix them up we shuffle the
+whole set and then reorder by `order`.
+
+    (defun org-mathsheet--generate-problems (template-name count)
+      "Use templates from TEMPLATE-NAME to generate COUNT problems
+    
+    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))
+          (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)))) ; load the 
table, drop the header
+    
+        ;; sort by weight (low to high)
+        (setq templates (sort templates (lambda (a b) (< (car a) (car b))))
+              ;; calc total weight
+              total-weight (float
+                            (seq-reduce (lambda (total item) (+ total (car 
item)))
+                                        templates
+                                        0)))
+    
+        ;; calculate number for each row
+        (dotimes (ii (length templates))
+          (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))))
+                 (added 0)
+                 (dup-count 0)
+                 problem-set)
+            (while (< added needed) ; add until "needed" are kept
+              (let* ((fill-ret (org-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
+                                                             calc-prefer-frac t
+                                                             calc-frac-format 
("/" nil))))))
+                (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)))
+            (cl-psetf (elt problems ii) (elt problems jj)
+                   (elt problems jj) (elt problems ii))))
+    
+        ;; sort by order
+        (sort problems (lambda (a b) (< (caddr a) (caddr b))))
+    
+        ;; return problems and answers, drop header
+        problems))
+
+
+## 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 or footer.
+
+`params` is a property list of params on the block header line.
+
+First we generate the problems and answers, then we write them out to
+a table in the dynamic block, finally, if the user wants it, we
+generate a PDF with these problems.
+
+The reason for the yes/no prompt is to allow you to see the problem
+set that was generated to decide if you want to use it or generate
+another.
+
+    ;;;###autoload
+    (defun org-dblock-write:problem-set (params)
+      "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, `:prob-cols' for the number of columns to use
+    for problems, 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 (org-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 |"
+                                    (car problem)
+                                    (cadr problem)))
+          problems
+          "\n"))
+    
+        ;; align table
+        (org-table-align)
+    
+        ;; should we generate the sheet?
+        (when (y-or-n-p "Write worksheet? ")
+          (org-mathsheet--gen-worksheet
+           (plist-get params :templates)
+           (plist-get params :instruction)
+           problems
+           (plist-get params :prob-cols)))))
+
+
+## Generate PDF
+
+
+### Lay out page
+
+This wraps the problems with a tex header and footer.
+
+This template doesn't use noweb but it uses noweb syntax (`<<label>>`) to
+mark where org-mathsheet will insert content. It's not possible actually
+use noweb here since this template must be tangled to org-mathsheet.el as
+a template.
+
+I found the solution for how to enumerate with circled numbers 
[here](https://latex.org/forum/viewtopic.php?p=40006&sid=d202f756313add2391c3140fbeafe2ff#p40006).
+
+
+### Convert calc to latex
+
+This converts a calc expression to latex format. The problems and
+answers are generated in standard emacs calc format. If they are to be
+written to a PDF we convert them to latex. emacs calc already knows
+how to convert between formats, so we let it do it.
+
+    (defun org-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)))
+             (latex-expr-cleaned (replace-regexp-in-string (rx "1:" (* space)) 
"" latex-expr)))
+        (concat "$" latex-expr-cleaned "$")))
+
+
+### Write PDF
+
+This inserts instruction line and generated problems into the page
+template, writes it to a local file, then runs `texi2pdf` to build a
+PDF. We save it as `[template-name].tex` and the final worksheet is
+named `[template-name].pdf`. Each execution with the same template name
+will overwrite the same file.
+
+    (defun org-mathsheet--gen-worksheet (file-name instruction problems 
prob-cols)
+      "Generate a worksheet with PROBLEMS.
+    
+    Write a file named FILE-NAME. Include the INSTRUCTION line at the
+    top. The problems will be arranged in PROB-COLS columns. The
+    answers will be in 4 columns."
+      (with-temp-file (concat file-name ".tex")
+        (insert org-mathsheet--worksheet-template)
+    
+        (goto-char (point-min))
+        (search-forward "<<instruction>>")
+        (replace-match "")
+        (insert instruction)
+    
+        (let ((answ-cols 5))
+          (goto-char (point-min))
+          (search-forward "<<problems>>")
+          (replace-match "")
+          (dolist (group (seq-partition problems prob-cols))
+            (insert (format "\\begin{multicols}{%d}\n" prob-cols))
+            (dolist (row group)
+              (if (cadddr row)
+                  (insert (format"\\question %s\n"
+                                 (org-mathsheet--convert-to-latex (car row))))
+                (insert (format"\\question %s = 
\\rule[-.2\\baselineskip]{2cm}{0.4pt}\n"
+                               (org-mathsheet--convert-to-latex (car row))))))
+            (insert "\\end{multicols}\n")
+            (insert "\\vspace{\\stretch{1}}\n"))
+    
+          (goto-char (point-min))
+          (search-forward "<<answers>>")
+          (replace-match "")
+          (dolist (group (seq-partition problems answ-cols))
+            (insert (format "\\begin{multicols}{%s}\n" answ-cols))
+            (dolist (row group)
+              (insert (format "\\question %s\n"
+                              (org-mathsheet--convert-to-latex (cadr row)))))
+            (insert "\\end{multicols}\n"))))
+      (shell-command (concat "texi2pdf " file-name ".tex")
+                     (get-buffer-create "*Standard output*")))
+
+
+### Footer
+
+    (provide 'org-mathsheet)
+    
+    ;;; org-mathsheet.el ends here
+
+
+# Literate Programming
+
+This is written as a [literate 
program](https://en.wikipedia.org/wiki/Literate_programming) using [emacs 
org-mode](https://orgmode.org/).  [The org
+file](gimp-comic-book.md) contains the code and documentation for the math 
worksheet
+generation script.  When this file is saved, the source code is
+generated using `org-babel-tangle` and the readme is generated using
+`org-md-export-to-file`.
+
+The first line of [the org file](gimp-comic-book.md) configures emacs to run 
those commands
+whenever this file is saved, which generates the scripts and readme.
+


Reply via email to