mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-01 06:36:47 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f2b7aa2f6 | ||
|
|
dc8078e168 |
@@ -12,7 +12,6 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../pdfcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
|
GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../pdfcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
|
||||||
|
|
||||||
|
Standards and learnings shared across the crafting apps live in `../../craftrules` (checked out next to the craft apps, or `storytold/craftrules`). Read its `AGENTS.md` at the start of a session, follow its standards, and contribute reusable learnings back there. Never code: repos don't share code.
|
||||||
|
|
||||||
## Start every session here
|
## Start every session here
|
||||||
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. Behaviour reference: `plan/excel/*.md` (`observed-ui.md` holds measured observations of the running app).
|
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. Behaviour reference: `plan/excel/*.md` (`observed-ui.md` holds measured observations of the running app).
|
||||||
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's.
|
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's.
|
||||||
@@ -18,7 +20,7 @@ GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Micros
|
|||||||
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
|
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
|
||||||
|
|
||||||
## Never crash
|
## Never crash
|
||||||
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.**
|
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
|
||||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`.
|
- **Errors are `Result<T, E>`** through the crate's error type and `?`.
|
||||||
- **Input-derived numbers are hostile** (files, formulas, commands, MCP/control params): validate indexes and ranges before access, use checked access when validity is not established, use checked/saturating arithmetic, and cap input-sized allocations (huge ranges, `REPT`, `SEQUENCE`, whole-column references).
|
- **Input-derived numbers are hostile** (files, formulas, commands, MCP/control params): validate indexes and ranges before access, use checked access when validity is not established, use checked/saturating arithmetic, and cap input-sized allocations (huge ranges, `REPT`, `SEQUENCE`, whole-column references).
|
||||||
@@ -29,8 +31,8 @@ People trust GridCraft with their numbers; a crash loses their work. **This outr
|
|||||||
|
|
||||||
## Non-negotiables
|
## Non-negotiables
|
||||||
- **Clean-room.** Microsoft Excel is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic workbooks, take screenshots (by window id) stored only under `plan/excel/screenshots/`. Never read, disassemble or copy anything inside the Office bundle, never commit files produced by Excel, never copy GPL/AGPL/LGPL code (LibreOffice, Gnumeric…). Function behaviour comes from public documentation and observation.
|
- **Clean-room.** Microsoft Excel is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic workbooks, take screenshots (by window id) stored only under `plan/excel/screenshots/`. Never read, disassemble or copy anything inside the Office bundle, never commit files produced by Excel, never copy GPL/AGPL/LGPL code (LibreOffice, Gnumeric…). Function behaviour comes from public documentation and observation.
|
||||||
- **Fonts live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts)**, never in this repo. It is an optional build input (`CRAFT_FONTS_DIR`); without it the app uses system fonts and egui's defaults.
|
- **Fonts live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts)**, never in this repo. It is an optional build input (`CRAFT_FONTS_DIR`); without it the app uses system fonts and egui's defaults. Standard: craftrules `standards/fonts.md`.
|
||||||
- **Test corpora** (real Excel-authored files) live in a separate corpus repo, fetched pinned and sha256-verified. Never commit large binary fixtures; build test workbooks in code.
|
- **Test corpora** (real Excel-authored files) live in a separate corpus repo, fetched pinned and sha256-verified (craftrules `standards/test-corpora.md`). Never commit large binary fixtures; build test workbooks in code.
|
||||||
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, ribbon path, shortcut, params doc, `enabled`, `run`) + tests. Ids follow Excel's ribbon (`home.bold`, `insert.chart`, `data.sortAscending`) plus primitives (`cell.set`, `selection.set`, `range.setValues`). UI-only commands live in `crates/ui-egui`. The control channel, CLI and MCP reach all of them.
|
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, ribbon path, shortcut, params doc, `enabled`, `run`) + tests. Ids follow Excel's ribbon (`home.bold`, `insert.chart`, `data.sortAscending`) plus primitives (`cell.set`, `selection.set`, `range.setValues`). UI-only commands live in `crates/ui-egui`. The control channel, CLI and MCP reach all of them.
|
||||||
- **Programmatic calls never open dialogs**: a command with empty params runs with defaults or returns an error; only menu-style invocation opens a dialog (`UiRequest::Dialog`).
|
- **Programmatic calls never open dialogs**: a command with empty params runs with defaults or returns an error; only menu-style invocation opens a dialog (`UiRequest::Dialog`).
|
||||||
- **Layering** is enforced by `cargo xtask layers`: `core`/`numfmt` (L0) → `formula`/`functions` (L1) → `model`/`calc`/`xlsx` (L2) → `chart` (L3) → `engine` (L5) → `ui-egui`/`mcp` (L6) → `apps/*` (L7). Nothing below L6 depends on egui/eframe/winit/rfd. **The UI crate is swappable.**
|
- **Layering** is enforced by `cargo xtask layers`: `core`/`numfmt` (L0) → `formula`/`functions` (L1) → `model`/`calc`/`xlsx` (L2) → `chart` (L3) → `engine` (L5) → `ui-egui`/`mcp` (L6) → `apps/*` (L7). Nothing below L6 depends on egui/eframe/winit/rfd. **The UI crate is swappable.**
|
||||||
@@ -49,7 +51,7 @@ People trust GridCraft with their numbers; a crash loses their work. **This outr
|
|||||||
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done.
|
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
`ROADMAP.md` (committed) is the one-page summary: stage, headline numbers, dimensions, languages, progress log. It points at the detail in `docs/`: `target-app-parity.md` (authoritative assessment), `gaps.md` (ranked work list: pick from it), `roadmap.md` (current focus, beta gates, milestones), `architecture.md`, and the area checklists `function-parity.md`, `chart-parity.md`, `file-format-parity.md`, `ui-parity.md`, `hardware-parity.md`, `localization-parity.md`. Update the affected docs (and their timestamp lines and revision history) whenever work lands. `cargo xtask parity` regenerates `docs/parity-checklist.md` from `crates/engine/src/catalog.rs`.
|
`ROADMAP.md` (committed) is the one-page summary: stage, headline numbers, dimensions, languages, progress log. It follows craftrules `standards/progress-docs.md` and points at the detail in `docs/`: `target-app-parity.md` (authoritative assessment), `gaps.md` (ranked work list: pick from it), `roadmap.md` (current focus, beta gates, milestones), `architecture.md`, and the area checklists `function-parity.md`, `chart-parity.md`, `file-format-parity.md`, `ui-parity.md`, `hardware-parity.md`, `localization-parity.md`. Update the affected docs (and their timestamp lines and revision history) whenever work lands. `cargo xtask parity` regenerates `docs/parity-checklist.md` from `crates/engine/src/catalog.rs`.
|
||||||
|
|
||||||
## Contributor credits (About window)
|
## Contributor credits (About window)
|
||||||
|
|
||||||
@@ -60,11 +62,14 @@ People trust GridCraft with their numbers; a crash loses their work. **This outr
|
|||||||
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
|
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
|
||||||
whether they want to be credited by more than their username: a real name, a display name, and/or
|
whether they want to be credited by more than their username: a real name, a display name, and/or
|
||||||
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
|
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
|
||||||
(copy the template at the top of the file)
|
(copy the template at the top of the file, or run
|
||||||
|
`python3 ../../craftrules/scripts/contributors.py --add-me . --real-name "…" --sync-github-name`)
|
||||||
and include it in their PR, committed as them. If no, change nothing: they are credited as
|
and include it in their PR, committed as them. If no, change nothing: they are credited as
|
||||||
`@username` anyway.
|
`@username` anyway.
|
||||||
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
|
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
|
||||||
GitHub profiles). Never hand-edit `contributors.json`.
|
GitHub profiles). Never hand-edit `contributors.json`.
|
||||||
|
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
|
||||||
|
re-verifies who wrote each `people.toml` entry; `--check` only verifies).
|
||||||
|
|
||||||
## CI workflows
|
## CI workflows
|
||||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||||
|
|||||||
+9
-9
@@ -12,15 +12,15 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
|
|||||||
| `epaint_default_fonts::HACK_REGULAR` (dependency font input) | Source Foundry Authors (Hack; DejaVu and Bitstream Vera foundations) | [egui 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [Hack](https://github.com/source-foundry/Hack) | MIT + Bitstream Vera License (full upstream texts in `NOTICE`) | Hack-Regular.ttf is egui's bundled monospace face and GridCraft's last-resort symbol fallback. Dependency asset unmodified here, no font file copied into this repository. |
|
| `epaint_default_fonts::HACK_REGULAR` (dependency font input) | Source Foundry Authors (Hack; DejaVu and Bitstream Vera foundations) | [egui 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [Hack](https://github.com/source-foundry/Hack) | MIT + Bitstream Vera License (full upstream texts in `NOTICE`) | Hack-Regular.ttf is egui's bundled monospace face and GridCraft's last-resort symbol fallback. Dependency asset unmodified here, no font file copied into this repository. |
|
||||||
| `epaint_default_fonts::NOTO_EMOJI_REGULAR` (dependency font input) | The Noto Project Authors | [egui 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [Noto Emoji](https://fonts.google.com/noto/specimen/Noto+Emoji) | SIL Open Font License 1.1 (full upstream text in `NOTICE`) | NotoEmoji-Regular.ttf is enabled by egui's `monochrome_emoji_fonts` feature and used as the monochrome emoji fallback. Dependency asset unmodified here, no font file copied into this repository. |
|
| `epaint_default_fonts::NOTO_EMOJI_REGULAR` (dependency font input) | The Noto Project Authors | [egui 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [Noto Emoji](https://fonts.google.com/noto/specimen/Noto+Emoji) | SIL Open Font License 1.1 (full upstream text in `NOTICE`) | NotoEmoji-Regular.ttf is enabled by egui's `monochrome_emoji_fonts` feature and used as the monochrome emoji fallback. Dependency asset unmodified here, no font file copied into this repository. |
|
||||||
| `epaint_default_fonts::UBUNTU_LIGHT` (dependency font input) | Canonical Ltd. and the Ubuntu Font Family authors | [egui 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [Ubuntu font](https://design.ubuntu.com/font) | Ubuntu Font Licence 1.0 (full upstream text in `NOTICE`) | Ubuntu-Light.ttf is egui's default proportional face. Dependency asset unmodified here, no font file copied into this repository. |
|
| `epaint_default_fonts::UBUNTU_LIGHT` (dependency font input) | Canonical Ltd. and the Ubuntu Font Family authors | [egui 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [Ubuntu font](https://design.ubuntu.com/font) | Ubuntu Font Licence 1.0 (full upstream text in `NOTICE`) | Ubuntu-Light.ttf is egui's default proportional face. Dependency asset unmodified here, no font file copied into this repository. |
|
||||||
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms (`docs/brand/LICENSE-brand.txt`) | Trademark, not open source |
|
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms (`docs/brand/LICENSE-brand.txt`) | Trademark, not open source |
|
||||||
| `docs/brand/artcraft-logo.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-logo.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/artcraft-mark.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-mark.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft brand terms | Trademark |
|
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
|
||||||
| `docs/brand/LICENSE-brand.txt` | (licence text) | project | — | |
|
| `docs/brand/LICENSE-brand.txt` | (licence text) | craftrules | — | |
|
||||||
| `assets/app-icon/gridcraft.svg` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | PLACEHOLDER app icon (ledger page); replaced by the owner's creature art later |
|
| `assets/app-icon/gridcraft.svg` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | PLACEHOLDER app icon (ledger page); replaced by the owner's creature art later |
|
||||||
| `packaging/macos/dmg/background.svg` | @XusBadia | original (hand-written SVG, derived from the GridCraft app icon `assets/app-icon/gridcraft.svg`, referenced via `<image>`, not copied; text is outlined glyph paths; `packaging/macos/dmg/generate.py` renders it to `background.tiff`) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | GridCraft macOS DMG window background for the Finder window; added 2026-10-08 |
|
| `packaging/macos/dmg/background.svg` | @XusBadia | original (hand-written SVG, derived from the GridCraft app icon `assets/app-icon/gridcraft.svg`, referenced via `<image>`, not copied; text is outlined glyph paths; `packaging/macos/dmg/generate.py` renders it to `background.tiff`) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | GridCraft macOS DMG window background for the Finder window; added 2026-10-08 |
|
||||||
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") |
|
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") |
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../pdfcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
|
GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../pdfcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
|
||||||
|
|
||||||
|
Standards and learnings shared across the crafting apps live in `../../craftrules` (checked out next to the craft apps, or `storytold/craftrules`). Read its `AGENTS.md` at the start of a session, follow its standards, and contribute reusable learnings back there. Never code: repos don't share code.
|
||||||
|
|
||||||
## Start every session here
|
## Start every session here
|
||||||
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. Behaviour reference: `plan/excel/*.md` (`observed-ui.md` holds measured observations of the running app).
|
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. Behaviour reference: `plan/excel/*.md` (`observed-ui.md` holds measured observations of the running app).
|
||||||
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's.
|
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's.
|
||||||
@@ -18,7 +20,7 @@ GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Micros
|
|||||||
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
|
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
|
||||||
|
|
||||||
## Never crash
|
## Never crash
|
||||||
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.**
|
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
|
||||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`.
|
- **Errors are `Result<T, E>`** through the crate's error type and `?`.
|
||||||
- **Input-derived numbers are hostile** (files, formulas, commands, MCP/control params): `get()` instead of indexing, checked/saturating arithmetic, cap input-sized allocations (huge ranges, `REPT`, `SEQUENCE`, whole-column references).
|
- **Input-derived numbers are hostile** (files, formulas, commands, MCP/control params): `get()` instead of indexing, checked/saturating arithmetic, cap input-sized allocations (huge ranges, `REPT`, `SEQUENCE`, whole-column references).
|
||||||
@@ -29,8 +31,8 @@ People trust GridCraft with their numbers; a crash loses their work. **This outr
|
|||||||
|
|
||||||
## Non-negotiables
|
## Non-negotiables
|
||||||
- **Clean-room.** Microsoft Excel is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic workbooks, take screenshots (by window id) stored only under `plan/excel/screenshots/`. Never read, disassemble or copy anything inside the Office bundle, never commit files produced by Excel, never copy GPL/AGPL/LGPL code (LibreOffice, Gnumeric…). Function behaviour comes from public documentation and observation.
|
- **Clean-room.** Microsoft Excel is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic workbooks, take screenshots (by window id) stored only under `plan/excel/screenshots/`. Never read, disassemble or copy anything inside the Office bundle, never commit files produced by Excel, never copy GPL/AGPL/LGPL code (LibreOffice, Gnumeric…). Function behaviour comes from public documentation and observation.
|
||||||
- **Fonts live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts)**, never in this repo. It is an optional build input (`CRAFT_FONTS_DIR`); without it the app uses system fonts and egui's defaults.
|
- **Fonts live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts)**, never in this repo. It is an optional build input (`CRAFT_FONTS_DIR`); without it the app uses system fonts and egui's defaults. Standard: craftrules `standards/fonts.md`.
|
||||||
- **Test corpora** (real Excel-authored files) live in a separate corpus repo, fetched pinned and sha256-verified. Never commit large binary fixtures; build test workbooks in code.
|
- **Test corpora** (real Excel-authored files) live in a separate corpus repo, fetched pinned and sha256-verified (craftrules `standards/test-corpora.md`). Never commit large binary fixtures; build test workbooks in code.
|
||||||
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, ribbon path, shortcut, params doc, `enabled`, `run`) + tests. Ids follow Excel's ribbon (`home.bold`, `insert.chart`, `data.sortAscending`) plus primitives (`cell.set`, `selection.set`, `range.setValues`). UI-only commands live in `crates/ui-egui`. The control channel, CLI and MCP reach all of them.
|
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, ribbon path, shortcut, params doc, `enabled`, `run`) + tests. Ids follow Excel's ribbon (`home.bold`, `insert.chart`, `data.sortAscending`) plus primitives (`cell.set`, `selection.set`, `range.setValues`). UI-only commands live in `crates/ui-egui`. The control channel, CLI and MCP reach all of them.
|
||||||
- **Programmatic calls never open dialogs**: a command with empty params runs with defaults or returns an error; only menu-style invocation opens a dialog (`UiRequest::Dialog`).
|
- **Programmatic calls never open dialogs**: a command with empty params runs with defaults or returns an error; only menu-style invocation opens a dialog (`UiRequest::Dialog`).
|
||||||
- **Layering** is enforced by `cargo xtask layers`: `core`/`numfmt` (L0) → `formula`/`functions` (L1) → `model`/`calc`/`xlsx` (L2) → `chart` (L3) → `engine` (L5) → `ui-egui`/`mcp` (L6) → `apps/*` (L7). Nothing below L6 depends on egui/eframe/winit/rfd. **The UI crate is swappable.**
|
- **Layering** is enforced by `cargo xtask layers`: `core`/`numfmt` (L0) → `formula`/`functions` (L1) → `model`/`calc`/`xlsx` (L2) → `chart` (L3) → `engine` (L5) → `ui-egui`/`mcp` (L6) → `apps/*` (L7). Nothing below L6 depends on egui/eframe/winit/rfd. **The UI crate is swappable.**
|
||||||
@@ -48,7 +50,7 @@ People trust GridCraft with their numbers; a crash loses their work. **This outr
|
|||||||
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done.
|
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
`ROADMAP.md` (committed) is the one-page summary: stage, headline numbers, dimensions, languages, progress log. It points at the detail in `docs/`: `target-app-parity.md` (authoritative assessment), `gaps.md` (ranked work list: pick from it), `roadmap.md` (current focus, beta gates, milestones), `architecture.md`, and the area checklists `function-parity.md`, `chart-parity.md`, `file-format-parity.md`, `ui-parity.md`, `hardware-parity.md`, `localization-parity.md`. Update the affected docs (and their timestamp lines and revision history) whenever work lands. `cargo xtask parity` regenerates `docs/parity-checklist.md` from `crates/engine/src/catalog.rs`.
|
`ROADMAP.md` (committed) is the one-page summary: stage, headline numbers, dimensions, languages, progress log. It follows craftrules `standards/progress-docs.md` and points at the detail in `docs/`: `target-app-parity.md` (authoritative assessment), `gaps.md` (ranked work list: pick from it), `roadmap.md` (current focus, beta gates, milestones), `architecture.md`, and the area checklists `function-parity.md`, `chart-parity.md`, `file-format-parity.md`, `ui-parity.md`, `hardware-parity.md`, `localization-parity.md`. Update the affected docs (and their timestamp lines and revision history) whenever work lands. `cargo xtask parity` regenerates `docs/parity-checklist.md` from `crates/engine/src/catalog.rs`.
|
||||||
|
|
||||||
## CI workflows
|
## CI workflows
|
||||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||||
|
|||||||
Generated
+30
-31
@@ -220,7 +220,7 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
"wl-clipboard-rs",
|
"wl-clipboard-rs",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
@@ -889,12 +889,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "craft-fonts-build"
|
name = "craft-fonts-build"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "craft-i18n"
|
name = "craft-i18n"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fixed_decimal",
|
"fixed_decimal",
|
||||||
"icu_decimal",
|
"icu_decimal",
|
||||||
@@ -917,10 +917,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "craft-print"
|
name = "craft-print"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"craft-winprint-gdi",
|
"craft-winprint-gdi",
|
||||||
"log",
|
|
||||||
"miniz_oxide 0.9.1",
|
"miniz_oxide 0.9.1",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@@ -931,7 +930,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "craft-ui"
|
name = "craft-ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"craft-fonts-build",
|
"craft-fonts-build",
|
||||||
"craft-i18n",
|
"craft-i18n",
|
||||||
@@ -951,7 +950,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "craft-update"
|
name = "craft-update"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
source = "git+https://github.com/storytold/craft-libs?rev=04aa1703b32d299e54f78fcde4182f2b393a9116#04aa1703b32d299e54f78fcde4182f2b393a9116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dunce",
|
"dunce",
|
||||||
@@ -977,7 +976,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "craft-window"
|
name = "craft-window"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"objc2 0.6.5",
|
"objc2 0.6.5",
|
||||||
@@ -1457,7 +1456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1937,7 +1936,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft"
|
name = "gridcraft"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1964,7 +1963,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-calc"
|
name = "gridcraft-calc"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"gridcraft-formula",
|
"gridcraft-formula",
|
||||||
@@ -1976,7 +1975,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-chart"
|
name = "gridcraft-chart"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-calc",
|
"gridcraft-calc",
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
@@ -1987,7 +1986,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-cli"
|
name = "gridcraft-cli"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-engine",
|
"gridcraft-engine",
|
||||||
"gridcraft-mcp",
|
"gridcraft-mcp",
|
||||||
@@ -1999,7 +1998,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-core"
|
name = "gridcraft-core"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -2010,7 +2009,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-engine"
|
name = "gridcraft-engine"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-calc",
|
"gridcraft-calc",
|
||||||
"gridcraft-chart",
|
"gridcraft-chart",
|
||||||
@@ -2032,7 +2031,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-formula"
|
name = "gridcraft-formula"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2041,7 +2040,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-functions"
|
name = "gridcraft-functions"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -2049,7 +2048,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-mcp"
|
name = "gridcraft-mcp"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-engine",
|
"gridcraft-engine",
|
||||||
"log",
|
"log",
|
||||||
@@ -2058,7 +2057,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-model"
|
name = "gridcraft-model"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"gridcraft-formula",
|
"gridcraft-formula",
|
||||||
@@ -2070,18 +2069,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-numfmt"
|
name = "gridcraft-numfmt"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-pdf"
|
name = "gridcraft-pdf"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-ui-egui"
|
name = "gridcraft-ui-egui"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"craft-ui",
|
"craft-ui",
|
||||||
"craft-update",
|
"craft-update",
|
||||||
@@ -2106,7 +2105,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-web"
|
name = "gridcraft-web"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -2123,7 +2122,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-xlsx"
|
name = "gridcraft-xlsx"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
@@ -3462,7 +3461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3876,7 +3875,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4186,7 +4185,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4245,7 +4244,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4979,7 +4978,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.5",
|
"rustix 1.1.5",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5938,7 +5937,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6404,7 +6403,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-engine",
|
"gridcraft-engine",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
|||||||
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
|
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.14.2"
|
version = "0.13.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
@@ -27,7 +27,7 @@ gridcraft-pdf = { path = "crates/pdf" }
|
|||||||
# Every craft-libs crate (craft-update and craft-window here, craft-ui in ui-egui, craft-print in
|
# Every craft-libs crate (craft-update and craft-window here, craft-ui in ui-egui, craft-print in
|
||||||
# the app) comes from one storytold/craft-libs revision.
|
# the app) comes from one storytold/craft-libs revision.
|
||||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||||
craft-update = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", features = ["egui"] }
|
craft-update = { git = "https://github.com/storytold/craft-libs", rev = "04aa1703b32d299e54f78fcde4182f2b393a9116", version = "=0.1.0", features = ["egui"] }
|
||||||
|
|
||||||
# foundation
|
# foundation
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
@@ -63,7 +63,7 @@ skrifa = { version = "0.44", default-features = false, features = ["std"] }
|
|||||||
epaint_default_fonts = "0.37"
|
epaint_default_fonts = "0.37"
|
||||||
# One native window per workbook: the window list, Window menu titles and the immediate-viewport
|
# One native window per workbook: the window list, Window menu titles and the immediate-viewport
|
||||||
# driver. Same craft-libs revision as craft-update above.
|
# driver. Same craft-libs revision as craft-update above.
|
||||||
craft-window = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", default-features = false, features = ["egui"] }
|
craft-window = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", default-features = false, features = ["egui"] }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
|
|||||||
+2
-2
@@ -233,7 +233,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||||||
| 2026-10-11 | PivotCharts (`insert.pivotChart`): charts bound to a PivotTable's report, following refreshes and layout changes (#311) |
|
| 2026-10-11 | PivotCharts (`insert.pivotChart`): charts bound to a PivotTable's report, following refreshes and layout changes (#311) |
|
||||||
| 2026-10-11 | FORECAST.ETS, FORECAST.ETS.CONFINT, FORECAST.ETS.SEASONALITY, FORECAST.ETS.STAT: AAA exponential smoothing forecasts (#314) |
|
| 2026-10-11 | FORECAST.ETS, FORECAST.ETS.CONFINT, FORECAST.ETS.SEASONALITY, FORECAST.ETS.STAT: AAA exponential smoothing forecasts (#314) |
|
||||||
| 2026-10-11 | File tab Backstage view (Home, New, Open, Info, Save As, Print preview, Export, Options) and `file.options` (#174) |
|
| 2026-10-11 | File tab Backstage view (Home, New, Open, Info, Save As, Print preview, Export, Options) and `file.options` (#174) |
|
||||||
| 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed |
|
| 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed |
|
||||||
| 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 |
|
| 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 |
|
||||||
| 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows |
|
| 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows |
|
||||||
| 2026-10-07 | About window with contributor and model credits; macOS release runner |
|
| 2026-10-07 | About window with contributor and model credits; macOS release runner |
|
||||||
@@ -296,5 +296,5 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||||||
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) |
|
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) |
|
||||||
| 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) |
|
| 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) |
|
||||||
| 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha |
|
| 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha |
|
||||||
| 2026-10-10 | major | Re-measured against Excel for Mac 16.113.4: stage alpha, breadth ~75%, ready ~50%, ~160–240 h to beta, ~700–1,100 h to full parity; progress docs restructured; detail moved to `docs/` |
|
| 2026-10-10 | major | Re-measured against Excel for Mac 16.113.4: stage alpha, breadth ~75%, ready ~50%, ~160–240 h to beta, ~700–1,100 h to full parity; restructured to the progress-docs standard; detail moved to `docs/` |
|
||||||
| 2026-10-07 | major | First estimates: catalog 89%, functions ~93%, depth ~65%, ~35 h to alpha, ~215 h to full parity |
|
| 2026-10-07 | major | First estimates: catalog 89%, functions ~93%, depth ~65%, ~35 h to alpha, ~215 h to full parity |
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ winit = { version = "0.30", default-features = false, features = ["x11"] }
|
|||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
# Same craft-libs revision as the workspace's craft-update and craft-window (see the root
|
# Same craft-libs revision as the workspace's craft-update and craft-window (see the root
|
||||||
# Cargo.toml).
|
# Cargo.toml).
|
||||||
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" }
|
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||||
craft-update = { workspace = true }
|
craft-update = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
arboard = { workspace = true }
|
arboard = { workspace = true }
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//! Graphics instance configuration, applied before wgpu can initialize any driver: Windows
|
//! Windows graphics instance configuration, applied before wgpu can initialize any driver.
|
||||||
//! backends and the DirectX 12 shader compiler, and the Intel Mac indirect-validation skip.
|
|
||||||
//!
|
//!
|
||||||
//! Vulkan is never initialized unless `WGPU_BACKEND` asks for it: enumerating Vulkan adapters
|
//! Vulkan is never initialized unless `WGPU_BACKEND` asks for it: enumerating Vulkan adapters
|
||||||
//! crashes inside Intel's `igvk64.dll` on some UHD Graphics drivers (#120), and that access
|
//! crashes inside Intel's `igvk64.dll` on some UHD Graphics drivers (#120), and that access
|
||||||
@@ -9,38 +8,12 @@
|
|||||||
//! whichever `dxcompiler.dll` the DLL search path happens to find (#339, #354): see
|
//! whichever `dxcompiler.dll` the DLL search path happens to find (#339, #354): see
|
||||||
//! [`dx12_compiler`].
|
//! [`dx12_compiler`].
|
||||||
|
|
||||||
// On macOS only the indirect-validation items are used outside tests; on Windows the other way
|
|
||||||
// round, so those two carry their own `allow`.
|
|
||||||
#![cfg_attr(target_os = "macos", allow(dead_code))]
|
|
||||||
|
|
||||||
use eframe::{
|
use eframe::{
|
||||||
NativeOptions,
|
NativeOptions,
|
||||||
egui_wgpu::WgpuSetup,
|
egui_wgpu::WgpuSetup,
|
||||||
wgpu::{Backends, DeviceType, Dx12Compiler, InstanceFlags},
|
wgpu::{Backends, DeviceType, Dx12Compiler},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// wgpu compiles its indirect-call validation compute shaders inside `request_device`. On Intel
|
|
||||||
/// Macs (Iris 5100 in a Mac mini 2014) Metal's compiler aborts on them ("Compiler encountered an
|
|
||||||
/// internal error"), the device is lost and eframe exits before the window opens. GridCraft issues
|
|
||||||
/// no indirect draws or dispatches, so Intel Macs skip them; every other GPU keeps wgpu's default.
|
|
||||||
/// Same as WordCraft (storytold/wordcraft#2652) and PhotoCraft (storytold/PhotoCraft#3171).
|
|
||||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
||||||
pub fn skips_indirect_validation(macos: bool, intel: bool) -> bool {
|
|
||||||
macos && intel
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Leaves wgpu's indirect-call validation out of the instance eframe creates when `skip`.
|
|
||||||
/// `WGPU_VALIDATION_INDIRECT_CALL=1` still turns it back on.
|
|
||||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
||||||
pub fn configure_indirect_validation(options: &mut NativeOptions, skip: bool) {
|
|
||||||
if !skip {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
|
|
||||||
create.instance_descriptor.flags = create.instance_descriptor.flags.difference(InstanceFlags::VALIDATION_INDIRECT_CALL).with_env();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the start-up probe learned about one DirectX 12 adapter.
|
/// What the start-up probe learned about one DirectX 12 adapter.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Dx12Adapter {
|
pub struct Dx12Adapter {
|
||||||
@@ -168,55 +141,6 @@ fn probe_dx12(options: &NativeOptions) -> Vec<Dx12Adapter> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn instance_flags(options: &NativeOptions) -> InstanceFlags {
|
|
||||||
let WgpuSetup::CreateNew(create) = &options.wgpu_options.wgpu_setup else {
|
|
||||||
panic!("default native options must create a wgpu instance");
|
|
||||||
};
|
|
||||||
create.instance_descriptor.flags
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn intel_macs_skip_the_indirect_validation_shaders() {
|
|
||||||
// Metal's Intel compiler aborted on them, losing the device before the window opened.
|
|
||||||
assert!(skips_indirect_validation(true, true));
|
|
||||||
// Apple silicon, Windows and Linux keep wgpu's validation.
|
|
||||||
for (macos, intel) in [(true, false), (false, true), (false, false)] {
|
|
||||||
assert!(!skips_indirect_validation(macos, intel), "macos={macos} intel={intel}");
|
|
||||||
}
|
|
||||||
let mut options = NativeOptions::default();
|
|
||||||
let before = instance_flags(&options);
|
|
||||||
configure_indirect_validation(&mut options, false);
|
|
||||||
assert_eq!(instance_flags(&options), before, "flags untouched when not skipping");
|
|
||||||
if std::env::var_os("WGPU_VALIDATION_INDIRECT_CALL").is_none() {
|
|
||||||
configure_indirect_validation(&mut options, true);
|
|
||||||
assert!(!instance_flags(&options).contains(InstanceFlags::VALIDATION_INDIRECT_CALL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `WGPU_VALIDATION_INDIRECT_CALL=1` turns the validation back on even when it is skipped.
|
|
||||||
/// Tests can't set environment variables (`set_var` is unsafe, and unsafe code is forbidden),
|
|
||||||
/// so this runs itself again as a child process with the variable set.
|
|
||||||
#[test]
|
|
||||||
fn the_environment_turns_indirect_validation_back_on() {
|
|
||||||
const NAME: &str = "graphics::tests::the_environment_turns_indirect_validation_back_on";
|
|
||||||
if std::env::var_os("GRIDCRAFT_INDIRECT_VALIDATION_CHILD").is_some() {
|
|
||||||
let mut options = NativeOptions::default();
|
|
||||||
configure_indirect_validation(&mut options, true);
|
|
||||||
assert!(instance_flags(&options).contains(InstanceFlags::VALIDATION_INDIRECT_CALL));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let exe = std::env::current_exe().unwrap();
|
|
||||||
let out = std::process::Command::new(exe)
|
|
||||||
.args(["--exact", NAME, "--test-threads=1"])
|
|
||||||
.env("GRIDCRAFT_INDIRECT_VALIDATION_CHILD", "1")
|
|
||||||
.env("WGPU_VALIDATION_INDIRECT_CALL", "1")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
||||||
// `--exact` matching nothing would pass too, so the child must report this one test.
|
|
||||||
assert!(out.status.success() && stdout.contains("1 passed"), "child test failed:\n{stdout}{}", String::from_utf8_lossy(&out.stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configured_backends(backends: Backends) -> Backends {
|
fn configured_backends(backends: Backends) -> Backends {
|
||||||
let mut options = NativeOptions::default();
|
let mut options = NativeOptions::default();
|
||||||
configure_backends(&mut options, backends);
|
configure_backends(&mut options, backends);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod apple_events;
|
mod apple_events;
|
||||||
mod control_server;
|
mod control_server;
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", test))]
|
#[cfg(any(target_os = "windows", test))]
|
||||||
mod graphics;
|
mod graphics;
|
||||||
mod logging;
|
mod logging;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -341,11 +341,8 @@ fn main() -> eframe::Result<()> {
|
|||||||
}
|
}
|
||||||
let window_builder = viewport.clone();
|
let window_builder = viewport.clone();
|
||||||
let options = eframe::NativeOptions { viewport, ..Default::default() };
|
let options = eframe::NativeOptions { viewport, ..Default::default() };
|
||||||
#[cfg(unix)]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
let mut options = options;
|
let mut options = options;
|
||||||
// Intel Macs: Metal's compiler aborts on wgpu's indirect-validation shaders (see graphics.rs).
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
graphics::configure_indirect_validation(&mut options, graphics::skips_indirect_validation(true, cfg!(target_arch = "x86_64")));
|
|
||||||
// winit has no file drag-and-drop on Wayland (only on X11), so dropping files from the file
|
// winit has no file drag-and-drop on Wayland (only on X11), so dropping files from the file
|
||||||
// manager showed a "no" cursor. Run through XWayland when it's there; GRIDCRAFT_WAYLAND=1
|
// manager showed a "no" cursor. Run through XWayland when it's there; GRIDCRAFT_WAYLAND=1
|
||||||
// keeps the native Wayland backend.
|
// keeps the native Wayland backend.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# GridCraft app icon (PLACEHOLDER)
|
# GridCraft app icon (PLACEHOLDER)
|
||||||
|
|
||||||
**This icon is temporary.** Every Crafting App's icon is an engraved creature portrait that the
|
**This icon is temporary.** Every Crafting App's icon is an engraved creature portrait that the
|
||||||
product owner makes; GridCraft's hasn't been made yet.
|
product owner makes (craftrules `standards/icon-design.md`); GridCraft's hasn't been made yet.
|
||||||
Until it is, this placeholder stands in: a simple ledger page with a header row, a row-number
|
Until it is, this placeholder stands in: a simple ledger page with a header row, a row-number
|
||||||
column, a selected cell with its fill handle and a double rule under a total, on a full-bleed
|
column, a selected cell with its fill handle and a double rule under a total, on a full-bleed
|
||||||
GridCraft-green tile. It is original geometry written by hand for this repository, deliberately
|
GridCraft-green tile. It is original geometry written by hand for this repository, deliberately
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||||
<!-- GridCraft PLACEHOLDER icon: a ledger page with a selected cell, on GridCraft green.
|
<!-- GridCraft PLACEHOLDER icon: a ledger page with a selected cell, on GridCraft green.
|
||||||
Original work; temporary until the owner's engraved creature portrait replaces it
|
Original work; temporary until the owner's engraved creature portrait replaces it
|
||||||
Palette: ink #0b0b0c, paper #efe9dc,
|
(craftrules standards/icon-design.md). Palette: ink #0b0b0c, paper #efe9dc,
|
||||||
app colour #1f9d55 (proposed). -->
|
app colour #1f9d55 (proposed). -->
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="tile"><rect width="512" height="512" rx="112"/></clipPath>
|
<clipPath id="tile"><rect width="512" height="512" rx="112"/></clipPath>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||||
<!-- GridCraft PLACEHOLDER icon: a ledger page with a selected cell, on GridCraft green.
|
<!-- GridCraft PLACEHOLDER icon: a ledger page with a selected cell, on GridCraft green.
|
||||||
Original work; temporary until the owner's engraved creature portrait replaces it
|
Original work; temporary until the owner's engraved creature portrait replaces it
|
||||||
Palette: ink #0b0b0c, paper #efe9dc,
|
(craftrules standards/icon-design.md). Palette: ink #0b0b0c, paper #efe9dc,
|
||||||
app colour #1f9d55 (proposed). -->
|
app colour #1f9d55 (proposed). -->
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="tile"><rect width="512" height="512" rx="112"/></clipPath>
|
<clipPath id="tile"><rect width="512" height="512" rx="112"/></clipPath>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,10 +1,13 @@
|
|||||||
# Contributor names for the About window.
|
# Contributor names for the About window (craftrules standards/contributors.md).
|
||||||
#
|
#
|
||||||
# Everyone who contributes is credited by GitHub username automatically. Anything more is up to
|
# Everyone who contributes is credited by GitHub username automatically. Anything more is up to
|
||||||
# you: add YOUR OWN entry below, in your own PR (commit it yourself). Never add someone else; a
|
# you: add YOUR OWN entry below, in your own PR (commit it yourself). Never add someone else; a
|
||||||
# maintainer may add a person only when that person asked, recording where in consent_source.
|
# maintainer may add a person only when that person asked, recording where in consent_source.
|
||||||
#
|
#
|
||||||
# Copy this block, replacing your-github-username (every field is optional):
|
# Easiest: python3 ../../craftrules/scripts/contributors.py --add-me . \
|
||||||
|
# [--real-name "Ada Lovelace"] [--display-name "Ada"] [--sync-github-name]
|
||||||
|
#
|
||||||
|
# Or copy this block, replacing your-github-username (every field is optional):
|
||||||
#
|
#
|
||||||
# [people.your-github-username]
|
# [people.your-github-username]
|
||||||
# real_name = "Your Name" # shown when the viewer picks "Real name"
|
# real_name = "Your Name" # shown when the viewer picks "Real name"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use super::layout::{DENSE_BLOCK, SheetLayout};
|
|||||||
use super::objects::draw_cell_picture;
|
use super::objects::draw_cell_picture;
|
||||||
use super::text::{TextPlacement, draw_text};
|
use super::text::{TextPlacement, draw_text};
|
||||||
use super::{
|
use super::{
|
||||||
BorderLine, BorderStyle, Borders, CellRef, Color, Font, Page, PatternType, Point, RangeRef, Rgb, Sheet, Style, StyleId, Underline, Value,
|
BorderLine, BorderStyle, Borders, CellRef, Color, Font, Page, PatternType, Point, RangeRef, Rgb, Sheet, Style, StyleId, Underline, Workbook,
|
||||||
Workbook, split_sheet,
|
split_sheet,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------- drawing
|
// ---------------------------------------------------------------- drawing
|
||||||
@@ -280,7 +280,6 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt
|
|||||||
|| merges.iter().any(|m| m.start == *c)
|
|| merges.iter().any(|m| m.start == *c)
|
||||||
|| !sh.value(*c).is_empty()
|
|| !sh.value(*c).is_empty()
|
||||||
|| l.style.borders != Borders::default()
|
|| l.style.borders != Borders::default()
|
||||||
|| l.style.num_fmt.as_str() == "checkbox"
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let rect_of = |c: CellRef| -> (f32, f32, f32, f32, bool) {
|
let rect_of = |c: CellRef| -> (f32, f32, f32, f32, bool) {
|
||||||
@@ -344,8 +343,6 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt
|
|||||||
}
|
}
|
||||||
if let Some(picture) = sh.cell_pictures.get(c) {
|
if let Some(picture) = sh.cell_pictures.get(c) {
|
||||||
draw_cell_picture(pg, &picture.data, (x, y, w, h), b.k);
|
draw_cell_picture(pg, &picture.data, (x, y, w, h), b.k);
|
||||||
} else if let Some(on) = checkbox_state(l, &sh.value(*c)) {
|
|
||||||
draw_checkbox(pg, (x, y, w, h), b.k, on);
|
|
||||||
} else {
|
} else {
|
||||||
draw_text(pg, b, TextPlacement { cell: *c, look: l, rect: (x, y, w, h), merged, merges: &merges, limits: (bx, bx + bw) });
|
draw_text(pg, b, TextPlacement { cell: *c, look: l, rect: (x, y, w, h), merged, merges: &merges, limits: (bx, bx + bw) });
|
||||||
}
|
}
|
||||||
@@ -353,37 +350,6 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt
|
|||||||
pg.restore();
|
pg.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A checkbox cell prints TRUE/FALSE (and empty) as a box, as on screen (#2972); other values
|
|
||||||
/// print as text.
|
|
||||||
fn checkbox_state(l: &Look, v: &Value) -> Option<bool> {
|
|
||||||
if l.style.num_fmt.as_str() != "checkbox" || l.cf.as_ref().is_some_and(|cf| cf.hide_value) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match v {
|
|
||||||
Value::Bool(on) => Some(*on),
|
|
||||||
Value::Empty => Some(false),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A checkbox centred in the cell: white with a grey outline, or green with a white tick.
|
|
||||||
fn draw_checkbox(pg: &mut Page, (x, y, w, h): (f32, f32, f32, f32), k: f32, on: bool) {
|
|
||||||
let s = (13.2 * k).min(w - 2.0 * k).min(h - 2.0 * k);
|
|
||||||
if s <= 0.0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let (bx, by) = (x + (w - s) / 2.0, y + (h - s) / 2.0);
|
|
||||||
let corners = [[bx, by], [bx + s, by], [bx + s, by + s], [bx, by + s]];
|
|
||||||
if on {
|
|
||||||
let green = [0x10, 0x7C, 0x41];
|
|
||||||
pg.polygon(&corners, Some(green), Some((green, 0.75 * k)));
|
|
||||||
let tick = [[bx + 0.22 * s, by + 0.52 * s], [bx + 0.42 * s, by + 0.72 * s], [bx + 0.78 * s, by + 0.30 * s]];
|
|
||||||
pg.polyline(&tick, 0.14 * s, [0xFF, 0xFF, 0xFF], &[]);
|
|
||||||
} else {
|
|
||||||
pg.polygon(&corners, Some([0xFF, 0xFF, 0xFF]), Some(([110, 110, 110], 0.75 * k)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct PendingPdfLink {
|
pub(super) struct PendingPdfLink {
|
||||||
pub(super) page: usize,
|
pub(super) page: usize,
|
||||||
pub(super) sheet: usize,
|
pub(super) sheet: usize,
|
||||||
|
|||||||
@@ -565,19 +565,3 @@ fn rotated_wrapped_text_prints_rotated() {
|
|||||||
assert_eq!(up + down, blocks.len(), "a header line printed flat: {blocks:?}");
|
assert_eq!(up + down, blocks.len(), "a header line printed flat: {blocks:?}");
|
||||||
assert!((1..=3).contains(&up) && (1..=3).contains(&down), "up {up}, down {down}: {blocks:?}");
|
assert!((1..=3).contains(&up) && (1..=3).contains(&down), "up {up}, down {down}: {blocks:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn checkbox_cells_print_as_boxes_not_true_false() {
|
|
||||||
// #2972: a checkbox cell printed its value as TRUE or FALSE.
|
|
||||||
let mut s = s();
|
|
||||||
s.execute("insert.checkbox", json!({"range": "A1:A2"})).unwrap();
|
|
||||||
s.execute("cell.toggleCheckbox", json!({"cell": "A2"})).unwrap();
|
|
||||||
s.execute("cell.set", json!({"cell": "C1", "input": "TRUE"})).unwrap();
|
|
||||||
let (pdf, _) = pdf_of(&mut s, json!({}));
|
|
||||||
assert!(!contains(&pdf, "(FALSE) Tj"), "the unchecked box printed as FALSE");
|
|
||||||
// Only the plain TRUE in C1 prints as text; the checked box is a filled box.
|
|
||||||
let text = String::from_utf8_lossy(&pdf);
|
|
||||||
assert_eq!(text.matches("(TRUE) Tj").count(), 1, "the checked box printed as TRUE");
|
|
||||||
assert!(contains(&pdf, "0.063 0.486 0.255 rg"), "the checked box is not filled");
|
|
||||||
assert!(contains(&pdf, "0.431 0.431 0.431 RG"), "the unchecked box is not outlined");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ fn write_struct_ref(s: &StructRef, at_shorthand: bool) -> String {
|
|||||||
let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or(""));
|
let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or(""));
|
||||||
}
|
}
|
||||||
(Some(c), None) if s.specifiers.is_empty() => {
|
(Some(c), None) if s.specifiers.is_empty() => {
|
||||||
// Names with spaces at their edges, or starting with `@` (this-row shorthand), use the double-bracket form.
|
// Excel writes a name with spaces at its edges in the double-bracket form.
|
||||||
if c.trim() == c && !c.starts_with('@') {
|
if c.trim() == c {
|
||||||
let _ = write!(out, "[{}]", esc(c));
|
let _ = write!(out, "[{}]", esc(c));
|
||||||
} else {
|
} else {
|
||||||
let _ = write!(out, "[[{}]]", esc(c));
|
let _ = write!(out, "[[{}]]", esc(c));
|
||||||
|
|||||||
@@ -558,12 +558,3 @@ fn reference_shapes_print_unchanged() -> Result<(), Box<dyn std::error::Error>>
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn column_starting_with_at_round_trips() {
|
|
||||||
for name in ["@a", "a@b"] {
|
|
||||||
let s = StructRef { table: "T".into(), specifiers: vec![], col_start: Some(name.into()), col_end: None };
|
|
||||||
let text = format!("={}", printer::struct_ref(&s));
|
|
||||||
assert_eq!(parse(&text).ok(), Some(Expr::Struct(s)), "printed as {text}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -449,15 +449,13 @@ pub(crate) fn yearfrac_core(sys: DateSystem, s: i64, e: i64, basis: i64) -> R<f6
|
|||||||
Ok(match basis {
|
Ok(match basis {
|
||||||
0 => {
|
0 => {
|
||||||
let (mut a, mut b) = (d1.day as i64, d2.day as i64);
|
let (mut a, mut b) = (d1.day as i64, d2.day as i64);
|
||||||
// Excel tests the end-31 rule against the original start day, before a February end moves to 30.
|
|
||||||
let start_30 = a >= 30;
|
|
||||||
if last_of_feb(&d1) && last_of_feb(&d2) {
|
if last_of_feb(&d1) && last_of_feb(&d2) {
|
||||||
b = 30;
|
b = 30;
|
||||||
}
|
}
|
||||||
if last_of_feb(&d1) || a == 31 {
|
if last_of_feb(&d1) || a == 31 {
|
||||||
a = 30;
|
a = 30;
|
||||||
}
|
}
|
||||||
if start_30 && b == 31 {
|
if a == 30 && b == 31 {
|
||||||
b = 30;
|
b = 30;
|
||||||
}
|
}
|
||||||
((d2.year as i64 - d1.year as i64) * 360 + (d2.month as i64 - d1.month as i64) * 30 + b - a) as f64 / 360.0
|
((d2.year as i64 - d1.year as i64) * 360 + (d2.month as i64 - d1.month as i64) * 30 + b - a) as f64 / 360.0
|
||||||
@@ -819,13 +817,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn yearfrac_basis0_feb_end_to_31() {
|
fn yearfrac_basis0_feb_end_to_31() {
|
||||||
// Excel 16: a February-end start keeps its original day for the end-31 test, so 31 days.
|
close(ev("YEARFRAC", vec![n(d(2021.0, 2.0, 28.0)), n(d(2021.0, 3.0, 31.0)), n(0.0)]), 30.0 / 360.0);
|
||||||
close(ev("YEARFRAC", vec![n(d(2021.0, 2.0, 28.0)), n(d(2021.0, 3.0, 31.0)), n(0.0)]), 0.086_111_111_111_111_11);
|
close(ev("YEARFRAC", vec![n(d(2020.0, 2.0, 29.0)), n(d(2020.0, 3.0, 31.0)), n(0.0)]), 30.0 / 360.0);
|
||||||
close(ev("YEARFRAC", vec![n(d(2020.0, 2.0, 29.0)), n(d(2020.0, 3.0, 31.0)), n(0.0)]), 0.086_111_111_111_111_11);
|
|
||||||
close(ev("YEARFRAC", vec![n(d(2021.0, 3.0, 31.0)), n(d(2021.0, 2.0, 28.0)), n(0.0)]), 0.086_111_111_111_111_11);
|
|
||||||
close(ev("YEARFRAC", vec![n(d(2021.0, 1.0, 31.0)), n(d(2021.0, 2.0, 28.0)), n(0.0)]), 0.077_777_777_777_777_78);
|
|
||||||
close(ev("YEARFRAC", vec![n(d(2021.0, 1.0, 30.0)), n(d(2021.0, 3.0, 31.0)), n(0.0)]), 1.0 / 6.0);
|
|
||||||
close(ev("YEARFRAC", vec![n(d(2020.0, 2.0, 29.0)), n(d(2021.0, 2.0, 28.0)), n(0.0)]), 1.0);
|
|
||||||
close(ev("YEARFRAC", vec![n(d(2021.0, 2.0, 28.0)), n(d(2021.0, 2.0, 28.0)), n(0.0)]), 0.0);
|
close(ev("YEARFRAC", vec![n(d(2021.0, 2.0, 28.0)), n(d(2021.0, 2.0, 28.0)), n(0.0)]), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ fn base_source(a: &[Arg]) -> R<String> {
|
|||||||
Ok(number_to_text(n))
|
Ok(number_to_text(n))
|
||||||
}
|
}
|
||||||
Value::Empty => Ok(String::new()),
|
Value::Empty => Ok(String::new()),
|
||||||
v => Ok(v.to_text()?.to_string()),
|
v => Ok(v.to_text()?.trim().to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,15 +62,6 @@ fn format_base(v: i64, radix: u32, places: Option<f64>) -> R<Value> {
|
|||||||
if v < min || v > max {
|
if v < min || v > max {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
let places = places
|
|
||||||
.map(|p| {
|
|
||||||
let p = p.trunc();
|
|
||||||
if !(1.0..=10.0).contains(&p) {
|
|
||||||
return Err(CellError::Num);
|
|
||||||
}
|
|
||||||
Ok(p as usize)
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
if v < 0 {
|
if v < 0 {
|
||||||
let u = v + (1i64 << bits);
|
let u = v + (1i64 << bits);
|
||||||
return text_val(to_radix(u, radix));
|
return text_val(to_radix(u, radix));
|
||||||
@@ -79,6 +70,11 @@ fn format_base(v: i64, radix: u32, places: Option<f64>) -> R<Value> {
|
|||||||
match places {
|
match places {
|
||||||
None => text_val(s),
|
None => text_val(s),
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
|
let p = p.trunc();
|
||||||
|
if !(1.0..=10.0).contains(&p) {
|
||||||
|
return Err(CellError::Num);
|
||||||
|
}
|
||||||
|
let p = p as usize;
|
||||||
if s.len() > p {
|
if s.len() > p {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
@@ -101,13 +97,7 @@ fn to_radix(mut v: i64, radix: u32) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn places_arg(a: &[Arg]) -> R<Option<f64>> {
|
fn places_arg(a: &[Arg]) -> R<Option<f64>> {
|
||||||
if !has(a, 1) {
|
if has(a, 1) { Ok(Some(num(a, 1)?)) } else { Ok(None) }
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
if matches!(a.get(1).map(scalar), Some(Value::Bool(_))) {
|
|
||||||
return Err(CellError::Value);
|
|
||||||
}
|
|
||||||
Ok(Some(num(a, 1)?))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_base(a: &[Arg], from: u32, to: u32) -> R<Value> {
|
fn convert_base(a: &[Arg], from: u32, to: u32) -> R<Value> {
|
||||||
@@ -185,9 +175,6 @@ fn bitxor(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
fn shift(a: &[Arg], left: bool) -> R<Value> {
|
fn shift(a: &[Arg], left: bool) -> R<Value> {
|
||||||
let n = bit_operand(a, 0)?;
|
let n = bit_operand(a, 0)?;
|
||||||
let s = num(a, 1)?.trunc();
|
let s = num(a, 1)?.trunc();
|
||||||
if n == 0 {
|
|
||||||
return Ok(Value::Number(0.0));
|
|
||||||
}
|
|
||||||
if s.abs() > 53.0 {
|
if s.abs() > 53.0 {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
@@ -415,12 +402,6 @@ mod tests {
|
|||||||
is_text(ev("BIN2OCT", vec![n(1111111111.0)]), "7777777777");
|
is_text(ev("BIN2OCT", vec![n(1111111111.0)]), "7777777777");
|
||||||
is_text(ev("DEC2BIN", vec![n(9.0), n(4.0)]), "1001");
|
is_text(ev("DEC2BIN", vec![n(9.0), n(4.0)]), "1001");
|
||||||
is_text(ev("DEC2BIN", vec![n(-100.0)]), "1110011100");
|
is_text(ev("DEC2BIN", vec![n(-100.0)]), "1110011100");
|
||||||
is_err(ev("DEC2BIN", vec![n(-1.0), n(0.0)]), CellError::Num);
|
|
||||||
is_err(ev("DEC2HEX", vec![n(-1.0), n(11.0)]), CellError::Num);
|
|
||||||
is_err(ev("DEC2OCT", vec![n(-1.0), n(-1.0)]), CellError::Num);
|
|
||||||
is_err(ev("DEC2BIN", vec![n(1.0), crate::Arg::val(Value::Bool(true))]), CellError::Value);
|
|
||||||
is_text(ev("DEC2BIN", vec![n(-1.0), n(1.0)]), "1111111111");
|
|
||||||
is_text(ev("DEC2HEX", vec![n(-1.0), n(10.0)]), "FFFFFFFFFF");
|
|
||||||
is_err(ev("DEC2BIN", vec![n(512.0)]), CellError::Num);
|
is_err(ev("DEC2BIN", vec![n(512.0)]), CellError::Num);
|
||||||
is_text(ev("DEC2HEX", vec![n(100.0), n(4.0)]), "0064");
|
is_text(ev("DEC2HEX", vec![n(100.0), n(4.0)]), "0064");
|
||||||
is_text(ev("DEC2HEX", vec![n(-54.0)]), "FFFFFFFFCA");
|
is_text(ev("DEC2HEX", vec![n(-54.0)]), "FFFFFFFFCA");
|
||||||
@@ -445,21 +426,6 @@ mod tests {
|
|||||||
is_text(ev("OCT2HEX", vec![n(100.0), n(4.0)]), "0040");
|
is_text(ev("OCT2HEX", vec![n(100.0), n(4.0)]), "0040");
|
||||||
is_text(ev("OCT2HEX", vec![n(7777777533.0)]), "FFFFFFFF5B");
|
is_text(ev("OCT2HEX", vec![n(7777777533.0)]), "FFFFFFFF5B");
|
||||||
is_err(ev("BIN2DEC", vec![n(102.0)]), CellError::Num);
|
is_err(ev("BIN2DEC", vec![n(102.0)]), CellError::Num);
|
||||||
for (f, input) in [
|
|
||||||
("BIN2DEC", " 10"),
|
|
||||||
("BIN2DEC", "10 "),
|
|
||||||
("BIN2DEC", "\t10"),
|
|
||||||
("BIN2HEX", " 10"),
|
|
||||||
("BIN2OCT", "10 "),
|
|
||||||
("HEX2DEC", " 10"),
|
|
||||||
("HEX2BIN", "10 "),
|
|
||||||
("HEX2OCT", "\t10"),
|
|
||||||
("OCT2DEC", " 10"),
|
|
||||||
("OCT2BIN", "10 "),
|
|
||||||
("OCT2HEX", "\t10"),
|
|
||||||
] {
|
|
||||||
is_err(ev(f, vec![t(input)]), CellError::Num);
|
|
||||||
}
|
|
||||||
is_err(ev("HEX2DEC", vec![t("GG")]), CellError::Num);
|
is_err(ev("HEX2DEC", vec![t("GG")]), CellError::Num);
|
||||||
is_err(ev("BIN2DEC", vec![t("11111111111")]), CellError::Num);
|
is_err(ev("BIN2DEC", vec![t("11111111111")]), CellError::Num);
|
||||||
is_err(ev("DEC2BIN", vec![t("abc")]), CellError::Value);
|
is_err(ev("DEC2BIN", vec![t("abc")]), CellError::Value);
|
||||||
@@ -480,21 +446,6 @@ mod tests {
|
|||||||
is_err(ev("BITAND", vec![n(1.5), n(5.0)]), CellError::Num);
|
is_err(ev("BITAND", vec![n(1.5), n(5.0)]), CellError::Num);
|
||||||
is_err(ev("BITLSHIFT", vec![n(1.0), n(54.0)]), CellError::Num);
|
is_err(ev("BITLSHIFT", vec![n(1.0), n(54.0)]), CellError::Num);
|
||||||
is_err(ev("BITLSHIFT", vec![n(2f64.powi(47)), n(1.0)]), CellError::Num);
|
is_err(ev("BITLSHIFT", vec![n(2f64.powi(47)), n(1.0)]), CellError::Num);
|
||||||
for f in ["BITLSHIFT", "BITRSHIFT"] {
|
|
||||||
for shift in [-100.0, -54.0, -53.0, 53.0, 54.0, 100.0] {
|
|
||||||
close(ev(f, vec![n(0.0), n(shift)]), 0.0);
|
|
||||||
}
|
|
||||||
for shift in [-100.0, -54.0, 54.0, 100.0] {
|
|
||||||
is_err(ev(f, vec![n(1.0), n(shift)]), CellError::Num);
|
|
||||||
}
|
|
||||||
if f == "BITLSHIFT" {
|
|
||||||
is_err(ev(f, vec![n(1.0), n(53.0)]), CellError::Num);
|
|
||||||
} else {
|
|
||||||
close(ev(f, vec![n(1.0), n(53.0)]), 0.0);
|
|
||||||
}
|
|
||||||
is_err(ev(f, vec![n(0.0), t("bad")]), CellError::Value);
|
|
||||||
}
|
|
||||||
is_err(ev("BITRSHIFT", vec![n(-1.0), n(100.0)]), CellError::Num);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -314,10 +314,6 @@ fn modulo(x: f64, d: f64) -> R<f64> {
|
|||||||
if d == 0.0 {
|
if d == 0.0 {
|
||||||
return Err(CellError::Div0);
|
return Err(CellError::Div0);
|
||||||
}
|
}
|
||||||
// Excel returns #NUM! once the quotient magnitude reaches 1.1259E12 (checked against Excel 16).
|
|
||||||
if (x / d).abs() >= 1.1259e12 {
|
|
||||||
return Err(CellError::Num);
|
|
||||||
}
|
|
||||||
// `%` is an exact remainder (truncated), so |r| < |d| even for tiny divisors.
|
// `%` is an exact remainder (truncated), so |r| < |d| even for tiny divisors.
|
||||||
let r = x % d;
|
let r = x % d;
|
||||||
// Keep the sign of the divisor and avoid returning |d| from rounding.
|
// Keep the sign of the divisor and avoid returning |d| from rounding.
|
||||||
|
|||||||
@@ -140,22 +140,16 @@ fn rounding() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mod_quotient_limit() {
|
fn mod_tiny_divisor_stays_in_range() {
|
||||||
// Excel 16: #NUM! whenever |number / divisor| >= 1.1259E12.
|
for (x, d) in [(3.0, 1e-300), (-1.0, 1e-300), (3.0, -1e-300), (-1.0, -1e-300)] {
|
||||||
for (x, d) in [
|
match ev2("MOD", x, d) {
|
||||||
(3.0, 1e-300),
|
Value::Number(r) => {
|
||||||
(-1.0, 1e-300),
|
assert!(r == 0.0 || (r < 0.0) == (d < 0.0), "MOD({x}, {d}) = {r}");
|
||||||
(3.0, -1e-300),
|
assert!(r.abs() < d.abs(), "MOD({x}, {d}) = {r}");
|
||||||
(-1.0, -1e-300),
|
}
|
||||||
(1e15, 7.0),
|
other => panic!("MOD({x}, {d}) = {other:?}"),
|
||||||
(1e10, 0.001),
|
}
|
||||||
(1_125_900_000_000.0, 1.0),
|
|
||||||
(-1_125_900_000_000.0, 1.0),
|
|
||||||
] {
|
|
||||||
is_err(ev2("MOD", x, d), Num);
|
|
||||||
}
|
}
|
||||||
close(ev2("MOD", 1e12 * 7.0 + 3.0, 7.0), 3.0);
|
|
||||||
close(ev2("MOD", 1_125_899_999_999.99, 1.0), 0.989_990_234_375);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -336,10 +336,6 @@ pub(super) fn percentrank_impl(a: &[Arg], exc: bool) -> R<Value> {
|
|||||||
if sig < 1.0 || s.is_empty() {
|
if sig < 1.0 || s.is_empty() {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
// Excel: 10^significance overflows from 309 up.
|
|
||||||
if sig >= 309.0 {
|
|
||||||
return Err(CellError::Div0);
|
|
||||||
}
|
|
||||||
let (first, last) = (s.first().copied().unwrap_or(0.0), s.last().copied().unwrap_or(0.0));
|
let (first, last) = (s.first().copied().unwrap_or(0.0), s.last().copied().unwrap_or(0.0));
|
||||||
if x < first || x > last {
|
if x < first || x > last {
|
||||||
return Err(CellError::NA);
|
return Err(CellError::NA);
|
||||||
@@ -363,10 +359,6 @@ pub(super) fn percentrank_impl(a: &[Arg], exc: bool) -> R<Value> {
|
|||||||
let (rl, rh) = (rank_of(lo), rank_of(hi));
|
let (rl, rh) = (rank_of(lo), rank_of(hi));
|
||||||
rl + (x - lo) / (hi - lo) * (rh - rl)
|
rl + (x - lo) / (hi - lo) * (rh - rl)
|
||||||
};
|
};
|
||||||
// From significance 15 up Excel returns 15 significant digits, rounded.
|
|
||||||
if sig >= 15.0 {
|
|
||||||
return num_val(format!("{r:.14e}").parse().unwrap_or(r));
|
|
||||||
}
|
|
||||||
num_val(trunc_digits(r, sig))
|
num_val(trunc_digits(r, sig))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,10 +420,7 @@ pub(super) fn geomean(v: &[f64]) -> R<f64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn harmean(v: &[f64]) -> R<f64> {
|
pub(super) fn harmean(v: &[f64]) -> R<f64> {
|
||||||
if v.is_empty() {
|
if v.is_empty() || v.iter().any(|&x| x <= 0.0) {
|
||||||
return Err(CellError::NA);
|
|
||||||
}
|
|
||||||
if v.iter().any(|&x| x <= 0.0) {
|
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
Ok(v.len() as f64 / v.iter().map(|x| 1.0 / x).sum::<f64>())
|
Ok(v.len() as f64 / v.iter().map(|x| 1.0 / x).sum::<f64>())
|
||||||
|
|||||||
@@ -22,14 +22,7 @@ fn t_cdf(t: f64, v: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn t_pdf(t: f64, v: f64) -> f64 {
|
fn t_pdf(t: f64, v: f64) -> f64 {
|
||||||
let z = v / 2.0;
|
(ln_gamma((v + 1.0) / 2.0) - ln_gamma(v / 2.0) - 0.5 * (v * std::f64::consts::PI).ln() - (v + 1.0) / 2.0 * (1.0 + t * t / v).ln()).exp()
|
||||||
let log_gamma_ratio = if z >= 16.0 {
|
|
||||||
let inv_z = 1.0 / z;
|
|
||||||
0.5 * z.ln() - inv_z / 8.0 + inv_z.powi(3) / 192.0 - inv_z.powi(5) / 640.0 + 17.0 * inv_z.powi(7) / 14_336.0
|
|
||||||
} else {
|
|
||||||
ln_gamma(z + 0.5) - ln_gamma(z)
|
|
||||||
};
|
|
||||||
(log_gamma_ratio - 0.5 * (v.ln() + std::f64::consts::PI.ln()) - (v + 1.0) / 2.0 * (t * t / v).ln_1p()).exp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// t ≥ 0 with P(T > t) = q, q in (0, 0.5].
|
/// t ≥ 0 with P(T > t) = q, q in (0, 0.5].
|
||||||
@@ -58,12 +51,6 @@ fn f_cdf(x: f64, d1: f64, d2: f64) -> f64 {
|
|||||||
if x <= 0.0 {
|
if x <= 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
if x == 1.0 && d1 == d2 {
|
|
||||||
return 0.5;
|
|
||||||
}
|
|
||||||
if d1 >= 1e8 && d2 / d1 <= 1e-8 {
|
|
||||||
return reg_gamma_q(d2 / 2.0, d2 / (2.0 * (x + d2 / d1)));
|
|
||||||
}
|
|
||||||
reg_inc_beta(d1 * x / (d1 * x + d2), d1 / 2.0, d2 / 2.0)
|
reg_inc_beta(d1 * x / (d1 * x + d2), d1 / 2.0, d2 / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +58,6 @@ fn f_upper(x: f64, d1: f64, d2: f64) -> f64 {
|
|||||||
if x <= 0.0 {
|
if x <= 0.0 {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
if d1 >= 1e8 && d2 / d1 <= 1e-8 {
|
|
||||||
// With a much larger numerator degree, the scaled beta tail converges to a gamma tail.
|
|
||||||
return reg_gamma_p(d2 / 2.0, d2 / (2.0 * (x + d2 / d1)));
|
|
||||||
}
|
|
||||||
reg_inc_beta(d2 / (d2 + d1 * x), d2 / 2.0, d1 / 2.0)
|
reg_inc_beta(d2 / (d2 + d1 * x), d2 / 2.0, d1 / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +74,6 @@ fn f_pdf(x: f64, d1: f64, d2: f64) -> f64 {
|
|||||||
((d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * x.ln() - ((d1 + d2) / 2.0) * (1.0 + d1 * x / d2).ln() - ln_beta(d1 / 2.0, d2 / 2.0)).exp()
|
((d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * x.ln() - ((d1 + d2) / 2.0) * (1.0 + d1 * x / d2).ln() - ln_beta(d1 / 2.0, d2 / 2.0)).exp()
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_BINOM_TRIALS: f64 = 2_147_483_646.0;
|
|
||||||
|
|
||||||
fn binom_pmf(k: f64, n: f64, p: f64) -> f64 {
|
fn binom_pmf(k: f64, n: f64, p: f64) -> f64 {
|
||||||
if k < 0.0 || k > n {
|
if k < 0.0 || k > n {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -195,14 +176,8 @@ fn df_arg(a: &[Arg], i: usize) -> R<f64> {
|
|||||||
|
|
||||||
pub(super) fn t_dist(a: &[Arg], _: C) -> R<Value> {
|
pub(super) fn t_dist(a: &[Arg], _: C) -> R<Value> {
|
||||||
let x = num(a, 0)?;
|
let x = num(a, 0)?;
|
||||||
let v = num(a, 1)?.trunc();
|
let v = df_arg(a, 1)?;
|
||||||
let cum = boolean(a, 2)?;
|
let cum = boolean(a, 2)?;
|
||||||
if v == 0.0 {
|
|
||||||
return Err(if cum { CellError::Num } else { CellError::Div0 });
|
|
||||||
}
|
|
||||||
if v < 0.0 {
|
|
||||||
return Err(CellError::Num);
|
|
||||||
}
|
|
||||||
num_val(if cum { t_cdf(x, v) } else { t_pdf(x, v) })
|
num_val(if cum { t_cdf(x, v) } else { t_pdf(x, v) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,9 +209,6 @@ pub(super) fn tdist_legacy(a: &[Arg], _: C) -> R<Value> {
|
|||||||
pub(super) fn t_inv(a: &[Arg], _: C) -> R<Value> {
|
pub(super) fn t_inv(a: &[Arg], _: C) -> R<Value> {
|
||||||
let p = num(a, 0)?;
|
let p = num(a, 0)?;
|
||||||
let v = df_arg(a, 1)?;
|
let v = df_arg(a, 1)?;
|
||||||
if p == 0.0 {
|
|
||||||
return Err(CellError::Div0);
|
|
||||||
}
|
|
||||||
if !(p > 0.0 && p < 1.0) {
|
if !(p > 0.0 && p < 1.0) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
@@ -375,7 +347,7 @@ fn f_args(a: &[Arg]) -> R<(f64, f64, f64)> {
|
|||||||
let x = num(a, 0)?;
|
let x = num(a, 0)?;
|
||||||
let d1 = num(a, 1)?.trunc();
|
let d1 = num(a, 1)?.trunc();
|
||||||
let d2 = num(a, 2)?.trunc();
|
let d2 = num(a, 2)?.trunc();
|
||||||
if d1 < 1.0 || d2 < 1.0 || d1 > 1e10 || d2 > 1e10 {
|
if d1 < 1.0 || d2 < 1.0 || d1 >= 1e10 || d2 >= 1e10 {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
Ok((x, d1, d2))
|
Ok((x, d1, d2))
|
||||||
@@ -406,8 +378,7 @@ pub(super) fn f_inv(a: &[Arg], _: C) -> R<Value> {
|
|||||||
if p == 0.0 {
|
if p == 0.0 {
|
||||||
return num_val(0.0);
|
return num_val(0.0);
|
||||||
}
|
}
|
||||||
let value = if p < 0.5 { invert(|x| f_cdf(x, d1, d2), p, 0.0, 1.0, 1e60) } else { invert(|x| f_upper(x, d1, d2), 1.0 - p, 0.0, 1.0, 1e60) };
|
num_val(invert(|x| f_cdf(x, d1, d2), p, 0.0, 1.0, 1e60))
|
||||||
num_val(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn f_inv_rt(a: &[Arg], _: C) -> R<Value> {
|
pub(super) fn f_inv_rt(a: &[Arg], _: C) -> R<Value> {
|
||||||
@@ -439,7 +410,7 @@ pub(super) fn binom_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
let n = num(a, 1)?.trunc();
|
let n = num(a, 1)?.trunc();
|
||||||
let p = num(a, 2)?;
|
let p = num(a, 2)?;
|
||||||
let cum = boolean(a, 3)?;
|
let cum = boolean(a, 3)?;
|
||||||
if k < 0.0 || n > MAX_BINOM_TRIALS || k > n || !(0.0..=1.0).contains(&p) {
|
if k < 0.0 || k > n || !(0.0..=1.0).contains(&p) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
num_val(if cum { binom_cdf(k, n, p) } else { binom_pmf(k, n, p) })
|
num_val(if cum { binom_cdf(k, n, p) } else { binom_pmf(k, n, p) })
|
||||||
@@ -450,7 +421,7 @@ pub(super) fn binom_dist_range(a: &[Arg], _: C) -> R<Value> {
|
|||||||
let p = num(a, 1)?;
|
let p = num(a, 1)?;
|
||||||
let s1 = num(a, 2)?.trunc();
|
let s1 = num(a, 2)?.trunc();
|
||||||
let s2 = if has(a, 3) { num(a, 3)?.trunc() } else { s1 };
|
let s2 = if has(a, 3) { num(a, 3)?.trunc() } else { s1 };
|
||||||
if !(0.0..=MAX_BINOM_TRIALS).contains(&n) || !(0.0..=1.0).contains(&p) || s1 < 0.0 || s1 > n || s2 < s1 || s2 > n {
|
if !(0.0..=1e15).contains(&n) || !(0.0..=1.0).contains(&p) || s1 < 0.0 || s1 > n || s2 < s1 || s2 > n {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
if s2 - s1 > 10_000.0 {
|
if s2 - s1 > 10_000.0 {
|
||||||
@@ -528,7 +499,13 @@ pub(super) fn gamma_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
return num_val(reg_gamma_p(al, x / be));
|
return num_val(reg_gamma_p(al, x / be));
|
||||||
}
|
}
|
||||||
if x == 0.0 {
|
if x == 0.0 {
|
||||||
return if al <= 1.0 { Err(CellError::Num) } else { num_val(0.0) };
|
return if al < 1.0 {
|
||||||
|
Err(CellError::Num)
|
||||||
|
} else if al == 1.0 {
|
||||||
|
num_val(1.0 / be)
|
||||||
|
} else {
|
||||||
|
num_val(0.0)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
num_val(((al - 1.0) * x.ln() - x / be - al * be.ln() - ln_gamma(al)).exp())
|
num_val(((al - 1.0) * x.ln() - x / be - al * be.ln() - ln_gamma(al)).exp())
|
||||||
}
|
}
|
||||||
@@ -571,7 +548,7 @@ pub(super) fn beta_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
if cum {
|
if cum {
|
||||||
return num_val(reg_inc_beta(z, al, be));
|
return num_val(reg_inc_beta(z, al, be));
|
||||||
}
|
}
|
||||||
if (z == 0.0 && al <= 1.0) || (z == 1.0 && be <= 1.0) {
|
if (z == 0.0 && al < 1.0) || (z == 1.0 && be < 1.0) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
let lp = if z == 0.0 || z == 1.0 {
|
let lp = if z == 0.0 || z == 1.0 {
|
||||||
@@ -631,9 +608,6 @@ pub(super) fn weibull_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
if x < 0.0 || al <= 0.0 || be <= 0.0 {
|
if x < 0.0 || al <= 0.0 || be <= 0.0 {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
if x == 0.0 {
|
|
||||||
return num_val(0.0);
|
|
||||||
}
|
|
||||||
let t = (x / be).powf(al);
|
let t = (x / be).powf(al);
|
||||||
num_val(if cum { -(-t).exp_m1() } else { al / be.powf(al) * x.powf(al - 1.0) * (-t).exp() })
|
num_val(if cum { -(-t).exp_m1() } else { al / be.powf(al) * x.powf(al - 1.0) * (-t).exp() })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,19 +151,6 @@ fn spread() {
|
|||||||
let g = [4.0, 5.0, 8.0, 7.0, 11.0, 4.0, 3.0];
|
let g = [4.0, 5.0, 8.0, 7.0, 11.0, 4.0, 3.0];
|
||||||
close_tol(ev("GEOMEAN", vec![c(&g)]), 5.476986969, 1e-9);
|
close_tol(ev("GEOMEAN", vec![c(&g)]), 5.476986969, 1e-9);
|
||||||
close_tol(ev("HARMEAN", vec![c(&g)]), 5.028375962, 1e-9);
|
close_tol(ev("HARMEAN", vec![c(&g)]), 5.028375962, 1e-9);
|
||||||
let text_values = arr(vec![vec![tv("1"), tv("2")]]);
|
|
||||||
let bool_values = arr(vec![vec![Value::Bool(true), Value::Bool(false)]]);
|
|
||||||
let mixed_values = arr(vec![vec![nv(2.0), tv("ignored"), Value::Bool(true)]]);
|
|
||||||
is_err(ev("HARMEAN", vec![av(text_values.clone())]), CellError::NA);
|
|
||||||
is_err(ev("HARMEAN", vec![av(bool_values.clone())]), CellError::NA);
|
|
||||||
is_err(ev("HARMEAN", vec![rf(text_values)]), CellError::NA);
|
|
||||||
is_err(ev("HARMEAN", vec![rf(bool_values)]), CellError::NA);
|
|
||||||
close(ev("HARMEAN", vec![av(mixed_values.clone())]), 2.0);
|
|
||||||
close(ev("HARMEAN", vec![rf(mixed_values)]), 2.0);
|
|
||||||
close(ev("HARMEAN", vec![b(true)]), 1.0);
|
|
||||||
close(ev("HARMEAN", vec![n(2.0), n(4.0)]), 8.0 / 3.0);
|
|
||||||
is_err(ev("HARMEAN", vec![av(arr(vec![vec![nv(0.0)]]))]), CellError::Num);
|
|
||||||
is_err(ev("HARMEAN", vec![av(arr(vec![vec![nv(-1.0)]]))]), CellError::Num);
|
|
||||||
is_err(ev("GEOMEAN", vec![c(&[1.0, 0.0])]), CellError::Num);
|
is_err(ev("GEOMEAN", vec![c(&[1.0, 0.0])]), CellError::Num);
|
||||||
close_tol(ev("TRIMMEAN", vec![c(&[4.0, 5.0, 6.0, 7.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0]), n(0.2)]), 3.777777778, 1e-9);
|
close_tol(ev("TRIMMEAN", vec![c(&[4.0, 5.0, 6.0, 7.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0]), n(0.2)]), 3.777777778, 1e-9);
|
||||||
is_err(ev("TRIMMEAN", vec![c(&[1.0]), n(1.0)]), CellError::Num);
|
is_err(ev("TRIMMEAN", vec![c(&[1.0]), n(1.0)]), CellError::Num);
|
||||||
@@ -227,19 +214,10 @@ fn percentiles() {
|
|||||||
close(ev("PERCENTRANK.EXC", vec![e9.clone(), n(5.43)]), 0.381);
|
close(ev("PERCENTRANK.EXC", vec![e9.clone(), n(5.43)]), 0.381);
|
||||||
close(ev("PERCENTRANK.EXC", vec![e9, n(5.43), n(1.0)]), 0.3);
|
close(ev("PERCENTRANK.EXC", vec![e9, n(5.43), n(1.0)]), 0.3);
|
||||||
let t = c(&[1.0, 2.0, 3.0]);
|
let t = c(&[1.0, 2.0, 3.0]);
|
||||||
// Excel 16: significance 309 and up is #DIV/0!, 308 still works.
|
close(ev("PERCENTRANK", vec![t.clone(), n(2.0), n(400.0)]), 0.5);
|
||||||
for name in ["PERCENTRANK", "PERCENTRANK.INC", "PERCENTRANK.EXC"] {
|
close(ev("PERCENTRANK.INC", vec![t.clone(), n(2.0), n(400.0)]), 0.5);
|
||||||
is_err(ev(name, vec![t.clone(), n(2.0), n(309.0)]), CellError::Div0);
|
close(ev("PERCENTRANK.INC", vec![t.clone(), n(1.0), n(400.0)]), 0.0);
|
||||||
is_err(ev(name, vec![t.clone(), n(1.0), n(400.0)]), CellError::Div0);
|
close(ev("PERCENTRANK.EXC", vec![t, n(2.0), n(400.0)]), 0.5);
|
||||||
close(ev(name, vec![t.clone(), n(2.0), n(308.0)]), 0.5);
|
|
||||||
}
|
|
||||||
// From significance 15 up the result has 15 significant digits.
|
|
||||||
let k = c(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
|
|
||||||
for sig in [15.0, 16.0, 308.0] {
|
|
||||||
close_tol(ev("PERCENTRANK.INC", vec![k.clone(), n(2.123_456_789), n(sig)]), 0.187_242_798_166_667, 1e-16);
|
|
||||||
}
|
|
||||||
close_tol(ev("PERCENTRANK.INC", vec![k.clone(), n(2.123_456_789), n(14.0)]), 0.187_242_798_166_66, 1e-15);
|
|
||||||
close(ev("PERCENTRANK.EXC", vec![k, n(2.123_456_789), n(16.0)]), 0.265_432_098_625);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -382,7 +360,6 @@ fn normal() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_chi_f() {
|
fn t_chi_f() {
|
||||||
close_tol(ev("T.DIST", vec![n(0.0), n(6e307), b(false)]), 0.3989422804, 1e-10);
|
|
||||||
close_tol(ev("T.DIST", vec![n(60.0), n(1.0), b(true)]), 0.99469533, D7);
|
close_tol(ev("T.DIST", vec![n(60.0), n(1.0), b(true)]), 0.99469533, D7);
|
||||||
close_tol(ev("T.DIST", vec![n(8.0), n(3.0), b(false)]), 0.00073691, 1e-5);
|
close_tol(ev("T.DIST", vec![n(8.0), n(3.0), b(false)]), 0.00073691, 1e-5);
|
||||||
close_tol(ev("T.DIST.2T", vec![n(1.959999998), n(60.0)]), 0.054644930, D7);
|
close_tol(ev("T.DIST.2T", vec![n(1.959999998), n(60.0)]), 0.054644930, D7);
|
||||||
@@ -420,7 +397,6 @@ fn t_chi_f() {
|
|||||||
close_tol(ev("F.DIST.RT", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
close_tol(ev("F.DIST.RT", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
||||||
close_tol(ev("FDIST", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
close_tol(ev("FDIST", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
||||||
close_tol(ev("F.INV", vec![n(0.01), n(6.0), n(4.0)]), 0.10930991, D7);
|
close_tol(ev("F.INV", vec![n(0.01), n(6.0), n(4.0)]), 0.10930991, D7);
|
||||||
close_tol(ev("F.INV", vec![n(0.99), n(6.0), n(4.0)]), 15.20686, D7);
|
|
||||||
close_tol(ev("F.INV.RT", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
close_tol(ev("F.INV.RT", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
||||||
close_tol(ev("FINV", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
close_tol(ev("FINV", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
||||||
close_tol(ev("F.TEST", vec![c(&[6.0, 7.0, 9.0, 15.0, 21.0]), c(&[20.0, 28.0, 31.0, 38.0, 40.0])]), 0.64831785, D7);
|
close_tol(ev("F.TEST", vec![c(&[6.0, 7.0, 9.0, 15.0, 21.0]), c(&[20.0, 28.0, 31.0, 38.0, 40.0])]), 0.64831785, D7);
|
||||||
@@ -428,48 +404,6 @@ fn t_chi_f() {
|
|||||||
is_err(ev("F.DIST", vec![n(1.0), n(0.0), n(4.0), b(true)]), CellError::Num);
|
is_err(ev("F.DIST", vec![n(1.0), n(0.0), n(4.0), b(true)]), CellError::Num);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn f_distributions_accept_ten_billion_degrees() {
|
|
||||||
close_tol(ev("F.DIST", vec![n(0.0), n(10_000_000_000.0), n(1.0), b(false)]), 0.0, 0.0);
|
|
||||||
close_tol(ev("F.DIST.RT", vec![n(0.0), n(1.0), n(10_000_000_000.0)]), 1.0, 0.0);
|
|
||||||
close_tol(ev("F.DIST", vec![n(0.01), n(1_000_000_000.0), n(1.0), b(true)]), 1.52397e-23, 1e-28);
|
|
||||||
close_tol(ev("F.DIST", vec![n(1.0), n(10_000_000_000.0), n(10_000_000_000.0), b(true)]), 0.5, 0.0);
|
|
||||||
close_tol(ev("F.DIST", vec![n(1_000_000.0), n(10_000_000_000.0), n(1.0), b(true)]), 0.999202115, 1e-8);
|
|
||||||
close_tol(ev("F.INV", vec![n(0.5), n(10_000_000_000.0), n(1.0)]), 2.198110953542527, 2e-6);
|
|
||||||
close_tol(ev("F.DIST.RT", vec![n(0.0), n(9_999_999_999.0), n(1.0)]), 1.0, 0.0);
|
|
||||||
is_err(ev("F.DIST.RT", vec![n(0.0), n(10_000_000_001.0), n(1.0)]), CellError::Num);
|
|
||||||
is_err(ev("F.DIST.RT", vec![n(0.0), n(1.0), n(10_000_000_001.0)]), CellError::Num);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn t_dist_zero_degrees_error_depends_on_mode() {
|
|
||||||
for df in [0.0, 0.9, -0.1] {
|
|
||||||
is_err(ev("T.DIST", vec![n(0.0), n(df), b(false)]), CellError::Div0);
|
|
||||||
is_err(ev("T.DIST", vec![n(0.0), n(df), b(true)]), CellError::Num);
|
|
||||||
}
|
|
||||||
is_err(ev("T.DIST", vec![n(0.0), n(-1.0), b(false)]), CellError::Num);
|
|
||||||
close_tol(ev("T.DIST", vec![n(0.0), n(1.99), b(false)]), 1.0 / std::f64::consts::PI, 1e-15);
|
|
||||||
close_tol(ev("T.DIST", vec![n(0.0), n(1.0), b(false)]), 1.0 / std::f64::consts::PI, 1e-15);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn t_density_is_stable_for_large_degrees() {
|
|
||||||
close_tol(ev("T.DIST", vec![n(0.0), n(100_000_000.0), b(false)]), 0.398942279404077, 1e-12);
|
|
||||||
close_tol(ev("T.DIST", vec![n(0.0), n(10_000_000_000.0), b(false)]), 0.3989422803914591, 1e-12);
|
|
||||||
close_tol(ev("T.DIST", vec![n(1.0), n(10_000_000_000_000_000.0), b(false)]), 0.24197072451914337, 1e-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn t_inv_zero_probability_returns_div0() {
|
|
||||||
is_err(ev("T.INV", vec![n(-0.1), n(1.0)]), CellError::Num);
|
|
||||||
is_err(ev("T.INV", vec![n(0.0), n(1.0)]), CellError::Div0);
|
|
||||||
close_tol(ev("T.INV", vec![n(0.75), n(2.0)]), 0.8164966, D7);
|
|
||||||
is_err(ev("T.INV", vec![n(1.0), n(1.0)]), CellError::Num);
|
|
||||||
is_err(ev("T.INV", vec![n(1.1), n(1.0)]), CellError::Num);
|
|
||||||
is_err(ev("T.INV", vec![n(0.0), n(0.0)]), CellError::Num);
|
|
||||||
is_err(ev("T.INV.2T", vec![n(0.0), n(1.0)]), CellError::Num);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn binom_inv_huge_trials_returns() {
|
fn binom_inv_huge_trials_returns() {
|
||||||
// Above 2^53 neighbouring f64 values are more than 1 apart; the bisection must still end.
|
// Above 2^53 neighbouring f64 values are more than 1 apart; the bisection must still end.
|
||||||
@@ -493,15 +427,7 @@ fn discrete() {
|
|||||||
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
||||||
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(true)]), 0.828125, 1e-12);
|
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(true)]), 0.828125, 1e-12);
|
||||||
close_tol(ev("BINOMDIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
close_tol(ev("BINOMDIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
||||||
close(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_646.0), n(0.0), b(false)]), 0.0);
|
|
||||||
close(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_646.9), n(0.0), b(false)]), 0.0);
|
|
||||||
is_err(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_647.0), n(0.0), b(false)]), CellError::Num);
|
|
||||||
is_err(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_647.9), n(0.0), b(false)]), CellError::Num);
|
|
||||||
is_err(ev("BINOM.DIST", vec![n(11.0), n(10.0), n(0.5), b(false)]), CellError::Num);
|
is_err(ev("BINOM.DIST", vec![n(11.0), n(10.0), n(0.5), b(false)]), CellError::Num);
|
||||||
close(ev("BINOM.DIST.RANGE", vec![n(2_147_483_646.0), n(0.0), n(1.0)]), 0.0);
|
|
||||||
close(ev("BINOM.DIST.RANGE", vec![n(2_147_483_646.9), n(0.0), n(1.0)]), 0.0);
|
|
||||||
is_err(ev("BINOM.DIST.RANGE", vec![n(2_147_483_647.0), n(0.0), n(1.0)]), CellError::Num);
|
|
||||||
is_err(ev("BINOM.DIST.RANGE", vec![n(2_147_483_647.9), n(0.0), n(1.0)]), CellError::Num);
|
|
||||||
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(48.0)]), 0.083974967, 1e-6);
|
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(48.0)]), 0.083974967, 1e-6);
|
||||||
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(45.0), n(50.0)]), 0.523629793, 1e-6);
|
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(45.0), n(50.0)]), 0.523629793, 1e-6);
|
||||||
close(ev("BINOM.INV", vec![n(6.0), n(0.5), n(0.75)]), 4.0);
|
close(ev("BINOM.INV", vec![n(6.0), n(0.5), n(0.75)]), 4.0);
|
||||||
@@ -540,19 +466,6 @@ fn continuous() {
|
|||||||
is_err(ev("GAMMA", vec![n(0.0)]), CellError::Num);
|
is_err(ev("GAMMA", vec![n(0.0)]), CellError::Num);
|
||||||
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(true), n(1.0), n(3.0)]), 0.6854706, D7);
|
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(true), n(1.0), n(3.0)]), 0.6854706, D7);
|
||||||
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(false), n(1.0), n(3.0)]), 1.4837646, D7);
|
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(false), n(1.0), n(3.0)]), 1.4837646, D7);
|
||||||
is_err(ev("BETA.DIST", vec![n(0.0), n(1.0), n(1.0), b(false)]), CellError::Num);
|
|
||||||
is_err(ev("BETA.DIST", vec![n(1.0), n(1.0), n(1.0), b(false)]), CellError::Num);
|
|
||||||
is_err(ev("BETA.DIST", vec![n(0.0), n(0.5), n(2.0), b(false)]), CellError::Num);
|
|
||||||
is_err(ev("BETA.DIST", vec![n(1.0), n(2.0), n(0.5), b(false)]), CellError::Num);
|
|
||||||
close(ev("BETA.DIST", vec![n(0.0), n(2.0), n(1.0), b(false)]), 0.0);
|
|
||||||
close(ev("BETA.DIST", vec![n(1.0), n(1.0), n(2.0), b(false)]), 0.0);
|
|
||||||
close(ev("BETA.DIST", vec![n(0.0), n(1.0), n(1.0), b(true)]), 0.0);
|
|
||||||
close(ev("BETA.DIST", vec![n(1.0), n(1.0), n(1.0), b(true)]), 1.0);
|
|
||||||
is_err(ev("BETA.DIST", vec![n(2.0), n(1.0), n(2.0), b(false), n(2.0), n(6.0)]), CellError::Num);
|
|
||||||
is_err(ev("BETA.DIST", vec![n(6.0), n(2.0), n(1.0), b(false), n(2.0), n(6.0)]), CellError::Num);
|
|
||||||
close(ev("BETA.DIST", vec![n(4.0), n(1.0), n(1.0), b(false), n(2.0), n(6.0)]), 0.25);
|
|
||||||
close(ev("BETA.DIST", vec![n(2.0), n(1.0), n(1.0), b(true), n(2.0), n(6.0)]), 0.0);
|
|
||||||
close(ev("BETA.DIST", vec![n(6.0), n(1.0), n(1.0), b(true), n(2.0), n(6.0)]), 1.0);
|
|
||||||
close_tol(ev("BETADIST", vec![n(2.0), n(8.0), n(10.0), n(1.0), n(3.0)]), 0.6854706, D7);
|
close_tol(ev("BETADIST", vec![n(2.0), n(8.0), n(10.0), n(1.0), n(3.0)]), 0.6854706, D7);
|
||||||
close_tol(ev("BETA.INV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
close_tol(ev("BETA.INV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
||||||
close_tol(ev("BETAINV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
close_tol(ev("BETAINV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
||||||
@@ -566,30 +479,6 @@ fn continuous() {
|
|||||||
close_tol(r[0][1].clone(), 0.841344746, 1e-8);
|
close_tol(r[0][1].clone(), 0.841344746, 1e-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn gamma_density_at_zero_shape_one_is_num() {
|
|
||||||
for scale in [0.1, 0.9, 1.0, 1.9, 2.0, 10_000_000_000.0] {
|
|
||||||
is_err(ev("GAMMA.DIST", vec![n(0.0), n(1.0), n(scale), b(false)]), CellError::Num);
|
|
||||||
}
|
|
||||||
is_err(ev("GAMMA.DIST", vec![n(0.0), n(0.5), n(1.0), b(false)]), CellError::Num);
|
|
||||||
close_tol(ev("GAMMA.DIST", vec![n(0.0), n(2.0), n(1.0), b(false)]), 0.0, 0.0);
|
|
||||||
close_tol(ev("GAMMA.DIST", vec![n(0.0), n(1.0), n(2.0), b(true)]), 0.0, 0.0);
|
|
||||||
close_tol(ev("GAMMA.DIST", vec![n(2.0), n(1.0), n(2.0), b(false)]), 0.5 * (-1.0f64).exp(), 1e-15);
|
|
||||||
close_tol(ev("GAMMA.DIST", vec![n(2.0), n(1.0), n(2.0), b(true)]), 1.0 - (-1.0f64).exp(), 1e-15);
|
|
||||||
is_err(ev("GAMMA.DIST", vec![n(-0.1), n(1.0), n(2.0), b(false)]), CellError::Num);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn weibull_density_at_zero_is_zero() {
|
|
||||||
for shape in [0.5, 1.0, 2.0] {
|
|
||||||
close_tol(ev("WEIBULL.DIST", vec![n(0.0), n(shape), n(1.0), b(false)]), 0.0, 0.0);
|
|
||||||
close_tol(ev("WEIBULL.DIST", vec![n(0.0), n(shape), n(1.0), b(true)]), 0.0, 0.0);
|
|
||||||
}
|
|
||||||
close_tol(ev("WEIBULL.DIST", vec![n(1.0), n(1.0), n(1.0), b(false)]), (-1.0f64).exp(), 1e-15);
|
|
||||||
close_tol(ev("WEIBULL.DIST", vec![n(1.0), n(1.0), n(1.0), b(true)]), 1.0 - (-1.0f64).exp(), 1e-15);
|
|
||||||
is_err(ev("WEIBULL.DIST", vec![n(-0.1), n(1.0), n(1.0), b(false)]), CellError::Num);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aggregate_helper() {
|
fn aggregate_helper() {
|
||||||
let vals = vec![nv(1.0), nv(2.0), tv("x"), Value::Empty, Value::Bool(true), nv(6.0)];
|
let vals = vec![nv(1.0), nv(2.0), tv("x"), Value::Empty, Value::Bool(true), nv(6.0)];
|
||||||
|
|||||||
+12
-113
@@ -111,31 +111,12 @@ fn len(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
num_val(text(a, 0)?.chars().count() as f64)
|
num_val(text(a, 0)?.chars().count() as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lower_char(ch: char) -> char {
|
|
||||||
match ch.to_lowercase().next() {
|
|
||||||
Some(lower) => lower,
|
|
||||||
None => ch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upper_char(ch: char) -> char {
|
|
||||||
let mut uppercase = ch.to_uppercase();
|
|
||||||
match uppercase.next() {
|
|
||||||
Some(upper) if uppercase.next().is_none() => upper,
|
|
||||||
_ => ch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn proper_upper_char(ch: char) -> char {
|
|
||||||
if ch == '\u{130}' { 'I' } else { upper_char(ch) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lower(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn lower(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
text_val(text(a, 0)?.chars().map(lower_char).collect::<String>())
|
text_val(text(a, 0)?.to_lowercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn upper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
text_val(text(a, 0)?.chars().map(upper_char).collect::<String>())
|
text_val(text(a, 0)?.to_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
@@ -145,9 +126,9 @@ fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
if ch.is_alphabetic() {
|
if ch.is_alphabetic() {
|
||||||
if prev_letter {
|
if prev_letter {
|
||||||
out.push(lower_char(ch));
|
out.extend(ch.to_lowercase());
|
||||||
} else {
|
} else {
|
||||||
out.push(proper_upper_char(ch));
|
out.extend(ch.to_uppercase());
|
||||||
}
|
}
|
||||||
prev_letter = true;
|
prev_letter = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -300,11 +281,7 @@ fn unichar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
if !(1.0..=1_114_111.0).contains(&n) {
|
if !(1.0..=1_114_111.0).contains(&n) {
|
||||||
return Err(CellError::Value);
|
return Err(CellError::Value);
|
||||||
}
|
}
|
||||||
let cp = n as u32;
|
match char::from_u32(n as u32) {
|
||||||
if (0xFDD0..=0xFDEF).contains(&cp) || cp & 0xFFFE == 0xFFFE {
|
|
||||||
return Err(CellError::NA);
|
|
||||||
}
|
|
||||||
match char::from_u32(cp) {
|
|
||||||
Some(c) => text_val(c.to_string()),
|
Some(c) => text_val(c.to_string()),
|
||||||
None => Err(CellError::NA),
|
None => Err(CellError::NA),
|
||||||
}
|
}
|
||||||
@@ -373,10 +350,10 @@ fn numbervalue(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
|||||||
/// Formats `x` (already rounded) with `decimals` places and optional thousands separators.
|
/// Formats `x` (already rounded) with `decimals` places and optional thousands separators.
|
||||||
fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
|
fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
|
||||||
let d = decimals.max(0) as usize;
|
let d = decimals.max(0) as usize;
|
||||||
let s = x.abs().to_string();
|
let s = format!("{:.*}", d, x.abs());
|
||||||
let (int_part, frac) = match s.split_once('.') {
|
let (int_part, frac) = match s.split_once('.') {
|
||||||
Some((i, f)) => (i, f),
|
Some((i, f)) => (i.to_string(), Some(f.to_string())),
|
||||||
None => (s.as_str(), ""),
|
None => (s.clone(), None),
|
||||||
};
|
};
|
||||||
let int_part = if commas {
|
let int_part = if commas {
|
||||||
let b = int_part.as_bytes();
|
let b = int_part.as_bytes();
|
||||||
@@ -389,14 +366,12 @@ fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
|
|||||||
}
|
}
|
||||||
o
|
o
|
||||||
} else {
|
} else {
|
||||||
int_part.to_string()
|
int_part
|
||||||
};
|
};
|
||||||
let mut out = int_part;
|
let mut out = int_part;
|
||||||
if d > 0 {
|
if let Some(f) = frac {
|
||||||
let frac_len = frac.len().min(d);
|
|
||||||
out.push('.');
|
out.push('.');
|
||||||
out.push_str(&frac[..frac_len]);
|
out.push_str(&f);
|
||||||
out.extend(std::iter::repeat_n('0', d - frac_len));
|
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -419,21 +394,13 @@ fn fixed(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
let (r, d) = rounded(a, 2.0)?;
|
let (r, d) = rounded(a, 2.0)?;
|
||||||
let no_commas = opt_bool(a, 2, false)?;
|
let no_commas = opt_bool(a, 2, false)?;
|
||||||
let body = format_fixed(r, d, !no_commas);
|
let body = format_fixed(r, d, !no_commas);
|
||||||
if body.len() + if r < 0.0 { 1 } else { 0 } > 255 {
|
|
||||||
return Err(CellError::Value);
|
|
||||||
}
|
|
||||||
text_val(if r < 0.0 { format!("-{body}") } else { body })
|
text_val(if r < 0.0 { format!("-{body}") } else { body })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dollar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn dollar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
let (r, d) = rounded(a, 2.0)?;
|
let (r, d) = rounded(a, 2.0)?;
|
||||||
let negative = r < 0.0 || (r == 0.0 && d >= 0 && num(a, 0)? < 0.0);
|
|
||||||
let body = format_fixed(r, d, true);
|
let body = format_fixed(r, d, true);
|
||||||
let out = if negative { format!("(${body})") } else { format!("${body}") };
|
text_val(if r < 0.0 { format!("(${body})") } else { format!("${body}") })
|
||||||
if out.len() > 254 {
|
|
||||||
return Err(CellError::Value);
|
|
||||||
}
|
|
||||||
text_val(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
@@ -1156,20 +1123,6 @@ mod tests {
|
|||||||
fn case_and_cleanup() {
|
fn case_and_cleanup() {
|
||||||
is_text(ev("UPPER", vec![t("abc")]), "ABC");
|
is_text(ev("UPPER", vec![t("abc")]), "ABC");
|
||||||
is_text(ev("LOWER", vec![t("ABC")]), "abc");
|
is_text(ev("LOWER", vec![t("ABC")]), "abc");
|
||||||
is_text(ev("UPPER", vec![t("Straße")]), "STRAßE");
|
|
||||||
is_text(ev("LOWER", vec![t("İ")]), "i");
|
|
||||||
is_text(ev("PROPER", vec![t("İSTANBUL")]), "Istanbul");
|
|
||||||
is_text(ev("PROPER", vec![t("ß")]), "ß");
|
|
||||||
is_text(ev("PROPER", vec![t("ÉCOLE")]), "École");
|
|
||||||
let proper = ev("PROPER", vec![t("İSTANBUL Straße")]);
|
|
||||||
is_text(proper.clone(), "Istanbul Straße");
|
|
||||||
assert_eq!(
|
|
||||||
match proper {
|
|
||||||
Value::Text(s) => s.chars().count(),
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
"Istanbul Straße".chars().count()
|
|
||||||
);
|
|
||||||
is_text(ev("PROPER", vec![t("this is a TITLE")]), "This Is A Title");
|
is_text(ev("PROPER", vec![t("this is a TITLE")]), "This Is A Title");
|
||||||
is_text(ev("PROPER", vec![t("2-way street's")]), "2-Way Street'S");
|
is_text(ev("PROPER", vec![t("2-way street's")]), "2-Way Street'S");
|
||||||
is_text(ev("TRIM", vec![t(" a b c ")]), "a b c");
|
is_text(ev("TRIM", vec![t(" a b c ")]), "a b c");
|
||||||
@@ -1228,15 +1181,8 @@ mod tests {
|
|||||||
close(ev("CODE", vec![t("€")]), 128.0);
|
close(ev("CODE", vec![t("€")]), 128.0);
|
||||||
is_err(ev("CODE", vec![t("")]), CellError::Value);
|
is_err(ev("CODE", vec![t("")]), CellError::Value);
|
||||||
is_text(ev("UNICHAR", vec![n(9731.0)]), "☃");
|
is_text(ev("UNICHAR", vec![n(9731.0)]), "☃");
|
||||||
for cp in [0xFDD0, 0xFDEF, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x10FFFE, 0x10FFFF] {
|
|
||||||
is_err(ev("UNICHAR", vec![n(f64::from(cp))]), CellError::NA);
|
|
||||||
}
|
|
||||||
for (cp, expected) in [(0xFDCF, "\u{FDCF}"), (0xFDF0, "\u{FDF0}"), (0xFFFD, "\u{FFFD}"), (0x1FFFD, "\u{1FFFD}")] {
|
|
||||||
is_text(ev("UNICHAR", vec![n(f64::from(cp))]), expected);
|
|
||||||
}
|
|
||||||
close(ev("UNICODE", vec![t("☃")]), 9731.0);
|
close(ev("UNICODE", vec![t("☃")]), 9731.0);
|
||||||
is_err(ev("UNICHAR", vec![n(0.0)]), CellError::Value);
|
is_err(ev("UNICHAR", vec![n(0.0)]), CellError::Value);
|
||||||
is_err(ev("UNICHAR", vec![n(1_114_112.0)]), CellError::Value);
|
|
||||||
is_err(ev("UNICHAR", vec![n(55296.0)]), CellError::NA);
|
is_err(ev("UNICHAR", vec![n(55296.0)]), CellError::NA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1291,57 +1237,10 @@ mod tests {
|
|||||||
is_text(ev("DOLLAR", vec![n(0.5), n(0.0)]), "$1");
|
is_text(ev("DOLLAR", vec![n(0.5), n(0.0)]), "$1");
|
||||||
is_text(ev("USDOLLAR", vec![n(1.0)]), "$1.00");
|
is_text(ev("USDOLLAR", vec![n(1.0)]), "$1.00");
|
||||||
is_text(ev("USDOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
|
is_text(ev("USDOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
|
||||||
let fixed_16 = format!("1.1{}", "0".repeat(15));
|
|
||||||
let dollar_16 = format!("$1.1{}", "0".repeat(15));
|
|
||||||
let fixed_20 = format!("1.005{}", "0".repeat(17));
|
|
||||||
let dollar_20 = format!("$1.005{}", "0".repeat(17));
|
|
||||||
let fixed_127 = format!("1.1{}", "0".repeat(126));
|
|
||||||
let dollar_127 = format!("$1.1{}", "0".repeat(126));
|
|
||||||
is_text(ev("FIXED", vec![n(1.1), n(16.0)]), &fixed_16);
|
|
||||||
is_text(ev("DOLLAR", vec![n(1.1), n(16.0)]), &dollar_16);
|
|
||||||
is_text(ev("FIXED", vec![n(1.005), n(20.0)]), &fixed_20);
|
|
||||||
is_text(ev("DOLLAR", vec![n(1.005), n(20.0)]), &dollar_20);
|
|
||||||
is_text(ev("FIXED", vec![n(1.1), n(127.0)]), &fixed_127);
|
|
||||||
is_text(ev("DOLLAR", vec![n(1.1), n(127.0)]), &dollar_127);
|
|
||||||
is_text(ev("T", vec![t("x")]), "x");
|
is_text(ev("T", vec![t("x")]), "x");
|
||||||
is_text(ev("T", vec![n(1.0)]), "");
|
is_text(ev("T", vec![n(1.0)]), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fixed_and_dollar_output_limits() {
|
|
||||||
assert!(matches!(ev("FIXED", vec![n(1e191), n(0.0)]), Value::Text(ref s) if s.len() == 255));
|
|
||||||
is_err(ev("FIXED", vec![n(1e192), n(0.0)]), CellError::Value);
|
|
||||||
assert!(matches!(ev("FIXED", vec![n(-1e253), n(0.0), b(true)]), Value::Text(ref s) if s.starts_with('-')));
|
|
||||||
is_err(ev("FIXED", vec![n(-1e191), n(0.0)]), CellError::Value);
|
|
||||||
assert!(matches!(ev("FIXED", vec![n(1e189), n(1.0)]), Value::Text(ref s) if s.len() == 255));
|
|
||||||
is_err(ev("FIXED", vec![n(1e190), n(1.0)]), CellError::Value);
|
|
||||||
|
|
||||||
assert!(matches!(ev("DOLLAR", vec![n(1e189), n(0.0)]), Value::Text(ref s) if s.len() == 254));
|
|
||||||
is_err(ev("DOLLAR", vec![n(1e190), n(0.0)]), CellError::Value);
|
|
||||||
assert!(matches!(ev("DOLLAR", vec![n(-1e188), n(0.0)]), Value::Text(ref s) if s.len() == 254));
|
|
||||||
is_err(ev("DOLLAR", vec![n(-1e189), n(0.0)]), CellError::Value);
|
|
||||||
assert!(matches!(ev("DOLLAR", vec![n(1.23456789e187), n(2.0)]), Value::Text(ref s) if s.len() == 254));
|
|
||||||
is_err(ev("DOLLAR", vec![n(1.23456789e188), n(2.0)]), CellError::Value);
|
|
||||||
is_err(ev("DOLLAR", vec![n(-1.23456789e186), n(2.0)]), CellError::Value);
|
|
||||||
assert!(matches!(ev("DOLLAR", vec![n(1e189), n(-1.0)]), Value::Text(ref s) if s.len() == 254));
|
|
||||||
is_err(ev("DOLLAR", vec![n(1e190), n(-1.0)]), CellError::Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dollar_keeps_negative_sign_when_nonnegative_precision_rounds_to_zero() {
|
|
||||||
is_text(ev("DOLLAR", vec![n(-0.0001), n(0.0)]), "($0)");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-0.0001), n(2.0)]), "($0.00)");
|
|
||||||
is_text(ev("DOLLAR", vec![n(0.0), n(0.0)]), "$0");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-0.0), n(0.0)]), "$0");
|
|
||||||
is_text(ev("DOLLAR", vec![n(0.0001), n(0.0)]), "$0");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-0.0001), n(-1.0)]), "$0");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-4.0), n(-1.0)]), "$0");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-5.0), n(-1.0)]), "($10)");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-0.4), n(0.0)]), "($0)");
|
|
||||||
is_text(ev("DOLLAR", vec![n(-0.5), n(0.0)]), "($1)");
|
|
||||||
is_text(ev("FIXED", vec![n(-0.0001), n(0.0)]), "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn before_after() {
|
fn before_after() {
|
||||||
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" ")]), "Red");
|
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" ")]), "Red");
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# The shared capture slot (`craft_ui::capture`) comes from the one craft-libs revision every
|
# The shared capture slot (`craft_ui::capture`) comes from the one craft-libs revision every
|
||||||
# craft-libs dependency uses (see the root Cargo.toml).
|
# craft-libs dependency uses (see the root Cargo.toml).
|
||||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" }
|
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||||
gridcraft-engine = { workspace = true }
|
gridcraft-engine = { workspace = true }
|
||||||
gridcraft-chart = { workspace = true }
|
gridcraft-chart = { workspace = true }
|
||||||
# `monochrome_emoji_fonts` restores the Noto Emoji and emoji-icon faces that 0.37 moved behind
|
# `monochrome_emoji_fonts` restores the Noto Emoji and emoji-icon faces that 0.37 moved behind
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Bake the contributor credits into the binary as static Rust tables. Nothing is read at run time.
|
//! Bake the contributor credits into the binary as static Rust tables (craftrules
|
||||||
|
//! standards/contributors.md). Nothing is read at run time.
|
||||||
//!
|
//!
|
||||||
//! - contributors/contributors.json: commit stats per GitHub username, with a mark for whose
|
//! - contributors/contributors.json: commit stats per GitHub username, generated by craftrules
|
||||||
//! people.toml entry is verified.
|
//! scripts/contributors.py, which also marks whose people.toml entry it verified.
|
||||||
//! - contributors/people.toml: the names people chose for themselves. A name is used only when the
|
//! - contributors/people.toml: the names people chose for themselves. A name is used only when the
|
||||||
//! JSON says the entry was verified (`people_entry`).
|
//! JSON says the entry was verified (`people_entry`).
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! The About window's credits: every contributor (GitHub login always; display and real names only
|
//! The About window's credits: every contributor (GitHub login always; display and real names only
|
||||||
//! with recorded consent) and the AI models that helped. The tables are baked into the binary by
|
//! with recorded consent) and the AI models that helped. The tables are baked into the binary by
|
||||||
//! build.rs from contributors/contributors.json; nothing is read at run time.
|
//! build.rs from contributors/contributors.json (craftrules standards/contributors.md); nothing is
|
||||||
|
//! read at run time.
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use super::controls::{
|
|||||||
use super::galleries::{cell_style_gallery, table_gallery};
|
use super::galleries::{cell_style_gallery, table_gallery};
|
||||||
use super::{
|
use super::{
|
||||||
HAlign, Icon, Key, Modifiers, Rect, SheetApp, Style, Tokens, Ui, VAlign, big_button, color_palette, icon_button, json, keytip_table,
|
HAlign, Icon, Key, Modifiers, Rect, SheetApp, Style, Tokens, Ui, VAlign, big_button, color_palette, icon_button, json, keytip_table,
|
||||||
small_button, small_split_button, split_button, theme, toggle_button, vec2,
|
small_button, split_button, theme, toggle_button, vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The Accounting Number Format dropdown: label and the code Excel applies for that symbol.
|
/// The Accounting Number Format dropdown: label and the code Excel applies for that symbol.
|
||||||
@@ -292,11 +292,12 @@ pub(super) fn home(app: &mut SheetApp, ui: &mut Ui) {
|
|||||||
if __k.clicked() {
|
if __k.clicked() {
|
||||||
act(app, "home.increaseIndent", json!({}));
|
act(app, "home.increaseIndent", json!({}));
|
||||||
}
|
}
|
||||||
let (m, ma) = small_split_button(ui, Icon::Merge, "Merge & Center", "Merge & Center");
|
let (m, ma) = split_button(ui, Icon::Merge, None, "Merge & Center");
|
||||||
control_keytip(app, ui, &ma, "home.mergeCenter");
|
control_keytip(app, ui, &ma, "home.mergeCenter");
|
||||||
if m.clicked() {
|
if m {
|
||||||
act(app, "home.mergeCenter", json!({}));
|
act(app, "home.mergeCenter", json!({}));
|
||||||
}
|
}
|
||||||
|
ui.label(egui::RichText::new(app.ui.language.tr("Merge & Center")).font(theme::ui_font(12.5)));
|
||||||
egui::Popup::menu(&ma).show(|ui| {
|
egui::Popup::menu(&ma).show(|ui| {
|
||||||
menu_items(
|
menu_items(
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
//! offers the new release: Update and Restart (direct self-update), Download or Later. The flow
|
//! offers the new release: Update and Restart (direct self-update), Download or Later. The flow
|
||||||
//! itself is `craft_update::updater::Updater`.
|
//! itself is `craft_update::updater::Updater`.
|
||||||
|
|
||||||
use craft_update::CheckResultResponse;
|
|
||||||
use craft_update::updater::{Action, Status, Updater};
|
use craft_update::updater::{Action, Status, Updater};
|
||||||
|
|
||||||
use crate::SheetApp;
|
use crate::SheetApp;
|
||||||
@@ -15,13 +14,11 @@ pub(crate) struct Runtime {
|
|||||||
startup: bool,
|
startup: bool,
|
||||||
/// Help ▸ Check for Updates… was chosen; the next `tick` starts it.
|
/// Help ▸ Check for Updates… was chosen; the next `tick` starts it.
|
||||||
manual: bool,
|
manual: bool,
|
||||||
/// The result dialog of a requested check is showing.
|
|
||||||
result_open: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Runtime {
|
impl Default for Runtime {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { updater: Updater::new(&crate::UPDATE_APP, VERSION), startup: false, manual: false, result_open: false }
|
Self { updater: Updater::new(&crate::UPDATE_APP, VERSION), startup: false, manual: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,14 +35,19 @@ pub(crate) fn tick(app: &mut SheetApp, ctx: &egui::Context) {
|
|||||||
let rt = &mut app.updates_rt;
|
let rt = &mut app.updates_rt;
|
||||||
if std::mem::take(&mut rt.manual) {
|
if std::mem::take(&mut rt.manual) {
|
||||||
rt.updater.check_now(ctx);
|
rt.updater.check_now(ctx);
|
||||||
rt.result_open = true;
|
|
||||||
} else if rt.startup {
|
} else if rt.startup {
|
||||||
rt.updater.check_at_start(ctx);
|
rt.updater.check_at_start(ctx);
|
||||||
}
|
}
|
||||||
rt.updater.tick(ctx);
|
rt.updater.tick(ctx);
|
||||||
// A requested check that found a release leaves the status idle and the prompt opens.
|
let lang = app.ui.language;
|
||||||
if matches!(rt.updater.status(), Status::Idle) {
|
let outcome = match rt.updater.status() {
|
||||||
rt.result_open = false;
|
Status::UpToDate => Some(lang.tr("GridCraft {version} is up to date.").replace("{version}", VERSION)),
|
||||||
|
Status::Failed(e) => Some(lang.tr("Could not check for updates: {error}").replace("{error}", e)),
|
||||||
|
Status::Idle | Status::Checking => None,
|
||||||
|
};
|
||||||
|
if let Some(message) = outcome {
|
||||||
|
rt.updater.dismiss_status();
|
||||||
|
app.message = Some(("GridCraft".into(), message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +65,4 @@ pub(crate) fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
|||||||
Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
|
Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
|
||||||
Action::None => {}
|
Action::None => {}
|
||||||
}
|
}
|
||||||
if app.updates_rt.result_open {
|
|
||||||
let rt = &mut app.updates_rt;
|
|
||||||
match craft_update::CheckResultDialog::new(&crate::UPDATE_APP, VERSION).show(ctx, rt.updater.status()) {
|
|
||||||
CheckResultResponse::Close => {
|
|
||||||
rt.updater.dismiss_status();
|
|
||||||
rt.result_open = false;
|
|
||||||
}
|
|
||||||
CheckResultResponse::CheckAgain => rt.updater.check_now(ctx),
|
|
||||||
CheckResultResponse::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ mod keytips_generated;
|
|||||||
mod macro_editor;
|
mod macro_editor;
|
||||||
mod macros_ui;
|
mod macros_ui;
|
||||||
mod menu_hover;
|
mod menu_hover;
|
||||||
mod merge_center_label;
|
|
||||||
mod merged_cells;
|
mod merged_cells;
|
||||||
mod more_colors;
|
mod more_colors;
|
||||||
mod native_print;
|
mod native_print;
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
//! Home › Merge & Center is a split button: clicking its text label runs `home.mergeCenter`, like
|
|
||||||
//! its icon does.
|
|
||||||
|
|
||||||
use egui::{Event, Shape, vec2};
|
|
||||||
use gridcraft_engine::Session;
|
|
||||||
use gridcraft_ui_egui::SheetApp;
|
|
||||||
use gridcraft_ui_egui::i18n::Language;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clicking_the_label_merges_and_centers() {
|
|
||||||
for language in [Language::En, Language::De] {
|
|
||||||
click_label(language);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn click_label(language: Language) {
|
|
||||||
let label = language.tr("Merge & Center");
|
|
||||||
let mut s = Session::new();
|
|
||||||
s.new_workbook();
|
|
||||||
s.execute("selection.set", json!({"range": "A1:B1"})).unwrap();
|
|
||||||
let mut h = crate::common::builder().with_size(vec2(1400.0, 800.0)).build_ui_state(
|
|
||||||
|ui, app: &mut SheetApp| {
|
|
||||||
app.logic(ui.ctx());
|
|
||||||
app.ui(ui);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let mut app = crate::common::app(s);
|
|
||||||
app.ui.language = language;
|
|
||||||
app
|
|
||||||
},
|
|
||||||
);
|
|
||||||
h.run_steps(4);
|
|
||||||
let at = h
|
|
||||||
.output()
|
|
||||||
.shapes
|
|
||||||
.iter()
|
|
||||||
.find_map(|s| match &s.shape {
|
|
||||||
Shape::Text(t) if t.galley.job.text == label => Some(t.pos + t.galley.size() / 2.0),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| panic!("the Home tab shows {label}"));
|
|
||||||
h.input_mut().events.push(Event::PointerMoved(at));
|
|
||||||
h.run_steps(2);
|
|
||||||
for pressed in [true, false] {
|
|
||||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
|
|
||||||
h.run_steps(2);
|
|
||||||
}
|
|
||||||
h.run_steps(2);
|
|
||||||
assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeCenter"), "{label}");
|
|
||||||
}
|
|
||||||
@@ -151,7 +151,6 @@ const FUTURE_FUNCTIONS: &[&str] = &[
|
|||||||
"CSCH",
|
"CSCH",
|
||||||
"DAYS",
|
"DAYS",
|
||||||
"DECIMAL",
|
"DECIMAL",
|
||||||
"DETECTLANGUAGE",
|
|
||||||
"DROP",
|
"DROP",
|
||||||
"ENCODEURL",
|
"ENCODEURL",
|
||||||
"ERF.PRECISE",
|
"ERF.PRECISE",
|
||||||
@@ -244,7 +243,6 @@ const FUTURE_FUNCTIONS: &[&str] = &[
|
|||||||
"SORTBY",
|
"SORTBY",
|
||||||
"STDEV.P",
|
"STDEV.P",
|
||||||
"STDEV.S",
|
"STDEV.S",
|
||||||
"STOCKHISTORY",
|
|
||||||
"SWITCH",
|
"SWITCH",
|
||||||
"T.DIST",
|
"T.DIST",
|
||||||
"T.DIST.2T",
|
"T.DIST.2T",
|
||||||
@@ -259,7 +257,6 @@ const FUTURE_FUNCTIONS: &[&str] = &[
|
|||||||
"TEXTSPLIT",
|
"TEXTSPLIT",
|
||||||
"TOCOL",
|
"TOCOL",
|
||||||
"TOROW",
|
"TOROW",
|
||||||
"TRANSLATE",
|
|
||||||
"TRIMRANGE",
|
"TRIMRANGE",
|
||||||
"UNICHAR",
|
"UNICHAR",
|
||||||
"UNICODE",
|
"UNICODE",
|
||||||
@@ -355,13 +352,6 @@ mod tests {
|
|||||||
assert_eq!(file_function_name("FILTER").as_deref(), Some("_xlfn._xlws.FILTER"));
|
assert_eq!(file_function_name("FILTER").as_deref(), Some("_xlfn._xlws.FILTER"));
|
||||||
assert_eq!(file_function_name("PERCENTOF").as_deref(), Some("_xlfn.PERCENTOF"));
|
assert_eq!(file_function_name("PERCENTOF").as_deref(), Some("_xlfn.PERCENTOF"));
|
||||||
assert_eq!(file_function_name("TRIMRANGE").as_deref(), Some("_xlfn.TRIMRANGE"));
|
assert_eq!(file_function_name("TRIMRANGE").as_deref(), Some("_xlfn.TRIMRANGE"));
|
||||||
assert_eq!(file_function_name("stockhistory").as_deref(), Some("_xlfn.STOCKHISTORY"));
|
|
||||||
assert_eq!(file_function_name("TRANSLATE").as_deref(), Some("_xlfn.TRANSLATE"));
|
|
||||||
assert_eq!(file_function_name("DETECTLANGUAGE").as_deref(), Some("_xlfn.DETECTLANGUAGE"));
|
|
||||||
assert_eq!(file_function_name("NETWORKDAYS.INTL").as_deref(), Some("_xlfn.NETWORKDAYS.INTL"));
|
|
||||||
assert_eq!(file_function_name("WORKDAY.INTL").as_deref(), Some("_xlfn.WORKDAY.INTL"));
|
|
||||||
// Excel 16.0 build 20527 saves COPILOT as plain `COPILOT(...)` (it does not know the name yet).
|
|
||||||
assert_eq!(file_function_name("COPILOT"), None);
|
|
||||||
assert_eq!(file_function_name("SUM"), None);
|
assert_eq!(file_function_name("SUM"), None);
|
||||||
assert_eq!(builtin_format(14), Some("m/d/yyyy"));
|
assert_eq!(builtin_format(14), Some("m/d/yyyy"));
|
||||||
assert_eq!(builtin_id("0.00%"), Some(10));
|
assert_eq!(builtin_id("0.00%"), Some(10));
|
||||||
|
|||||||
+21
-4
@@ -1,14 +1,16 @@
|
|||||||
# Contributors in the About window
|
# Contributors in the About window
|
||||||
|
|
||||||
**About ▸ Contributors** credits everyone who contributed to GridCraft, and **About ▸ Models** credits
|
**About ▸ Contributors** credits everyone who contributed to GridCraft, and **About ▸ Models** credits
|
||||||
the AI models named in `Co-Authored-By` trailers.
|
the AI models named in `Co-Authored-By` trailers. This follows the shared craftrules standard
|
||||||
|
[`standards/contributors.md`](https://github.com/storytold/craftrules/blob/main/standards/contributors.md);
|
||||||
|
this page is the local copy of the decision.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
- Two files, both **compiled into the binary** by the UI crate's `build.rs` (nothing is read from
|
- Two files, both **compiled into the binary** by the UI crate's `build.rs` (nothing is read from
|
||||||
disk or the network at run time; the web build has them too):
|
disk or the network at run time; the web build has them too):
|
||||||
- [`contributors/contributors.json`](../contributors/contributors.json): commit stats per GitHub
|
- [`contributors/contributors.json`](../contributors/contributors.json): commit stats per GitHub
|
||||||
username. Never hand-edit.
|
username. **Generated** by `craftrules/scripts/contributors.py`; never hand-edit.
|
||||||
- [`contributors/people.toml`](../contributors/people.toml): the names people chose for
|
- [`contributors/people.toml`](../contributors/people.toml): the names people chose for
|
||||||
themselves. **Each contributor adds only their own entry**, in their own PR.
|
themselves. **Each contributor adds only their own entry**, in their own PR.
|
||||||
- Each contributor gets one line: GitHub username (always), display name (opt-in, or their synced
|
- Each contributor gets one line: GitHub username (always), display name (opt-in, or their synced
|
||||||
@@ -35,5 +37,20 @@ display_name = "Nickname"
|
|||||||
sync_github_name = true # use the name on your public GitHub profile as display name
|
sync_github_name = true # use the name on your public GitHub profile as display name
|
||||||
```
|
```
|
||||||
|
|
||||||
Names show for entries committed by their own user (or by a maintainer with a `consent_source`).
|
or, with craftrules checked out next to `craft-apps/`:
|
||||||
Delete your entry to remove your names. Git author names and emails are never recorded.
|
|
||||||
|
```sh
|
||||||
|
python3 ../../craftrules/scripts/contributors.py --add-me . --real-name "Your Name" --sync-github-name
|
||||||
|
```
|
||||||
|
|
||||||
|
Names show after the next refresh: the script checks that each entry was committed by its own
|
||||||
|
user (or by a maintainer with a `consent_source`). Delete your entry to remove your names.
|
||||||
|
|
||||||
|
## Refreshing (maintainers)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 ../../craftrules/scripts/contributors.py . # regenerate + verify; needs git, Python 3.11+, gh
|
||||||
|
python3 ../../craftrules/scripts/contributors.py --check . # verify people.toml only
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit the updated `contributors/contributors.json`. Git author names and emails are never recorded.
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
|
|||||||
|
|
||||||
| Work | Hours |
|
| Work | Hours |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Real-world XLSX corpus run, and confirm Excel opens our output with no repair prompt (owner runs Excel) | 15–25 |
|
| Real-world XLSX corpus run (craftrules test-corpora standard), and confirm Excel opens our output with no repair prompt (owner runs Excel) | 15–25 |
|
||||||
| Preserve the rest of the unmodelled XLSX content (slicers, timelines, data model, table query tables, `extLst` entries); pass-through landed for the rest (#330) | 6–10 |
|
| Preserve the rest of the unmodelled XLSX content (slicers, timelines, data model, table query tables, `extLst` entries); pass-through landed for the rest (#330) | 6–10 |
|
||||||
| Streaming XLSX reader for very large files (#175), part-size limits that degrade instead of failing | 10–15 |
|
| Streaming XLSX reader for very large files (#175), part-size limits that degrade instead of failing | 10–15 |
|
||||||
| Legacy `.xls` (BIFF8): styles (fonts, fills, borders, alignment), comments, CF, validation, pictures and charts on import; corpus run; then write | 10–15 |
|
| Legacy `.xls` (BIFF8): styles (fonts, fills, borders, alignment), comments, CF, validation, pictures and charts on import; corpus run; then write | 10–15 |
|
||||||
|
|||||||
+1
-28
@@ -1,6 +1,6 @@
|
|||||||
# Function parity: worksheet functions and formulas
|
# Function parity: worksheet functions and formulas
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (YEARFRAC basis 0 February-end rule, MOD quotient limit, PERCENTRANK significance; earlier: edge cases matched to Excel: base conversions, BITLSHIFT/BITRSHIFT, UNICHAR noncharacters, UPPER/LOWER/PROPER casing, FIXED/DOLLAR output, F/T/GAMMA/WEIBULL/BETA distributions, BINOM trial cap, HARMEAN; earlier: ECMA.CEILING listed with USDOLLAR as not on Microsoft's list; earlier: IMSQRT reproduces the stray real part Excel gives for negative reals) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-27 · **Change:** minor (ECMA.CEILING listed with USDOLLAR as not on Microsoft's list; earlier: IMSQRT reproduces the stray real part Excel gives for negative reals, and the other IM* differences from Excel are listed; earlier: the LAMBDA row lists the recursion limit and its `#NUM!`, and the error a defined name holding an error gives when called; earlier: number text with accounting parentheses coerces like Excel 365; INDEX with one index on a 2-D reference is #REF! as in Excel; earlier: SUBTOTAL and AGGREGATE cap the cells they visit, #2175) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||||
|
|
||||||
Excel's worksheet functions, category by category, against GridCraft's function library. Part of
|
Excel's worksheet functions, category by category, against GridCraft's function library. Part of
|
||||||
[target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md).
|
[target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md).
|
||||||
@@ -86,14 +86,11 @@ This improves series preparation; it does not change the model-fitting or season
|
|||||||
| Implicit intersection `@`, legacy CSE array formulas | done; legacy array range fill (PR #156) |
|
| Implicit intersection `@`, legacy CSE array formulas | done; legacy array range fill (PR #156) |
|
||||||
| Implicit intersection `@`, legacy CSE array formulas | done, the entered range filled (PR #156) |
|
| Implicit intersection `@`, legacy CSE array formulas | done, the entered range filled (PR #156) |
|
||||||
| Array lifting: an array in a scalar argument evaluates the function per element | done. The Analysis ToolPak functions (EOMONTH, EDATE, WORKDAY(.INTL), NETWORKDAYS(.INTL), YEARFRAC, WEEKNUM, the COUP*, PRICE*, YIELD*, ODD*, TBILL*, ACCRINT*, AMOR*, DISC, DURATION, CUMIPMT/CUMPRINC, EFFECT, NOMINAL, DOLLARDE/FR, XNPV/XIRR rate and guess, FVSCHEDULE principal, ISEVEN/ISODD, QUOTIENT, MROUND, FACTDOUBLE, SQRTPI, RANDBETWEEN, SERIESSUM, the base conversions, CONVERT, DELTA, GESTEP, ERF/ERFC, BESSEL*, COMPLEX and IM* except IMSUM/IMPRODUCT) give `#VALUE!` for a reference to more than one cell in a value argument, as Excel 365 does in both dynamic-array and legacy formulas; array constants and computed arrays (`A1:A3+0`) still lift, and range arguments (holidays, schedule, cash flows) and the .INTL weekend accept ranges. Difference: BYROW/BYCOL pass their row or column to the LAMBDA as values, so `BYROW(A1:B3,LAMBDA(r,EOMONTH(r,0)))` lifts where Excel gives `#VALUE!` |
|
| Array lifting: an array in a scalar argument evaluates the function per element | done. The Analysis ToolPak functions (EOMONTH, EDATE, WORKDAY(.INTL), NETWORKDAYS(.INTL), YEARFRAC, WEEKNUM, the COUP*, PRICE*, YIELD*, ODD*, TBILL*, ACCRINT*, AMOR*, DISC, DURATION, CUMIPMT/CUMPRINC, EFFECT, NOMINAL, DOLLARDE/FR, XNPV/XIRR rate and guess, FVSCHEDULE principal, ISEVEN/ISODD, QUOTIENT, MROUND, FACTDOUBLE, SQRTPI, RANDBETWEEN, SERIESSUM, the base conversions, CONVERT, DELTA, GESTEP, ERF/ERFC, BESSEL*, COMPLEX and IM* except IMSUM/IMPRODUCT) give `#VALUE!` for a reference to more than one cell in a value argument, as Excel 365 does in both dynamic-array and legacy formulas; array constants and computed arrays (`A1:A3+0`) still lift, and range arguments (holidays, schedule, cash flows) and the .INTL weekend accept ranges. Difference: BYROW/BYCOL pass their row or column to the LAMBDA as values, so `BYROW(A1:B3,LAMBDA(r,EOMONTH(r,0)))` lifts where Excel gives `#VALUE!` |
|
||||||
| YEARFRAC basis 0, February-end start to day 31 | done: Excel 16.0 returns 31/360 for `YEARFRAC(2021-02-28,2021-03-31,0)` (0.08611111111111111); regression also covers leap day and reversed dates |
|
|
||||||
| Trim references `A1:.E10` (trailing), `A1.:E10` (leading), `A1.:.E10` (both), also on whole rows/columns (`A:.A`) | done; evaluated as `TRIMRANGE(range, code, code)`, adjusted on insert/delete; files store `_xlfn.TRIMRANGE(range,code,code)` (Excel's own file spelling unconfirmed in public docs), `_xlfn._TRO_ALL/_TRO_LEADING/_TRO_TRAILING(...)` read too |
|
| Trim references `A1:.E10` (trailing), `A1.:E10` (leading), `A1.:.E10` (both), also on whole rows/columns (`A:.A`) | done; evaluated as `TRIMRANGE(range, code, code)`, adjusted on insert/delete; files store `_xlfn.TRIMRANGE(range,code,code)` (Excel's own file spelling unconfirmed in public docs), `_xlfn._TRO_ALL/_TRO_LEADING/_TRO_TRAILING(...)` read too |
|
||||||
| LET, LAMBDA, named LAMBDAs, ISOMITTED, MAP/REDUCE/SCAN/BYROW/BYCOL/MAKEARRAY | done. A LAMBDA that calls itself more than 140 times, or a formula nested past the depth limit, gives `#NUM!` (the target app allows deeper recursion, #1581). Calling a defined name that holds an error value gives that error, not `#NAME?` |
|
| LET, LAMBDA, named LAMBDAs, ISOMITTED, MAP/REDUCE/SCAN/BYROW/BYCOL/MAKEARRAY | done. A LAMBDA that calls itself more than 140 times, or a formula nested past the depth limit, gives `#NUM!` (the target app allows deeper recursion, #1581). Calling a defined name that holds an error value gives that error, not `#NAME?` |
|
||||||
| Iterative calculation for circular references | done: with Enable Iterative Calculation on (`formulas.calculationOptions` `{iterative, maxIterations, maxChange}`, saved as `<calcPr iterate iterateCount iterateDelta>`), loops are evaluated again from their previous values (blank = 0) until no cell on a loop changes by Maximum Change or Maximum Iterations is reached; off, cycles give `#CIRC!`. Set in File › Options › Formulas (Enable iterative calculation, Maximum Iterations 1 to 32767, Maximum Change 0 or more; the two fields are disabled while iteration is off). Differences: GridCraft recalculates on open, so a loop that never settles (`=A1+1`) advances again each time the file is opened
|
| Iterative calculation for circular references | done: with Enable Iterative Calculation on (`formulas.calculationOptions` `{iterative, maxIterations, maxChange}`, saved as `<calcPr iterate iterateCount iterateDelta>`), loops are evaluated again from their previous values (blank = 0) until no cell on a loop changes by Maximum Change or Maximum Iterations is reached; off, cycles give `#CIRC!`. Set in File › Options › Formulas (Enable iterative calculation, Maximum Iterations 1 to 32767, Maximum Change 0 or more; the two fields are disabled while iteration is off). Differences: GridCraft recalculates on open, so a loop that never settles (`=A1+1`) advances again each time the file is opened
|
||||||
| 15-significant-digit comparison semantics | done (#57): `=0.1+0.2=0.3` is TRUE, and MATCH and COUNTIF compare the same way. Arithmetic keeps full precision, as Excel does — `=(0.1+0.2)-0.3` is 5.55E-17 |
|
| 15-significant-digit comparison semantics | done (#57): `=0.1+0.2=0.3` is TRUE, and MATCH and COUNTIF compare the same way. Arithmetic keeps full precision, as Excel does — `=(0.1+0.2)-0.3` is 5.55E-17 |
|
||||||
| IMSQRT and the other IM* functions against Excel's text | IMSQRT follows Excel's polar route, so a negative real gives Excel's stray real part: `IMSQRT("-4")` is `1.22514845490862E-16+2i` (checked in Excel 16.0 on 79 inputs). Differences kept: IMSQRT of a number with a part above about 1E154 or below about 1E-155 gives the exact root (Excel gives `#NUM!` or 0); IMPOWER, IMEXP, IMSIN, IMCOS, IMSINH and IMCOSH keep exact results where Excel shows the same kind of noise (`IMPOWER("-4",2)` is `16-3.92047505570758E-15i`) or different trailing digits near multiples of pi/2; IMCSC and IMCSCH differ by one unit in the 15th digit; IMARGUMENT returns 16–17 digits where Excel returns 15; the text of IM* and COMPLEX results differs for some magnitudes (Excel writes `COMPLEX(1E-10,1)` as `0.0000000001+i` but `COMPLEX(0.000000123456789012345,1)` as `1.23456789012345E-07+i`, and an integer past 15 digits rounded and zero-padded) |
|
| IMSQRT and the other IM* functions against Excel's text | IMSQRT follows Excel's polar route, so a negative real gives Excel's stray real part: `IMSQRT("-4")` is `1.22514845490862E-16+2i` (checked in Excel 16.0 on 79 inputs). Differences kept: IMSQRT of a number with a part above about 1E154 or below about 1E-155 gives the exact root (Excel gives `#NUM!` or 0); IMPOWER, IMEXP, IMSIN, IMCOS, IMSINH and IMCOSH keep exact results where Excel shows the same kind of noise (`IMPOWER("-4",2)` is `16-3.92047505570758E-15i`) or different trailing digits near multiples of pi/2; IMCSC and IMCSCH differ by one unit in the 15th digit; IMARGUMENT returns 16–17 digits where Excel returns 15; the text of IM* and COMPLEX results differs for some magnitudes (Excel writes `COMPLEX(1E-10,1)` as `0.0000000001+i` but `COMPLEX(0.000000123456789012345,1)` as `1.23456789012345E-07+i`, and an integer past 15 digits rounded and zero-padded) |
|
||||||
| MOD quotient limit | done: Excel 16.0 returns `#NUM!` when `ABS(number/divisor) >= 1.1259E12`; lower quotients retain the remainder |
|
|
||||||
| PERCENTRANK significance and precision | done: Excel 16.0 returns `#DIV/0!` from significance 309, and rounds to 15 significant digits for significance 15–308 |
|
|
||||||
| External workbook references (`[Book.xlsx]Sheet1!A1`) | parsed; links dropped on open, no update |
|
| External workbook references (`[Book.xlsx]Sheet1!A1`) | parsed; links dropped on open, no update |
|
||||||
| Localized function names and `;` separators | **missing** (#45); en-US only |
|
| Localized function names and `;` separators | **missing** (#45); en-US only |
|
||||||
| Long flat operator chains (`=A1+A2+…`, `-`, `*`, `/`, `^`, `&`, comparisons) | done: no operand limit below Excel's 8,192-character formula limit (Excel, observed: `=1+1+…` with 4,096 operands is exactly 8,192 characters with the `=` and calculates; one more character is refused). Evaluated (#484), printed (#628), stored, copied, adjusted, saved and reopened without recursing once per operand, on a 1 MiB stack (#916); written to XLSX as plain formulas, not dynamic arrays; Spanish entry accepts them |
|
| Long flat operator chains (`=A1+A2+…`, `-`, `*`, `/`, `^`, `&`, comparisons) | done: no operand limit below Excel's 8,192-character formula limit (Excel, observed: `=1+1+…` with 4,096 operands is exactly 8,192 characters with the `=` and calculates; one more character is refused). Evaluated (#484), printed (#628), stored, copied, adjusted, saved and reopened without recursing once per operand, on a 1 MiB stack (#916); written to XLSX as plain formulas, not dynamic arrays; Spanish entry accepts them |
|
||||||
@@ -101,12 +98,6 @@ This improves series preparation; it does not change the model-fitting or season
|
|||||||
|
|
||||||
## Depth (estimated)
|
## Depth (estimated)
|
||||||
|
|
||||||
| Function | Excel parity note |
|
|
||||||
|---|---|
|
|
||||||
| BINOM.DIST / BINOM.DIST.RANGE | Trial count is truncated, then must be at most 2,147,483,646; larger trials return `#NUM!`. |
|
|
||||||
| BETA.DIST | Density mode returns `#NUM!` at the lower or upper interval endpoint when the corresponding shape parameter is less than or equal to 1; cumulative endpoint results remain 0 and 1. |
|
|
||||||
| HARMEAN | Returns `#N/A` when arrays and references contain no numeric observations; zero and negative numeric observations still return `#NUM!`. |
|
|
||||||
|
|
||||||
**~82% ready.** The library is broad and has 96 test functions in `crates/functions` with many
|
**~82% ready.** The library is broad and has 96 test functions in `crates/functions` with many
|
||||||
assertions each, but there is no conformance suite against Excel-computed results, and user
|
assertions each, but there is no conformance suite against Excel-computed results, and user
|
||||||
reports keep finding edge cases: MIN() with no arguments (#138), COUNTBLANK past the used range
|
reports keep finding edge cases: MIN() with no arguments (#138), COUNTBLANK past the used range
|
||||||
@@ -133,24 +124,6 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 2026-10-31 | minor | BIN2*, HEX2* and OCT2* reject surrounding spaces and tabs in source strings with `#NUM!`, matching Excel; plain unpadded values remain valid |
|
|
||||||
| 2026-10-31 | minor | DEC2BIN, DEC2HEX and DEC2OCT validate `places` before returning negative two's-complement output, and Boolean places return `#VALUE!`, matching Excel |
|
|
||||||
| 2026-10-31 | minor | BITLSHIFT and BITRSHIFT return `0` for zero operands shifted beyond 53 bits in either direction; nonzero operands retain the `#NUM!` limit, matching Excel |
|
|
||||||
| 2026-10-31 | minor | UNICHAR returns `#N/A` for U+FDD0–U+FDEF and U+FFFE/U+FFFF on every plane; adjacent valid characters and existing invalid-range/surrogate errors remain unchanged, matching Excel |
|
|
||||||
| 2026-10-31 | minor | UPPER, LOWER and PROPER use single-character casing: `UPPER("Straße")` keeps ß, `LOWER("İ")` returns i, and `PROPER("İSTANBUL")` returns Istanbul, matching Windows Excel 16.0 build 20527 in LCID 1033 |
|
|
||||||
| 2026-10-31 | minor | FIXED and DOLLAR format the rounded number's shortest decimal representation before padding requested fractional zeroes, so 1.1 at 16 places and 1.005 at 20 places do not expose binary noise; measured in Windows Excel 16.0 build 20527, LCID 1033 |
|
|
||||||
| 2026-10-31 | minor | FIXED accepts output through 255 characters; DOLLAR accepts output through 254. Windows Excel 16.0 build 20527 (LCID 1033) confirmed the distinct cutoffs with grouped and ungrouped values, signs, currency layout and decimal places |
|
|
||||||
| 2026-10-31 | minor | DOLLAR uses the input sign for negative amounts that round to zero at nonnegative decimal places (`DOLLAR(-0.0001,0)` is `($0)`); when negative decimal places round to zero, Excel returns `$0`. Exact zero and negative zero stay unsigned; FIXED still returns `0` for `FIXED(-0.0001,0)` |
|
|
||||||
| 2026-10-31 | minor | F.DIST, F.DIST.RT, F.INV and F.INV.RT accept degrees of freedom at the 10-billion boundary; F.INV uses the complementary tail, with a gamma-tail limit when numerator degrees dominate, to preserve inverse accuracy; values above the limit remain invalid |
|
|
||||||
| 2026-10-31 | minor | T.DIST returns `#DIV/0!` in density mode and `#NUM!` in cumulative mode when degrees truncate to zero; negative integer degrees remain `#NUM!` |
|
|
||||||
| 2026-10-31 | minor | Student-t density uses an asymptotic log-gamma ratio for large degrees of freedom, avoiding cancellation while preserving bounded work; checked against Excel at 100 million and 10 billion degrees |
|
|
||||||
| 2026-10-31 | minor | T.INV probability zero returns `#DIV/0!`, while other out-of-range probabilities and invalid degrees remain `#NUM!`; T.INV.2T keeps its separate boundary behavior |
|
|
||||||
| 2026-10-31 | minor | GAMMA.DIST density at x=0 and shape 1 returns `#NUM!` for all positive scales; the cumulative branch and other shape boundaries are unchanged |
|
|
||||||
| 2026-10-31 | minor | WEIBULL.DIST returns zero at x=0 in both density and cumulative modes, including shape below one; negative x remains `#NUM!` |
|
|
||||||
| 2026-10-31 | minor | BINOM.DIST and BINOM.DIST.RANGE reject trial counts above 2,147,483,646 after truncation |
|
|
||||||
| 2026-10-31 | minor | BETA.DIST density returns `#NUM!` at the lower endpoint with alpha less than or equal to 1 and upper endpoint with beta less than or equal to 1; cumulative behavior is unchanged |
|
|
||||||
| 2026-10-31 | minor | HARMEAN returns `#N/A` when array or reference inputs contain no numbers; nonpositive numeric observations remain `#NUM!` |
|
|
||||||
| 2026-10-31 | minor | YEARFRAC basis 0 tests the end-31 rule against the original start day; MOD returns `#NUM!` from a quotient of 1.1259E12; PERCENTRANK returns `#DIV/0!` from significance 309 and 15 significant digits from 15 (Excel 16) |
|
|
||||||
| 2026-10-27 | minor | ECMA.CEILING added to the functions that are in our library but not on Microsoft's list (with USDOLLAR), so the counts don't change |
|
| 2026-10-27 | minor | ECMA.CEILING added to the functions that are in our library but not on Microsoft's list (with USDOLLAR), so the counts don't change |
|
||||||
| 2026-10-26 | minor | IMSQRT uses Excel's polar route with its cosine reduction, so `IMSQRT("-4")` is `1.22514845490862E-16+2i` as in Excel (exact `2i` before); the rows list the IM* differences still open |
|
| 2026-10-26 | minor | IMSQRT uses Excel's polar route with its cosine reduction, so `IMSQRT("-4")` is `1.22514845490862E-16+2i` as in Excel (exact `2i` before); the rows list the IM* differences still open |
|
||||||
| 2026-10-25 | minor | LAMBDA row: a LAMBDA that calls itself more than 140 times, and a formula nested past the depth limit, give `#NUM!` (#1809; nesting gave `#CALC!` before). `=Foo(1)` where the defined name Foo holds `#N/A` gives `#N/A`, not `#NAME?` |
|
| 2026-10-25 | minor | LAMBDA row: a LAMBDA that calls itself more than 140 times, and a formula nested past the depth limit, give `#NUM!` (#1809; nesting gave `#CALC!` before). `=Foo(1)` where the defined name Foo holds `#N/A` gives `#N/A`, not `#NAME?` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Hardware parity
|
# Hardware parity
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** trivial (Intel Mac start-up) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (Arrange All) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||||
|
|
||||||
The hardware Excel uses, per platform, against GridCraft. Part of
|
The hardware Excel uses, per platform, against GridCraft. Part of
|
||||||
[target-app-parity.md](target-app-parity.md). **Hardware: ~40% ready, 25–40 h** (estimated).
|
[target-app-parity.md](target-app-parity.md). **Hardware: ~40% ready, 25–40 h** (estimated).
|
||||||
@@ -8,7 +8,7 @@ The hardware Excel uses, per platform, against GridCraft. Part of
|
|||||||
| Hardware feature | Excel | GridCraft (macOS · Windows · Linux · BSD · web) | Gap and estimate |
|
| Hardware feature | Excel | GridCraft (macOS · Windows · Linux · BSD · web) | Gap and estimate |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Multi-core recalculation | Multi-threaded recalculation on all cores (Windows and Mac), configurable thread count | Single-threaded everywhere (no threading in `crates/calc`) | Parallel recalc of independent dependency chains on native builds; WASM stays single-threaded or uses workers later: 12–18 h |
|
| Multi-core recalculation | Multi-threaded recalculation on all cores (Windows and Mac), configurable thread count | Single-threaded everywhere (no threading in `crates/calc`) | Parallel recalc of independent dependency chains on native builds; WASM stays single-threaded or uses workers later: 12–18 h |
|
||||||
| GPU rendering of the grid and charts | DirectX / Metal accelerated drawing | wgpu: Metal (Intel Macs skip wgpu's indirect-call validation shaders, which Metal's Intel compiler aborts on, #2812) · DirectX 12 (default since 10-08; OpenGL when DirectX 12 has no GPU driver or fails to start; Vulkan only on request, so the Intel UHD `igvk64.dll` crash (#120) can't happen; shaders compiled with FXC, so no DXC runtime is needed, #339) · Vulkan/GL · Vulkan/GL · WebGPU or WebGL2 | At parity in kind. Driver problems: screen flashing on Windows 11 (#76): 2–4 h |
|
| GPU rendering of the grid and charts | DirectX / Metal accelerated drawing | wgpu: Metal · DirectX 12 (default since 10-08; OpenGL when DirectX 12 has no GPU driver or fails to start; Vulkan only on request, so the Intel UHD `igvk64.dll` crash (#120) can't happen; shaders compiled with FXC, so no DXC runtime is needed, #339) · Vulkan/GL · Vulkan/GL · WebGPU or WebGL2 | At parity in kind. Driver problems: screen flashing on Windows 11 (#76): 2–4 h |
|
||||||
| Pen and touch (Draw tab) | Pen pressure and tilt, eraser end, touch mode, Ink to Shape, ink replay | Pen, highlighter, eraser, Ink to Shape with mouse/pen input; **no pressure or tilt**, no touch mode | Pressure-sensitive strokes where winit reports force; touch-friendly ribbon spacing: 4–6 h |
|
| Pen and touch (Draw tab) | Pen pressure and tilt, eraser end, touch mode, Ink to Shape, ink replay | Pen, highlighter, eraser, Ink to Shape with mouse/pen input; **no pressure or tilt**, no touch mode | Pressure-sensitive strokes where winit reports force; touch-friendly ribbon spacing: 4–6 h |
|
||||||
| Trackpad and mouse | Smooth scrolling, pinch zoom, horizontal scroll, Magic Mouse | Smooth scrolling and zoom via egui | Verify pinch zoom on each platform: 1–2 h |
|
| Trackpad and mouse | Smooth scrolling, pinch zoom, horizontal scroll, Magic Mouse | Smooth scrolling and zoom via egui | Verify pinch zoom on each platform: 1–2 h |
|
||||||
| Multiple windows and monitors | One window per workbook, New Window for the same workbook, per-monitor DPI | A native window per workbook window on the desktop, New Window for the same workbook; new windows cascade from the last one on its monitor; View › Arrange All tiles, stacks or cascades them over the main window's monitor (web: one window) | Arrange All across monitors; verify per-monitor DPI with windows on two monitors: 2–3 h |
|
| Multiple windows and monitors | One window per workbook, New Window for the same workbook, per-monitor DPI | A native window per workbook window on the desktop, New Window for the same workbook; new windows cascade from the last one on its monitor; View › Arrange All tiles, stacks or cascades them over the main window's monitor (web: one window) | Arrange All across monitors; verify per-monitor DPI with windows on two monitors: 2–3 h |
|
||||||
@@ -20,7 +20,6 @@ The hardware Excel uses, per platform, against GridCraft. Part of
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 2026-10-31 | trivial | macOS: Intel Macs skip wgpu's indirect-call validation shaders, so they no longer exit on Metal's compiler error there (#2812). Macs run with OpenCore Legacy Patcher on macOS 15 can still fail earlier, on the MSL version (#1116). |
|
|
||||||
| 2026-10-22 | minor | View › Arrange All over the main window's monitor. |
|
| 2026-10-22 | minor | View › Arrange All over the main window's monitor. |
|
||||||
| 2026-10-22 | minor | Multiple windows: each workbook window is a native window of its own (desktop). |
|
| 2026-10-22 | minor | Multiple windows: each workbook window is a native window of its own (desktop). |
|
||||||
| 2026-10-12 | minor | Desktop File > Print now discovers printers and submits PDF jobs through pinned craft-print with printer choice, copies, duplex and grayscale; browser printing and PDF export remain available. Remaining print parity includes page ranges and driver-specific tray/options UI; physical printer fidelity needs hardware validation. |
|
| 2026-10-12 | minor | Desktop File > Print now discovers printers and submits PDF jobs through pinned craft-print with printer choice, copies, duplex and grayscale; browser printing and PDF export remain available. Remaining print parity includes page ranges and driver-specific tray/options UI; physical printer fidelity needs hardware validation. |
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Release playbook
|
||||||
|
|
||||||
|
The release playbook shared by every crafting app lives in craftrules:
|
||||||
|
[`../craftrules/release/playbook.md`](../../../craftrules/release/playbook.md)
|
||||||
|
(`storytold/craftrules`, `release/playbook.md`). It is the canonical copy; don't duplicate it
|
||||||
|
here. The signing runbook is next to it in `release/signing-setup.md`.
|
||||||
|
|
||||||
|
GridCraft's copy was taken from DesignCraft (`.github/workflows/release.yml`, `packaging/`,
|
||||||
|
`cargo xtask version`). GridCraft-specific details are in [releasing.md](releasing.md).
|
||||||
+24
-22
@@ -5,7 +5,8 @@ signed installers for macOS, Windows, Linux, FreeBSD and the web, then creates o
|
|||||||
**draft** GitHub Release named `GridCraft v<version>`. Nobody sees a draft until a maintainer
|
**draft** GitHub Release named `GridCraft v<version>`. Nobody sees a draft until a maintainer
|
||||||
publishes it.
|
publishes it.
|
||||||
|
|
||||||
This process was copied from DesignCraft and renamed. User-facing names say
|
This is GridCraft's implementation of the shared [release playbook](release-playbook.md)
|
||||||
|
(craftrules `release/playbook.md`), copied from DesignCraft and renamed. User-facing names say
|
||||||
**GridCraft**. Files, binaries and ids stay lowercase
|
**GridCraft**. Files, binaries and ids stay lowercase
|
||||||
(`gridcraft-<version>-<platform>-<arch>.<ext>`, `ai.storyteller.gridcraft`).
|
(`gridcraft-<version>-<platform>-<arch>.<ext>`, `ai.storyteller.gridcraft`).
|
||||||
|
|
||||||
@@ -18,24 +19,25 @@ This process was copied from DesignCraft and renamed. User-facing names say
|
|||||||
| Web app dir `apps/gridcraft-web` | `packaging/web/package.sh` |
|
| Web app dir `apps/gridcraft-web` | `packaging/web/package.sh` |
|
||||||
| App colour `#1f9d55` (ink `#147a40`), **proposed** | `assets/app-icon/README.md`, metainfo `<branding>` |
|
| App colour `#1f9d55` (ink `#147a40`), **proposed** | `assets/app-icon/README.md`, metainfo `<branding>` |
|
||||||
|
|
||||||
## Application release prerequisites
|
## Prerequisites still owned by the app crates
|
||||||
|
|
||||||
The `apps/gridcraft` and `apps/gridcraft-web` crates are workspace members, and
|
The pipeline references crates that don't exist yet; the first release run fails until they do:
|
||||||
`apps/gridcraft-cli` supports `--version`. The `cargo xtask` alias is defined in
|
|
||||||
`.cargo/config.toml`. The current app-specific release setup includes:
|
|
||||||
|
|
||||||
- **Windows resources:** `apps/gridcraft/build.rs` embeds the icon and version information with
|
- **`apps/gridcraft`** (the GUI binary `gridcraft`) and **`apps/gridcraft-web`** (trunk
|
||||||
`winresource` (instructions at the top of the file). `package.ps1` sets
|
site) must be workspace members. `apps/gridcraft-cli` exists and supports `--version`.
|
||||||
`GRIDCRAFT_REQUIRE_WINRES=1`, so a release build without the icon fails, and it checks that
|
- **Windows resources:** move `packaging/windows/app-build.rs.in` to `apps/gridcraft/build.rs`
|
||||||
`gridcraft.exe` is a GUI-subsystem binary. `apps/gridcraft/src/main.rs` sets
|
and add `winresource = "0.1"` under `[build-dependencies]` (instructions at the top of the
|
||||||
`windows_subsystem = "windows"` for release builds.
|
file). `package.ps1` sets `GRIDCRAFT_REQUIRE_WINRES=1`, so a release build without the icon
|
||||||
- **Version information:** `gridcraft --version`, `gridcraft-cli --version` and the About dialog
|
fails, and it checks that `gridcraft.exe` is a GUI-subsystem binary, so `main.rs` needs
|
||||||
use `env!("CARGO_PKG_VERSION")` from `[workspace.package]`. `packaging/env.sh` also exports
|
`#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]`.
|
||||||
`GRIDCRAFT_BUILD_SHA` and `GRIDCRAFT_BUILD_DATE` for artifact metadata.
|
- **Build info:** CI exports `GRIDCRAFT_BUILD_SHA` and `GRIDCRAFT_BUILD_DATE`; read them
|
||||||
- **Runtime icon:** `apps/gridcraft/src/main.rs` embeds
|
with `option_env!` (fallback: "dev build") for `--version` and *Help › About GridCraft*.
|
||||||
`assets/app-icon/gridcraft-macos-512.png` (macOS Dock) and
|
- **Runtime icon:** embed `assets/app-icon/gridcraft-macos-512.png` (macOS Dock) and
|
||||||
`hicolor/256x256/apps/ai.storyteller.gridcraft.png` (Windows/Linux) as the window icon, and
|
`hicolor/256x256/apps/ai.storyteller.gridcraft.png` (Windows/Linux) as the window icon, and
|
||||||
uses `ai.storyteller.gridcraft` as the Wayland app id / `StartupWMClass`.
|
use `ai.storyteller.gridcraft` as the Wayland app id / `StartupWMClass`.
|
||||||
|
- **`cargo xtask` alias:** the docs use `cargo xtask …`, which needs
|
||||||
|
`.cargo/config.toml` with `[alias] xtask = "run --package xtask --"`. Until it exists, use
|
||||||
|
`cargo run -p xtask -- …`. The workflows already call `cargo run -p xtask` directly.
|
||||||
|
|
||||||
## Cutting a release
|
## Cutting a release
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ shows a `::warning::`. Never store these as org- or repo-level Actions secrets (
|
|||||||
|
|
||||||
`GITHUB_TOKEN` creates the release; only the final job gets `contents: write`.
|
`GITHUB_TOKEN` creates the release; only the final job gets `contents: write`.
|
||||||
|
|
||||||
The repository setup commands, run by an org
|
The exact commands (from craftrules `release/signing-setup.md`, steps 1–3), run by an org
|
||||||
admin. Done for storytold/gridcraft on 2026-10-07; the `release-managers` team also has write
|
admin. Done for storytold/gridcraft on 2026-10-07; the `release-managers` team also has write
|
||||||
access to the repo, which its ruleset bypass needs.
|
access to the repo, which its ruleset bypass needs.
|
||||||
|
|
||||||
@@ -272,7 +274,8 @@ the publisher.
|
|||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
`assets/app-icon/gridcraft.svg` is a **placeholder** (an original ledger-page drawing on
|
`assets/app-icon/gridcraft.svg` is a **placeholder** (an original ledger-page drawing on
|
||||||
GridCraft green) until the owner makes GridCraft's engraved creature portrait. `packaging/icons.sh` regenerates the 1024 px PNG, the macOS 512 px
|
GridCraft green) until the owner makes GridCraft's engraved creature portrait (craftrules
|
||||||
|
`standards/icon-design.md`). `packaging/icons.sh` regenerates the 1024 px PNG, the macOS 512 px
|
||||||
PNG, the `.icns` (iconutil), the `.ico` (`cargo xtask ico`) and the hicolor PNGs from it; it
|
PNG, the `.icns` (iconutil), the `.ico` (`cargo xtask ico`) and the hicolor PNGs from it; it
|
||||||
needs `resvg`. The outputs are committed, so packaging never needs those tools. Every file has a
|
needs `resvg`. The outputs are committed, so packaging never needs those tools. Every file has a
|
||||||
row in `ATTRIBUTION.md` (`cargo xtask assets`).
|
row in `ATTRIBUTION.md` (`cargo xtask assets`).
|
||||||
@@ -286,8 +289,7 @@ row in `ATTRIBUTION.md` (`cargo xtask assets`).
|
|||||||
installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner.
|
installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner.
|
||||||
- `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).
|
- `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).
|
||||||
|
|
||||||
**CI and release builds.** `.github/workflows/ci.yml` runs the shared `cargo xtask ci` gate on
|
**Only the `release` branch builds.** No build workflow runs on `main` or pull requests; every
|
||||||
every push to `main` and every pull request. Release artifacts are built on `release` pushes or
|
build job checks `github.ref == 'refs/heads/release'`, so a manual run from another branch is
|
||||||
manually; manual runs from other branches are dry runs as described above. `packaging-lint.yml`
|
skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs.
|
||||||
also runs on `main` and PRs.
|
|
||||||
- Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).
|
- Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,6 @@
|
|||||||
# UI parity
|
# UI parity
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (checkbox cells print as boxes; earlier: Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||||
|
|
||||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||||
@@ -194,7 +194,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 2026-10-30 | minor | Checkbox cells print and export to PDF as boxes, as on screen: unchecked white with a grey outline, checked green with a white tick (#2972). They printed their value as TRUE or FALSE; other values in a checkbox cell still print as text |
|
|
||||||
| 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl |
|
| 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl |
|
||||||
| 2026-10-29 | minor | Insert Hyperlink opens with an existing link's address, display text and ScreenTip (#2876). An address-only edit preserves the cell's content and formula; Cancel leaves the link unchanged, and Undo restores the edit |
|
| 2026-10-29 | minor | Insert Hyperlink opens with an existing link's address, display text and ScreenTip (#2876). An address-only edit preserves the cell's content and formula; Cancel leaves the link unchanged, and Undo restores the edit |
|
||||||
| 2026-10-27 | minor | Ctrl with the numpad's + inserts rows, columns or cells as Ctrl+Shift+= does, as in Excel; it did nothing because the numpad key arrives without Shift |
|
| 2026-10-27 | minor | Ctrl with the numpad's + inserts rows, columns or cells as Ctrl+Shift+= does, as in Excel; it did nothing because the numpad key arrives without Shift |
|
||||||
|
|||||||
@@ -20,12 +20,10 @@ APP_ID=ai.storyteller.gridcraft
|
|||||||
FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo
|
FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
TEST=1
|
TEST=1
|
||||||
OCI=0
|
|
||||||
TARBALL=""
|
TARBALL=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--no-test) TEST=0; shift ;;
|
--no-test) TEST=0; shift ;;
|
||||||
--oci) OCI=1; shift ;;
|
|
||||||
-h | --help) sed -n '2,14p' "$0"; exit 0 ;;
|
-h | --help) sed -n '2,14p' "$0"; exit 0 ;;
|
||||||
-*) echo "unknown argument: $1" >&2; exit 2 ;;
|
-*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||||
*) TARBALL="$1"; shift ;;
|
*) TARBALL="$1"; shift ;;
|
||||||
@@ -63,12 +61,6 @@ OUT="$DIST/$BASENAME.flatpak"
|
|||||||
# --runtime-repo lets `flatpak install` fetch the freedesktop runtime from Flathub if missing.
|
# --runtime-repo lets `flatpak install` fetch the freedesktop runtime from Flathub if missing.
|
||||||
flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable
|
flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable
|
||||||
echo "wrote $OUT"
|
echo "wrote $OUT"
|
||||||
if [[ "$OCI" = 1 ]]; then
|
|
||||||
OUT_OCI="$DIST/$BASENAME-oci"
|
|
||||||
rm -rf "$OUT_OCI"
|
|
||||||
flatpak build-bundle --oci "$WORK/repo" "$OUT_OCI" "$APP_ID" stable
|
|
||||||
echo "wrote OCI layout to $OUT_OCI"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$TEST" = 1 ]]; then
|
if [[ "$TEST" = 1 ]]; then
|
||||||
flatpak install --user -y --noninteractive --reinstall "$OUT"
|
flatpak install --user -y --noninteractive --reinstall "$OUT"
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build gridcraft as an OCI container layout and push it to GitHub Container Registry (GHCR)
|
|
||||||
# for consumption by Flatpak OCI clients (flatpak-oci-specs).
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# packaging/linux/flatpak-oci.sh [--push] [TARBALL]
|
|
||||||
#
|
|
||||||
# Environment variables:
|
|
||||||
# REGISTRY Target registry image (default: ghcr.io/storytold/gridcraft)
|
|
||||||
# TAG Target image tag (default: $VERSION-$ARCH, also tags stable-$ARCH)
|
|
||||||
# GITHUB_TOKEN Token for GHCR authentication when --push is used
|
|
||||||
set -euo pipefail
|
|
||||||
# shellcheck source=../env.sh
|
|
||||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
|
||||||
HERE="$ROOT/packaging/linux"
|
|
||||||
|
|
||||||
PUSH=0
|
|
||||||
TARBALL=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--push) PUSH=1; shift ;;
|
|
||||||
-h | --help) sed -n '2,12p' "$0"; exit 0 ;;
|
|
||||||
-*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
||||||
*) TARBALL="$1"; shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
ARCH="$(uname -m)"
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64) ;;
|
|
||||||
aarch64 | arm64) ARCH=aarch64 ;;
|
|
||||||
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
BASENAME="gridcraft-$VERSION-linux-$ARCH"
|
|
||||||
OUT_OCI="$DIST/$BASENAME-oci"
|
|
||||||
|
|
||||||
echo "==> Building Flatpak OCI layout for $BASENAME"
|
|
||||||
"$HERE/flatpak-bundle.sh" --oci --no-test "${TARBALL:-}"
|
|
||||||
|
|
||||||
[[ -d "$OUT_OCI" ]] || { echo "error: $OUT_OCI layout not generated" >&2; exit 1; }
|
|
||||||
|
|
||||||
REGISTRY="${REGISTRY:-ghcr.io/storytold/gridcraft}"
|
|
||||||
TAG="${TAG:-$VERSION-$ARCH}"
|
|
||||||
|
|
||||||
echo "==> OCI layout ready at $OUT_OCI (Target: $REGISTRY:$TAG)"
|
|
||||||
|
|
||||||
if [[ "$PUSH" = 1 ]]; then
|
|
||||||
command -v skopeo >/dev/null || { echo "error: skopeo is required to push OCI images (apt install skopeo)" >&2; exit 1; }
|
|
||||||
|
|
||||||
AUTH_ARGS=()
|
|
||||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
|
||||||
AUTH_ARGS=(--dest-creds "${GITHUB_ACTOR:-storytold}:$GITHUB_TOKEN")
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> Pushing to $REGISTRY:$TAG"
|
|
||||||
skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:$TAG"
|
|
||||||
|
|
||||||
# Also update architecture-specific stable tag
|
|
||||||
echo "==> Pushing to $REGISTRY:stable-$ARCH"
|
|
||||||
skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:stable-$ARCH"
|
|
||||||
|
|
||||||
echo "==> Push complete"
|
|
||||||
fi
|
|
||||||
@@ -41,11 +41,6 @@ finish-args:
|
|||||||
- --filesystem=xdg-documents
|
- --filesystem=xdg-documents
|
||||||
- --filesystem=xdg-download
|
- --filesystem=xdg-download
|
||||||
# Network: update checks reach GitHub. Home/host filesystem access is deliberately not granted.
|
# Network: update checks reach GitHub. Home/host filesystem access is deliberately not granted.
|
||||||
# The release runner's flatpak-builder has no eu-strip (elfutils), which its debuginfo pass runs
|
|
||||||
# on cups-client's unstripped tools, in whichever module comes first after they're installed. The
|
|
||||||
# app's own binaries come from the release tarball, already stripped.
|
|
||||||
build-options:
|
|
||||||
no-debuginfo: true
|
|
||||||
modules:
|
modules:
|
||||||
# CUPS client tools for craft-print: the runtime has libcups but not lpstat/lp/lpoptions.
|
# CUPS client tools for craft-print: the runtime has libcups but not lpstat/lp/lpoptions.
|
||||||
# Linked statically against this release's libcups, so a runtime update can't break them;
|
# Linked statically against this release's libcups, so a runtime update can't break them;
|
||||||
|
|||||||
Reference in New Issue
Block a user