Back

Excel workbook diff

Data conversion

Loading

Loading tool

The tool is loaded only when you open it.

All processing for this tool happens in your browser. Your input is not sent to a server.

About this tool

Compare two saved XLSX workbooks without uploading their contents or changing either file. The default whole-workbook comparison pairs sheets by their exact names and cells by their addresses. You can instead explicitly pair one sheet from each file and match rows by one or more key columns. The report separates cell values, types, formula text, stored formula results and number formats from workbook and sheet metadata. Exact comparison retains stored numeric text and distinguishes a missing cell from an explicit blank. Review filtered, paginated differences and export every matching result as typed JSON or tagged CSV. ExcelJS 4.4.0 and local XML guards run in a browser worker; formulas, macros and external data connections are not executed.

Common uses

  • Check two versions of a budget or operating report for changed input values, formulas or stale cached formula results before distributing the newer workbook.
  • Compare reordered inventory or order rows using unique typed IDs or a composite key, keeping text 001 different from numeric 1 and preserving both source addresses.
  • Review a workbook handoff for added or removed sheets, sheet order or visibility, merge ranges, declared dimensions and column metadata alongside cell-level changes.

How to use it

  1. 1.Select a before and after .xlsx file, each no larger than 16 MiB, then load both. Inspect the available sheets and date systems. Use saved, unencrypted XLSX copies; renaming an XLS, XLSB or XLSM file does not convert it.
  2. 2.Keep whole-workbook coordinate matching to pair exact sheet names automatically. For a deliberate renamed-sheet comparison, select one sheet on each side. In that selected pair, choose coordinate matching or enter up to eight distinct key-column letters such as A,C. Key mode requires a header row from 1 to 1048575 and considers only rows after it that contain actual cells.
  3. 3.Run the comparison and review structural events as well as cell changes. In key mode every chosen key component must be present, nonblank, nonformula and unique as a typed tuple on each side. Resolve rejected keys in a separate copy rather than guessing which record corresponds to which. Choose the result filters and browse 25 events per page. A typed JSON preview may be clipped at 600 characters; exports retain complete values for all filtered results, not just the visible page. Download JSON for structured interchange or the mandatory tagged CSV for spreadsheet review. Clear or replace the files to discard the current session.

Executable workbook snapshot comparisons

Raw numeric text stays exact

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "1"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "1.0"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "number",
        "value": "1",
        "formula": null,
        "cached": null,
        "numberFormat": null
      },
      "after": {
        "type": "number",
        "value": "1.0",
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

These executable examples describe small synthetic XLSX file contents, not JSON to paste into this tool. Omitted settings use whole-workbook coordinate mode. The build check writes real OOXML files, parses them and checks the shown report excerpts; actual exports also include sheet, key and option context. Both stored values represent one, but the XML text changes from 1 to 1.0. The comparison reports that exact change without rounding or tolerance.

A text ID is not a number

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "001"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "1"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "string",
        "value": "001",
        "formula": null,
        "cached": null,
        "numberFormat": null
      },
      "after": {
        "type": "number",
        "value": "1",
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

Text 001 becomes numeric 1. Both the cell type and its stored value change; leading zeros are never silently converted.

Missing cell versus explicit blank

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "B1",
            "type": "string",
            "value": "keep"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "blank",
            "value": null
          },
          {
            "address": "B1",
            "type": "string",
            "value": "keep"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 1,
    "removed": 0,
    "changed": 0
  },
  "changes": [
    {
      "kind": "cell",
      "status": "added",
      "field": "cell",
      "leftAddress": null,
      "rightAddress": "A1",
      "before": null,
      "after": {
        "type": "blank",
        "value": null,
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

B1 exists on both sides so row presence is unchanged. A1 is absent before and explicitly present with a blank value after, producing a cell addition.

Blank versus empty text

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "blank",
            "value": null
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": ""
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "blank",
        "value": null,
        "formula": null,
        "cached": null,
        "numberFormat": null
      },
      "after": {
        "type": "string",
        "value": "",
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

An existing blank A1 becomes an existing empty string. Neither side is a missing cell, and the type difference is preserved.

Changed formula, same stored result

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "2",
            "formula": "1+1"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "2",
            "formula": "2*1"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "number",
        "value": "2",
        "formula": "1+1",
        "cached": {
          "type": "number",
          "value": "2"
        },
        "numberFormat": null
      },
      "after": {
        "type": "number",
        "value": "2",
        "formula": "2*1",
        "cached": {
          "type": "number",
          "value": "2"
        },
        "numberFormat": null
      }
    }
  ]
}

