Typesetting Markdown – Part 7: Mathematics

This part of the series describes a way to produce beautifully typeset mathematics.


Part 6 introduced using R to perform computation on variables sourced from YAML files. This section walks through an example of how to typeset math and provides a practical usage of document variables. Along the way, this part will attempt to answer a deceptively simple question: what’s the return on investment for an electric vehicle, roughly?

Here is a preview of the summary page that this part creates:

Preview of Summary

And a preview of beautifully typeset math:

Preview of Typeset Math

Throughout this part, take note of the software development rhythm. The whole process is separated into dozens of steps. Each step is confirmed to work as expected before further development proceeds.

Build Script

Commands typically use -V (or -v) and --verbose (rather than -d and --debug) to show what the script is doing while running. Confusing the issue, some commands interpret -v and --version as instructions to display version information. For now, let’s swap debug for verbose by abstracting these common options to the build template. Change the arguments as follows:

  1. Edit build-template.
  2. Change -d|--debug to -V|--verbose.
  3. Change the ARGUMENTS array as follows:
  "V,verbose,Log messages while processing"
  "h,help,Show this help message then exit"

This also begins to address some slight duplication with dashes and double-dashes. Adjust the usage function as follows:

  printf "Usage: %s [OPTIONS...]\n\n" "${SCRIPT_NAME}" >&2

  local -r PADDING=2
  local -r LEN=$(echo "${args[@]}" | \
    awk -F"," '{print length($2)+'${PADDING}'}' | \
    sort -n | tail -1)

  for argument in "${args[@]}"; do
    IFS=',' read -ra arg <<< "${argument}"

    printf "  -%s, --%-${LEN}s%s\n" "${arg[0]}" "${arg[1]}" "${arg[2]}" >&2

The code determines the longest command-line argument then adjusts the padding such that the descriptive texts displayed for the option are always left-aligned. Take a closer look at the most complex part:

  local -r LEN=$(echo "${args[@]}" | \
    awk -F"," '{print length($2)+'${PADDING}'}' | \
    sort -n | tail -1)

Having already parsed the comma-separated list of command-line arguments into the args variable, the code:

  1. Pipes each line from the ARGUMENTS list into awk (via args[@]).
  2. Instructs awk to treat commas as field separators (-F",").
  3. Prints the second field’s length to standard output (print length($2)).
  4. Breaks out of the single quote environment to add padding to the length, otherwise awk will be given the literal value of ${PADDING} instead of the expanded value of 2. Notice that $2 is interpreted by awk to mean the second field while ${PADDING} is expanded by bash, on the same line, even though both awk and bash use $ sigils to signify variable references.
  5. Sorts the lengths in numerical order (sort -n).
  6. Retrieves the last line from the sorted list (tail -1).

The printf line has a few minor changes:

