# Pure scripts

Pure scripts run where speed matters: once per pipeline row, thousands of times in one
schedule optimisation. They have the whole language and its libraries but no `ctx`,
and they cannot load other scripts, so everything a Pure script needs arrives in its
data. Where CoCoCo knows the shape of that data, such as a pipeline's columns or a
product template's components, reading a field that doesn't exist is an error when
the script is checked.

## Data pipeline stages

A pipeline stage runs once for every row. The row is `input`. Return the row to keep,
changed as you like, or nil to drop it.

```lua
-- A pipeline stage runs once for every row, so it is Pure: the language only,
-- with no ctx. The row is `input`. Return the row to keep, changed as you like,
-- or nil to drop it.
if input.status == "cancelled" then return nil end

local net = input.gross / (1 + input.taxRate)

return {
  orderId = input.orderId,
  net = math.floor(net * 100 + 0.5) / 100,
  region = string.upper(input.region),
}
```

## Report preparation

A report's preparation script runs before its outputs are generated. `input` holds the
report's `parameters`, `context` and `datasets`. Change the datasets and return
`input`; the parameters stay as they were passed. The Reports manual shows how a
report declares its preparation script.

```lua
-- A report's preparation script runs before its outputs are generated. `input`
-- holds the report's parameters, context and datasets. Change the datasets and
-- return `input`; the parameters stay as they were passed.
local totals = {}
local customers = {}
for _, row in ipairs(input.datasets.orders) do
  if totals[row.customer] == nil then
    totals[row.customer] = 0
    table.insert(customers, row.customer)
  end
  totals[row.customer] = totals[row.customer] + row.amount
end

local summary = {}
for _, customer in ipairs(customers) do
  table.insert(summary, { customer = customer, total = totals[customer] })
end

input.datasets.summary = summary
return input
```

## Scheduler penalty functions

A penalty function scores each candidate schedule the scheduler considers; the
scheduler prefers the lowest score. It defines `exports.calculatePenalty(schedule)`,
which receives the schedule's `jobs` (each with its `id`, `isLate`, `priority` and
`lateness`) and `operations` (each with its `id`, `jobId`, `machine`, `startTime`,
`endTime` and `setupTime`), and returns a number. It runs very often, so its limits
are tight. If it fails or doesn't return a number, the schedule scores 0.

```lua
-- The scheduler calls exports.calculatePenalty for every candidate schedule it
-- considers, thousands of times in one optimisation, and prefers the lowest
-- score. So the function is Pure and has a tight time limit.
exports.calculatePenalty = function(schedule)
  local penalty = 0

  -- A late job costs ten points per unit of lateness, more for high priority.
  for _, job in ipairs(schedule.jobs) do
    if job.isLate then
      penalty = penalty + job.lateness * 10 * (1 + job.priority)
    end
  end

  -- Every unit of setup time costs a point.
  for _, operation in ipairs(schedule.operations) do
    penalty = penalty + operation.setupTime
  end

  return penalty
end
```

## Product template resolvers

A product template's resolver adjusts the job a template creates to the options chosen
for it. `input.config` holds those options, `input.components` the template's
components and `input.finishing` its finishing flags. It returns a patch: fields to
change on a component, named by its `localId`; components to leave out; and finishing
flags to set or clear. Returning nil changes nothing.

```lua
-- A product template's resolver adjusts the job a template creates to the
-- options chosen for it. `input.config` holds those choices,
-- `input.components` the template's components and `input.finishing` its
-- finishing flags. Return a patch: fields to change on a component (by its
-- localId), components to leave out, and finishing flags to set or clear.
-- Returning nil changes nothing.
local patch = { components = {}, exclude = {}, finishing = {} }

if type(input.config.pages) == "number" then
  patch.components.body = { totalPages = input.config.pages }
end
if input.config.cover == "none" then
  table.insert(patch.exclude, "cover")
end
if input.config.finish == "gloss" then
  patch.finishing.LAMINATING = true
end

return patch
```

## JDF templates

A JDF template is a stored script that another script loads with
`ctx.jdf.loadTemplate(name)`, and a JMF template one it loads with
`ctx.jmf.loadTemplate(name)`. The template runs on its own, with no data and no `ctx`,
and the value it returns is what the loading script receives. Return data, such as
the settings a ticket needs: functions don't carry over.

```lua
-- A JDF template is a stored script that another script loads with
-- ctx.jdf.loadTemplate(name). It runs on its own, with no data and no ctx,
-- and the value it returns is what the loading script receives. Return
-- data, such as the settings a ticket needs; functions don't carry over.
return {
  binding = "SaddleStitch",
  sheetsPerSignature = 4,
  colours = { front = "CMYK", back = "CMYK" },
}
```