This is docPS9lle.info, produced by makeinfo version 6.8 from
mathsheet.texi.


File: docPS9lle.info,  Node: Top,  Next: Overview,  Up: (dir)

Math Worksheet Generator
************************

* Menu:

* Overview::
* Code walkthrough::
* Literate Programming::

— The Detailed Node Listing —

Overview

* Description::
* Audience::
* Examples::
* Requirements::
* Usage::

Usage

* Starting Mathsheet::
* Defining a Worksheet::
* Customization::
* Problem Templates::

Code walkthrough

* Front matter::
* UI Form::
* Extract configuration from form::
* Problem generation::
* Generate PDF::
* Convenience functions::
* Footer::

Front matter

* GNU header components::
* Full header::
* Dependencies::
* Variables::

UI Form

* Form configuration::
* New record defaults::
* Clean up template rows::
* Layout the actual form::

Extract configuration from form

* Validate form fields::
* Extract and parse::
* Initiate sheet generation::

Problem generation

* Scan problem::
* Reduce field::
* Replace field::
* DFS visit::
* Fill fields in problem::
* Generate problem set from templates::

Generate PDF

* Lay out page::
* Convert calc to EQN::
* Write PDF::

Convenience functions

* Add key binding to form::
* Open mathsheet::



File: docPS9lle.info,  Node: Overview,  Next: Code walkthrough,  Prev: Top,  Up: Top

1 Overview
**********

* Menu:

* Description::
* Audience::
* Examples::
* Requirements::
* Usage::


File: docPS9lle.info,  Node: Description,  Next: Audience,  Up: Overview

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.


File: docPS9lle.info,  Node: Audience,  Next: Examples,  Prev: Description,  Up: Overview

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.


File: docPS9lle.info,  Node: Examples,  Next: Requirements,  Prev: Audience,  Up: Overview

1.3 Examples
============

Here are some example worksheets generated by this tool, along with the
templates from which they were built.  Template syntax is described in
greater in the "Problem Templates" section below.

  1. arithmetic (examples/add-sub-1.pdf) made from these templates:
          w | o | template
          --+---+------------------------------------
          3 | 2 | [1..10] + [8..15]
          2 | 2 | [a=3..10] - [0..$a]
          1 | 3 | [1..10] + [1..7] + [1..5]
          1 | 4 | [a=1..10] + [0..10] - [0..$a]
          1 | 5 | [a=1..10] + [b=0..10] - [0..($a+$b)]
  2. algebra (examples/algebra-1.pdf) made from these templates:
          w | o | template
          --+---+------------------------------------
          3 | 1 | x / ([2..4] + [a=0..5]) = [$a..10]
          2 | 2 | [$a*[2..10]] / x = [a=1,2,4]
          2 | 3 | x/[-5..-1,1..5] + [1..10] = [-10..10]
          1 | 3 | x = x/[a=2..6] + [round([1..20]/$a)]
          1 | 3 | x^2 = sqrt([16 - $a] + [a=1..5])


File: docPS9lle.info,  Node: Requirements,  Next: Usage,  Prev: Examples,  Up: Overview

1.4 Requirements
================

