Commit Graph
2518 Commits
Author SHA1 Message Date
Dylan M. Taylor a3a98cf745 functions: REGEXREPLACE keeps only the requested occurrence's captures (#3288)
* REGEXREPLACE keeps only the requested occurrence's captures

* address review: do not truncate REGEXREPLACE occurrence on 32-bit targets
2026-11-10 18:05:25 -05:00
Dylan M. Taylor fe39331ac4 engine: Consolidate finds keys through hash maps and refuses an oversized table (#3285)
* engine: Consolidate finds keys through hash maps and refuses an oversized table

data.consolidate searched the keys collected so far for every row and
column key, so 100,000 positional rows cost about five billion
comparisons. It also built every row × column combination without a
size check. Keys are now indexed, and a table over 5 million cells is
refused before it is built.

* address review: count label cells in the consolidate output limit
2026-11-10 18:05:08 -05:00
Dylan M. Taylor a84259d58c ui-egui: embed craft-fonts files only from inside the checkout (#3281)
* ui-egui: embed craft-fonts files only from inside the checkout

The build hook canonicalised each manifest row's file and embedded it
with include_bytes! wherever it led, so an absolute path, a .. path or
a symlink in a craft-fonts checkout could put any readable file of the
build machine into the binary. Manifests and font files must now
resolve inside the canonical checkout directory.

* address review: register the manifest path before validation and keep the Chinese manifest fallback optional
2026-11-10 18:04:49 -05:00
Dylan M. Taylor 85d32c20be engine: show overlong format codes as General without hashing them per cell (#3273)
* engine: show overlong format codes as General without hashing them per cell

The display cache hashed the full format code on every lookup, so one
huge code shared by many cells cost its length for every cell drawn.
A code with more than 4 × 255 bytes can't be within Excel's 255
characters and is now formatted as General straight away.

* address review: keep the original code in the public number_format for overlong formats
2026-11-10 18:04:31 -05:00
Dylan M. Taylor 1deb108a7c xtask: probe keytip commands without file access, and refuse text that could escape generated code (#3269)
* xtask: probe keytip commands without file access, and refuse text that could escape generated code

cargo xtask keytips ran every command keytips.json names, with its
params, under the user's file permissions (file.exportPdf could replace
any file, also during --check). It also wrote target, excel and note
strings into generated Rust without escaping. Probes now run inside the
engine's ScriptSandbox, and those fields may not hold quotes,
backslashes, braces or control characters.

* address review: make ScriptSandbox !Send so it drops on the thread that entered it
2026-11-10 18:04:13 -05:00
Dylan M. Taylor 2c9e2d1eec Go To: index defined names when resolving and listing them (#3206)
Go To resolved every visible name, and each alias inside a name was
looked up with Workbook::name, a linear scan of all names. A workbook
of many short aliases made the Go To list quadratic (or worse along
alias chains) in the number of names.

A Resolver now indexes the names by scope and folded spelling once
per command and remembers, per name, sheet and depth, whether a name
resolves and to how many areas, so listing names resolves each alias
once. Lookups keep the old precedence and matching rules.
2026-11-10 18:03:53 -05:00
Dylan M. Taylor 9c8b4a37ea PDF export: index merged areas when drawing a block (#3203)
draw_block checked every cell against every merge meeting the block,
and the fill, border and text passes searched the merge list again
for each merge start, so an imported sheet with many merge ranges in
a print area cost cells x merges (and merges x merges) comparisons.

The merges meeting a block are now gathered once and swept down the
rows: whether a cell is covered is a BTreeMap lookup among the merges
active on its row, and merge starts are found through a HashMap. A
merge overlapping one kept before it (only possible in a damaged file)
is left out, as Excel does when it repairs the file. Hyperlink
rectangles use the same index instead of scanning all sheet merges.
2026-11-10 18:03:35 -05:00
FredAppsandFredApps a36e4987c1 i18n(uk): translate Chart as Діаграма (#3703)
Use the shared craft-i18n Ukrainian term, distinct from Diagram (Схема).
Also translate the remaining Chart-labelled strings that were still English.

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 17:07:10 +01:00
FredAppsandFredApps 457bc7e374 Naming: use Diagram and Decorative Text in the feature catalog (#3622)
* Naming: use Diagram and Decorative Text in the feature catalog

* Naming: align the Chinese decorative text term

* test(naming): guard neutral display labels

* Naming: Diagram no longer repeats the chart word in de, uk and ro

German Schema and Ukrainian Схема match craft-i18n#211; Romanian Schemă
avoids GridCraft's own chart word Diagramă. A catalog test checks every
shipped language translates both labels and that Diagram differs from
Chart/Charts. The display-name guard now handles escaped char literals
and any #[cfg(test)] item, asserts it scanned catalog.rs and a minimum
of files and literals, and tests word boundaries. The catalog test
asserts each ID appears once. Docs carry timestamps and revision history.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 16:13:13 +01:00
Harrison Booth 50a9e9ede4 XLS: formulas open as legacy formulas with Excel's implicit @ (#410) (#1367)
.xls formulas are read as legacy formulas with Excel's implicit @ (apply_legacy via read_xls_with), one-cell Ctrl+Shift+Enter arrays keep their range, @ of a 2-D range meets the formula's row and column, and defined-name analyses are shared across the formulas of a pass and replayed exactly.

Closes #410
2026-11-10 15:05:02 +01:00
74ab5cc969 M12: regional format data (Region, 20 regions) (#3048)
* M12: regional format data (Region, 15 regions, Workbook::region)

Adds the data model for Excel-style regional formats, with no behaviour
change: nothing reads it yet.

- core: region.rs with `Region` (separators, grouping, date order, short and
  long date and time codes, month and day names, AM/PM words, currency) and
  the 15 regions en-US, en-GB, en-IN, es-ES, de-DE, fr-FR, it-IT, pt-BR,
  pt-PT, ru-RU, cs-CZ, ja-JP, ko-KR, zh-CN, zh-TW. Values follow Unicode CLDR;
  the deliberate departures (four-digit short-date year, Windows grouping
  spaces) are listed in the module doc. `all`, `find` (strict tag),
  `lookup` (system or browser tag, best effort) and `default_region` (en-US)
  select one; `EN_US` reproduces today's behaviour exactly. Unit tests check
  the tables and the lookups.
- model: `Workbook::region`, a `#[serde(skip)]` runtime field that is never
  saved and defaults to en-US. It is here so that the following changes can
  be reviewed independently of each other.

The region is independent of the interface language and of the formula
language. Builds on this, in order: typed input (core `parse_input_in`),
display (numfmt and engine display), the engine session and `app.region`
command, then the Options UI and the app defaults.

* M12: region data: review fixes (tag extensions, zh territory, month periods)

- lookup stops at an extension or private-use singleton:
  en-GB-u-ca-gregory is en-GB (ca was read as Canada) and
  zh-Hant-u-nu-latn is zh-TW.
- For Chinese an explicit territory wins over the script, so
  zh-Hant-CN is China's conventions, consistent with zh-Hans-TW.
- In regions whose month abbreviations end in a period (ru-RU, fr-FR),
  month_from_name accepts any abbreviation typed with one (мар., июн.);
  elsewhere a period stays outside the name, so en-US is unchanged.

* Region: document where dates and month names follow Windows, not CLDR

* M12: Swiss regions (en-CH, de-CH, fr-CH, it-CH) and nl-NL

The Swiss regions group digits with an apostrophe (CLDR 48) and accept the
typographic one glibc uses; lookup maps CH and LI tags to them. Dutch
(Netherlands) follows CLDR 48.

* Region: table-driven lookup test, under main's cognitive-complexity limit

* Region: French and Italian Liechtenstein tags resolve to the Swiss regions

* Region: credit the Unicode CLDR data under the Unicode License v3 (NOTICE, ATTRIBUTION.md)

* Region: month_from_name takes one word; the currency format has the currency's own decimals

---------

Co-authored-by: quantmind-br <dsrodrigues20@gmail.com>
Co-authored-by: quantforge-br <218010578+quantforge-br@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 13:54:53 +01:00
RockerHXandFredApps ec12eba83e UI.1265: align CJK fallback text with the Latin baseline (#3081)
* UI.1265: align CJK fallback text with the Latin baseline

Use epaint's existing baseline alignment for system CJK fallback faces
and embedded faces tagged with CJK scripts. Mixed CJK, Latin and digits
then share a baseline in cells, editors and window titles.

Add regression coverage for zoom, font styles, wrapping, fixed-height
clipping, vertical alignment, editor height and title truncation.

Refs #1265, #2621.

* UI.1265: assert title baselines and document visual validation

* UI.1265: add baseline comparison and review validation

* UI.1265: adapt tooltip regression after upstream merge

The upstream shortcut update uses the runtime OS to choose tooltip text.
Use the harness OS for the existing split-button test expectation too.

Validation: cargo xtask ci; all 10 steps pass; 3,002 tests passed, 1 ignored, no exclusions.

* UI.1265: propagate CJK fallback to installed fonts

* UI.1265: align fallback baselines across cells

* UI.1265: align wrapped and unwrapped cell text

* UI.1265: preserve blank lines in wrapped cell alignment

* UI.1265: preserve centered fallback placement

* UI.1265: find CJK cell text by its visible bounds

Use the overlap of text ink, clip rect and cell rect to locate painted text
instead of testing a fixed offset from the galley origin. Baseline-aligned
fallbacks can move that origin above the cell on Ubuntu.

Keep the existing baseline and wrapping assertions unchanged.

Validation: cargo xtask ci on macOS; all 3 cjk_baseline tests on Ubuntu 24.04 x86_64.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 12:44:19 +01:00
ca39b5ab9f XLSX: legacy formulas (no dynamic-array cm) open with Excel's implicit @ and calculate like Excel (#417)
* XLSX: legacy formulas (no dynamic-array cm) open with Excel's implicit @ and calculate like Excel; implied @ saved back without cm

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* XLSX: long operator chains analysed in full and unfinished legacy analysis leaves the formula unchanged (never saved as SINGLE); @ of a union keeps its parentheses; IF's condition intersected inside SUMPRODUCT

* XLSX: a legacy union already in parentheses saves back without a second pair (never as SINGLE); a name's definition analysed as an array throughout, inserting no @

* XLSX: drop the unreachable keep-unions retry; an @ of a bare union saves as a dynamic formula

* XLSX: an @ keeps parentheses the reader would not put back (redundant ones, a union's)

* XLSX: a redundant @ keeps a legacy formula legacy; IFERROR and IFNA intersected inside SUMPRODUCT; file parentheses and unfinished analyses saved as read

An edit could leave an implied @ on a single cell: deleting rows 2:3 under the legacy SQRT(A1:A3), read as SQRT(@A1:A3), gives SQRT(@A1:A1), and a name redefined to one cell does the same to SQRT(@Rng). The writer kept an @ only when re-analysing the formula without it put the same @ back, which it no longer does for one cell, so the formula was saved as SQRT(_xlfn.SINGLE(A1:A1)) with cm, readable only by dynamic-array Excel. The writer now analyses the formula with its own @ in place: an @ is accepted when the analysis implies it at that position or when its operand is a single value there (such an @ changes nothing), and any @ inserted elsewhere or any other @ still makes the formula dynamic. SQRT(@A1:A1) saves as SQRT(A1:A1), SQRT(@A1) as SQRT(A1) and @SUM(A1:A3) as SUM(A1:A3), all without cm; SUMPRODUCT(@SQRT(A1:A3)) stays dynamic because SQRT(A1:A3) is an array there.

Legacy Excel intersects IFERROR's and IFNA's arguments inside SUMPRODUCT unless the formula is entered with Ctrl+Shift+Enter, like IF's condition: =SUMPRODUCT(IFERROR(VALUE(C19:C21),0)) returns one row's value or #VALUE!, and the accepted fix is CSE or N(+range) (https://stackoverflow.com/questions/59228707). They now get the same single-value parameter class as IF's condition (Param::Single), so the legacy SUMPRODUCT(IFERROR(A1:A3/A1:A3,0)) reads as SUMPRODUCT(IFERROR(@A1:A3/@A1:A3,0)), and a typed SUMPRODUCT(IFERROR(VALUE(A1:A3),0)), which dynamic-array Excel evaluates as an array, is saved with cm. IF's result arguments are left as arrays: in https://stackoverflow.com/questions/29682328, a non-CSE =SUMPRODUCT(--(B4=IF(NOT($C$4:$C$11),$B$4:$B$11))) in row 4 returns 2 (both "apple" rows of B4:B11 counted, so the result branch was not intersected), 0 in a row where C is TRUE and #VALUE! outside rows 4:11 (the condition was intersected).

The reader's @ in front of a whole-formula array result took no extra parentheses when the file already had them, so ({1,2}+1) and {1,2}+1 both read as @({1,2}+1), and the writer dropped the file's parentheses. An operand already in parentheses now gets a second pair, so ({1,2}+1) saves as written.

A legacy formula the analysis could not finish (node budget, nesting) was read unchanged, evaluated as a dynamic-array formula and saved with cm, so Excel spilled it. Names were analysed again at every use, so a 399-term name used six times exhausted the 4096-node budget; their kinds are now remembered per formula, and the budget is 16384 nodes. The nesting limit is 128, so Excel's 64 levels of nested functions are analysed. A formula that still exceeds the limits is saved as it was read, without cm.

* XLSX: an implied @ is dropped and any other @ of a single value kept as _xlfn.SINGLE in a legacy formula (as Excel writes it); a legacy formula the analysis cannot finish or whose semantics it does not know opens with a whole-formula @ and never spills; analysis depth follows the parser's limit

* XLSX: a legacy union given to a function's single-value parameter keeps its implied @, so it calculates to #VALUE! instead of spilling and saves as read; checked on a calculated workbook

* XLSX: only a one-cell range an @ applies to counts as one an edit shrank; a reading past the parser's nesting limit leaves the formula as read, and the writer saves the text a formula it cannot analyse was read from

* XLSX: a formula nested past the parser's limit for its reading is left as read (documented: it may spill); test covers it

* XLSX: a name's definition counts its one-cell ranges as collapsed only when it is itself a reference, so CHOOSE's index or IF's condition in it keeps the name legacy

* XLSX: an @ on a name or one-cell range counts as implied when ranges an edit may have shrunk, anywhere the name returns them (CHOOSE's choices, parentheses), make it so; deleting rows keeps such formulas legacy

* XLSX: the collapsed-range retry of an @ runs only when the first pass reached a one-cell range written as a range (tracked through cached names)

* docs: describe unfinished formula export fallback

* docs: the unfinished-analysis fallback differs for formulas with an explicit @

* Preserve main formula analyzer fixes

* Order file-format revision history

---------

Co-authored-by: neilcawse <8085959+neilcawse@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Local contributor <contributor@localhost>
2026-11-10 12:43:07 +01:00
e9addf52fe Arabic bidi layout and cell reading direction in the grid and editors (#947)
* M12.AR4: preserve Arabic logical text in bidi display and editing

* M12.AR4: drop the stale "no bidi" note from the RTL sheet status

* M12.AR4: open the Arabic editors and place the caret through key and pointer events in tests

* M12.AR4: skip the conditional-format lookup in the formula bar unless a rule sets reading order

* M12.AR4: test that the caret after typed Arabic is drawn at the text's left end (#1070)

* M12.AR4: pass the theme body font in the reading-order style test

* M12.AR4: adapt the Arabic UI tests to egui 0.37 (row direction, editor ids, Ctrl+End to the logical end)

* M12.AR4: read the active sheet through Workbook::active_sheet() in the formula bar

main made `Workbook::active_sheet` an `ActiveSheet` (shared between windows on one
workbook); the formula bar's reading-order lookup takes the index from the accessor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR4: keep explicit Left on wrapped right-to-left cells and trailing spaces in the editor

Wrapped cell text lays out every row from the left when the painted alignment is Left, instead of aligning right-to-left rows to their start. The cell and formula bar editor keep trailing whitespace in their layout so a right-to-left entry ending in a space keeps the caret inside the text.

* M12.AR4: keep Right and General on right-to-left sheets aligned right in wrapped cells

* M12.AR4: keep right-aligned Arabic with a marked first letter inside its cell

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:55:43 +01:00
cda767b49b Arabic font fallback in cells, the formula bar and the web (#931)
* M12.AR2: Arabic font fallback in cells and the interface (#229)

* M12.AR2: reserve the preferred Arabic file only once it has loaded

load_fallback treated fontconfig's Arabic file as already loaded even
when load_preferred_arabic could not load it, so a system Arabic face
from the same file was skipped and the chains could end up with no
Arabic system face. The file is now reserved only when the preferred
face is in the font set; the candidate loop moves to load_faces so a
test can drive it with a fixture file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR2: say where fontconfig's Arabic choice applies

fontconfig's Arabic face leads the Arabic fallbacks, so it draws Arabic
where the UI or cell font has none. A UI font with Arabic of its own,
such as DejaVu Sans in the Flatpak, still draws interface Arabic (#229),
which is left for a follow-up. The docs and the test's doc comment no
longer say the choice comes first in every chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:54:55 +01:00
c35234e5f5 Number formats: honour the B1/B2 calendar prefixes (Hijri dates for B2) (#1918)
* M12.AR6: honour the B1/B2 calendar prefixes in number formats (Hijri for B2)

* M12.AR6: match Excel's B1/B2 behaviour measured in review

Changes from a reviewer's Excel run (TEXT and Range.NumberFormat):
- A [$-...] locale tag before B2 keeps the date Gregorian (prefix still
  hidden); B2[$-401] stays Hijri.
- Any other unquoted B and digit (B3dd, dd/mm/yyyyB2, dd B2 mm), and B2
  before a number format (B20.00), make the code invalid: TEXT gives
  #VALUE!, a cell shows General, as for a code over 255 characters.
- Under B2, ddd and dddd show the Arabic weekday name (full name for both).
- Serial 60 has no Hijri date: TEXT gives #VALUE!, a cell shows ####.
- Serial 0 shows as serial 1 (29/08/1317).

Also fixes a break from merging main: days_from_civil now returns 0 for
day 0 and 29 February 1900, which sent serials 0 and 60 to 1970
(23/10/1389) under B2. The Hijri day count no longer passes those dates.

Docs: localization-parity states the behaviour, including the one
measured case not matched (mmmm next to dddd prints the weekday in Excel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR6: Hijri leap year in cycle year 15, not 16, as Excel shows

A reviewer compared Excel's B2yyyy-mm-dd on 43,512 serials with this
formula: it disagreed by one day on 1,372, all in cycle years 15 and 16,
and moving the leap year from 16 to 15 left no disagreement. The leap
years are now 2, 5, 7, 10, 13, 15, 18, 21, 24, 26 and 29 (still 10,631
days a cycle).

The count of leap years before a year becomes (4 + 11y) div 30; it
differs from the old (3 + 11y) div 30 only when y = 16 (mod 30), which is
exactly the move from 16 to 15. The inverse's year estimate can now be one
year off at a boundary and is corrected by one step.

Tests: Excel values for 2 March 2005 (1426-01-21) and 29 January 2006
(1426-12-29) plus the 1425/1426 year boundaries they imply, the reviewer's
other anchors, and exhaustive checks: year, month and 30-year cycle
lengths for Hijri years 1 to 10,000; day -> Hijri -> day round trip and
day-by-day succession for every day from 1 Muharram 1 AH to one cycle past
31 December 9999; every 1900-system serial 1 to 2,958,465 (except 60).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR6: match B1/B2 details checked in Excel 16.0 build 20527

Compared GridCraft with Excel 16.0 (build 20527, en-US) through COM: TEXT
with B2dd/mm/yyyy on every serial from 1 to 2,958,465 (no difference), and
about 1,750 format/value pairs through both a cell's NumberFormat and TEXT.
Fixed what differed and is in scope:

- Two-word Hijri month names use a no-break space (U+00A0), as Excel does.
- B1 before a number format is rejected like B2 (B10.00, B1#,##0).
- Prefix runs: a repeated prefix is accepted (B2B2, B2b2, [Red]B2B2, B1B1);
  a mixed one (B1B2, B2B1), more digits (B12, B21, B02), a prefix after a
  tag or space (B2[Red]B2, B2 B2) and an escaped \B2 are rejected.
- Serial 60 under B2 is #VALUE! for any date or clock part (hh:mm, ss,
  AM/PM), not only date parts; [h] and literal-only sections still show.

Regression tests use the Excel values (all 12 month and 7 weekday names,
the accepted and rejected codes, serial 60). Not changed, listed in the
docs: Excel's weekday/month-name mix-ups when ddd(d) and mmm(m) share a B2
section, weekday-only B2 sections (real weekday for serials 0-59, #VALUE!
for the last days of 9999), and Buddhist-year b/bb/bbbb tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:54:05 +01:00
004523c286 AutoFit: estimate Arabic text width from measured glyph widths (#1933)
* M12.AR12: estimate Arabic text width from measured glyph widths in AutoFit

* M12.AR12: treat U+0897–089F as marks and U+08E2 as visible; size the honorific ligatures (review)

* M12.AR12: size the remaining honorific ligatures like their siblings (review)

U+FD4C-FD4F (Unicode 14) and U+FDC8-FDCF (FDCF Unicode 14, FDC8-FDCE
Unicode 17) are honorific word ligatures, not letters, but fell into
the 0.48 em letter arm. Give them the 1.6 em of the measured
U+FD40-FD4B honorifics and assert each range's endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR12: size Arabic-Indic digits like ASCII digits and the remaining honorific ligatures (review)

Arabic-Indic (U+0660-0669) and Extended Arabic-Indic (U+06F0-06F9)
digits measure 0.53-0.57 em in Noto Sans Arabic, Arial, Tahoma and
Times New Roman, the same as each font's ASCII digits, so they get
the ASCII digit estimate (0.56) instead of the letter estimate.
Arabic punctuation measures 0.17-0.50 em, so it keeps 0.48 rather
than falling back to 1 em.

U+FBC3-FBD2 and U+FD90-FD91 are honorific word ligatures like
U+FD40-FD4F and get the same 1.6 em.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:52:55 +01:00
517a4b9256 Sort: order Arabic text by its letters, not its vowel marks or digit script (#1940)
* M12.AR7: sort Arabic text by its letters, not its vowel marks or digit script

* M12.AR7: order marked Arabic text the way Excel does

Checked compare_text against Excel 16.0 build 20527 (en-US) via COM:
Range.Sort, =SORT() and the < / = operators over ~300 strings. Fixes the
mismatches inside this change's scope:

- The tie-break compared raw code points, so an RLM-prefixed word sorted
  after every vowelled form of it, "‏12" after "١٢", and a tatweel
  form after a shadda form. Ignorable characters are now skipped at the
  tie-break too.
- Marks are compared letter by letter (an unmarked letter first), so a
  mark on an earlier letter outranks the digit or letter after it, as in
  Excel ("ب١" < "بَ1", "بپ" < "بَپ").
- Marks rank in Excel's measured order (sukun before shadda), and the
  Quranic marks Excel skips (U+0610–061A, U+06D8–06ED except 06DD/06DE/
  06DF/06E0/06E5/06E6/06E9) are skipped by letter order.
- Excel also ignores bidi embeddings/overrides/isolates, BOM, soft hyphen,
  CGJ and word joiner; they now are ignored too (ZWSP is not, as in Excel).

Equality is unchanged. Regression tests use the orders Excel produced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR7: compare the mark level of Arabic text without allocating

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-09 15:49:26 +01:00
Valentin KleinandClaude Opus 5.5 7102429a9a perf(grid): skip hidden row runs and keep sheet geometry and CF statistics between frames (#3580) (#3629)
A sheet whose filter hides a million rows walked every hidden row
several times per frame (cells, gridlines, headers), rebuilt the row
index, the outline and the used range every frame, and a conditional
format over a whole column rescanned it every frame for its statistics.

- LineIndex::next_visible / visible step over a run of hidden lines in
  one O(log n) search; the grid's row and column loops use them.
- The grid keeps the active sheet's line index, outlines, used range and
  CfCache between frames, tied to the sheet by a Weak: any change goes
  through Arc::make_mut, which moves or copies a sheet that has a weak
  reference, so the cache can't outlive a change.
- Scroll offsets for the scrollbars and the scroll limit come from the
  line index instead of walking every custom row.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:46:53 +01:00
dotCooCoo 3004a05e46 Updates: take craft-update from craft-libs main at 1b34eba41 2026-11-08 19:45:34 -08:00
dotCooCoo 99bbd237fc Updates: the updater reports installing, ready, installed and failed, and installs at exit
craft-update, craft-window, craft-print and craft-ui move from craft-libs
c5a1cbfb to c7c84230, a branch with storytold/craft-libs#687 and #696
merged into craft-libs main.

- The check result window shows whenever the updater's status is not
  idle, so the first start after an update reports that it installed or
  why it failed. Restart to Update asks the app to quit once nothing is
  unsaved.
- An update's close request quits the app (Quit asks about each unsaved
  workbook), so every workbook window closes.
- on_exit starts the install helper of a ready update before the macOS
  process exit.
2026-11-08 11:53:25 -08:00
FredApps 2c58c76914 Merge pull request #2696 from vangel76/perf/calc-spill-unblock-2684
perf(calc): find formulas a cleared cell may unblock from a set of #SPILL! cells
2026-11-08 13:01:25 +01:00
FredApps c000e1b3ff Merge pull request #2694 from vangel76/perf/calc-spill-index-2683
perf(calc): index spill areas so cell and range reads don't scan every spill
2026-11-08 12:58:44 +01:00
FredApps 21920d3d61 Merge pull request #3297: complete equation input caps with #3328
Equations: refuse macro definitions and cap matrix cells
2026-11-08 12:47:35 +01:00
vangel76andClaude Opus 5.5 0f5a860d74 Merge perf/calc-spill-index-2683 (with upstream/main) into perf/calc-spill-unblock-2684
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:23:30 +01:00
vangel76andClaude Opus 5.5 17131fb0e5 Merge upstream/main into perf/calc-spill-index-2683
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:18:31 +01:00
FredApps 94a8fb084a Merge pull request #1152 from vangel76/perf/chart-measure-allocs
Charts: label fitting and legend measuring avoid hundreds of text layouts per render (#1149)
2026-11-08 12:08:59 +01:00
FredApps fca22f6e93 Merge pull request #1155 from vangel76/fix/chart-work-budgets
Charts: work and memory are bounded by series x points; ranges are clipped before evaluation (#1147)
2026-11-08 12:08:35 +01:00
FredApps 300b074894 Merge pull request #3325 from vangel76/fix/binom-poisson-accuracy
BINOM.DIST, POISSON.DIST: Loader mass and recurrence sums, error 1e-11 -> 1e-14 (#3324)
2026-11-08 12:08:16 +01:00
FredApps 9dd3b51bc6 Merge pull request #3383 from vangel76/fix/3350-ctrl-tab-arrows
Tabs: Ctrl-click on the sheet arrows jumps to the first or last sheet
2026-11-08 12:07:28 +01:00
FredApps 4a93722ea6 Merge pull request #2691 from vangel76/perf/calc-range-memo-2681
perf(calc): SUM/COUNT/AVERAGE over big ranges reuse constant-only results
2026-11-08 12:06:13 +01:00
FredApps 6338cef450 Merge pull request #3578 from vangel76/fix/3577-json-formula-trees
Cache formula trees when a workbook is read from JSON
2026-11-08 12:05:30 +01:00
FredApps 24119e49fe Merge pull request #3581 from vangel76/perf/3575-store-shifts
Move whole rows when inserting or deleting rows and columns
2026-11-08 12:05:00 +01:00
vangel76andClaude Opus 5.5 9ab297ced1 perf(engine): rewrite only the formulas a structural edit changes
rewrite_all_formulas borrows each tree with Formula::with_expr and calls
adjust_if_changed, so a formula the edit leaves alone is neither copied
nor printed, and a changed one is printed once. A legacy array whose
range moves while its formula stays the same still gets the new range.

Sheet rename and delete compare the adjusted tree with the old one
instead of printed text, and replace only formulas that changed. Delete
no longer rebuilds every formula in the workbook, so unrelated formulas
keep their array range and dynamic-array mark.

A stored non-canonical formula text is no longer re-printed by an edit
that does not change the formula; a test pins this.

Part of #3576

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:06:21 +01:00
vangel76andClaude Opus 5.5 2ee261c0e8 perf(model): move whole rows when inserting or deleting rows and columns (#3575)
CellStore::shift_rows, shift_cols, shift_cols_in_rows and take_range removed
and re-set every affected cell one by one. They now take whole row maps (or
the split-off part of one) out of the bands and put them back at their new
position, so a row shift costs a few map operations per row instead of a
band lookup, a remove and a set per cell. take_range does this when the
block reaches the last column.

Behaviour is unchanged, including blank cells left behind by get_mut, cells off the grid (they
are still dropped, and still clear the cell they land on), cells shifted off
the sheet, and copy-on-write: bands without moved cells stay shared with undo
snapshots. A differential test runs 12,000 random operations against the old
per-cell code and compares contents, length, iteration order and band sharing.

shift_rows_in_cols, and take_range on a block with columns to its right, keep
moving cell by cell: the row-map version measured no faster there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 03:51:57 +01:00
vangel76andClaude Opus 5.5 7cb0088361 perf(formula): adjust_if_changed skips formulas a structural edit leaves alone
adjust had no way to report that an edit does not affect a formula, so
callers copied, rebuilt and printed every formula in the workbook.
adjust_if_changed borrows the formula and returns None when nothing
moves. Its check reuses adjust_ref on each reference into the edited
sheet, on a copy without the sheet name, so it cannot disagree with
adjust. adjust_ref and shift_ref take the reference by value and no
longer clone sheet names.

An integration test keeps the previous implementation as an oracle
over every edit kind and a randomized property test.

Fixes #3576

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 03:04:24 +01:00
vangel76andClaude Opus 5.5 4bba98fd50 perf(model): cache formula trees when reading a workbook from JSON
A Formula read from JSON had no cached tree (`expr` is not saved), so
every graph rebuild and every recalculation parsed each formula again.
Reading one now parses its text once and keeps the tree, as
Formula::new does. The saved text is kept as written.

Fixes #3577

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:57:51 +01:00
640fbf22f6 fix(edit): paste, Ctrl+Enter, Fill and Copy skip rows a filter hides (#1101) (#1175)
* Paste, Ctrl+Enter and Fill skip rows hidden by a filter (#1101)

Repeating one cell or row down a filtered selection (paste of a single
cell or row, Ctrl+Enter, Fill Down/Up/Right/Left) now leaves the rows an
applied filter hides alone, as Excel does. A taller pasted block still
goes straight down, and rows hidden by hand are still written, also as
in Excel. Each case was measured in Excel for Microsoft 365.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2bJ3LxrZ6YnmppgrfwHwr

* Copying a filtered range takes only the rows showing (#1101)

A copied range with rows hidden by an applied filter is split into its
runs of showing rows and packed like a multi-area copy (formulas as
values), so pasting it inside GridCraft no longer brings the hidden rows
back. One run is copied as a plain range, keeping formulas. Cut still
moves every row. All as measured in Excel for Microsoft 365.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2bJ3LxrZ6YnmppgrfwHwr

* Find the rows a filter hides without rescanning every table per row

filtered_rows asks the sheet for its filter row spans once instead of
checking every table for each hidden row.

* Paste over filtered rows leaves their validation rules alone

When one row is tiled over filtered rows, paste_validations no longer
clears or copies rules on the rows the filter hides.

* Filtered copy packs links and validation with the rows showing

slide_together now moves hyperlinks and validation rules with their rows, and a
filtered whole-row copy records the packed rows as its selection so Insert
Copied Cells inserts only the rows copied.

* Group validation paste options and format the new tests

* Pack a filtered copy in one pass over notes, merges, links and spills

slide_together finds each entry's area by search instead of scanning every map
once per area, so a filter leaving thousands of separate runs no longer costs
runs times map size on every copy.

---------

Co-authored-by: Richard <richard1912@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar <hi@skylar.cc>
2026-11-07 19:30:06 -06:00
vangel76andClaude Sonnet 5.5 865a7f6a06 Merge upstream/main into perf/calc-spill-unblock-2684
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:09:51 +01:00
vangel76andClaude Sonnet 5.5 1458f45214 Merge upstream/main into perf/calc-spill-index-2683
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:09:31 +01:00
vangel76andClaude Sonnet 5.5 05a5845234 Merge upstream/main into perf/calc-range-memo-2681
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:38:18 +01:00
vangel76andClaude Sonnet 5.5 8e5bcf1575 Merge upstream/main into fix/binom-poisson-accuracy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:37:33 +01:00
vangel76andClaude Sonnet 5.5 83670dfe3a Merge upstream/main into fix/math-cap-lookahead
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:37:18 +01:00
vangel76andClaude Sonnet 5.5 93dab0c420 Merge upstream/main into fix/3350-ctrl-tab-arrows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:36:49 +01:00
vangel76andClaude Sonnet 5.5 9354fdb0c4 Chart: fit labels with a line break by a linear scan
The width of a prefix plus an ellipsis is not monotone when the text has a
line break, so the binary search could drop a label that fits. Also pass the
series index to plot_kind in the data table (upstream/main does not compile
without it).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 17:37:20 +01:00
vangel76 5f84fadaf5 Merge remote-tracking branch 'upstream/main' into c1152 2026-11-07 17:32:43 +01:00
vangel76 9ba79c3e38 fmt 2026-11-07 17:31:41 +01:00
vangel76andClaude Sonnet 5.5 6a7cf9cf66 Chart: bound the category axis by the point cap, not the raw cell count
Read only the cells the category levels use and cut the category dimension
at the per-series cap, so a multi-level axis keeps all its levels.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 17:31:27 +01:00
vangel76andClaude Sonnet 5.5 c35878948a Merge upstream/main into fix/chart-work-budgets
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 17:31:27 +01:00
vangel76andClaude Sonnet 5.5 42370639e0 Merge upstream/main into perf/calc-range-memo-2681
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 17:29:04 +01:00