I have built maths algorithms for Long Multiplication, Addition and Subtraction, but unable to build one for Long Division. Inspired by latex xlop
package.
Hopefully someone can help make the final one!
#set text(3em)
// convert a number to a string, split it into characters, and convert it back into digits
#let digits(x) = str(x).clusters().map(int)
#let algorithm_add(..summands) = {
assert.eq(summands.named(), (:))
// represent each summand as an array, least significant digit first
let summands = summands.pos().map(x => digits(x).rev())
let len = calc.max(..summands.map(array.len))
// do the summation, digit by digit
let columns = ()
let carry = 0
for i in range(len) {
// get all the digits
let summands = summands.map(x => x.at(i, default: none))
// calculate sum, split into ones and tens
let sum = summands.sum() + carry
let (sum, new-carry) = (calc.rem(sum, 10), int(sum/10))
// save the result, always prepending so that the most significant
// digits end up left
columns.insert(0, (
summands: summands,
sum: sum,
carry: carry,
))
carry = new-carry
}
// add a final column if there's a carry
if carry != 0 {
columns.insert(0, (
summands: (none,) * summands.len(),
sum: carry,
carry: 0,
))
}
// add a dummy column for the addition sign
columns.insert(0, none)
block(breakable: false)[
#grid(
columns: columns.len(),
inset: 0.1em,
// go through all summands
..range(summands.len()).map(r => {
// go through all columns
columns.enumerate().map(((c, col)) => {
if col == none {
// this is the first column
if r == summands.len() - 1 [+]
else []
} else {
// this is a regular column
if r == 0 and col.carry != 0 {
// first line: put the carry
place(dx: -0.15em, dy: -0.1em, text(0.3em)[#col.carry])
}
// put the digit
[#col.summands.at(r)]
}
})
}).flatten(),
grid.hline(),
// sum
..columns.enumerate().map(((c, col)) => {
if col == none []
else [#col.sum]
}),
)
]
}
#let algorithm_mult(multiplicand, multiplier) = {
// represent numbers as arrays, least significant digit first
let multiplicand-digits = digits(multiplicand).rev()
let multiplier-digits = digits(multiplier).rev()
// calculate all partial products with carry information
let partial-products = ()
for (i, mult-digit) in multiplier-digits.enumerate() {
if mult-digit == 0 {
// skip zero multipliers but maintain position
partial-products.push((digits: (0,), carries: (), offset: i))
continue
}
let product-digits = ()
let carries = ()
let carry = 0
// multiply each digit of multiplicand by current multiplier digit
for (j, digit) in multiplicand-digits.enumerate() {
let product = digit * mult-digit + carry
product-digits.push(calc.rem(product, 10))
let new-carry = int(product / 10)
// Store the carry that will be used for the NEXT position
if new-carry > 0 and j < multiplicand-digits.len() - 1 {
carries.push((pos: j + 1, value: new-carry))
}
carry = new-carry
}
// add final carry if exists
if carry > 0 {
product-digits = product-digits + digits(carry).rev()
}
partial-products.push((digits: product-digits, carries: carries, offset: i))
}
// calculate final sum by adding all partial products
let max-len = calc.max(..partial-products.map(p => p.digits.len() + p.offset))
let sum-digits = (0,) * max-len
let carry = 0
for pos in range(max-len) {
let sum = carry
for product in partial-products {
let digit-pos = pos - product.offset
if digit-pos >= 0 and digit-pos < product.digits.len() {
sum += product.digits.at(digit-pos)
}
}
sum-digits.at(pos) = calc.rem(sum, 10)
carry = int(sum / 10)
}
// Build column structure
let total-width = calc.max(multiplicand-digits.len(), multiplier-digits.len(), sum-digits.len()) + 1
let num-carry-rows = multiplier-digits.len() // One row of carries per multiplier digit
// Initialize columns
let columns = ()
for i in range(total-width) {
columns.push(())
}
// Add carry rows (above multiplicand)
for carry-row in range(num-carry-rows) {
columns.at(0).push(none) // no operator for carry rows
let multiplicand-start = total-width - multiplicand-digits.len()
// Add carries for this step - use reverse order so bottom carry row corresponds to first multiplier digit
let product-index = num-carry-rows - 1 - carry-row
if product-index < partial-products.len() {
let product = partial-products.at(product-index)
for (i, digit) in multiplicand-digits.rev().enumerate() {
let col-pos = multiplicand-start + i
let multiplicand-pos = multiplicand-digits.len() - 1 - i
// Check if there's a carry for this position in this step
let carry-value = none
for carry-info in product.carries {
if carry-info.pos == multiplicand-pos {
carry-value = carry-info.value
break
}
}
if carry-value != none {
columns.at(col-pos).push(place(dy: 0em, dx: 0.21em, text(0.3em)[#carry-value]))
} else {
columns.at(col-pos).push(none)
}
}
}
// Fill empty columns with none
for i in range(1, multiplicand-start) {
columns.at(i).push(none)
}
}
// Add multiplicand (right-aligned)
columns.at(0).push(none)
let multiplicand-start = total-width - multiplicand-digits.len()
for (i, digit) in multiplicand-digits.rev().enumerate() {
columns.at(multiplicand-start + i).push(digit)
}
// Fill empty columns with none
for i in range(1, multiplicand-start) {
columns.at(i).push(none)
}
// Add multiplier with × symbol (right-aligned)
columns.at(0).push([×])
let multiplier-start = total-width - multiplier-digits.len()
for (i, digit) in multiplier-digits.rev().enumerate() {
columns.at(multiplier-start + i).push(digit)
}
// Fill empty columns with none
for i in range(1, multiplier-start) {
columns.at(i).push(none)
}
// Add partial products (if multi-digit multiplier)
if multiplier-digits.len() > 1 {
for product in partial-products {
if product.digits != (0,) {
columns.at(0).push(none)
// Add product digits right-aligned with proper offset
let product-start = total-width - product.digits.len() - product.offset
for (i, digit) in product.digits.rev().enumerate() {
columns.at(product-start + i).push(digit)
}
// Fill empty columns with none
for i in range(1, product-start) {
columns.at(i).push(none)
}
for i in range(product-start + product.digits.len(), total-width) {
columns.at(i).push(none)
}
}
}
}
// Add final sum (right-aligned)
columns.at(0).push(none)
let sum-start = total-width - sum-digits.len()
for (i, digit) in sum-digits.rev().enumerate() {
columns.at(sum-start + i).push(digit)
}
// Fill empty columns with none
for i in range(1, sum-start) {
columns.at(i).push(none)
}
// Calculate number of rows for each column
let num-rows = columns.at(0).len()
let multiplicand-row = num-carry-rows
let multiplier-row = num-carry-rows + 1
block(breakable: false)[
#grid(
columns: total-width,
inset: (x, y) => {
// Reduce horizontal spacing for carry rows (first num-carry-rows rows)
if y < num-carry-rows {
(x: 0.05em, y: 0.15em)
} else {
0.1em
}
},
// Render all rows
..range(num-rows).map(r => {
// Add horizontal line after multiplier
if r == multiplier-row + 1 {
range(total-width).map(_ => grid.hline())
}
// Add horizontal line before sum (last row)
else if r == num-rows - 1 {
range(total-width).map(_ => grid.hline())
}
// Render row content
columns.map(col => {
let cell = col.at(r, default: none)
if cell == none { [] } else { [#cell] }
})
}).flatten()
)
]
}
#let algorithm_subtract(minuend, subtrahend) = {
// determine if result will be negative
let is-negative = minuend < subtrahend
// keep original order - don't swap for young students
// represent numbers as arrays, least significant digit first
let minuend-digits = digits(minuend).rev()
let subtrahend-digits = digits(subtrahend).rev()
let max-len = calc.max(minuend-digits.len(), subtrahend-digits.len())
// pad with zeros if needed
while minuend-digits.len() < max-len {
minuend-digits.push(0)
}
while subtrahend-digits.len() < max-len {
subtrahend-digits.push(0)
}
// perform subtraction with borrowing, tracking changes
let result-digits = ()
let working-minuend = minuend-digits.map(x => x) // deep copy
let borrow-info = () // track borrowing details for each position
for i in range(max-len) {
let min-digit = working-minuend.at(i)
let sub-digit = subtrahend-digits.at(i)
// check if we need to borrow
if min-digit < sub-digit {
// find the next non-zero digit to borrow from
let borrow-pos = i + 1
let borrow-chain = () // track the chain of borrowing
// handle borrowing through zeros
while borrow-pos < max-len and working-minuend.at(borrow-pos) == 0 {
borrow-chain.push(borrow-pos)
working-minuend.at(borrow-pos) = 9
borrow-pos += 1
}
if borrow-pos < max-len {
// borrow from this position
working-minuend.at(borrow-pos) -= 1
borrow-chain.push(borrow-pos)
working-minuend.at(i) += 10
// record borrowing info for visual display
for pos in borrow-chain {
borrow-info.push((position: pos, borrowed-from: true))
}
borrow-info.push((position: i, borrowed-to: true))
}
}
// perform the subtraction
let diff = working-minuend.at(i) - sub-digit
result-digits.push(diff)
}
// remove leading zeros from result
while result-digits.len() > 1 and result-digits.at(-1) == 0 {
let _ = result-digits.pop()
}
// Build the visual representation
let total-width = max-len + 1
let has-borrows = borrow-info.len() > 0
// Initialize columns
let columns = ()
for i in range(total-width) {
columns.push(())
}
// Add borrow row (above minuend) if there are any borrows
if has-borrows {
columns.at(0).push(none)
for (i, original-digit) in minuend-digits.rev().enumerate() {
let col-pos = i + 1
let digit-pos = max-len - 1 - i
// Check if this position was borrowed from
let was-borrowed-from = original-digit != working-minuend.at(digit-pos)
if was-borrowed-from {
// Show small digit above the crossed-out original
columns.at(col-pos).push(
place(dx: 0em, dy: -0.15em, text(0.4em)[#working-minuend.at(digit-pos)])
)
} else {
columns.at(col-pos).push(none)
}
}
}
// Add minuend row (with crossed out digits where borrowed from)
columns.at(0).push(none)
for (i, original-digit) in minuend-digits.rev().enumerate() {
let col-pos = i + 1
let digit-pos = max-len - 1 - i
// Check if this position was borrowed from
let was-borrowed-from = original-digit != working-minuend.at(digit-pos)
if was-borrowed-from {
// Show crossed out original digit
columns.at(col-pos).push(text(strike[#original-digit]))
} else {
// Show original digit
columns.at(col-pos).push(original-digit)
}
}
// Add subtrahend with − symbol
columns.at(0).push([−])
for (i, digit) in subtrahend-digits.rev().enumerate() {
let col-pos = i + 1
if digit != 0 or subtrahend-digits.rev().slice(0, i+1).any(d => d != 0) {
columns.at(col-pos).push(digit)
} else {
columns.at(col-pos).push(none)
}
}
// Add result with negative sign if needed
if is-negative {
columns.at(0).push([−])
} else {
columns.at(0).push(none)
}
// For negative results, we need to calculate the absolute difference
let display-digits = if is-negative {
// Calculate subtrahend - minuend for display (but show as positive digits)
let temp-result = ()
let temp-working = subtrahend-digits.map(x => x) // copy subtrahend
let temp-borrow-info = ()
for i in range(max-len) {
let sub-digit = temp-working.at(i)
let min-digit = minuend-digits.at(i)
if sub-digit < min-digit {
// borrow from next position
let borrow-pos = i + 1
while borrow-pos < max-len and temp-working.at(borrow-pos) == 0 {
temp-working.at(borrow-pos) = 9
borrow-pos += 1
}
if borrow-pos < max-len {
temp-working.at(borrow-pos) -= 1
temp-working.at(i) += 10
}
}
let diff = temp-working.at(i) - min-digit
temp-result.push(diff)
}
// remove leading zeros
while temp-result.len() > 1 and temp-result.at(-1) == 0 {
let _ = temp-result.pop()
}
temp-result
} else {
result-digits
}
for (i, digit) in display-digits.rev().enumerate() {
let col-pos = max-len - display-digits.len() + i + 1
columns.at(col-pos).push(digit)
}
// Fill empty columns in result row
for i in range(1, max-len - display-digits.len() + 1) {
columns.at(i).push(none)
}
// Calculate number of rows
let num-rows = columns.at(0).len()
let subtrahend-row = if has-borrows { 2 } else { 1 }
block(breakable: false)[
#grid(
columns: total-width,
inset: (x, y) => {
// Reduce spacing for borrow row
if y == 0 and has-borrows {
(x: 0.05em, y: 0.05em)
} else {
0.1em
}
},
// Render all rows
..range(num-rows).map(r => {
// Add horizontal line after subtrahend
if r == subtrahend-row + 1 {
range(total-width).map(_ => grid.hline())
}
// Render row content
columns.map(col => {
let cell = col.at(r, default: none)
if cell == none { [] } else { [#cell] }
})
}).flatten()
)
]
}
#algorithm_add(744, 2480)
#algorithm_mult(409,49)
#algorithm_subtract(5432, 1876)
#algorithm_subtract(50, 123) //negative result