Groff (https://www.gnu.org/software/groff/#introduction) is required to
generate mathsheets.  If ‘groff’ is not installed on the system PDF
generation will fail and there will be a ‘command not found’ message in
the mini-buffer.


File: docPS9lle.info,  Node: Usage,  Prev: Requirements,  Up: Overview

1.5 Usage
=========

* Menu:

* Starting Mathsheet::
* Defining a Worksheet::
* Customization::
* Problem Templates::


File: docPS9lle.info,  Node: Starting Mathsheet,  Next: Defining a Worksheet,  Up: Usage

1.5.1 Starting Mathsheet
------------------------

Open mathsheet using M-x mathsheet-open


File: docPS9lle.info,  Node: Defining a Worksheet,  Next: Customization,  Prev: Starting Mathsheet,  Up: Usage

1.5.2 Defining a Worksheet
--------------------------

Worksheets are defined using a form.  Forms-mode provides a way to add,
save, load records based on a form.  See forms-mode doc
(https://www.gnu.org/software/emacs/manual/html_mono/forms.html#Forms-Commands)
for details.  The mathsheet form specifies the following fields:
‘name’
     The base name of the file to write.  Spaces will be converted to
     dashes and a ‘pdf’ extension will be added.
‘count’
     the total number of problems to put on the worksheet
‘columns’
     the number of columns the worksheet should have.
‘instruction’
     a brief, one sentence instruction that will be included at the top
     of the sheet to guide the student.
‘problems’
     A multi-line, pipe (‘|’) delimited string describing the problems
     to include on the worksheet.

   Consider this example value for ‘problems’:
     3 | 1 | [1..10] + [1..20]
     1 | 2 | [a=1..10] - [0..$a]

   Each ‘problems’ line contains the following fields:
‘weight (w)’
     The relative number of this type of problem to include on the
     worksheet.  A weight of zero means the template will not be used.
     In the example above, three fourths of the worksheet problems will
     be addition.
‘order (o)’
     Problems are ordered on the sheet in ascending order.  Two problems
     with the same order will be intermingled.  In the example above,
     all of the addition problems will come before the subtraction
     problems.
‘template’
     this is the template used to generate problems of this type.

   Generate a worksheet by running C-c C-r from the mathsheet form.


File: docPS9lle.info,  Node: Customization,  Next: Problem Templates,  Prev: Defining a Worksheet,  Up: Usage

1.5.3 Customization
-------------------

Mathsheet allows for the following customizations:

‘mathsheet-data-file’
     This is where mathsheet data is stored.  It defaults to a file in
     your emacs user directory.  You can probably leave it there.
‘mathsheet-output-directory’
     This is where worksheets should be written.  It defaults to your
     home directory.  You’ll probably want to move it somewhere else.


File: docPS9lle.info,  Node: Problem Templates,  Prev: Customization,  Up: Usage

1.5.4 Problem 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.

  1. Expression Templates

     Expression templates define an expression which must be evaluated.
     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
     (https://www.gnu.org/software/emacs/manual/html_mono/calc.html#Arithmetic).

  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]


File: docPS9lle.info,  Node: Code walkthrough,  Next: Literate Programming,  Prev: Overview,  Up: Top

2 Code walkthrough
******************

* Menu:

* Front matter::
* UI Form::
* Extract configuration from form::
* Problem generation::
* Generate PDF::
* Convenience functions::
* Footer::


File: docPS9lle.info,  Node: Front matter,  Next: UI Form,  Up: Code walkthrough

2.1 Front matter
================

* Menu:

* GNU header components::
* Full header::
* Dependencies::
* Variables::


File: docPS9lle.info,  Node: GNU header components,  Next: Full header,  Up: Front matter

2.1.1 GNU header components
---------------------------

This lays out some standard header content that is repeated for each
file.


File: docPS9lle.info,  Node: Full header,  Next: Dependencies,  Prev: GNU header components,  Up: Front matter

2.1.2 Full header
-----------------

This is the standard Emacs package header.

   ‘peg’ is used to parse the problem templates.  ‘calc’ is used to
solve the problems as well as converting them to mathematical notation
in EQN format.

     ;;; mathsheet.el --- Generate dynamic math worksheets  -*- lexical-binding:t -*-

     ;; Copyright (C) 2025 Free Software Foundation, Inc.

     ;; Author: Ian Martins <ianxm@jhu.edu>
     ;; Keywords: tools, education, math
     ;; Homepage: https://gitlab.com/ianxm/mathsheet
     ;; Version: 1.2
     ;; Package-Requires: ((peg "1.0")
     ;;                    (emacs "28.1"))

     ;; 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 using templates and exported to PDF for printing.

     ;;; Code:


File: docPS9lle.info,  Node: Dependencies,  Next: Variables,  Prev: Full header,  Up: Front matter

2.1.3 Dependencies
------------------

This package needs forms-mode
(https://www.gnu.org/software/emacs/manual/html_mono/forms.html), peg
(https://elpa.gnu.org/packages/peg.html), calc
(https://www.gnu.org/software/emacs/manual/html_mono/calc.html).  Forms
mode and Calc are included in Emacs but we need to make sure they have
been loaded.

     (require 'forms)
     (require 'peg)
     (require 'calc)

     (declare-function math-read-expr "calc-ext")
     (declare-function calc-set-language "calc-lang")


File: docPS9lle.info,  Node: Variables,  Prev: Dependencies,  Up: Front matter

2.1.4 Variables
---------------

Here we define a customize group, some customize variables that provide
for configuring where form records are stored and where output is
written, and some non-customize variables used internally.

     (defgroup mathsheet nil
       "Options for customizing Mathsheet."
       :prefix "mathsheet-"
       :group 'applications
       :tag "mathsheet")

     (defcustom mathsheet-data-file
       (expand-file-name "mathsheet.dat" user-emacs-directory)
       "Where to store saved mathsheet configurations.

     The default is to save them to a file in the private emacs
     configuration directory."
       :type 'file
       :group 'mathsheet)

     (defcustom mathsheet-output-directory
       (expand-file-name "~")
       "Where to write generated worksheets.

     The default is to write the to the home directory."
       :type 'directory
       :group 'mathsheet)

   We need ‘mathsheet--var-list’ to keep track of the variables between
fields since we need to access the list from multiple top level
functions.

   ‘mathsheet--worksheet-template’ is the Groff template for the
worksheet, which is defined in an example block below.  This assigns the
constant directly to that named block.

   ‘mathsheet--num-pat’ is defined here since it is referenced in a
macro that is used in multiple places.  If it was in the macro it would
be redefined by expansion, and since the macro is called from different
scopes we’d have to define it in multiple places to define it in the
scope where the macro is called.

     (defvar mathsheet--var-list '()
       "List of variables used within a problem.")

     (defconst mathsheet--worksheet-template page
       "Groff template for the worksheet.")

     (defconst mathsheet--num-pat (rx string-start (+ num) string-end)
       "Pattern for integers.")

     (defvar mathsheet--field-sheet-name nil
       "The form record name field.")

     (defvar mathsheet--field-count nil
       "The form record count field.")

     (defvar mathsheet--field-cols nil
       "The form record cols field.")

     (defvar mathsheet--field-instruction nil
       "The form record instruction field.")

     (defvar mathsheet--field-problems nil
       "The form record problems field.")



File: docPS9lle.info,  Node: UI Form,  Next: Extract configuration from form,  Prev: Front matter,  Up: Code walkthrough

2.2 UI Form
===========

* Menu:

* Form configuration::
* New record defaults::
* Clean up template rows::
* Layout the actual form::


File: docPS9lle.info,  Node: Form configuration,  Next: New record defaults,  Up: UI Form

2.2.1 Form configuration
------------------------

See details here
(https://www.gnu.org/software/emacs/manual/html_mono/forms.html).

     (setq forms-file mathsheet-data-file)

     (setq forms-number-of-fields
           (forms-enumerate
            '(mathsheet--field-sheet-name
              mathsheet--field-count
              mathsheet--field-cols
              mathsheet--field-instruction
              mathsheet--field-problems)))

     (setq forms-field-sep "||")


File: docPS9lle.info,  Node: New record defaults,  Next: Clean up template rows,  Prev: Form configuration,  Up: UI Form

2.2.2 New record defaults
-------------------------

When new records are created using the form, initialize them with
default values.

     (defun mathsheet--new-record-filter (record)
       "Set defaults in new RECORD."
       (aset record 2 "20")                  ; default
       (aset record 3 "2")                   ; default
       (aset record 4 "Find the answer.")    ; default
       (aset record 5 "1 | 1 | ")            ; lay out structure
       record)

     (setq forms-new-record-filter 'mathsheet--new-record-filter)


File: docPS9lle.info,  Node: Clean up template rows,  Next: Layout the actual form,  Prev: New record defaults,  Up: UI Form

2.2.3 Clean up template rows
----------------------------

When the form is saved, clean up the template field by lining up the
columns.

     (defun mathsheet--format-templates (record)
       "Format the template rows in RECORD to line up with the header."
       (let ((rows (string-split (aref record 5) "\n"))
             (pat (rx (* space) (group (+ alnum)) (* space) "|"
                      (* space) (group (+ alnum)) (* space) "|"
                      (* space) (group (+ nonl)))))
         (setq rows (mapconcat
                     (lambda (row)
                       (string-match pat row)
                       (format "%s | %s | %s"
                               (match-string 1 row)
                               (match-string 2 row)
                               (match-string 3 row)))
                     rows
                     "\n"))
         (aset record 5 rows))
       record)
     (setq forms-modified-record-filter 'mathsheet--format-templates)


File: docPS9lle.info,  Node: Layout the actual form,  Prev: Clean up template rows,  Up: UI Form

2.2.4 Layout the actual form
----------------------------

This defines the form itself and the locations of the fields.

     (setq forms-format-list
           (list
            "====== Math Sheet Generator ======"
            "\nSee https://gitlab.com/ianxm/mathsheet for details."

            "\n\nThe base-name of the mathsheet file to write, not including extension."
            "\nName: " mathsheet--field-sheet-name

            "\n\nThe total number of problems to put on the sheet."
            "\nCount: " mathsheet--field-count

            "\n\nThe number of columns the sheet should have."
            "\nColumns: " mathsheet--field-cols

            "\n\nThe instruction to give at the top of the sheet."
            "\nInstruction: " mathsheet--field-instruction

            "\n\nThe problem templates from which to generate problems for the sheet."
            "\nOne per line, formatted as \"(w)eight | (o)rder | template\".\n\n"

            "w | o | template\n"
            "--+---+------------------------------------\n"
            mathsheet--field-problems
            "\n"))


File: docPS9lle.info,  Node: Extract configuration from form,  Next: Problem generation,  Prev: UI Form,  Up: Code walkthrough

2.3 Extract configuration from form
===================================

* Menu:

* Validate form fields::
* Extract and parse::
* Initiate sheet generation::


File: docPS9lle.info,  Node: Validate form fields,  Next: Extract and parse,  Up: Extract configuration from form

2.3.1 Validate form fields
--------------------------

This adds validation checks as needed for each field.

     (defmacro mathsheet--validate (field-name field-str checks)
       "Add specified checks to validate field input.

     FIELD-NAME is the name of the field.  FIELD-STR is the string
     value in the record.  CHECKS is a list of symbols specifying
     which validation checks to perform."
       (let (ret)
         (dolist (check checks)
           (pcase check
             ('not-null-p
              (push
               `(when (null ,field-str)
                  (error (format "`%s' cannot be empty" ,field-name)))
               ret))
             ('is-num-p
              (when (not (null field-str))
                (push
                 `(when (not (string-match-p mathsheet--num-pat ,field-str))
                    (error (format "`%s' must be a number" ,field-name)))
                 ret)))
             (`(in-range-p ,min ,max)
              (push
               `(when
                    (or
                     (< (string-to-number ,field-str) ,min)
                     (> (string-to-number ,field-str) ,max))
                  (error (format "`%s' must be between %s and %s, inclusive"
                                 ,field-name ,min ,max)))
               ret))
             (_
              (push
               `(error (format "Unknown check: %s" ,check))
               ret))
             ))
         (append '(progn) ret)))


File: docPS9lle.info,  Node: Extract and parse,  Next: Initiate sheet generation,  Prev: Validate form fields,  Up: Extract configuration from form

2.3.2 Extract and parse
-----------------------

‘emacs-forms’ treats everything like strings so we have to validate and
convert the numbers.  Also the problem field contains multi-line
delimited data so we have to parse it.

   This is also where limits are set.  The max problems on a sheet is
‘50’.  The max columns allowed is ‘4’.

     (defun mathsheet--parse (record)
       "Parse all of the fields of the current RECORD into an alist."
       (let (count cols problems)

         (pcase record
           (`(,name ,count-str ,cols-str ,instruction ,problems-str)

            ;; validate the form fields
            (mathsheet--validate "name" name (not-null-p))
            (mathsheet--validate "count" count-str (not-null-p is-num-p (in-range-p 1 50)))
            (mathsheet--validate "cols" cols-str (not-null-p is-num-p (in-range-p 1 4)))
            (mathsheet--validate "problems" problems-str (not-null-p))

            ;; convert the numbers and parse the problems field
            (setq count (string-to-number count-str)
                  cols (string-to-number cols-str)
                  problems (mapcar           ; parse rows
                            #'mathsheet--parse-problem-row
                            (seq-filter      ; remove possible trailing empty line
                             (lambda (x) (not (string-empty-p x)))
                             (string-split   ; split lines
                              problems-str
                              "\n"))))

            `((:name . ,name)
              (:count . ,count)
              (:cols . ,cols)
              (:instr . ,instruction)
              (:probs .  ,problems)))
           (_ (error "Invalid form data")))))

   This function is used to parse each problem row.

     (defun mathsheet--parse-problem-row (row)
       "Parse one ROW of the problem field into a list."
       (let* ((fields (mapcar                ; trim whitespace
                       #'string-trim
                       (split-string         ; split fields
                        row
                        "|")))
              (weight-str (nth 0 fields))
              (order-str (nth 1 fields))
              (template (nth 2 fields))
              weight order)
         (mathsheet--validate "weight" weight-str (not-null-p is-num-p))
         (mathsheet--validate "order" order-str (not-null-p is-num-p))
         (mathsheet--validate "template" template (not-null-p))
         (setq weight (string-to-number weight-str)
               order (string-to-number order-str))
         (list weight order template)))


File: docPS9lle.info,  Node: Initiate sheet generation,  Prev: Extract and parse,  Up: Extract configuration from form

2.3.3 Initiate sheet generation
-------------------------------

     (defun mathsheet-generate-sheet ()
       "Generate sheet for current form data."
       (interactive)
       (when (not (string= major-mode "forms-mode"))
         (error "Mathsheet must be open to generate a sheet"))
       (let ((config (mathsheet--parse forms--the-record-list)))
         (let ((problems (mathsheet--generate-problems
                          (alist-get :probs config)
                          (alist-get :count config)))
               ;; absolute path without extension
               (fname (concat
                       (file-name-as-directory mathsheet-output-directory)
                       (string-replace " " "-" (alist-get :name config)))))
           (mathsheet--write-worksheet
            fname
            (alist-get :instr config)
            problems
            (alist-get :cols config))
           (message "Wrote %s problems to %s.pdf"
                    (alist-get :count config)
                    fname))))


File: docPS9lle.info,  Node: Problem generation,  Next: Generate PDF,  Prev: Extract configuration from form,  Up: Code walkthrough

2.4 Problem generation
======================

* Menu:

* Scan problem::
* Reduce field::
* Replace field::
* DFS visit::
* Fill fields in problem::
* Generate problem set from templates::


File: docPS9lle.info,  Node: Scan problem,  Next: Reduce field,  Up: Problem generation

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


File: docPS9lle.info,  Node: Reduce field,  Next: Replace field,  Prev: Scan problem,  Up: Problem generation

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

   The list of supported operators and math functions are listed both
here and in ‘mathsheet--scan-problem’, so changes must be made in both
places to keep them synced.

     (defun mathsheet--reduce-field ()
       "Reduce the field to a number.

     Parse the field again, replacing spans with random numbers and
     evaluating arithmetic operations.  The field shouldn't have any
     internal fields so this should result in a single number.  Return
     that number."
       (with-peg-rules
           ((field "[" space (or 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) 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) 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))))))


File: docPS9lle.info,  Node: Replace field,  Next: DFS visit,  Prev: Reduce field,  Up: Problem generation

2.4.3 Replace field
-------------------

Replace a field with the value returned from reducing it.  This uses
‘mathsheet--reduce-field’ to determine the value to use in place of the
field.

     (defun mathsheet--replace-field (node)
       "Replace a field in NODE 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 (mathsheet--reduce-field))
           (goto-char start)
           (delete-char (- end start) t)
           (insert (number-to-string val)))))


File: docPS9lle.info,  Node: DFS visit,  Next: Fill fields in problem,  Prev: Replace field,  Up: Problem generation

2.4.4 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 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
          (dolist (dep (cadr node))
            (mathsheet--dfs-visit
             (assq dep fields)
             fields))
          (mathsheet--replace-field node) ; visit
          (setcar (cdddr node) 2)))) ; mark done


File: docPS9lle.info,  Node: Fill fields in problem,  Next: Generate problem set from templates,  Prev: DFS visit,  Up: Problem generation

2.4.5 Fill fields in problem
----------------------------

processes all fields in a problem.

     (full-problem (buffer-substring (point-at-bol) (point-at-eol)))

     (defun mathsheet--fill-problem (full-problem)
       "Replace all fields in FULL-PROBLEM.

     Goes through all fields in the given problem in dependency order
     and replaces fields with numbers.  When this completes the problem
     will be ready to solve."
         (with-temp-buffer
           ;; stage problem in temp buffer
           (insert full-problem)
           (goto-char (point-min))

           ;; find fields, assignment variables, algebraic variables, dependencies
           (let* ((scan-ret (mathsheet--scan-problem))
                  (fields (alist-get :fields scan-ret))
                  (alg-vars (alist-get :alg-vars scan-ret)))

             ;; visit fields ordered according to dependencies
             (dolist (node fields)
               (mathsheet--dfs-visit node fields))
             (setq mathsheet--var-list '())

             ;; return filled problem
             `((:problem . ,(buffer-string))
               (:alg-vars . ,alg-vars)))))


File: docPS9lle.info,  Node: Generate problem set from templates,  Prev: Fill fields in problem,  Up: Problem generation

2.4.6 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 mathsheet--generate-problems (templates count)
       "Use TEMPLATES 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 problems)
         ;; sort by weight (low to high)
         (setq templates (sort templates #'car-less-than-car)
               ;; calc total weight
               total-weight (seq-reduce (lambda (total item) (+ total (car item)))
                                        templates
                                        0.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 (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
         (setq problems (sort problems (lambda (a b) (< (caddr a) (caddr b)))))

         ;; return problems and answers, drop header
         problems))


File: docPS9lle.info,  Node: Generate PDF,  Next: Convenience functions,  Prev: Problem generation,  Up: Code walkthrough

2.5 Generate PDF
================

* Menu:

* Lay out page::
* Convert calc to EQN::
* Write PDF::


File: docPS9lle.info,  Node: Lay out page,  Next: Convert calc to EQN,  Up: Generate PDF

2.5.1 Lay out page
------------------

This lays out the page in Groff, with placeholders for where details
must be filled in.

   This template doesn’t use noweb
(https://orgmode.org/manual/Noweb-Reference-Syntax.html) but it uses
noweb syntax (‘<<label>>’) to mark where mathsheet will insert content.
It’s not possible actually use noweb here since the problems and answers
are coming from elisp and generated at runtime.  Instead this template
must be tangled to mathsheet.el as a template so the elisp functions can
use it.

     .VM 0 -0.5i                                     \" reduce bottom margin
     .PH "'Name: \l'20\_'''Date:\l'10\_''"           \" header
     <<instruction>>
     .SP 1
     .fam C                                          \" set font
     <<layout>>
     .MC \n[clen]p 10p                               \" start columns mode
     <<problems>>
     .1C 1                                           \" end of columns mode
     .BS                                             \" floating bottom box
     \l'\n(.lu'                                      \" horizontal rule
     .S 8 10                                         \" reduce font and vertical space
     .ss 6                                           \" reduce horizontal space
     .gcolor grey                                    \" answers color
     <<answers>>
     .BE


File: docPS9lle.info,  Node: Convert calc to EQN,  Next: Write PDF,  Prev: Lay out page,  Up: Generate PDF

2.5.2 Convert calc to EQN
-------------------------

This converts a calc expression to EQN format for use with Groff.  The
problems and answers are generated in Emacs Calc normal format.  Emacs
Calc already knows how to convert between formats, so we let it do it.

     (defun mathsheet--convert-to-eqn (expr)
       "Format the given calc expression EXPR for groff.

       EXPR should be in normal calc format.  The result is the same
       expression (not simplified) but in eqn format for groff."
       (let ((current-language calc-language)
             calc-expr)
         (calc-set-language nil)
         (setq calc-expr (math-read-expr expr))
         (calc-set-language 'eqn)
         (let* ((eqn-expr (math-format-stack-value (list calc-expr 1 nil)))
                (eqn-expr-cleaned (replace-regexp-in-string (rx "1:" (* space)) "" eqn-expr)))
           (calc-set-language current-language)
           eqn-expr-cleaned)))

     (defun mathsheet--convert-to-eqn (expr)
       "Format the given calc expression EXPR for groff.

       EXPR should be in normal calc format.  The result is the same
       expression (not simplified) but in eqn format for groff."
       (let ((current-language calc-language)
             calc-expr)
         (calc-set-language nil)
         (setq calc-expr (math-read-expr expr))
         (calc-set-language 'eqn)
         (let* ((eqn-expr (math-format-stack-value (list calc-expr 1 nil)))
                (eqn-expr-cleaned (replace-regexp-in-string (rx "1:" (* space)) "" eqn-expr)))
           (calc-set-language current-language)
           eqn-expr-cleaned)))
     (mathsheet--convert-to-eqn "x/(1+2)=3")


File: docPS9lle.info,  Node: Write PDF,  Prev: Convert calc to EQN,  Up: Generate PDF

2.5.3 Write PDF
---------------

This inserts instruction line and generated problems into the page
template, writes it to a local file, then runs ‘groff’ to build a PDF
named ‘[template-name].pdf’.  Each execution with the same template name
will overwrite that file.

   Sub-sections are identified by noweb
(https://orgmode.org/manual/Noweb-Reference-Syntax.html) syntax
(‘<<section>>’).  The details of how each section is filled in is
described below.

     (defun mathsheet--write-worksheet (fname instruction problems prob-cols)
       "Write a worksheet to FNAME with INSTRUCTION and PROBLEMS.

       Write a file named FNAME.  Include the INSTRUCTION line at the
       top.  The problems will be arranged in PROB-COLS columns.  The
       answers will be in 5 columns."
       (with-temp-buffer
         (insert mathsheet--worksheet-template)

         (let ((probs-per-col (ceiling (/ (float (length problems)) prob-cols))))
           <<fill-instruction>>

           <<fill-layout>>

           <<fill-problems>>

           <<fill-answers>>)

         ;; write the groff file for debugging
         ;; (write-region (point-min) (point-max) (concat fname ".mm"))

         ;; run groff to generate the pdf
         (let* ((default-directory mathsheet-output-directory)
                (ret (shell-command-on-region
                      (point-min) (point-max)
                      (format "groff -mm -e -Tpdf - > %s" (concat fname ".pdf")))))
           (unless (eq ret 0)
             (error "PDF generation failed")))))

   This fills in the instruction line.  It’s just a single string taken
from the config and added to the top of the sheet.

   fill-instruction:
     (goto-char (point-min))
     (search-forward "<<instruction>>")
     (replace-match
      (if (null instruction)
          ""
        (concat ".B \"" instruction "\"")))

   This figures out the column with and spacing between rows and sets
them in registers.

   Column width is computed based on line length.  Line length
(‘\n[.l]’) is reported in basic units, which are 1/72000 of an inch.
Line length is 6.5 inches for letter paper.

   To find the space between rows we take page height, subtract header
and footer, a little more for the instruction, some more for each of the
problems, then divide the remainder by the number of problems per column
plus one.

   Groff has no rules for order of operations but calculates left to
right and numbers are integers, so we need to include parenthesis to
ensure order of operations and use large units (like points) to reduce
loss of precision due to integer division.

   fill-layout:
     (goto-char (point-min))
     (search-forward "<<layout>>")
     (replace-match "")
     (insert (format ".nr ncols %d\n" prob-cols))
     (insert ".nr clen ((\\n[.l]/1000)-((\\n[ncols]-1)*10)/\\n[ncols])\n")
     (insert (format ".nr cl %d\n" probs-per-col))
     (insert ".nr vs (\\n[.p]/1000-(2*72)-20-(12*\\n[cl]))/(\\n[cl]+1)")

   Here we fill the problems into the sheet.  First we group them into
columns.

   fill-problems:
     (goto-char (point-min))
     (search-forward "<<problems>>")
     (replace-match "")
     (insert ".AL\n")
     (let ((colsize probs-per-col))
       (seq-do-indexed
        (lambda (group index)
          (unless (= index 0)
            (insert ".NCOL\n"))
          (dolist (row group)
            (message "convert to eqn %s -> %s" (car row) (mathsheet--convert-to-eqn (car row)))
            (insert (format (if (nth 3 row)
                                ".LI\n.EQ\n%s\n.EN\n.SP \\n[vs]p\n"
                              ".LI\n.EQ\n%s =\n.EN\n\\l'5\\_'\n.SP \\n[vs]p\n")
                            (mathsheet--convert-to-eqn (car row))))))
        (seq-partition problems colsize)))
     (insert ".LE")

   Here we fill in the answers.  They are written as a comma delimited
list at the bottom of the sheet.

   fill-answers:
     (goto-char (point-min))
     (search-forward "<<answers>>")
     (replace-match "")
     (let ((index 0))
       (dolist (row problems)
         (setq index (1+ index))
         (insert
          (format ".EQ\n%d. %s%s\n.EN%s"
                  index
                  (mathsheet--convert-to-eqn (cadr row))
                  (if (< index (length problems)) "\",\"~" "")
                  (if (< index (length problems)) "\n" "")))))


File: docPS9lle.info,  Node: Convenience functions,  Next: Footer,  Prev: Generate PDF,  Up: Code walkthrough

2.6 Convenience functions
=========================

* Menu:

* Add key binding to form::
* Open mathsheet::


File: docPS9lle.info,  Node: Add key binding to form,  Next: Open mathsheet,  Up: Convenience functions

2.6.1 Add key binding to form
-----------------------------

This adds the keybinding to run the mathsheet generator from the
mathsheet form.

     (when (null forms-mode-map)
       (add-to-list
        'forms-mode-hook
        (lambda ()
          (when (string= "mathsheet.el" (buffer-name))
            (define-key forms-mode-map "\C-r" #'mathsheet-generate-sheet)))))


File: docPS9lle.info,  Node: Open mathsheet,  Prev: Add key binding to form,  Up: Convenience functions

2.6.2 Open mathsheet
--------------------

This is a helper to open mathsheet with the configured data file.

     ;;;###autoload
     (defun mathsheet-open ()
       "Open mathsheet."
       (interactive)
       (forms-find-file (locate-file "mathsheet.el" load-path)))


File: docPS9lle.info,  Node: Footer,  Prev: Convenience functions,  Up: Code walkthrough

2.7 Footer
==========

This is the form file footer.

     (provide 'mathsheet)

     ;;; mathsheet.el ends here


File: docPS9lle.info,  Node: Literate Programming,  Prev: Code walkthrough,  Up: Top

3 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 (mathsheet.org) 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 (mathsheet.org) configures emacs to
run those commands whenever this file is saved, which generates the
scripts and readme.



Tag Table:
Node: Top79
Node: Overview1242
Node: Description1432
Node: Audience1784
Node: Examples2069
Node: Requirements3163
Node: Usage3527
Node: Starting Mathsheet3720
Node: Defining a Worksheet3904
Node: Customization5685
Node: Problem Templates6230
Ref: Expression Templates6545
Ref: Equation Templates7153
Ref: Field Rules7522
Ref: Template Examples8759
Node: Code walkthrough9087
Node: Front matter9383
Node: GNU header components9585
Node: Full header9811
Node: Dependencies11538
Node: Variables12152
Node: UI Form14507
Node: Form configuration14767
Node: New record defaults15337
Node: Clean up template rows15997
Node: Layout the actual form17107
Node: Extract configuration from form18310
Node: Validate form fields18600
Node: Extract and parse20186
Node: Initiate sheet generation22944
Node: Problem generation24093
Node: Scan problem24418
Node: Reduce field29096
Node: Replace field32591
Node: DFS visit33500
Node: Fill fields in problem34622
Node: Generate problem set from templates35913
Node: Generate PDF40014
Node: Lay out page40239
Node: Convert calc to EQN41703
Node: Write PDF43471
Node: Convenience functions47916
Node: Add key binding to form48139
Node: Open mathsheet48620
Node: Footer48999
Node: Literate Programming49205

End Tag Table


Local Variables:
coding: utf-8
End:
