Compare commits

...

320 Commits

Author SHA1 Message Date
Nicolás Hatcher
96a5482e01 FIX: Don not clone Locales and Languages, pass them by reference 2025-12-14 20:38:08 +01:00
Nicolás Hatcher
ffe5d1a158 UPDATE: Adds bindings to update timezone and locale
UPDATE: Update "generate locale" utility

FIX: Minor fixes to UI and proper support for locales/timezones

UPDATE: Adds "display language" setting to core
2025-12-13 08:12:11 +01:00
Daniel
402a13bd00 fix: mobile adjustments 2025-12-13 05:20:59 +01:00
Daniel
a345d7c9ac fix: force default values 2025-12-13 05:20:59 +01:00
Daniel
3fd55bb5c9 update: add dropdowns to content 2025-12-13 05:20:59 +01:00
Daniel
bab24a5207 update: open dialog from footer 2025-12-13 05:20:59 +01:00
Daniel
5288665f70 update: add a dialog for settings 2025-12-13 05:20:59 +01:00
Daniel González-Albo
ba75ffcf4f Merge pull request #598 from elsaminsut/testfixes
fix: remove duplicate xlsx tests
2025-12-10 00:48:27 +01:00
Daniel
b5c977d3aa fix: comments 2025-12-09 17:15:39 +01:00
Daniel
4029441cea fix: tiny details in styling 2025-12-09 17:15:39 +01:00
Daniel
cd47c609a0 update: remove drawer button from toolbar, small fixes 2025-12-09 17:15:39 +01:00
Daniel
ae6acdcdd5 fix: styles 2025-12-09 17:15:39 +01:00
Daniel
c196db2115 update: click on list items selects cells and ranges 2025-12-09 17:15:39 +01:00
Daniel
a3c201e4e4 update: list ranges in menu 2025-12-09 17:15:39 +01:00
Daniel
126e62957a update: allow opening nm drawer from menu 2025-12-09 17:15:39 +01:00
Daniel
294a651ae5 update: add a name manager menu in formula bar 2025-12-09 17:15:39 +01:00
Elsa Minsut
6f8a1e0da6 fix: syntax fixes in unit tests 2025-12-01 20:23:32 +01:00
Elsa Minsut
205ba6ee2d fix: removes failing currency edge case 2025-12-01 19:53:18 +01:00
Elsa Minsut
547b331773 fix: xlsx test without array formulas 2025-12-01 19:44:15 +01:00
Nicolás Hatcher
f96612cf23 FIX: NOW test cases 2025-11-28 20:52:34 +01:00
Nicolás Hatcher
745435b950 FIX: Copilot requests 2025-11-28 20:52:34 +01:00
Nicolás Hatcher
4ca996cd3f UPDATE(easter egg): Add an argument for NOW
This shows the time in different timezones
2025-11-28 20:52:34 +01:00
Elsa Minsut
3fbb91c414 fix: deletes old xlsx test with failing cases 2025-11-28 20:26:34 +01:00
Elsa Minsut
93c9c42607 fix: uploads xlsx tests split per function 2025-11-28 20:26:34 +01:00
Elsa Minsut
11edc2378e fix: deletes old test file and replaces it with detailed one 2025-11-28 20:26:34 +01:00
Elsa Minsut
962e70c834 fix: adds new line at end of file 2025-11-28 20:26:34 +01:00
Elsa Minsut
f803dad0a3 fix: updates test to remove failing edge cases 2025-11-28 20:26:34 +01:00
Elsa Minsut
19580fc1ad update: warning message shows the function as implemented 2025-11-28 20:26:34 +01:00
Elsa Minsut
e760b2d08e update: sets status as available for implemented functions 2025-11-28 20:26:34 +01:00
Elsa Minsut
0e6ded7154 update: adds unit test for CELL, INFO, N and SHEETS 2025-11-28 20:26:34 +01:00
Elsa Minsut
db26403432 docs: available status for implemented functions 2025-11-28 20:23:52 +01:00
Elsa Minsut
9193479cce update: adds xlsx test for SUMSQ 2025-11-28 20:23:52 +01:00
Elsa Minsut
f814a75ae5 update: adds unit test for SUMSQ 2025-11-28 20:23:52 +01:00
Elsa Minsut
c8da5efb5f update: removes old xlsx test file 2025-11-28 20:23:52 +01:00
Daniel
522e734395 update: use different header styling for full column or row selection 2025-11-28 20:10:24 +01:00
tolgakaan12
2a7d59e512 FIX: Floating-point precision bug in FLOOR functions
Fixes #571

- Add EXCEL_PRECISION constant (15 significant digits)
- Fix FLOOR(7.1, 0.1) returning 7.0 instead of 7.1
- Apply to_excel_precision to ratio before floor/ceil operations
- Affects FLOOR, FLOOR.MATH, and FLOOR.PRECISE functions
- Add test_floor with 6 test cases
2025-11-28 20:05:31 +01:00
Nicolás Hatcher
c4142d4bf8 UPDATE: Adds 12 more statistical functions:
* GAUSS
* HARMEAN
* KURT
* MAXA
* MEDIAN
* MINA
* RANK.EQ
* RANK.AVG
* SKEW
* SKEW.P
* SMALL
* LARGE
2025-11-28 19:55:43 +01:00
Daniel González-Albo
885d344b5b Merge pull request #581 from blueboy93/column-documentation
Edited Column Documentation
2025-11-27 23:44:18 +01:00
Tom
bed6f007cd FIX: Typos adjusted. Thanks Elsa! 2025-11-27 20:26:28 +01:00
Tom
dbd1b2df60 FIX: Edits after Elsa's review and further tests 2025-11-27 18:49:32 +01:00
Elsa Minsut
db552047c8 fix: format fixes 2025-11-26 23:57:31 +01:00
Elsa Minsut
bcbacdb0a3 fix: adds missing file format and missing reference in mod 2025-11-26 23:44:42 +01:00
Elsa Minsut
d0f37854d9 fix: removes duplicate COMBIN and COMBINA tests 2025-11-26 23:36:59 +01:00
Elsa Minsut
99b03f70c3 fix: removes duplicate database functions test 2025-11-26 23:35:31 +01:00
Elsa Minsut
3e1605a494 fix: removes a bunch of duplicate math functions tests 2025-11-26 23:33:13 +01:00
Elsa Minsut
d6aad08e73 fix: remove duplicate MROUND, TRUNC, INT test 2025-11-26 23:31:37 +01:00
Nicolás Hatcher
8597d14a4e UPDATE: Implements CORREL, SLOPE, INTERCEPT, RSQ and STEYX
These are all functions that follow a very simmilar path code
2025-11-26 22:33:49 +01:00
Nicolás Hatcher
01b19b9c35 FIX: Add comments. Thank you copilot! 2025-11-26 20:09:58 +01:00
Nicolás Hatcher
4649a0c78c UPDATE: Adds SUMX2MY2, SUMX2PY2 and SUMXMY2 mathematical functions 2025-11-26 20:09:58 +01:00
Daniel González-Albo
cd0baf5ba7 Merge pull request #591 from ironcalc/dani/app/mobile-adjustments
fix: mobile issues in app
2025-11-26 00:51:08 +01:00
Daniel
167d169f1a chore: use a constant instead of a number 2025-11-26 00:44:06 +01:00
Nicolás Hatcher
080574b112 UPDATE: Implement FTEST function 2025-11-26 00:30:37 +01:00
Daniel
6056b8f122 fix: mobile issues 2025-11-26 00:01:12 +01:00
Nicolás Hatcher
e61b15655a UPDATE: Adds a bunch of tests 2025-11-25 01:20:03 +01:00
Nicolás Hatcher
6822505602 UPDATE: Adds 56 functions in the Statistical section
Uses statrs for numerical functions

REFACTOR: Put statistical functions on its own module

This might seem counter-intuitive but the wasm build after this refactor
is 1528 bytes smaller :)
2025-11-25 01:20:03 +01:00
Tom
25f7891343 FIX: added missing comma (Thanks copilot!) 2025-11-24 18:36:58 +01:00
Tom
bdd0af0a39 FIX: Fix mispelled word (Thanks Copilot) 2025-11-24 18:27:38 +01:00
Tom
261924396d Edited Column Documentation 2025-11-24 18:12:34 +01:00
Nicolás Hatcher
67ef3bcf87 FIX: Correct number of arguments for functions 2025-11-23 21:02:59 +01:00
Daniel González-Albo
9aac285964 Merge pull request #570 from ironcalc/dani/app/localstorage-warning
update: add data-storage warnings to the app
2025-11-23 13:12:35 +01:00
Daniel
ba40c3c673 update: copy 2025-11-23 13:04:32 +01:00
Daniel
cc01556387 fix: nicos suggestions 2025-11-23 13:03:47 +01:00
Daniel
35323df20e fix: copilot's suggestions 2025-11-23 13:03:47 +01:00
Daniel
19c115b32f update: allow to edit sheet anems directly from tab buttons 2025-11-23 13:03:47 +01:00
Daniel
6b60b339d6 update: show tab menu on right click 2025-11-23 13:03:47 +01:00
Nicolás Hatcher
41c8d88b80 UPDATE: Adds the rest of the DATABASE functions 2025-11-23 10:48:23 +01:00
Daniel
73e5c305cc update: add a dismissable alert to the left drawer 2025-11-21 00:26:51 +01:00
Elsa Minsut
774b447c84 update: adds xlsx test for these functions 2025-11-20 22:13:23 +01:00
Elsa Minsut
23b7333572 docs: available status for implemented functions 2025-11-20 22:13:23 +01:00
Elsa Minsut
ef47c26c50 update: adds unit test for the reciprocal trigonometric functions 2025-11-20 22:13:23 +01:00
Elsa Minsut
5cc61b0de4 update: adds unit test for EXP and SIGN 2025-11-20 22:12:29 +01:00
Elsa Minsut
42e8d44454 docs: available status for implemented functions 2025-11-20 22:12:29 +01:00
Elsa Minsut
f840806f94 docs: adds style guide page to sidebar 2025-11-20 22:11:45 +01:00
Elsa Minsut
4a21d4b03a docs: style guide clarity fix 2025-11-20 22:11:45 +01:00
Elsa Minsut
4cf162eb82 docs: documentation guide edits for clarity, readability and consistency 2025-11-20 22:11:45 +01:00
Nicolás Hatcher
2cab93be18 UPDATE: Also use erfc (thanks copilot!) 2025-11-20 22:01:00 +01:00
Nicolás Hatcher
fd34e46689 UPDATE: Uses statrs instead of our own erf
This adds 2630 bytes to the wasm build and a dependency.
It is ok-ish

