---
title: "Formatting — quario"
description: "Presenting numbers and dates in a quario definition with the four format kinds, without turning a number into a string."
url: "https://getquario.com/docs/diving-deeper/formatting"
---

# Formatting

## Intent

A cell declares what kind of value it holds and lets each target present it.
The number stays a number, so the workbook can still compute with it and the
page still reads as money. Formatting the value yourself produces a string, and
a string is the same string on every surface.

## Design decisions

**`format` is a closed kind, never a pattern string.** The four kinds are
`number`, `currency`, `percent` and `date`. A pattern would have to mean the
same thing to a page, a PDF and a worksheet, and those three present numbers
through different machinery. A kind is a statement about the value, which every
target can honour in its own way.

**A format needs one value to speak about.** A target reads it only where its
run renders exactly one value token. A cell that mixes text and an interpolation
under a `format` now renders the value plainly on every target. The page used
to format it and the sheet didn’t. To format a value inside a sentence, give
it a run of its own.

**A modifier names a reading, never a pattern.** The three number kinds take
`digits`, a whole number from 0 to 20. They also take two readings that decide
how a figure appears rather than what it holds. `negative` is `minus` or
`parens`, and `zero` is `number` or `dash`, while `date` takes `form`. Each
modifier names a choice the kind already had to make. The declaration stays a
statement about the value rather than a format string in disguise.

**Precision converges across every target that presents.** Absent a `digits` of
its own, `number` and `percent` present two places and `currency` presents the
minor units of the code that cell wears. A figure therefore reads with the same
cents on the page and in the grid.

**Form doesn’t converge across every target.** HTML and PDF honour a date’s
`form` exactly, and a worksheet approximates it. There the four forms map to
number formats a spreadsheet re-presents in the reader’s own application
language. The shape stays the author’s and the language becomes the reader’s,
which is the property a workbook should keep.

**Locale and timezone belong to the instance.** They describe who is reading,
not what the value is, so they sit on `quario({ locale, timeZone })` beside the
license key. One compiled report then serves more than one market without being
rewritten.

**A currency code describes the value, so a cell may declare one.** This is the
single piece of instance configuration a document may override. A listing whose
rows arrive in different currencies writes `"currency": "=@.ccy"` and keeps its
numbers, with no host function in the path.

**`date` is the only kind that revives a string.** A date is the one type JSON
can’t carry. Numbers arrive as numbers, so the other three kinds have nothing
to revive and never read a string.

**A modifier a kind can’t read is a definition error.** `{ "kind": "date",
"digits": 2 }` fails, located, naming the cell. A kind presents a value, so
the engine refuses a declaration it couldn’t present rather than quietly
ignoring it.

**`format` layers whole.** A cell that names `format` restates the entire
declaration. A column declaring `{ "kind": "currency", "digits": 0 }` under a
cell declaring plain `"currency"` leaves that cell the default count, because
the cell replaced the object rather than merging into it. A run that names
`format` restates it the same way, and a run that names none inherits the
cell’s.

**A failed `format` expression drops out, so the layer below stands.** Only
the whole declaration may be an expression, never a modifier inside it. When
that expression resolves to nothing naming a kind, the cell keeps the layer
beneath. A cell inside a currency column still presents as money. It never
said it wasn’t money.

**`round` settles the stored number, not the presented one.** The HTML and PDF
targets present through `format`, so `round` never changes what they present. The
XLSX and CSV targets keep the number the engine gives them, so an unrounded
sum appears there in full. No target rounds a value on your behalf, because that
would turn presentation into data.

## API walkthrough

`format` is legal wherever a node has a cell value, which means text items,
column cells, headers, and total cells. It’s a definition error on an image, on
a table row’s `style`, and on the report default, where `family` and `size`
stay the only names. It’s also one of the twelve inline names a styled run may
carry.

### Declaring a kind on a cell

**`"format": "number"`** presents two fraction digits. The bare kind is
shorthand for `{ "kind": "number" }`.

**`"format": "currency"`** presents the minor units of whichever code the cell
wears. Without a code from either the cell or the instance, the kind
contributes nothing and the value renders as it arrived.

**`"format": "percent"`** presents a stored fraction as a percentage, so
`0.125` reads as `12.5%`. The value in the workbook stays `0.125` under a
percent number format.

**`"format": "date"`** presents a `Date`, or a string it can revive. With no
`form` it presents `medium`.

**`{ "kind": "…", "digits": n }`** overrides the count the three number kinds
would otherwise present, minor units included. On `percent`, `digits` counts
the places of the presented percentage rather than the stored fraction, which
is what both `Intl` and a worksheet already mean.

**`{ "kind": "…", "negative": "…" }`** chooses how a negative amount reads.
`minus` gives `-1,204.00` and `parens` gives `(1,204.00)`.

**`{ "kind": "…", "zero": "…" }`** chooses how a zero reads. `number` gives
`0.00` and `dash` gives `-`.

Both default to what every kind presented before, so no definition you have
already written moves. They compose with `digits`, with the percent kind’s
scaling, and with a currency’s code, and `date` takes neither. A worksheet
keeps this page’s bargain. A cell declaring either gets a sectioned number
format, `#,##0.00;(#,##0.00);"-"`, so the grid presents what the page presents and the
cell stays a number. A host recipe producing the same text would leave a string
where an amount belongs.