The formula changes from 1+1 to 2*1 while both caches contain numeric 2. Formula text is compared without executing either expression.

Same formula, changed cache

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "2",
            "formula": "1+1"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "3",
            "formula": "1+1"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "number",
        "value": "2",
        "formula": "1+1",
        "cached": {
          "type": "number",
          "value": "2"
        },
        "numberFormat": null
      },
      "after": {
        "type": "number",
        "value": "3",
        "formula": "1+1",
        "cached": {
          "type": "number",
          "value": "3"
        },
        "numberFormat": null
      }
    }
  ]
}

Both files store 1+1, but the cached result changes from 2 to 3. This intentionally inconsistent fixture proves that the tool reports the stored cache rather than recalculating it.

Number-format change only

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "1",
            "numberFormat": "0.0"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "number",
            "value": "1",
            "numberFormat": "0.00"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "number",
        "value": "1",
        "formula": null,
        "cached": null,
        "numberFormat": "0.0"
      },
      "after": {
        "type": "number",
        "value": "1",
        "formula": null,
        "cached": null,
        "numberFormat": "0.00"
      }
    }
  ]
}

The stored number remains 1 while its number format changes from 0.0 to 0.00. This is reported even though no numeric value changed.

Moving a cell in coordinate mode

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A2",
            "type": "string",
            "value": "keep"
          },
          {
            "address": "B2",
            "type": "string",
            "value": "move"
          },
          {
            "address": "A3",
            "type": "string",
            "value": "keep"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A2",
            "type": "string",
            "value": "keep"
          },
          {
            "address": "A3",
            "type": "string",
            "value": "keep"
          },
          {
            "address": "B3",
            "type": "string",
            "value": "move"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 1,
    "removed": 1,
    "changed": 0
  },
  "changes": [
    {
      "kind": "cell",
      "status": "removed",
      "field": "cell",
      "leftAddress": "B2",
      "rightAddress": null,
      "before": {
        "type": "string",
        "value": "move",
        "formula": null,
        "cached": null,
        "numberFormat": null
      },
      "after": null
    },
    {
      "kind": "cell",
      "status": "added",
      "field": "cell",
      "leftAddress": null,
      "rightAddress": "B3",
      "before": null,
      "after": {
        "type": "string",
        "value": "move",
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

The string move moves from B2 to B3. Fixed addresses produce one removal and one addition; the tool does not infer a row insertion or moved block.

Composite keys ignore row order

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "id"
          },
          {
            "address": "B1",
            "type": "string",
            "value": "quantity"
          },
          {
            "address": "C1",
            "type": "string",
            "value": "color"
          },
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B2",
            "type": "number",
            "value": "5"
          },
          {
            "address": "C2",
            "type": "string",
            "value": "red"
          },
          {
            "address": "A3",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B3",
            "type": "number",
            "value": "8"
          },
          {
            "address": "C3",
            "type": "string",
            "value": "blue"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "id"
          },
          {
            "address": "B1",
            "type": "string",
            "value": "quantity"
          },
          {
            "address": "C1",
            "type": "string",
            "value": "color"
          },
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B2",
            "type": "number",
            "value": "8"
          },
          {
            "address": "C2",
            "type": "string",
            "value": "blue"
          },
          {
            "address": "A4",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B4",
            "type": "number",
            "value": "5"
          },
          {
            "address": "C4",
            "type": "string",
            "value": "red"
          }
        ]
      }
    ]
  },
  "options": {
    "scope": "pair",
    "leftSheet": "Data",
    "rightSheet": "Data",
    "mode": "keys",
    "keyColumns": "A,C",
    "headerRow": 1
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 0
  },
  "changes": []
}