Apply these new changes to all scripts that use the build template (such as ci and intarsia) as follows:

  1. Concatentate the dependencies using DEPENDENCIES+=(.
  2. Remove the dashes and double-dashes from the ARGUMENTS list.

Presuming that build-template is on the path and the ci script is in the current directory, run ./ci -h to see the new output format, which will resemble the following:

$ ./ci -h
Usage: ci [OPTIONS...]

  -a, --artefacts  Artefact path (default: artefacts)
  -f, --filename   Output PDF file (default: output)
  -h, --help       Show this help message then exit

The initial changes are complete.

Continuous Integration Script

When pandoc uses a template (via its --template argument), it tries to substitute values delimited by dollar symbols ($) with the variables defined in the YAML header. This works well until pandoc encounters a currency value that is prefixed with a dollar symbol. There are rudimentary solutions that use lua to replace placeholders with YAML metadata values, but they have the following drawbacks:

This means that R Markdown and regular Markdown files must be processed via pandoc in two distinct ways. Previously, it was possible to mix inline R variable references and pandoc’s $ template variables. By introducing currency, pandoc can no longer perform variable substitution of template variable definitions. Attempts to do so will result in errors such as:

"template" (line 20, column 59):
unexpected "3"
expecting letter

To resolve this issue, the ci script must be modified as follows:

These changes are straightforward and will not be discussed at length. The key changes include creating a utile_convert_markdown function that calls pandoc similar to the previous invocation:


  $log "Convert ${FILE_CAT} to ${FILE_TEX}"
  pandoc "${FILE_CAT}" \
    --template "${FILE_CAT}" \
    --metadata pagetitle="${ARG_DOC_TITLE}" | \
    pandoc -t context > "${FILE_TEX}"

Here, a pagetitle argument is introduced to suppress the warning pandoc gives when converting a document that has metadata (i.e., a YAML header). This change was instigated by removing the code fragment that suppresses messages written to standard error (i.e., 2>/dev/null) to see problems that arise from document conversion. With standard error no longer suppressed, the missing title warning would otherwise be displayed. Rather than hard-code a specific title, an option was added (-t) to let the user specify the title from the command-line. The title is useless because converting Markdown documents to PDF files involves a separate mechanism to set the document title (using ConTeXt setups).

In addition, a new utile_convert_rmarkdown function calls pandoc to convert files that have already been processed using knitr into ConTeXt code:

utile_convert_rmarkdown() {
  $log "Convert ${FILE_CAT} to ${FILE_TEX}"
  pandoc "${FILE_CAT}" -t context > "${FILE_TEX}"

See build_document(), argument() and the variable initialization at the bottom of the ci script for more details. (Available in the download section.)

R Integration

This section describes changes to the bootstrap script and creation of other scripts necessary to perform calculations for the mathematics shown in the final document.

Begin by installing the following packages:

install.packages( c( 'XML', 'FinancialMath', 'english' ) )

Bootstrap Script

The bootstrap.R script contains superfluous resources from the previous part. Change the top of the file as follows:

library( 'english' )
library( 'knitr' )
library( 'tools' )
library( 'yaml' )

source( 'cardinal.R' )
source( 'expression.R' )
source( 'milage.R' )
source( 'money.R' )

Changes to the bootstrap script are complete.

Fuel Economy Script

Representational State Transfer (REST) is a software development approach that defines a set of operations to store, retrieve, and update information using standard Web protocols. The rOpenGov/mpg R package is no longer maintained and has a bug that prevents it from using the fueleconomy.gov REST services. Although reinventing the wheel is often wasteful, implementing a solution from scratch for demonstration purposes seems faster than cloning and maintaining the existing source code.

Error handling the REST requests is left as an exercise for the reader.

The Extensible Markup Language (XML) defines how to encode documents that are meant to be machine- and developer-readable. XML was not designed to be as svelt and space-efficient as other structured formats (YAML, JSON, TOML, and such), so arguing about its verbosity is moot. Nor was it designed to be parsed quickly, making large XML documents and datasets more burdensome than other formats. One benefit of using XML is that it has an entire ecosystem of tools and specifications built around data validation, which is possible because XML can define document formats without ambiguity.

Edit a new file called milage.R in the book directory. Put atop the file:

require( 'XML' )

xml.url.base <- 'https://www.fueleconomy.gov/ws/rest/vehicle/'
xml.url.vid <- paste0( xml.url.base,
  'menu/options?year=YEAR&make=MAKE&model=MODEL' )
xml.url.mpg <- paste0( xml.url.base, 'VID' )

default.year <- 1997
default.make <- 'Toyota'
default.model <- 'RAV4 2WD'

MPG_TO_KPL <- 0.4251
KWH_MI_TO_KWH_KM <- 1.609344

The above lines define global variables for referencing the government’s public REST API. The xml.url.vid provides a way to download information about a particular vehicle, given its year, make, and model. Here are a couple of examples:

Since the search results can contain multiple matches, for simplicity’s sake the last item from the search results must be returned. To accomplish this task, first create a function that can download an XML document:

milage.download.url <- function( url, silent = TRUE ) {
  temp <- tempfile()
  download.file( url = url, destfile = temp, quiet = silent )
  data <- xmlParse( temp )
  unlink( temp )

  return( data )

Even though calling xmlParse would be less code, the function cannot handle complex Uniform Resource Locators (URLs) with parameters (e.g., ?year=1997). Create a function to create a URL for retrieving the unique vehicle indentifier (VID) associated with a vehicle’s particular year, make, and model:

milage.vid.url <- function(
  year = default.year, make = default.make, model = default.model,
  url.vid = xml.url.vid ) {

  url.vid <- gsub( 'YEAR', year, url.vid )
  url.vid <- gsub( 'MAKE', make, url.vid )
  url.vid <- gsub( 'MODEL', model, url.vid )
  URLencode( url.vid )

Breaking code into short functions (e.g., fewer than twenty lines long) promotes reusability and helps with both testing and isolating bugs.

milage.vid <- function(
  year = default.year, make = default.make, model = default.model,
  url.vid = xml.url.vid ) {

  url.vid <- milage.vid.url( year, make, model, url.vid )
  document <- milage.download.url( url.vid )

    path = '//menuItems/menuItem[last()]',
    'value' )

Passing the xml.url.vid by default avoids the bug that plagues the rOpenGov/mpg software: a hard-coded protocol, domain name, and path in multiple places with no easy way to override its value. The gsub calls replace the placeholders YEAR, MAKE, and MODEL with the year, make, and model variable values in the VID URL, respectively. This is a simple way to generate the URL needed to query the government database using REST over HTTP.

The XML Path Language (XPath) provides CSS-like selectors for XML. That is, XPath allows the extraction of information from XML documents by defining the path to an element, not unlike the slash-separated paths to a file in a file system.

The XPath expression requests the value element nested within the last menuItem element. The double-slash (//) instructs the XPath parser to start its search at the document’s top, also called the root:

path = "//menuItems/menuItem[last()]/value"

Mirror the VID URL function by creating a function that returns the URL to retrieve the vehicle’s descriptive XML document:

milage.mpg.url <- function(
  year = default.year, make = default.make, model = default.model,
  url.vid = xml.url.vid, url.mpg = xml.url.mpg ) {

  vid <- milage.vid( year, make, model, url.vid )
  gsub( 'VID', vid, url.mpg )

Obtaining the fuel economy document, which will be downloaded once and used multiple times, can be implemented as follows:

milage.economy.document <- function(
  year = default.year, make = default.make, model = default.model,
  url.vid = xml.url.vid, url.mpg = xml.url.mpg ) {

  url.mpg <- milage.mpg.url(
    year, make, model, url.vid, url.mpg

  milage.download.url( url.mpg )

Defining a function to extract an element value from the XML document as a number is accomplished with a couple of lines:

milage.extract <- function(
  document, path = '//vehicle', element ) {

  xpath <- paste( path, element, sep = '/' )

  sapply( document[ xpath ], as, 'numeric' )

Ensuring that the code works as expected is now trivial. Start R in the book directory and then type in the following:

source( 'milage.R' )
doc <- milage.economy.document()
xpath <- '//vehicle/comb08'

An XML document appears, showing the available values. After storing the XML document in a variable named doc and the path through the element hierarchy in xpath, the value of doc[ xpath ] is:


[1] "XMLNodeSet"

The value is present, but not usable directly. Convert the string element to a number using the following code:

  sapply( doc[ xpath ], as, "numeric" )

In R, the as function attempts to coerce an object to a given class. In this case, the value associated with the XMLNodeSet object will be converted to a numeric value. The sapply function executes the as function for all objects returned from document[ xpath ]. Knowing that there is only one object returned, a shorter way to write the last line of the milage.extract function is to call as directly:

  as( document[ xpath ][[1]], "numeric" )

Hard-coding the [[1]] index is vulgar when sapply can extract the value.

Avoid leaking information about the XML element structure into 01.Rmd by wrapping element value extraction into short functions:

milage.economy.ice <- function( document ) {
  milage.extract( document, element = "comb08" )

milage.economy.ev <- function( document ) {
  milage.extract( document, element = "combE" )

Make sure that all units are metric by converting the US government data from imperial using the following functions:

milage.to.kpl <- function( economy ) {
  round( (1 / MPG_TO_KPL * 100) / economy, 2 )

milage.to.kWh.km <- function( efficiency ) {
  round( 1 / KWH_MI_TO_KWH_KM * efficiency / 100, 4 )

The milage functions are complete.

Money Script

A cost comparison between a vehicle that burns gasoline using an internal combustion engine (ICE) and an electric vehicle propelled by energy from batteries must include a regular payment amount to be informative.

Create a new file named money.R inside the books directory. At the top, import the libraries necessary to compute loan repayment schedules along with some default values:

require( 'FinancialMath' )
require( 'pander' )

default.principal <- 50000
default.payments <- 60
default.interest <- 2.99
default.frequency <- 4

The FinancialMath library provides functions that wrap the math behind calculating amortization. Compute amortization by defining a new function with a name and function prototype that exposes a slightly less cryptic interface:

money.loan.period <- function(
  principal = default.principal,
  payments = default.payments,
  interest = default.interest,
  frequency = default.frequency ) {

    Loan = principal,
    n = payments,
    i = money.percentage( interest ),
    pf = frequency )["PMT", 1]

For a loan amount of $50,000 at 2.99% interest, repaid over 60 months, with a payment frequency of 48 payments per year (i.e., four times per month), the amort.period function returns the following values:

Loan       50000.000000
PMT          849.032690
Eff Rate       0.029900
i^(48)         0.029471
Periods       60.000000
Years          1.250000
At Time 1:     1.000000
Int Paid      30.698702
Princ Paid   818.333989
Balance    49181.666011

Exracting the payment amount (PMT) by its name is a little more future-friendly than by its index. The amort.period function returns a matrix of values. To determine the type returned, ask R for the class, such as:

class( amort.period( Loan=50000, n=60, i=2.99/100, pf=48 ) )

Knowing the data type (a matrix), leads to understanding that the first column contains the addressable list of names and the second column contains the values. Chances are the name will remain the same even if the order of returned values changes. Thus the payment value can be retrieved using:

amort.period( Loan=50000, n=60, i=2.99/100, pf=48 )["PMT", 1]

Another aspect of the loan that may be of interest is the payment schedule. Retrieve the schedule as follows:

money.loan.schedule <- function(
  principal = default.principal,
  payments = default.payments,
  interest = default.interest,
  frequency = default.frequency ) {

    Loan = principal,
    n = payments,
    i = money.percentage( interest ),
    pf = frequency )$Schedule

As it stands, the schedule cannot be embedded in a PDF file using the framework in place. The data must be converted into an ASCII table. Notice that this leads to a clean separation of concerns: one function extracts the data (money.loan.schedule) while the other converts it into the required form (money.loan.schedule.table). Append the following snippet:

money.loan.schedule.table <- function(
  principal = default.principal,
  payments = default.payments,
  interest = default.interest,
  frequency = default.frequency ) {

  schedule <- money.loan.schedule(
    principal, payments, interest, frequency )

  panderOptions( 'digits', 6 )
  panderOptions( 'round', 3 )
  panderOptions( 'keep.trailing.zeros', TRUE )

  pander( schedule )

Each panderOptions call applies number formatting to the ASCII table. These options help align the columns of numbers and set a consistent number of decimal places. The last line of the function calls pander to generate the ASCII table based on the payment schedule listing.

Append the following helper function that calculates taxes on a given value:

money.tax <- function( n, rate ) {
  rate <- money.percentage( rate )

  n + n * rate

Most financial institutions or companies that issue loans advertise their loan interest rates in per cent form (%). To set the value verbatim in the variable definitions file means that the actual value must be divided by 100 when calling the amort functions. The following code segment converts a given value to a percentage value for subsequent computations:

money.percentage <- function( n ) {
  n / 100

To help format numbers that represent money and strings that determine profit or loss, append the following functions:

money.currency <- function( n, d = 0 ) {
    formatC( n, format = "f", digits = d, big.mark = "," )

money.profit.label <- function( delta ) {
  if( delta >= 0 ) 'savings' else 'expenses'

Save the file, the money-related functions are complete.

Expression Script

Inside the forthcoming definitions.yaml file are expressions such as:

  total: $tax.region.amount$ + $tax.federal.amount$

These expressions would normally be interpreted by R as a string of characters, not a number resulting from addition. To make evaluating expressions defined in YAML files possible, create a new file named expression.R that contains the following:

x <- function( s ) {
      r = eval( parse( text = s ) )

      if( is.null( r ) ) { s }
      else if( is.atomic( r ) ) { r }
      else { s }
    warning = function( w ) { s },
    error = function( e ) { s })

Care must be taken to ensure that any value passed into the x function are safe for R to evaluate. This means that values in definitions.yaml must come from a trusted, reliable, and safe source.

Clean Up

The following files may be removed:

Delete them from command-line using rm as follows:

rm climate.R excursion.R *.csv


Replace definitions.yaml with the following content:

  make: Toyota
  model: RAV4 2WD
  name: $ice.make$ $ice.model$
  year: 1997
    annual: 1001.41

  make: Hyundai
  model: Kona Electric
  name: $ev.make$ $ev.model$
  year: 2019
    annual: 272.29
  msrp: 44999
  charger: 4000
      name: Provincial
      value: 5000
      name: Scrap-It
      value: 6000
    total: $ev.rebate.regional.value$ + $ev.rebate.additional.value$

    price: 1.60
    unit: litre
    price: 0.0945
    unit: kWh

  annual: 13100

  term: 96
  interest: 2.99
  frequency: 4

    name: BC
    amount: 7
    name: Canada
    amount: 5
  total: $tax.region.amount$ + $tax.federal.amount$

Finding out the exact name of a particular make and model can be a bit hit-or-miss. Use the REST API to list all vehicle models for a particular manufacturer. For example, change Toyota to Hyundai in the following URL to list all vehicles that Hyundai produced in 1997:


Cost-Benefit Analysis

All the setup to this point makes writing a dynamic document much easier. Edit 01.Rmd, clear out its contents, then start the document as follows:

# Cost-Benefit Analysis

This document is a cost-benefit analysis for purchasing an
electric vehicle.

If building succeeded, the output.pdf document resembles the following figure:

Cost-Benefit Analysis

If building did not succeed, look at the output from the ci script, which may contain clues to help resolve the issue.

Define global variables—available to the entire document—that are the results from calculating YAML variable expressions and the total loan amount for a new electric vehicle plus its residential charger:

``` {r, echo=FALSE}
ev.rebate <- x( v$ev$rebate$total )
money.tax.rate <- x( v$tax$total )
money.loan <- money.tax(
    v$ev$msrp - ev.rebate + v$ev$charger,

The first line indicates that what follows is an R statement that spans multiple lines. Using echo=FALSE prevents the R statement itself from being included in the output document. The fourth line calls the money.tax function, storing the result in money.loan upon completion. The remaining lines provide the parameter values to money.tax(). Quite important are the calls to the x function, such as:

money.tax.rate <- x( v$tax$total )

Recall that v$tax$total refers to a sum (i.e., $tax.region.amount$ + $tax.federal.amount$). Since the sum is an addition expression, the YAML variable must be evalulated before R can use it as a numeric value.

Armed with this knowledge, insert a blank line then the following paragraph:

The `r v$ev$name` has an MSRP of
`r money.currency( v$ev$msrp )`, or
`r money.currency( money.tax( v$ev$msrp, money.tax.rate ) )`
with `r money.tax.rate`% taxes. `r v$tax$region$name` offers a
`r money.currency( v$ev$rebate$regional$value )`
`r v$ev$rebate$regional$name` rebate on top of the
`r money.currency( v$ev$rebate$additional$value )`
`r v$ev$rebate$additional$name` program rebate,
which shaves `r money.currency( ev.rebate )`
off the sticker. On average, a residential Level 2 fast
charger costs `r money.currency( v$ev$charger )` to
purchase and install. All told, this brings the loan to
**`r money.currency( money.loan )`**.

Although allowing end-users to provide their own values would be useful, it would also require sanitizing the inputs. One possibility would be to call eval.secure instead of eval. Another possibility would be to eliminate permitting expressions in YAML documents altogether, deferring calculations until writing the document. By eliminating the need for eval( parse( ... in documents, end-user values can be written into the document directly.

The following figure shows the generated paragraph:

Calculate Loan Amount

Including accelerated amortized payments takes some effort:

``` {r, echo=FALSE}
loan.payment.month <- money.loan.period(
  principal = money.loan,
  payments = v$loan$term,
  interest = v$loan$interest,
  frequency = v$loan$frequency * length( month.name )

loan.payment.accel <- loan.payment.month / v$loan$frequency
loan.payment.label <- ''

if( v$loan$frequency > 1 ) {
  loan.payment.label <- ', accelerated'

Calling money.loan.period is the workhorse that determines the total monthly payment. Yet accelerated payments pay off the debt faster because more payments (albeit smaller) are made against the principal. How many payments take place per month is controlled by loan.frequency in the YAML file. Setting it to 4, for example, would be 48 payments per year.

Using the length of month.name to derive 12 is a bit cheeky: it’s like addressing the Y2K problem but for when the code is run in an extrasolar planetary system that has a different number of months.

Comparing the loan frequency to 1 allows the prose to include an indicator that the payments are accelerated.

The english package now comes into use:

`r Indefinite( v$loan$term, words = FALSE )`-month term at
`r indefinite( v$loan$interest, words = FALSE )`% interest
rate is `r money.currency( loan.payment.month, 2 )`
per month`r loan.payment.label`.

Calling Indefinite inserts a capitalized indefinite article if the given number starts with a vowel sound. For example, Indefinite( 86 ) will write An eighty-six to the document and indefinite( 86, words = FALSE ) will write an 86 instead. The output resembles the following:

Compute Monthly Payments

Assessing annual vehicle maintenance costs is not easy, especially for new models without a multi-year history. The Kona Electric annual maintenance cost is listed as $206 USD, equivalent to $272.29 CAD at time of writing.

Until a registration-free public resource is available for looking up average annual maintenance costs for a vehicle, using a service record from the vehicle’s primary auto repair shop is a fair substitute. For my vehicle, the last several years of maintenance work was $1001.41 per year, on average.

Let’s calculate the total monthly payments for a new electric vehicle:

``` {r, echo=FALSE}
MONTHS <- length( month.name )

milage.ev <- milage.economy.document(
  v$ev$year, v$ev$make, v$ev$model

milage.ev <- milage.economy.ev( milage.ev )
milage.ev <- milage.to.kWh.km( milage.ev )

cost.ev.maintain.month <- v$ev$maintenance$annual / MONTHS
cost.ev.km <- milage.ev * v$energy$electricity$price
cost.ev.annual <- v$travel$annual * cost.ev.km
cost.ev.month <- cost.ev.annual / MONTHS
cost.ev.total <- loan.payment.amount +
  cost.ev.maintain.month +

The code starts by storing the number of months per year. Next, it calls milage.economy.document to get the XML document that describes the vehicle given by the year, make, and model variables from definitions.yaml (technically from artefacts/interpolated.yaml). Immediately after, milage.economy.ev extracts the fuel economy value for the electric vehicle back into milage.ev. Calling milage.to.kWh.km converts milage.ev from kWh per 100 miles to kWh per kilometre. Using kilometres eases calculating the vehicle’s annual electrical costs.

Precalculating the various costs into cost.ev variables simplifies writing the next passages:

`r Indefinite( v$ev$year, words = FALSE )` model costs
`r money.currency( v$ev$maintenance$annual )` to maintain
annually, or `r money.currency( cost.ev.maintain.month, 2 )`
per month; uses `r milage.ev` `r v$energy$electricity$unit` per
kilometre in electricity, which works out to
`r money.currency( cost.ev.km, 4 )` per kilometre at
`r money.currency( v$energy$electricity$price, 4 )` per
`r v$energy$electricity$unit`; and, assuming an annual
average driving distance of
`r prettyNum( v$travel$annual, big.mark = ',')` kilometres,
costs `r money.currency( cost.ev.annual, 2 )` per year in
electricity, or `r money.currency( cost.ev.month, 2 )` per month.

Thus the total operational cost for the vehicle is
**`r money.currency( cost.ev.total, 2 )`** per month.

Everything should be self-explanatory, especially given that the output resembles the following:

EV Monthly Costs

This cost-benefit analysis compares an electric vehicle to a gas guzzling vehicle that has been paid in full. Predicting gas prices is as likely as predicting the stock market. Some forecasts suggest that the world average will be around $1.40 CAD per litre into 2020. I opted for $1.60 due to living on an island.

Repeat a similar series of calculations for the gas guzzler:

``` {r, echo=FALSE}
milage.ice <- milage.economy.document(
  v$ice$year, v$ice$make, v$ice$model

milage.ice <- milage.economy.ice( milage.ice )
milage.ice <- milage.to.kpl( milage.ice )

cost.ice.maintain.month <- v$ice$maintenance$annual / MONTHS
cost.ice.km <- milage.ice * v$energy$gasoline$price
cost.ice.annual <- v$travel$annual * cost.ice.km / 100
cost.ice.month <- cost.ice.annual / MONTHS
cost.ice.total <- cost.ice.maintain.month + cost.ice.month

cost.delta <- cost.ice.total - cost.ev.total
cost.delta.paid <- cost.ice.total -
  (cost.ev.total - loan.payment.amount)

The milage.to.kpl function converts imperial miles per gallon to litres per 100 kilometres. Since the annual distance is given in kilometres, the annual costs must be divded by 100 so that the units match.

Learning how the purchase affects monthly expenses is the goal. Begin with a summary statement that captures the total monthly costs for an existing, fully paid, non-electric vehicle:

The `r v$ice$name` guzzles `r milage.ice` litres every 100
kilometres. At `r money.currency( v$energy$gasoline$price, 2 )`
per `r v$energy$gasoline$unit`, driving
`r format( v$travel$annual, big.mark = ',' )` kilometres
per year costs `r money.currency( cost.ice.annual, 2 )`
in gasoline, or `r money.currency( cost.ice.month, 2 )` monthly.
Annual maintenance is
`r money.currency( v$ice$maintenance$annual, 2 )`, which is
`r money.currency( cost.ice.maintain.month, 2 )` per month.

The total operational cost for the unindebted vehicle is
**`r money.currency( cost.ice.total, 2 )`** per month.

With the symmetrical calculations of the EV and ICE in place, the document resembles:

ICE Monthly Costs

Now add the goal:

During the first `r v$loan$term / MONTHS` years, monthly
`r money.profit.label( cost.delta )` increase by
`r money.currency( abs( cost.delta ), 2 )`. After clearing
the loan, the monthly `r money.profit.label( cost.delta.paid )`
increase to **`r money.currency( abs( cost.delta.paid ), 2 )`**
per month, replacement battery pack notwithstanding.

Watch the output change to reveal the return on investment:

Return on Investment

Your—ahem—milage may vary.


Nobody enjoys reading walls of text. As an exercise for the reader, insert headings to tear down that wall into digestible chunks:

Summary Document

Reuse the YAML variables whenever possible.


A few microtypographical issues are present in the resulting document, such as words extending into the page margins. ConTeXt has \setup commands for fine control over typesetting behaviour.

Edit styles/alignment.tex to reduce the likelihood of text protruding into the margins:


Edit styles/paragraphs.tex to increase the whitespace between paragraphs:


Create styles/bullet.tex with the following contents:



This configures bullet lists to be indented and uses a specific Unicode character for the bullet itself.

Update styles/fonts.tex to define a font for math, otherwise ConTeXt will complain with a cryptic error about \\Umathquad\\displaystyle:

  [BookTypeface] [mm] [math] [schola] [default] [rscale=auto]

Be sure to add references to the new .tex files within main.tex by appending the following lines to the list of \input macros:

\input alignment
\input paragraphs
\input bullet

The styles are configured.


By default, formulas are not passed from pandoc into ConTeXt. The tex_math_dollars argument enables this feature.

Edit ci and update utile_convert_markdown() to have pandoc interpet $$ tokens as formula delimeters. Change the last line in the function to:

    pandoc -t context+tex_math_dollars > "${FILE_TEX}"

The + symbol indicates that pandoc is to enable the feature; a - symbol forces pandoc to disable the feature.

Edit 01.Rmd again, then append the following text:

## Calculations

This section describes the mathematics behind calculations used
throughout the document.

### Amortization

Amortization is the distribution of loan repayments into multiple
installments, determined by a schedule. The general formula to
calculate a payment follows:

A = P \times \frac{ r(1 + r)^n }{ (1 + r)^n - 1 }


* $A$ is the periodic amortization amount;
* $P$ is the principal amount borrowed;
* $r$ is the periodic interest rate divided by 100; and
* $n$ is the total number of payments.

After the docment is re-rendered, it resembles the following:

Typeset Math

Pandoc writes the equation to body.tex, bracketed by \startformula and \stopformula macros. The syntax for the math uses TeX macro notation. Note the difference between $$ (a multi-line formula) and $x$ (an inline formula). Spaces are not permitted between the dollar symbols for inline formulas.

Most of the math above will probably be familiar, including:

ConTeXt, built on TeX, supports TeX macros. Find a TeX reference card online to see many more macros available.

Repayment Schedule

Let’s round out the document by including the loan repayment schedule.

Edit 01.Rmd with the following text:

# Loan Repayment Schedule

The following table shows interest and principal payments on
the **`r money.currency( money.loan )`** loan:

```{r, echo=FALSE}
  principal = money.loan, 
  payments = v$loan$term,
  interest = v$loan$interest,
  frequency = v$loan$frequency * length( month.name )

Table: `r v$ev$name` Repayment Schedule

Upon saving, the output PDF file shows:

Loan Repayment Schedule

The document is complete.


Download the resulting files, distributed under the MIT license.


This part described how to typeset math in documents, addressed minor duplication issues with the build script template, and showed a practical application of document variables. A few adventures in ConTeXt were encountered, including microtypography, line spacing, bullet lists, and a font for math.

This part was partially inspired by Bret Victor’s blog post, What Can A Technologist Do About Climate Change? Notably, the interactive section on model-driven debate where numbers in a paragraph can be recalculated by changing the value for any (green) number given.

Part 8 charges headlong into typesetting novels.


About the Author

My career has spanned tele- and radio communications, enterprise-level e-commerce solutions, finance, transportation, modernization projects in both health and education, and much more.

Delighted to discuss opportunities to work with revolutionary companies combatting climate change.