@rowsncolumns/version-comparison
v9.0.0
Published
Version comparison and diff highlighting for spreadsheets
Readme
@rowsncolumns/version-comparison
A library for comparing spreadsheet versions and highlighting differences between cell data, values, and formatting.
Installation
npm install @rowsncolumns/version-comparison
# or
yarn add @rowsncolumns/version-comparisonFeatures
- Cell Value Comparison: Detect added, deleted, and modified cell values
- Format Comparison: Track formatting changes (bold, italic, font size, colors, etc.)
- React Hooks: Easy integration with React applications via
useVersionComparison - Diff Engine: Core comparison algorithms for cell values and formats
Usage
Basic Usage with React
import { useState } from "react";
import { useVersionComparison } from "@rowsncolumns/version-comparison";
const MyComponent = () => {
// Manage comparison mode state yourself
const [isComparing, setIsComparing] = useState(false);
// Get current cell data (from your spreadsheet state)
const getCellData = (
sheetId: number,
rowIndex: number,
columnIndex: number,
) => {
return currentSheetData[sheetId]?.[rowIndex]?.values?.[columnIndex];
};
// Get previous cell data (from a snapshot)
const getPreviousCellData = (
sheetId: number,
rowIndex: number,
columnIndex: number,
) => {
return previousSheetData[sheetId]?.[rowIndex]?.values?.[columnIndex];
};
// Version comparison hook - pass null when not comparing
const { getCellDiff } = useVersionComparison({
getCellData,
getPreviousCellData: isComparing ? getPreviousCellData : null,
});
return (
<div>
<button onClick={() => setIsComparing(true)}>Compare Versions</button>
<button onClick={() => setIsComparing(false)}>Exit Comparison</button>
<CanvasGrid
// ... other props
isComparing={isComparing}
getCellDiff={getCellDiff}
/>
</div>
);
};With Shared Strings
If your cell data uses shared strings (ss property), pass the shared strings map:
const { getCellDiff } = useVersionComparison({
getCellData,
getPreviousCellData,
sharedStrings, // Map<string, string>
});If the previous version has different shared strings (e.g., from a snapshot), pass both:
const { getCellDiff } = useVersionComparison({
getCellData,
getPreviousCellData,
sharedStrings: currentSharedStrings,
previousSharedStrings: snapshotSharedStrings,
});Format Resolution (cellXfs, cellStyleStore, conditional formats, …)
Format resolution is caller-driven. The hook never inspects ef.sid / uf.sid style references and never looks anything up in a cellXfs registry — it just compares the CellFormat objects you hand back. This keeps the hook agnostic to whatever resolution chain your app uses (cellXfs sid lookup, runtime overlays from a cell style store, conditional formatting, derived formats, …).
Pass getEffectiveFormat / getPreviousEffectiveFormat to opt in to format comparison:
const { getCellDiff } = useVersionComparison({
getCellData,
getPreviousCellData,
// Current side: resolve via your existing pipeline. If you use
// useSpreadsheetState, its getEffectiveFormat already merges
// cellStyleStore + cellXfs sid + inline + derived format and is a
// drop-in fit:
getEffectiveFormat,
// Previous side: walk the snapshot yourself when there's no
// spreadsheet-state instance attached to it.
getPreviousEffectiveFormat: (sheetId, row, col) => {
const cell = previousSheetData[sheetId]?.[row]?.values?.[col];
if (!cell) return null;
const fmt = cell.ef ?? cell.uf;
if (!fmt) return null;
if ("sid" in fmt && typeof fmt.sid === "string") {
return previousCellXfs.get(fmt.sid) ?? null;
}
return fmt;
},
});If you omit both resolvers, format changes simply aren't detected — only value changes will show up in the diff.
If both sides share a resolver, just pass getEffectiveFormat — getPreviousEffectiveFormat defaults to it.
Diff States
The getCellDiff function returns a CellDiff object with one of these states:
| State | Description |
| ------------ | -------------------------------------------------- |
| "added" | Cell exists in current version but not in previous |
| "deleted" | Cell exists in previous version but not in current |
| "modified" | Cell exists in both but value or format changed |
| null | No difference (cell unchanged or both empty) |
CellDiff Structure
type CellDiff = {
sheetId: number;
rowIndex: number;
columnIndex: number;
state: "added" | "deleted" | "modified";
detail?: {
valueChanged: boolean; // True if cell value changed
formatChanged: boolean; // True if formatting changed
oldValue?: ExtendedValue; // Previous value
newValue?: ExtendedValue; // Current value
oldFormattedValue?: string; // Previous formatted display value
newFormattedValue?: string; // Current formatted display value
oldFormat?: CellFormat; // Previous cell format
newFormat?: CellFormat; // Current cell format
};
};Diff Engine
The library exports comparison functions for direct use:
import {
areCellValuesEqual,
areCellFormatsEqual,
} from "@rowsncolumns/version-comparison";
// Compare two cell values
const valuesMatch = areCellValuesEqual(
{ sv: "Hello" }, // ExtendedValue
{ sv: "Hello" },
); // true
// Compare two cell formats
const formatsMatch = areCellFormatsEqual(
{ textFormat: { bold: true } },
{ textFormat: { bold: false } },
); // falseValue Comparison
areCellValuesEqual compares all value types in ExtendedValue:
- String values (
sv/stringValue) - Number values (
nv/numberValue) - Boolean values (
bv/boolValue) - Formula values (
fv/formulaValue) - Error values (
ev/errorValue)
Format Comparison
areCellFormatsEqual compares all properties in CellFormat:
- Text format (bold, italic, font size, font family, color, strikethrough, underline)
- Background color
- Borders (top, bottom, left, right)
- Number format (type, pattern)
- Alignment (horizontal, vertical)
- Wrap strategy
- Indent
- Text rotation
Integration with CanvasGrid
The version comparison integrates with CanvasGrid via the CellDiff component:
<CanvasGrid
// ... standard props
isComparing={isComparing}
getCellDiff={getCellDiff}
/>Visual Highlighting
| Diff State | Background | Text Color | Additional |
| ---------- | ------------------------------------------ | -------------------------- | ------------- |
| Added | Light green (#d0fae1) | Dark green (#046e38) | - |
| Deleted | Light red (#ffdbdb) | Dark red (#b21313) | Strikethrough |
| Modified | Shows both old and new values side by side | Red for old, green for new | - |
Format-Only Changes
When only the format changes (same value, different formatting):
- Both old and new values are displayed side by side
- Old value shows with previous formatting (e.g., bold)
- New value shows with current formatting (e.g., normal)
API Reference
useVersionComparison
function useVersionComparison<T extends CellData = CellData>(
options: UseVersionComparisonOptions<T>,
): UseVersionComparisonReturn;
type UseVersionComparisonOptions<T extends CellData = CellData> = {
/** Function to get current cell data */
getCellData: (
sheetId: number,
rowIndex: number,
columnIndex: number,
) => T | null | undefined;
/** Function to get previous cell data (null = no previous version / not comparing) */
getPreviousCellData:
| ((
sheetId: number,
rowIndex: number,
columnIndex: number,
) => T | null | undefined)
| null;
/**
* Resolved effective `CellFormat` for the current version. The caller owns
* all resolution — inline `ef` / `uf`, sid → cellXfs lookup, cellStyleStore
* overlays, conditional formatting, etc. If omitted, format changes are
* not detected (only value changes contribute to the diff).
*/
getEffectiveFormat?: (
sheetId: number,
rowIndex: number,
columnIndex: number,
) => CellFormat | null | undefined;
/**
* Resolved effective `CellFormat` for the previous version. Defaults to
* `getEffectiveFormat` if omitted (useful when both sides share a resolver).
*/
getPreviousEffectiveFormat?: (
sheetId: number,
rowIndex: number,
columnIndex: number,
) => CellFormat | null | undefined;
/** Shared strings map for current version */
sharedStrings?: Map<string, string> | null;
/** Shared strings map for previous version (defaults to sharedStrings) */
previousSharedStrings?: Map<string, string> | null;
};
type UseVersionComparisonReturn = {
/** Get diff for a specific cell */
getCellDiff: (
sheetId: number,
rowIndex: number,
columnIndex: number,
) => CellDiff | null;
};areCellValuesEqual
function areCellValuesEqual(
a: ExtendedValue | null | undefined,
b: ExtendedValue | null | undefined,
): boolean;areCellFormatsEqual
function areCellFormatsEqual(
a: CellFormat | null | undefined,
b: CellFormat | null | undefined,
): boolean;Types
All types are exported from the library:
import type {
CellDiff,
CellDiffState,
CellDiffDetail,
CellDiffHighlight,
GetCellDataFn,
GetEffectiveFormatFn,
UseVersionComparisonOptions,
UseVersionComparisonReturn,
} from "@rowsncolumns/version-comparison";License
MIT