A alone is repeated, but the typed A,C tuples distinguish red and blue. Both records change physical row positions without changing values. No row-presence events are emitted in key mode.

Keyed change retains both addresses

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "id"
          },
          {
            "address": "B1",
            "type": "string",
            "value": "quantity"
          },
          {
            "address": "C1",
            "type": "string",
            "value": "color"
          },
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B2",
            "type": "number",
            "value": "5"
          },
          {
            "address": "C2",
            "type": "string",
            "value": "red"
          },
          {
            "address": "A3",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B3",
            "type": "number",
            "value": "8"
          },
          {
            "address": "C3",
            "type": "string",
            "value": "blue"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "id"
          },
          {
            "address": "B1",
            "type": "string",
            "value": "quantity"
          },
          {
            "address": "C1",
            "type": "string",
            "value": "color"
          },
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B2",
            "type": "number",
            "value": "8"
          },
          {
            "address": "C2",
            "type": "string",
            "value": "blue"
          },
          {
            "address": "A4",
            "type": "string",
            "value": "001"
          },
          {
            "address": "B4",
            "type": "number",
            "value": "6"
          },
          {
            "address": "C4",
            "type": "string",
            "value": "red"
          }
        ]
      }
    ]
  },
  "options": {
    "scope": "pair",
    "leftSheet": "Data",
    "rightSheet": "Data",
    "mode": "keys",
    "keyColumns": "A,C",
    "headerRow": 1
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "B2",
      "rightAddress": "B4",
      "before": {
        "type": "number",
        "value": "5",
        "formula": null,
        "cached": null,
        "numberFormat": null
      },
      "after": {
        "type": "number",
        "value": "6",
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

The red record moves from row 2 to row 4 and its quantity changes from 5 to 6. Its unchanged composite key matches the records, and the changed cell retains B2 and B4.

Reject an ambiguous repeated key

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          },
          {
            "address": "A3",
            "type": "string",
            "value": "001"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          }
        ]
      }
    ]
  },
  "options": {
    "scope": "pair",
    "leftSheet": "Data",
    "rightSheet": "Data",
    "mode": "keys",
    "keyColumns": "A",
    "headerRow": 1
  }
}
{
  "error": "duplicateKeys"
}

The before workbook contains two identical text keys 001. The comparison fails with duplicateKeys instead of choosing a first or last match.

Formula cells cannot be row keys

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A2",
            "type": "number",
            "value": "1",
            "formula": "1+0"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A2",
            "type": "string",
            "value": "001"
          }
        ]
      }
    ]
  },
  "options": {
    "scope": "pair",
    "leftSheet": "Data",
    "rightSheet": "Data",
    "mode": "keys",
    "keyColumns": "A",
    "headerRow": 1
  }
}
{
  "error": "keyFormula"
}

A2 contains a formula with cached numeric 1. A formula is not an eligible key even if its stored result looks like a usable identifier.

A sheet rename is not guessed

