Projects/Build a Spreadsheet Engine

Build a Spreadsheet Engine

Model a spreadsheet as a grid of cells that hold literals or formulas, so every recalculation is exact and testable. Each lesson is one concrete spec with exact addresses and values: A1 maps to column 0 row 0 and AA1 to column 26, "=A1+B1*2" parses to (A1 + (B1 * 2)), SUM(A1:A3) folds a range, the dependency chain A1 to B1 to C1 sorts to a topological order, editing A1 recomputes B1 and C1 but not an unrelated D1, two cells that reference each other are flagged #CIRC! instead of hanging, and a #DIV/0! error propagates to every cell downstream.

39 lessonsMedium~20 min / lessonDependency graphsTopological sortRecalculation
The project

What you'll build over the next 39 lessons

Over 39 lessons you build the calculation engine that sits behind a spreadsheet - not a UI, but the library that makes one work: you set cells and formulas, call recalculate, and read back the computed values, all with exact, testable results. A cell is addressed A1-style and holds either a literal (a number, text, or a boolean) or a formula string that starts with `=`.

You start with A1 addressing and a grid of cells, then build a formula pipeline: a tokenizer, a precedence-aware parser that turns `=A1+B1*2` into an abstract syntax tree whose leaves are cell references and ranges, and an evaluator for arithmetic plus a starter function set - SUM, AVERAGE, MIN, MAX, COUNT, and IF. On top of that sits the real heart of a spreadsheet: a dependency graph that records which cells each formula reads, a topological recalculation order computed with Kahn's algorithm so every cell is evaluated after its inputs, incremental recalculation that recomputes only a changed cell's transitive dependents, circular-reference detection that flags a cycle instead of looping forever, and error values (`#DIV/0!`, `#REF!`, `#NAME?`, `#CIRC!`) that propagate to every cell downstream. The capstone recalculates a real sheet, edits an input and asserts exactly which cells recompute, and introduces a cycle that gets flagged rather than hanging.

This is a genuinely working, teaching-grade calculation engine - the same dependency-graph-and-topological-recalc design that VisiCalc, Lotus 1-2-3, and Excel are built on - but it deliberately stops short of a full spreadsheet application: it is a single in-memory sheet with a starter function library, no user interface, no file formats, no cross-sheet or absolute (`$A$1`) references, and no row or column insertion. It is the honest core those products extend with hundreds of functions, persistence, and a grid you can click.

build-a-spreadsheet-engine / lesson-01.md
Lesson 01 · Cells and A1 addressing

Column letters to an index

A spreadsheet addresses cells by column letters and row numbers - A1, B7, AA10. Before we can do anything with a cell, we have to turn its column letters into a plain number. Today you convert a column label into a zero-based column index.

The goal

Convert a column label like "A", "Z", or "AA" into its zero-based column index.

Start here - the target
TO DO
Scenario: Column labels map to indices
Givena column label made of one or more uppercase letters
WhencolToIndex is called on it
ThencolToIndex("A") is 0 and colToIndex("Z") is 25
AndcolToIndex("AA") is 26, colToIndex("AB") is 27, and colToIndex("ZZ") is 701
Background

Every cell in a spreadsheet has an address like A1 or AB10: some column letters followed by a row number. The letters name the column, and they count in a peculiar way - A through Z, then AA, AB, … AZ, BA, and so on. It looks like base-26, but it is not quite: there is no digit that means zero, so A is 1, Z is 26, and AA is 27 in that one-based counting.

To use columns as array indices we want them zero-based: A is column 0, Z is column 25, and AA (the 27th column) is index 26. The trick is to fold the letters as if they were one-based digits (A=1 … Z=26), then subtract one at the end. That single - 1 is what turns the letter-counting into an index you can use everywhere else. Tomorrow you build the inverse.

Make it work
// like base-26, but there is no "zero digit": A..Z are 1..26,
// and you subtract 1 at the very end to make it zero-based.
func colToIndex(s string) int {
n := 0
for _, c := range s {
n = n*26 + int(c-'A'+1) // fold each letter in
}
return n - 1
}
CheckpointDONE
You can turn any column label into its zero-based index. Commit and stop here.
Scope & extensions

Where this project stops - and where to go next

A genuinely working, teaching-grade calculation engine over a single in-memory sheet - A1 addressing, a precedence-aware formula parser, the SUM/AVERAGE/MIN/MAX/COUNT/IF function set, dependency-graph recalculation via Kahn's algorithm, incremental updates that touch only a changed cell's dependents, circular-reference detection, and error propagation - but it has no string literals inside formulas, no lookup or logical functions beyond IF, and no user interface, file formats, cross-sheet or absolute references, or cell enumeration.

Extend it next
  • Add string literals to the formula grammar so IF and future functions can branch on or produce text, not just numbers
  • Add the logical functions (NOT, AND, OR) and a lookup function (VLOOKUP, or INDEX plus MATCH) on top of the existing dispatch table
  • Make comparisons and arithmetic type-aware (comparing text and booleans directly) instead of coercing every operand to a number
  • Add a method to enumerate the cells a sheet holds (and their addresses) so callers do not have to track their own address list
  • Support absolute and cross-sheet references (the $A$1 and Sheet2!A1 forms) and row or column insertion that rewrites the affected references
  • Add persistence (load and save a sheet) and more operators (exponent, modulo, string concatenation) to round out the formula language
Recommended reading

Books & references that go deeper

  • A from-scratch build of a small spreadsheet - parsing formulas, resolving cell references, and recalculating - that mirrors the pipeline this project builds.

  • The reference for the recalculation order at the heart of this project: repeatedly remove a node with no remaining incoming edges. A leftover node with a nonzero in-degree is exactly a cell caught in a circular reference.

  • Crafting Interpreters · Robert Nystrom

    The clearest treatment of tokenizing and parsing with operator precedence. Chapters on scanning and Pratt-style parsing map directly onto the formula parser in Chapter 2.

  • A spreadsheet's formulas form a DAG - as long as there is no cycle. This is the structure recalculation walks, and the reason a circular reference has to be detected and refused.

  • A Brief History of the Electronic Spreadsheet · Dan Bricklin, Bob Frankston

    VisiCalc's co-inventor on how the first electronic spreadsheet came to be. Useful context for why automatic recalculation - the thing this engine implements - was the whole point.

  • Essays on the recalculation problem and dependency tracking that a spreadsheet solves - background reading on why naive re-evaluation is wrong and topological order is right.