Formatting numbers in Lua

Posted on September 9, 2019

I often use Lua to generate solution for homework assignments. Ideally, I want the solution to look exactly how it would look if it were written by hand. But this can be tricker than it appears at first glance. In this post, I’ll explain the issue and how I solve it.

To illustrate the formatting issue, let me consider an example of writing the solution of how to find roots of a quadratic equation. Let’s start with a simple example. First let’s define

\defineenumeration[question]
\defineenumeration[solution]

Then consider the following example.

\startquestion
  Find the roots of $x^2 + 5x + 6 = 0$.
\stopquestion

\startsolution
  Let's start by computing the determinant.
  \startformula
     Δ = b^2 - 4ac = 1
  \stopformula
  Since $Δ > 0$, the roots are given by
  \startformula
    r_{1,2} = \dfrac{ -b \pm \sqrt{Δ} }{ 2a }
            = -2 \text{ and } -3.
  \stopformula
\stopsolution

Now suppose I want to generate a homework assignment with four or five such questions. In order to ensure that I don’t make any mistakes, I generate the questions and the answers using Lua. For simplicity, let’s assume that both roots are real. Then, I can use string.formatters to easily generate the assignment and the solution.

\startluacode

local formatters = string.formatters

local question = formatters[ [[
\startquestion
   Find the roots of $%s x^2 + %s x + %s = 0$.
\stopquestion
]] ]

local solution = formatters[ [[
\startsolution
  Let's start by computing the determinant.
  \startformula
     Δ = b^2 - 4ac = %s.
  \stopformula
  Since $Δ > 0$, the roots are given by
  \startformula
    r_{1,2} = \dfrac{ -b \pm \sqrt{Δ} }{ 2a }
            = %s \text{ and } %s.
  \stopformula
\stopsolution
]] ]

local sqrt = math.sqrt

assignment = assignment or { }

assignment.roots = function(a, b, c)
  context(question(a == 1 and "" or a, b, c))

  D = b^2 - 4*a*c
  r1 = (-b + sqrt(D))/(2*a)
  r2 = (-b - sqrt(D))/(2*a)

  context(solution(D, r1, r2))

end
\stopluacode

Then, in the homework assignment, I can generate the above question and its solution using:

\ctxlua{assignment.roots(1, 5, 6)}

The above generated solution works well when all numbers are integer valued. However, the generated solution is not ideal when some of the calculations result in floats. For example:

\ctxlua{assignment.roots(1, 4, 2)}

will generate:

\startquestion
  Find the roots of $x^2 + 4x + 2 = 0$.
\stopquestion

\startsolution
  Let's start by computing the determinant.
  \startformula
     Δ = b^2 - 4ac = 8.0.
  \stopformula
  Since $Δ > 0$, the roots are given by
  \startformula
    r_{1,2} = \dfrac{ -b \pm \sqrt{Δ} }{ 2a }
            = -0.5857864376269 \text{ and } -3.4142135623731.
  \stopformula
\stopsolution

Technically, the solution is correct. But one never types a float with a precision of 12 decimal places. Of course, I could change the %s in the template to %.3f as follows:

local solution = formatters[ [[
\startsolution
  Let's start by computing the determinant.
  \startformula
     Δ = b^2 - 4ac = %.3f.
  \stopformula
  Since $Δ > 0$, the roots are given by
  \startformula
    r_{1,2} = \dfrac{ -b \pm \sqrt{Δ} }{ 2a }
            = %.3f \text{ and } %.3f.
  \stopformula
\stopsolution
]] ]

For the second problem, this will generate:

\startquestion
  Find the roots of $x^2 + 4x + 2 = 0$.
\stopquestion

\startsolution
  Let's start by computing the determinant.
  \startformula
     Δ = b^2 - 4ac = 8.000.
  \stopformula
  Since $Δ > 0$, the roots are given by
  \startformula
    r_{1,2} = \dfrac{ -b \pm \sqrt{Δ} }{ 2a }
            = -0.586 \text{ and } -3.414.
  \stopformula
\stopsolution

which is partially acceptable (when typesetting the solution by hand, one would uses Δ = 8 rather than Δ = 8.000) but for the first problem, we now get

\startquestion
  Find the roots of $x^2 + 5x + 6 = 0$.
\stopquestion

\startsolution
  Let's start by computing the determinant.
  \startformula
     Δ = b^2 - 4ac = 1.000.
  \stopformula
  Since $Δ > 0$, the roots are given by
  \startformula
    r_{1,2} = \dfrac{ -b \pm \sqrt{Δ} }{ 2a }
            = -2.000 \text{ and } -3.000.
  \stopformula
\stopsolution

which is not ideal.

So, to typeset such examples, I need to format numbers as follows:

However, once you think about it, the above spec is not complete. In addition, what we want is that

Who knew simply formatting numbers could be so complicated! Anyways, here is a simple function that does this formatting:

local mathtype, floor  = math.type, math.floor
local format, strlen, match = string.format, string.len, string.match

formatnumber = function(a)
    if mathtype(a) == "integer" or floor(a) == a then
        return format("%d", a)
    else
        str = format("%s", a)
        fmt = format("%.3f", a)
        exp = format("%.3e", a)
        if strlen(str) < strlen(fmt) then
            return str
        elseif fmt == "0.000" then
             x = match(exp, "(.*)e")
             y = match(exp, "e(.*)")
             return format("%s \\times 10^{%d}", x, y)
        else
            return fmt
        end
    end
end

We first check if the number is an integer (using math.type(a) == "integer") or if the number is a float of the type 8.0 (using math.floor(a) == a) and if so, format the number using %d.

We then next check if casting the number to a string leads to a shorter string than formatting it using %.3f. If so, we format it using %s. This ensures that a number like 8.2 is typeset as 8.2 rather than 8.200.

Finally, we check if formatting the number using %.3f gives "0.000". If so, we typeset the tex equvalent of %.3e.

Phew!

To use this code, I use %s in my templates, and then call the template as

context(solution(formatnumber(D), formatnumber(r1), formatnumber(r2)))

This gives me correct formatting in all the edge cases.


This entry was posted in Formatting and tagged luatex, programming.