{
  "left": {
    "sheets": [
      {
        "name": "Old",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "same"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "New",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "same"
          }
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 2,
    "removed": 2,
    "changed": 0
  },
  "changes": [
    {
      "kind": "sheet",
      "status": "removed",
      "field": "sheet",
      "before": {
        "name": "Old",
        "index": 0,
        "state": "visible",
        "dimension": null,
        "rows": [
          1
        ],
        "columns": [],
        "merges": []
      },
      "after": null
    },
    {
      "kind": "cell",
      "status": "removed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": null,
      "before": {
        "type": "string",
        "value": "same",
        "formula": null,
        "cached": null,
        "numberFormat": null
      },
      "after": null
    },
    {
      "kind": "sheet",
      "status": "added",
      "field": "sheet",
      "before": null,
      "after": {
        "name": "New",
        "index": 0,
        "state": "visible",
        "dimension": null,
        "rows": [
          1
        ],
        "columns": [],
        "merges": []
      }
    },
    {
      "kind": "cell",
      "status": "added",
      "field": "cell",
      "leftAddress": null,
      "rightAddress": "A1",
      "before": null,
      "after": {
        "type": "string",
        "value": "same",
        "formula": null,
        "cached": null,
        "numberFormat": null
      }
    }
  ]
}

Old and New have equal content, but whole-workbook mode pairs exact names only. It emits a sheet removal and addition, plus the old and new A1 cells.

Pair renamed sheets explicitly

{
  "left": {
    "sheets": [
      {
        "name": "Old",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "same"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "New",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "same"
          }
        ]
      }
    ]
  },
  "options": {
    "scope": "pair",
    "leftSheet": "Old",
    "rightSheet": "New",
    "mode": "coordinates",
    "keyColumns": "A",
    "headerRow": 1
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 1
  },
  "changes": [
    {
      "kind": "structure",
      "status": "changed",
      "field": "name",
      "before": "Old",
      "after": "New"
    }
  ]
}

Selecting Old and New as a pair compares their equal A1 cells and reports only the sheet-name change. Similarity never supplies this pairing automatically.

Keep serial 60 and its date system

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "date",
            "value": "60",
            "numberFormat": "yyyy-mm-dd"
          }
        ]
      }
    ],
    "dateSystem": "1900"
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "date",
            "value": "60",
            "numberFormat": "yyyy-mm-dd"
          }
        ]
      }
    ],
    "dateSystem": "1904"
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 2
  },
  "changes": [
    {
      "kind": "structure",
      "status": "changed",
      "field": "dateSystem",
      "before": "1900",
      "after": "1904"
    },
    {
      "kind": "cell",
      "status": "changed",
      "field": "cell",
      "leftAddress": "A1",
      "rightAddress": "A1",
      "before": {
        "type": "date",
        "value": "60",
        "formula": null,
        "cached": null,
        "numberFormat": "yyyy-mm-dd",
        "dateSystem": "1900"
      },
      "after": {
        "type": "date",
        "value": "60",
        "formula": null,
        "cached": null,
        "numberFormat": "yyyy-mm-dd",
        "dateSystem": "1904"
      }
    }
  ]
}

The serial text and format are unchanged, but the workbook changes from 1900 to 1904. Both the metadata and date cell context change. The serial is never converted to a calendar string.

Inspect structural metadata separately

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "same"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "same"
          }
        ],
        "state": "hidden",
        "dimension": "A1:C3",
        "columns": [
          {
            "min": 1,
            "max": 2,
            "width": "12.5",
            "hidden": true
          }
        ],
        "merges": [
          "A1:B1"
        ]
      }
    ]
  }
}
{
  "counts": {
    "added": 0,
    "removed": 0,
    "changed": 4
  },
  "changes": [
    {
      "kind": "structure",
      "status": "changed",
      "field": "state",
      "before": "visible",
      "after": "hidden"
    },
    {
      "kind": "structure",
      "status": "changed",
      "field": "dimension",
      "before": null,
      "after": "A1:C3"
    },
    {
      "kind": "structure",
      "status": "changed",
      "field": "columns",
      "before": [],
      "after": [
        {
          "min": 1,
          "max": 2,
          "width": "12.5",
          "hidden": true
        }
      ]
    },
    {
      "kind": "structure",
      "status": "changed",
      "field": "merges",
      "before": [],
      "after": [
        "A1:B1"
      ]
    }
  ]
}

Visibility, the declared range, a column width/hidden range and a merge range change while A1 stays the same. A declared range or merge does not create additional cell values.

Keep formula-like text behind report tags

