How to display a long division algorithm based on multiplication and subtraction?

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

1 Like