Math Worksheet Generator

Next:   [Contents]

Math Worksheet Generator

Table of Contents


1 Overview


Next: , Up: Overview   [Contents]

1.1 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.


Next: , Previous: , Up: Overview   [Contents]

1.2 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.


Next: , Previous: , Up: Overview   [Contents]

1.3 Examples

Here are some example worksheets generated by this tool:

  1. arithmetic
  2. algebra

They were generated using this configuration.


Next: , Previous: , Up: Overview   [Contents]

1.4 Requirements

texi2pdf is required to generate the PDF worksheet. Without it you can still generate the table of problems and solutions.


Previous: , Up: Overview   [Contents]

1.5 Usage

There are two main components involved in defining a worksheet:

  1. the problem templates
  2. the problem-set block

1.5.1 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=…]

    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]
    

1.5.2 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

    weightordertemplatedescription
    31[1..10] + [1..20]addition
    12[a=1..10] - [0..$a]subtraction above zero

    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.

    weightordertemplatedescription
    31[1..10] + [0..10]simple
    22[1..10] + [8..15]second number bigger
    12[a=3..10] - [0..$a]subtraction
    13[1..10] + [1..7] + [1..5]three terms
    14[a=1..10] + [0..10] - [0..$a]three terms with subtraction
    00[$a*[1..5]] / [a=1..10]division

1.5.3 The Problem-Set Block

  1. Overview

    The second thing needed to generate a mathsheet is an org dynamic block. 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

    C-c C-c 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.


2 Code walkthrough


2.1 Problem generation


Next: , Previous: , Up: Problem generation   [Contents]

2.1.2 Dependencies

This package needs peg.

(require 'peg)

2.1.3 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")

Next: , Previous: , Up: Problem generation   [Contents]

2.1.4 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))

2.1.5 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))

Next: , Previous: , Up: Problem generation   [Contents]

2.1.6 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)))))

2.1.7 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

2.1.8 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]]

2.1.9 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))

2.2 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)))))

2.3 Generate PDF


2.3.1 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.

\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}

Next: , Previous: , Up: Generate PDF   [Contents]

2.3.2 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 "$")))

Next: , Previous: , Up: Generate PDF   [Contents]

2.3.3 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*")))

3 Literate Programming

This is written as a literate program using emacs org-mode. 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 the org file configures emacs to run those commands whenever this file is saved, which generates the scripts and readme.