How can i display rounded numbers with a specific number of digits in a table? Or, how to turn content into numbers?

I’m on the latest development version of typst

❯ typst --version
typst 0.11.0 (fcdccc9c)

I have a table with numbers generated by another program, and I’d like to get them all to display as float values with 4 decimal places. For example

#table(columns:2,
[Thing], [number],
[a],     [0.01],
[b],     [0],
[c],     [-0.005],
[d],     [-0.0000001])

I would like to display the same as

#table(columns:2,
[Thing], [number],
[a],     [0.0100],
[b],     [0.0000],
[c],     [-0.0050],
[d],     [-0.0000])

image

At first, I thought I wanted the #calc.round() function and was trying to make some #show rules, but having a couple of problems. The first is that, even if I add the function manually, #calc.round() doesn’t do exactly what I want

#table(columns:2,
[Thing], [number],
[a],     [#calc.round(0.01, digits:4)],
[b],     [#calc.round(0, digits:4)],
[c],     [#calc.round(-0.005, digits:4)],
[d],     [#calc.round(-0.0000001, digits:4)])

image

The second is that I can’t figure out how to get this via a show rule. The closest I got (I think) is something like

#show table.cell: it => {
    if it.x == 1 and it.y != 0 {
        calc.round(float(it))
    }
    else {
        it
    }
}

or the same without the call to float(), and i get

error: expected float, boolean, integer, decimal, ratio, or string, found content
     ┌─ manuscript/main.typ:1076:25
     │
1076 │         calc.round(float(it))
     │                          ^^

My lack of understanding clearly goes deeper, because I also attempted to use the Oxifmt package and

#import "@preview/oxifmt:0.2.1": strfmt
#table(columns:2,
[Thing], [number],
[a],     [#strfmt("{0:.4}", 0.01)],
[b],     [#strfmt("{0:.4}", 0)],
[c],     [#strfmt("{0:.4}", -0.005)],
[d],     [#strfmt("{0:.4}", -0.000001)])

Does the right thing (other than the integer 0, but I can get around that), but when I tried it in a show rule it didn’t work.

I advice to do it differently. It seems that you know exactly the structure of the tables you want to create. If you succeed to create the table-show-rule, you would have problems to create other tables. Furthermore, working with the content type can be challenging.

I would create a function, which gets the data in the structure and type you want it to. I.e.

#let num-table(..args) = {
  let header = args.pos().slice(0, 2)
  let body = args.pos().slice(2)
  
  table(
    columns: 2,
    table.header(..header),
    ..body.chunks(2).map(((t, n)) => (
      t, str(calc.round(n, digits: 4))
    )).flatten()
  )
}

#num-table(
[Thing], [number],
[a],     0.0100,
[b],     0.0000,
[c],     -0.0050,
[d],     -0.0000)

Definitely could do this, but in my actual use-case, I have multiple tables with slightly different (and much more complicated) structure, eg

#figure(
    caption:[*Longitudinal FSEA, visit 2 stool -> visit 3 VEP*],
    table(
        columns: 5,
        stroke: 0.5pt,
        table.hline(stroke: 2pt),
    [Gene set], [Feature], [Component], [Enrichment], [Q value],
    table.cell(fill: rgb("#89CDD8"))[GABA synthesis],
        [latency],
            [N1], [-0.376215897], [0.107325],
    table.cell(rowspan:3, fill: rgb("#89CDD8"))[Glutamate synthesis],
        table.cell(rowspan:2)[latency],
            [N1], [-0.16762197], [0.160915663],
            [P1], [0.297968912], [0.0159],
        [amplitude],
            [P1], [0.23424529], [0.03816],
    table.cell(rowspan:2, fill: rgb("#89CDD8"))[Glutamate degradation],
        [latency],
            [P1], [0.317418013], [0.14045],
        [amplitude],
            [N2], [0.282946442], [0.187555102],
    table.cell(rowspan:2, fill: rgb("#82B574"))[Tryptophan synthesis],
        table.cell(rowspan:2)[amplitude],
            [N1], [-0.155489037], [0.060834783],
            [P1], [0.186539112], [0.029353846],
//...
))

Relating to this, and also tying into your title (“Or, how to turn content into numbers?”): don’t specify your data as content (i.e. with [ ]) directly, only convert it to content when it’s fully processed. @ludwig’s suggestion was in the right direction: you’d specify your data as a float (which can be rounded), use oxifmt’s strfmt function to round it to a certain amount of decimals, and only then you convert it to content (by just passing the final string to the table).
Their suggested num-table function would only apply this to the second column, but it should be easy enough to generalize this to all floats (or even integers) you specify to the table, regardless of the table’s structure:

#import "@preview/oxifmt:0.2.1": strfmt

#let num-table(..args) = {
  table(
    ..args.pos().map(n => {
        if type(n) == float or type(n) == int {
          // float(n) to ensure integers like '0' are also formatted
          strfmt("{:.4}", float(n))
        } else {
          // Not a number, don't format
          n
        }
      }),

    ..args.named()
  )
}

#num-table(
  columns: 3,
  table.header([Thing], [number], [other]),
  [a],     0.0100,  0.5,
  [b],     0.0000,  0.6,
  [c],     -0.0050, 0.7,
  [d],     -0.0000, 0
)

output: every number is formatted

1 Like

Hi, this also sounds like something that Zero can do for you.

Round/truncate and pad numbers

#import "@preview/zero:0.1.0": ztable

#ztable(
  columns:2,
  format: (none, (digits: 4)),
  [Thing], [number ],
  [a],     [0.01],
  [b],     [0],
  [c],     [-0.005],
  [d],     [-0.0000001]
)

image

This will set the number of digits for all numbers in the second column. Note that no rounding but just truncation is performed here. You can additionally configure rounding by passing (round: (mode: "places", precision: 4)) instead of (digits: 4).

Note that this also aligns all numbers at the decimal point, so the minus sign won’t become a problem.

On a side note: I don’t know your precise use-case but if you add trailing 0’s to a value in a scientific context of measured quantities, it suggests that your measurement precision is higher (than it maybe was). You might want to be aware of this in that case.

Just align the numbers

If you just want the alignment, then

#ztable(
  columns: 2,
  format: (none, auto),
  [Thing], [number ],
  [a],     [0.01],
 ...
)

will do the trick.
image
Or if you still want to round the numbers that are too long:

#ztable(
  columns: 2,
  format: (none, (round: (mode: "places", precision: 4, pad: false))),
  [Thing], [number ],
  [a],     [0.01],
 ...
)

image

5 Likes

Oh, this looks great, thanks! I decided to flag the previous post as the answer, because it answers the question I asked a bit more directly, but I definitely think I will use Zero in my actual document :wink:

1 Like

Sure :)

By the way, starting tomorrow (or the day after), I recommend using zero:0.2.0 which features significant performance upgrades. I released the new version today and presumably it will be merged to the package manager in a day or so.

2 Likes