Commit Graph
2630 Commits
Author SHA1 Message Date
aiongg 2b808def4f refactor(engine): move cmd/edit tests to src/cmd/edit/tests.rs 2026-10-26 18:15:40 +00:00
aiongg 97c2a984e0 refactor(engine): move cmd/data tests to src/cmd/data/tests.rs 2026-10-26 18:15:39 +00:00
aiongg da40ecba3a refactor(gridcraft-cli): move check tests to src/check/tests.rs 2026-10-26 18:15:39 +00:00
aiongg 58e33dfef6 refactor(gridcraft): move logging tests to logging/tests.rs 2026-10-26 18:15:32 +00:00
aiongg b3e2a6a2be refactor(xlsx): move crates/xlsx/src/tests/mod.rs to crates/xlsx/src/tests.rs 2026-10-26 18:15:26 +00:00
aiongg 24a18620d2 refactor(xlsx): move crates/xlsx/src/xls/mod.rs to crates/xlsx/src/xls.rs 2026-10-26 18:15:26 +00:00
aiongg e95c29b1fe refactor(engine): move crates/engine/src/cmd/mod.rs to crates/engine/src/cmd.rs 2026-10-26 18:15:25 +00:00
Harrison Booth acc32227cb IMSQRT: match the zero real part by pattern (clippy redundant_guards) 2026-10-26 13:03:51 -05:00
Harrison Booth 1fadf98a10 IMSQRT: reproduce Excel's stray real part for negative reals
Excel computes IMSQRT through the polar form, so IMSQRT("-4") is
1.22514845490862E-16+2i rather than 2i. Tests assert strings read back
from Excel; the function-parity doc lists the remaining IM* differences.
2026-10-26 12:58:54 -05:00
Harrison Booth f4f48f3b23 CSV export values: choose the date or time shape from the format section that formats the value 2026-10-26 12:51:42 -05:00
Harrison Booth 9595fc8ee7 CSV export values: pick the ISO shape from the format's date and time parts; guard the file test on wasm32 2026-10-26 12:42:23 -05:00
Harrison Booth 9515c29969 CSV export: file.exportCsv values:true writes raw values for automation (#2648)
The default stays Excel's displayed text, byte for byte.
2026-10-26 12:34:18 -05:00
Harrison Booth c72f5a49b0 PERCENTRANK test: check the fifteenth digit with a tight tolerance 2026-10-26 11:48:54 -05:00
Harrison Booth 5ed5c09bc6 NUMBERVALUE: a leftover dot is a decimal point, not a date separator 2026-10-26 11:39:22 -05:00
Harrison Booth a75b19e678 MOD test: use the literal Excel reads (15 significant digits) 2026-10-26 11:32:08 -05:00
Harrison Booth 551ec6590d Fix literal grouping in the MOD test 2026-10-26 11:25:20 -05:00
Harrison Booth 9cc126409b Match Excel for YEARFRAC basis 0, MOD, NUMBERVALUE and PERCENTRANK edge cases
Checked against Excel 16: YEARFRAC basis 0 tests the end-31 rule against the original start day; MOD gives #NUM! once the quotient magnitude reaches 1.1259E12; NUMBERVALUE no longer rejects unmatched separators; PERCENTRANK gives #DIV/0! from significance 309 and 15 significant digits from significance 15.
2026-10-26 11:15:00 -05:00
prxnvt 1d7206ebdd Docs: Python cells as data (architecture, file formats, functions, roadmap)
architecture.md gains a Python cells section: the model type, the text
forms and the size limit, the xlsx Python part, calc laying out kept
results as graph nodes, and the limits on open. file-format-parity.md
gains a Python cells table (xlsx as [MS-XLSX] 2.3.10 specifies, the
formula naming its code in the Python part, with cached values and
spills; other formats; GridCraft JSON; the limits on open; and
rich-value object results as not kept). function-parity.md marks the
four Python errors done with their ERROR.TYPE numbers and adds a Python
in cells row (stored, not run). ROADMAP.md records the progress. The
return types are written as "0 or 1" so no table cell holds an
unescaped pipe. architecture.md's "Last updated" label loses the space
inside its bold markers.
2026-10-26 10:18:38 -04:00
prxnvt fcbbfa5142 XLSX: Python cells as _xlfn._xlws.PY() formulas, code in the Python part, cached values; opening limits
A Python cell is written to xlsx as [MS-XLSX] 2.3.10 specifies: its
formula is _xlfn._xlws.PY(i,0|1), where i is the zero-based index of the
cell's script in the workbook's Python part and 0|1 the return type, 0
for an Excel value and 1 for a Python object. The part (2.1.23) is
xl/python.xml, content type application/vnd.ms-excel.python+xml,
related from the workbook part with
http://schemas.microsoft.com/office/2023/09/relationships/Python; its
root python holds pythonScripts, one pythonScript per Python cell in the
order the cells are written, with the code in code. The code keeps its
xl() references as text, so the formula has no further arguments. The
cell keeps its cached value. A spilling result is written like a dynamic
array (cm="1", t="array" ref=...) with the spilled values cached, so
other applications show the values (and #NAME? if they recalculate).

The reader turns a PY( formula with or without the _xlfn. and _xlws.
prefixes into a Python cell. An index names a script of the Python part,
and each %Px% in the script becomes the text of the formula's argument x
(2.3.10.4). The string-literal form, PY("chunk"&"chunk",0|1), still
reads, from the formula's own text so line breaks in the code survive. A
formula naming a script the part doesn't hold keeps only its cached
value, with a warning: no calculation can evaluate it. The reader keeps
the cached spilled values as the cell's result, so a workbook from Excel
shows its arrays without running anything. Python errors are cached as
#VALUE!, as #SPILL! and #CALC! already are. Native JSON keeps Cell.py
whole: code, return type, the last result and the exact error.

Calc lays out a Python cell's kept result: Python cells are nodes of the
dependency graph that read nothing, and a pass takes a dirty Python
cell's result instead of evaluating it, so its array spills, is blocked
(#SPILL!) and unblocked, and feeds its dependents exactly like a
formula's. An array that is malformed or has more than 1,000,000 cells
(only a crafted file holds one) shows #SPILL!. A dirty Python cell is
pending in the pass, as a dirty formula is, so a formula, a range read
or a cell-by-cell range walk that reaches it before its turn lays it out
first rather than reading its old value.

Opening a file can't make the app loop over a claimed size: the reader
keeps a Python cell's spill range only when it has at most 1,000,000
cells (a range claiming the whole sheet is dropped with its values, as a
formula's spilled values are), and io::adopt_python rebuilds results for
at most 4,000,000 cells over the whole workbook, with a warning for the
cells it skips. The Python part is read up to 64 MB and 1,000,000
scripts, a script is kept up to one character past what a cell may
hold, and the cells take at most 64 MB of code from the part in all,
since several formulas can name one script. io::enforce_python_limits
drops the code of a cell whose code doesn't fit in 8,192 characters as
PY(...) (the cell keeps its cached value, with a warning) and a kept
result that isn't a well-formed array within the cap, each with its
spill range and spilled values, so the sheet and a save no longer show
the old spill. Formats that keep PY() as an ordinary formula (xls, xlsb,
ods) are adopted the same way. Opening a file runs nothing.

The command-sequence fuzzer sets Python cells (code with xl()
references, quotes, line breaks, and code at and over the size limit),
and its xlsx round-trip check allows the Python errors becoming
#VALUE!, which is what the file format stores.

Tests: the written formula naming its script, the Python part with its
content type and relationship, return types, a spilling table saved as a
dynamic array, Python errors as #VALUE!, the round trip of values, code,
long code with quotes and line breaks and the error, and no Python part
without Python cells; a workbook laid out as the format specifies (an
index, a parameter named as %P2%) read as Python cells with their
spilled values and saved back with its code in a Python part; the
string-literal form; a formula naming a missing script, with and without
a Python part; 20,000 cells naming one long script against the code
budget; the part's reader and writer (escaping, odd %P tokens, a long
script, a repeated parameter); a blocker in the spill area giving #SPILL!
and clearing it spilling again; the native JSON round trip; a spill
range claiming the whole sheet; the workbook-wide budget; hostile JSON
(code over the limit, a result claiming far more cells than it holds); a
malformed result that never spills; PY() formulas at 8,079 and 8,080
characters of code. Calc: formulas, MAX, SUM, COUNTA and SUMPRODUCT read
a dirty Python cell whose array is blocked before its turn, in a full
and an incremental recalculation, and see the array once it is
unblocked; enforce_python_limits removes the spill of dropped code and
of a dropped result, and keeps a sound one.
2026-10-26 10:18:38 -04:00
prxnvt c6c54c57e6 Engine: python.set makes a Python cell; this build stores code and
doesn't run it

python.set {cell?, code, output?: object|value} makes the cell (the
active cell by default) a Python cell, keeping its style and taking a
formula and its spill away. `output` is the return type Microsoft
documents for PY(): an Excel value (the default) or a Python object;
without it a Python cell keeps the one it had. The code must fit in
Excel's 8,192-character formula once written as PY(...), quotes doubled
and literals joined, so the command checks the encoded formula rather
than a character count. A table's header row refuses Python (it holds
the column's name, as Excel keeps formulas out of headers), and a locked
cell on a protected sheet refuses it through the usual protection check.

Nothing runs Python here. Unchanged code keeps its cached result and
value; new or changed code shows #BLOCKED!. One undo step takes the cell
back.

Sheet::clear_spill removes the spilled values inside a range with work
bounded by the values stored, not by the range's size.

In the interface, F2, a double click or the formula bar on a Python cell
edits its code, and a changed text is committed through python.set,
which keeps the return type; through cell.set it would have replaced the
Python cell with an ordinary cell holding the code as text. Typing over
a Python cell replaces it, as typing over a formula does, and committing
the code unchanged writes nothing. Deleting all the code clears the
cell through cell.set, as an empty entry clears any other cell; code
of spaces is still code.

Tests: the code and return type in cell.get, #BLOCKED! for new code,
cached results kept for unchanged code, the active-cell default, undo,
refused params (missing or non-text code, a bad cell, a bad return
type), the encoded limit at 8,079 and 8,080 characters and with doubled
quotes, a table header, a protected sheet, and a formula's spill taken
away. A UI test edits a Python cell's code with F2 and Enter, commits it
unchanged, and types over it; another clears a Python cell by deleting
all its code, keeps code of spaces, and undoes the clearing.
2026-10-26 10:18:38 -04:00
prxnvt 44c663aea2 Model: Python cells, and the #PYTHON!, #BUSY!, #TIMEOUT! and #BLOCKED! errors
Cell gains `py: Option<Arc<PyCode>>`, read with a serde default and not
written for ordinary cells, so older native files load and ordinary
cells serialize as before. PyCode holds the code, the return type (an
Excel value or a Python object), the last result (value, type name,
preview, whether it converted to cells) and a stale flag for manual
calculation. The cell's `value` stays the cached result, so formulas,
display and saving read a Python cell like any other. is_blank,
CellStore::has and input_text (which returns the code) take the code
into account; Cell::has_content and clear_content cover value, formula
and code together.

The model reads and writes the PY function's syntax as Microsoft
documents it, PY("chunk"&"chunk",0|1), with string literals of at most
255 characters and the return type 0 for an Excel value, 1 for a Python
object. The reader takes the `_xlfn._xlws.` prefix (and `_xlfn.` or
`_xlws.` alone, in any letter case); to_file_formula writes it. (An xlsx
file keeps the code in its Python part instead; the xlsx commit adds
it.) The size limit is Excel's 8,192 characters for that formula, not a
count of code characters: doubled quotes and the "&" between literals
count, so code with many quotes fits in fewer characters (8,079
characters of plain code fit exactly). The model also finds the
xl("...") literals in code, skipping comments and other strings; a plain
literal may escape a quote or a backslash. A rewritten reference that
holds the literal's quote (a sheet name that needs quotes, 'Q4
2026'!B2) takes the other quote as the literal's delimiter, or an
escaped quote when it holds both, so it never ends the string early.

Edits: cell.set over a Python cell, Clear Contents, Clear All and the
cells a merge empties remove the code; Clear Formats keeps it. Copy and
paste (All, Formulas), Fill, sort, cut-move, sheet copy and undo carry
it; a cut to another sheet first qualifies its xl() references with the
source sheet, as a moved formula's references are, so they keep
reading the same cells. Paste Values and Paste Link do not carry it,
and paste operations do not combine numbers with a Python cell. Fill
copies Python cells instead of extending them as a series. cell.set
with =PY(...) is refused with a message that points to python.set, and
Text to Columns keeps a piece that reads as =PY(...) as text instead of
clearing it. Goal Seek refuses a Python cell as its changing cell, and
Solver as a variable cell, as both refuse formulas: the number written
there would replace the code. The sheet-protection check compares the
code, so a locked Python cell cannot be changed. cell.get reports
{python: code, output, typeName, preview, stale}.

The xl() literals follow reference-moving edits: rewrite_all_formulas
(row and column insert and delete, cell insert and delete, cut-move),
sheet rename and sheet delete adjust each literal like a formula
reference, and replace it only when it changed.

Errors: CellError gains Python, Busy, Timeout and Blocked, and ALL has
14 entries, so the lexer reads them as literals. ERROR.TYPE gives 11 for
#BLOCKED!, the number Excel returns. Microsoft documents no ERROR.TYPE
number for #PYTHON!, #BUSY! or #TIMEOUT!; they take 19, 20 and 21, after
the numbers Excel is known to use. The xlsx writer caches the four as
#VALUE!, as it already does #SPILL! and #CALC!, because they are not
error codes of the file format.

Tests cover the =PY refusal, removal by entry and clearing, undo, copy,
paste values, fill and sort, xl() rewrites through row insert, sheet
rename (also to names that need quotes, with one quote and with both),
cut-move (also to another sheet) and column delete, Goal Seek and
Solver refusing Python cells, Text to Columns keeping a =PY piece,
native JSON round trips, the new errors, the prefixes, escaped quotes in
literals, and the encoded size limit at 8,192 characters exactly and
with doubled quotes.
2026-10-26 10:18:38 -04:00
Dylan M. TaylorandCodebuff d8c5f5885f refactor: reduce cognitive complexity in options, python highlighter, i18n scanner, loop search (#2611)
* refactor: split complex functions for Sonar S3776

Extract helpers from backstage options (general, calculation and iteration
sections), code_editor python (string scanning), xtask i18n literals
(escape and string readers) and calc recalc loops_among (SCC search).
No behaviour change.

* Restore the loop count that loops_among's refactor dropped

The S3776 split moved the loop-detection body into loops_among without
the 'let n = circ.len()' it reads, so calc failed to compile and the
Sonar build broke. Restore the binding from main.

* Extract the SCC pop step from components() to reduce cognitive complexity (S3776)

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

---------

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-26 09:48:03 -04:00
Florent Siegenthaler 8cfe970ff8 Declare the UI languages to AppKit so the open and save panels are translated 2026-10-26 09:24:52 +01:00
prxnvt e40291fc3d Macro interface: follow per-window dialogs and egui 0.37 accessibility
Main now keeps a dialog with its own workbook window, so the step editor is
parked with its workbook rather than drawn over another one, and closing that
workbook drops the editor without drawing it again: forget_closed_editor
clears it each frame. The Macros, Record Macro and editor text boxes are
labelled by their captions, and the tests pick the question's Cancel and the
chart menu's Delete by role, as the formula bar and ribbon share those names.
2026-10-26 00:38:32 -04:00
prxnvt c801e50e40 Merge #2152 (f8e1f8df, with main at f6b0cec6) into #478
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/io.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/lib.rs
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-10-26 00:32:55 -04:00
prxnvt f8e1f8dfc0 Merge #2145 (4d26e395, with main at f6b0cec6) into #2152
# Conflicts:
#	crates/engine/src/io.rs
#	crates/engine/src/lib.rs
#	docs/target-app-parity.md
2026-10-26 00:29:13 -04:00
prxnvt 4d26e395fc Merge #2550 (d4096eee, with main at f6b0cec6) into #2145
# Conflicts:
#	crates/engine/src/lib.rs
2026-10-26 00:26:35 -04:00
prxnvt d4096eee5a Merge #2144 (34eee860, with main at f6b0cec6) into #2550 2026-10-26 00:24:57 -04:00
prxnvt 34eee86053 Merge #2151 (6781aa49, with main at f6b0cec6) into #2144
# Conflicts:
#	crates/engine/src/io.rs
#	crates/engine/src/lib.rs
2026-10-26 00:24:11 -04:00
prxnvt 6781aa4927 Merge main (f6b0cec6) into #2151 2026-10-26 00:21:54 -04:00
prxnvt 483276f719 Merge #465 (3772f2c6, with main at 42bbf6ba) into #2151 2026-10-26 00:21:42 -04:00
prxnvt 3772f2c61a Macros: write the macro names into xlsx again after the main merge
The merge with main kept main's xlsx line in save_bytes_as, which dropped
macros::with_names, so an xlsx save carried no macros.
2026-10-26 00:21:00 -04:00
aiongg f6b0cec67e Merge pull request #2619 from aiongg/g4
Window per workbook (4/4): two windows on one workbook without copying it; Arrange All
2026-10-26 00:00:24 -04:00
aiongg 862dfc1920 Merge pull request #2618 from aiongg/g3
Window per workbook (3/4): a native window per workbook window
2026-10-25 23:59:45 -04:00
aiongg 1b63c3f335 Merge pull request #2617 from aiongg/g2
Window per workbook (2/4): windows in the engine; New Window shares the workbook
2026-10-25 23:58:35 -04:00
aiongg ecb0e546d8 Merge pull request #2616 from aiongg/g1
Window per workbook (1/4): a cell draft stays in its workbook; Switch Windows
2026-10-25 23:57:56 -04:00
Pranav 1cd45cc909 Merge branch 'main' into pr/macros-ui 2026-10-25 23:54:09 -04:00
Pranav e85297e26c Merge branch 'main' into pr/macros-engine 2026-10-25 23:49:20 -04:00
Pranav b552b7a929 Merge branch 'main' into pr/macros-record 2026-10-25 23:49:08 -04:00
Pranav 910b50c1dd Merge branch 'main' into pr/macros-run 2026-10-25 23:48:13 -04:00
aiongg 1f2da4048d Merge g3 into g4 2026-10-26 03:36:29 +00:00
aiongg dbec5c5800 Merge g2 into g3 2026-10-26 03:36:18 +00:00
aiongg 57da3d4641 Merge g1 into g2 2026-10-26 03:35:47 +00:00
aiongg 380be36ac3 G1: the W, W key tip opens Switch Windows through its control 2026-10-26 03:33:34 +00:00
aiongg 5c250a7fea Merge g3 into g4 2026-10-26 03:29:08 +00:00
aiongg c7911c836a G3: craft-libs at storytold main b926b5c0 2026-10-26 03:28:46 +00:00
aiongg 0f5fc81a6c Merge g2 into g3 2026-10-26 03:27:27 +00:00
Dmussi 848b0e658a Click a status bar statistic to copy its value 2026-10-25 23:24:26 -04:00
aiongg 0316765204 Merge g1 into g2 2026-10-26 03:16:42 +00:00
aiongg 320bcfcf54 G1: regenerate the key tip tests 2026-10-26 03:16:42 +00:00