{
  "left": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "old"
          }
        ]
      }
    ]
  },
  "right": {
    "sheets": [
      {
        "name": "Data",
        "cells": [
          {
            "address": "A1",
            "type": "string",
            "value": "=SUM(1,2)"
          }
        ]
      }
    ]
  },
  "format": "csv",
  "filter": {
    "sheet": "",
    "status": "changed"
  }
}
{
  "csvRecords": [
    [
      "id",
      "kind",
      "status",
      "leftSheet",
      "rightSheet",
      "leftAddress",
      "rightAddress",
      "key",
      "field",
      "before",
      "after"
    ],
    [
      "text:1",
      "text:cell",
      "text:changed",
      "text:Data",
      "text:Data",
      "text:A1",
      "text:A1",
      "null:",
      "text:cell",
      "json:{\"type\":\"string\",\"value\":\"old\",\"formula\":null,\"cached\":null,\"numberFormat\":null}",
      "json:{\"type\":\"string\",\"value\":\"=SUM(1,2)\",\"formula\":null,\"cached\":null,\"numberFormat\":null}"
    ]
  ]
}

The result shown is parsed CSV records. Fixed headers are followed by text:, null: or json: tags on every data field. The untrusted =SUM(1,2) remains inside a JSON string rather than becoming a leading spreadsheet formula.

Common workbook comparison mistakes

  • Assuming similar sheet names or reordered columns are matched automatically.
  • Using a nonunique business label as a row key, or forgetting that a formula cell is not an eligible key.
  • Selecting the wrong header row and accidentally excluding data from key comparison.
  • Treating missing cells, explicit blanks and empty strings as equivalent.
  • Reading equal-looking numbers, dates or formula caches as proof that stored cells are identical.
  • Interpreting structural metadata as an inferred sequence of insertions, deletions or formatting operations.
  • Forgetting an active filter before downloading a report of every difference.
  • Removing CSV safety tags or assuming the report is a writable workbook patch.

