How to align line/bar in maths mode for primary school addition?

Hi folks, I’m new to typst and up to now I enjoy it a lot.
I’m stuck on something pretty basic (I guess). I’m trying to write a primary school style addition and I can’t get a way to have both a line as I would want and to properly vertically align digits.
The closer I get until now is this :

$
&2,&7 \
+" " 1&2,&8 \
#line(length: 3em) \
1&5 ,&5
$

which yields

Obviously I would like the line to fit correctly under the digits (and ideally have it scale automatically to the right size) but I struggle to find a way to do that.

I have tried to mess with the fraction bar :

$
(&2,&7 \
+" " 1&2,&8  ) /

(1&5 ,&5)
$

but then I lose vertical alignment :
screenshot-2025-06-12-12:27:59

Is there a way to get what I want ?
Thanks !

I think the problem here is that lines (and nothing else) can cross the alignment points (&) – this means that the line in your example will always be to the left of the first & you place.

We can work around this by only placing a single alignment point to the left:

$
  12.7 &\
  + space.fig 2.8 &\
  #line(length: 3em) &\
  15.5 &\
$

image

  • We can remove all except one of the & and it would still give the same result. Typst just needs one to know to right-align stuff.
  • space.fig is a space that is exactly the width of a digit. Use this to align numbers by hand.
2 Likes

Adding to the previous answer, have a look here if you want to use decimal commas instead of points. In particular, the following show rule provided in that GitHub Issue replaces the decimal points with commas.

// numbers in plain text
#show regex("\d+\.\d+"): it => {
    if it.func() != heading {show ".": ","}
    it
  }

// numbers in math mode
#show math.equation: it => {
    show regex("\d+\.\d+"): it => {show ".": {","+h(0pt)}
        it}
    it
}
1 Like

The plain text doesn’t work. And formatting is hard to read, at least for math.

the package zero’s ztable also does an ok job with this. Alignment looks good and it could work for this task. The zero package is specialized on doing good number formatting. In a table it automatically aligns decimal separators in a column.

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

#ztable(
  columns: 2,
  format: (none, (decimal-separator: ",", group: (threshold: calc.inf))),
  stroke: none,
  [], [2.7],
  [$+$], [12.8],
  table.hline(),
  [], [15.5]
)

However in other cases I see that it could be necessary to split digits into separate columns, but not in this case.

(What if we want to show the carry digit? Then I don’t think ztable does that easily. Whyy do I always do these extra things… here’s how it looks good

Carry digit row with a bit of a hack

#show table.cell.where(y: 0): it => {
  set text(size: 0.6em)
  show: pad.with(y: -0.2em)
  it
}
#ztable(
  columns: 2,
  format: (none, (decimal-separator: ",", group: (threshold: calc.inf))),
  stroke: none,
  inset: (y: 0.3em),
  [], [1],  // carry digit
  [], [2.7],
  [$+$], [12.8],
  table.hline(),
  [], [15.5]
)
1 Like

Oh right, carry digit would have been my next question actually. Currently I have this for addition and substraction :

$
14.#sub[1]01 &\
-" "3.#hide(sub[1])50&\
#sub[1] space.fig space.fig space.fig&\
#line(length:2em)&\
10.51&
$

It’s not great, but it kind of works. Thanks for pointing me to ztable, it looks cleaner at least for handling decimal commas

ztable does very smart alignment, but I’m not sure it’s easy to work with when wanting to insert extra stuff - the carry digits - that don’t fit into the model.

Here are two approaches, just a bit creative (opposite of useful?). The first one is the one I’d do in school (took some effort to remember) and the second and third are the way you set it up.

I think the short syntax with replacement letters is great for a one off, but it doesn’t scale ultimately, it’s just cute.


#[
// being very creative with our own syntax
#let mkrow(text) = text.split("").filter(elt => elt != "").map(elt => $#elt$)
#show "-": math.minus
#show "X": text(size: 0.6em, math.underline[10])
#let smashit(x) = box(width: 0pt, $ script(#x) $)
#show "Y": $attach(cancel(4), tl: smashit(text(size: #0.8em, 3)))$
#table(
  inset: (y: 0.3em, x: 0.05em),
  align: right,
  stroke: none,
  columns: 6,
  ..mkrow("    X "),
  ..mkrow(" 1Y,01"),
  ..mkrow("- 3,50"),
  table.hline(stroke: 1pt),
  ..mkrow(" 10,51"),
)

#show "Z": $attach(0, bl: 1)$
#show "I": text(size: 0.8em, [1])
#table(
  inset: (y: 0.3em, x: 0.05em),
  align: right,
  stroke: none,
  columns: 6,
  ..mkrow(" 14,Z1"),
  ..mkrow("- 3,50"),
  ..mkrow("  I   "),
  table.hline(stroke: 1pt),
  ..mkrow(" 10,51"),
)
]
#[
// slightly more verbose but good if we want to have
// more regular syntax and functions
#import "@preview/rowmantic:0.2.0": rowtable
#let s1 = $text(size: #0.6em, 1)$ // s1: small one
#rowtable(
  inset: (y: 0.3em, x: 0.05em),
  align: right,
  stroke: none,
  separator: ",",
  $,1,4,.,s1 0,1$,
  $-,,3,.,   5,0$,
  $ ,,s1, $,
  table.hline(stroke: 1pt),
  $,1,0,.,   5,1$,
)
]

Ultimately going back to @nleanba’s smart answer and using the smash function, it’s almost working out. But we kind of want table alignment here…

#let smashit(x) = box(width: 0pt, $ script(#x) $)
$
  14. med smashit(1 med)01 &\
  - space.fig 3.med 50 &\
  space.fig s1 space.fig space.fig space.fig &\
  #line(length: 3em) &\
  10. med 51 &\
$
1 Like