env! fixes the path to contributors/ when the build script is compiled.
After a checkout is moved or copied, the compiled script keeps looking in
the old location: the About window loses its contributor list, and
because the rerun-if-changed path no longer exists, Cargo reruns the
script and rebuilds the UI crate on every build. Reading the variable at
run time follows the checkout.
Excel lets a worksheet function be passed by name where a function is
expected. In GridCraft GROUPBY(A2:A9,B2:B9,SUM) and PIVOTBY gave #NAME?
(their documented form), and BYROW, BYCOL, MAP, REDUCE, SCAN and
MAKEARRAY gave #VALUE!. A name that is a worksheet function, and not a
LET/LAMBDA variable or a defined name, now works as a function there.
Reading a file, the _xleta. prefix Excel writes for these names is
removed.
Builds on the Solver of #327. data.solver takes reports: [answer, limits]
and writes them on new sheets before the model's sheet once a solution
is found. The Answer report lists the objective and variable cells with
their original and final values and each constraint's value, formula,
status (Binding or Not Binding) and slack; cells are named by the
nearest labels to their left and above. The Limits report minimises and
maximises each variable with the others held at the solution, under the
model's constraints, and gives the objective at both ends (Simplex LP
and GRG Nonlinear, without integer constraints).
data.solverReport writes the reports for a given solution and model,
so data.solver's result (which now carries the original values and the
model) can be passed to it as is. The Solver Results dialog offers the
two reports; they are written after Keep Solver Solution or Restore
Original Values.
Builds on the Solver of #327. method: evolutionary runs a genetic search
for models that aren't smooth: tournament selection, blend crossover and
Gaussian mutation inside the variable bounds, feasibility-first ranking
and integer rounding, then a compass search around the best member. It
stops when the population converges, when the best member stops
improving for a number of generations, or at the evaluation budget. A
seeded generator makes runs repeatable. Population size, mutation rate,
random seed, convergence and Require Bounds are options; the method and
its options are saved in the hidden solver_* names. The Solver
Parameters dialog offers the method.
The recent workbooks on the File view's Home and Open pages gain a
Modified column: "Just now", "5 min ago", "3 h ago", "Yesterday",
"4 days ago", "3 weeks ago", "5 months ago", "2 years ago", or
"Not available" for a file that can't be reached.
The file system is never asked while drawing. A thread asks for each
file's metadata and the list shows each answer as it comes back, so a
network drive that is asleep or a share that is away doesn't hold up
the view. One thread asks at a time; a view opened anew asks anew. Only
metadata is read: no workbook is opened until the user picks one.
The list tells ages rather than dates because the UI has no time zone
to tell a date by. From two days on a count is never one, so each
language needs one form of "days", "weeks", "months" and "years"; the
ten strings are in the nine catalogs, with the count placed by a {n}
in each, as "Page {page} of {pages}" does.
A long name or folder now stops short of the column instead of running
under it. The web build, which has no files to ask about, and a narrow
window show the list as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed with cargo clippy --workspace --all-targets -- -D warnings,
not a test. The actual cause: I'd been validating locally against
rustc/clippy 1.95.0 (the toolchain this machine had installed before
now), while CI installs plain "stable" (1.99.0 as of this run) — two
lints new since 1.95.0 fired there and nowhere in my local checks:
- clippy::int_plus_one on `src.end.row + 1 <= cap` in grid.rs,
simplified to `src.end.row < cap` (same meaning, no overflow risk
change: row is a u32 and cap is already saturating-clamped below
MAX_ROWS - 1, so src.end.row + 1 can't wrap).
- clippy::disallowed_methods on the new fill_handle_perf example's
three Instant::now() calls — the project's own lint from the time-
panic fix (#125 history): reading the clock traps on wasm. This
example is native-only (a `cargo run --release` benchmark, never
built for wasm), so it gets the same #![allow(...)] perf.rs already
carries, with the same justification.
Updated the local toolchain to stable (rustup update stable) and
re-ran cargo clippy --workspace --all-targets -- -D warnings clean
before this push, to actually match what CI checks instead of
guessing again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The completeness test's failure (missing "Different First Page") was
only the visible tip: fr.rs was missing that key and "Different Odd &
Even Pages" outright, but also had roughly 50 *present* keys mapped to
the wrong value — reading as a real, different English string's French
translation instead of its own. The completeness test only checks that
a translation exists and is non-empty, not that it's correct, so none
of this was caught before.
Examples found by inspection (key => previous wrong value => what that
value actually means):
"Click to add data" => "Fermer" (= Close)
"Header" => "Informations" (= Info)
"Ruler" => "Enregistrer sous" (= Save As)
"Info" => "Insérer les cellules copiées…" (= Insert Copied Cells…)
"Animations" => "Auteur" (= Author)
"Auto" => "Retour" (= Back)
"Previous Comment" => "Imprimer tout le classeur" (= Print Entire Workbook)
The wrong values aren't a local shuffle (adjacent keys swapped) — "Info"
drew a value that belongs near the start of the key list while "Info"
itself is near the end, so whatever produced this scattered entries
from unrelated parts of the file onto each other. Given the extent and
that it wasn't contained to one block, patching individual lines risked
missing more of them, so this regenerates every one of the 381 entries
from the English source text (same source list i18n-serbian used),
rather than auditing the existing file entry by entry.
Verified: the existing completeness test (translations_are_unique_...)
now passes end to end — it was failing on a clean main checkout before
this change, purely from the 2 missing keys. Full cargo test -p
gridcraft-ui-egui --lib: 75/75 (was 74/75 on main).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Language::Sr ("sr"), following the existing per-language module
pattern (crates/ui-egui/src/i18n/<lang>.rs with a translate(text) ->
Option<&'static str>). All 381 strings in the JAPANESE master list are
covered, verified by the existing completeness test.
Latin script (sr-Latn), ekavian dialect — the common default for
digital/software Serbian. Cyrillic (sr-Cyrl) could follow as a
separate variant if wanted; this keeps the PR to one script, matching
how every other language here is a single variant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cargo fmt reformatted the new example's while-loop condition onto its
own line; clippy's nonminimal_bool caught a negated is_some_and that
reads more plainly as is_none_or.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The value list kept egui's default auto_shrink, so its scroll bar sat mid-menu after the longest
value and the wheel only scrolled it with the pointer over a value. Keep the height shrinking but
not the width: the scroll bar is at the menu's right edge and the wheel works anywhere over the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #210, which the reviewer correctly rejected: the stated
cause (a sparse column with a stray distant value) doesn't hold, since
the scan already stops at the first empty cell. This is a different,
narrower fix that doesn't change behaviour for any input — it never
truncates a long fill, unlike #210's cap — only how fast a genuinely
long, densely-populated run is scanned.
grid.rs's fill-handle double-click called Sheet::value_ref once per
row to find where the neighbouring column's data ends. Each call does
a band lookup + row lookup + column lookup (three BTreeMap probes) via
CellStore::get. For a real dense run (e.g. a filled column near the
1,048,576-row sheet limit) that's a lot of point lookups on the UI
thread in one frame: ~59ms for a 1,000,000-row run in a release build
(measured with the new fill_handle_perf example), likely worse under
wasm where GridCraft actually runs in the browser.
- crates/model/src/store.rs: CellStore::contiguous_value_run walks
each band's row BTreeMap with `range` (one in-order pass per band)
instead of one `get` per row, so a dense run costs roughly one
tree-traversal step per row instead of one binary search per row.
Deliberately narrower than `has`/`get`: a stored cell always decides
the row on its own (even with an empty value), matching what
`value_ref`'s callers actually test for, not `has`'s "formula counts
regardless of its cached value" (used for Ctrl+arrow navigation).
- crates/model/src/sheet.rs: Sheet::last_contiguous_nonempty layers
`spill` back in wherever contiguous_value_run finds no stored cell,
exactly where plain `value_ref` would fall through to it.
- crates/ui-egui/src/grid.rs: the fill-handle double-click now calls
this once instead of looping over value_ref.
Result: 999,999-row dense run, 59.2ms -> 24.7ms (2.40x), same answer
(asserted in the example). Four new tests in sheet.rs, including a
300-trial property test against a naive value_ref-loop reference
(spills, formula-with-empty-value, gaps, blanks) — all pass, plus the
full existing suite (`cargo test -p gridcraft-model -p gridcraft-ui-egui
-p gridcraft-engine`; the only failures are #210-era gridcraft-engine
CSV tests already failing on main, unrelated to this change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
write_file writes `<path>.sctmp` and renames it over the workbook. When the
write or the rename failed (a full disk, or the target locked by another
app such as Excel or a sync client), the temporary file was left beside the
user's workbook. Remove it on either failure and report the same error.
Adds io::tests::failed_save_leaves_no_temporary_file, which fails before
this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resubmit of the selection half of #224 against current main, as
requested — the Delete-key half was a duplicate of #219 and is
already merged.
Insert > Recommended Charts left the prior cell/range selection in
place instead of selecting the new chart, unlike every other object
insertion path (see insert.chart in dialogs.rs, which already selects
the chart it creates). With the selection left on the source data,
pressing Del right after inserting deleted that data and left an
empty chart behind (#214).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
XLSX: strip _xlpm. from LET/LAMBDA parameters on read, keep line breaks in table columns, allow leading backslash in table names
# Conflicts:
# docs/file-format-parity.md
A `;`-separated CSV (the usual CSV where `,` is the decimal separator) was
rewritten with `,` on save: read_csv sniffed the delimiter but the result was
dropped, and save_bytes passed a hard-coded b','. file.save, Save As .csv,
file.exportCsv, file.saveBytes and gridcraft-cli convert all went through it.
The workbook now remembers the separator it was read with
(Workbook::csv_delimiter, set by read_csv) and a CSV save writes it back.
Workbooks not read from CSV, and .tsv/.txt sources saved as .csv, keep `,`.
Regression tests: `;`, `,` and tab .csv round trips byte for byte, new and
.xlsx-origin workbooks save with `,`, and open-by-path + edit + file.save on a
`;` file keeps `;` (also through exportCsv and saveBytes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>