A small deterministic spreadsheet tool: short JSON in, short JSON out. It creates styled workbooks, computes formulas, preserves existing XLSX packages during edits and chart insertion, and exports paginated Unicode PDF reports. It needs no network or Office installation at runtime.
op |
Fields | Result |
|---|---|---|
create |
output, sheets: [{name, rows, header?, widths?, number_format?, styles?}] |
XLSX or single-sheet CSV. Strings beginning with = are formulas with calculated caches. |
read |
input, sheet?, range?, max_cells? (default/ceiling 2000) |
Cell values in data, with sheet names and dimensions in the summary. |
edit |
input, output, sheet?, set: {"B2": value} |
Changes cells and recalculates formulas. XLSX→XLSX patches worksheet XML, preserving untouched parts including charts and styles. Input and output may be the same. |
calc |
input or sheets (not both), cells: ["B10", "Other!C2"] |
Calculated values in data. |
chart |
input, output, sheet?, type, data_range, title? |
Full workbook plus a native Excel chart beside the data. Types: column, bar, line, area, pie, doughnut, scatter, and column_stacked/bar_stacked/line_stacked/area_stacked. Input and output must be different files, including filesystem aliases. |
audit |
input, scope: "sheet" / "range" / "model", sheet?, range?, max_findings?, output? |
Sorted findings in data, plus an optional Markdown report (.md). Does not modify the input. |
convert |
input, output |
Input: XLSX/CSV/TSV/ODS/XLS. Output: XLSX, single-sheet CSV or PDF. XLSX→XLSX is an exact file copy. |
Paths are relative to Options::base_dir. Absolute paths, directory traversal and all symbolic
links are rejected, including links inside the base, dangling links and links in output parents.
Links that escape the base still return path.escapes_base, and other links return path.symlink.
Inputs must be regular files (input.not_regular otherwise). File kind and size are checked before
opening and again on the opened descriptor, and on Linux, opens use nonblocking/no-follow flags.
Parsers read a bounded in-memory snapshot instead of reopening the input path.
Input files default to a 100 MB limit, and ZIP packages also have a 256 MB expanded-size limit.
Cell XML and shared strings have a separate 64 MiB budget before decoding. XLSX values and formulas
are streamed into sparse maps (at most 200,000 records and 64 MiB of cell data/overhead across
sheets), and distant cells never allocate their bounding rectangle. ODS/XLS eager readers are
preflighted against a conservative 200,000-cell bounding-rectangle budget. Excess returns
input.cell_budget, independently of the response-only max_cells limit. Calls in the same process
are serialized so in-place edits read the latest state. If separate processes write the same file,
coordinate them externally. All output formats are built in memory, written to a unique temporary
file in the destination directory, synced, then renamed atomically. A failure before the rename
leaves the old destination unchanged. XLSX copies recognize inode aliases (including hardlinks),
buffer the source, and safely replace the destination entry. Other hardlinks keep their original
bytes. On Linux, parent directories are opened component by component without following links.
Temporary creation, cleanup and rename use the same pinned directory descriptor, so replacing its
pathname during a write cannot redirect the commit. Descriptor-safe filesystem I/O currently
requires Linux, and other targets fail closed.
Exports have a 32 MiB byte ceiling, a conservative uncompressed model/cache preflight and
bounded CSV/ZIP write buffers. CSV and PDF reject bounding rectangles above 200,000 slots
with export.budget before expanding gaps. Width fitting, PDF cell drawing and JSON reads
visit existing cells, and only bounded layout coordinates and output placeholders are
materialized. PDF also preflights drawing/text complexity, limits pagination to 500 pages and
checks serialized bytes before committing. Exact XLSX copies are limited to 32 MiB before reading.
calc accepts at most 2000 requested cells, and read clamps max_cells to 2000 with a truncation
warning when applicable. These conservative limits can reject compressible workbooks even
when their final ZIP would be small.
tests/acceptance.rs exercises a monthly sales table with 50 data rows, dates, regions,
products, integer units, unit prices, revenue formulas, percentage margins, 16-digit order IDs,
negative values and blanks:
| Scenario | Content checks |
|---|---|
S1 create → XLSX |
Bold/frozen header, fitted widths, number formats, text IDs, native product/SUM/AVERAGE formulas and caches, checked through XML, read, and forced LibreOffice recalculation. |
S2 create → CSV |
Raw numbers without grouping/binary arithmetic noise, ISO dates, unchanged IDs and numeric-looking text. |
S3 read |
Native XLSX, LibreOffice-saved XLSX and ODS, with typed values, detected header, worksheet and range selection. |
S4 edit |
Two Units changes update products, totals and averages, matching forced LibreOffice recalculation. |
S5 calc |
Selected computed cells from the same sales workbook. |
The seeded table generator checks 200 distinct tables through XLSX/read and CSV. It compares product/SUM/AVERAGE caches against independently recalculated LibreOffice caches to the last displayed digit, and checks repeat-request bytes and JSON. Seeds are included in assertions. A separate test spans a wall-clock change to check deterministic metadata. Hostile-input probes run under subprocess timeouts and check for stable, short JSON errors. LibreOffice is only a test oracle, and no Office process is used at runtime.
Date columns use an Excel date number format (for example yyyy-mm-dd). In such a column,
ISO YYYY-MM-DD input strings become native numeric date cells, and read returns ISO dates.
calc keeps numeric serials for arithmetic. read.summary_params.header reports the detected
header flag. CSV export writes raw numbers (15 significant decimal digits), not display grouping,
currency symbols or percent signs. Number cells with more than 15 significant digits emit
input.precision with a qualified cell. Use JSON strings for exact identifiers and decimals.
XLSX creation uses a fixed creation date, not the current clock.
Charts, audit, PDF, financial-model conventions and general conversion remain available but are outside S1–S5. Their existing coded warnings are listed in the warning catalog below. Unsupported or unaffordable transformations fail before committing output. In particular:
- PDF is approximate (
pdf.print_approximation,pdf.import_approximation). Unsupported chart types become labeled frames (pdf.chart_placeholder), and shortened category labels are identified (pdf.category_shortened). - Rebuilt imports and conversions can lose styling or objects (
convert.lossy,edit.formatting_not_preserved,chart.converted_input,read.legacy_cached_values). - Audit is heuristic, not assurance (
audit.heuristic_review,audit.unchecked_structure,audit.metadata_unavailable). - CSV has no type metadata. Exporting preserves numeric-looking strings verbatim, but reading
arbitrary CSV infers types and cannot tell a numeric string from a number. Use XLSX
for typed round trips. A CSV read reports
read.csv_inferred_types. - Formula cycles currently remain cell-level
#REF!values witheval.circular_referenceandok:true, which preserves the existing formula/audit API. They do not yet meet the stricter malformed-input invariant that requiresok:false, and that invariant is not claimed complete. - Date rendering uses the documented 1899-12-30 epoch.
readreturns time-only and date-time formats as calendar dates. The 1904 date system and the fictitious 1900-02-29 are not supported.
Creation defaults to a bold navy header with white text, a frozen first row, fitted column widths
and A4 landscape printing fitted to one page across. header: false disables the header treatment.
widths overrides fitted widths in Excel character units (finite values in [0, 255], where zero
keeps auto-fitting). Invalid dimensions return input.dimension_invalid with field, min and
max, including direct library calls with NaN/infinity. Imported dimensions are checked before
rendering. Row heights are limited to 409 points, and chart coordinates and extents to 1,000,000 mm.
A sheet number_format applies to body cells. styles is a positional array of column objects
accepting number_format, bold and background (a hex color). A column format overrides the
sheet format.
XLSX edits keep existing cell style indexes, column widths, merges, other sheets, drawings,
shared strings and other unmodified ZIP parts byte for byte. Literal text replacements use local
string values instead of rebuilding the shared string table, and formula caches are recalculated
across sheets. A stale calculation-chain part is reset when present. The ZIP container is
recompressed, so the archive as a whole is not byte-identical after an edit. Unhandled worksheet
XML shapes fail with xlsx.unsupported_xml instead of reporting a successful edit without changing
cells. XLSX chart insertion only changes the required worksheet/drawing relationships and
content-type registrations, and it adds new numbered parts rather than replacing existing charts.
PDF uses worksheet column widths and row heights, merged cells, solid fills, border colors,
bold text, horizontal/vertical alignment, wrapped text, numeric/date formats and repeated first-row
headers. Wide sheets use landscape and scale to the page width. Each sheet starts on a new page.
Charts are vector drawings at their worksheet anchors (two-cell, one-cell and absolute anchors),
not appended pages. Column, horizontal bar, line, area, pie, doughnut and numeric-X scatter charts
are supported, including stacked and percent-stacked series. References resolve to current cell
values before falling back to chart caches, and explicit RGB/theme series and pie-point colors are
read. Unsupported types, including combo/3D charts, print a titled frame with pdf.chart_placeholder
and the native type name. Malformed scatter data still fails with pdf.chart_unsupported.
Numeric axes use outward-rounded nice-number bounds and 4–8 intervals, including scatter X
and 100% stacked axes. Axis labels inherit source series number formats (grouping and percent
included), without redundant zero decimal places. Horizontal numeric labels are measured, centered
and clamped to the plot, and labels are skipped when their glyph bounds would overlap. Titles use
approximately 14 pt and axes and legends 9 pt, scaled with the chart anchor. Right-hand legends
stay to the right. Short category names use up to three staggered rails to keep month names whole.
Longer categories wrap or shrink. Unavoidable truncation uses an ellipsis, pdf.category_shortened,
and a following legend page with the complete category names. Ordinary area charts keep the first
logical series in front.
This is not a complete Excel print engine. pdf.print_approximation explicitly reports differences
in font metrics, chart styling, implicit grid lines and automatic page breaks. Numeric axes are
auto-scaled rather than reproducing manual bounds or log scales, and non-right legends use a bottom
layout. Markers and pie hole size use renderer defaults. Conditional formatting, pattern fills,
rich-text runs, rotated text and custom print areas are not reproduced. Long text is wrapped even
without a source wrap flag. Explicit row heights take precedence over auto-sizing. The first bold
header row repeats, but arbitrary multi-row print-title ranges are not imported. Common decimal,
grouping, percentage and calendar-date formats are supported. Commas after the last digit
placeholder scale by 1,000 each, so 0.0,, displays 1,234,567 as 1.2, while #,##0.0
still displays 1,234,567.0. Quoted or escaped commas remain literals. Unsupported number
formats fail before replacing an existing output. Unaffordable PDF layout spans or complexity
are rejected with export.budget before rendering.
ODS formulas are translated from OpenFormula references and separators. ODS and BIFF8 XLS PDF
imports include common cell styles, number formats, merges, column widths, row heights and embedded
charts. ODS chart references resolve within the workbook, and unresolved references or missing
embedded chart parts produce placeholders rather than silent omission. External links are never
fetched. BIFF8 chart groups, cell-based drawing anchors, palette colors and cached worksheet values
are read without an Office process at runtime. pdf.import_approximation reports that advanced
styles, images and other drawing objects are not reproduced. Malformed or excessive appearance data
fails with import.appearance_invalid. Non-PDF conversions of these formats still rebuild the
workbook and warn about lost styling and objects. They are not package-preserving conversions.
Legacy XLS uses saved cell values, not decoded formulas, because the upstream reader cannot reliably
decode BIFF relative references. Unsupported BIFF formula records and formulas containing text
literals fail with xls.formula_unsupported. The recommended workaround is to save such a workbook
as XLSX. XLS tests cover numeric formula caches and Unicode text, not general BIFF formula
recalculation.
create accepts workbook-level names (name → Sheet!A1:B3) and checks
(label → boolean formula starting with =). Name targets are validated and stored as absolute
ranges. Global and imported sheet-local names resolve case-insensitively during calculation.
Sheet specs also accept:
comments: {"B2": "Source: Annual report, 2026-01-01, Note 4"}: native Excel notes.font_colors: {"B2:D8": "blue", "E2": "black", "F2": "green"}, or#RRGGBB.merges: ["A1:F1"]: horizontal section headers. Covered cells must be empty.freeze_panes: "B3": the first scrollable cell (freezes the rows above and columns left).- Existing column/sheet
number_formatsupports multiples such as0.0\"x\".
edit accepts the same workbook fields, plus comments, font_colors, merges, and
freeze_panes on the selected sheet. set may be omitted for metadata-only edits.
Checks are upserted by label on a Checks tab, wrapped to return TRUE/FALSE, and get native
red/green conditional formatting. Check formulas that evaluate to errors are rejected before
writing. Repeating an edit preserves existing names, notes and Checks, and concurrent library calls
read the latest package under the same file-operation lock. Existing rich notes and VML shapes
are kept, except for note cells you explicitly update. Threaded-comment updates fail explicitly
instead of being silently converted. Model metadata requires XLSX/XLSM output, never CSV.
{"op":"edit","input":"model.xlsx","output":"model.xlsx","sheet":"Inputs","names":{"Growth":"Inputs!B2"},"comments":{"B2":"Source: Approved forecast, 2026-01-01"},"font_colors":{"B2":"blue"},"checks":{"Growth below WACC":"=Growth<Inputs!B3"}}{"op":"audit","input":"model.xlsx","scope":"model","max_findings":100,"output":"audit.md"}sheet reviews the selected sheet (the first sheet by default). range requires an A1 range on
that sheet. model reviews every sheet and rejects sheet/range restrictions. Findings reference
the original cells and contain severity, sheet, cell, check, message, and params.
They are deduplicated per check/cell and sorted by severity (critical, warning, info), sheet name,
row, column and check code before max_findings is applied (default 1000, minimum 1).
The summary includes model_type and the total and returned counts. A report contains the returned
findings, not undisclosed rows beyond the limit. Markdown output cannot alias the input.
Check code (audit. prefix) |
Rule |
|---|---|
formula_error |
Excel error values or supported formulas evaluating to errors. |
hardcode |
Numeric literals anywhere in a formula AST, including 0/1. Quoted text and reference digits are not constants. |
inconsistent_formula |
A formula differs from two immediately flanking, matching copy-pattern signatures, horizontally or vertically. $ anchors are respected. |
omitted_range_edge |
Single-range SUM/AVERAGE omits an immediately adjacent numeric row. The aggregate cell itself is excluded. |
pasted_value |
A numeric/boolean literal lies between matching formulas. |
circular_reference |
Every member of a dependency cycle, including ranges and names. Downstream errors are separate error findings. |
missing_sheet, missing_name |
Parsed references cannot resolve. No duplicate generic error is reported on that same cell. |
scale_mismatch |
A numeric literal is about 1000× or 1/1000× two comparable immediate neighbors (ratio 500–2000 or 0.0005–0.002). |
whole_percentage |
Absolute value >1 with a percent format or a % row/column label. |
hidden_data |
First selected data cell of each hidden sheet, or each hidden row on a visible sheet. |
balance_sheet |
Total Assets versus Total Liabilities + Total Equity/Equity, or combined Total Liabilities and Equity/Total Liabilities & Equity. |
cash_tie_out |
Ending Cash/Closing Cash on CF/CFS/Cash Flow/Cash Flow Statement versus Cash/Cash and equivalents/Cash & equivalents on BS/Balance Sheet. |
roll_forward |
Beginning Balance/Opening Balance through Ending Balance/Closing Balance, including repeated blocks. Uses signed activity, or treats positive Deductions/Reversals/Payments/Settlements/Dividends as outflows. |
input_separation |
A formula appears on Inputs/Assumptions/Drivers. Numeric interruptions in calculation blocks are covered by pasted_value. |
font_color |
Numeric inputs blue, formulas black, cross-sheet formulas green (info). |
Model checks use case-insensitive, trimmed English labels in column A and numeric period columns
B onward, with an absolute tolerance of 0.01. Cash periods align by column position, and periods
and currencies must already be aligned. Model type is inferred from DCF/WACC, LBO/Sources & Uses,
comps/comparable, or BS/balance-sheet identifiers, otherwise custom. The audit does not
understand the meaning of arbitrary financial models. Historical inputs, legitimate formula
constants, intentional circularity, valid >100% rates and changes in scale may require human review.
Unrecognized structures produce audit.unchecked_structure, not a clean bill of health.
Unsupported formulas keep their evaluator warnings, and parser-synthesized errors alone are not
reported as verified spreadsheet errors. Legacy XLS has cached values only. XLSX is recommended for
full audits. Hidden-data metadata is currently imported from XLSX/XLSM, not ODS/XLS.
A small reproducible DCF with five injected defects is in
examples/dcf-audit-create.json. Run it, then run
examples/dcf-audit.json. Expected findings: broken reference at DCF!B6,
hardcoded uplift at B7, missing name at B8, cycle at B9, whole-number percentage at B10.
The audit also explicitly warns that this DCF lacks the three financial-statement structures.
{
"op": "create",
"output": "report.xlsx",
"sheets": [{
"name": "Sales",
"styles": [{}, {"number_format": "#,##0.00", "bold": true}],
"rows": [["Month", "Total"], ["Jan", 100], ["Feb", 150], ["Sum", "=SUM(B2:B3)"]]
}]
}{
"ok": true,
"output": "report.xlsx",
"bytes": 5400,
"summary": "created 1 sheet(s) with 8 cell(s)",
"summary_code": "create.done",
"summary_params": {"sheets": 1, "cells": 8},
"warnings": []
}The byte size above is illustrative. Empty optional fields are omitted. Warnings are structured:
{"code":"read.truncated","message":"output truncated to max_cells=2","params":{"max_cells":2}}Diagnostics keep at most 32 distinct messages. Identical code/parameter pairs are aggregated
with params.occurrences when repeated, and diagnostics.truncated reports the exact params.omitted
count beyond that budget. Evaluation itself uses this budget, independently of max_cells.
Response strings are limited to 512 Unicode characters plus an ellipsis. response.truncated
reports shortened data values, and shortened message parameters carry truncated: true.
Saved workbook cells are not shortened. At most 34 warnings are returned (32 distinct
messages plus diagnostic-omission and response-truncation notices).
All built-in messages are in src/messages.rs. Options::locale defaults to en, and unknown
locales fall back to English. Errors expose code() and params() and keep underlying
I/O, parser and writer causes where available. Their Display is English.
| Code | Trigger |
|---|---|
audit.heuristic_review |
Every audit: rules are heuristics, not financial assurance. |
audit.metadata_unavailable |
ODS/XLS audit cannot import names, hidden-data metadata or font colors. XLSX is recommended. |
audit.unchecked_structure |
Model scope cannot identify balance-sheet, cash tie-out, or roll-forward structures. |
audit.truncated |
max_findings limits the findings list and report. Total and returned counts are supplied. |
read.truncated |
max_cells limits the result. |
input.precision |
A numeric input exceeds 15 significant decimal digits, and params.cell names the cell. Use text for exact IDs/decimals. |
read.csv_inferred_types |
CSV has no cell types. Numeric-looking fields are inferred and may require XLSX for typed preservation. |
diagnostics.truncated |
Diagnostic budget exceeded. params.omitted counts omitted messages, including previously aggregated occurrences. |
response.truncated |
Long response values shortened. params.limit and params.values describe the truncation. |
read.legacy_cached_values |
XLS input uses saved values instead of decoded BIFF formulas. |
eval.unknown_sheet |
A formula refers to a missing sheet. |
eval.circular_reference |
A formula cycle produces #REF!. |
eval.formula_unparseable |
A formula cannot be parsed and produces #VALUE!. |
eval.range_too_large |
A formula range exceeds 200,000 cells. |
eval.unknown_function |
An unknown function produces #NAME?. |
eval.invalid_arguments |
An invalid function argument count produces #VALUE! (#N/A above Calc's 255-argument limit) before evaluating arguments. |
eval.format_unsupported |
TEXT rejects a format or value outside formatter bounds with #VALUE!, rather than returning misleading text. |
eval.depth_limit |
Formula complexity or evaluation/dependency depth exceeds the fixed budget. The result is #VALUE!. |
eval.numeric_overflow |
A cell has a numeric #NUM! result, including non-finite arithmetic/aggregation. |
eval.text_budget |
A text value exceeds 32,767 characters or cumulative evaluation text work exceeds 16 MiB. Affected results become #VALUE!. |
edit.formatting_not_preserved |
A non-XLSX edit rebuilds cell data. |
chart.converted_input |
Non-XLSX input is rebuilt before adding a chart. |
convert.lossy |
A conversion cannot preserve source styling/objects (including CSV export). |
pdf.layout_adjusted |
Reserved legacy report-layout warning. The code and meaning are retained, but anchored printing no longer emits it. |
pdf.print_approximation |
PDF preserves common print features but uses its own font metrics, chart styling, grid lines and page breaks. |
pdf.import_approximation |
ODS/XLS PDF imports common appearance and charts, but not advanced styles, images or other drawing objects. params.format names the input format. |
pdf.chart_placeholder |
An unsupported chart type prints a titled frame. params.kind names the native type. |
pdf.category_shortened |
A chart category needs an ellipsis. Full categories are retained on a following legend page. |
pdf.font_missing |
No fallback font is available. The built-in font may lose Unicode. |
pdf.font_unparseable |
Font parsing fails. The built-in font may lose Unicode. |
pdf.save_warning |
The PDF serializer reports a non-fatal diagnostic. |
Supported syntax: A1 references, ranges, cross-sheet references, + - * / ^ & = <> < > <= >=,
percentages and these functions:
TRUE, FALSE, SUM, AVERAGE, MIN, MAX, COUNT, COUNTA, IF, AND, OR, NOT, ROUND, ABS,
ROUNDUP, ROUNDDOWN, SUMIF, COUNTIF, AVERAGEIF, SUMIFS, COUNTIFS, AVERAGEIFS,
VLOOKUP, HLOOKUP, XLOOKUP, INDEX, MATCH, OFFSET, CHOOSE, IFERROR, IFS, SWITCH, TEXT, CONCAT,
TEXTJOIN, LEFT, RIGHT, MID, LEN, UPPER, LOWER, TRIM, SUBSTITUTE, SUMPRODUCT,
MEDIAN, DATE, YEAR, MONTH, DAY, EOMONTH, NETWORKDAYS.
IF, IFERROR, IFS and SWITCH evaluate only the selected result branch. Criteria aggregates
in the *IFS family and SUMPRODUCT reject mismatched array dimensions. SUMIF/AVERAGEIF instead
extend the supplied result range from its top-left cell to the criteria range's dimensions, as Calc
does. VLOOKUP/HLOOKUP default to approximate lookup. Exact VLOOKUP/HLOOKUP/MATCH support *, ?
and escaped ~*, ~?, ~~ patterns (case-insensitive).
XLOOKUP supports exact, nearest and wildcard modes with forward/reverse searches. Conditional
aggregates propagate error-valued criteria and errors in selected value cells. Text and empty value
cells are ignored, while booleans contribute 1/0. Literal text criteria such as "#N/A" can match
error cells. Numeric criteria do not match numeric text, but textual numeric criteria can. Logical
criteria compare logical/numeric 1/0 rather than arbitrary truthiness. Wildcards do not match
blank cells or empty text. Aggregates treat direct text arguments differently from text in
references. SUM/AVERAGE/MIN/MAX/MEDIAN reject direct text and ignore reference text, COUNT accepts
direct numeric text, and AND/OR reject direct text. SUMPRODUCT includes booleans as 1/0.
ROUND uses decimal input precision around half-way boundaries, and ROUNDUP/ROUNDDOWN avoid
intermediate overflow for large finite inputs. Lookup-key errors generally propagate, but Calc's
next-smaller XLOOKUP and exact reference-error keys with a not-found alternative use that fallback.
Approximate horizontal HLOOKUP/MATCH scans stop at an error in the key row, and vertical ascending
searches skip error keys. These error behaviors are checked against forced LibreOffice
recalculation, including selected and unselected errors.
Functions return scalar values. Dynamic spilling and general array arithmetic are not implemented.
TEXT shares the supported PDF number-format subset, including ddd/dddd weekday names and
mmm/mmmm month names. Scientific, fraction, elapsed-time and optional fractional-digit formats
are rejected explicitly. Date values outside the supported calendar range and overflowing
percentages are also rejected. Finite large fixed-point values are formatted at 15 significant
digits without intermediate overflow. Dates use Calc's 1899-12-30 epoch, and neither the fictitious
1900-02-29 nor the 1904 system is implemented. CHOOSE accepts 1–30 choices (31 arguments including
its index), matching Calc rather than Excel's larger choice list. Two-digit DATE years keep the
fixed 1900–1999 window, and the oracle profile sets that cutoff explicitly instead of inheriting
local Office preferences. Formula comparisons keep numbers and text distinct (numbers sort before
text), and logical values compare numerically as in Calc. IF(TRUE) returns TRUE, while an
explicitly empty selected result argument returns zero. All function signatures are checked.
TODAY and NOW are deliberately unsupported, and no formula reads the system clock. They return
#NAME? with the same named warning as any unknown function.
Unary signs bind before exponentiation, and powers associate left to right, as in Excel/Calc.
For example, -2^2 is 4 and 2^3^2 is 64. Numeric results must be finite. Overflow returns
#NUM!, never JSON null or a zero cache. Text functions propagate argument errors, and literal
errors are native OOXML error cells, not strings. INDEX, lookup indexes, OFFSET and CHOOSE truncate
fractional indexes before checking bounds. OFFSET returns a scalar or a range consumed by an
aggregate. CHOOSE evaluates only its selected alternative.
Computed text values are limited to 32,767 Unicode characters. Concatenation (&, CONCAT,
TEXTJOIN) and SUBSTITUTE calculate output lengths before allocating, and case conversion also
checks expansion. A shared 16 MiB cumulative text-work allowance covers intermediates and
cache reads, is not refunded, and produces #VALUE! plus eval.text_budget when exhausted.
So repeated reads can exhaust the allowance even if the final live result would be smaller.
Formula parsing conservatively allows at most 64 structural tokens (parentheses, operators,
percent signs), which bounds recursive descent and AST traversal/drop. Evaluation separately caps
expression/name/cell dependency depth at 64. Audit reference expansion uses the same combined
expression/name depth budget, including names in unevaluated branches. Exceeding it fails with
audit.depth_limit rather than returning a review that only looks complete. Dependency cycle
detection is iterative. Wide formulas can reach the complexity limit even without deep nesting.
Rejection returns #VALUE! with eval.depth_limit and, for parser rejection,
eval.formula_unparseable with a human-readable complexity-limit detail.
Excel errors are ordinary cell values. This is not a full Excel calculation engine, so check the warnings when you process unfamiliar formulas.
use sheets::{run, Input, Options};
let input: Input = serde_json::from_value(serde_json::json!({
"op": "read", "input": "report.xlsx"
}))?;
let output = run(&input, &Options::new("."))?;
println!("{}", serde_json::to_string(&output)?);sheets '{"op":"read","input":"report.xlsx"}'
echo '{"op":"read","input":"report.xlsx"}' | sheets
sheets --schemaThe CLI uses the current directory as its base and prints exactly one JSON object on stdout for both success (exit 0) and failure (exit 1). Request errors do not write to stderr:
{"ok":false,"summary_code":"io.failed","summary_params":{"path":"missing.xlsx","cause":"not_found"},"summary":"cannot read or write \"missing.xlsx\"","warnings":[]}Errors use exactly ok, summary, summary_code, summary_params, and warnings, with no
nested error object. Library errors serialize to the same envelope, and Error::to_output(locale)
provides the corresponding Output. parse_input reports missing or invalid schema field paths and
JSON syntax line/column, and schema() gives allowed types and values. Audit/chart validation
also supplies field, cause and allowed. Errors include the library's stable code and params.
I/O adds a safe cause category, such as not_found, permission_denied, storage_full or
other, and output I/O also has operation: "write". Raw OS errors are kept as library causes
and are not printed by the CLI. Invalid UTF-8 arguments or stdin return cli.invalid_encoding,
and extra arguments return cli.unexpected_arguments. Requests larger than 100 MB return
cli.request_too_large, and stdin is read with that byte limit.
sheets::schema() exposes the input JSON Schema.
sheets --messages(alsosheets docs --messages) returns{"tool":"sheets","version":"…","messages":[…]}directly from the rendering catalogue. Each entry hascode,kind, an Englishtemplate, and the template's parameter names inparams. Codes remain stable.sheets --schemareturns a form-oriented projection of the schemars-derived input schema: a top-leveloneOfdiscriminated byop, documented properties and operation variants, and no property-level unions or conditional keywords. Optional values are omitted in forms, but the decoder still accepts legacy null inputs. Input paths carryx-file: "input"andx-accept, and output paths carryx-file: "output"and orderedx-formats(primary first). Extension lists contain names without a leading dot.- Cross-field rules are validated at runtime. For example,
calcrequires exactly one ofinputandsheets. Every error includessummary_params.fieldas an RFC 6901 JSON Pointer (""for the whole request), andcause. Enum errors include anallowedarray. Pointer tokens escape/as~1and~as~0. Syntax errors also includeline/column. - All operation envelopes include
warnings, even when empty. Argument and stdin requests use the same decoder and operation path and return identical JSON. Errors use the same flat envelope in the CLI and library, with a nonzero CLI exit status and no diagnostic stderr. - File access, byte and diagnostic budgets, atomic commits and Linux descriptor guarantees are documented above. Error and data strings are bounded, but saved files are not response-truncated.
tests/integration_contract.rs walks every schema node, checks catalogue consistency and
transport parity, and exercises escaped field pointers. tests/invariants.rs generates bounded
malformed-file probes and checks cross-clock deterministic XLSX output.
Run cargo test and cargo clippy --all-targets -- -D warnings.
cargo test --test formula_oracle compares 9387 distinct formulas, inline and through XLSX, against
committed, freshly recalculated LibreOffice results in tests/fixtures/formula_oracle.json.
Numbers compare at Calc's 15-significant-digit XLSX cache precision. Text, logical values and
errors compare exactly.
The CI workflow runs this offline regression without Office. The corpus covers every supported
function, signatures, mixed types, error data, differing range shapes, wildcard escapes and
calendar/scaling/percentage/extreme formats. scripts/generate_formula_cases.py generates the
function × argument-role × value-type matrix with direct/reference/single-cell-range provenance,
including every optional fixed-arity position. cargo test --test variadic_oracle adds 270568
position/type/provenance vectors for all 18 variadic functions, stored compactly in
tests/fixtures/variadic_oracle.json. Every supported argument index is exercised through 255
(254 for paired IFS/COUNTIFS, and CHOOSE follows Calc's 31-argument ceiling). Short-arity,
mixed-range and invalid-arity vectors supplement that matrix. Together the tests compare 279955
vectors on each input path. Long TEXTJOIN results are also checked against complete native XLSX
caches, not only the deliberately shortened JSON response.
Independent batches keep differential evaluation and responses bounded. Explicitly unsupported
formats keep the Calc expectation alongside their documented rejection and must produce an error
plus a warning. They cannot silently pass with a different value. Refresh with
python3 scripts/refresh_formula_oracle.py and python3 scripts/refresh_variadic_oracle.py.
Both need the Python stdlib, Cargo and soffice, and the exhaustive variadic pass takes several
minutes. The variadic script also accepts --only FUNCTION... for selective refresh. Both replace
formula caches with a sentinel and enable OOXML recalculation in an isolated Office profile.
The source worksheet is generated independently of engine results. Modern OOXML functions
use _xlfn prefixes, and Calc's importer receives native NA()/division expressions for literal
error values. Non-text TEXT format arguments and unsupported format tokens keep explicit rejection
expectations, never an engine-generated expected value.
The Office interoperability test requires soffice (LibreOffice) locally and generates its ODS/XLS
fixtures in temporary directories. Function vectors (including 105 conditional-aggregate/lookup
error cases) are compared to CSV after forced Office recalculation with deliberately stale input
caches (the older live interoperability test uses an explicit 1900 two-digit-year cutoff). Native
chart tests also round-trip through LibreOffice. Appearance tests import Office-generated ODS and
XLS bytes, assert styles, dimensions, series, stacked flags and chart placeholders, then check
their PDF output. All fixtures are generated, and no binary fixtures are committed.
Developed with AI assistance (Claude, GPT agents) under the maintainer's review.
MIT OR Apache-2.0, at your option.