The idea is that it will help us greatly with the statistical functions
2025-11-20 22:01:00 +01:00
Nicolás Hatcher
3bb49d1e8f FIX: Minor cleanups 2025-11-20 21:44:05 +01:00
Nicolás Hatcher
1391f196b5 UPDATE: Adds name validation and exposes it in wasm
We do a trick I am not proud of. Because all of our errors are Strings,
we don't have a way to separate a name error from an index error,
for instance. What I do in prepend the error with a string that indicates
where it comes from.
2025-11-20 21:44:05 +01:00
Nicolás Hatcher
3db094c956 FIX: Select range in worksheet when the name is selected if possible 2025-11-20 21:44:05 +01:00
Nicolás Hatcher
50941cb6ef FIX: Make properties not optional 2025-11-20 21:44:05 +01:00
Daniel
150b516863 update: add a warning tooltip next to the title 2025-11-20 00:55:32 +01:00
Nicolás Hatcher
dc49afa2c3 FIX: Format numbers a tad better
I still think there is some way to go, but this is closer to Excel
2025-11-19 23:53:07 +01:00
Nicolás Hatcher
acb90fbb9d FIX: Issues with trigonometric functions
* Right branch for ACOT for negative numbers
* correct error for ACOTH
* Correct approx for COTH for x > 20
2025-11-19 23:53:07 +01:00
Nicolás Hatcher
7676efca44 FIX: Issues with SIGN and EXP
Fixes #563
2025-11-19 04:24:23 +01:00
Daniel
8e15c623dd docs: add a guide for documenting functions 2025-11-17 22:45:53 +01:00
Nicolás Hatcher
eb76d8dd23 FIX: Issues with INT
Fixes #535
2025-11-16 20:34:25 +01:00
Nicolás Hatcher
1053d00d22 FIX: Copilot's suggestions 2025-11-16 19:45:18 +01:00
Nicolás Hatcher
5ff4774c5a FIX: Cast to string now checks for dates, currencies or percentages
Fixes part of #535
2025-11-16 19:45:18 +01:00
Nicolás Hatcher
7e966baa0d FIX: Copilot's catch 2025-11-16 11:29:57 +01:00
Nicolás Hatcher
c52c05aa8e FIX: Fixes several issues with DATABASE functions
Fixes #547
2025-11-16 11:29:57 +01:00
Elsa Minsut
129959137d update: adds testing for MROUND, TRUNC, and INT (#542)
* update: available status for implemented functions

* update: adds xlsx test for MROUND, TRUNC and INT

* update: adds unit test for MROUND, TRUNC and INT
2025-11-16 11:25:28 +01:00
Elsa Minsut
4d5af45711 fix: removes failing cases from xlsx test 2025-11-16 11:22:29 +01:00
Elsa Minsut
471f32f92a update: adds unit test for ARABIC and ROMAN 2025-11-16 11:22:29 +01:00
Elsa Minsut
7b5427196d update: updates docs to show ARABIC and ROMAN as implemented functions 2025-11-16 11:22:29 +01:00
Elsa Minsut
66b7586730 update: adds xlsx test for ARABIC and ROMAN 2025-11-16 11:22:29 +01:00
Nicolás Hatcher
630f0e1baf FIX: Biome automatic "unsafe" updates 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
bc9fefcb70 FIX: Biome automatic updates 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
3d970acc34 FIX: Make biome happy 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
e0e566db76 UPDATE: Update frontend dependencies 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
e3fc1d229a FIX: Disables telemetry for storybook 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
78d1f6b4a4 UPDATE: Updates vite and dependencies 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
45ee1c35fe UPDATE: Update dependencies 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
671cfff619 UPDATE: Update React and Storybook 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
7e2fcec4a3 FIX: Biome apply "unsafe" changes 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
12342da649 FIX: Delete unused button 2025-11-13 19:49:04 +01:00
Nicolás Hatcher
4e9d7611a8 FIX: Update biome and apply automatic fixes 2025-11-13 19:49:04 +01:00
Elsa Minsut
e0339f641b UPDATE: updates test for TIME, HOUR, MINUTE, SECOND (#461)
* UPDATE: updates test for TIME, HOUR, MINUTE, SECOND

* fix: updates test to remove failing edge cases

* fix: xlsx file rename for compatibility
2025-11-13 18:15:34 +01:00
Nicolás Hatcher
aa953e1ece UPDATE: Add some DATABASE functions
DAVERAGE
DCOUNT
DGET
DMAX
DMIN
DSUM
2025-11-12 23:18:47 +01:00
Daniel Gonzalez Albo
cbf75c059b fix: nicos suggestions 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
b2744efeb5 fix: copilot and nicos comments 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
ef6849e822 fix: copilot suggestions 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
aa4dd598b1 chore: remove old name manager 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
8b3bd7943e update: mobile support 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
a1d1b64b76 update: add empty space 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
5094a7fe4d update: in toolbar, open drawer instead of dialog 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
c283fd7b60 update: improve error handling 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
36beccd4ae style: adjustments in scope select 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
a252f9c626 fix: footer, header, translation file 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
f8bd03d92c update: add actions, allow drawer resize 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
e44a2e8c3e update: styling and layout 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
4217c1455b update: move all functionalities from dialog to drawer 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
d8b3ba0dae update: populate drawer, styling 2025-11-12 22:33:44 +01:00
Daniel Gonzalez Albo
95a7782f22 update: move drawer to its own component 2025-11-12 22:33:44 +01:00
Nicolás Hatcher
087211ebc3 UPDATE: WIP 2025-11-12 22:33:44 +01:00
Elsa Minsut
46d766c85c update: warning message shows function as implemented 2025-11-12 20:44:08 +01:00
Elsa Minsut
2a14ee73c4 update: replaces warning text on doc pages 2025-11-12 20:44:08 +01:00
Elsa Minsut
401c7c4289 update: sets implemented functions as available 2025-11-12 20:44:08 +01:00
Elsa Minsut
3246137545 update: adds unit test for COMBIN and COMBINA 2025-11-12 20:44:08 +01:00
Elsa Minsut
b1f45511d0 update: adds xlsx test for COMBIN and COMBINA 2025-11-12 20:44:08 +01:00
Nicolás Hatcher
4b93174261 FIX: Value of SEC at 0 was incorrect
Also fixed imported errors of trigonometrical functions

Fixes #531
2025-11-11 22:25:10 +01:00
Nicolás Hatcher
3111a74530 FIX: Propagate name correctly 2025-11-11 08:28:50 +01:00
Nicolás Hatcher
ae3fcaf9e9 FIX: New workbooks are created in the users TZ falling back to UTC 2025-11-11 08:28:50 +01:00
Nicolás Hatcher
dd78db3d2b FIX: NOW shows now formatted output 2025-11-11 08:28:50 +01:00
Nicolás Hatcher
acf334074f FIX: Include misconfigured test file 2025-11-11 08:28:50 +01:00
Nicolás Hatcher
e48810d91b FIX: Removed some console.log lines 2025-11-11 08:28:50 +01:00
Nicolás Hatcher
18db1cf052 FIX: Two small fixes to YEARFRAC
* Takes abs value in between two dates
* Follows ODFv1.2 part 2 section 4.11.7.7
2025-11-08 22:40:18 +01:00
Elsa Minsut
ed40f79324 FIX: Skip numerical failure in windows 2025-11-08 17:56:07 +01:00
Elsa Minsut
10ee95c48f FIX: Badge type 2025-11-08 17:56:07 +01:00
Elsa Minsut
741a223f3d update: Math and Trigonometry main page links to new docs 2025-11-08 17:56:07 +01:00
Elsa Minsut
ba139d1b6c update: adds MOD and QUOTIENT doc pages 2025-11-08 17:56:07 +01:00
Elsa Minsut
e0306cb161 update: adds unit test for MOD and QUOTIENT 2025-11-08 17:56:07 +01:00
Elsa Minsut
cea1f67cd0 update: adds xlsx tests for MOD and QUOTIENT 2025-11-08 17:56:07 +01:00
Nicolás Hatcher Andrés
4a3eef5a81 FIX: TRUE/FALSE for QUOTIENT (#524)
Excel returns #VALUE! when arguments are boolean

NB: MOD is different!
2025-11-08 17:25:02 +01:00
Elsa Minsut
91299e3c0b update: fixes status for implemented functions (#520) 2025-11-08 08:54:38 +01:00
Nicolás Hatcher Andrés
1b38d79b81 FIX: Make clippy happy (#521) 2025-11-08 08:53:50 +01:00
Elsa Minsut
a2d11a42cc update: adds docs, unit tests and xlsx tests for EVEN and ODD functions (#517)
* update: adds unit test for EVEN and ODD functions

* update: adds xlsx test for EVEN and ODD functions

* update: adds EVEN and ODD doc pages

* update: Math and Trigonometry main page links to new functions

* update: changes to functions badge type in main Math and Trigonometry page
2025-11-07 04:26:01 +01:00
Elsa Minsut
480a2d1769 update: adds docs, unit tests and xlsx tests for DATEVALUE and TIMEVALUE functions (#506)
* update: adds documentation for DATEVALUE and TIMEVALUE functions

* update: adds DATEVALUE and TIMEVALUE unit tests

* update: adds DATEVALUE and TIMEVALUE xlsx tests

* update: Date and Time main page links

* update: adds testing for multiple arguments

* update: removes links to example files

* update: removes DATEVALUE and TIMEVALUE xlsx tests
2025-11-06 22:56:14 +01:00
Elsa Minsut
f30f6864e2 update: adds docs and xlsx tests for DEGREES and RADIANS functions (#507)
* update: adds DEGREES and RADIANS documentation pages

* update: adds DEGREES and RADIANS xlsx tests

* update: Math and Trigonometry main page links

* update: removes links to missing example file
2025-11-06 22:55:28 +01:00
Nicolás Hatcher Andrés
d4f69f2ec2 UPDATE: Adds missing information functions (#514)
* UPDATE: Adds missing information functions

Implements N, CELL, INFO and SHEETS

Note that INFO is implemented as N/IMPL! and CELL is not implemented
for those values that is not implemented in Excel for the web

* FIX: Copilot fixes

* FIX: Make clippy happy
2025-11-06 18:58:39 +01:00
Daniel González-Albo
3d265bba27 update: in the app, add missing favicons and use dynamic title (#508)
* update: adds multiple favicon options to the app

* update: uses the current workbook name in as page title

* update: replace favicons in assets
2025-11-05 20:54:39 +01:00
Nicolás Hatcher Andrés
68a33a5f87 UPDATE: Adds COMBIN, COMBINA and SUMSQ (#511) 2025-11-04 22:16:16 +01:00
Nicolás Hatcher Andrés
e5854ab3d7 UPDATE: Adds ARABIC and ROMAN (#509) 2025-11-03 23:44:22 +01:00
Nicolás Hatcher Andrés
7f57826371 UPDATE: Implements BASE and DECIMAL (#504) 2025-11-02 23:30:43 +01:00
Daniel González-Albo
8b7fdce278 style: widget footer improvements (#503)
* fix: add menu items to translation file

* style: tooltips, icons and paddings in footer

* style: beautify link to main site
2025-11-02 19:59:13 +01:00
Nicolás Hatcher Andrés
3e2b177ffe UPDATE: Adds GCD and LCM functions (#502)
* UPDATE: Adds GCD and LCM functions

They follow SUM and accept arrays

* FIX: Implement copilot suggestions
2025-11-02 19:50:58 +01:00
Nicolás Hatcher Andrés
efb3b66777 UPDATE: Adds time formats (#501)
* UPDATE: Adds time formats

This is the initial implementation of time formats. Simple things like:

"hh:mm:ss AM/PM"

works

* FIX: Correct padded vs unppadded time formats

Thank you copilot!
2025-11-02 13:18:26 +01:00
Nicolás Hatcher Andrés
b2d848ae2a UPDATE: Adds a bunch of mathematical functions (#496) 2025-11-01 19:32:49 +01:00
Nicolás Hatcher Andrés
c8ae835bbe UPDATE: Adds unit tests for DEGREES and RADIANS (#495) 2025-11-01 11:23:29 +01:00
Nicolás Hatcher Andrés
6ce4756d55 UPDATE: Adds DEGREES and RADIANS (#493) 2025-10-30 23:45:29 +01:00
Nicolás Hatcher Andrés
a768bc5974 Bugfix/nicolas bufixes (#491)
* UPDATE: package lock

* FIX: Add function definitions

* FIX: Small fix to get FACT working

* FIX: We only need integer FACT and FACTDOUBLE

* FIX: Make clippy happy
2025-10-30 23:24:47 +01:00
Nicolás Hatcher Andrés
7e379e24e7 UPDATE: Adds simple functions (#489)
Exp, Fact, Factdouble and sign
2025-10-30 18:28:07 +01:00
Nicolás Hatcher Andrés
f2f4992230 UPDATE: Add some missing trigonometric functions (#487)
Acot, Acoth, Cot, Coth, Csc, Csch, Sec, Sech,
2025-10-30 17:38:02 +01:00
Nicolás Hatcher Andrés
a890865eaf FIX: Quote sheet names properly (#486)
Fixes #485
2025-10-29 23:26:18 +01:00
Nicolás Hatcher Andrés
1edfb2df1c FIX: Correct order when stringify -(A1^1.22) and (-A1)^1.22 (#484)
Fixes #483
2025-10-27 19:09:31 +01:00
Nicolás Hatcher Andrés
c88bcb94ae FIX: Uses a dump randomUUID in non secure environmentes (#482)
Fixes #480
2025-10-25 17:25:29 +02:00
Nicolás Hatcher Andrés
371bec2805 FIX: Add image info (#479) 2025-10-24 22:11:39 +02:00
Elsa Minsut
92527b5e92 update: fixes to Date and Time main page (#477) 2025-10-22 12:22:24 +02:00
Nicolás Hatcher Andrés
f6b7af3555 FIX: Updates docs and minor fixes (#474) 2025-10-22 02:30:28 +02:00
Nicolás Hatcher Andrés
fad8bc7d0c UPDATE: Update release to 0.6.0 (#463)
Should we have a single version number at the root?
2025-10-19 18:32:07 +02:00
Nicolás Hatcher Andrés
bbba875da3 FIX: Cleanup CSS code (#462) 2025-10-19 17:57:26 +02:00
Daniel González-Albo
1b34db0bc3 Merge pull request #455 from ironcalc/empty-fix
FIX: Evaluate after deleting content in the user API
2025-10-19 17:25:48 +02:00
Daniel González-Albo
d9aac1d77c Merge pull request #460 from ironcalc/xfln
FIX: DAYS and ISOWEEKNUM are "XLFN" functions
2025-10-19 17:24:40 +02:00
Nicolás Hatcher Andrés
d429bd8f60 FIX: Remove transition so there is no close drawer glitch (#459) 2025-10-19 17:12:49 +02:00
Nicolás Hatcher
2dbc3f4790 FIX: DAYS and ISOWEEKNUM are "XLFN" functions 2025-10-19 17:12:12 +02:00
Daniel González-Albo
292ecafb31 Merge pull request #458 from ironcalc/name-sync
FIX: sync changes of the localstorage in the left drawer
2025-10-19 16:13:38 +02:00
Daniel González-Albo
ead4bc713c docs: update the section 'managing workbooks' with new info about the left sidebar (#457) 2025-10-19 16:11:59 +02:00
Nicolás Hatcher
a9748eafec FIX: sync changes of the localstorage in the left drawer
This is a bit of a HACK. going a bit "against" React philosophy.
2025-10-19 16:05:50 +02:00
Nicolás Hatcher Andrés
330a018202 FIX: Adds test for TIME/HOUR/MINUTE/SECOND (#456) 2025-10-19 15:38:11 +02:00
Daniel González-Albo
d9812876e2 update: show which format is active in FormatMenu (#450)
* update: show which format is active in formatmenu

* update: requested fixes
2025-10-19 12:44:58 +02:00
Nicolás Hatcher
895244ed11 FIX: Evaluate after deleting content in the user API 2025-10-19 12:41:05 +02:00
Daniel González-Albo
f2da24326b update: Add a left drawer to improve workbook management (#453)
* update: add leftbar to app

* style: a few cosmetic changes

* update: allow pinning workbooks

* style: show ellipsis button only on hover

* update: add basic responsiveness

* style: use active state when file and help menus are open

* style: increase transition time

* update: allow duplication of workbooks

* chore: standardize menus
2025-10-19 10:20:31 +02:00
Brian Hung
dd4467f95d date time functions (#425)
* merge networkdays, networkdays.intl #33

* merge time, timevalue, hour, minute, second #35

* merge datedif, datevalue #36

* merge days, days360, weekday, weeknum, workday, workday.intl, yearfrac, isoweeknum #41

* from excel helper

* fix build

* date time macros

* de-dupe weekend

* serial helper

* de-dupe now today

* weekend pattern enum

* remove unused clippy wrong self

* fix docs

* add test coverage

* fix build

* fix cursor comment

* PR coments + xlsx date time
2025-10-19 10:19:19 +02:00
Nicolás Hatcher Andrés
29989b9fd7 UPDATE: Add info in README about the Dockerfile (#452)
* UPDATE: Add info in README about the Dockerfile

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-15 22:51:18 +02:00
Nicolás Hatcher
1efc921ce6 UPDATE: First Docker images! 2025-10-15 22:19:25 +02:00
Nicolás Hatcher
ed64716f0f FIX: Refactor both dialogs to get common code 2025-10-12 14:23:12 +02:00
Nicolás Hatcher
dd29287c5a FIX: Lifts TemplateDialog to App.tsx 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
7841abe2d2 update: use a different dialog for templates only 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
49c3d1e03a chore: remove optional props 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b709041f9d chore: move new ic icon to icons folder 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b177a33815 fix: remove obsolete css prefixes 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b506ccf908 update: rename function 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
eb3e92ffd8 update: make dialogs content change depending on scenario 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
0b925a4d6a update: add a second New from template action to FileMenu 2025-10-12 14:23:12 +02:00
Nicolás Hatcher
6a3e37f4c1 FIX: Integration of Welcome dialog 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
2496227344 fix: lint 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
72355a5201 update: add a compontent for the list items 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
81901ec717 update: add a welcome dialog to the app 2025-10-12 14:23:12 +02:00
Dmitry S
aa664a95a1 Fix example in README to reference Model correctly.
Since https://github.com/ironcalc/IronCalc/pull/27, using
`base::model::Model` complains about `model` being a private modulde,
and `base::Model` should be used instead.
2025-10-08 08:56:03 +02:00
Daniel Gonzalez Albo
c1aa743763 fix: typo in format presets 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
6321030ac8 style: add offset to arrow's tooltips 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
c2c5751ee3 style: add nicer tooltips 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
6c27ae1355 update: show scroll arrows on narrow displays 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
7bcd978998 style: tiny adjustment in dividers 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
3f083d9882 adjust spacing and group button groups 2025-10-07 20:19:57 +02:00
Nicolás Hatcher
8844b80c51 FIX: cargo fmt issue 2025-09-28 17:33:17 +02:00
Nicolás Hatcher
0f8f345aae UPDATE: have xlsx_2_icalc specify its output name
This is nice for deployments
2025-09-28 17:33:17 +02:00
Nicolás Hatcher
3191e12b93 FIX: Make clippy happy 2025-09-28 17:33:17 +02:00
Nicolás Hatcher
61cecb7af5 FIX: Fixes case with unicode characters
This is an ugly bug in ugly code. Pretty much technical deb in here
2025-09-28 12:46:16 +02:00
Nicolás Hatcher
fdeae2c771 UPDATE: Add templates 2025-09-28 12:46:16 +02:00
Matt Lehrer
3e9c69f122 add model.evaluate() call 2025-09-28 12:03:28 +02:00
Tom
c1c43143cc Adds information about references and corrected syntax on Column
This is a test commit
2025-09-25 19:25:32 +02:00
Tom
763b43a590 UPDATE: Added documentation for the Column Function 2025-09-25 19:25:32 +02:00
Daniel González-Albo
8dbfe07392 Merge pull request #443 from elsaminsut/mathfunctions
docs: adds ATAN2, ASINH, ACOSH, ATANH documentation pages
2025-09-24 20:26:53 +02:00
Elsa Minsut
e39bfe912a docs: improve consistency in ATAN2, ASINH, ACOSH, ATANH documentation 2025-09-24 19:36:30 +02:00
Elsa Minsut
9bbf94e033 update: Math and Trigonometry main page links 2025-09-24 19:19:16 +02:00
Elsa Minsut
0194912845 update: adds ATANH documentation page 2025-09-24 18:05:18 +02:00
Elsa Minsut
1d4d84bb57 update: adds ASINH documentation page 2025-09-24 17:46:56 +02:00
Elsa Minsut
e841c17aca update: adds ACOSH documentation page 2025-09-24 17:19:37 +02:00
Elsa Minsut
f2c43f2070 update: adds ATAN2 documentation page 2025-09-24 17:06:20 +02:00
Nicolás Hatcher
32b1f8ef4e FIX: Update documentation of some documented functions 2025-09-23 18:01:17 +02:00
Nicolás Hatcher
81e96f1401 FIX: Shift+Letter starts editing cell 2025-09-20 10:54:27 +02:00
Daniel González-Albo
aa4ecb2c89 Merge pull request #438 from elsaminsut/elsaminsut/hyperbolicfunctions
docs: adds SINH, COSH, TANH documentation pages
2025-09-15 16:28:18 +02:00
Elsa Minsut
e116d7d29f chore: retrigger CI 2025-09-15 16:15:18 +02:00
Elsa Minsut
cd75380923 update: links in general page, typo fix in ATAN 2025-09-13 16:53:05 +02:00
Elsa Minsut
79af9c6cb5 update: adds a ATAN page to documentation 2025-09-13 16:53:05 +02:00
Elsa Minsut
96fb0aaa96 update: adds a ACOS page to documentation 2025-09-13 16:53:05 +02:00
Elsa Minsut
03dfc151e2 update: adds ASIN documentation page 2025-09-13 16:53:05 +02:00
Andrew Grosser
d122f0cbd1 irconcalc code style enforcement 2025-09-13 09:35:35 +02:00
Andrew Grosser
1476e8f6da added dimensions
Signed-off-by: Andrew Grosser <dioptre@gmail.com>
2025-09-13 09:35:35 +02:00
Nicolás Hatcher
8ca73c6224 FIX: cargo fmt. Automatic fixes 2025-09-12 19:13:33 +02:00
Nicolás Hatcher
1017eef981 FIX: Cargo clippy. Manual fixes 2025-09-12 19:13:33 +02:00
Nicolás Hatcher
1981b0833a FIX: Clippy fix. Automatic fixes 2025-09-12 19:13:33 +02:00
Elsa Minsut
fb2f2a9fcf docs: updates Math and Trigonometry main page links 2025-09-08 19:38:48 +02:00
Elsa Minsut
91276b0f60 docs: adds TANH documentation page 2025-09-08 19:36:18 +02:00
Elsa Minsut
ec841f2fd9 docs: format fix in SINH documentation page 2025-09-08 19:19:46 +02:00
Elsa Minsut
8ebcb5dcb9 docs: adds COSH documentation page 2025-09-08 19:17:21 +02:00
Elsa Minsut
f7a3b95db5 update: adds SINH documentation page 2025-08-20 18:02:23 +02:00
Daniel González-Albo
911175f0d2 Merge pull request #434 from elsaminsut/elsaminsut/TAN-typo
docs: fix typo in TAN function page
2025-08-12 18:19:48 +02:00
Elsa Minsut
4d75f6b5c0 fix: typo in TAN function page 2025-08-12 18:04:23 +02:00
Nicolás Hatcher
f3f59dbda7 FIX: Make fmt happy 2025-08-09 10:43:19 +02:00
Nicolás Hatcher
f581ce5570 FIX: Update xlsx test for ROUND, ROUNDUP 2025-08-09 10:43:19 +02:00
Nicolás Hatcher
429615ae85 FIX: Fixes stringify with parentheses 2025-08-09 10:43:19 +02:00
Nicolás Hatcher
f2cb05d7bf FIX: Fixes ROUND, ROUNDUP and ROUNDDOWN behaviour 2025-08-09 10:43:19 +02:00
Nicolás Hatcher
71d6a3d66c FIX: Make copilot happy
Oh wait, that's a thing now?
2025-08-02 16:11:38 +02:00
Nicolás Hatcher
07854f1593 UPDATE: Python bindings for the user API 2025-08-02 16:11:38 +02:00
Nicolás Hatcher
faa0ff9b69 FIX: Minimal support for inlineStr
Fixes #424
2025-08-02 12:55:56 +02:00
Nicolás Hatcher
b9b3cb1628 FIX: Lint issues 2025-07-26 15:00:22 +02:00
Nicolás Hatcher
b157347e68 UPDATE: Add move/row column to the UI 2025-07-26 15:00:22 +02:00
Nicolás Hatcher
fb7886ca9e UPDATE: Add keyboard shortcuts for move column/row
Also clean up a bit of the  keyboard shortcuts mess
2025-07-26 15:00:22 +02:00
BrianHung
caf26194df feat: implement move column move row with tests 2025-07-26 10:37:03 +02:00
Brian Hung
e420f7e998 fix intermediate rows cols 2025-07-26 10:37:03 +02:00
BrianHung
d73b5ff12d call diff list in user model move row move col 2025-07-26 10:37:03 +02:00
BrianHung
d45e8fd56d add row height test case + nodejs bindings 2025-07-26 10:37:03 +02:00
BrianHung
c2777c73ac feat: implement move column move row with tests 2025-07-26 10:37:03 +02:00
Brian Hung
7dc49d5dd7 fully deprecate old single row col methods 2025-07-24 22:51:39 +02:00
Brian Hung
183d04b923 fix build 2025-07-24 22:51:39 +02:00
Brian Hung
037766c744 feat: add bulk diff types for insert/delete row/column operations 2025-07-24 22:51:39 +02:00
BrianHung
d5ccd9dbdd fix build 2025-07-24 22:51:39 +02:00
Brian Hung
3f1f2bb896 add validation 2025-07-24 22:51:39 +02:00
Brian Hung
a2181a5a48 fix empty row deletion 2025-07-24 22:51:39 +02:00
Brian Hung
b07603b728 fix deleting empty row 2025-07-24 22:51:39 +02:00
Brian Hung
fe87dc49b4 allow panic 2025-07-24 22:51:39 +02:00
Brian Hung
b4349ff5da fix diff generation and add test coverage 2025-07-24 22:51:39 +02:00
BrianHung
51f2da8663 deprecate singlular case insert delete rows columns 2025-07-24 22:51:39 +02:00
BrianHung
87cdfb2ba1 add tests for user model insert delete rows columns 2025-07-24 22:51:39 +02:00
BrianHung
d7113622e7 deduplicate single insert delete row column 2025-07-24 22:51:39 +02:00
BrianHung
2d23f5d4e4 feat: insert delete rows cols 2025-07-24 22:51:39 +02:00
Nicolás Hatcher
56abac79ca FIX: Skip cells that have already been computed 2025-07-19 09:07:11 +02:00
Nicolás Hatcher
7193c9bf1b FIX: Identify off the view port cells to the left correctly 2025-07-19 09:07:11 +02:00
Nicolás Hatcher
266c14d5d2 FIX: Apply copilot suggestions 2025-07-19 09:07:11 +02:00
Nicolás Hatcher
9852ce2504 UPDATE: Text spills now to adjacent cells if needed 2025-07-19 09:07:11 +02:00
Nicolás Hatcher
107fc99409 FIX: $isActive is a required property of FileMenuWrapper 2025-07-18 07:02:00 +02:00
Nicolás Hatcher
77bb7ebe0e FIX: Removes ununsed code 2025-07-18 07:02:00 +02:00
Daniel
f8af302413 update: change link destination 2025-07-18 07:02:00 +02:00
Daniel
c700101f35 fix: copilot suggestions 2025-07-18 07:02:00 +02:00
Daniel
5f659a2eb5 update: lint 2025-07-18 07:02:00 +02:00
Daniel
40baf16a73 update: adjustments in file menu 2025-07-18 07:02:00 +02:00
Daniel
61c71cd6f6 update: create a Help menu 2025-07-18 07:02:00 +02:00
Nicolás Hatcher
b99ddbaee2 FIX[docs]: Inlude Linux also in keyboadr shortcuts 2025-07-18 06:27:42 +02:00
Nicolás Hatcher
2428c6c89b FIX[docs]: Update dependencies 2025-07-18 06:27:42 +02:00
Nicolás Hatcher
46b1ade34a FIX: Adds sheet "block" navigation
This was a left over from the old days

Control+right arrow takes you to the next cell with text
or the end of the block

NB: Missing tests
2025-07-17 18:39:46 +02:00
Nicolás Hatcher
1eea2a8c46 FIX: apply copilot suggestion :) 2025-07-17 07:59:55 +02:00
Daniel
eb8b129431 update: in Type and Bucket buttons, use a colored div instead of a border 2025-07-17 07:59:55 +02:00
Daniel
4850b6518f docs: complete the Keyboar Shortcuts page 2025-07-17 07:59:33 +02:00
Nicolás Hatcher
77a784df86 FIX: Removes old icon references 2025-07-17 07:44:59 +02:00
Daniel González-Albo
57896f83ca Merge pull request #413 from ironcalc/update/dgac-decimal-icons 2025-07-14 00:37:43 +02:00
Daniel
cfaa373510 update: replace custom made icons with lucide's 2025-07-13 21:04:30 +02:00
Nicolás Hatcher
cc140b087d FIX: Export fronzen rows/columns properly 2025-07-13 12:44:21 +02:00
Nicolás Hatcher
42c651da29 FIX: Do not propagate on cut or double click in the editor 2025-07-13 12:41:24 +02:00
Nicolás Hatcher
2a5f001361 UPDATE: Adds LOG10 and LN for Elsa 2025-07-13 00:10:32 +02:00
Daniel
df913e73d4 fix: greys in theme palette 2025-07-08 21:33:18 +02:00
Nicolás Hatcher
198f3108ef FIX: Remove wrong content-type is sharing workbooks
This failed to share workbooks with emojis, for instance
2025-07-06 23:08:27 +02:00
Nicolás Hatcher
3a68145848 FIX: Copied csv had an extra line
Fixes #393
2025-07-04 23:30:51 +02:00
Nicolás Hatcher
5d7f4a31d6 FIX: Correct range when pasting csv tetx 2025-07-04 23:30:51 +02:00
Nicolás Hatcher
7c8cd22ad8 FIX: stop propagation on copy in the editor
Fixes #392
2025-07-04 18:18:38 +02:00
Nicolás Hatcher
84c3cf01ce FIX: Checks also that the sheet is the same
Fixes #395
2025-07-04 02:33:24 +02:00
Nicolás Hatcher
33e9894f9b FIX: Quote sheet names properly
This is a "Hot Fix". A proper fix should use the wasm module
2025-07-04 01:52:37 +02:00
Nicolás Hatcher
483cd43041 FIX: Removing styles of full columns or rows are a single diff list
Fixes #400
2025-07-04 01:43:30 +02:00
Nicolás Hatcher
b4aed93bbb FIX[workbook]: Default fill should be None
Fixes #398
2025-07-03 23:21:54 +02:00
Nicolás Hatcher
689f55effa FIX: Apply copilot hints 2025-07-03 20:40:48 +02:00
Nicolás Hatcher
b1327d83d4 UPDATE: Removes once_Cell dependency 2025-07-03 20:40:48 +02:00
Nicolás Hatcher
8f7798d088 FIX: Apply copilot suggestion 2025-07-03 20:02:33 +02:00
Nicolás Hatcher
df0012a1c4 FIX[webapp]: Allow downloading workbooks with unicode charaters 2025-07-03 20:02:33 +02:00
Nicolás Hatcher
8a9ae00cad FIX: Set the padding in coor swatches to 0 2025-06-29 11:20:22 +02:00
Nicolás Hatcher
97d3b04772 FIX: Prevent scroll when focus
This are fixes to make the widget embedable
2025-06-29 11:20:22 +02:00
Nicolás Hatcher
5744ae4d77 FIX: Cargo fmt 2025-06-29 11:07:05 +02:00
Nicolás Hatcher
0be7d9b85a FIX: Make clippy happy 2025-06-29 11:07:05 +02:00
Nicolás Hatcher
46ea92966f FIX[import]: Set color black for text properly
I would like to have a text case for this. Preferably automatic.

But manual is ok, maybe we should have a series of workbooks we check
before every large deployment
2025-06-24 00:26:16 +02:00
Nicolás Hatcher
a19124cc16 FIX[import xlsx]: Sets default text color to black
Default text color was transparent white. I am not sure how this didn't
cause more problems :)
2025-06-23 18:03:54 +02:00
Nicolás Hatcher
b0a5e2553a FIX: Include "tests" is the set of phony files
Ooops. First time this actually bites me....
2025-06-15 21:16:09 +02:00
Brian Hung
5ca50f15d7 feat: use wasm-bindgen unchecked_return_type 2025-06-14 11:11:50 +02:00
Brian Hung
03e227fbb2 fix grammar spelling 2025-06-08 08:49:03 +02:00
Nicolás Hatcher
2b3ae8e20f FIX: Remove some unused dependencies and updated dependencies 2025-06-07 15:32:23 +02:00
Nicolás Hatcher
138a483c65 UPDATE: Double click in the outline handle fills column
This also removes React from the equation.
So all event handling is done outside of the React loop.
This simplifies some things and helps us in a possible move
away from React.

This is closer to how we deal with the column and row handle resizers.

I think it works quite well and it is more future proof.

But TBH I just want to try it out and see what is the DX after this.

Fixes #359
2025-06-07 11:12:10 +02:00
Nicolás Hatcher
2eb9266c30 UPDATE: Factor out a couple of helper functions from the main canvas file 2025-06-07 11:12:10 +02:00
Nicolás Hatcher
b9d3f5329b FIX: There are two fills in every new Excel model
Excel expects two default fills. If a different fill is present Excel
removes it and substitutes it with the defaults.

This resulted in models having the default fill for the first style.

Thanks @scandel!
2025-06-05 06:36:34 +02:00
Nicolás Hatcher
af49d7ad96 FIX: Download to png off by one errors 2025-06-02 21:11:18 +02:00
Nicolás Hatcher
3e015bf13a FIX: control+shitf selects area 2025-06-02 20:59:18 +02:00
Nicolás Hatcher
a5d8ee9ef0 FIX: Download all selected area
We were previously downloading only the bounds of the visible cells,
without taking into account the frozen rows/colums.

Fixes #343
2025-05-17 11:49:42 +02:00
484 changed files with 34394 additions and 10289 deletions

View File

@@ -117,7 +117,7 @@ jobs:
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
with:
command: upload
args: "--skip-existing **/*.whl"
args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python
publish-pypi:
@@ -137,5 +137,5 @@ jobs:
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
with:
command: upload
args: "--skip-existing **/*.whl"
args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python

89
Cargo.lock generated
View File

@@ -43,6 +43,15 @@ dependencies = [
"libc",
]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -414,7 +423,7 @@ dependencies = [
[[package]]
name = "ironcalc"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"bitcode",
"chrono",
@@ -430,25 +439,25 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"bitcode",
"chrono",
"chrono-tz",
"csv",
"js-sys",
"once_cell",
"rand",
"regex",
"regex-lite",
"ryu",
"serde",
"serde_json",
"statrs",
]
[[package]]
name = "ironcalc_nodejs"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"ironcalc",
"napi",
@@ -721,11 +730,10 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.23.4"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
@@ -739,9 +747,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.23.4"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
dependencies = [
"once_cell",
"target-lexicon",
@@ -749,9 +757,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.23.4"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
dependencies = [
"libc",
"pyo3-build-config",
@@ -759,9 +767,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.23.4"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -771,9 +779,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.23.4"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
dependencies = [
"heck",
"proc-macro2",
@@ -784,8 +792,9 @@ dependencies = [
[[package]]
name = "pyroncalc"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"bitcode",
"ironcalc",
"pyo3",
"serde",
@@ -872,6 +881,12 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.17"
@@ -960,6 +975,16 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "statrs"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
dependencies = [
"approx",
"num-traits",
]
[[package]]
name = "subtle"
version = "2.5.0"
@@ -979,9 +1004,9 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.16"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
@@ -1070,7 +1095,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"ironcalc_base",
"serde",
@@ -1081,23 +1106,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
@@ -1118,9 +1144,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1128,9 +1154,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
@@ -1141,9 +1167,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"

93
Dockerfile Normal file
View File

@@ -0,0 +1,93 @@
FROM rust:latest AS builder
WORKDIR /app
COPY . .
# Tools + wasm toolchain + Node via nvm
RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl ca-certificates make \
&& rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& bash -lc "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash -" \
&& bash -lc '\
export NVM_DIR="$HOME/.nvm" && \
source "$NVM_DIR/nvm.sh" && \
nvm install 22 && nvm alias default 22 && \
nroot="$NVM_DIR/versions/node/$(nvm version default)/bin" && \
ln -sf "$nroot/node" /usr/local/bin/node && \
ln -sf "$nroot/npm" /usr/local/bin/npm && \
ln -sf "$nroot/npx" /usr/local/bin/npx \
' \
&& npm install typescript \
&& rm -rf /var/lib/apt/lists/*
# build the server
RUN cargo build --release --manifest-path webapp/app.ironcalc.com/server/Cargo.toml
# build the wasm
RUN make -C bindings/wasm
# build the widget
WORKDIR /app/webapp/IronCalc
RUN npm install && npm run build
# build the frontend app
WORKDIR /app/webapp/app.ironcalc.com/frontend
RUN npm install && npm run build
# build the xlsx_2_icalc binary (we don't need the release version here)
WORKDIR /app/xlsx
RUN cargo build
WORKDIR /app
# copy the artifacts to a dist/ directory
RUN mkdir dist
RUN mkdir dist/frontend
RUN cp -r webapp/app.ironcalc.com/frontend/dist/* dist/frontend/
RUN mkdir dist/server
RUN cp webapp/app.ironcalc.com/server/target/release/ironcalc_server dist/server/
RUN cp webapp/app.ironcalc.com/server/Rocket.toml dist/server/
RUN cp webapp/app.ironcalc.com/server/ironcalc.sqlite dist/server/
# Create ic files in docs
RUN mkdir -p dist/frontend/models
# Loop over all xlsx files in xlsx/tests/docs & templates and convert them to .ic
RUN bash -lc 'set -euo pipefail; \
mkdir -p dist/frontend/models; \
shopt -s nullglob; \
for xlsx_file in xlsx/tests/docs/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done; \
for xlsx_file in xlsx/tests/templates/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done'
# ---------- server runtime ----------
FROM debian:bookworm-slim AS server-runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy EVERYTHING you put in dist/server (binary + Rocket.toml + DB)
COPY --from=builder /app/dist/server/ ./
# Make sure Rocket binds to the container IP; explicitly point to the config file
ENV ROCKET_ADDRESS=0.0.0.0 \
ROCKET_PORT=8000 \
ROCKET_CONFIG=/app/Rocket.toml
EXPOSE 8000
# Run from /app so relative paths in Rocket.toml/DB work
CMD ["./ironcalc_server"]
# ---------- caddy runtime (serves frontend + reverse-proxy /api) ----------
FROM caddy:latest AS caddy-runtime
WORKDIR /srv
# Copy the frontend build output to /srv
COPY --from=builder /app/dist/frontend/ /srv/
# Copy the Caddyfile
COPY --from=builder /app/webapp/app.ironcalc.com/Caddyfile.compose /etc/caddy/Caddyfile

View File

@@ -31,7 +31,17 @@ This repository contains the main engine and the xlsx reader and writer.
Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go.
We will build different _skins_: in the terminal, as a desktop application or use it in you own web application.
We will build different _skins_: in the terminal, as a desktop application or use it in your own web application.
# Docker
If you have docker installed just run:
```bash
docker compose up --build
```
head over to <http://localhost:2080> to test the application.
# Building
@@ -84,7 +94,7 @@ And then use this code in `main.rs`:
```rust
use ironcalc::{
base::{expressions::utils::number_to_column, model::Model},
base::{expressions::utils::number_to_column, Model},
export::save_to_xlsx,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,6 +1,6 @@
[package]
name = "ironcalc_base"
version = "0.5.0"
version = "0.6.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
homepage = "https://www.ironcalc.com"
@@ -17,9 +17,9 @@ chrono = "0.4"
chrono-tz = "0.10"
regex = { version = "1.0", optional = true}
regex-lite = { version = "0.1.6", optional = true}
once_cell = "1.16.0"
bitcode = "0.6.3"
csv = "1.3.0"
statrs = { version = "0.18.0", default-features = false, features = [] }
[features]
default = ["use_regex_full"]

View File

@@ -1,7 +1,7 @@
use ironcalc_base::{types::CellType, Model};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC", "en")?;
// A1
model.set_user_input(0, 1, 1, "1".to_string())?;
// A2

View File

@@ -1,7 +1,7 @@
use ironcalc_base::{cell::CellValue, Model};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut model = Model::new_empty("hello-world", "en", "UTC")?;
let mut model = Model::new_empty("hello-world", "en", "UTC", "en")?;
// A1
model.set_user_input(0, 1, 1, "Hello".to_string())?;
// B1

View File

@@ -1,5 +1,7 @@
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData};
use crate::expressions::parser::stringify::{
to_localized_string, to_string_displaced, DisplaceData,
};
use crate::expressions::types::CellReferenceRC;
use crate::model::Model;
@@ -8,7 +10,7 @@ use crate::model::Model;
// In IronCalc, if one of the edges of the range is deleted will replace the edge with #REF!
// I feel this is unimportant for now.
impl Model {
impl<'a> Model<'a> {
fn shift_cell_formula(
&mut self,
sheet: u32,
@@ -29,7 +31,7 @@ impl Model {
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
let formula = to_localized_string(node, &cell_reference, self.locale, self.language);
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
if formula != formula_displaced {
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
@@ -108,7 +110,9 @@ impl Model {
// FIXME: we need some user_input getter instead of get_text
let formula_or_value = self
.get_cell_formula(sheet, source_row, source_column)?
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
.unwrap_or_else(|| {
source_cell.get_localized_text(&self.workbook.shared_strings, self.language)
});
self.set_user_input(sheet, target_row, target_column, formula_or_value)?;
self.workbook
.worksheet_mut(sheet)?
@@ -212,6 +216,12 @@ impl Model {
if column_count <= 0 {
return Err("Please use insert columns instead".to_string());
}
if !(1..=LAST_COLUMN).contains(&column) {
return Err(format!("Column number '{column}' is not valid."));
}
if column + column_count - 1 > LAST_COLUMN {
return Err("Cannot delete columns beyond the last column of the sheet".to_string());
}
// first column being deleted
let column_start = column;
@@ -384,6 +394,13 @@ impl Model {
if row_count <= 0 {
return Err("Please use insert rows instead".to_string());
}
if !(1..=LAST_ROW).contains(&row) {
return Err(format!("Row number '{row}' is not valid."));
}
if row + row_count - 1 > LAST_ROW {
return Err("Cannot delete rows beyond the last row of the sheet".to_string());
}
// Move cells
let worksheet = &self.workbook.worksheet(sheet)?;
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
@@ -444,7 +461,7 @@ impl Model {
/// * Column is one of the extremes of the range. The new extreme would be target_column.
/// Range is then normalized
/// * Any other case, range is left unchanged.
/// NOTE: This does NOT move the data in the columns or move the colum styles
/// NOTE: This moves the data and column styles along with the formulas
pub fn move_column_action(
&mut self,
sheet: u32,
@@ -460,7 +477,72 @@ impl Model {
return Err("Initial column out of boundaries".to_string());
}
// TODO: Add the actual displacement of data and styles
if delta == 0 {
return Ok(());
}
// Preserve cell contents, width and style of the column being moved
let original_refs = self
.workbook
.worksheet(sheet)?
.column_cell_references(column)?;
let mut original_cells = Vec::new();
for r in &original_refs {
let cell = self
.workbook
.worksheet(sheet)?
.cell(r.row, column)
.ok_or("Expected Cell to exist")?;
let style_idx = cell.get_style();
let formula_or_value =
self.get_cell_formula(sheet, r.row, column)?
.unwrap_or_else(|| {
cell.get_localized_text(&self.workbook.shared_strings, self.language)
});
original_cells.push((r.row, formula_or_value, style_idx));
self.cell_clear_all(sheet, r.row, column)?;
}
let width = self.workbook.worksheet(sheet)?.get_column_width(column)?;
let style = self.workbook.worksheet(sheet)?.get_column_style(column)?;
if delta > 0 {
for c in column + 1..=target_column {
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
for r in refs {
self.move_cell(sheet, r.row, c, r.row, c - 1)?;
}
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(c - 1, w, s)?;
}
} else {
for c in (target_column..=column - 1).rev() {
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
for r in refs {
self.move_cell(sheet, r.row, c, r.row, c + 1)?;
}
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(c + 1, w, s)?;
}
}
for (r, value, style_idx) in original_cells {
self.set_user_input(sheet, r, target_column, value)?;
self.workbook
.worksheet_mut(sheet)?
.set_cell_style(r, target_column, style_idx)?;
}
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(target_column, width, style)?;
// Update all formulas in the workbook
self.displace_cells(
@@ -473,4 +555,88 @@ impl Model {
Ok(())
}
/// Displaces cells due to a move row action
/// from initial_row to target_row = initial_row + row_delta
/// References will be updated following the same rules as move_column_action
/// NOTE: This moves the data and row styles along with the formulas
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
// Check boundaries
let target_row = row + delta;
if !(1..=LAST_ROW).contains(&target_row) {
return Err("Target row out of boundaries".to_string());
}
if !(1..=LAST_ROW).contains(&row) {
return Err("Initial row out of boundaries".to_string());
}
if delta == 0 {
return Ok(());
}
let original_cols = self.get_columns_for_row(sheet, row, false)?;
let mut original_cells = Vec::new();
for c in &original_cols {
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, *c)
.ok_or("Expected Cell to exist")?;
let style_idx = cell.get_style();
let formula_or_value = self.get_cell_formula(sheet, row, *c)?.unwrap_or_else(|| {
cell.get_localized_text(&self.workbook.shared_strings, self.language)
});
original_cells.push((*c, formula_or_value, style_idx));
self.cell_clear_all(sheet, row, *c)?;
}
if delta > 0 {
for r in row + 1..=target_row {
let cols = self.get_columns_for_row(sheet, r, false)?;
for c in cols {
self.move_cell(sheet, r, c, r - 1, c)?;
}
}
} else {
for r in (target_row..=row - 1).rev() {
let cols = self.get_columns_for_row(sheet, r, false)?;
for c in cols {
self.move_cell(sheet, r, c, r + 1, c)?;
}
}
}
for (c, value, style_idx) in original_cells {
self.set_user_input(sheet, target_row, c, value)?;
self.workbook
.worksheet_mut(sheet)?
.set_cell_style(target_row, c, style_idx)?;
}
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_rows = Vec::new();
for r in worksheet.rows.iter() {
if r.r == row {
let mut nr = r.clone();
nr.r = target_row;
new_rows.push(nr);
} else if delta > 0 && r.r > row && r.r <= target_row {
let mut nr = r.clone();
nr.r -= 1;
new_rows.push(nr);
} else if delta < 0 && r.r < row && r.r >= target_row {
let mut nr = r.clone();
nr.r += 1;
new_rows.push(nr);
} else {
new_rows.push(r.clone());
}
}
worksheet.rows = new_rows;
// Update all formulas in the workbook
self.displace_cells(&(DisplaceData::RowMove { sheet, row, delta }))?;
Ok(())
}
}

View File

@@ -22,7 +22,7 @@ fn to_f64(value: &ArrayNode) -> Result<f64, Error> {
}
}
impl Model {
impl<'a> Model<'a> {
/// Applies `op` elementwise for arrays/numbers.
pub(crate) fn handle_arithmetic(
&mut self,

View File

@@ -5,6 +5,7 @@ use crate::{
token::Error,
types::CellReferenceIndex,
},
formatter::format::parse_formatted_number,
model::Model,
};
@@ -13,7 +14,32 @@ pub(crate) enum NumberOrArray {
Array(Vec<Vec<ArrayNode>>),
}
impl Model {
impl<'a> Model<'a> {
pub(crate) fn cast_number(&self, s: &str) -> Option<f64> {
match s.trim().parse::<f64>() {
Ok(f) => Some(f),
_ => {
let currency = &self.locale.currency.symbol;
let mut currencies = vec!["$", ""];
if !currencies.iter().any(|e| *e == currency) {
currencies.push(currency);
}
let (decimal_separator, group_separator) =
if self.locale.numbers.symbols.decimal == "," {
(b',', b'.')
} else {
(b'.', b',')
};
// Try to parse as a formatted number (e.g., dates, currencies, percentages)
if let Ok((v, _number_format)) =
parse_formatted_number(s, &currencies, decimal_separator, group_separator)
{
return Some(v);
}
None
}
}
}
pub(crate) fn get_number_or_array(
&mut self,
node: &Node,
@@ -21,9 +47,9 @@ impl Model {
) -> Result<NumberOrArray, CalcResult> {
match self.evaluate_node_in_context(node, cell) {
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
CalcResult::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(NumberOrArray::Number(f)),
_ => Err(CalcResult::new_error(
CalcResult::String(s) => match self.cast_number(&s) {
Some(f) => Ok(NumberOrArray::Number(f)),
None => Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expecting number".to_string(),
@@ -89,16 +115,16 @@ impl Model {
self.cast_to_number(result, cell)
}
fn cast_to_number(
pub(crate) fn cast_to_number(
&mut self,
result: CalcResult,
cell: CellReferenceIndex,
) -> Result<f64, CalcResult> {
match result {
CalcResult::Number(f) => Ok(f),
CalcResult::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(f),
_ => Err(CalcResult::new_error(
CalcResult::String(s) => match self.cast_number(&s) {
Some(f) => Ok(f),
None => Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expecting number".to_string(),
@@ -159,7 +185,7 @@ impl Model {
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
// See function Exact
match result {
CalcResult::Number(f) => Ok(format!("{}", f)),
CalcResult::Number(f) => Ok(format!("{f}")),
CalcResult::String(s) => Ok(s),
CalcResult::Boolean(f) => {
if f {

View File

@@ -122,11 +122,17 @@ impl Cell {
}
}
pub fn get_text(&self, shared_strings: &[String], language: &Language) -> String {
pub fn get_localized_text(&self, shared_strings: &[String], language: &Language) -> String {
match self.value(shared_strings, language) {
CellValue::None => "".to_string(),
CellValue::String(v) => v,
CellValue::Boolean(v) => v.to_string().to_uppercase(),
CellValue::Boolean(v) => {
if v {
language.booleans.r#true.to_string()
} else {
language.booleans.r#false.to_string()
}
}
CellValue::Number(v) => to_excel_precision_str(v),
}
}
@@ -171,7 +177,13 @@ impl Cell {
match self.value(shared_strings, language) {
CellValue::None => "".to_string(),
CellValue::String(value) => value,
CellValue::Boolean(value) => value.to_string().to_uppercase(),
CellValue::Boolean(value) => {
if value {
language.booleans.r#true.to_string()
} else {
language.booleans.r#false.to_string()
}
}
CellValue::Number(value) => format_number(value),
}
}

View File

@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
pub(crate) const LAST_COLUMN: i32 = 16_384;
pub(crate) const LAST_ROW: i32 = 1_048_576;
// Excel uses 15 significant digits of precision for all numeric calculations.
pub(crate) const EXCEL_PRECISION: usize = 15;
// 693_594 is computed as:
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug

View File

@@ -79,19 +79,24 @@ pub enum LexerMode {
/// Tokenize an input
#[derive(Clone)]
pub struct Lexer {
pub struct Lexer<'a> {
position: usize,
next_token_position: Option<usize>,
len: usize,
chars: Vec<char>,
mode: LexerMode,
locale: Locale,
language: Language,
locale: &'a Locale,
language: &'a Language,
}
impl Lexer {
impl<'a> Lexer<'a> {
/// Creates a new `Lexer` that returns the tokens of a formula.
pub fn new(formula: &str, mode: LexerMode, locale: &Locale, language: &Language) -> Lexer {
pub fn new(
formula: &str,
mode: LexerMode,
locale: &'a Locale,
language: &'a Language,
) -> Lexer<'a> {
let chars: Vec<char> = formula.chars().collect();
let len = chars.len();
Lexer {
@@ -100,8 +105,8 @@ impl Lexer {
next_token_position: None,
len,
mode,
locale: locale.clone(),
language: language.clone(),
locale,
language,
}
}
@@ -110,6 +115,16 @@ impl Lexer {
self.mode = mode;
}
/// Sets the locale
pub fn set_locale(&mut self, locale: &'a Locale) {
self.locale = locale;
}
/// Sets the language
pub fn set_language(&mut self, language: &'a Language) {
self.language = language;
}
// FIXME: I don't think we should have `is_a1_mode` and `get_formula`.
// The caller already knows those two
@@ -142,7 +157,7 @@ impl Lexer {
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
let nt = self.next_token();
if mem::discriminant(&nt) != mem::discriminant(&tk) {
return Err(self.set_error(&format!("Error, expected {:?}", tk), self.position));
return Err(self.set_error(&format!("Error, expected {tk:?}"), self.position));
}
Ok(())
}
@@ -188,6 +203,7 @@ impl Lexer {
':' => TokenType::Colon,
';' => TokenType::Semicolon,
'@' => TokenType::At,
'\\' => TokenType::Backslash,
',' => {
if self.locale.numbers.symbols.decimal == "," {
match self.consume_number(',') {
@@ -314,6 +330,9 @@ impl Lexer {
} else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false);
}
if self.peek_char() == Some('(') {
return TokenType::Ident(name);
}
if self.mode == LexerMode::A1 {
let parsed_reference = utils::parse_reference_a1(&name_upper);
if parsed_reference.is_some()
@@ -511,7 +530,7 @@ impl Lexer {
self.position = position;
chars.parse::<i32>().map_err(|_| LexerError {
position,
message: format!("Failed to parse to int: {}", chars),
message: format!("Failed to parse to int: {chars}"),
})
}
@@ -572,9 +591,7 @@ impl Lexer {
}
self.position = position;
match chars.parse::<f64>() {
Err(_) => {
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
}
Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)),
Ok(v) => Ok(v),
}
}

View File

@@ -4,7 +4,7 @@ use crate::expressions::{token::TokenType, utils::column_to_number};
use super::Lexer;
use super::{ParsedRange, ParsedReference, Result};
impl Lexer {
impl<'a> Lexer<'a> {
/// Consumes a reference in A1 style like:
/// AS23, $AS23, AS$23, $AS$23, R12
/// Or returns an error
@@ -148,15 +148,16 @@ impl Lexer {
let row_left = match row_left.parse::<i32>() {
Ok(n) => n,
Err(_) => {
return Err(self
.set_error(&format!("Failed parsing row {}", row_left), position))
return Err(
self.set_error(&format!("Failed parsing row {row_left}"), position)
)
}
};
let row_right = match row_right.parse::<i32>() {
Ok(n) => n,
Err(_) => {
return Err(self
.set_error(&format!("Failed parsing row {}", row_right), position))
.set_error(&format!("Failed parsing row {row_right}"), position))
}
};
if row_left > LAST_ROW {

View File

@@ -16,7 +16,7 @@ use crate::expressions::token::{TableReference, TableSpecifier};
use super::Result;
use super::{Lexer, LexerError};
impl Lexer {
impl<'a> Lexer<'a> {
fn consume_table_specifier(&mut self) -> Result<Option<TableSpecifier>> {
if self.peek_char() == Some('#') {
// It's a specifier

View File

@@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::expressions::utils::column_to_number;
use crate::language::get_language;
use crate::locale::get_locale;
@@ -10,7 +11,7 @@ use crate::expressions::{
types::ParsedReference,
};
fn new_lexer(formula: &str, a1_mode: bool) -> Lexer {
fn new_lexer(formula: &str, a1_mode: bool) -> Lexer<'_> {
let locale = get_locale("en").unwrap();
let language = get_language("en").unwrap();
let mode = if a1_mode {
@@ -654,7 +655,9 @@ fn test_comma() {
// Used for testing locales where the comma is the decimal separator
let mut lx = new_lexer("12,34", false);
lx.locale.numbers.symbols.decimal = ",".to_string();
let locale = get_locale("de").unwrap();
lx.locale = locale;
assert_eq!(lx.next_token(), Number(12.34));
assert_eq!(lx.next_token(), EOF);
}
@@ -685,3 +688,29 @@ fn test_comparisons() {
assert_eq!(lx.next_token(), Number(7.0));
assert_eq!(lx.next_token(), EOF);
}
#[test]
fn test_log10_is_cell_reference() {
let mut lx = new_lexer("LOG10", true);
assert_eq!(
lx.next_token(),
Reference {
sheet: None,
column: column_to_number("LOG").unwrap(),
row: 10,
absolute_column: false,
absolute_row: false,
}
);
assert_eq!(lx.next_token(), EOF);
}
#[test]
fn test_log10_is_function() {
let mut lx = new_lexer("LOG10(100)", true);
assert_eq!(lx.next_token(), Ident("LOG10".to_string()));
assert_eq!(lx.next_token(), LeftParenthesis);
assert_eq!(lx.next_token(), Number(100.0));
assert_eq!(lx.next_token(), RightParenthesis);
assert_eq!(lx.next_token(), EOF);
}

View File

@@ -7,7 +7,7 @@ use crate::expressions::{
use crate::language::get_language;
use crate::locale::get_locale;
fn new_lexer(formula: &str) -> Lexer {
fn new_lexer(formula: &str) -> Lexer<'_> {
let locale = get_locale("en").unwrap();
let language = get_language("en").unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)

View File

@@ -6,11 +6,11 @@ use crate::{
token::{Error, TokenType},
},
language::get_language,
locale::get_locale,
locale::get_default_locale,
};
fn new_language_lexer(formula: &str, language: &str) -> Lexer {
let locale = get_locale("en").unwrap();
fn new_language_lexer<'a>(formula: &str, language: &str) -> Lexer<'a> {
let locale = get_default_locale();
let language = get_language(language).unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)
}

View File

@@ -9,7 +9,7 @@ use crate::{
locale::get_locale,
};
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
fn new_language_lexer<'a>(formula: &str, locale: &str, language: &str) -> Lexer<'a> {
let locale = get_locale(locale).unwrap();
let language = get_language(language).unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)

View File

@@ -10,7 +10,7 @@ use crate::expressions::{
use crate::language::get_language;
use crate::locale::get_locale;
fn new_lexer(formula: &str) -> Lexer {
fn new_lexer(formula: &str) -> Lexer<'_> {
let locale = get_locale("en").unwrap();
let language = get_language("en").unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)

View File

@@ -7,7 +7,7 @@ use crate::expressions::{
use crate::language::get_language;
use crate::locale::get_locale;
fn new_lexer(formula: &str) -> Lexer {
fn new_lexer(formula: &str) -> Lexer<'_> {
let locale = get_locale("en").unwrap();
let language = get_language("en").unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)

View File

@@ -31,8 +31,12 @@ f_args => e (',' e)*
use std::collections::HashMap;
use crate::functions::Function;
use crate::language::get_default_language;
use crate::language::get_language;
use crate::language::Language;
use crate::locale::get_default_locale;
use crate::locale::get_locale;
use crate::locale::Locale;
use crate::types::Table;
use super::lexer;
@@ -202,28 +206,35 @@ pub enum Node {
}
#[derive(Clone)]
pub struct Parser {
lexer: lexer::Lexer,
pub struct Parser<'a> {
lexer: lexer::Lexer<'a>,
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
context: CellReferenceRC,
tables: HashMap<String, Table>,
locale: &'a Locale,
language: &'a Language,
}
impl Parser {
pub fn new_parser_english<'a>(
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser<'a> {
let locale = get_default_locale();
let language = get_default_language();
Parser::new(worksheets, defined_names, tables, locale, language)
}
impl<'a> Parser<'a> {
pub fn new(
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new(
"",
lexer::LexerMode::A1,
#[allow(clippy::expect_used)]
get_locale("en").expect(""),
#[allow(clippy::expect_used)]
get_language("en").expect(""),
);
locale: &'a Locale,
language: &'a Language,
) -> Parser<'a> {
let lexer = lexer::Lexer::new("", lexer::LexerMode::A1, locale, language);
let context = CellReferenceRC {
sheet: worksheets.first().map_or("", |v| v).to_string(),
column: 1,
@@ -235,12 +246,24 @@ impl Parser {
defined_names,
context,
tables,
locale,
language,
}
}
pub fn set_lexer_mode(&mut self, mode: lexer::LexerMode) {
self.lexer.set_lexer_mode(mode)
}
pub fn set_locale(&mut self, locale: &'a Locale) {
self.locale = locale;
self.lexer.set_locale(locale);
}
pub fn set_language(&mut self, language: &'a Language) {
self.language = language;
self.lexer.set_language(language);
}
pub fn set_worksheets_and_names(
&mut self,
worksheets: Vec<String>,
@@ -256,6 +279,27 @@ impl Parser {
self.parse_expr()
}
// Returns the token used to separate arguments in functions and arrays
// If the locale decimal separator is '.', then it is a comma ','
// Otherwise, it is a semicolon ';'
fn get_argument_separator_token(&self) -> TokenType {
if self.locale.numbers.symbols.decimal == "." {
TokenType::Comma
} else {
TokenType::Semicolon
}
}
// Returns the token used to separate columns in arrays
// If the locale decimal separator is '.', then it is a semicolon ';'
fn get_column_separator_token(&self) -> TokenType {
if self.locale.numbers.symbols.decimal == "." {
TokenType::Semicolon
} else {
TokenType::Backslash
}
}
fn get_sheet_index_by_name(&self, name: &str) -> Option<u32> {
let worksheets = &self.worksheets;
for (i, sheet) in worksheets.iter().enumerate() {
@@ -464,6 +508,7 @@ impl Parser {
fn parse_array_row(&mut self) -> Result<Vec<ArrayNode>, Node> {
let mut row = Vec::new();
let column_separator_token = self.get_argument_separator_token();
// and array can only have numbers, string or booleans
// otherwise it is a syntax error
let first_element = match self.parse_expr() {
@@ -471,6 +516,20 @@ impl Parser {
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
Node::UnaryKind {
kind: OpUnary::Minus,
right,
} => {
if let Node::NumberKind(n) = *right {
ArrayNode::Number(-n)
} else {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
}
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
@@ -482,14 +541,27 @@ impl Parser {
};
row.push(first_element);
let mut next_token = self.lexer.peek_token();
// FIXME: this is not respecting the locale
while next_token == TokenType::Comma {
while next_token == column_separator_token {
self.lexer.advance_token();
let value = match self.parse_expr() {
Node::BooleanKind(s) => ArrayNode::Boolean(s),
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
Node::UnaryKind {
kind: OpUnary::Minus,
right,
} => {
if let Node::NumberKind(n) = *right {
ArrayNode::Number(-n)
} else {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
}
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
@@ -527,6 +599,7 @@ impl Parser {
TokenType::String(s) => Node::StringKind(s),
TokenType::LeftBrace => {
// It's an array. It's a collection of rows all of the same dimension
let column_separator_token = self.get_column_separator_token();
let first_row = match self.parse_array_row() {
Ok(s) => s,
@@ -536,9 +609,8 @@ impl Parser {
let mut matrix = Vec::new();
matrix.push(first_row);
// FIXME: this is not respecting the locale
let mut next_token = self.lexer.peek_token();
while next_token == TokenType::Semicolon {
while next_token == column_separator_token {
self.lexer.advance_token();
let row = match self.parse_array_row() {
Ok(s) => s,
@@ -687,12 +759,7 @@ impl Parser {
message: err.message,
};
}
if let Some(function_kind) = Function::get_function(&name) {
return Node::FunctionKind {
kind: function_kind,
args,
};
}
// We should do this *only* importing functions from xlsx
if &name == "_xlfn.SINGLE" {
if args.len() != 1 {
return Node::ParseErrorKind {
@@ -707,6 +774,17 @@ impl Parser {
child: Box::new(args[0].clone()),
};
}
// We should do this *only* importing functions from xlsx
if let Some(function_kind) = self
.language
.functions
.lookup(name.trim_start_matches("_xlfn."))
{
return Node::FunctionKind {
kind: function_kind,
args,
};
}
return Node::InvalidFunctionKind { name, args };
}
let context = &self.context;
@@ -717,7 +795,7 @@ impl Parser {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
message: format!("sheet not found: {}", context.sheet),
};
}
};
@@ -821,6 +899,7 @@ impl Parser {
| TokenType::RightBracket
| TokenType::Colon
| TokenType::Semicolon
| TokenType::Backslash
| TokenType::RightBrace
| TokenType::Comma
| TokenType::Bang
@@ -828,7 +907,7 @@ impl Parser {
| TokenType::Percent => Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: format!("Unexpected token: '{:?}'", next_token),
message: format!("Unexpected token: '{next_token:?}'"),
},
TokenType::LeftBracket => Node::ParseErrorKind {
formula: self.lexer.get_formula(),
@@ -850,7 +929,7 @@ impl Parser {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
message: format!("sheet not found: {}", context.sheet),
};
}
};
@@ -878,7 +957,7 @@ impl Parser {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
message: format!("table sheet not found: {}", table.sheet_name),
};
}
};
@@ -1020,12 +1099,13 @@ impl Parser {
}
fn parse_function_args(&mut self) -> Result<Vec<Node>, Node> {
let arg_separator_token = &self.get_argument_separator_token();
let mut args: Vec<Node> = Vec::new();
let mut next_token = self.lexer.peek_token();
if next_token == TokenType::RightParenthesis {
return Ok(args);
}
if self.lexer.peek_token() == TokenType::Comma {
if &self.lexer.peek_token() == arg_separator_token {
args.push(Node::EmptyArgKind);
} else {
let t = self.parse_expr();
@@ -1035,11 +1115,11 @@ impl Parser {
args.push(t);
}
next_token = self.lexer.peek_token();
while next_token == TokenType::Comma {
while &next_token == arg_separator_token {
self.lexer.advance_token();
if self.lexer.peek_token() == TokenType::Comma {
if &self.lexer.peek_token() == arg_separator_token {
args.push(Node::EmptyArgKind);
next_token = TokenType::Comma;
next_token = arg_separator_token.clone();
} else if self.lexer.peek_token() == TokenType::RightParenthesis {
args.push(Node::EmptyArgKind);
return Ok(args);

View File

@@ -5,6 +5,8 @@ use super::{
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
expressions::token::OpUnary,
language::Language,
locale::Locale,
};
use crate::{
expressions::types::{Area, CellReferenceRC},
@@ -38,39 +40,79 @@ pub(crate) struct MoveContext<'a> {
/// We are moving a formula in (row, column) to (row+row_delta, column + column_delta).
/// All references that do not point to a cell in area will be left untouched.
/// All references that point to a cell in area will be displaced
pub(crate) fn move_formula(node: &Node, move_context: &MoveContext) -> String {
to_string_moved(node, move_context)
pub(crate) fn move_formula(
node: &Node,
move_context: &MoveContext,
locale: &Locale,
language: &Language,
) -> String {
to_string_moved(node, move_context, locale, language)
}
fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> String {
fn move_function(
name: &str,
args: &Vec<Node>,
move_context: &MoveContext,
locale: &Locale,
language: &Language,
) -> String {
let mut first = true;
let mut arguments = "".to_string();
for el in args {
if !first {
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
arguments = format!(
"{},{}",
arguments,
to_string_moved(el, move_context, locale, language)
);
} else {
first = false;
arguments = to_string_moved(el, move_context);
arguments = to_string_moved(el, move_context, locale, language);
}
}
format!("{}({})", name, arguments)
format!("{name}({arguments})")
}
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
match node {
ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(),
ArrayNode::Number(number) => to_excel_precision_str(*number),
ArrayNode::String(value) => format!("\"{}\"", value),
ArrayNode::Error(kind) => format!("{}", kind),
fn format_number_locale(number: f64, locale: &Locale) -> String {
let s = to_excel_precision_str(number);
let decimal = &locale.numbers.symbols.decimal;
if decimal == "." {
s
} else {
s.replace('.', decimal)
}
}
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
pub(crate) fn to_string_array_node(
node: &ArrayNode,
locale: &Locale,
language: &Language,
) -> String {
match node {
ArrayNode::Boolean(value) => {
if *value {
language.booleans.r#true.to_ascii_uppercase()
} else {
language.booleans.r#false.to_ascii_uppercase()
}
}
ArrayNode::Number(number) => format_number_locale(*number, locale),
ArrayNode::String(value) => format!("\"{value}\""),
ArrayNode::Error(kind) => format!("{kind}"),
}
}
fn to_string_moved(
node: &Node,
move_context: &MoveContext,
locale: &Locale,
language: &Language,
) -> String {
use self::Node::*;
match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value),
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
NumberKind(number) => format_number_locale(*number, locale),
StringKind(value) => format!("\"{value}\""),
ReferenceKind {
sheet_name,
sheet_index,
@@ -241,7 +283,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row,
full_column,
);
format!("{}:{}", s1, s2)
format!("{s1}:{s2}")
}
WrongReferenceKind {
sheet_name,
@@ -325,59 +367,85 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row,
full_column,
);
format!("{}:{}", s1, s2)
format!("{s1}:{s2}")
}
OpRangeKind { left, right } => format!(
"{}:{}",
to_string_moved(left, move_context),
to_string_moved(right, move_context),
to_string_moved(left, move_context, locale, language),
to_string_moved(right, move_context, locale, language),
),
OpConcatenateKind { left, right } => format!(
"{}&{}",
to_string_moved(left, move_context),
to_string_moved(right, move_context),
to_string_moved(left, move_context, locale, language),
to_string_moved(right, move_context, locale, language),
),
OpSumKind { kind, left, right } => format!(
"{}{}{}",
to_string_moved(left, move_context),
to_string_moved(left, move_context, locale, language),
kind,
to_string_moved(right, move_context),
to_string_moved(right, move_context, locale, language),
),
OpProductKind { kind, left, right } => {
let x = match **left {
OpSumKind { .. } => format!("({})", to_string_moved(left, move_context)),
CompareKind { .. } => format!("({})", to_string_moved(left, move_context)),
_ => to_string_moved(left, move_context),
OpSumKind { .. } => format!(
"({})",
to_string_moved(left, move_context, locale, language)
),
CompareKind { .. } => format!(
"({})",
to_string_moved(left, move_context, locale, language)
),
_ => to_string_moved(left, move_context, locale, language),
};
let y = match **right {
OpSumKind { .. } => format!("({})", to_string_moved(right, move_context)),
CompareKind { .. } => format!("({})", to_string_moved(right, move_context)),
OpProductKind { .. } => format!("({})", to_string_moved(right, move_context)),
OpSumKind { .. } => format!(
"({})",
to_string_moved(right, move_context, locale, language)
),
CompareKind { .. } => format!(
"({})",
to_string_moved(right, move_context, locale, language)
),
OpProductKind { .. } => format!(
"({})",
to_string_moved(right, move_context, locale, language)
),
UnaryKind { .. } => {
format!("({})", to_string_moved(right, move_context))
format!(
"({})",
to_string_moved(right, move_context, locale, language)
)
}
_ => to_string_moved(right, move_context),
_ => to_string_moved(right, move_context, locale, language),
};
format!("{}{}{}", x, kind, y)
format!("{x}{kind}{y}")
}
OpPowerKind { left, right } => format!(
"{}^{}",
to_string_moved(left, move_context),
to_string_moved(right, move_context),
to_string_moved(left, move_context, locale, language),
to_string_moved(right, move_context, locale, language),
),
InvalidFunctionKind { name, args } => move_function(name, args, move_context),
InvalidFunctionKind { name, args } => {
move_function(name, args, move_context, locale, language)
}
FunctionKind { kind, args } => {
let name = &kind.to_string();
move_function(name, args, move_context)
let name = &kind.to_localized_name(language);
move_function(name, args, move_context, locale, language)
}
ArrayKind(args) => {
let mut first_row = true;
let mut matrix_string = String::new();
// Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
let row_separator = if locale.numbers.symbols.decimal == "." {
';'
} else {
'/'
};
let col_separator = if row_separator == ';' { ',' } else { ';' };
for row in args {
if !first_row {
matrix_string.push(',');
matrix_string.push(col_separator);
} else {
first_row = false;
}
@@ -387,13 +455,13 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
let mut row_string = String::new();
for el in row {
if !first_col {
row_string.push(',');
row_string.push(row_separator);
} else {
first_col = false;
}
// Reuse your existing element-stringification function
row_string.push_str(&to_string_array_node(el));
row_string.push_str(&to_string_array_node(el, locale, language));
}
// Enclose the row in braces
@@ -403,22 +471,28 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
}
// Enclose the whole matrix in braces
format!("{{{}}}", matrix_string)
format!("{{{matrix_string}}}")
}
DefinedNameKind((name, ..)) => name.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!(
"{}{}{}",
to_string_moved(left, move_context),
to_string_moved(left, move_context, locale, language),
kind,
to_string_moved(right, move_context),
to_string_moved(right, move_context, locale, language),
),
UnaryKind { kind, right } => match kind {
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
OpUnary::Minus => format!(
"-{}",
to_string_moved(right, move_context, locale, language)
),
OpUnary::Percentage => format!(
"{}%",
to_string_moved(right, move_context, locale, language)
),
},
ErrorKind(kind) => format!("{}", kind),
ErrorKind(kind) => format!("{kind}"),
ParseErrorKind {
formula,
message: _,
@@ -429,7 +503,10 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
automatic: _,
child,
} => {
format!("@{}", to_string_moved(child, move_context))
format!(
"@{}",
to_string_moved(child, move_context, locale, language)
)
}
}
}

View File

@@ -2,15 +2,19 @@ use crate::functions::Function;
use super::Node;
use once_cell::sync::Lazy;
use regex::Regex;
use std::sync::OnceLock;
static RANGE_REFERENCE_REGEX: OnceLock<Regex> = OnceLock::new();
#[allow(clippy::expect_used)]
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
fn get_re() -> &'static Regex {
RANGE_REFERENCE_REGEX
.get_or_init(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"))
}
fn is_range_reference(s: &str) -> bool {
RE.is_match(s)
get_re().is_match(s)
}
/*
@@ -337,7 +341,8 @@ fn static_analysis_offset(args: &[Node]) -> StaticResult {
}
_ => return StaticResult::Unknown,
};
StaticResult::Unknown
// Both height and width are explicitly 1, so OFFSET will return a single cell
StaticResult::Scalar
}
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
@@ -571,6 +576,37 @@ fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
}
}
// NETWORKDAYS(start_date, end_date, [holidays])
// Parameters: start_date (scalar), end_date (scalar), holidays (optional vector)
fn args_signature_networkdays(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
// NETWORKDAYS.INTL(start_date, end_date, [weekend], [holidays])
// Parameters: start_date (scalar), end_date (scalar), weekend (optional scalar), holidays (optional vector)
fn args_signature_networkdays_intl(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
Signature::Vector,
]
} else {
vec![Signature::Error; arg_count]
}
}
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
// 1. When computing the function
// 2. Checking the arguments to see if we need to insert the implicit intersection operator
@@ -605,6 +641,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Choose => vec![Signature::Scalar; arg_count],
Function::Column => args_signature_row(arg_count),
Function::Columns => args_signature_one_vector(arg_count),
Function::Ln => args_signature_scalars(arg_count, 1, 0),
Function::Log => args_signature_scalars(arg_count, 1, 1),
Function::Log10 => args_signature_scalars(arg_count, 1, 0),
Function::Cos => args_signature_scalars(arg_count, 1, 0),
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
Function::Max => vec![Signature::Vector; arg_count],
@@ -672,6 +711,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Value => args_signature_scalars(arg_count, 1, 0),
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
Function::Average => vec![Signature::Vector; arg_count],
Function::Avedev => vec![Signature::Vector; arg_count],
Function::Averagea => vec![Signature::Vector; arg_count],
Function::Averageif => args_signature_sumif(arg_count),
Function::Averageifs => vec![Signature::Vector; arg_count],
@@ -683,13 +723,28 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Maxifs => vec![Signature::Vector; arg_count],
Function::Minifs => vec![Signature::Vector; arg_count],
Function::Date => args_signature_scalars(arg_count, 3, 0),
Function::Datedif => args_signature_scalars(arg_count, 3, 0),
Function::Datevalue => args_signature_scalars(arg_count, 1, 0),
Function::Day => args_signature_scalars(arg_count, 1, 0),
Function::Edate => args_signature_scalars(arg_count, 2, 0),
Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
Function::Month => args_signature_scalars(arg_count, 1, 0),
Function::Time => args_signature_scalars(arg_count, 3, 0),
Function::Timevalue => args_signature_scalars(arg_count, 1, 0),
Function::Hour => args_signature_scalars(arg_count, 1, 0),
Function::Minute => args_signature_scalars(arg_count, 1, 0),
Function::Second => args_signature_scalars(arg_count, 1, 0),
Function::Now => args_signature_no_args(arg_count),
Function::Today => args_signature_no_args(arg_count),
Function::Year => args_signature_scalars(arg_count, 1, 0),
Function::Days => args_signature_scalars(arg_count, 2, 0),
Function::Days360 => args_signature_scalars(arg_count, 2, 1),
Function::Weekday => args_signature_scalars(arg_count, 1, 1),
Function::Weeknum => args_signature_scalars(arg_count, 1, 1),
Function::Workday => args_signature_scalars(arg_count, 2, 1),
Function::WorkdayIntl => args_signature_scalars(arg_count, 2, 2),
Function::Yearfrac => args_signature_scalars(arg_count, 2, 1),
Function::Isoweeknum => args_signature_scalars(arg_count, 1, 0),
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
Function::Db => args_signature_scalars(arg_count, 4, 1),
@@ -778,6 +833,180 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
Function::Unicode => args_signature_scalars(arg_count, 1, 0),
Function::Geomean => vec![Signature::Vector; arg_count],
Function::Networkdays => args_signature_networkdays(arg_count),
Function::NetworkdaysIntl => args_signature_networkdays_intl(arg_count),
Function::Acot => args_signature_scalars(arg_count, 1, 0),
Function::Acoth => args_signature_scalars(arg_count, 1, 0),
Function::Cot => args_signature_scalars(arg_count, 1, 0),
Function::Coth => args_signature_scalars(arg_count, 1, 0),
Function::Csc => args_signature_scalars(arg_count, 1, 0),
Function::Csch => args_signature_scalars(arg_count, 1, 0),
Function::Sec => args_signature_scalars(arg_count, 1, 0),
Function::Sech => args_signature_scalars(arg_count, 1, 0),
Function::Exp => args_signature_scalars(arg_count, 1, 0),
Function::Fact => args_signature_scalars(arg_count, 1, 0),
Function::Factdouble => args_signature_scalars(arg_count, 1, 0),
Function::Sign => args_signature_scalars(arg_count, 1, 0),
Function::Radians => args_signature_scalars(arg_count, 1, 0),
Function::Degrees => args_signature_scalars(arg_count, 1, 0),
Function::Int => args_signature_scalars(arg_count, 1, 0),
Function::Even => args_signature_scalars(arg_count, 1, 0),
Function::Odd => args_signature_scalars(arg_count, 1, 0),
Function::Ceiling => args_signature_scalars(arg_count, 2, 0),
Function::CeilingMath => args_signature_scalars(arg_count, 1, 2),
Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1),
Function::Floor => args_signature_scalars(arg_count, 2, 0),
Function::FloorMath => args_signature_scalars(arg_count, 1, 2),
Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1),
Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1),
Function::Mod => args_signature_scalars(arg_count, 2, 0),
Function::Quotient => args_signature_scalars(arg_count, 2, 0),
Function::Mround => args_signature_scalars(arg_count, 2, 0),
Function::Trunc => args_signature_scalars(arg_count, 1, 1),
Function::Gcd => vec![Signature::Vector; arg_count],
Function::Lcm => vec![Signature::Vector; arg_count],
Function::Base => args_signature_scalars(arg_count, 2, 1),
Function::Decimal => args_signature_scalars(arg_count, 2, 0),
Function::Roman => args_signature_scalars(arg_count, 1, 1),
Function::Arabic => args_signature_scalars(arg_count, 1, 0),
Function::Combin => args_signature_scalars(arg_count, 2, 0),
Function::Combina => args_signature_scalars(arg_count, 2, 0),
Function::Sumsq => vec![Signature::Vector; arg_count],
Function::N => args_signature_scalars(arg_count, 1, 0),
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
Function::Cell => args_signature_scalars(arg_count, 1, 1),
Function::Info => args_signature_scalars(arg_count, 1, 1),
Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dmax => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dmin => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dsum => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dcounta => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dproduct => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dstdev => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::BetaDist => args_signature_scalars(arg_count, 4, 2),
Function::BetaInv => args_signature_scalars(arg_count, 3, 2),
Function::BinomDist => args_signature_scalars(arg_count, 4, 0),
Function::BinomDistRange => args_signature_scalars(arg_count, 3, 1),
Function::BinomInv => args_signature_scalars(arg_count, 3, 0),
Function::ChisqDist => args_signature_scalars(arg_count, 4, 0),
Function::ChisqDistRT => args_signature_scalars(arg_count, 3, 0),
Function::ChisqInv => args_signature_scalars(arg_count, 3, 0),
Function::ChisqInvRT => args_signature_scalars(arg_count, 2, 0),
Function::ChisqTest => {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
Function::ConfidenceNorm => args_signature_scalars(arg_count, 3, 0),
Function::ConfidenceT => args_signature_scalars(arg_count, 3, 0),
Function::CovarianceP => {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
Function::CovarianceS => {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
Function::Devsq => vec![Signature::Vector; arg_count],
Function::ExponDist => args_signature_scalars(arg_count, 3, 0),
Function::FDist => args_signature_scalars(arg_count, 4, 0),
Function::FDistRT => args_signature_scalars(arg_count, 3, 0),
Function::FInv => args_signature_scalars(arg_count, 3, 0),
Function::FInvRT => args_signature_scalars(arg_count, 3, 0),
Function::FTest => vec![Signature::Vector; 2],
Function::Fisher => args_signature_scalars(arg_count, 1, 0),
Function::FisherInv => args_signature_scalars(arg_count, 1, 0),
Function::Gamma => args_signature_scalars(arg_count, 1, 0),
Function::GammaDist => args_signature_scalars(arg_count, 4, 0),
Function::GammaInv => args_signature_scalars(arg_count, 3, 0),
Function::GammaLn => args_signature_scalars(arg_count, 1, 0),
Function::GammaLnPrecise => args_signature_scalars(arg_count, 1, 0),
Function::HypGeomDist => args_signature_scalars(arg_count, 5, 0),
Function::LogNormDist => args_signature_scalars(arg_count, 4, 0),
Function::LogNormInv => args_signature_scalars(arg_count, 3, 0),
Function::NegbinomDist => args_signature_scalars(arg_count, 4, 0),
Function::NormDist => args_signature_scalars(arg_count, 4, 0),
Function::NormInv => args_signature_scalars(arg_count, 3, 0),
Function::NormSdist => args_signature_scalars(arg_count, 2, 0),
Function::NormSInv => args_signature_scalars(arg_count, 1, 0),
Function::Pearson => {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
Function::Phi => args_signature_scalars(arg_count, 1, 0),
Function::PoissonDist => args_signature_scalars(arg_count, 3, 0),
Function::Standardize => args_signature_scalars(arg_count, 3, 0),
Function::StDevP => vec![Signature::Vector; arg_count],
Function::StDevS => vec![Signature::Vector; arg_count],
Function::Stdeva => vec![Signature::Vector; arg_count],
Function::Stdevpa => vec![Signature::Vector; arg_count],
Function::TDist => args_signature_scalars(arg_count, 3, 0),
Function::TDist2T => args_signature_scalars(arg_count, 2, 0),
Function::TDistRT => args_signature_scalars(arg_count, 2, 0),
Function::TInv => args_signature_scalars(arg_count, 2, 0),
Function::TInv2T => args_signature_scalars(arg_count, 2, 0),
Function::TTest => {
if arg_count == 4 {
vec![
Signature::Vector,
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
]
} else {
vec![Signature::Error; arg_count]
}
}
Function::VarP => vec![Signature::Vector; arg_count],
Function::VarS => vec![Signature::Vector; arg_count],
Function::VarpA => vec![Signature::Vector; arg_count],
Function::VarA => vec![Signature::Vector; arg_count],
Function::WeibullDist => args_signature_scalars(arg_count, 4, 0),
Function::ZTest => {
if arg_count == 2 {
vec![Signature::Vector, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
Function::Sumx2my2 => vec![Signature::Vector; 2],
Function::Sumx2py2 => vec![Signature::Vector; 2],
Function::Sumxmy2 => vec![Signature::Vector; 2],
Function::Correl => vec![Signature::Vector; 2],
Function::Rsq => vec![Signature::Vector; 2],
Function::Intercept => vec![Signature::Vector; 2],
Function::Slope => vec![Signature::Vector; 2],
Function::Steyx => vec![Signature::Vector; 2],
Function::Gauss => args_signature_scalars(arg_count, 1, 0),
Function::Harmean => vec![Signature::Vector; arg_count],
Function::Kurt => vec![Signature::Vector; arg_count],
Function::Large => vec![Signature::Vector, Signature::Scalar],
Function::MaxA => vec![Signature::Vector; arg_count],
Function::Median => vec![Signature::Vector; arg_count],
Function::MinA => vec![Signature::Vector; arg_count],
Function::RankAvg => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::RankEq => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::Skew => vec![Signature::Vector; arg_count],
Function::SkewP => vec![Signature::Vector; arg_count],
Function::Small => vec![Signature::Vector, Signature::Scalar],
}
}
@@ -803,7 +1032,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Atan => scalar_arguments(args),
Function::Atan2 => scalar_arguments(args),
Function::Atanh => scalar_arguments(args),
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
Function::Choose => scalar_arguments(args),
Function::Column => not_implemented(args),
Function::Columns => not_implemented(args),
Function::Cos => scalar_arguments(args),
@@ -816,6 +1045,9 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Round => scalar_arguments(args),
Function::Rounddown => scalar_arguments(args),
Function::Roundup => scalar_arguments(args),
Function::Ln => scalar_arguments(args),
Function::Log => scalar_arguments(args),
Function::Log10 => scalar_arguments(args),
Function::Sin => scalar_arguments(args),
Function::Sinh => scalar_arguments(args),
Function::Sqrt => scalar_arguments(args),
@@ -847,7 +1079,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Lookup => not_implemented(args),
Function::Match => not_implemented(args),
Function::Offset => static_analysis_offset(args),
// FIXME: Row could return an array
Function::Row => StaticResult::Scalar,
Function::Rows => not_implemented(args),
Function::Vlookup => not_implemented(args),
@@ -876,6 +1107,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Valuetotext => not_implemented(args),
Function::Average => not_implemented(args),
Function::Averagea => not_implemented(args),
Function::Avedev => not_implemented(args),
Function::Averageif => not_implemented(args),
Function::Averageifs => not_implemented(args),
Function::Count => not_implemented(args),
@@ -886,12 +1118,27 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Maxifs => not_implemented(args),
Function::Minifs => not_implemented(args),
Function::Date => not_implemented(args),
Function::Datedif => not_implemented(args),
Function::Datevalue => not_implemented(args),
Function::Day => not_implemented(args),
Function::Edate => not_implemented(args),
Function::Month => not_implemented(args),
Function::Time => not_implemented(args),
Function::Timevalue => not_implemented(args),
Function::Hour => not_implemented(args),
Function::Minute => not_implemented(args),
Function::Second => not_implemented(args),
Function::Now => not_implemented(args),
Function::Today => not_implemented(args),
Function::Year => not_implemented(args),
Function::Days => not_implemented(args),
Function::Days360 => not_implemented(args),
Function::Weekday => not_implemented(args),
Function::Weeknum => not_implemented(args),
Function::Workday => not_implemented(args),
Function::WorkdayIntl => not_implemented(args),
Function::Yearfrac => not_implemented(args),
Function::Isoweeknum => not_implemented(args),
Function::Cumipmt => not_implemented(args),
Function::Cumprinc => not_implemented(args),
Function::Db => not_implemented(args),
@@ -980,5 +1227,136 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args),
Function::Networkdays => not_implemented(args),
Function::NetworkdaysIntl => not_implemented(args),
Function::Acot => scalar_arguments(args),
Function::Acoth => scalar_arguments(args),
Function::Cot => scalar_arguments(args),
Function::Coth => scalar_arguments(args),
Function::Csc => scalar_arguments(args),
Function::Csch => scalar_arguments(args),
Function::Sec => scalar_arguments(args),
Function::Sech => scalar_arguments(args),
Function::Exp => scalar_arguments(args),
Function::Fact => scalar_arguments(args),
Function::Factdouble => scalar_arguments(args),
Function::Sign => scalar_arguments(args),
Function::Radians => scalar_arguments(args),
Function::Degrees => scalar_arguments(args),
Function::Int => scalar_arguments(args),
Function::Even => scalar_arguments(args),
Function::Odd => scalar_arguments(args),
Function::Ceiling => scalar_arguments(args),
Function::CeilingMath => scalar_arguments(args),
Function::CeilingPrecise => scalar_arguments(args),
Function::Floor => scalar_arguments(args),
Function::FloorMath => scalar_arguments(args),
Function::FloorPrecise => scalar_arguments(args),
Function::IsoCeiling => scalar_arguments(args),
Function::Mod => scalar_arguments(args),
Function::Quotient => scalar_arguments(args),
Function::Mround => scalar_arguments(args),
Function::Trunc => scalar_arguments(args),
Function::Gcd => not_implemented(args),
Function::Lcm => not_implemented(args),
Function::Base => scalar_arguments(args),
Function::Decimal => scalar_arguments(args),
Function::Roman => scalar_arguments(args),
Function::Arabic => scalar_arguments(args),
Function::Combin => scalar_arguments(args),
Function::Combina => scalar_arguments(args),
Function::Sumsq => StaticResult::Scalar,
Function::N => scalar_arguments(args),
Function::Sheets => scalar_arguments(args),
Function::Cell => scalar_arguments(args),
Function::Info => scalar_arguments(args),
Function::Dget => not_implemented(args),
Function::Dmax => not_implemented(args),
Function::Dmin => not_implemented(args),
Function::Dcount => not_implemented(args),
Function::Daverage => not_implemented(args),
Function::Dsum => not_implemented(args),
Function::Dcounta => not_implemented(args),
Function::Dproduct => not_implemented(args),
Function::Dstdev => not_implemented(args),
Function::Dvar => not_implemented(args),
Function::Dvarp => not_implemented(args),
Function::Dstdevp => not_implemented(args),
Function::BetaDist => StaticResult::Scalar,
Function::BetaInv => StaticResult::Scalar,
Function::BinomDist => StaticResult::Scalar,
Function::BinomDistRange => StaticResult::Scalar,
Function::BinomInv => StaticResult::Scalar,
Function::ChisqDist => StaticResult::Scalar,
Function::ChisqDistRT => StaticResult::Scalar,
Function::ChisqInv => StaticResult::Scalar,
Function::ChisqInvRT => StaticResult::Scalar,
Function::ChisqTest => StaticResult::Scalar,
Function::ConfidenceNorm => StaticResult::Scalar,
Function::ConfidenceT => StaticResult::Scalar,
Function::CovarianceP => StaticResult::Scalar,
Function::CovarianceS => StaticResult::Scalar,
Function::Devsq => StaticResult::Scalar,
Function::ExponDist => StaticResult::Scalar,
Function::FDist => StaticResult::Scalar,
Function::FDistRT => StaticResult::Scalar,
Function::FInv => StaticResult::Scalar,
Function::FInvRT => StaticResult::Scalar,
Function::FTest => StaticResult::Scalar,
Function::Fisher => StaticResult::Scalar,
Function::FisherInv => StaticResult::Scalar,
Function::Gamma => StaticResult::Scalar,
Function::GammaDist => StaticResult::Scalar,
Function::GammaInv => StaticResult::Scalar,
Function::GammaLn => StaticResult::Scalar,
Function::GammaLnPrecise => StaticResult::Scalar,
Function::HypGeomDist => StaticResult::Scalar,
Function::LogNormDist => StaticResult::Scalar,
Function::LogNormInv => StaticResult::Scalar,
Function::NegbinomDist => StaticResult::Scalar,
Function::NormDist => StaticResult::Scalar,
Function::NormInv => StaticResult::Scalar,
Function::NormSdist => StaticResult::Scalar,
Function::NormSInv => StaticResult::Scalar,
Function::Pearson => StaticResult::Scalar,
Function::Phi => StaticResult::Scalar,
Function::PoissonDist => StaticResult::Scalar,
Function::Standardize => StaticResult::Scalar,
Function::StDevP => StaticResult::Scalar,
Function::StDevS => StaticResult::Scalar,
Function::Stdeva => StaticResult::Scalar,
Function::Stdevpa => StaticResult::Scalar,
Function::TDist => StaticResult::Scalar,
Function::TDist2T => StaticResult::Scalar,
Function::TDistRT => StaticResult::Scalar,
Function::TInv => StaticResult::Scalar,
Function::TInv2T => StaticResult::Scalar,
Function::TTest => StaticResult::Scalar,
Function::VarP => StaticResult::Scalar,
Function::VarS => StaticResult::Scalar,
Function::VarpA => StaticResult::Scalar,
Function::VarA => StaticResult::Scalar,
Function::WeibullDist => StaticResult::Scalar,
Function::ZTest => StaticResult::Scalar,
Function::Sumx2my2 => StaticResult::Scalar,
Function::Sumx2py2 => StaticResult::Scalar,
Function::Sumxmy2 => StaticResult::Scalar,
Function::Correl => StaticResult::Scalar,
Function::Rsq => StaticResult::Scalar,
Function::Intercept => StaticResult::Scalar,
Function::Slope => StaticResult::Scalar,
Function::Steyx => StaticResult::Scalar,
Function::Gauss => StaticResult::Scalar,
Function::Harmean => StaticResult::Scalar,
Function::Kurt => StaticResult::Scalar,
Function::Large => StaticResult::Scalar,
Function::MaxA => StaticResult::Scalar,
Function::Median => StaticResult::Scalar,
Function::MinA => StaticResult::Scalar,
Function::RankAvg => StaticResult::Scalar,
Function::RankEq => StaticResult::Scalar,
Function::Skew => StaticResult::Scalar,
Function::SkewP => StaticResult::Scalar,
Function::Small => StaticResult::Scalar,
}
}

View File

@@ -2,7 +2,9 @@ use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::move_formula::to_string_array_node;
use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::OpUnary;
use crate::expressions::token::{OpSum, OpUnary};
use crate::language::{get_language, Language};
use crate::locale::{get_locale, Locale};
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
pub enum DisplaceData {
@@ -28,6 +30,11 @@ pub enum DisplaceData {
column: i32,
delta: i32,
},
RowMove {
sheet: u32,
row: i32,
delta: i32,
},
ColumnMove {
sheet: u32,
column: i32,
@@ -38,17 +45,44 @@ pub enum DisplaceData {
/// This is the internal mode in IronCalc
pub fn to_rc_format(node: &Node) -> String {
stringify(node, None, &DisplaceData::None, false)
#[allow(clippy::expect_used)]
let locale = get_locale("en").expect("");
#[allow(clippy::expect_used)]
let language = get_language("en").expect("");
stringify(node, None, &DisplaceData::None, false, locale, language)
}
/// This is the mode used to display the formula in the UI
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false)
pub fn to_localized_string(
node: &Node,
context: &CellReferenceRC,
locale: &Locale,
language: &Language,
) -> String {
stringify(
node,
Some(context),
&DisplaceData::None,
false,
locale,
language,
)
}
/// This is the mode used to export the formula to Excel
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true)
#[allow(clippy::expect_used)]
let locale = get_locale("en").expect("");
#[allow(clippy::expect_used)]
let language = get_language("en").expect("");
stringify(
node,
Some(context),
&DisplaceData::None,
true,
locale,
language,
)
}
pub fn to_string_displaced(
@@ -56,7 +90,11 @@ pub fn to_string_displaced(
context: &CellReferenceRC,
displace_data: &DisplaceData,
) -> String {
stringify(node, Some(context), displace_data, false)
#[allow(clippy::expect_used)]
let locale = get_locale("en").expect("");
#[allow(clippy::expect_used)]
let language = get_language("en").expect("");
stringify(node, Some(context), displace_data, false, locale, language)
}
/// Converts a local reference to a string applying some displacement if needed.
@@ -159,6 +197,29 @@ pub(crate) fn stringify_reference(
}
}
}
DisplaceData::RowMove {
sheet,
row: move_row,
delta,
} => {
if sheet_index == *sheet {
if row == *move_row {
row += *delta;
} else if *delta > 0 {
// Moving the row downwards
if row > *move_row && row <= *move_row + *delta {
// Intermediate rows move up by one position
row -= 1;
}
} else if *delta < 0 {
// Moving the row upwards
if row < *move_row && row >= *move_row + *delta {
// Intermediate rows move down by one position
row += 1;
}
}
}
}
DisplaceData::ColumnMove {
sheet,
column: move_column,
@@ -167,14 +228,18 @@ pub(crate) fn stringify_reference(
if sheet_index == *sheet {
if column == *move_column {
column += *delta;
} else if (*delta > 0
&& column > *move_column
&& column <= *move_column + *delta)
|| (*delta < 0
&& column < *move_column
&& column >= *move_column + *delta)
{
column -= *delta;
} else if *delta > 0 {
// Moving the column to the right
if column > *move_column && column <= *move_column + *delta {
// Intermediate columns move left by one position
column -= 1;
}
} else if *delta < 0 {
// Moving the column to the left
if column < *move_column && column >= *move_column + *delta {
// Intermediate columns move right by one position
column += 1;
}
}
}
}
@@ -184,16 +249,16 @@ pub(crate) fn stringify_reference(
return "#REF!".to_string();
}
let mut row_abs = if absolute_row {
format!("${}", row)
format!("${row}")
} else {
format!("{}", row)
format!("{row}")
};
let column = match crate::expressions::utils::number_to_column(column) {
Some(s) => s,
None => return "#REF!".to_string(),
};
let mut col_abs = if absolute_column {
format!("${}", column)
format!("${column}")
} else {
column
};
@@ -208,27 +273,27 @@ pub(crate) fn stringify_reference(
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
}
None => {
format!("{}{}", col_abs, row_abs)
format!("{col_abs}{row_abs}")
}
}
}
None => {
let row_abs = if absolute_row {
format!("R{}", row)
format!("R{row}")
} else {
format!("R[{}]", row)
format!("R[{row}]")
};
let col_abs = if absolute_column {
format!("C{}", column)
format!("C{column}")
} else {
format!("C[{}]", column)
format!("C[{column}]")
};
match &sheet_name {
Some(name) => {
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
}
None => {
format!("{}{}", row_abs, col_abs)
format!("{row_abs}{col_abs}")
}
}
}
@@ -241,22 +306,44 @@ fn format_function(
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
export_to_excel: bool,
locale: &Locale,
language: &Language,
) -> String {
let mut first = true;
let mut arguments = "".to_string();
let arg_separator = if locale.numbers.symbols.decimal == "." {
','
} else {
';'
};
for el in args {
if !first {
arguments = format!(
"{},{}",
"{}{}{}",
arguments,
stringify(el, context, displace_data, export_to_excel)
arg_separator,
stringify(
el,
context,
displace_data,
export_to_excel,
locale,
language
)
);
} else {
first = false;
arguments = stringify(el, context, displace_data, export_to_excel);
arguments = stringify(
el,
context,
displace_data,
export_to_excel,
locale,
language,
);
}
}
format!("{}({})", name, arguments)
format!("{name}({arguments})")
}
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
@@ -289,12 +376,27 @@ fn stringify(
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
export_to_excel: bool,
locale: &Locale,
language: &Language,
) -> String {
use self::Node::*;
match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value),
BooleanKind(value) => {
if *value {
language.booleans.r#true.to_string()
} else {
language.booleans.r#false.to_string()
}
}
NumberKind(number) => {
let s = to_excel_precision_str(*number);
if locale.numbers.symbols.decimal == "." {
s
} else {
s.replace(".", &locale.numbers.symbols.decimal)
}
}
StringKind(value) => format!("\"{value}\""),
WrongReferenceKind {
sheet_name,
column,
@@ -384,7 +486,7 @@ fn stringify(
full_row,
full_column,
);
format!("{}:{}", s1, s2)
format!("{s1}:{s2}")
}
WrongRangeKind {
sheet_name,
@@ -433,63 +535,153 @@ fn stringify(
full_row,
full_column,
);
format!("{}:{}", s1, s2)
format!("{s1}:{s2}")
}
OpRangeKind { left, right } => format!(
"{}:{}",
stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, export_to_excel)
stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language
),
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
),
OpConcatenateKind { left, right } => format!(
"{}&{}",
stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, export_to_excel)
stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language
),
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
),
CompareKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, export_to_excel),
stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language
),
kind,
stringify(right, context, displace_data, export_to_excel)
),
OpSumKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, export_to_excel),
kind,
stringify(right, context, displace_data, export_to_excel)
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
),
OpSumKind { kind, left, right } => {
let left_str = stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language,
);
// if kind is minus then we need parentheses in the right side if they are OpSumKind or CompareKind
let right_str = if (matches!(kind, OpSum::Minus) && matches!(**right, OpSumKind { .. }))
| matches!(**right, CompareKind { .. })
{
format!(
"({})",
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
)
} else {
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language,
)
};
format!("{left_str}{kind}{right_str}")
}
OpProductKind { kind, left, right } => {
let x = match **left {
OpSumKind { .. } => format!(
OpSumKind { .. } | CompareKind { .. } => format!(
"({})",
stringify(left, context, displace_data, export_to_excel)
stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language
)
),
CompareKind { .. } => format!(
"({})",
stringify(left, context, displace_data, export_to_excel)
_ => stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language,
),
_ => stringify(left, context, displace_data, export_to_excel),
};
let y = match **right {
OpSumKind { .. } => format!(
OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
),
CompareKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
_ => stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language,
),
OpProductKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
),
_ => stringify(right, context, displace_data, export_to_excel),
};
format!("{}{}{}", x, kind, y)
format!("{x}{kind}{y}")
}
OpPowerKind { left, right } => {
let x = match **left {
BooleanKind(_)
| NumberKind(_)
| UnaryKind { .. }
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
@@ -497,7 +689,14 @@ fn stringify(
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
| WrongRangeKind { .. } => stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language,
),
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
@@ -505,7 +704,6 @@ fn stringify(
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| UnaryKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| OpSumKind { .. }
@@ -513,7 +711,14 @@ fn stringify(
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(left, context, displace_data, export_to_excel)
stringify(
left,
context,
displace_data,
export_to_excel,
locale,
language
)
),
};
let y = match **right {
@@ -526,9 +731,14 @@ fn stringify(
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(right, context, displace_data, export_to_excel)
}
| WrongRangeKind { .. } => stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language,
),
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
@@ -544,29 +754,56 @@ fn stringify(
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
),
};
format!("{}^{}", x, y)
}
InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, export_to_excel)
format!("{x}^{y}")
}
InvalidFunctionKind { name, args } => format_function(
&name.to_ascii_lowercase(),
args,
context,
displace_data,
export_to_excel,
locale,
language,
),
FunctionKind { kind, args } => {
let name = if export_to_excel {
kind.to_xlsx_string()
} else {
kind.to_string()
kind.to_localized_name(language)
};
format_function(&name, args, context, displace_data, export_to_excel)
format_function(
&name,
args,
context,
displace_data,
export_to_excel,
locale,
language,
)
}
ArrayKind(args) => {
let mut first_row = true;
let mut matrix_string = String::new();
let row_separator = if locale.numbers.symbols.decimal == "." {
';'
} else {
'/'
};
let col_separator = if row_separator == ';' { ',' } else { ';' };
for row in args {
if !first_row {
matrix_string.push(';');
matrix_string.push(row_separator);
} else {
first_row = false;
}
@@ -574,34 +811,87 @@ fn stringify(
let mut row_string = String::new();
for el in row {
if !first_column {
row_string.push(',');
row_string.push(col_separator);
} else {
first_column = false;
}
row_string.push_str(&to_string_array_node(el));
row_string.push_str(&to_string_array_node(el, locale, language));
}
matrix_string.push_str(&row_string);
}
format!("{{{}}}", matrix_string)
format!("{{{matrix_string}}}")
}
TableNameKind(value) => value.to_string(),
DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind {
OpUnary::Minus => {
format!(
"-{}",
stringify(right, context, displace_data, export_to_excel)
)
let needs_parentheses = match **right {
BooleanKind(_)
| NumberKind(_)
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| WrongRangeKind { .. }
| OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| ImplicitIntersection { .. }
| CompareKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| EmptyArgKind => false,
OpPowerKind { .. } | OpSumKind { .. } | UnaryKind { .. } => true,
};
if needs_parentheses {
format!(
"-({})",
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
)
} else {
format!(
"-{}",
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
)
}
}
OpUnary::Percentage => {
format!(
"{}%",
stringify(right, context, displace_data, export_to_excel)
stringify(
right,
context,
displace_data,
export_to_excel,
locale,
language
)
)
}
},
ErrorKind(kind) => format!("{}", kind),
ErrorKind(kind) => format!("{kind}"),
ParseErrorKind {
formula,
position: _,
@@ -618,17 +908,38 @@ fn stringify(
add_implicit_intersection(&mut new_node, true);
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
return stringify(child, context, displace_data, export_to_excel);
return stringify(
child,
context,
displace_data,
export_to_excel,
locale,
language,
);
}
return format!(
"_xlfn.SINGLE({})",
stringify(child, context, displace_data, export_to_excel)
stringify(
child,
context,
displace_data,
export_to_excel,
locale,
language
)
);
}
format!(
"@{}",
stringify(child, context, displace_data, export_to_excel)
stringify(
child,
context,
displace_data,
export_to_excel,
locale,
language
)
)
}
}

View File

@@ -3,7 +3,11 @@ mod test_arrays;
mod test_general;
mod test_implicit_intersection;
mod test_issue_155;
mod test_issue_483;
mod test_languages;
mod test_locales;
mod test_move_formula;
mod test_ranges;
mod test_stringify;
mod test_tables;
mod utils;

View File

@@ -2,8 +2,8 @@ use std::collections::HashMap;
use crate::expressions::{
parser::{
stringify::{to_excel_string, to_string},
Parser,
stringify::to_excel_string,
tests::utils::{new_parser, to_english_localized_string},
},
types::CellReferenceRC,
};
@@ -13,7 +13,7 @@ use crate::expressions::parser::static_analysis::add_implicit_intersection;
#[test]
fn simple_test() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -72,7 +72,7 @@ fn simple_test() {
for (formula, expected) in cases {
let mut t = parser.parse(formula, &cell_reference);
add_implicit_intersection(&mut t, true);
let r = to_string(&t, &cell_reference);
let r = to_english_localized_string(&t, &cell_reference);
assert_eq!(r, expected);
let excel_formula = to_excel_string(&t, &cell_reference);
assert_eq!(excel_formula, formula);

View File

@@ -2,14 +2,15 @@
use std::collections::HashMap;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::{ArrayNode, Node, Parser};
use crate::expressions::parser::stringify::to_rc_format;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
use crate::expressions::parser::{ArrayNode, Node};
use crate::expressions::types::CellReferenceRC;
#[test]
fn simple_horizontal() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -28,13 +29,16 @@ fn simple_horizontal() {
);
assert_eq!(to_rc_format(&horizontal), "{1,2,3}");
assert_eq!(to_string(&horizontal, &cell_reference), "{1,2,3}");
assert_eq!(
to_english_localized_string(&horizontal, &cell_reference),
"{1,2,3}"
);
}
#[test]
fn simple_vertical() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -52,13 +56,16 @@ fn simple_vertical() {
])
);
assert_eq!(to_rc_format(&vertical), "{1;2;3}");
assert_eq!(to_string(&vertical, &cell_reference), "{1;2;3}");
assert_eq!(
to_english_localized_string(&vertical, &cell_reference),
"{1;2;3}"
);
}
#[test]
fn simple_matrix() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -88,5 +95,8 @@ fn simple_matrix() {
])
);
assert_eq!(to_rc_format(&matrix), "{1,2,3;4,5,6;7,8,9}");
assert_eq!(to_string(&matrix, &cell_reference), "{1,2,3;4,5,6;7,8,9}");
assert_eq!(
to_english_localized_string(&matrix, &cell_reference),
"{1,2,3;4,5,6;7,8,9}"
);
}

View File

@@ -3,12 +3,12 @@
use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::{
to_rc_format, to_string, to_string_displaced, DisplaceData,
};
use crate::expressions::parser::{Node, Parser};
use crate::expressions::parser::stringify::{to_rc_format, to_string_displaced, DisplaceData};
use crate::expressions::parser::Node;
use crate::expressions::types::CellReferenceRC;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
struct Formula<'a> {
initial: &'a str,
expected: &'a str,
@@ -17,7 +17,7 @@ struct Formula<'a> {
#[test]
fn test_parser_reference() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -32,7 +32,7 @@ fn test_parser_reference() {
#[test]
fn test_parser_absolute_column() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -47,7 +47,7 @@ fn test_parser_absolute_column() {
#[test]
fn test_parser_absolute_row_col() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -62,7 +62,7 @@ fn test_parser_absolute_row_col() {
#[test]
fn test_parser_absolute_row_col_1() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -77,7 +77,7 @@ fn test_parser_absolute_row_col_1() {
#[test]
fn test_parser_simple_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -93,7 +93,7 @@ fn test_parser_simple_formula() {
#[test]
fn test_parser_boolean() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -109,7 +109,7 @@ fn test_parser_boolean() {
#[test]
fn test_parser_bad_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -138,7 +138,7 @@ fn test_parser_bad_formula() {
#[test]
fn test_parser_bad_formula_1() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -167,7 +167,7 @@ fn test_parser_bad_formula_1() {
#[test]
fn test_parser_bad_formula_2() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -196,7 +196,7 @@ fn test_parser_bad_formula_2() {
#[test]
fn test_parser_bad_formula_3() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -225,7 +225,7 @@ fn test_parser_bad_formula_3() {
#[test]
fn test_parser_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let formulas = vec![
Formula {
@@ -266,14 +266,17 @@ fn test_parser_formulas() {
},
);
assert_eq!(to_rc_format(&t), formula.expected);
assert_eq!(to_string(&t, &cell_reference), formula.initial);
assert_eq!(
to_english_localized_string(&t, &cell_reference),
formula.initial
);
}
}
#[test]
fn test_parser_r1c1_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
parser.set_lexer_mode(LexerMode::R1C1);
let formulas = vec![
@@ -330,7 +333,10 @@ fn test_parser_r1c1_formulas() {
column: 1,
},
);
assert_eq!(to_string(&t, &cell_reference), formula.expected);
assert_eq!(
to_english_localized_string(&t, &cell_reference),
formula.expected
);
assert_eq!(to_rc_format(&t), formula.initial);
}
}
@@ -338,7 +344,7 @@ fn test_parser_r1c1_formulas() {
#[test]
fn test_parser_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -354,7 +360,7 @@ fn test_parser_quotes() {
#[test]
fn test_parser_escape_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -370,7 +376,7 @@ fn test_parser_escape_quotes() {
#[test]
fn test_parser_parenthesis() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -386,7 +392,7 @@ fn test_parser_parenthesis() {
#[test]
fn test_parser_excel_xlfn() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -407,7 +413,7 @@ fn test_to_string_displaced() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column {
@@ -427,7 +433,7 @@ fn test_to_string_displaced_full_ranges() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let node = parser.parse("SUM(3:3)", context);
let displace_data = DisplaceData::Column {
@@ -460,7 +466,7 @@ fn test_to_string_displaced_too_low() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column {
@@ -480,7 +486,7 @@ fn test_to_string_displaced_too_high() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column {

View File

@@ -1,13 +1,15 @@
#![allow(clippy::panic)]
use crate::expressions::parser::{Node, Parser};
use crate::expressions::parser::Node;
use crate::expressions::types::CellReferenceRC;
use std::collections::HashMap;
use crate::expressions::parser::tests::utils::new_parser;
#[test]
fn simple() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
@@ -40,7 +42,7 @@ fn simple() {
#[test]
fn simple_add() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {

View File

@@ -2,14 +2,13 @@
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_155_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -18,13 +17,13 @@ fn issue_155_parser() {
column: 2,
};
let t = parser.parse("A$1:A2", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "A$1:A2");
assert_eq!(to_english_localized_string(&t, &cell_reference), "A$1:A2");
}
#[test]
fn issue_155_parser_case_2() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -33,13 +32,13 @@ fn issue_155_parser_case_2() {
column: 20,
};
let t = parser.parse("C$1:D2", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "C$1:D2");
assert_eq!(to_english_localized_string(&t, &cell_reference), "C$1:D2");
}
#[test]
fn issue_155_parser_only_row() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -49,13 +48,13 @@ fn issue_155_parser_only_row() {
};
// This is tricky, I am not sure what to do in these cases
let t = parser.parse("A$2:B1", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "A1:B$2");
assert_eq!(to_english_localized_string(&t, &cell_reference), "A1:B$2");
}
#[test]
fn issue_155_parser_only_column() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -65,5 +64,5 @@ fn issue_155_parser_only_column() {
};
// This is tricky, I am not sure what to do in these cases
let t = parser.parse("D1:$A3", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "$A1:D3");
assert_eq!(to_english_localized_string(&t, &cell_reference), "$A1:D3");
}

View File

@@ -0,0 +1,30 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
use crate::expressions::parser::Node;
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_483_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 2,
column: 2,
};
let t = parser.parse("-(A1^1.22)", &cell_reference);
assert!(matches!(t, Node::UnaryKind { .. }));
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"-(A1^1.22)"
);
let t = parser.parse("-A1^1.22", &cell_reference);
assert!(matches!(t, Node::OpPowerKind { .. }));
assert_eq!(to_english_localized_string(&t, &cell_reference), "-A1^1.22");
}

View File

@@ -0,0 +1,58 @@
#![allow(clippy::unwrap_used)]
use std::collections::HashMap;
use crate::expressions::parser::{DefinedNameS, Node, Parser};
use crate::expressions::types::CellReferenceRC;
use crate::expressions::parser::stringify::to_localized_string;
use crate::functions::Function;
use crate::language::get_language;
use crate::locale::get_locale;
use crate::types::Table;
pub fn to_string(t: &Node, cell_reference: &CellReferenceRC) -> String {
let locale = get_locale("en").unwrap();
let language = get_language("es").unwrap();
to_localized_string(t, cell_reference, locale, language)
}
pub fn new_parser<'a>(
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser<'a> {
let locale = get_locale("en").unwrap();
let language = get_language("es").unwrap();
Parser::new(worksheets, defined_names, tables, locale, language)
}
#[test]
fn simple_language() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let t = parser.parse("FALSO", &cell_reference);
assert!(matches!(t, Node::BooleanKind(false)));
let t = parser.parse("VERDADERO", &cell_reference);
assert!(matches!(t, Node::BooleanKind(true)));
let t = parser.parse("TRUE()", &cell_reference);
assert!(matches!(t, Node::InvalidFunctionKind { ref name, args: _} if name == "TRUE"));
let t = parser.parse("VERDADERO()", &cell_reference);
assert!(matches!(
t,
Node::FunctionKind {
kind: Function::True,
args: _
}
));
assert_eq!(to_string(&t, &cell_reference), "VERDADERO()".to_string());
}

View File

@@ -0,0 +1 @@

View File

@@ -1,8 +1,17 @@
use std::collections::HashMap;
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
use crate::expressions::parser::Parser;
use crate::expressions::parser::move_formula::{move_formula as mf, MoveContext};
use crate::expressions::parser::tests::utils::new_parser;
use crate::expressions::parser::Node;
use crate::expressions::types::{Area, CellReferenceRC};
use crate::language::get_default_language;
use crate::locale::get_default_locale;
fn move_formula(node: &Node, context: &MoveContext) -> String {
let locale = get_default_locale();
let language = get_default_language();
mf(node, context, locale, language)
}
#[test]
fn test_move_formula() {
@@ -15,7 +24,7 @@ fn test_move_formula() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -102,7 +111,7 @@ fn test_move_formula_context_offset() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -140,7 +149,7 @@ fn test_move_formula_area_limits() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -195,7 +204,7 @@ fn test_move_formula_ranges() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let area = &Area {
sheet: 0,
@@ -318,7 +327,7 @@ fn test_move_formula_wrong_reference() {
height: 5,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Wrong formulas will NOT be displaced
let node = parser.parse("Sheet3!AB31", context);
@@ -377,7 +386,7 @@ fn test_move_formula_misc() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -445,7 +454,7 @@ fn test_move_formula_another_sheet() {
};
// we add two sheets and we cut/paste from Sheet1 to Sheet2
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -487,7 +496,7 @@ fn move_formula_implicit_intersetion() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -524,7 +533,7 @@ fn move_formula_implicit_intersetion_with_ranges() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {

View File

@@ -2,8 +2,8 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::Parser;
use crate::expressions::parser::stringify::to_rc_format;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> {
@@ -14,7 +14,7 @@ struct Formula<'a> {
#[test]
fn test_parser_formulas_with_full_ranges() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let formulas = vec![
Formula {
@@ -59,7 +59,10 @@ fn test_parser_formulas_with_full_ranges() {
},
);
assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
assert_eq!(
to_english_localized_string(&t, &cell_reference),
formula.formula_a1
);
}
// Now the inverse
@@ -74,14 +77,17 @@ fn test_parser_formulas_with_full_ranges() {
},
);
assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
assert_eq!(
to_english_localized_string(&t, &cell_reference),
formula.formula_a1
);
}
}
#[test]
fn test_range_inverse_order() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -96,7 +102,7 @@ fn test_range_inverse_order() {
&cell_reference,
);
assert_eq!(
to_string(&t, &cell_reference),
to_english_localized_string(&t, &cell_reference),
"SUM(C2:D4)*SUM(Sheet2!C4:D20)*SUM($C4:D$20)".to_string()
);
}

View File

@@ -2,14 +2,13 @@
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
use crate::expressions::types::CellReferenceRC;
#[test]
fn exp_order() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let mut parser = new_parser(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -18,17 +17,65 @@ fn exp_order() {
column: 1,
};
let t = parser.parse("(1 + 2)^3 + 4", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4");
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"(1+2)^3+4"
);
let t = parser.parse("(C5 + 3)^R4", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4");
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"(C5+3)^R4"
);
let t = parser.parse("(C5 + 3)^(R4*6)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)");
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"(C5+3)^(R4*6)"
);
let t = parser.parse("(C5)^(R4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "C5^R4");
assert_eq!(to_english_localized_string(&t, &cell_reference), "C5^R4");
let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4");
assert_eq!(to_english_localized_string(&t, &cell_reference), "5^4");
}
#[test]
fn correct_parenthesis() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = new_parser(worksheets, vec![], HashMap::new());
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let t = parser.parse("-(1 + 1)", &cell_reference);
assert_eq!(to_english_localized_string(&t, &cell_reference), "-(1+1)");
let t = parser.parse("1 - (3 + 4)", &cell_reference);
assert_eq!(to_english_localized_string(&t, &cell_reference), "1-(3+4)");
let t = parser.parse("-(1.05*(0.0284 + 0.0046) - 0.0284)", &cell_reference);
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"-(1.05*(0.0284+0.0046)-0.0284)"
);
let t = parser.parse("1 + (3+5)", &cell_reference);
assert_eq!(to_english_localized_string(&t, &cell_reference), "1+3+5");
let t = parser.parse("1 - (3+5)", &cell_reference);
assert_eq!(to_english_localized_string(&t, &cell_reference), "1-(3+5)");
let t = parser.parse("(1 - 3) - (3+5)", &cell_reference);
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"1-3-(3+5)"
);
let t = parser.parse("1 + (3<5)", &cell_reference);
assert_eq!(to_english_localized_string(&t, &cell_reference), "1+(3<5)");
}

View File

@@ -2,8 +2,7 @@
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::parser::tests::utils::{new_parser, to_english_localized_string};
use crate::expressions::types::CellReferenceRC;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
@@ -62,7 +61,7 @@ fn simple_table() {
let row_count = 3;
let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
let mut parser = Parser::new(worksheets, vec![], tables);
let mut parser = new_parser(worksheets, vec![], tables);
// Reference cell is 'Sheet One'!F2
let cell_reference = CellReferenceRC {
sheet: "Sheet One".to_string(),
@@ -72,7 +71,10 @@ fn simple_table() {
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"SUM($A$2:$E$2)"
);
// Cell A3
let cell_reference = CellReferenceRC {
@@ -82,7 +84,10 @@ fn simple_table() {
};
let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
assert_eq!(
to_english_localized_string(&t, &cell_reference),
"SUBTOTAL(109,$A$2:$A$3)"
);
// Cell A3 in 'Second Sheet'
let cell_reference = CellReferenceRC {
@@ -93,7 +98,7 @@ fn simple_table() {
let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &cell_reference);
assert_eq!(
to_string(&t, &cell_reference),
to_english_localized_string(&t, &cell_reference),
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"
);
}

View File

@@ -0,0 +1,29 @@
use std::collections::HashMap;
use crate::{
expressions::{
parser::{DefinedNameS, Node, Parser},
types::CellReferenceRC,
},
language::get_default_language,
locale::get_default_locale,
types::Table,
};
use crate::expressions::parser::stringify::to_localized_string;
pub fn to_english_localized_string(t: &Node, cell_reference: &CellReferenceRC) -> String {
let locale = get_default_locale();
let language = get_default_language();
to_localized_string(t, cell_reference, locale, language)
}
pub fn new_parser<'a>(
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser<'a> {
let locale = get_default_locale();
let language = get_default_language();
Parser::new(worksheets, defined_names, tables, locale, language)
}

View File

@@ -241,6 +241,7 @@ pub enum TokenType {
Percent, // %
And, // &
At, // @
Backslash, // \
Reference {
sheet: Option<String>,
row: i32,

View File

@@ -211,15 +211,19 @@ pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
pub fn is_valid_identifier(name: &str) -> bool {
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
// https://github.com/MartinTrummer/excel-names/
// NOTE: We are being much more restrictive than Excel.
// In particular we do not support non ascii characters.
let upper = name.to_ascii_uppercase();
let bytes = upper.as_bytes();
let len = bytes.len();
// length of chars
let len = upper.chars().count();
let mut chars = upper.chars();
if len > 255 || len == 0 {
return false;
}
let first = bytes[0] as char;
let first = match chars.next() {
Some(ch) => ch,
None => return false,
};
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
return false;
@@ -237,20 +241,10 @@ pub fn is_valid_identifier(name: &str) -> bool {
if parse_reference_r1c1(name).is_some() {
return false;
}
let mut i = 1;
while i < len {
let ch = bytes[i] as char;
match ch {
'a'..='z' => {}
'A'..='Z' => {}
'0'..='9' => {}
'_' => {}
'.' => {}
_ => {
return false;
}
for ch in chars {
if !(ch.is_alphanumeric() || ch == '_' || ch == '.') {
return false;
}
i += 1;
}
true
@@ -259,15 +253,23 @@ pub fn is_valid_identifier(name: &str) -> bool {
fn name_needs_quoting(name: &str) -> bool {
let chars = name.chars();
// it contains any of these characters: ()'$,;-+{} or space
for char in chars {
for (i, char) in chars.enumerate() {
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
return true;
}
// if it starts with a number
if i == 0 && char.is_ascii_digit() {
return true;
}
}
if parse_reference_a1(name).is_some() {
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
return true;
}
if parse_reference_r1c1(name).is_some() {
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
return true;
}
// TODO:
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
// integers
false
}
@@ -279,3 +281,32 @@ pub fn quote_name(name: &str) -> String {
};
name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_name() {
assert_eq!(quote_name("Sheet1"), "Sheet1");
assert_eq!(quote_name("Sheet 1"), "'Sheet 1'");
// escape and quote
assert_eq!(quote_name("Sheet1'"), "'Sheet1'''");
assert_eq!(quote_name("Data(2024)"), "'Data(2024)'");
assert_eq!(quote_name("Data$2024"), "'Data$2024'");
assert_eq!(quote_name("Data-2024"), "'Data-2024'");
assert_eq!(quote_name("Data+2024"), "'Data+2024'");
assert_eq!(quote_name("Data,2024"), "'Data,2024'");
assert_eq!(quote_name("Data;2024"), "'Data;2024'");
assert_eq!(quote_name("Data{2024}"), "'Data{2024}'");
assert_eq!(quote_name("2024"), "'2024'");
assert_eq!(quote_name("1Data"), "'1Data'");
assert_eq!(quote_name("A1"), "'A1'");
assert_eq!(quote_name("R1C1"), "'R1C1'");
assert_eq!(quote_name("MySheet"), "MySheet");
assert_eq!(quote_name("B1048576"), "'B1048576'");
assert_eq!(quote_name("B1048577"), "B1048577");
}
}

View File

@@ -196,6 +196,7 @@ fn test_names() {
assert!(is_valid_identifier("_."));
assert!(is_valid_identifier("_1"));
assert!(is_valid_identifier("\\."));
assert!(is_valid_identifier("truñe"));
// invalid
assert!(!is_valid_identifier("true"));
@@ -209,6 +210,7 @@ fn test_names() {
assert!(!is_valid_identifier("1true"));
assert!(!is_valid_identifier("test€"));
assert!(!is_valid_identifier("truñe"));
assert!(!is_valid_identifier("tr&ue"));
assert!(!is_valid_identifier("LOG10"));
}

View File

@@ -8,6 +8,8 @@ use crate::constants::EXCEL_DATE_BASE;
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
pub const DATE_OUT_OF_RANGE_MESSAGE: &str = "Out of range parameters for date";
#[inline]
fn convert_to_serial_number(date: NaiveDate) -> i32 {
date.num_days_from_ce() - EXCEL_DATE_BASE
@@ -21,14 +23,12 @@ fn is_date_within_range(date: NaiveDate) -> bool {
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!(
"Excel date must be greater than {}",
MINIMUM_DATE_SERIAL_NUMBER
"Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
));
};
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!(
"Excel date must be less than {}",
MAXIMUM_DATE_SERIAL_NUMBER
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
));
};
#[allow(clippy::expect_used)]
@@ -39,7 +39,7 @@ pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
match NaiveDate::from_ymd_opt(year, month, day) {
Some(native_date) => Ok(convert_to_serial_number(native_date)),
None => Err("Out of range parameters for date".to_string()),
None => Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()),
}
}
@@ -57,7 +57,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
return Ok(MINIMUM_DATE_SERIAL_NUMBER);
}
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
return Err("Out of range parameters for date".to_string());
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
};
// One thing to note for example is that even if you started with a year out of range
@@ -70,7 +70,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
// As a result, we have to run range checks as we parse the date from the biggest unit to the
// smallest unit.
if !is_date_within_range(date) {
return Err("Out of range parameters for date".to_string());
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
}
date = {
@@ -82,7 +82,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
date = date + Months::new(abs_month);
}
if !is_date_within_range(date) {
return Err("Out of range parameters for date".to_string());
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
}
date
};
@@ -96,7 +96,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
date = date + Days::new(abs_day);
}
if !is_date_within_range(date) {
return Err("Out of range parameters for date".to_string());
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
}
date
};

View File

@@ -15,7 +15,7 @@ pub struct Formatted {
/// Returns the vector of chars of the fractional part of a *positive* number:
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> {
let b = format!("{:.1$}", value.fract(), precision as usize)
.chars()
.collect::<Vec<char>>();
@@ -30,6 +30,12 @@ fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
if last_non_zero < 2 {
return vec![];
}
let max_len = if int_len > 15 {
2_usize
} else {
15_usize - int_len + 1
};
let last_non_zero = usize::min(last_non_zero, max_len + 1);
b[2..last_non_zero].to_vec()
}
@@ -120,8 +126,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
// We should have different codepaths for general formatting and errors
let value_abs = value.abs();
if (1.0e-8..1.0e+11).contains(&value_abs) {
let mut text = format!("{:.9}", value);
let mut text = format!("{value:.9}");
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
if locale.numbers.symbols.decimal != "." {
text = text.replace('.', &locale.numbers.symbols.decimal.to_string());
}
Formatted {
text,
color: None,
@@ -138,14 +147,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let exponent = value_abs.log10().floor();
value /= 10.0_f64.powf(exponent);
let sign = if exponent < 0.0 { '-' } else { '+' };
let s = format!("{:.5}", value);
let s = format!("{value:.5}");
let mut text = format!(
"{}E{}{:02}",
s.trim_end_matches('0').trim_end_matches('.'),
sign,
exponent.abs()
);
if locale.numbers.symbols.decimal != "." {
text = text.replace('.', &locale.numbers.symbols.decimal.to_string());
}
Formatted {
text: format!(
"{}E{}{:02}",
s.trim_end_matches('0').trim_end_matches('.'),
sign,
exponent.abs()
),
text,
color: None,
error: None,
}
@@ -154,48 +167,72 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Date(p) => {
let tokens = &p.tokens;
let mut text = "".to_string();
let date = match from_excel_date(value as i64) {
Ok(d) => d,
Err(e) => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(e),
}
}
};
let time_fract = value.fract();
let hours = (time_fract * 24.0).floor();
let minutes = ((time_fract * 24.0 - hours) * 60.0).floor();
let seconds = ((((time_fract * 24.0 - hours) * 60.0) - minutes) * 60.0).round();
let date = from_excel_date(value as i64).ok();
for token in tokens {
match token {
TextToken::Literal(c) => {
text = format!("{}{}", text, c);
text = format!("{text}{c}");
}
TextToken::Text(t) => {
text = format!("{}{}", text, t);
text = format!("{text}{t}");
}
TextToken::Ghost(_) => {
// we just leave a whitespace
// This is what the TEXT function does
text = format!("{} ", text);
text = format!("{text} ");
}
TextToken::Spacer(_) => {
// we just leave a whitespace
// This is what the TEXT function does
text = format!("{} ", text);
text = format!("{text} ");
}
TextToken::Raw => {
text = format!("{}{}", text, value);
text = format!("{text}{value}");
}
TextToken::Digit(_) => {}
TextToken::Period => {}
TextToken::Day => {
let day = date.day() as usize;
text = format!("{}{}", text, day);
}
TextToken::Day => match date {
Some(date) => {
let day = date.day() as usize;
text = format!("{text}{day}");
}
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
},
TextToken::DayPadded => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let day = date.day() as usize;
text = format!("{}{:02}", text, day);
text = format!("{text}{day:02}");
}
TextToken::DayNameShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let mut day = date.weekday().number_from_monday() as usize;
if day == 7 {
day = 0;
@@ -203,6 +240,16 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names_short[day]);
}
TextToken::DayName => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let mut day = date.weekday().number_from_monday() as usize;
if day == 7 {
day = 0;
@@ -210,32 +257,144 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names[day]);
}
TextToken::Month => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{}{}", text, month);
text = format!("{text}{month}");
}
TextToken::MonthPadded => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{}{:02}", text, month);
text = format!("{text}{month:02}");
}
TextToken::MonthNameShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months_short[month - 1]);
}
TextToken::MonthName => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months[month - 1]);
}
TextToken::MonthLetter => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
let months_letter = &locale.dates.months_letter[month - 1];
text = format!("{}{}", text, months_letter);
text = format!("{text}{months_letter}");
}
TextToken::YearShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
text = format!("{}{}", text, date.format("%y"));
}
TextToken::Year => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
text = format!("{}{}", text, date.year());
}
TextToken::Hour => {
let mut hour = hours as i32;
if p.use_ampm {
if hour == 0 {
hour = 12;
} else if hour > 12 {
hour -= 12;
}
}
text = format!("{text}{hour}");
}
TextToken::HourPadded => {
let mut hour = hours as i32;
if p.use_ampm {
if hour == 0 {
hour = 12;
} else if hour > 12 {
hour -= 12;
}
}
text = format!("{text}{hour:02}");
}
TextToken::Second => {
let second = seconds as i32;
text = format!("{text}{second}");
}
TextToken::SecondPadded => {
let second = seconds as i32;
text = format!("{text}{second:02}");
}
TextToken::AMPM => {
let ampm = if hours < 12.0 { "AM" } else { "PM" };
text = format!("{text}{ampm}");
}
TextToken::Minute => {
let minute = minutes as i32;
text = format!("{text}{minute}");
}
TextToken::MinutePadded => {
let minute = minutes as i32;
text = format!("{text}{minute:02}");
}
}
}
Formatted {
@@ -247,7 +406,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Number(p) => {
let mut text = "".to_string();
if let Some(c) = p.currency {
text = format!("{}", c);
text = format!("{c}");
}
let tokens = &p.tokens;
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
@@ -277,7 +436,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if value_abs as i64 == 0 {
int_part = vec![];
}
let fract_part = get_fract_part(value_abs, p.precision);
let fract_part = get_fract_part(value_abs, p.precision, int_part.len());
// ln is the number of digits of the integer part of the value
let ln = int_part.len() as i32;
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
@@ -295,26 +454,26 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
for token in tokens {
match token {
TextToken::Literal(c) => {
text = format!("{}{}", text, c);
text = format!("{text}{c}");
}
TextToken::Text(t) => {
text = format!("{}{}", text, t);
text = format!("{text}{t}");
}
TextToken::Ghost(_) => {
// we just leave a whitespace
// This is what the TEXT function does
text = format!("{} ", text);
text = format!("{text} ");
}
TextToken::Spacer(_) => {
// we just leave a whitespace
// This is what the TEXT function does
text = format!("{} ", text);
text = format!("{text} ");
}
TextToken::Raw => {
text = format!("{}{}", text, value);
text = format!("{text}{value}");
}
TextToken::Period => {
text = format!("{}{}", text, decimal_separator);
text = format!("{text}{decimal_separator}");
}
TextToken::Digit(digit) => {
if digit.number == 'i' {
@@ -322,7 +481,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let index = digit.index;
let number_index = ln - digit_count + index;
if index == 0 && is_negative {
text = format!("-{}", text);
text = format!("-{text}");
}
if ln <= digit_count {
// The number of digits is less or equal than the number of digit tokens
@@ -347,7 +506,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} else {
""
};
text = format!("{}{}{}", text, c, sep);
text = format!("{text}{c}{sep}");
}
digit_index += 1;
} else {
@@ -373,18 +532,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if index < fract_part.len() {
text = format!("{}{}", text, fract_part[index]);
} else if digit.kind == '0' {
text = format!("{}0", text);
text = format!("{text}0");
} else if digit.kind == '?' {
text = format!("{} ", text);
text = format!("{text} ");
}
} else if digit.number == 'e' {
// 3. Exponent part
let index = digit.index;
if index == 0 {
if exponent_is_negative {
text = format!("{}E-", text);
text = format!("{text}E-");
} else {
text = format!("{}E+", text);
text = format!("{text}E+");
}
}
let number_index = l_exp - (p.exponent_digit_count - index);
@@ -400,7 +559,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
exponent_part[number_index as usize]
};
text = format!("{}{}", text, c);
text = format!("{text}{c}");
}
} else {
for i in 0..number_index + 1 {
@@ -422,6 +581,13 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => {}
TextToken::YearShort => {}
TextToken::Year => {}
TextToken::Hour => {}
TextToken::HourPadded => {}
TextToken::Minute => {}
TextToken::MinutePadded => {}
TextToken::Second => {}
TextToken::SecondPadded => {}
TextToken::AMPM => {}
}
}
Formatted {
@@ -591,15 +757,17 @@ fn parse_date(value: &str) -> Result<(i32, String), String> {
/// "30.34%" => (0.3034, "0.00%")
/// 100€ => (100, "100€")
pub(crate) fn parse_formatted_number(
value: &str,
original: &str,
currencies: &[&str],
decimal_separator: u8,
group_separator: u8,
) -> Result<(f64, Option<String>), String> {
let value = value.trim();
let value = original.trim();
let scientific_format = "0.00E+00";
// Check if it is a percentage
if let Some(p) = value.strip_suffix('%') {
let (f, options) = parse_number(p.trim())?;
let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
if options.is_scientific {
return Ok((f / 100.0, Some(scientific_format.to_string())));
}
@@ -614,8 +782,8 @@ pub(crate) fn parse_formatted_number(
// check if it is a currency in currencies
for currency in currencies {
if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
let (f, options) = parse_number(p.trim())?;
if let Some(p) = value.strip_prefix(&format!("-{currency}")) {
let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
if options.is_scientific {
return Ok((f, Some(scientific_format.to_string())));
}
@@ -624,7 +792,7 @@ pub(crate) fn parse_formatted_number(
}
return Ok((-f, Some(format!("{currency}#,##0"))));
} else if let Some(p) = value.strip_prefix(currency) {
let (f, options) = parse_number(p.trim())?;
let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
if options.is_scientific {
return Ok((f, Some(scientific_format.to_string())));
}
@@ -633,7 +801,7 @@ pub(crate) fn parse_formatted_number(
}
return Ok((f, Some(format!("{currency}#,##0"))));
} else if let Some(p) = value.strip_suffix(currency) {
let (f, options) = parse_number(p.trim())?;
let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
if options.is_scientific {
return Ok((f, Some(scientific_format.to_string())));
}
@@ -646,12 +814,13 @@ pub(crate) fn parse_formatted_number(
}
}
if let Ok((serial_number, format)) = parse_date(value) {
// check if it is a date. NOTE: we don't trim the original here
if let Ok((serial_number, format)) = parse_date(original) {
return Ok((serial_number as f64, Some(format)));
}
// Lastly we check if it is a number
let (f, options) = parse_number(value)?;
let (f, options) = parse_number(value, decimal_separator, group_separator)?;
if options.is_scientific {
return Ok((f, Some(scientific_format.to_string())));
}
@@ -674,7 +843,11 @@ struct NumberOptions {
// tries to parse 'value' as a number.
// If it is a number it either uses commas as thousands separator or it does not
fn parse_number(value: &str) -> Result<(f64, NumberOptions), String> {
fn parse_number(
value: &str,
decimal_separator: u8,
group_separator: u8,
) -> Result<(f64, NumberOptions), String> {
let mut position = 0;
let bytes = value.as_bytes();
let len = bytes.len();
@@ -682,8 +855,6 @@ fn parse_number(value: &str) -> Result<(f64, NumberOptions), String> {
return Err("Cannot parse number".to_string());
}
let mut chars = String::from("");
let decimal_separator = b'.';
let group_separator = b',';
let mut group_separator_index = Vec::new();
// get the sign
let sign = if bytes[0] == b'-' {

View File

@@ -26,19 +26,23 @@ pub enum Token {
Scientific, // E+
ScientificMinus, // E-
General, // General
// Dates
// Dates and time
Day, // d
DayPadded, // dd
DayNameShort, // ddd
DayName, // dddd+
Month, // m
MonthPadded, // mm
Month, // m (or minute)
MonthPadded, // mm (or minute padded)
MonthNameShort, // mmm
MonthName, // mmmm or mmmmmm+
MonthLetter, // mmmmm
YearShort, // y or yy
Year, // yyy+
// TODO: Hours Minutes and Seconds
Hour, // h
HourPadded, // hh
Second, // s
SecondPadded, // ss
AMPM, // AM/PM (or A/P)
ILLEGAL,
EOF,
}
@@ -333,7 +337,7 @@ impl Lexer {
} else if s == '-' {
Token::ScientificMinus
} else {
self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
Token::ILLEGAL
}
} else {
@@ -361,8 +365,8 @@ impl Lexer {
self.read_next_char();
}
match m {
1 => Token::Month,
2 => Token::MonthPadded,
1 => Token::Month, // (or minute)
2 => Token::MonthPadded, // (or minute padded)
3 => Token::MonthNameShort,
4 => Token::MonthName,
5 => Token::MonthLetter,
@@ -381,18 +385,75 @@ impl Lexer {
Token::Year
}
}
'h' => {
let mut h = 1;
while let Some('h') = self.peek_char() {
h += 1;
self.read_next_char();
}
if h == 1 {
Token::Hour
} else if h == 2 {
Token::HourPadded
} else {
self.set_error("Unexpected character after 'h'");
Token::ILLEGAL
}
}
's' => {
let mut s = 1;
while let Some('s') = self.peek_char() {
s += 1;
self.read_next_char();
}
if s == 1 {
Token::Second
} else if s == 2 {
Token::SecondPadded
} else {
self.set_error("Unexpected character after 's'");
Token::ILLEGAL
}
}
'A' | 'a' => {
if let Some('M') | Some('m') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'A'");
return Token::ILLEGAL;
}
if let Some('/') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AM'");
return Token::ILLEGAL;
}
if let Some('P') | Some('p') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AM'");
return Token::ILLEGAL;
}
if let Some('M') | Some('m') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AMP'");
return Token::ILLEGAL;
}
Token::AMPM
}
'g' | 'G' => {
for c in "eneral".chars() {
let cc = self.read_next_char();
if Some(c) != cc {
self.set_error(&format!("Unexpected character: {}", x));
self.set_error(&format!("Unexpected character: {x}"));
return Token::ILLEGAL;
}
}
Token::General
}
_ => {
self.set_error(&format!("Unexpected character: {}", x));
self.set_error(&format!("Unexpected character: {x}"));
Token::ILLEGAL
}
},

View File

@@ -27,6 +27,13 @@ pub enum TextToken {
MonthLetter,
YearShort,
Year,
Hour,
HourPadded,
Minute,
MinutePadded,
Second,
SecondPadded,
AMPM,
}
pub struct NumberPart {
pub color: Option<i32>,
@@ -45,6 +52,7 @@ pub struct NumberPart {
pub struct DatePart {
pub color: Option<i32>,
pub use_ampm: bool,
pub tokens: Vec<TextToken>,
}
@@ -101,6 +109,7 @@ impl Parser {
let mut digit_count = 0;
let mut precision = 0;
let mut is_date = false;
let mut use_ampm = false;
let mut is_number = false;
let mut found_decimal_dot = false;
let mut use_thousands = false;
@@ -116,6 +125,7 @@ impl Parser {
let mut number = 'i';
let mut index = 0;
let mut currency = None;
let mut is_time = false;
while token != Token::EOF && token != Token::Separator {
let next_token = self.lexer.next_token();
@@ -200,6 +210,9 @@ impl Parser {
index += 1;
}
Token::Literal(value) => {
if value == ':' {
is_time = true;
}
tokens.push(TextToken::Literal(value));
}
Token::Text(value) => {
@@ -236,12 +249,22 @@ impl Parser {
tokens.push(TextToken::MonthName);
}
Token::Month => {
is_date = true;
tokens.push(TextToken::Month);
if is_time {
// minute
tokens.push(TextToken::Minute);
} else {
is_date = true;
tokens.push(TextToken::Month);
}
}
Token::MonthPadded => {
is_date = true;
tokens.push(TextToken::MonthPadded);
if is_time {
// minute padded
tokens.push(TextToken::MinutePadded);
} else {
is_date = true;
tokens.push(TextToken::MonthPadded);
}
}
Token::MonthLetter => {
is_date = true;
@@ -255,6 +278,32 @@ impl Parser {
is_date = true;
tokens.push(TextToken::Year);
}
Token::Hour => {
is_date = true;
is_time = true;
tokens.push(TextToken::Hour);
}
Token::HourPadded => {
is_date = true;
is_time = true;
tokens.push(TextToken::HourPadded);
}
Token::Second => {
is_date = true;
is_time = true;
tokens.push(TextToken::Second);
}
Token::SecondPadded => {
is_date = true;
is_time = true;
tokens.push(TextToken::SecondPadded);
}
Token::AMPM => {
is_date = true;
use_ampm = true;
tokens.push(TextToken::AMPM);
}
Token::Scientific => {
if !is_scientific {
index = 0;
@@ -282,7 +331,11 @@ impl Parser {
if is_number {
return ParsePart::Error(ErrorPart {});
}
ParsePart::Date(DatePart { color, tokens })
ParsePart::Date(DatePart {
color,
use_ampm,
tokens,
})
} else {
ParsePart::Number(NumberPart {
color,

View File

@@ -1,2 +1,3 @@
mod test_general;
mod test_parse_formatted_number;
mod test_time;

View File

@@ -1,4 +1,5 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::{
formatter::format::format_number,
@@ -202,3 +203,9 @@ fn test_date() {
"Sat-September-12"
);
}
#[test]
fn test_german_locale() {
let locale = get_locale("de").expect("");
assert_eq!(format_number(1234.56, "General", locale).text, "1234,56");
}

View File

@@ -1,9 +1,13 @@
#![allow(clippy::unwrap_used)]
use crate::formatter::format::parse_formatted_number as parse;
use crate::formatter::format::parse_formatted_number;
const PARSE_ERROR_MSG: &str = "Could not parse number";
fn parse(input: &str, currencies: &[&str]) -> Result<(f64, Option<String>), String> {
parse_formatted_number(input, currencies, b'.', b',')
}
#[test]
fn numbers() {
// whole numbers

View File

@@ -0,0 +1,32 @@
#![allow(clippy::unwrap_used)]
use crate::{
formatter::format::format_number,
locale::{get_locale, Locale},
};
fn get_default_locale() -> &'static Locale {
get_locale("en").unwrap()
}
#[test]
fn simple_test() {
let locale = get_default_locale();
let format = "h:mm AM/PM";
let value = 16.001_423_611_111_11; // =1/86400 => 12:02 AM
let formatted = format_number(value, format, locale);
assert_eq!(formatted.text, "12:02 AM");
}
#[test]
fn padded_vs_unpadded() {
let locale = get_default_locale();
let padded_format = "hh:mm:ss AM/PM";
let unpadded_format = "h:m:s AM/PM";
let value = 0.25351851851851853; // => 6:05:04 AM (21904/(24*60*60)) where 21904 = 6 * 3600 + 5*60 + 4
let formatted = format_number(value, padded_format, locale);
assert_eq!(formatted.text, "06:05:04 AM");
let formatted = format_number(value, unpadded_format, locale);
assert_eq!(formatted.text, "6:5:4 AM");
}

View File

@@ -137,7 +137,7 @@ pub(crate) fn binary_search_descending_or_greater<T: Ord>(target: &T, array: &[T
Some((n - r - 1) as i32)
}
impl Model {
impl<'a> Model<'a> {
/// Returns an array with the list of cell values in the range
pub(crate) fn prepare_array(
&mut self,

View File

@@ -0,0 +1,946 @@
use chrono::Datelike;
use crate::{
calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
formatter::dates::date_to_serial_number,
Model,
};
use super::util::{compare_values, from_wildcard_to_regex, result_matches_regex};
impl<'a> Model<'a> {
// =DAVERAGE(database, field, criteria)
pub(crate) fn fn_daverage(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut sum = 0.0f64;
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
sum += n;
count += 1;
}
}
}
row += 1;
}
if count == 0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "No numeric values matched criteria".to_string(),
};
}
CalcResult::Number(sum / count as f64)
}
// =DCOUNT(database, field, criteria)
// Counts numeric entries in the field for rows that match criteria
pub(crate) fn fn_dcount(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if matches!(v, CalcResult::Number(_)) {
count += 1;
}
}
row += 1;
}
CalcResult::Number(count as f64)
}
// =DGET(database, field, criteria)
// Returns the (single) field value for the unique matching row
pub(crate) fn fn_dget(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut result: Option<CalcResult> = None;
let mut matches = 0usize;
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
matches += 1;
if matches > 1 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "More than one matching record".to_string(),
};
}
result = Some(self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
}));
}
row += 1;
}
match (matches, result) {
(0, _) | (_, None) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No matching record".to_string(),
},
(_, Some(v)) => v,
}
}
// =DMAX(database, field, criteria)
pub(crate) fn fn_dmax(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.db_extreme(args, cell, true)
}
// =DMIN(database, field, criteria)
pub(crate) fn fn_dmin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.db_extreme(args, cell, false)
}
// =DSUM(database, field, criteria)
pub(crate) fn fn_dsum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut sum = 0.0;
// skip header
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
sum += n;
}
}
}
row += 1;
}
CalcResult::Number(sum)
}
// =DCOUNTA(database, field, criteria)
// Counts non-empty entries (any type) in the field for rows that match criteria
pub(crate) fn fn_dcounta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut count = 0;
for row in (db_left.row + 1)..=db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if !matches!(v, CalcResult::EmptyCell | CalcResult::EmptyArg) {
count += 1;
}
}
}
CalcResult::Number(count as f64)
}
// =DPRODUCT(database, field, criteria)
pub(crate) fn fn_dproduct(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut product = 1.0f64;
let mut has_numeric = false;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
product *= n;
has_numeric = true;
}
}
}
row += 1;
}
// Excel returns 0 when no rows / no numeric values match for DPRODUCT
if has_numeric {
CalcResult::Number(product)
} else {
CalcResult::Number(0.0)
}
}
// Small internal helper for DSTDEV / DVAR
// Collects sum, sum of squares, and count of numeric values in the field
// for rows that match the criteria.
fn db_numeric_stats(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> Result<(f64, f64, usize), CalcResult> {
if args.len() != 3 {
return Err(CalcResult::new_args_number_error(cell));
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return Err(e),
};
let field_col = self.resolve_db_field_column(db_left, db_right, &args[1], cell)?;
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return Err(e),
};
if db_right.row <= db_left.row {
// no data rows
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
});
}
let mut sum = 0.0f64;
let mut sumsq = 0.0f64;
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
sum += n;
sumsq += n * n;
count += 1;
}
}
}
row += 1;
}
Ok((sum, sumsq, count))
}
// =DSTDEV(database, field, criteria)
// Sample standard deviation of matching numeric values
pub(crate) fn fn_dstdev(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
if count < 2 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Not enough numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var.sqrt())
}
// =DVAR(database, field, criteria)
// Sample variance of matching numeric values
pub(crate) fn fn_dvar(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
if count < 2 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Not enough numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var)
}
// =DSTDEVP(database, field, criteria)
// Population standard deviation of matching numeric values
pub(crate) fn fn_dstdevp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if no numeric values match
if count == 0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "No numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / n;
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var.sqrt())
}
// =DVARP(database, field, criteria)
// Population variance of matching numeric values
pub(crate) fn fn_dvarp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if no numeric values match
if count == 0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "No numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / n;
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var)
}
/// Resolve the "field" (2nd arg) to an absolute column index (i32) within the sheet.
/// Field can be a number (1-based index) or a header name (case-insensitive).
/// Returns the absolute column index, not a 1-based offset within the database range.
fn resolve_db_field_column(
&mut self,
db_left: CellReferenceIndex,
db_right: CellReferenceIndex,
field_arg: &Node,
cell: CellReferenceIndex,
) -> Result<i32, CalcResult> {
let field_column_name = match self.evaluate_node_in_context(field_arg, cell) {
CalcResult::String(s) => s.to_lowercase(),
CalcResult::Number(index) => {
let index = index.floor() as i32;
if index < 1 || db_left.column + index - 1 > db_right.column {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Field index out of range".to_string(),
});
}
return Ok(db_left.column + index - 1);
}
CalcResult::Boolean(b) => {
return if b {
Ok(db_left.column)
} else {
// Index 0 is out of range
Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid field specifier".to_string(),
})
};
}
error @ CalcResult::Error { .. } => {
return Err(error);
}
CalcResult::Range { .. } => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
};
// We search in the database a column whose header matches field_column_name
for column in db_left.column..=db_right.column {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row: db_left.row,
column,
});
match &v {
CalcResult::String(s) => {
if s.to_lowercase() == field_column_name {
return Ok(column);
}
}
CalcResult::Number(n) => {
if field_column_name == n.to_string() {
return Ok(column);
}
}
CalcResult::Boolean(b) => {
if field_column_name == b.to_string() {
return Ok(column);
}
}
CalcResult::Error { .. }
| CalcResult::Range { .. }
| CalcResult::EmptyCell
| CalcResult::EmptyArg
| CalcResult::Array(_) => {}
}
}
Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Field header not found".to_string(),
})
}
/// Check whether a database row matches the criteria range.
/// Criteria logic: OR across criteria rows; AND across columns within a row.
fn db_row_matches_criteria(
&mut self,
db_left: CellReferenceIndex,
db_right: CellReferenceIndex,
row: i32,
criteria: (CellReferenceIndex, CellReferenceIndex),
) -> bool {
let (c_left, c_right) = criteria;
// Read criteria headers (first row of criteria range)
// Map header name (lowercased) -> db column (if exists)
let mut crit_cols: Vec<i32> = Vec::new();
let mut header_count = 0;
// We cover the criteria table:
// headerA | headerB | ...
// critA1 | critA2 | ...
// critB1 | critB2 | ...
// ...
for column in c_left.column..=c_right.column {
let cell = CellReferenceIndex {
sheet: c_left.sheet,
row: c_left.row,
column,
};
let criteria_header = self.evaluate_cell(cell);
if let Ok(s) = self.cast_to_string(criteria_header, cell) {
// Non-empty string header. If the header is non string we skip it
header_count += 1;
let wanted = s.to_lowercase();
// Find corresponding Database column
let mut found = false;
for db_column in db_left.column..=db_right.column {
let db_header = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row: db_left.row,
column: db_column,
});
if let Ok(hs) = self.cast_to_string(db_header, cell) {
if hs.to_lowercase() == wanted {
crit_cols.push(db_column);
found = true;
break;
}
}
}
if !found {
// that means the criteria column has no matching DB column
// If the criteria condition is empty then we remove this condition
// otherwise this condition can never be satisfied
// We evaluate all criteria rows to see if any is non-empty
let mut has_non_empty = false;
for r in (c_left.row + 1)..=c_right.row {
let ccell = self.evaluate_cell(CellReferenceIndex {
sheet: c_left.sheet,
row: r,
column,
});
if !matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
has_non_empty = true;
break;
}
}
if has_non_empty {
// This criteria column can never be satisfied
header_count -= 1;
}
}
};
}
if c_right.row <= c_left.row {
// If no criteria rows (only headers), everything matches
return true;
}
if header_count == 0 {
// If there are not "String" headers, nothing matches
// NB: There might be String headers that do not match any DB columns,
// in that case everything matches.
return false;
}
// Evaluate each criteria row (OR)
for r in (c_left.row + 1)..=c_right.row {
// AND across columns for this criteria row
let mut and_ok = true;
for (offset, db_col) in crit_cols.iter().enumerate() {
// Criteria cell
let ccell = self.evaluate_cell(CellReferenceIndex {
sheet: c_left.sheet,
row: r,
column: c_left.column + offset as i32,
});
// Empty criteria cell -> ignored
if matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
continue;
}
// Database value for this row/column
let db_val = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: *db_col,
});
if !self.criteria_cell_matches(&db_val, &ccell) {
and_ok = false;
break;
}
}
if and_ok {
// This criteria row satisfied (OR)
return true;
}
}
// none matched
false
}
/// Implements Excel-like criteria matching for a single value.
/// Supports prefixes: <>, >=, <=, >, <, = ; wildcards * and ? for string equals.
fn criteria_cell_matches(&self, db_val: &CalcResult, crit_cell: &CalcResult) -> bool {
// Convert the criteria cell to a string for operator parsing if possible,
// otherwise fall back to equality via compare_values.
let mut criteria = match crit_cell {
CalcResult::String(s) => s.trim().to_string(),
CalcResult::Number(n) => {
// treat as equality with number
return match db_val {
CalcResult::Number(v) => (*v - *n).abs() <= f64::EPSILON,
_ => false,
};
}
CalcResult::Boolean(b) => {
// check equality with boolean
return match db_val {
CalcResult::Boolean(v) => *v == *b,
_ => false,
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Error { .. } => return false,
CalcResult::Range { .. } | CalcResult::Array(_) => return false,
};
// Detect operator prefix
let mut op = "="; // default equality (with wildcard semantics for strings)
let prefixes = ["<>", ">=", "<=", ">", "<", "="];
for p in prefixes.iter() {
if criteria.starts_with(p) {
op = p;
criteria = criteria[p.len()..].trim().to_string();
break;
}
}
// Is it a number?
let rhs_num = criteria.parse::<f64>().ok();
// Is it a date?
// FIXME: We should parse dates according to locale settings
let rhs_date = criteria.parse::<chrono::NaiveDate>().ok();
match op {
">" | ">=" | "<" | "<=" => {
if let Some(d) = rhs_date {
// date comparison
let serial = match date_to_serial_number(d.day(), d.month(), d.year()) {
Ok(sn) => sn as f64,
Err(_) => return false,
};
if let CalcResult::Number(n) = db_val {
match op {
">" => *n > serial,
">=" => *n >= serial,
"<" => *n < serial,
"<=" => *n <= serial,
_ => false,
}
} else {
false
}
} else if let Some(t) = rhs_num {
// numeric comparison
if let CalcResult::Number(n) = db_val {
match op {
">" => *n > t,
">=" => *n >= t,
"<" => *n < t,
"<=" => *n <= t,
_ => false,
}
} else {
false
}
} else {
// string comparison (case-insensitive) using compare_values semantics
let rhs = CalcResult::String(criteria.to_lowercase());
let lhs = match db_val {
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
x => x.clone(),
};
let c = compare_values(&lhs, &rhs);
match op {
">" => c > 0,
">=" => c >= 0,
"<" => c < 0,
"<=" => c <= 0,
_ => false,
}
}
}
"<>" => {
// not equal (with wildcard semantics for strings)
// If rhs has wildcards and db_val is string, do regex; else use compare_values != 0
if let CalcResult::String(s) = db_val {
if criteria.contains('*') || criteria.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
return !result_matches_regex(
&CalcResult::String(s.to_lowercase()),
&re,
);
}
}
}
let rhs = if let Some(n) = rhs_num {
CalcResult::Number(n)
} else {
CalcResult::String(criteria.to_lowercase())
};
let lhs = match db_val {
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
x => x.clone(),
};
compare_values(&lhs, &rhs) != 0
}
_ => {
// equality. For strings, support wildcards (*, ?)
if let Some(n) = rhs_num {
// numeric equals
if let CalcResult::Number(m) = db_val {
(*m - n).abs() <= f64::EPSILON
} else {
compare_values(db_val, &CalcResult::Number(n)) == 0
}
} else {
// textual/boolean equals (case-insensitive), wildcard-enabled for strings
if let CalcResult::String(s) = db_val {
if criteria.contains('*') || criteria.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
return result_matches_regex(
&CalcResult::String(s.to_lowercase()),
&re,
);
}
}
// This is weird but we only need to check if "starts with" for equality
return s.to_lowercase().starts_with(&criteria.to_lowercase());
}
// Fallback: compare_values equality
compare_values(db_val, &CalcResult::String(criteria.to_lowercase())) == 0
}
}
}
}
/// Shared implementation for DMAX/DMIN
fn db_extreme(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
want_max: bool,
) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut best: Option<f64> = None;
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(value) = v {
if value.is_finite() {
best = Some(match best {
None => value,
Some(cur) => {
if want_max {
value.max(cur)
} else {
value.min(cur)
}
}
});
}
}
}
row += 1;
}
match best {
Some(v) => CalcResult::Number(v),
None => CalcResult::Number(0.0),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,18 @@
use statrs::function::erf::{erf, erfc};
use crate::{
calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
model::Model,
};
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y, erf};
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y};
// https://root.cern/doc/v610/TMath_8cxx_source.html
// Notice that the parameters for Bessel functions in Excel and here have inverted order
// EXCEL_BESSEL(x, n) => bessel(n, x)
impl Model {
impl<'a> Model<'a> {
pub(crate) fn fn_besseli(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
@@ -160,7 +162,7 @@ impl Model {
Ok(f) => f,
Err(s) => return s,
};
CalcResult::Number(1.0 - erf(x))
CalcResult::Number(erfc(x))
}
pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -171,6 +173,6 @@ impl Model {
Ok(f) => f,
Err(s) => return s,
};
CalcResult::Number(1.0 - erf(x))
CalcResult::Number(erfc(x))
}
}

View File

@@ -7,7 +7,7 @@ use crate::{
// 2^48-1
const MAX: f64 = 281474976710655.0;
impl Model {
impl<'a> Model<'a> {
// BITAND( number1, number2)
pub(crate) fn fn_bitand(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {

View File

@@ -46,18 +46,18 @@ impl fmt::Display for Complex {
// it is a bit weird what Excel does but it seems it uses general notation for
// numbers > 1e-20 and scientific notation for the rest
let y_str = if y.abs() <= 9e-20 {
format!("{:E}", y)
format!("{y:E}")
} else if y == 1.0 {
"".to_string()
} else if y == -1.0 {
"-".to_string()
} else {
format!("{}", y)
format!("{y}")
};
let x_str = if x.abs() <= 9e-20 {
format!("{:E}", x)
format!("{x:E}")
} else {
format!("{}", x)
format!("{x}")
};
if y == 0.0 && x == 0.0 {
write!(f, "0")
@@ -182,7 +182,7 @@ fn parse_complex_number(s: &str) -> Result<(f64, f64, Suffix), String> {
}
}
impl Model {
impl<'a> Model<'a> {
fn get_complex_number(
&mut self,
node: &Node,

View File

@@ -41,7 +41,7 @@ fn convert_temperature(
}
}
impl Model {
impl<'a> Model<'a> {
// CONVERT(number, from_unit, to_unit)
pub(crate) fn fn_convert(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {

View File

@@ -5,7 +5,7 @@ use crate::{
number_format::to_precision,
};
impl Model {
impl<'a> Model<'a> {
// DELTA(number1, [number2])
pub(crate) fn fn_delta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();

View File

@@ -31,7 +31,7 @@ fn from_binary_to_decimal(value: f64) -> Result<i64, String> {
Ok(result)
}
impl Model {
impl<'a> Model<'a> {
// BIN2DEC(number)
pub(crate) fn fn_bin2dec(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
@@ -76,7 +76,7 @@ impl Model {
if value < 0 {
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
} else {
let result = format!("{:X}", value);
let result = format!("{value:X}");
if let Some(places) = places {
if places < result.len() as i32 {
return CalcResult::new_error(
@@ -120,7 +120,7 @@ impl Model {
if value < 0 {
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
} else {
let result = format!("{:o}", value);
let result = format!("{value:o}");
if let Some(places) = places {
if places < result.len() as i32 {
return CalcResult::new_error(
@@ -163,7 +163,7 @@ impl Model {
if value < 0 {
value += 1024;
}
let result = format!("{:b}", value);
let result = format!("{value:b}");
if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -202,7 +202,7 @@ impl Model {
if value < 0 {
value += HEX_MAX;
}
let result = format!("{:X}", value);
let result = format!("{value:X}");
if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -242,7 +242,7 @@ impl Model {
if value < 0 {
value += OCT_MAX;
}
let result = format!("{:o}", value);
let result = format!("{value:o}");
if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -301,7 +301,7 @@ impl Model {
if value < 0 {
value += 1024;
}
let result = format!("{:b}", value);
let result = format!("{value:b}");
if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -391,7 +391,7 @@ impl Model {
if value < 0 {
value += OCT_MAX;
}
let result = format!("{:o}", value);
let result = format!("{value:o}");
if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -446,7 +446,7 @@ impl Model {
if value < 0 {
value += 1024;
}
let result = format!("{:b}", value);
let result = format!("{value:b}");
if let Some(places) = places {
if value < 512 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -532,7 +532,7 @@ impl Model {
if value < 0 {
value += HEX_MAX;
}
let result = format!("{:X}", value);
let result = format!("{value:X}");
if let Some(places) = places {
if value < HEX_MAX_HALF && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());

View File

@@ -1,53 +0,0 @@
pub(crate) fn erf(x: f64) -> f64 {
let cof = vec![
-1.3026537197817094,
6.419_697_923_564_902e-1,
1.9476473204185836e-2,
-9.561_514_786_808_63e-3,
-9.46595344482036e-4,
3.66839497852761e-4,
4.2523324806907e-5,
-2.0278578112534e-5,
-1.624290004647e-6,
1.303655835580e-6,
1.5626441722e-8,
-8.5238095915e-8,
6.529054439e-9,
5.059343495e-9,
-9.91364156e-10,
-2.27365122e-10,
9.6467911e-11,
2.394038e-12,
-6.886027e-12,
8.94487e-13,
3.13092e-13,
-1.12708e-13,
3.81e-16,
7.106e-15,
-1.523e-15,
-9.4e-17,
1.21e-16,
-2.8e-17,
];
let mut d = 0.0;
let mut dd = 0.0;
let x_abs = x.abs();
let t = 2.0 / (2.0 + x_abs);
let ty = 4.0 * t - 2.0;
for j in (1..=cof.len() - 1).rev() {
let tmp = d;
d = ty * d - dd + cof[j];
dd = tmp;
}
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
if x < 0.0 {
res - 1.0
} else {
1.0 - res
}
}

View File

@@ -4,7 +4,6 @@ mod bessel_j1_y1;
mod bessel_jn_yn;
mod bessel_k;
mod bessel_util;
mod erf;
#[cfg(test)]
mod test_bessel;
@@ -13,4 +12,3 @@ pub(crate) use bessel_i::bessel_i;
pub(crate) use bessel_jn_yn::jn as bessel_j;
pub(crate) use bessel_jn_yn::yn as bessel_y;
pub(crate) use bessel_k::bessel_k;
pub(crate) use erf::erf;

View File

@@ -191,7 +191,7 @@ fn compute_ppmt(
// All, except for rate are easily solvable in terms of the others.
// In these formulas the payment (pmt) is normally negative
impl Model {
impl<'a> Model<'a> {
fn get_array_of_numbers_generic(
&mut self,
arg: &Node,
@@ -231,7 +231,7 @@ impl Model {
CalcResult::new_error(
Error::ERROR,
*cell,
format!("Invalid worksheet index: '{}'", sheet),
format!("Invalid worksheet index: '{sheet}'"),
)
})?
.dimension()
@@ -245,7 +245,7 @@ impl Model {
CalcResult::new_error(
Error::ERROR,
*cell,
format!("Invalid worksheet index: '{}'", sheet),
format!("Invalid worksheet index: '{sheet}'"),
)
})?
.dimension()

View File

@@ -1,10 +1,10 @@
use crate::{
calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
expressions::{parser::Node, token::Error, types::CellReferenceIndex, utils::number_to_column},
model::{Model, ParsedDefinedName},
};
impl Model {
impl<'a> Model<'a> {
pub(crate) fn fn_isnumber(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 {
match self.evaluate_node_in_context(&args[0], cell) {
@@ -320,4 +320,152 @@ impl Model {
message: "Invalid name".to_string(),
}
}
pub(crate) fn fn_n(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(n) => n,
CalcResult::String(_) => 0.0,
CalcResult::Boolean(f) => {
if f {
1.0
} else {
0.0
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
CalcResult::Number(value)
}
pub(crate) fn fn_sheets(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count > 1 {
return CalcResult::new_args_number_error(cell);
}
if arg_count == 1 {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Sheets function with an argument is not implemented".to_string(),
};
}
let sheet_count = self.workbook.worksheets.len() as f64;
CalcResult::Number(sheet_count)
}
/// INFO(info_type, [reference])
/// NB: In Excel "info_type" is localized. Here it is always in English.
pub(crate) fn fn_cell(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count == 0 || arg_count > 2 {
return CalcResult::new_args_number_error(cell);
}
let reference = if arg_count == 2 {
match self.evaluate_node_with_reference(&args[1], cell) {
CalcResult::Range { left, right: _ } => {
// we just take the left cell of the range
left
}
_ => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument must be a reference".to_string(),
}
}
}
} else {
CellReferenceIndex {
sheet: cell.sheet,
row: cell.row,
column: cell.column,
}
};
let info_type = match self.get_string(&args[0], cell) {
Ok(s) => s.to_uppercase(),
Err(e) => return e,
};
match info_type.as_str() {
"ADDRESS" => {
if reference.sheet != cell.sheet {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "References to other sheets not implemented".to_string(),
};
}
let column = match number_to_column(reference.column) {
Some(c) => c,
None => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid column".to_string(),
}
}
};
let address = format!("${}${}", column, reference.row);
CalcResult::String(address)
}
"COL" => CalcResult::Number(reference.column as f64),
"COLOR" | "FILENAME" | "FORMAT" | "PARENTHESES" | "PREFIX" | "PROTECT" | "WIDTH" => {
CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "info_type not implemented".to_string(),
}
}
"CONTENTS" => self.evaluate_cell(reference),
"ROW" => CalcResult::Number(reference.row as f64),
"TYPE" => {
let cell_type = match self.evaluate_cell(reference) {
CalcResult::EmptyCell => "b",
CalcResult::String(_) => "l",
CalcResult::Number(_) => "v",
CalcResult::Boolean(_) => "v",
CalcResult::Error { .. } => "v",
CalcResult::Range { .. } => "v",
CalcResult::EmptyArg => "v",
CalcResult::Array(_) => "v",
};
CalcResult::String(cell_type.to_string())
}
_ => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid info_type".to_string(),
},
}
}
pub(crate) fn fn_info(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() || args.len() > 2 {
return CalcResult::new_args_number_error(cell);
}
CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Info function not implemented".to_string(),
}
}
}

View File

@@ -6,7 +6,7 @@ use crate::{
use super::util::compare_values;
impl Model {
impl<'a> Model<'a> {
pub(crate) fn fn_true(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
CalcResult::Boolean(true)
@@ -246,7 +246,7 @@ impl Model {
}
// None of the cases matched so we return the default
// If there is an even number of args is the last one otherwise is #N/A
if args_count % 2 == 0 {
if args_count.is_multiple_of(2) {
return self.evaluate_node_in_context(&args[args_count - 1], cell);
}
CalcResult::Error {
@@ -262,7 +262,7 @@ impl Model {
if args_count < 2 {
return CalcResult::new_args_number_error(cell);
}
if args_count % 2 != 0 {
if !args_count.is_multiple_of(2) {
// Missing value for last condition
return CalcResult::new_args_number_error(cell);
}

View File

@@ -7,7 +7,7 @@ use crate::{
use super::util::{compare_values, from_wildcard_to_regex, result_matches_regex, values_are_equal};
impl Model {
impl<'a> Model<'a> {
pub(crate) fn fn_index(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let row_num;
let col_num;
@@ -698,7 +698,7 @@ impl Model {
let parsed_reference = ParsedReference::parse_reference_formula(
Some(cell.sheet),
&s,
&self.locale,
self.locale,
|name| self.get_sheet_index_by_name(name),
);
@@ -839,6 +839,10 @@ impl Model {
CalcResult::Range { left, right }
}
// FORMULATEXT(reference)
// Returns a formula as a string. Two differences with Excel:
// - It returns the formula in English
// - It formats the formula without spaces between elements
pub(crate) fn fn_formulatext(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
@@ -860,7 +864,7 @@ impl Model {
message: "argument must be a reference to a single cell".to_string(),
};
}
if let Ok(Some(f)) = self.get_cell_formula(left.sheet, left.row, left.column) {
if let Ok(Some(f)) = self.get_english_cell_formula(left.sheet, left.row, left.column) {
CalcResult::String(f)
} else {
CalcResult::Error {

View File

@@ -68,14 +68,14 @@ macro_rules! single_number_fn {
},
// If String, parse to f64 then apply or #VALUE! error
ArrayNode::String(s) => {
let node = match s.parse::<f64>() {
Ok(f) => match $op(f) {
let node = match self.cast_number(&s) {
Some(f) => match $op(f) {
Ok(x) => ArrayNode::Number(x),
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
Err(e) => ArrayNode::Error(e),
},
Err(_) => ArrayNode::Error(Error::VALUE),
None => ArrayNode::Error(Error::VALUE),
};
data_row.push(node);
}

View File

@@ -0,0 +1,200 @@
/// Parse Roman (classic or Excel variants) → number
pub fn from_roman(s: &str) -> Result<u32, String> {
if s.is_empty() {
return Err("empty numeral".into());
}
fn val(c: char) -> Option<u32> {
Some(match c {
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
_ => return None,
})
}
// Accept the union of subtractive pairs used by the tables above (Excel-compatible).
fn allowed_subtractive(a: char, b: char) -> bool {
matches!(
(a, b),
// classic:
('I','V')|('I','X')|('X','L')|('X','C')|('C','D')|('C','M')
// Excel forms:
|('V','L')|('L','D')|('L','M') // VL, LD, LM
|('X','D')|('X','M') // XD, XM
|('V','M') // VM
|('I','L')|('I','C')|('I','D')|('I','M') // IL, IC, ID, IM
|('V','D')|('V','C') // VD, VC
)
}
let chars: Vec<char> = s.chars().map(|c| c.to_ascii_uppercase()).collect();
let mut total = 0u32;
let mut i = 0usize;
// Repetition rules similar to classic Romans:
// V, L, D cannot repeat; I, X, C, M max 3 in a row.
let mut last_char: Option<char> = None;
let mut run_len = 0usize;
while i < chars.len() {
let c = chars[i];
let v = val(c).ok_or_else(|| format!("invalid character '{c}'"))?;
if Some(c) == last_char {
run_len += 1;
match c {
'V' | 'L' | 'D' => return Err(format!("invalid repetition of '{c}'")),
_ if run_len >= 3 => return Err(format!("invalid repetition of '{c}'")),
_ => {}
}
} else {
last_char = Some(c);
run_len = 0;
}
if i + 1 < chars.len() {
let c2 = chars[i + 1];
let v2 = val(c2).ok_or_else(|| format!("invalid character '{c2}'"))?;
if v < v2 {
if !allowed_subtractive(c, c2) {
return Err(format!("invalid subtractive pair '{c}{c2}'"));
}
// Disallow stacked subtractives like IIV, XXL:
if run_len > 0 {
return Err(format!("malformed numeral near position {i}"));
}
total += v2 - v;
i += 2;
last_char = None;
run_len = 0;
continue;
}
}
total += v;
i += 1;
}
Ok(total)
}
/// Classic Roman (strict) encoder used as a base for all forms.
fn to_roman(mut n: u32) -> Result<String, String> {
if !(1..=3999).contains(&n) {
return Err("value out of range (must be 1..=3999)".into());
}
const MAP: &[(u32, &str)] = &[
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut out = String::with_capacity(15);
for &(val, sym) in MAP {
while n >= val {
out.push_str(sym);
n -= val;
}
if n == 0 {
break;
}
}
Ok(out)
}
/// Excel/Google Sheets compatible ROMAN(number, [form]) encoder.
/// `form`: 0..=4 (0=Classic, 4=Simplified).
pub fn to_roman_with_form(n: u32, form: i32) -> Result<String, String> {
let mut s = to_roman(n)?;
if form == 0 {
return Ok(s);
}
if !(0..=4).contains(&form) {
return Err("form must be between 0 and 4".into());
}
// Base rules (apply for all f >= 1)
let base_rules: &[(&str, &str)] = &[
// C(D|M)XC -> L$1XL
("CDXC", "LDXL"),
("CMXC", "LMXL"),
// C(D|M)L -> L$1
("CDL", "LD"),
("CML", "LM"),
// X(L|C)IX -> V$1IV
("XLIX", "VLIV"),
("XCIX", "VCIV"),
// X(L|C)V -> V$1
("XLV", "VL"),
("XCV", "VC"),
];
// Level 2 extra rules
let lvl2_rules: &[(&str, &str)] = &[
// V(L|C)IV -> I$1
("VLIV", "IL"),
("VCIV", "IC"),
// L(D|M)XL -> X$1
("LDXL", "XD"),
("LMXL", "XM"),
// L(D|M)VL -> X$1V
("LDVL", "XDV"),
("LMVL", "XMV"),
// L(D|M)IL -> X$1IX
("LDIL", "XDIX"),
("LMIL", "XMIX"),
];
// Level 3 extra rules
let lvl3_rules: &[(&str, &str)] = &[
// X(D|M)V -> V$1
("XDV", "VD"),
("XMV", "VM"),
// X(D|M)IX -> V$1IV
("XDIX", "VDIV"),
("XMIX", "VMIV"),
];
// Level 4 extra rules
let lvl4_rules: &[(&str, &str)] = &[
// V(D|M)IV -> I$1
("VDIV", "ID"),
("VMIV", "IM"),
];
// Helper to apply a batch of (from -> to) globally, in order.
fn apply_rules(mut t: String, rules: &[(&str, &str)]) -> String {
for (from, to) in rules {
if t.contains(from) {
t = t.replace(from, to);
}
}
t
}
s = apply_rules(s, base_rules);
if form >= 2 {
s = apply_rules(s, lvl2_rules);
}
if form >= 3 {
s = apply_rules(s, lvl3_rules);
}
if form >= 4 {
s = apply_rules(s, lvl4_rules);
}
Ok(s)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,230 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
// Helper to check if two shapes are the same or compatible 1D shapes
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
(rows1 == rows2 && cols1 == cols2)
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
}
impl<'a> Model<'a> {
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let result = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(s) => return s,
};
let (_, _, values_left, values_right) = result;
let mut sum = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
let x = x_opt.unwrap_or(0.0);
let y = y_opt.unwrap_or(0.0);
sum += x * x - y * y;
}
CalcResult::Number(sum)
}
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let result = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(s) => return s,
};
let (_rows, _cols, values_left, values_right) = result;
let mut sum = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
let x = x_opt.unwrap_or(0.0);
let y = y_opt.unwrap_or(0.0);
sum += x * x + y * y;
}
CalcResult::Number(sum)
}
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let result = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(s) => return s,
};
let (_, _, values_left, values_right) = result;
let mut sum = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
let x = x_opt.unwrap_or(0.0);
let y = y_opt.unwrap_or(0.0);
let diff = x - y;
sum += diff * diff;
}
CalcResult::Number(sum)
}
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
// Returns (rows, cols, values_left, values_right) or an error.
pub(crate) fn fn_get_two_matrices(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> Result<TwoMatricesResult, CalcResult> {
if args.len() != 2 {
return Err(CalcResult::new_args_number_error(cell));
}
let x_range = self.evaluate_node_in_context(&args[0], cell);
let y_range = self.evaluate_node_in_context(&args[1], cell);
let result = match (x_range, y_range) {
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Range {
left: l2,
right: r2,
},
) => {
if l1.sheet != l2.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges must be of the same shape".to_string(),
));
}
let values_left = self.values_from_range(l1, r1)?;
let values_right = self.values_from_range(l2, r2)?;
(rows1, cols1, values_left, values_right)
}
(
CalcResult::Array(left),
CalcResult::Range {
left: l2,
right: r2,
},
) => {
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
let rows1 = left.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Array and range must be of the same shape".to_string(),
));
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
));
}
Ok(v) => v,
};
let values_right = self.values_from_range(l2, r2)?;
(rows2, cols2, values_left, values_right)
}
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Array(right),
) => {
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = right.len() as i32;
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Range and array must be of the same shape".to_string(),
));
}
let values_left = self.values_from_range(l1, r1)?;
let values_right = match self.values_from_array(right) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
));
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
(CalcResult::Array(left), CalcResult::Array(right)) => {
let rows1 = left.len() as i32;
let rows2 = right.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Arrays must be of the same shape".to_string(),
));
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
));
}
Ok(v) => v,
};
let values_right = match self.values_from_array(right) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
));
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
_ => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Both arguments must be ranges or arrays".to_string(),
));
}
};
Ok(result)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,733 +0,0 @@
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::{CalcResult, Range},
expressions::parser::Node,
expressions::token::Error,
model::Model,
};
use super::util::build_criteria;
impl Model {
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut sum = 0.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
count += 1.0;
sum += value;
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
} else {
sum += if b { 1.0 } else { 0.0 };
count += 1.0;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
count += 1.0;
sum += value;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
} else if let Ok(t) = s.parse::<f64>() {
sum += t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
}
}
_ => {
// Ignore everything else
}
};
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(sum / count)
}
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut sum = 0.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::String(_) => count += 1.0,
CalcResult::Number(value) => {
count += 1.0;
sum += value;
}
CalcResult::Boolean(b) => {
if b {
sum += 1.0;
}
count += 1.0;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
}
}
}
CalcResult::Number(value) => {
count += 1.0;
sum += value;
}
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
count += 1.0;
} else if let Ok(t) = s.parse::<f64>() {
sum += t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
}
}
CalcResult::Boolean(b) => {
count += 1.0;
if b {
sum += 1.0;
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(sum / count)
}
pub(crate) fn fn_count(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut result = 0.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(_) => {
result += 1.0;
}
CalcResult::Boolean(_) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
result += 1.0;
}
}
CalcResult::String(s) => {
if !matches!(arg, Node::ReferenceKind { .. }) && s.parse::<f64>().is_ok() {
result += 1.0;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
if let CalcResult::Number(_) = self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
result += 1.0;
}
}
}
}
_ => {
// Ignore everything else
}
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_counta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut result = 0.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
_ => {
result += 1.0;
}
}
}
}
}
_ => {
result += 1.0;
}
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_countblank(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// COUNTBLANK requires only one argument
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let mut result = 0.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
CalcResult::String(s) => {
if s.is_empty() {
result += 1.0
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
CalcResult::String(s) => {
if s.is_empty() {
result += 1.0
}
}
_ => {}
}
}
}
}
_ => {}
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 2 {
let arguments = vec![args[0].clone(), args[1].clone()];
self.fn_countifs(&arguments, cell)
} else {
CalcResult::new_args_number_error(cell)
}
}
/// AVERAGEIF(criteria_range, criteria, [average_range])
/// if average_rage is missing then criteria_range will be used
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 2 {
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
self.fn_averageifs(&arguments, cell)
} else if args.len() == 3 {
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
self.fn_averageifs(&arguments, cell)
} else {
CalcResult::new_args_number_error(cell)
}
}
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len();
if args_count < 2 || args_count % 2 == 1 {
return CalcResult::new_args_number_error(cell);
}
let case_count = args_count / 2;
// NB: this is a beautiful example of the borrow checker
// The order of these two definitions cannot be swapped.
let mut criteria = Vec::new();
let mut fn_criteria = Vec::new();
let ranges = &mut Vec::new();
for case_index in 0..case_count {
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
criteria.push(criterion);
// NB: We cannot do:
// fn_criteria.push(build_criteria(&criterion));
// because criterion doesn't live long enough
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
if result.is_error() {
return result;
}
if let CalcResult::Range { left, right } = result {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
// TODO test ranges are of the same size as sum_range
ranges.push(Range { left, right });
} else {
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
}
}
for criterion in criteria.iter() {
fn_criteria.push(build_criteria(criterion));
}
let mut total = 0.0;
let first_range = &ranges[0];
let left_row = first_range.left.row;
let left_column = first_range.left.column;
let right_row = first_range.right.row;
let right_column = first_range.right.column;
let dimension = match self.workbook.worksheet(first_range.left.sheet) {
Ok(s) => s.dimension(),
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
)
}
};
let max_row = dimension.max_row;
let max_column = dimension.max_column;
let open_row = left_row == 1 && right_row == LAST_ROW;
let open_column = left_column == 1 && right_column == LAST_COLUMN;
for row in left_row..right_row + 1 {
if open_row && row > max_row {
// If the row is larger than the max row in the sheet then all cells are empty.
// We compute it only once
let mut is_true = true;
for fn_criterion in fn_criteria.iter() {
if !fn_criterion(&CalcResult::EmptyCell) {
is_true = false;
break;
}
}
if is_true {
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
}
break;
}
for column in left_column..right_column + 1 {
if open_column && column > max_column {
// If the column is larger than the max column in the sheet then all cells are empty.
// We compute it only once
let mut is_true = true;
for fn_criterion in fn_criteria.iter() {
if !fn_criterion(&CalcResult::EmptyCell) {
is_true = false;
break;
}
}
if is_true {
total += (LAST_COLUMN - max_column) as f64;
}
break;
}
let mut is_true = true;
for case_index in 0..case_count {
// We check if value in range n meets criterion n
let range = &ranges[case_index];
let fn_criterion = &fn_criteria[case_index];
let value = self.evaluate_cell(CellReferenceIndex {
sheet: range.left.sheet,
row: range.left.row + row - first_range.left.row,
column: range.left.column + column - first_range.left.column,
});
if !fn_criterion(&value) {
is_true = false;
break;
}
}
if is_true {
total += 1.0;
}
}
}
CalcResult::Number(total)
}
pub(crate) fn apply_ifs<F>(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
mut apply: F,
) -> Result<(), CalcResult>
where
F: FnMut(f64),
{
let args_count = args.len();
if args_count < 3 || args_count % 2 == 0 {
return Err(CalcResult::new_args_number_error(cell));
}
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
if arg_0.is_error() {
return Err(arg_0);
}
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
if left.sheet != right.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
Range { left, right }
} else {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expected a range".to_string(),
));
};
let case_count = (args_count - 1) / 2;
// NB: this is a beautiful example of the borrow checker
// The order of these two definitions cannot be swapped.
let mut criteria = Vec::new();
let mut fn_criteria = Vec::new();
let ranges = &mut Vec::new();
for case_index in 1..=case_count {
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
// NB: criterion might be an error. That's ok
criteria.push(criterion);
// NB: We cannot do:
// fn_criteria.push(build_criteria(&criterion));
// because criterion doesn't live long enough
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
if result.is_error() {
return Err(result);
}
if let CalcResult::Range { left, right } = result {
if left.sheet != right.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
// TODO test ranges are of the same size as sum_range
ranges.push(Range { left, right });
} else {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expected a range".to_string(),
));
}
}
for criterion in criteria.iter() {
fn_criteria.push(build_criteria(criterion));
}
let left_row = sum_range.left.row;
let left_column = sum_range.left.column;
let mut right_row = sum_range.right.row;
let mut right_column = sum_range.right.column;
if left_row == 1 && right_row == LAST_ROW {
right_row = match self.workbook.worksheet(sum_range.left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return Err(CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
));
}
};
}
if left_column == 1 && right_column == LAST_COLUMN {
right_column = match self.workbook.worksheet(sum_range.left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return Err(CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
));
}
};
}
for row in left_row..right_row + 1 {
for column in left_column..right_column + 1 {
let mut is_true = true;
for case_index in 0..case_count {
// We check if value in range n meets criterion n
let range = &ranges[case_index];
let fn_criterion = &fn_criteria[case_index];
let value = self.evaluate_cell(CellReferenceIndex {
sheet: range.left.sheet,
row: range.left.row + row - sum_range.left.row,
column: range.left.column + column - sum_range.left.column,
});
if !fn_criterion(&value) {
is_true = false;
break;
}
}
if is_true {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: sum_range.left.sheet,
row,
column,
});
match v {
CalcResult::Number(n) => apply(n),
CalcResult::Error { .. } => return Err(v),
_ => {}
}
}
}
}
Ok(())
}
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut total = 0.0;
let mut count = 0.0;
let average = |value: f64| {
total += value;
count += 1.0;
};
if let Err(e) = self.apply_ifs(args, cell, average) {
return e;
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "division by 0".to_string(),
};
}
CalcResult::Number(total / count)
}
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut min = f64::INFINITY;
let apply_min = |value: f64| min = value.min(min);
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
return e;
}
if min.is_infinite() {
min = 0.0;
}
CalcResult::Number(min)
}
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut max = -f64::INFINITY;
let apply_max = |value: f64| max = value.max(max);
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
return e;
}
if max.is_infinite() {
max = 0.0;
}
CalcResult::Number(max)
}
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut product = 1.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
} else {
product *= if b { 1.0 } else { 0.0 };
count += 1.0;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
} else if let Ok(t) = s.parse::<f64>() {
product *= t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
}
}
_ => {
// Ignore everything else
}
};
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(product.powf(1.0 / count))
}
}

View File

@@ -0,0 +1,213 @@
use statrs::distribution::{Beta, Continuous, ContinuousCDF};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// BETA.DIST(x, alpha, beta, cumulative, [A], [B])
pub(crate) fn fn_beta_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if !(4..=6).contains(&arg_count) {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let alpha = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let beta_param = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
// cumulative argument: interpret like Excel
let cumulative = match self.evaluate_node_in_context(&args[3], cell) {
CalcResult::Boolean(b) => b,
CalcResult::Number(n) => n != 0.0,
CalcResult::String(s) => {
let up = s.to_ascii_uppercase();
if up == "TRUE" {
true
} else if up == "FALSE" {
false
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "cumulative must be TRUE/FALSE or numeric".to_string(),
};
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid cumulative argument".to_string(),
}
}
};
// Optional A, B
let a = if arg_count >= 5 {
match self.get_number_no_bools(&args[4], cell) {
Ok(f) => f,
Err(e) => return e,
}
} else {
0.0
};
let b = if arg_count >= 6 {
match self.get_number_no_bools(&args[5], cell) {
Ok(f) => f,
Err(e) => return e,
}
} else {
1.0
};
// Excel: alpha <= 0 or beta <= 0 → #NUM!
if alpha <= 0.0 || beta_param <= 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"alpha and beta must be > 0 in BETA.DIST".to_string(),
);
}
// Excel: if x < A, x > B, or A = B → #NUM!
if b == a || x < a || x > b {
return CalcResult::new_error(
Error::NUM,
cell,
"x must be between A and B and A < B in BETA.DIST".to_string(),
);
}
// Transform to standard Beta(0,1)
let width = b - a;
let t = (x - a) / width;
let dist = match Beta::new(alpha, beta_param) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Beta distribution".to_string(),
)
}
};
let result = if cumulative {
dist.cdf(t)
} else {
// general-interval beta pdf: f_X(x) = f_T(t) / (B - A), t=(x-A)/(B-A)
dist.pdf(t) / width
};
if result.is_nan() || result.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for BETA.DIST".to_string(),
);
}
CalcResult::Number(result)
}
pub(crate) fn fn_beta_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if !(3..=5).contains(&arg_count) {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let alpha = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let beta_param = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let a = if arg_count >= 4 {
match self.get_number_no_bools(&args[3], cell) {
Ok(f) => f,
Err(e) => return e,
}
} else {
0.0
};
let b = if arg_count >= 5 {
match self.get_number_no_bools(&args[4], cell) {
Ok(f) => f,
Err(e) => return e,
}
} else {
1.0
};
if alpha <= 0.0 || beta_param <= 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"alpha and beta must be > 0 in BETA.INV".to_string(),
);
}
// probability <= 0 or probability > 1 → #NUM!
if p <= 0.0 || p > 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"probability must be in (0,1] in BETA.INV".to_string(),
);
}
if b <= a {
return CalcResult::new_error(
Error::NUM,
cell,
"A must be < B in BETA.INV".to_string(),
);
}
let dist = match Beta::new(alpha, beta_param) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Beta distribution".to_string(),
)
}
};
let t = dist.inverse_cdf(p);
if t.is_nan() || t.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for BETA.INV".to_string(),
);
}
// Map back from [0,1] to [A,B]
let x = a + t * (b - a);
CalcResult::Number(x)
}
}

View File

@@ -0,0 +1,311 @@
use statrs::distribution::{Binomial, Discrete, DiscreteCDF};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_binom_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
// number_s
let number_s = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// trials
let trials = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// probability_s
let p = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
// cumulative (logical)
let cumulative = match self.get_boolean(&args[3], cell) {
Ok(b) => b,
Err(e) => return e,
};
// Domain checks
if trials < 0.0
|| number_s < 0.0
|| number_s > trials
|| p.is_nan()
|| !(0.0..=1.0).contains(&p)
{
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for BINOM.DIST".to_string(),
);
}
// Limit to u64
if trials > u64::MAX as f64 {
return CalcResult::new_error(
Error::NUM,
cell,
"Number of trials too large".to_string(),
);
}
let n = trials as u64;
let k = number_s as u64;
let dist = match Binomial::new(p, n) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for binomial distribution".to_string(),
)
}
};
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
if prob.is_nan() || prob.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for BINOM.DIST".to_string(),
);
}
CalcResult::Number(prob)
}
pub(crate) fn fn_binom_dist_range(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() < 3 || args.len() > 4 {
return CalcResult::new_args_number_error(cell);
}
// trials
let trials = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// probability_s
let p = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
// number_s (lower)
let number_s = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// number_s2 (upper, optional)
let number_s2 = if args.len() == 4 {
match self.get_number_no_bools(&args[3], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
}
} else {
number_s
};
if trials < 0.0
|| number_s < 0.0
|| number_s2 < 0.0
|| number_s > number_s2
|| number_s2 > trials
|| p.is_nan()
|| !(0.0..=1.0).contains(&p)
{
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for BINOM.DIST.RANGE".to_string(),
);
}
if trials > u64::MAX as f64 {
return CalcResult::new_error(
Error::NUM,
cell,
"Number of trials too large".to_string(),
);
}
let n = trials as u64;
let lower = number_s as u64;
let upper = number_s2 as u64;
let dist = match Binomial::new(p, n) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for binomial distribution".to_string(),
)
}
};
let prob = if lower == 0 {
dist.cdf(upper)
} else {
let cdf_upper = dist.cdf(upper);
let cdf_below_lower = dist.cdf(lower - 1);
cdf_upper - cdf_below_lower
};
if prob.is_nan() || prob.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for BINOM.DIST.RANGE".to_string(),
);
}
CalcResult::Number(prob)
}
pub(crate) fn fn_binom_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
// trials
let trials = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// probability_s
let p = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
// alpha
let alpha = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
if trials < 0.0
|| trials > u64::MAX as f64
|| p.is_nan()
|| !(0.0..=1.0).contains(&p)
|| alpha.is_nan()
|| !(0.0..=1.0).contains(&alpha)
{
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for BINOM.INV".to_string(),
);
}
let n = trials as u64;
let dist = match Binomial::new(p, n) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for binomial distribution".to_string(),
)
}
};
// DiscreteCDF::inverse_cdf returns u64 for binomial
let k = statrs::distribution::DiscreteCDF::inverse_cdf(&dist, alpha);
CalcResult::Number(k as f64)
}
pub(crate) fn fn_negbinom_dist(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
use statrs::distribution::{Discrete, DiscreteCDF, NegativeBinomial};
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
let number_f = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let number_s = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let probability_s = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[3], cell) {
Ok(b) => b,
Err(e) => return e,
};
if number_f < 0.0 || number_s < 1.0 || !(0.0..=1.0).contains(&probability_s) {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
};
}
// Guard against absurdly large failures that won't fit in u64
if number_f > (u64::MAX as f64) {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
};
}
let dist = match NegativeBinomial::new(number_s, probability_s) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
}
}
};
let f_u = number_f as u64;
let result = if cumulative {
dist.cdf(f_u)
} else {
dist.pmf(f_u)
};
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
};
}
CalcResult::Number(result)
}
}

View File

@@ -0,0 +1,397 @@
use statrs::distribution::{ChiSquared, Continuous, ContinuousCDF};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// CHISQ.DIST(x, deg_freedom, cumulative)
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[2], cell) {
Ok(b) => b,
Err(e) => return e,
};
if x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"x must be >= 0 in CHISQ.DIST".to_string(),
);
}
if df < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.DIST".to_string(),
);
}
let dist = match ChiSquared::new(df) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Chi-squared distribution".to_string(),
)
}
};
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
if result.is_nan() || result.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for CHISQ.DIST".to_string(),
);
}
CalcResult::Number(result)
}
// CHISQ.DIST.RT(x, deg_freedom)
pub(crate) fn fn_chisq_dist_rt(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df_raw = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df = df_raw.trunc();
if x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"x must be >= 0 in CHISQ.DIST.RT".to_string(),
);
}
if df < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.DIST.RT".to_string(),
);
}
let dist = match ChiSquared::new(df) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Chi-squared distribution".to_string(),
)
}
};
// Right-tail probability: P(X > x).
// Use sf(x) directly for better numerical properties than 1 - cdf(x).
let result = dist.sf(x);
if result.is_nan() || result.is_infinite() || result < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for CHISQ.DIST.RT".to_string(),
);
}
CalcResult::Number(result)
}
// CHISQ.INV(probability, deg_freedom)
pub(crate) fn fn_chisq_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// if probability < 0 or > 1 → #NUM!
if !(0.0..=1.0).contains(&p) {
return CalcResult::new_error(
Error::NUM,
cell,
"probability must be in [0,1] in CHISQ.INV".to_string(),
);
}
if df < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.INV".to_string(),
);
}
let dist = match ChiSquared::new(df) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Chi-squared distribution".to_string(),
)
}
};
let x = dist.inverse_cdf(p);
if x.is_nan() || x.is_infinite() || x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for CHISQ.INV".to_string(),
);
}
CalcResult::Number(x)
}
// CHISQ.INV.RT(probability, deg_freedom)
pub(crate) fn fn_chisq_inv_rt(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df_raw = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df = df_raw.trunc();
// if probability < 0 or > 1 → #NUM!
if !(0.0..=1.0).contains(&p) {
return CalcResult::new_error(
Error::NUM,
cell,
"probability must be in [0,1] in CHISQ.INV.RT".to_string(),
);
}
if df < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.INV.RT".to_string(),
);
}
let dist = match ChiSquared::new(df) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Chi-squared distribution".to_string(),
)
}
};
// Right-tail inverse: p = P(X > x) = SF(x) = 1 - CDF(x)
// So x = inverse_cdf(1 - p).
let x = dist.inverse_cdf(1.0 - p);
if x.is_nan() || x.is_infinite() || x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for CHISQ.INV.RT".to_string(),
);
}
CalcResult::Number(x)
}
pub(crate) fn values_from_range(
&mut self,
left: CellReferenceIndex,
right: CellReferenceIndex,
) -> Result<Vec<Option<f64>>, CalcResult> {
let mut values = Vec::new();
for row_offset in 0..=(right.row - left.row) {
for col_offset in 0..=(right.column - left.column) {
let cell_ref = CellReferenceIndex {
sheet: left.sheet,
row: left.row + row_offset,
column: left.column + col_offset,
};
let cell_value = self.evaluate_cell(cell_ref);
match cell_value {
CalcResult::Number(v) => {
values.push(Some(v));
}
error @ CalcResult::Error { .. } => return Err(error),
_ => {
values.push(None);
}
}
}
}
Ok(values)
}
pub(crate) fn values_from_array(
&mut self,
array: Vec<Vec<ArrayNode>>,
) -> Result<Vec<Option<f64>>, Error> {
let mut values = Vec::new();
for row in array {
for item in row {
match item {
ArrayNode::Number(f) => {
values.push(Some(f));
}
ArrayNode::Error(error) => {
return Err(error);
}
_ => {
values.push(None);
}
}
}
}
Ok(values)
}
// CHISQ.TEST(actual_range, expected_range)
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell)
{
Ok(v) => v,
Err(r) => return r,
};
let mut values = Vec::with_capacity(values_left.len());
// Now we have:
// - values: flattened (observed, expected)
// - width, height: shape
for i in 0..values_left.len() {
match (values_left[i], values_right[i]) {
(Some(v1), Some(v2)) => {
values.push((v1, v2));
}
_ => {
values.push((1.0, 1.0));
}
}
}
if width == 0 || height == 0 || values.len() < 2 {
return CalcResult::new_error(
Error::NUM,
cell,
"CHISQ.TEST requires at least two data points".to_string(),
);
}
let mut chi2 = 0.0;
for (obs, exp) in &values {
if *obs < 0.0 || *exp < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Negative value in CHISQ.TEST data".to_string(),
);
}
if *exp == 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"Zero expected value in CHISQ.TEST".to_string(),
);
}
let diff = obs - exp;
chi2 += (diff * diff) / exp;
}
if chi2 < 0.0 && chi2 > -1e-12 {
chi2 = 0.0;
}
let total = width * height;
if total <= 1 {
return CalcResult::new_error(
Error::NUM,
cell,
"CHISQ.TEST degrees of freedom is zero".to_string(),
);
}
let df = if width > 1 && height > 1 {
(width - 1) * (height - 1)
} else {
total - 1
};
let dist = match ChiSquared::new(df as f64) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid degrees of freedom in CHISQ.TEST".to_string(),
);
}
};
let mut p = 1.0 - dist.cdf(chi2);
// clamp tiny fp noise
if p < 0.0 && p > -1e-15 {
p = 0.0;
}
if p > 1.0 && p < 1.0 + 1e-15 {
p = 1.0;
}
CalcResult::Number(p)
}
}

View File

@@ -0,0 +1,227 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_y2 = 0.0;
let mut sum_xy = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
if let (Some(x), Some(y)) = (x_opt, y_opt) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_y2 += y * y;
sum_xy += x * y;
}
}
// Need at least 2 valid pairs
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"CORREL requires at least two numeric data points in each range".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let denom_x = n * sum_x2 - sum_x * sum_x;
let denom_y = n * sum_y2 - sum_y * sum_y;
let denom = (denom_x * denom_y).sqrt();
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in CORREL".to_string(),
);
}
let r = num / denom;
CalcResult::Number(r)
}
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_xy = 0.0;
let len = values_y.len().min(values_x.len());
for i in 0..len {
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_xy += x * y;
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"SLOPE requires at least two numeric data points".to_string(),
);
}
let denom = n * sum_x2 - sum_x * sum_x;
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in SLOPE".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let slope = num / denom;
CalcResult::Number(slope)
}
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_xy = 0.0;
let len = values_y.len().min(values_x.len());
for i in 0..len {
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_xy += x * y;
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"INTERCEPT requires at least two numeric data points".to_string(),
);
}
let denom = n * sum_x2 - sum_x * sum_x;
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in INTERCEPT".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let slope = num / denom;
let intercept = (sum_y - slope * sum_x) / n;
CalcResult::Number(intercept)
}
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_xy = 0.0;
// We need the actual pairs again later for residuals
let mut pairs: Vec<(f64, f64)> = Vec::new();
let len = values_y.len().min(values_x.len());
for i in 0..len {
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_xy += x * y;
pairs.push((x, y));
}
}
// Need at least 3 points for STEYX (n - 2 in denominator)
if n < 3.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STEYX requires at least three numeric data points".to_string(),
);
}
let denom = n * sum_x2 - sum_x * sum_x;
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in STEYX".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let slope = num / denom;
let intercept = (sum_y - slope * sum_x) / n;
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
let mut sse = 0.0;
for (x, y) in pairs {
let y_hat = intercept + slope * x;
let diff = y - y_hat;
sse += diff * diff;
}
let dof = n - 2.0;
if dof <= 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STEYX has non-positive degrees of freedom".to_string(),
);
}
let sey = (sse / dof).sqrt();
if !sey.is_finite() {
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
}
CalcResult::Number(sey)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,264 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_covariance_p(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"First argument must be a range or array".to_string(),
);
}
};
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"Second argument must be a range or array".to_string(),
);
}
};
// Same number of cells
if values1_opts.len() != values2_opts.len() {
return CalcResult::new_error(
Error::NA,
cell,
"COVARIANCE.P requires arrays of the same size".to_string(),
);
}
// Count numeric data points in each array (ignoring text/booleans/empty)
let count1 = values1_opts.iter().filter(|v| v.is_some()).count();
let count2 = values2_opts.iter().filter(|v| v.is_some()).count();
if count1 == 0 || count2 == 0 {
return CalcResult::new_error(
Error::DIV,
cell,
"COVARIANCE.P requires at least one numeric value in each array".to_string(),
);
}
if count1 != count2 {
return CalcResult::new_error(
Error::NA,
cell,
"COVARIANCE.P arrays must have the same number of numeric data points".to_string(),
);
}
// Build paired numeric vectors, position by position
let mut xs: Vec<f64> = Vec::with_capacity(count1);
let mut ys: Vec<f64> = Vec::with_capacity(count2);
for (v1_opt, v2_opt) in values1_opts.into_iter().zip(values2_opts.into_iter()) {
if let (Some(x), Some(y)) = (v1_opt, v2_opt) {
xs.push(x);
ys.push(y);
}
}
let n = xs.len();
if n == 0 {
// Should be impossible given the checks above, but guard anyway
return CalcResult::new_error(
Error::DIV,
cell,
"COVARIANCE.P has no paired numeric data points".to_string(),
);
}
let n_f = n as f64;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
for i in 0..n {
sum_x += xs[i];
sum_y += ys[i];
}
let mean_x = sum_x / n_f;
let mean_y = sum_y / n_f;
let mut sum_prod = 0.0;
for i in 0..n {
let dx = xs[i] - mean_x;
let dy = ys[i] - mean_y;
sum_prod += dx * dy;
}
let cov = sum_prod / n_f;
CalcResult::Number(cov)
}
pub(crate) fn fn_covariance_s(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"First argument must be a range or array".to_string(),
);
}
};
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"Second argument must be a range or array".to_string(),
);
}
};
// Same number of cells
if values1_opts.len() != values2_opts.len() {
return CalcResult::new_error(
Error::NA,
cell,
"COVARIANCE.S requires arrays of the same size".to_string(),
);
}
// Count numeric data points in each array (ignoring text/booleans/empty)
let count1 = values1_opts.iter().filter(|v| v.is_some()).count();
let count2 = values2_opts.iter().filter(|v| v.is_some()).count();
if count1 == 0 || count2 == 0 {
return CalcResult::new_error(
Error::DIV,
cell,
"COVARIANCE.S requires numeric values in each array".to_string(),
);
}
if count1 != count2 {
return CalcResult::new_error(
Error::NA,
cell,
"COVARIANCE.S arrays must have the same number of numeric data points".to_string(),
);
}
// Build paired numeric vectors
let mut xs: Vec<f64> = Vec::with_capacity(count1);
let mut ys: Vec<f64> = Vec::with_capacity(count2);
for (v1_opt, v2_opt) in values1_opts.into_iter().zip(values2_opts.into_iter()) {
if let (Some(x), Some(y)) = (v1_opt, v2_opt) {
xs.push(x);
ys.push(y);
}
}
let n = xs.len();
if n < 2 {
return CalcResult::new_error(
Error::DIV,
cell,
"COVARIANCE.S requires at least two paired data points".to_string(),
);
}
let n_f = n as f64;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
for i in 0..n {
sum_x += xs[i];
sum_y += ys[i];
}
let mean_x = sum_x / n_f;
let mean_y = sum_y / n_f;
let mut sum_prod = 0.0;
for i in 0..n {
let dx = xs[i] - mean_x;
let dy = ys[i] - mean_y;
sum_prod += dx * dy;
}
let cov = sum_prod / (n_f - 1.0);
CalcResult::Number(cov)
}
}

View File

@@ -0,0 +1,135 @@
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// DEVSQ(number1, [number2], ...)
pub(crate) fn fn_devsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
// tiny helper so we don't repeat ourselves
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// We ignore booleans and strings
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// We ignore booleans and strings
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// We ignore booleans and strings
}
};
}
if count == 0 {
// No numeric data at all
return CalcResult::new_error(
Error::DIV,
cell,
"DEVSQ with no numeric data".to_string(),
);
}
let n = count as f64;
let mut result = sumsq - (sum * sum) / n;
// Numerical noise can make result slightly negative when it should be 0
if result < 0.0 && result > -1e-12 {
result = 0.0;
}
CalcResult::Number(result)
}
}

View File

@@ -0,0 +1,54 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_expon_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// EXPON.DIST(x, lambda, cumulative)
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let lambda = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[2], cell) {
Ok(b) => b,
Err(e) => return e,
};
if x < 0.0 || lambda <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for EXPON.DIST".to_string(),
};
}
let result = if cumulative {
// CDF
1.0 - (-lambda * x).exp()
} else {
// PDF
lambda * (-lambda * x).exp()
};
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for EXPON.DIST".to_string(),
};
}
CalcResult::Number(result)
}
}

View File

@@ -0,0 +1,418 @@
use statrs::distribution::{Continuous, ContinuousCDF, FisherSnedecor};
use crate::expressions::types::CellReferenceIndex;
use crate::functions::statistical::t_dist::sample_var;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// FISHER(x) = 0.5 * ln((1 + x) / (1 - x))
pub(crate) fn fn_fisher(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
if x <= -1.0 || x >= 1.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "x must be between -1 and 1 (exclusive) in FISHER".to_string(),
};
}
let ratio = (1.0 + x) / (1.0 - x);
let result = 0.5 * ratio.ln();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for FISHER".to_string(),
};
}
CalcResult::Number(result)
}
// FISHERINV(y) = (e^(2y) - 1) / (e^(2y) + 1) = tanh(y)
pub(crate) fn fn_fisher_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let y = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
// Use tanh directly to avoid overflow from exp(2y)
let result = y.tanh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for FISHERINV".to_string(),
};
}
CalcResult::Number(result)
}
// F.DIST(x, deg_freedom1, deg_freedom2, cumulative)
pub(crate) fn fn_f_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df1 = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let df2 = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[3], cell) {
Ok(b) => b,
Err(e) => return e,
};
// Excel domain checks
if x < 0.0 {
return CalcResult::new_error(Error::NUM, cell, "x must be >= 0 in F.DIST".to_string());
}
if df1 < 1.0 || df2 < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in F.DIST".to_string(),
);
}
let dist = match FisherSnedecor::new(df1, df2) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for F distribution".to_string(),
)
}
};
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
if result.is_nan() || result.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for F.DIST".to_string(),
);
}
CalcResult::Number(result)
}
pub(crate) fn fn_f_dist_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// F.DIST.RT(x, deg_freedom1, deg_freedom2)
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df1 = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let df2 = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
if x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"x must be >= 0 in F.DIST.RT".to_string(),
);
}
if df1 < 1.0 || df2 < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in F.DIST.RT".to_string(),
);
}
let dist = match FisherSnedecor::new(df1, df2) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for F distribution".to_string(),
)
}
};
// Right-tail probability: P(F > x) = 1 - CDF(x)
let result = 1.0 - dist.cdf(x);
if result.is_nan() || result.is_infinite() || result < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for F.DIST.RT".to_string(),
);
}
CalcResult::Number(result)
}
// F.INV(probability, deg_freedom1, deg_freedom2)
pub(crate) fn fn_f_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df1 = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let df2 = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// probability < 0 or > 1 → #NUM!
if !(0.0..=1.0).contains(&p) {
return CalcResult::new_error(
Error::NUM,
cell,
"probability must be in [0,1] in F.INV".to_string(),
);
}
if df1 < 1.0 || df2 < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in F.INV".to_string(),
);
}
let dist = match FisherSnedecor::new(df1, df2) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for F distribution".to_string(),
)
}
};
let x = dist.inverse_cdf(p);
if x.is_nan() || x.is_infinite() || x < 0.0 {
return CalcResult::new_error(Error::NUM, cell, "Invalid result for F.INV".to_string());
}
CalcResult::Number(x)
}
// F.INV.RT(probability, deg_freedom1, deg_freedom2)
pub(crate) fn fn_f_inv_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let df1 = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let df2 = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
if p <= 0.0 || p > 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"probability must be in (0,1] in F.INV.RT".to_string(),
);
}
if df1 < 1.0 || df2 < 1.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in F.INV.RT".to_string(),
);
}
let dist = match FisherSnedecor::new(df1, df2) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for F distribution".to_string(),
)
}
};
// p is right-tail: p = P(F > x) = 1 - CDF(x)
let x = dist.inverse_cdf(1.0 - p);
if x.is_nan() || x.is_infinite() || x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for F.INV.RT".to_string(),
);
}
CalcResult::Number(x)
}
// F.TEST(array1, array2)
pub(crate) fn fn_f_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"First argument must be a range or array".to_string(),
);
}
};
// Get second sample as Vec<Option<f64>>
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"Second argument must be a range or array".to_string(),
);
}
};
let values1: Vec<f64> = values1_opts.into_iter().flatten().collect();
let values2: Vec<f64> = values2_opts.into_iter().flatten().collect();
let n1 = values1.len();
let n2 = values2.len();
// If fewer than 2 numeric values in either sample -> #DIV/0!
if n1 < 2 || n2 < 2 {
return CalcResult::new_error(
Error::DIV,
cell,
"F.TEST requires at least two numeric values in each sample".to_string(),
);
}
let v1 = sample_var(&values1);
let v2 = sample_var(&values2);
if v1 <= 0.0 || v2 <= 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"Variance of one sample is zero in F.TEST".to_string(),
);
}
// F ratio: larger variance / smaller variance
let mut f = v1 / v2;
let mut df1 = (n1 - 1) as f64;
let mut df2 = (n2 - 1) as f64;
if f < 1.0 {
f = 1.0 / f;
std::mem::swap(&mut df1, &mut df2);
}
let dist = match FisherSnedecor::new(df1, df2) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for F distribution in F.TEST".to_string(),
);
}
};
// One-tailed right-tail probability
let tail = 1.0 - dist.cdf(f);
// F.TEST is two-tailed: p = 2 * tail (with F >= 1)
let mut p = 2.0 * tail;
// Clamp tiny FP noise
if p < 0.0 && p > -1e-15 {
p = 0.0;
}
if p > 1.0 && p < 1.0 + 1e-15 {
p = 1.0;
}
CalcResult::Number(p)
}
}

View File

@@ -0,0 +1,194 @@
use statrs::distribution::{Continuous, ContinuousCDF, Gamma};
use statrs::function::gamma::{gamma, ln_gamma};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_gamma(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
if x < 0.0 && x.floor() == x {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for Gamma function".to_string(),
};
}
let result = gamma(x);
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for Gamma function".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_gamma_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// GAMMA.DIST(x, alpha, beta, cumulative)
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let alpha = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let beta_scale = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[3], cell) {
Ok(b) => b,
Err(e) => return e,
};
if x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"x must be >= 0 in GAMMA.DIST".to_string(),
);
}
if alpha <= 0.0 || beta_scale <= 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"alpha and beta must be > 0 in GAMMA.DIST".to_string(),
);
}
let rate = 1.0 / beta_scale;
let dist = match Gamma::new(alpha, rate) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Gamma distribution".to_string(),
)
}
};
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
if result.is_nan() || result.is_infinite() {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for GAMMA.DIST".to_string(),
);
}
CalcResult::Number(result)
}
pub(crate) fn fn_gamma_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// GAMMA.INV(probability, alpha, beta)
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let alpha = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let beta_scale = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
if !(0.0..=1.0).contains(&p) {
return CalcResult::new_error(
Error::NUM,
cell,
"probability must be in [0,1] in GAMMA.INV".to_string(),
);
}
if alpha <= 0.0 || beta_scale <= 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"alpha and beta must be > 0 in GAMMA.INV".to_string(),
);
}
let rate = 1.0 / beta_scale;
let dist = match Gamma::new(alpha, rate) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for Gamma distribution".to_string(),
)
}
};
let x = dist.inverse_cdf(p);
if x.is_nan() || x.is_infinite() || x < 0.0 {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid result for GAMMA.INV".to_string(),
);
}
CalcResult::Number(x)
}
pub(crate) fn fn_gamma_ln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
if x < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for Gamma function".to_string(),
};
}
let result = ln_gamma(x);
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for Gamma Ln function".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_gamma_ln_precise(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
self.fn_gamma_ln(args, cell)
}
}

View File

@@ -0,0 +1,39 @@
use statrs::distribution::{ContinuousCDF, Normal};
use crate::expressions::token::Error;
use crate::expressions::types::CellReferenceIndex;
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
impl<'a> Model<'a> {
pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let z = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let dist = match Normal::new(0.0, 1.0) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Failed to construct standard normal distribution".to_string(),
}
}
};
let result = dist.cdf(z) - 0.5;
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for GAUSS".to_string(),
};
}
CalcResult::Number(result)
}
}

View File

@@ -0,0 +1,87 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut product = 1.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
} else {
product *= if b { 1.0 } else { 0.0 };
count += 1.0;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
} else if let Ok(t) = s.parse::<f64>() {
product *= t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
}
}
_ => {
// Ignore everything else
}
};
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(product.powf(1.0 / count))
}
}

View File

@@ -0,0 +1,108 @@
use statrs::distribution::{Discrete, DiscreteCDF, Hypergeometric};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// =HYPGEOM.DIST(sample_s, number_sample, population_s, number_pop, cumulative)
pub(crate) fn fn_hyp_geom_dist(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 5 {
return CalcResult::new_args_number_error(cell);
}
// sample_s (number of successes in the sample)
let sample_s = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// number_sample (sample size)
let number_sample = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// population_s (number of successes in the population)
let population_s = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// number_pop (population size)
let number_pop = match self.get_number_no_bools(&args[3], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[4], cell) {
Ok(b) => b,
Err(e) => return e,
};
if sample_s < 0.0 || sample_s > f64::min(number_sample, population_s) {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
};
}
if sample_s < f64::max(0.0, number_sample + population_s - number_pop) {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
};
}
if number_sample <= 0.0 || number_sample > number_pop {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
};
}
if population_s <= 0.0 || population_s > number_pop {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
};
}
let n_pop = number_pop as u64;
let k_pop = population_s as u64;
let n_sample = number_sample as u64;
let k = sample_s as u64;
let dist = match Hypergeometric::new(n_pop, k_pop, n_sample) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for hypergeometric distribution".to_string(),
)
}
};
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
if !prob.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for HYPGEOM.DIST".to_string(),
};
}
CalcResult::Number(prob)
}
}

View File

@@ -0,0 +1,337 @@
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::types::CellReferenceIndex;
use crate::functions::util::build_criteria;
use crate::{
calc_result::{CalcResult, Range},
expressions::parser::Node,
expressions::token::Error,
model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 2 {
let arguments = vec![args[0].clone(), args[1].clone()];
self.fn_countifs(&arguments, cell)
} else {
CalcResult::new_args_number_error(cell)
}
}
/// AVERAGEIF(criteria_range, criteria, [average_range])
/// if average_rage is missing then criteria_range will be used
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 2 {
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
self.fn_averageifs(&arguments, cell)
} else if args.len() == 3 {
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
self.fn_averageifs(&arguments, cell)
} else {
CalcResult::new_args_number_error(cell)
}
}
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len();
if args_count < 2 || !args_count.is_multiple_of(2) {
return CalcResult::new_args_number_error(cell);
}
let case_count = args_count / 2;
// NB: this is a beautiful example of the borrow checker
// The order of these two definitions cannot be swapped.
let mut criteria = Vec::new();
let mut fn_criteria = Vec::new();
let ranges = &mut Vec::new();
for case_index in 0..case_count {
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
criteria.push(criterion);
// NB: We cannot do:
// fn_criteria.push(build_criteria(&criterion));
// because criterion doesn't live long enough
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
if result.is_error() {
return result;
}
if let CalcResult::Range { left, right } = result {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
// TODO test ranges are of the same size as sum_range
ranges.push(Range { left, right });
} else {
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
}
}
for criterion in criteria.iter() {
fn_criteria.push(build_criteria(criterion));
}
let mut total = 0.0;
let first_range = &ranges[0];
let left_row = first_range.left.row;
let left_column = first_range.left.column;
let right_row = first_range.right.row;
let right_column = first_range.right.column;
let dimension = match self.workbook.worksheet(first_range.left.sheet) {
Ok(s) => s.dimension(),
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
)
}
};
let max_row = dimension.max_row;
let max_column = dimension.max_column;
let open_row = left_row == 1 && right_row == LAST_ROW;
let open_column = left_column == 1 && right_column == LAST_COLUMN;
for row in left_row..right_row + 1 {
if open_row && row > max_row {
// If the row is larger than the max row in the sheet then all cells are empty.
// We compute it only once
let mut is_true = true;
for fn_criterion in fn_criteria.iter() {
if !fn_criterion(&CalcResult::EmptyCell) {
is_true = false;
break;
}
}
if is_true {
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
}
break;
}
for column in left_column..right_column + 1 {
if open_column && column > max_column {
// If the column is larger than the max column in the sheet then all cells are empty.
// We compute it only once
let mut is_true = true;
for fn_criterion in fn_criteria.iter() {
if !fn_criterion(&CalcResult::EmptyCell) {
is_true = false;
break;
}
}
if is_true {
total += (LAST_COLUMN - max_column) as f64;
}
break;
}
let mut is_true = true;
for case_index in 0..case_count {
// We check if value in range n meets criterion n
let range = &ranges[case_index];
let fn_criterion = &fn_criteria[case_index];
let value = self.evaluate_cell(CellReferenceIndex {
sheet: range.left.sheet,
row: range.left.row + row - first_range.left.row,
column: range.left.column + column - first_range.left.column,
});
if !fn_criterion(&value) {
is_true = false;
break;
}
}
if is_true {
total += 1.0;
}
}
}
CalcResult::Number(total)
}
pub(crate) fn apply_ifs<F>(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
mut apply: F,
) -> Result<(), CalcResult>
where
F: FnMut(f64),
{
let args_count = args.len();
if args_count < 3 || args_count.is_multiple_of(2) {
return Err(CalcResult::new_args_number_error(cell));
}
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
if arg_0.is_error() {
return Err(arg_0);
}
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
if left.sheet != right.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
Range { left, right }
} else {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expected a range".to_string(),
));
};
let case_count = (args_count - 1) / 2;
// NB: this is a beautiful example of the borrow checker
// The order of these two definitions cannot be swapped.
let mut criteria = Vec::new();
let mut fn_criteria = Vec::new();
let ranges = &mut Vec::new();
for case_index in 1..=case_count {
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
// NB: criterion might be an error. That's ok
criteria.push(criterion);
// NB: We cannot do:
// fn_criteria.push(build_criteria(&criterion));
// because criterion doesn't live long enough
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
if result.is_error() {
return Err(result);
}
if let CalcResult::Range { left, right } = result {
if left.sheet != right.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
// TODO test ranges are of the same size as sum_range
ranges.push(Range { left, right });
} else {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expected a range".to_string(),
));
}
}
for criterion in criteria.iter() {
fn_criteria.push(build_criteria(criterion));
}
let left_row = sum_range.left.row;
let left_column = sum_range.left.column;
let mut right_row = sum_range.right.row;
let mut right_column = sum_range.right.column;
if left_row == 1 && right_row == LAST_ROW {
right_row = match self.workbook.worksheet(sum_range.left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return Err(CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
));
}
};
}
if left_column == 1 && right_column == LAST_COLUMN {
right_column = match self.workbook.worksheet(sum_range.left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return Err(CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
));
}
};
}
for row in left_row..right_row + 1 {
for column in left_column..right_column + 1 {
let mut is_true = true;
for case_index in 0..case_count {
// We check if value in range n meets criterion n
let range = &ranges[case_index];
let fn_criterion = &fn_criteria[case_index];
let value = self.evaluate_cell(CellReferenceIndex {
sheet: range.left.sheet,
row: range.left.row + row - sum_range.left.row,
column: range.left.column + column - sum_range.left.column,
});
if !fn_criterion(&value) {
is_true = false;
break;
}
}
if is_true {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: sum_range.left.sheet,
row,
column,
});
match v {
CalcResult::Number(n) => apply(n),
CalcResult::Error { .. } => return Err(v),
_ => {}
}
}
}
}
Ok(())
}
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut total = 0.0;
let mut count = 0.0;
let average = |value: f64| {
total += value;
count += 1.0;
};
if let Err(e) = self.apply_ifs(args, cell, average) {
return e;
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "division by 0".to_string(),
};
}
CalcResult::Number(total / count)
}
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut min = f64::INFINITY;
let apply_min = |value: f64| min = value.min(min);
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
return e;
}
if min.is_infinite() {
min = 0.0;
}
CalcResult::Number(min)
}
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut max = -f64::INFINITY;
let apply_max = |value: f64| max = value.max(max);
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
return e;
}
if max.is_infinite() {
max = 0.0;
}
CalcResult::Number(max)
}
}

View File

@@ -0,0 +1,124 @@
use statrs::distribution::{Continuous, ContinuousCDF, LogNormal};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_log_norm_dist(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let mean = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let std_dev = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[3], cell) {
Ok(b) => b,
Err(e) => return e,
};
// Excel domain checks
if x <= 0.0 || std_dev <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for LOGNORM.DIST".to_string(),
};
}
let dist = match LogNormal::new(mean, std_dev) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for LOGNORM.DIST".to_string(),
}
}
};
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for LOGNORM.DIST".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_log_norm_inv(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
use statrs::distribution::{ContinuousCDF, LogNormal};
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let mean = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let std_dev = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
// Excel domain checks
if p <= 0.0 || p >= 1.0 || std_dev <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for LOGNORM.INV".to_string(),
};
}
let dist = match LogNormal::new(mean, std_dev) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for LOGNORM.INV".to_string(),
}
}
};
let result = dist.inverse_cdf(p);
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameter for LOGNORM.INV".to_string(),
};
}
CalcResult::Number(result)
}
}

View File

@@ -0,0 +1,26 @@
mod beta;
mod binom;
mod chisq;
mod correl;
mod count_and_average;
mod covariance;
mod devsq;
mod exponential;
mod fisher;
mod gamma;
mod gauss;
mod geomean;
mod hypegeom;
mod if_ifs;
mod log_normal;
mod normal;
mod pearson;
mod phi;
mod poisson;
mod rank_eq_avg;
mod standard_dev;
mod standardize;
mod t_dist;
mod variance;
mod weibull;
mod z_test;

View File

@@ -0,0 +1,325 @@
use statrs::distribution::{Continuous, ContinuousCDF, Normal, StudentsT};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// NORM.DIST(x, mean, standard_dev, cumulative)
pub(crate) fn fn_norm_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let mean = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let std_dev = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[3], cell) {
Ok(b) => b,
Err(e) => return e,
};
// Excel: standard_dev must be > 0
if std_dev <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "standard_dev must be > 0 in NORM.DIST".to_string(),
};
}
let dist = match Normal::new(mean, std_dev) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for NORM.DIST".to_string(),
}
}
};
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for NORM.DIST".to_string(),
};
}
CalcResult::Number(result)
}
// NORM.INV(probability, mean, standard_dev)
pub(crate) fn fn_norm_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let mean = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let std_dev = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
if p <= 0.0 || p >= 1.0 || std_dev <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for NORM.INV".to_string(),
};
}
let dist = match Normal::new(mean, std_dev) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for NORM.INV".to_string(),
}
}
};
let x = dist.inverse_cdf(p);
if !x.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for NORM.INV".to_string(),
};
}
CalcResult::Number(x)
}
// NORM.S.DIST(z, cumulative)
pub(crate) fn fn_norm_s_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let z = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[1], cell) {
Ok(b) => b,
Err(e) => return e,
};
let dist = match Normal::new(0.0, 1.0) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Failed to construct standard normal distribution".to_string(),
}
}
};
let result = if cumulative { dist.cdf(z) } else { dist.pdf(z) };
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for NORM.S.DIST".to_string(),
};
}
CalcResult::Number(result)
}
// NORM.S.INV(probability)
pub(crate) fn fn_norm_s_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let p = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
if p <= 0.0 || p >= 1.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "probability must be in (0,1) in NORM.S.INV".to_string(),
};
}
let dist = match Normal::new(0.0, 1.0) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Failed to construct standard normal distribution".to_string(),
}
}
};
let z = dist.inverse_cdf(p);
if !z.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for NORM.S.INV".to_string(),
};
}
CalcResult::Number(z)
}
pub(crate) fn fn_confidence_norm(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let alpha = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let std_dev = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let size = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.floor(),
Err(e) => return e,
};
if alpha <= 0.0 || alpha >= 1.0 || std_dev <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for CONFIDENCE.NORM".to_string(),
};
}
if size < 1.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Sample size must be at least 1".to_string(),
};
}
let normal = match Normal::new(0.0, 1.0) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Failed to construct normal distribution".to_string(),
)
}
};
let quantile = normal.inverse_cdf(1.0 - alpha / 2.0);
if !quantile.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid quantile for CONFIDENCE.NORM".to_string(),
};
}
let margin = quantile * std_dev / size.sqrt();
CalcResult::Number(margin)
}
pub(crate) fn fn_confidence_t(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let alpha = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let std_dev = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let size = match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// Domain checks
if alpha <= 0.0 || alpha >= 1.0 || std_dev <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for CONFIDENCE.T".to_string(),
};
}
// Need at least 2 observations so df = n - 1 > 0
if size < 2.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Sample size must be at least 2".to_string(),
};
}
let df = size - 1.0;
let t_dist = match StudentsT::new(0.0, 1.0, df) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Failed to construct Student's t distribution".to_string(),
)
}
};
// Two-sided CI => use 1 - alpha/2
let t_crit = t_dist.inverse_cdf(1.0 - alpha / 2.0);
if !t_crit.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid quantile for CONFIDENCE.T".to_string(),
};
}
let margin = t_crit * std_dev / size.sqrt();
CalcResult::Number(margin)
}
}

View File

@@ -0,0 +1,113 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// PEARSON(array1, array2)
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
Ok(result) => result,
Err(e) => return e,
};
// Flatten into (x, y) pairs, skipping non-numeric entries (None)
let mut n: f64 = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_y2 = 0.0;
let mut sum_xy = 0.0;
let len = values_left.len().min(values_right.len());
for i in 0..len {
match (values_left[i], values_right[i]) {
(Some(x), Some(y)) => {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_y2 += y * y;
sum_xy += x * y;
}
_ => {
// Ignore pairs where at least one side is non-numeric
}
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"PEARSON requires at least two numeric pairs".to_string(),
);
}
// Pearson correlation:
// r = [ n*Σxy - (Σx)(Σy) ] / sqrt( [n*Σx² - (Σx)²] [n*Σy² - (Σy)²] )
let num = n * sum_xy - sum_x * sum_y;
let denom_x = n * sum_x2 - sum_x * sum_x;
let denom_y = n * sum_y2 - sum_y * sum_y;
if denom_x.abs() < 1e-15 || denom_y.abs() < 1e-15 {
// Zero variance in at least one series
return CalcResult::new_error(
Error::DIV,
cell,
"PEARSON cannot be computed when one series has zero variance".to_string(),
);
}
let denom = (denom_x * denom_y).sqrt();
CalcResult::Number(num / denom)
}
// RSQ(array1, array2) = CORREL(array1, array2)^2
pub(crate) fn fn_rsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values1, values2) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0_f64;
let mut sum_x = 0.0_f64;
let mut sum_y = 0.0_f64;
let mut sum_x2 = 0.0_f64;
let mut sum_y2 = 0.0_f64;
let mut sum_xy = 0.0_f64;
let len = values1.len().min(values2.len());
for i in 0..len {
if let (Some(x), Some(y)) = (values1[i], values2[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_y2 += y * y;
sum_xy += x * y;
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"RSQ requires at least two numeric data points in each range".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let denom_x = n * sum_x2 - sum_x * sum_x;
let denom_y = n * sum_y2 - sum_y * sum_y;
let denom = (denom_x * denom_y).sqrt();
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(Error::DIV, cell, "Division by zero in RSQ".to_string());
}
let r = num / denom;
CalcResult::Number(r * r)
}
}

View File

@@ -0,0 +1,21 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
impl<'a> Model<'a> {
// PHI(x) = standard normal PDF at x
pub(crate) fn fn_phi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
// Standard normal PDF: (1 / sqrt(2π)) * exp(-x^2 / 2)
let result = (-(x * x) / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
CalcResult::Number(result)
}
}

View File

@@ -0,0 +1,94 @@
use statrs::distribution::{Discrete, DiscreteCDF, Poisson};
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// =POISSON.DIST(x, mean, cumulative)
pub(crate) fn fn_poisson_dist(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
// x
let x = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
// mean (lambda)
let lambda = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let cumulative = match self.get_boolean(&args[2], cell) {
Ok(b) => b,
Err(e) => return e,
};
if x < 0.0 || lambda < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for POISSON.DIST".to_string(),
};
}
// Guard against insane k for u64
if x < 0.0 || x > (u64::MAX as f64) {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for POISSON.DIST".to_string(),
};
}
let k = x as u64;
// Special-case lambda = 0: degenerate distribution at 0
if lambda == 0.0 {
let result = if cumulative {
// For x >= 0, P(X <= x) = 1
1.0
} else {
// P(X = 0) = 1, P(X = k>0) = 0
if k == 0 {
1.0
} else {
0.0
}
};
return CalcResult::Number(result);
}
let dist = match Poisson::new(lambda) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid parameters for POISSON.DIST".to_string(),
}
}
};
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
if !prob.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for POISSON.DIST".to_string(),
};
}
CalcResult::Number(prob)
}
}

View File

@@ -0,0 +1,202 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
// Helper to collect numeric values from the 2nd argument of RANK.*
fn collect_rank_values(
&mut self,
arg: &Node,
cell: CellReferenceIndex,
) -> Result<Vec<f64>, CalcResult> {
let values = match self.evaluate_node_in_context(arg, cell) {
CalcResult::Array(array) => match self.values_from_array(array) {
Ok(v) => v,
Err(e) => {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Unsupported array argument: {}", e),
})
}
},
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
CalcResult::Boolean(value) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
vec![Some(if value { 1.0 } else { 0.0 })]
} else {
return Err(CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
});
}
}
_ => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Unsupported argument type".to_string(),
})
}
};
let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
Ok(numeric_values)
}
// RANK.EQ(number, ref, [order])
pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
// number
let number = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
// ref
let mut values = match self.collect_rank_values(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for RANK.EQ".to_string(),
};
}
// order: default 0 (descending)
let order = if args.len() == 2 {
0.0
} else {
match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
}
};
values.retain(|v| !v.is_nan());
// "better" = greater (descending) or smaller (ascending)
let mut better = 0;
let mut equal = 0;
if order == 0.0 {
// descending
for v in &values {
if *v > number {
better += 1;
} else if *v == number {
equal += 1;
}
}
} else {
// ascending
for v in &values {
if *v < number {
better += 1;
} else if *v == number {
equal += 1;
}
}
}
if equal == 0 {
return CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Number not found in reference for RANK.EQ".to_string(),
};
}
let rank = (better as f64) + 1.0;
CalcResult::Number(rank)
}
// RANK.AVG(number, ref, [order])
pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
// number
let number = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
// ref
let mut values = match self.collect_rank_values(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for RANK.AVG".to_string(),
};
}
// order: default 0 (descending)
let order = if args.len() == 2 {
0.0
} else {
match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
}
};
values.retain(|v| !v.is_nan());
// > or < depending on order
let mut better = 0;
let mut equal = 0;
if order == 0.0 {
// descending
for v in &values {
if *v > number {
better += 1;
} else if *v == number {
equal += 1;
}
}
} else {
// ascending
for v in &values {
if *v < number {
better += 1;
} else if *v == number {
equal += 1;
}
}
}
if equal == 0 {
return CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Number not found in reference for RANK.AVG".to_string(),
};
}
// For ties, average of the ranks. If the equal values occupy positions
// (better+1) ..= (better+equal), the average is:
// better + (equal + 1) / 2
let better_f = better as f64;
let equal_f = equal as f64;
let rank = better_f + (equal_f + 1.0) / 2.0;
CalcResult::Number(rank)
}
}

View File

@@ -0,0 +1,519 @@
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl<'a> Model<'a> {
pub(crate) fn fn_stdev_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
if count == 0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEV.P with no numeric data".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / n;
// clamp tiny negatives from FP noise
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
pub(crate) fn fn_stdev_s(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
if count <= 1 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEV.S requires at least two numeric values".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
pub(crate) fn fn_stdeva(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::String(_) => {
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
}
CalcResult::Boolean(value) => {
let val = if value { 1.0 } else { 0.0 };
accumulate(&mut sum, &mut sumsq, &mut count, val);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric for now
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
if count <= 1 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEVA requires at least two numeric values".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
pub(crate) fn fn_stdevpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::String(_) => {
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
}
CalcResult::Boolean(value) => {
let val = if value { 1.0 } else { 0.0 };
accumulate(&mut sum, &mut sumsq, &mut count, val);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric for now
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
if count == 0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEVPA with no numeric data".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / n;
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
}

Some files were not shown because too many files have changed in this diff Show More