**`{ "kind": "date", "form": "…" }`** takes `short`, `medium`, `long` or
`full`.

```json title="a VAT column that reads 21%, not 21.00%"
{
  "header": { "value": "VAT" },
  "value": "{{ @.vatRate / 100 }}",
  "style": { "align": "right", "format": { "kind": "percent", "digits": 0 } }
}
```

```json title="a balance column an accountant can read"
{
  "header": { "value": "Balance" },
  "value": "{{ round($.balance, 2) }}",
  "style": { "align": "right", "format": { "kind": "currency", "negative": "parens", "zero": "dash" } }
}
```

A computed count goes on the whole declaration as a hash literal, since the
modifier itself can’t be an expression.

```json title="a per-row digit count"
{ "value": "{{ @.amount }}", "style": { "format": "={'kind':'number','digits':@.dp}" } }
```

### Denominating a money cell

**`"currency": "EUR"`** names what a money cell holds. It’s read only under
`format: "currency"`, and a cell’s own code beats the instance’s.

**`"currency": "=@.ccy"`** takes the code from the row. A literal that’s not
three uppercase letters is a definition error, while an expression result that
isn’t stays lenient at render. An unusable code drops that cell to plain
display. Presenting the figure under the instance’s currency would label an
amount in a denomination nobody named.

**The instance’s `currency`** is the default for documents that only ever hold
one denomination.

A target reading the code itself branches on whether the key is present, not on
its value. An unusable code crosses the stream as `null` rather than as absent.
The engine exports `currencyOf(style, options)` so a target answers that once
instead of getting it wrong.

### Presenting a date

Two string shapes revive, and they’re the two that ECMAScript reads the same
way on every machine.

**`2026-08-14`** is a calendar date. It revives as UTC midnight, so an instance
in a western timezone presents the day before, exactly as an injected
`new Date("2026-08-14")` already does.

**`2026-08-14T12:30:00Z`** is a timestamp naming its offset, and `+02:00` works
the same way.

**`2026-08-14T00:00:00`** is zoneless, and doesn’t revive. It means local
time, so it would present a different day per machine. A loose `14/8/2026`, a
partial `2026-08`, a lowercase `t` or `z`, and a day that doesn’t exist all
fall through the same way, rendering as authored.

**A date inside a line of text** needs a run of its own. Written as one string,
`Issued {{ $.input.issued }}` renders the ISO date, because its run holds text
and a value together. Split it, and the cell’s `format` reaches the run holding
the date. A bare date cell becomes a typed date in the workbook, while a mixed
cell becomes text carrying the presented date.

```json title="a date inside a sentence"
{ "value": [{ "value": "Issued " }, { "value": "{{ $.input.issued }}" }], "style": { "format": "date" } }
```

Epoch milliseconds stay a number under every kind. A month heading with no day
still needs a registered function, because none of the four forms prints one.

```ts title="report.ts"
const funcs = {
  // '2026-08' → 'August 2026'
  period: (ym: string) => {
    const [year, month] = ym.split('-').map(Number)
    return new Intl.DateTimeFormat('en-GB', { month: 'long', year: 'numeric' })
      .format(new Date(year, month - 1, 1))
  },
}

const report = q.report(definition, funcs)
```

Registered functions are configuration you trust, the same standing as the
definition itself. A function of the same name as a built-in shadows it, so a
host that already supplies its own `round` keeps it.

### Rounding what a cell computes

**`round(x, n?)`** rounds to `n` decimal places, halves away from zero, and `n`
defaults to 0. It rounds the decimal you wrote rather than the double it’s
stored as, so `round(2.675, 2)` is `2.68`.

**`floor(x, n?)`** and **`ceil(x, n?)`** are the directed pair. They round
towards negative and positive infinity.

**`abs(x)`** takes no places argument.

An `n` outside 0 to 100, or one that’s not a number, is an error naming the cell
and the call. One cell holds as many calls as you write. A non-finite `x` passes
through untouched instead of folding to zero, which leaves the cell as visible
display text rather than a confident wrong total.

Watch the gap between the two counting systems. A `percent` cell presents the
value multiplied by 100, so matching a two-decimal percent display takes
`round(x, 4)`, not `round(x, 2)`.

`min` and `max` are reducer names and aren’t scalar functions here.
`min(1, 2)` is a render error saying so. Register your own if a report needs
one, and note that a registered function shadows a built-in of the same name.

### Configuring the instance

**`quario({ locale })`** defaults to `en-US` when a kind presents a value.

**`quario({ currency })`** is the document-wide default code, overridden by a
cell’s own.

**`quario({ timeZone })`** defaults to `UTC`.

The instance checks all three when you create it. A `locale` or `timeZone`
that `Intl` refuses throws a `TypeError` — `quario({ locale: 'en_US' })` names
`options.locale` — and `currency` must be three upper-case letters. Otherwise
one mistyped locale would display every formatted cell as its raw value. A cell’s
own `currency` still fails soft, because it comes from data.

```ts title="report.ts"
import { quario } from 'quario'

const q = quario({ locale: 'en-IE', currency: 'EUR', timeZone: 'Europe/Dublin' })
```

Custom patterns such as `#,##0.00` remain the province of a registered
function. A formatted value is a string, so such a cell is display text on
every surface. In a worksheet it lands as a text cell with no number
format, even when the cell also declares a kind.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Formatting","item":"https://getquario.com/docs/diving-deeper/formatting"}]}
```
