Skip to content

Numfmt: one built-in format table, fewer allocations rendering numbers and General (#817) - #820

Draft
vangel76 wants to merge 5 commits into
storytold:mainfrom
vangel76:perf/numfmt-render-cleanup
Draft

vangel76 wants to merge 5 commits into
storytold:mainfrom
vangel76:perf/numfmt-render-cleanup

Conversation

@vangel76

Copy link
Copy Markdown
Contributor

Fixes #817

Findings S1, S3, O3 and O4 of the crates/numfmt review. No behaviour change; one PR because the S3 dead arm and the literal_text calls sit in the lines O3 rewrites.

What changed, finding by finding

S1, one table of built-in formats. The two tables were identical (checked by diffing the entries). cargo xtask layers forbids gridcraft-xlsx -> gridcraft-numfmt (FormatDep; only a dev-dependency is allowed), so the table stays in crates/xlsx/src/tables.rs and numfmt's copy (builtin.rs, builtin_format, builtin_id; used by nothing but numfmt's own tests) is removed. Nothing else in crates/, apps/, docs/, xtask/ referenced them (git grep outside xlsx). xlsx's lookup rules are untouched and now pinned by tables::tests::builtin_format_ids (36 ids, 23-36 and 50 absent, builtin_id exact-case, ids 5-8 not written as built-ins). In numfmt's tests the three uses of the table became literal codes, and builtins (which tested the removed case-insensitive lookup) became builtin_codes_render, which keeps the three rendering assertions.

S3, dead arm and literal_text. The Tok::At | General | Date | Comma => {} arm in render_number printed nothing, which the fallback (the token's piece, empty) does too, because no code ever gives these tokens a piece. So it is removed whether or not the tokens can occur; the comment says that At can occur (0@) and Comma/General/Date cannot after analyze. New Tok::push_literal(&mut String); literal_text calls it. render_text, render_general, render_date and render_number append through it and allocate nothing per token.

O3, allocations per number.

  • render_number: pieces, before and lits (a String per token) are now one Parts value: one text buffer and one Vec of byte ranges (+ the "this literal becomes k spaces" marker for fractions). fill_int writes digits left to right (digit, then the separator after it) instead of building the reversed string and reversing it; fill_frac and the exponent text write into the buffer too.
  • render_date rescanned the tokens three times per call. The three results (sub_digits, calendar part present, AM/PM present) are fields of Section, computed once in analyze. Date parts are written straight into the output (no String per part).

O4, General. significant_digits formats into a 64-byte stack buffer (the 800-digit exact-tie check keeps a heap string, it is the rare path) and fills a caller array instead of returning a Vec. format_general_fit computed the 15 digits once already; the width loop now re-rounds them with fixed_into (the old fixed_from_digits body, with the digit Vec replaced by a 16-byte array plus a zero count) into three strings that live for the whole call, instead of a new Vec and two Strings per width. fixed_from_digits is a wrapper, so render.rs callers are unchanged. The starting decimals are clamped to the existing 400 cap: zeros past it were trimmed anyway, and a width of usize::MAX / 2 no longer asks for a gigantic zero string (output identical, see the width list below).

No allocation count or speed claim is made; this is argued by structure only (fewer per-token/per-iteration allocations), nothing was measured.

Tests

  • crates/numfmt/tests/render_equality.rs + tests/golden/*.txt: 152 format codes (numbers, thousands, percent, scientific, fractions, dates/times, [h]:mm, text sections, conditions, colours, fill *x, invalid codes) x 75 values (0, -0, negatives, tiny, huge, 0.5, 1.005, 2.675, 999.9995, NaN, +-inf, serials 0/59/60/61/2958465/2958466, ...) through format_value and text_function in the 1900 system (and 1904 for date codes), plus text/bool/empty values; and 555 values (480 generated across magnitudes) through format_general_fit at widths 1..=30, 31, 40, 100, 399, 400, 401, 500, 1000. The golden files are FNV-1a digests of each row's full output, recorded from the code before this change (upstream/main 9f896c1), not strings; one row per code / per value, so a failure names the row and prints its new output. NUMFMT_DUMP_DIR writes the full text, NUMFMT_BLESS=1 re-records. I checked the test fails when the thousands separator logic is changed on purpose. One extra code (a 420-placeholder format) was dropped from the grid by hand (and its golden row deleted) because Numfmt: no panic on a fraction denominator with 65,536 zeros; format codes are capped at 255 characters (#807) #809 caps code length; decimal::tests::rounding has asserts for the beyond-400-decimals and 1e300 cases instead.
  • A temporary old-vs-new comparison of decimal_digits/fixed/format_general_fit over ~9,000 values x 15 decimals x 44 widths was run locally with the old decimal.rs kept as a test module; it passed and was deleted (not committed).
  • Existing numfmt tests pass; the only edits are the ones listed under S1.

Commands run

  • cargo test -p gridcraft-numfmt -p gridcraft-xlsx -p gridcraft-calc -p gridcraft-chart
  • cargo test -p gridcraft-engine format (9 tests; the numfmt filter matched none)
  • cargo clippy -p gridcraft-numfmt -p gridcraft-xlsx --all-targets -- -D warnings
  • cargo fmt -p gridcraft-numfmt -p gridcraft-xlsx -- --check
  • cargo xtask layers, cargo xtask assets
  • cargo check -p gridcraft-numfmt --target wasm32-unknown-unknown

Not run: the full cargo xtask ci, the full engine/ui-egui test suites, cargo xtask wasm.

Interaction with other open PRs (trial merges with git merge-tree)

Not done / unverified

  • Still allocating per number: digit_indices (three small Vecs), the String pair returned by fixed, and the denominator pieces in fractions (small strings), plus the result string.
  • The At claim (0@ prints nothing for the token) is proven by structure and by the unchanged outputs of the rows with @ in the grid; no number-section-with-@ code is in the grid.
  • Timestamp header of ROADMAP.md left alone (already dated today); a progress row and a revision row were added.

🤖 Generated with Claude Code

…ions (storytold#817)

S1: the built-in format table lives only in gridcraft-xlsx (file formats do not
depend on numfmt); numfmt's unused copy and its exports are removed, xlsx gets
tests pinning its lookup rules.
S3: drop the render_number arm that printed what the fallback prints; literal
text is appended to the output instead of built as a String per call.
O3: render_number keeps pieces, text-before and literal replacements in one
buffer (Parts), fill_int writes digits left to right, Section stores the date
flags (sub-second digits, calendar, AM/PM) computed at parse time, render_date
writes into one buffer.
O4: significant_digits formats into a stack buffer; format_general_fit re-rounds
the 15 digits per width into reused strings.

An equality test compares format_value, TEXT() and format_general_fit with
digests recorded from the previous implementation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vangel76
vangel76 marked this pull request as draft October 12, 2026 20:45
Port the scientific-exponent (storytold#773) and negative-zero (storytold#740) changes onto the single-buffer renderer; re-record 99 golden code rows (negative values that round to zero lose the minus sign, zero in ##0.0E+0 / #.##E+00 shows 0 in integer placeholders, 1904 dates end at 2957003).

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

coderabbitai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@vangel76

Copy link
Copy Markdown
Contributor Author

Merged main (conflicts in ROADMAP.md and the fraction renderer in numfmt, where main now keeps fraction numerators as f64). Re-recorded 4 golden rows for huge fractions such as 1e20, which now print exact digits. Ran: fmt check, workspace check, numfmt tests, full xtask ci (all 8 steps).

vangel76 and others added 2 commits October 26, 2026 00:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Numfmt: one built-in format table, no dead render arms, fewer allocations when rendering numbers and General

1 participant