Limits and notes

  • Only supported, unencrypted OOXML .xlsx workbooks are accepted, at most 16 MiB per file. Legacy XLS, binary XLSB, macro-enabled XLSM, encrypted workbooks and remote URLs are not supported. Macro, ActiveX, embedded-object and external-workbook-link packages are rejected; ordinary external hyperlink targets are ignored and never fetched. Only supported normal and shared formulas are read; unsupported formula forms fail. ZIP/XML validation and resource caps reject malformed, unsupported or excessive input. A worker and cancellation reduce UI blocking but do not create a hard browser memory sandbox. Per workbook, limits include 20 sheets, 100,000 cells, 10,000 physical row records, maximum material cell row 100,000 and column 1,024, 2,048 column-range records, 10,000 style XML nodes, 100,000 shared-string entries, 32,767 characters per text value and 8,192 characters per formula. ZIP allows at most 256 entries, 16 MiB expanded per entry, 32 MiB expanded total and a 200:1 expansion ratio; XML depth is at most 48 with 600,000 nodes across the package. Valid full-grid declared dimensions and merge ranges may be retained only as metadata; they do not bypass material-cell limits or allocate their whole area. Across a workbook, normal and expanded shared formulas total at most 4,194,304 UTF-16 code units; logical scalar values plus effective number-format strings total at most 16,777,216 UTF-16 code units, counting repeated references. The rebuilt core XML package passed to ExcelJS is capped at 32 MiB of UTF-8 bytes before ZIP overhead.
  • Whole-workbook mode matches sheet names exactly, including case. A rename therefore appears as a removed sheet and an added sheet with their cells, unless you explicitly select a sheet pair. Coordinate mode compares the same addresses: moved values produce removed/added or changed-address events. It does not infer row or column insertion, detect moved blocks, guess renames or perform fuzzy matching. Key mode is available for one selected sheet pair. Use one to eight distinct Excel column letters, with the same positions on both sides. Rows at or above the chosen header row are excluded; physically declared rows containing no cells are ignored. Missing, blank, empty-string, error or formula key cells and repeated typed composite keys stop the comparison. Text, numbers, booleans and date representations remain distinct; no trimming, case folding, Unicode normalization or numeric coercion occurs. Row-coordinate changes alone are ignored, while changed cells retain the before and after addresses. Declared dimension, column, merge or other sheet metadata may still differ after rows move. Encoded composite keys across both workbooks together are capped at 6 MiB, even if the records are unchanged.
  • Cells compare typed decoded values. Strings are compared after XML/shared-string decoding and booleans use true/false; equivalent encodings of those values are not byte-level differences. Numeric XML text is retained rather than rounded or compared with a tolerance: 1, 1.0 and 1e0 can differ even when Excel displays the same number. A missing cell, an explicit blank and empty text are different. Formula text and its cached typed result are separate fields; cached results can be absent or stale. No formula is recalculated, no external link is fetched, and text that resembles a formula is still text. Numeric date serials remain paired with the workbook’s 1900 or 1904 date system, and supported effective number-format text is compared separately. General is normalized to no special format. Date classification uses the parsed value and supported number-format detection rather than reproducing Excel’s full rendering engine. Explicit ISO date text is compared as stored and does not acquire an epoch-dependent cell change when only the workbook date system changes. No automatic epoch, calendar or time-zone conversion is performed. In the 1900 system, serial 60 remains the raw stored value rather than becoming a fabricated valid calendar date. Equal serial text with different workbook date systems must not be read as equal calendar dates. Some locale-specific built-in formats are represented by an ID marker such as [builtin:27], with known date IDs retaining date typing; the tool does not invent a locale-specific display pattern.
  • Structure means the reported workbook date system, sheet presence/name/order/visibility, declared sheet ranges, declared row presence in coordinate mode, column width/hidden ranges and merge ranges. Physical empty row records can produce coordinate-mode row events; key mode ignores row-presence events. These are metadata differences, not a reconstructed editing history. Number formats are compared, but fonts, fills, borders, visual layout, charts, images, comments, validation rules and other unsupported workbook features are not a visual or complete workbook-equivalence check. Rich text is flattened to text; merge areas and declared ranges are not expanded into invented cells.
  • A comparison is limited to 20,000 events and a bounded serialized event budget; each export is limited to 6 MiB, and each worker operation has a 20-second deadline subject to browser scheduling. Excessive results or exports fail rather than being silently truncated. The preview’s 25-event pages and 600-character value limit do not change the export scope. Filters intentionally restrict both the displayed events and the exported report; a specific sheet filter excludes workbook-level events such as dateSystem. Clear all filters when you need every difference. CSV is a UTF-8 report with fixed headers and mandatory tags on every data field: text: for report labels/addresses, null: for absent metadata, and json: for before/after typed JSON values. Remove exactly the first tag when deliberately decoding a field; json:null represents a missing cell and a blank cell is a JSON object. CSV quoting alone is not formula protection. Tags keep untrusted source text away from a leading spreadsheet formula; removing tags later reintroduces interpretation risks. JSON preserves the structured report directly. Neither format is a replacement workbook, binary backup or automatic patch. Input and results are not uploaded, stored persistently or sent to a third-party CDN; site assets still load normally and downloaded reports may contain sensitive data.

Frequently asked questions

Why does a renamed sheet appear as removed and added?

Whole-workbook comparison only pairs exact sheet names. It does not guess that similar sheets are the same. Select the before and after sheet explicitly to compare a renamed pair and report the name change alongside its cells.

Does a formula change mean its calculated result changed?

Not necessarily. Formula text and the stored cached result are compared separately, without recalculation. A changed formula may keep the same cache, and an unchanged formula may have a changed or missing cache. Open a trusted copy in your spreadsheet application if you need recalculated values.

Why are blank or repeated row keys rejected?

They cannot identify an unambiguous one-to-one row match. Every chosen key component must be present, nonblank and nonformula, and the complete typed tuple must be unique on each side. Text 001, text 1 and number 1 are different keys. Fix the source copy or choose a suitable composite key.

Does download include only the current page?

No. It includes all events matching the current filters, with complete values, subject to the export size limit. Clearing filters includes the whole comparison. The preview may clip long values, but successful exports do not silently clip them.

Related tools