Compare commits

...

640 Commits

Author SHA1 Message Date
Daniel
8ce34045d6 fix: mobile adjustments 2025-11-25 22:04:04 +01:00
Daniel
bcd1f66c9c fix: force default values 2025-11-25 21:51:00 +01:00
Daniel
5a891483b6 update: add dropdowns to content 2025-11-24 17:20:28 +01:00
Daniel
0eafc9b599 update: open dialog from footer 2025-11-23 17:47:10 +01:00
Daniel
e48e539bd6 update: add a dialog for settings 2025-11-23 17:02:40 +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
Nicolás Hatcher
c554d929f4 FIX: Renaming a sheet updates formula in defined name 2025-04-16 09:56:17 +02:00
Nicolás Hatcher
acdf85dbc3 FIX: thick line should be 3px in the picker 2025-04-15 15:56:25 +02:00
Nicolás Hatcher
6ac8f7e948 FIX: Use local typescript instead of global 2025-04-15 15:52:07 +02:00
Nicolás Hatcher
9a4e798313 FIX: Make clippy happy. Manual fixes 2025-04-15 15:44:56 +02:00
Nicolás Hatcher
7756ef7f48 FIX: Make clippy happy. automatic update 2025-04-15 15:44:56 +02:00
Daniel
793534b190 fix: cleanup code 2025-03-26 18:13:38 +01:00
Nicolás Hatcher
efc925a046 FIX: Adds border to default swatch 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
155f891f8b FIX: Split the color picker in two 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
5683d02b93 FIX: Clean up (anchorOrigin/transformOrigin) logic 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
475c3e9d49 FIX: Add defaultColor to color picker
NB: Dani will have to pass the color from the theme
2025-03-23 16:02:14 +01:00
Nicolás Hatcher
9e65ea3024 FIX: Add 'title' to ColorPicker 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
03ad87cd8f FIX: Removes unused file 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
e2a466c500 FIX: Simplify ColorPicker code a bit
* Removes "renderMenuItem"
* Color is no longer optional
* Removes 'index' unused variable
2025-03-23 16:02:14 +01:00
Daniel
08b3d71e9e update: new color picker 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
e5ec75495a UPDATE: Introducing Arrays
# This PR introduces:

## Parsing arrays:

{1,2,3} and {1;2;3}

Note that array elements can be numbers, booleans and errors (#VALUE!)

## Evaluating arrays in the SUM function

=SUM({1,2,3}) works!

## Evaluating arithmetic operation with arrays

=SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works

This is done with just one function (handle_arithmetic) for most operations

## Some mathematical functions implement arrays

=SUM(SIN({1,2,3})) works

This is done with macros. See fn_single_number
So that implementing new functions that supports array are easy


# Not done in this PR

## Most functions are not supporting arrays

When that happens we either through #N/IMPL! (not implemented error)
or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard

## The final result in a cell cannot be an array

The formula ={1,2,3} in a cell will result in #N/IMPL!

## Exporting arrays to Excel might not work correctly

Excel uses the cm (cell metadata) for formulas that contain dynamic arrays.
Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3}))
is considered a dynamic formula

## There are not a lot of tests in this delivery

The bulk of the tests will be added once we start going function by function# This PR introduces:

## Parsing arrays:

{1,2,3} and {1;2;3}

Note that array elements can be numbers, booleans and errors (#VALUE!)

## Evaluating arrays in the SUM function

=SUM({1,2,3}) works!

## Evaluating arithmetic operation with arrays

=SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works

This is done with just one function (handle_arithmetic) for most operations

## Some mathematical functions implement arrays

=SUM(SIN({1,2,3})) works

This is done with macros. See fn_single_number
So that implementing new functions that supports array are easy


# Not done in this PR

## Most functions are not supporting arrays

When that happens we either through #N/IMPL! (not implemented error)
or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard

## The final result in a cell cannot be an array

The formula ={1,2,3} in a cell will result in #N/IMPL!

## Exporting arrays to Excel might not work correctly

Excel uses the cm (cell metadata) for formulas that contain dynamic arrays.
Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3}))
is considered a dynamic formula

## There are not a lot of tests in this delivery

The bulk of the tests will be added once we start going function by function

## The array parsing does not respect the locale

Locales that use ',' as a decimal separator need to use something different for arrays

## The might introduce a small performance penalty

We haven't been benchmarking, and having closures for every arithmetic operation and every function
evaluation will introduce a performance hit. Fixing that in he future is not so hard writing tailored
code for the operation
2025-03-17 20:04:47 +01:00
Nicolás Hatcher
e07fdd2091 FIX[app.ironcalc.com]: Clean up code for the title 2025-03-12 18:24:15 +01:00
Nicolás Hatcher
cde6f0e49f FIX: Update missing packages 2025-03-06 22:32:23 +01:00
Nicolás Hatcher
da017b6113 UPDATE: Implement the implicit Intersection Operator
The II operator takes a range and returns a single cell that is in the same column or the same row
as the present cell.

This is needed for backwards compatibility with old Excel models and as a first step towards dynamic arrays.

In the past Excel would evaluate `=A1:A10` in cell `C3` as `A3`, but today in results in an array containing all
values in the range. To be compatible with old workbooks Excel inserts the II operator
on those cases.

So this PR performs an static analysis on all formulas inserting on import automatically the II operator
where necessary. This we call the _automatic implicit operator_. When exporting to Excel the operator is striped away.
You can also manually use the II. For instance `=SUM(@A1:A10)` in cell `C3`.
This was not possible before and such a formula would break backwards compatibility with Excel. To Excel that "non automatic"
form of the II is exported as `_xlfn.SINGLE()`.

Th static analysis has to be done for all arithmetic operations and all functions.
This is a bit of a daunting task and it is not done fully in this PR. We also need to implement arrays and dynamic arrays.
My believe is that once the core operations have been implemented we can go formula by formula writing proper tests and documentation.

After this PR formulas like `=A1:A10` for instance will return `#N/IMPL!` instead of performing the implicit intersection
2025-03-03 21:59:42 +01:00
Nicolás Hatcher
90763048bc FIX: Wrap text properly
This fixes a bug where if you were wrapping text,
the first word was repeated.

We really need tests!
2025-03-03 21:06:05 +01:00
Nicolás Hatcher
532386b448 FIX: The glog was wrong :S 2025-03-01 15:34:59 +01:00
Nicolás Hatcher
84b2bdd7c9 FIX: We are back on tial and error, regrettably 2025-03-01 12:25:23 +01:00
Nicolás Hatcher
25bb1ab8dc FIX: Fix CI script to deal with conflicting names 2025-02-28 13:06:10 +01:00
Nicolás Hatcher
5c13f241c6 FIX: Fixes for the CI builds 2025-02-28 12:00:54 +01:00
Nicolás Hatcher
26b20eea43 UPDATE: Bump versions to 0.5 2025-02-28 01:00:50 +01:00
Nicolás Hatcher
b62256963a UPDATE: Adds wrapping! 2025-02-28 00:29:44 +01:00
Nicolás Hatcher
4f627b4363 FIX: More sensible decrease/increase font-size 2025-02-28 00:29:44 +01:00
Daniel
a9a8c4f615 UPDATE: Add a dialog when 'Share' buttons is clickled 2025-02-27 18:13:20 +01:00
Nicolás Hatcher
f9c9467e6c FIX: Correct height/width of cells with different font sizes 2025-02-26 23:44:08 +01:00
Nicolás Hatcher
409b77c210 FIX: Default size should be 13 pixels 2025-02-26 20:29:36 +01:00
Nicolás Hatcher
eecf6f3c3b UPDATE: Download to PNG the visible part of the selected area
This downloads only the visible part of the selected area.
To download the full selected area we would need to work a bit more
2025-02-26 19:27:56 +01:00
Nicolás Hatcher
ce7318840d UPDATE: We can now change the font size! 2025-02-26 19:11:38 +01:00
Nicolás Hatcher
7bc563ef29 FIX: Make biome happy 2025-02-26 18:03:15 +01:00
Nicolás Hatcher
8ed88e1445 FIX: Update versions in widget 2025-02-26 18:03:15 +01:00
Nicolás Hatcher
a1353e0817 FIX: More consistent naming conventions in widget 2025-02-26 18:03:15 +01:00
Nicolás Hatcher
c0fa55c5f7 FIX: Add "Apply" button to color picker 2025-02-24 19:00:05 +01:00
Nicolás Hatcher
1ff0c38aa5 FIX: Control+B,I,U work again
This clearly shows we need beter testing in the frontend
2025-02-23 11:27:59 +01:00
Nicolás Hatcher
e5a2db4d8c FIX: Adds localhost in the development server Caddyfile
Useful for MacOs
2025-02-22 18:57:13 +01:00
Nicolás Hatcher
fc7335707a UPDATE: Double click resizes columns/rows automatically 2025-02-19 18:26:49 +01:00
Nicolás Hatcher
4095b7db6e UPDATE[API rename]: set_column_with => set_columns_with
Similarly set_row_height => set_rows_height
2025-02-19 18:26:49 +01:00
Nicolás Hatcher
dd9ca4224d UPDATE: Select multiple columns/rows
Also fixed a bug where a second column would not pick up salyes correctly
2025-02-19 18:26:49 +01:00
Nicolás Hatcher
5aa7617e97 FIX: It it possible to have DF scoped to the first sheet 2025-02-19 13:40:32 +01:00
Nicolás Hatcher
a10d1f4615 FIX: Fix a bug were a new column style would introduce an invalid format 2025-02-15 15:25:36 +00:00
Nicolás Hatcher
1e8441a674 FIX: Displace column styles properly when inserting columns 2025-02-15 15:25:36 +00:00
Nicolás Hatcher
b2c5027f56 FIX: Shows borders correctly in case of frozen rows.
Also draws the lines at the correct position
2025-02-15 12:46:11 +00:00
Nicolás Hatcher
91984dc920 FIX: Forces calculation after insert/delete columns/rows 2025-02-15 10:16:05 +00:00
Nicolás Hatcher
74be62823d FIX: Minor fixes and refactor 2025-02-15 09:46:39 +00:00
Nicolás Hatcher
edd00096b6 FIX: Minor fixes in column/row styles
Most notably deleting the formatting does not change width/height
2025-02-15 09:46:39 +00:00
Nicolás Hatcher
d764752f16 FIX: diverse issues with set/delete column and row styles 2025-02-15 09:46:39 +00:00
Nicolás Hatcher
ce6c908dc7 FIX: Delete row/column formatting
Also clear formatting clears all formatting including row/column
2025-02-15 09:46:39 +00:00
Nicolás Hatcher
6ee450709a FIX: Numerous fixes
This also fix old issues:

* Top border is only the top border of the selected area
  (and not he top border of each cell in the area)
* Same for bottom, left and right borders

Factored out all the border related issues to a new file
2025-02-15 09:46:39 +00:00
Nicolás Hatcher
23ab5dfef2 UPDATE: Add rows/column style APIs 2025-02-15 09:46:39 +00:00
Daniel
7e54cb6aa2 style: added a divider 2025-02-11 15:34:13 +01:00
Daniel
857ebabf16 style: menu padding 2025-02-11 15:34:13 +01:00
Daniel
f0af3048b7 style: replace hex colors with theme colors 2025-02-11 15:34:13 +01:00
Nicolás Hatcher
99125f1fea UPDATE: Adds cell context menu 2025-02-07 19:15:55 +01:00
Nicolás Hatcher
f96481feb8 UPDATE: Update documentation 2025-02-06 20:48:38 +01:00
Nicolás Hatcher
dc8bb6da21 FIX: Undo/redo delete/add page
Now we can undo adding or deleting worksheets
2025-02-05 21:52:34 +01:00
Nicolás Hatcher
d866e283e9 UPDATE: Update to React 19.0.0
Diverse fixes
2025-02-04 22:04:26 +01:00
Nicolás Hatcher
8a54f45d75 UPDATE: Add clear formatting
Fixes #267
2025-02-04 19:02:05 +01:00
Nicolás Hatcher
42d557d485 UPDATE: Python docs 2025-02-01 17:18:02 +01:00
Nicolás Hatcher
293f7c6de6 FIX: Use @ironcalc/workbook from file and bimp dependencies 2025-01-31 18:44:25 +01:00
Daniel
38325b0bb9 UPDATE: Add an empty state message to the Name Manager 2025-01-31 00:07:50 +01:00
Daniel
282ed16f0d fix: Remove commented code 2025-01-30 23:41:27 +01:00
Daniel
fd744d28a3 fix: use theme colors on divider 2025-01-30 23:41:27 +01:00
Daniel
9a717daf04 update: makes AddressContainer's width flexible to allow more space on mobile 2025-01-30 23:41:27 +01:00
Daniel
84bf859c2c fix: remove min-width in formatPicker to avoid input overlay on mobile devices 2025-01-30 23:41:27 +01:00
Daniel
e57101f279 update: remove ironcalc link on mobile, padding adjustments 2025-01-30 23:41:27 +01:00
Nicolás Hatcher
264fcac63c UPDATE: Exposes the model in UserModel
Fixes #262
2025-01-30 07:50:14 +01:00
Nicolás Hatcher
7777f8e5d6 UPDATE: Adds fixes to python upload script 2025-01-29 23:50:33 +01:00
Daniel
6aa73171c7 FIX: Wrong Discord invite link 2025-01-29 18:44:33 +01:00
Daniel
8051913b2d update: remove the Name Manager from the 'Unsupported Features' page 2025-01-29 18:44:13 +01:00
Daniel
cfa38548d5 update: Name Manager documentation page 2025-01-29 18:44:13 +01:00
Nicolás Hatcher
9787721c5a UPDATE: Add python workflow to publish on testpypi 2025-01-29 18:41:13 +01:00
Nicolás Hatcher
610b899f66 UPDATE: Add raw API to nodejs bindings 2025-01-28 20:26:39 +01:00
Nicolás Hatcher Andrés
24fb87721f Update npm.yml 2025-01-28 08:09:17 +01:00
Nicolás Hatcher
d3bc8b135c FIX: Try a boolean argument for the CI instead 2025-01-28 08:09:17 +01:00
Daniel González-Albo
0f6d311de2 Merge pull request #252 from ironcalc/fix/dani-delete-sheet
UPDATE: Deleting a sheet prompts a confirmation dialog
2025-01-28 01:11:46 +01:00
Daniel
0c15ae194d Fix: simplified code by having only 1 dynamic message 2025-01-26 19:00:00 +01:00
Nicolás Hatcher
5d429b1660 UPDATE: Add node bindings 2025-01-26 11:01:42 +01:00
Daniel
20c4a596bf UPDATE: Localization 2025-01-21 01:21:03 +01:00
Daniel
f07a69260f UPDATE: Deleting a sheet prompts a confirmation dialog 2025-01-21 01:20:42 +01:00
Nicolás Hatcher
ec4e7b1ca3 FIX: Fix ERF.PRECISE and ERFC.PRECISE links in documentation 2025-01-20 17:22:33 +01:00
Steve Fanning
81d25b6ec9 Add ERFC, ERF.PRECISE and ERFC.PRECISE functions 2025-01-20 17:22:33 +01:00
Nicolás Hatcher
3a001d96b8 FIX: Can cut and paste in Custom Format Dialog
Fixes #240

It is a pity we have to do this. There probably is a better way
2025-01-19 22:37:42 +01:00
Nicolás Hatcher
69ca1f178c FIX: Uses python3 if python is not available 2025-01-19 22:27:00 +01:00
Nicolás Hatcher
feb22cced3 UPDATE: Adds storybook
This is a bit on an emergency PR to unblock Dani
2025-01-19 17:38:33 +01:00
Nicolás Hatcher
c88304ba96 FIX: Fixes from "the big split" 2025-01-19 14:43:30 +01:00
Nicolás Hatcher
fa0b386abc FIX: Added file from "the big split" 2025-01-19 14:31:58 +01:00
Nicolás Hatcher
ff0d05e3a0 FIX: Make clippy happy (upgrade dependencies) 2025-01-19 14:31:58 +01:00
Nicolás Hatcher
1b7389fd23 FIX: Make Clippy happy
Automatic fixes
2025-01-19 14:31:58 +01:00
Steve Fanning
263bab2cf9 "Change markdown file names for ERF.PRECISE and ERFC.PRECISE" 2025-01-18 10:56:51 +01:00
Steve Fanning
2e0722f9b5 Fix typos in ERF function description 2025-01-18 10:56:51 +01:00
Steve Fanning
fd72bca141 Add description for ERF function. 2025-01-18 10:56:51 +01:00
Nicolás Hatcher
8215cfc9fb UPDATE: split the webapp in a widget and the app itself
This splits the webapp in:

* IronCalc (the widget to be published on npmjs)
* The frontend for our "service"
* Adds "dummy code" for the backend using sqlite
2025-01-17 19:27:55 +01:00
Daniel
378f8351d3 Small fix in maxWidth 2025-01-15 18:03:44 +01:00
Daniel
c770b3229c Mobile view 2025-01-15 18:03:44 +01:00
Daniel
b0f57b20c2 Height adjusment, theme/hex replacement 2025-01-15 18:03:44 +01:00
Daniel
912fcae0a3 Footer and header are now consistent with other dialogs 2025-01-15 18:03:44 +01:00
Daniel
cc72d031b5 Action buttons consistency and tooltips 2025-01-15 18:03:44 +01:00
Daniel
e8c18ebc5e More row height and alignment 2025-01-15 18:03:44 +01:00
Daniel
576c358e2a More alignment fixes 2025-01-15 18:03:44 +01:00
Daniel
eb03efba3e FIX: Layout and spacing adjustments 2025-01-15 18:03:44 +01:00
Daniel
43e9cb3523 FIX: Small adjustments after feedback 2025-01-11 19:05:48 +01:00
Daniel
b95c0642da FIX: UploadFileDialog is now a proper dialog for easy focusing 2025-01-11 19:05:48 +01:00
Daniel
185a70224c FIX: Makes sure tabIndex works properly on formatPicker dialog 2025-01-11 19:05:48 +01:00
Daniel
a4c3233253 FIX: Makes sure tabIndex works properly on SheetRenameDialog 2025-01-11 19:05:48 +01:00
Steve Fanning
ac9dc22972 Minor updates to DAY function page and example spreadsheet 2025-01-02 18:29:12 +01:00
Steve Fanning
68b5364bbd Fix typo to reflect minimum serial number of 1. 2025-01-02 18:19:43 +01:00
Steve Fanning
02e726b445 Add YEAR function description 2025-01-02 18:19:43 +01:00
Steve Fanning
f63d307fec Fix typo to reflect minimum serial number of 1. 2025-01-02 18:18:12 +01:00
Steve Fanning
99e1110261 Add MONTH function description 2025-01-02 18:18:12 +01:00
Nicolás Hatcher
91eb66993d FIX: Add link to name manager page 2025-01-01 18:25:39 +01:00
Nicolás Hatcher
87e8b7a20b FIX: Make biome happy 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
97b27006cf FIX: Correct size of dropdown fonts 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
b7f7e73824 FIX: Ranges selected should be absolute.
Sheet1!$D$1 rather than Sheet1!D1

There reason is that if we extend a formula that has those will behave in
surprising ways.
2025-01-01 18:08:52 +01:00
Nicolás Hatcher
ea194ee730 FIX: Move model out of te nameManager 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
cbb413f100 FIX: Isolate model specific stuff 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
a4cf93c49a FIX: Add link to help 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
70366ea60c FIX: Remove redundant variable 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
9aa1b4574e FIX: Remove Model dependency 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
82b2d28663 FIX: Set a visual cue when a name is wrong 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
d2ba34166b FIX: Refactor model out of the dialogs 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
99d42cb1e2 FIX: onDelete and onCancel are mandatory properties
In general optional arguments are a bad idea, because you loose the type
and all it's benefits.

Did you forget an argument or you didn't need it?
2025-01-01 18:08:52 +01:00
Nicolás Hatcher
ddc785e7a6 FIX: Minor formatting issues 2025-01-01 18:08:52 +01:00
Nicolás Hatcher
8ab1382e75 FIX: Renames folder NameManager => NameManagerDialog 2025-01-01 18:08:52 +01:00
francisco aloi
ec5714e3ec more style changes, changelog and locale 2025-01-01 11:54:33 +01:00
francisco aloi
4660f0e456 refactored NamedRanges logic and styles 2025-01-01 11:54:33 +01:00
francisco aloi
f2757e7d76 updated util, added test 2025-01-01 11:54:33 +01:00
francisco aloi
5ca15033f7 changes to name manager after review 2025-01-01 11:54:33 +01:00
francisco aloi
75e04696b5 refactored NameManager logic for new design 2025-01-01 11:54:33 +01:00
francisco aloi
832ca02e16 NamedRanges changes 2025-01-01 11:54:33 +01:00
Nicolás Hatcher
cbda30f951 FIX: Use 1 as the first serial number corresponding to 1899-12-31 2025-01-01 11:53:05 +01:00
Steve Fanning
564d4bac7a Add DAY function description 2025-01-01 11:44:44 +01:00
Shalom Yiblet
0dd26e8fee fix: fix xml escape in worksheet xml 2024-12-31 09:07:39 +01:00
Nicolás Hatcher
f6fbb4b303 FIX: The default border color should be black 2024-12-30 14:15:48 +01:00
Nicolás Hatcher
c6adf8449b FIX: Dates are only valid up to the last day of 9999 2024-12-30 14:15:09 +01:00
Shalom Yiblet
d04691b790 refactor(dates): adjust date handling logic for improved accuracy
Updated the logic for handling months and days to improve date calculations. Also modified the constants for Excel date ranges to align with supported dates.
2024-12-30 13:12:33 +01:00
Shalom Yiblet
7c32088480 feat: update date 2024-12-30 13:12:33 +01:00
Nicolás Hatcher
6326c44941 FIX: TRUE and FALSE can also be functions
Previously the engine was internally transforming TRUE() to TRUE

Note that the friendly giant implements this only for
compatibility reasons
2024-12-29 19:17:54 +01:00
Nicolás Hatcher
d3af994866 FIX[docs]: Fixes broken links 2024-12-29 13:28:56 +01:00
Nicolás Hatcher
b859af1dc4 FIX: Disable TAN from being tested 2024-12-29 13:28:56 +01:00
Steve Fanning
f9cfdeb35b Add TAN function description 2024-12-29 11:03:07 +01:00
Nicolás Hatcher
669a5eec39 FIX: Removes types.js after used 2024-12-28 14:49:32 +01:00
Steve Fanning
e268dda9e8 Add SIN function description 2024-12-28 12:28:16 +01:00
Nicolás Hatcher
e0205d6c9a FIX: Increments the dum counter in the number of documented functions 2024-12-28 10:55:09 +01:00
Steve Fanning
81ad724348 Add COS function description 2024-12-28 10:46:32 +01:00
Nicolás Hatcher
dc3bf8826b FIX: PV Return #DIV/0! instead of #NUM! if rate = -1 2024-12-27 20:19:32 +01:00
Steve Fanning
38023d3156 Include PV function description 2024-12-27 20:19:32 +01:00
Gian Hancock
655d663590 FIX: Make XOR, OR, AND functions more consistent with Excel
The way these functions interpret their arguments is inconsistent with
Excel in a few ways:

- EmptyCell: Excel ignores arguments evaluating to these types of
  values, treating them as if they didn't exist.

- Text: Text cells are ignored unless they are "TRUE" or "FALSE" (case
  insensitive). EXCEPT if the string value comes from a reference, in
  which case it is always ignored regardless of its value.

- Error if no args: Excel returns a #VALUE! error for these functions if
  no arguments are provided, or if all arguments are ignored (see
  above).

- EmptyArg: Bizarrely, Unlike EmptyCell, EmptyArg is not ignored and is
  treated as if it were FALSE by Excel.

- ErrorPropagation: Excel propagates errors in the arguments and in
  cells belonging to any Range arguments.

Additionally, these functions are not consistent with each other, XOR,
OR, AND vary in how they handle the cases mentioned above.

Rectify these consistency issues by re-implementing them all in terms of
a single base function which is more consistent with Excel behavior.
2024-12-26 15:06:54 +01:00
Gian Hancock
8ba30fde33 Add more tests for OR and XOR
Some of these tests fail due to inconsistencies with Excel

cleanup
2024-12-26 15:06:54 +01:00
Nicolás Hatcher
690032c811 FIX: Remove optional context in parser
The context was optional because I thought that paring an RC formula
did not need context.

You at least need the sheet in which you are parsing
For instance toknow if a defined name is local
2024-12-26 10:21:21 +01:00
Nicolás Hatcher
86213a8434 FIX: Add test for get_defined_name_list
Also uses the scope instead of the opaque sheet_id
2024-12-26 10:21:21 +01:00
Nicolás Hatcher
2ed5fb9bbc FIX: Adds some validation and tests 2024-12-26 10:21:21 +01:00
Nicolás Hatcher
e455ed14ea UPDATE: API for defined names 2024-12-26 10:21:21 +01:00
Daniel
ad2efad3ae FIX: Remove tabIndex and onKeyDown as they were not used 2024-12-25 20:04:48 +01:00
Daniel
40461b897b FIX: Adds crossRef back to UploadFileDialog 2024-12-25 20:04:48 +01:00
Daniel
2e7410552f FIX: Apply useTranslation on dialog tooltips 2024-12-25 20:04:48 +01:00
Daniel
095002710b Fix: Removed unnecessary ref 2024-12-25 20:04:48 +01:00
Daniel
8ba131011e FIX: Replace SVG "X" icons with Lucide icons for consistency in dialogs 2024-12-25 20:04:48 +01:00
Nicolás Hatcher Andrés
dbddc027fb Update test-coverage.yaml 2024-12-25 16:57:54 +01:00
Steve Fanning
de997f38f5 Minor updates to Error Types and Value Types documentation 2024-12-25 13:47:21 +01:00
Steve Fanning
df4b4ca353 Update to FV description and associated pages in the Features area. 2024-12-25 13:47:21 +01:00
Daniel
3b944cd659 FIX: Use the right icon for paste format action 2024-12-20 20:13:25 +01:00
Nicolás Hatcher Andrés
d1f2b2acdd Update test-coverage.yaml 2024-12-20 14:10:02 +01:00
Daniel González-Albo
36f915b193 Merge pull request #207 from ironcalc/feature/nicolas-hidden
UPDATE: Hide/Unhide sheets
2024-12-20 10:14:09 +01:00
Nicolás Hatcher
5d8e6255a3 UPDATE: Hide/Unhide sheets 2024-12-20 00:49:33 +01:00
Daniel González-Albo
73f3c06203 Merge pull request #205 from ironcalc/bugfix/nico-dialog-fixes
FIX[WebApp]: Some fixes to the DeleteWorkbook and Import dialogs
2024-12-20 00:16:42 +01:00
Nicolás Hatcher
13b1157c61 FIX[WebApp]: Some fixes to the DeleteWorkbook and Import dialogs 2024-12-19 21:14:05 +01:00
Daniel
44f7929f4e FIX: responsiveness 2024-12-19 17:48:54 +01:00
Daniel
23643f0fae UPDATE: connect FileMenu button to open confirmation modal 2024-12-19 17:48:54 +01:00
Daniel
ad91d47db0 UPDATE: add confirmation modal for deleting workbooks 2024-12-19 17:48:54 +01:00
Daniel
8f36a1f750 FIX: make grid header colors consistent 2024-12-19 17:37:10 +01:00
Sinan Yumurtaci
8ad407432f lint 2024-12-19 17:36:27 +01:00
Sinan Yumurtaci
ebc31780ab FIX[WebApp]: Disable delete for sheet if it is the last one 2024-12-19 17:36:27 +01:00
Daniel
6e8c47d4f6 UPDATE: Replace one preset color from color picker 2024-12-18 20:00:52 +01:00
Daniel González-Albo
ed42667e87 Merge pull request #198 from ironcalc/bugfix/nicolas-modal-fixes
FIX[UI]: Rename modal dialog fixes
2024-12-16 22:35:49 +01:00
Nicolás Hatcher
0cd3470a97 FIX[UI]: Rename modal dialog fixes
This will be a standard "Prompt" widget

* ESC closes the dialog without changes
* Can copy/cut paste
2024-12-16 21:24:41 +01:00
Daniel
ab3f9c276d FIX: Border picker cosmetics 2024-12-16 17:33:34 +01:00
Nicolás Hatcher
e098105531 FIX: When cut and paste to a different sheet origin is removed
Fixes #193
2024-12-16 08:01:51 +01:00
Daniel
a5919d837f UPDATE: Better active states on Sheet Nav + Double click to rename sheets 2024-12-15 21:29:10 +01:00
Daniel
f214070299 FIX: Several cosmetic changes to make the toolbar look like the figmas 2024-12-15 19:18:51 +01:00
Daniel
0b2de92053 FIX: Made menu on SheetTab consistent with Figmas 2024-12-15 19:15:34 +01:00
Nicolás Hatcher
98dc1f3b06 FIX[WebApp]: Rename navigation => SheetTabBar
Also rename all widgets in that folder to more standard names
2024-12-15 12:12:14 +01:00
Nicolás Hatcher
fb764fed1c FIX[WebApp]: Splits the menu.tsx in two files
This is so that SheetListMenu and SheetRenameDialog are on its own files.

Also renamed isOpen => open and close => onClose
2024-12-15 12:12:14 +01:00
Nicolás Hatcher
23814ec18c FIX: Several fixes on the FV function
(1+x)^(1+y) was stringifyfied incorrectly
We still need work on this

FV now returns currency

FV(-1,-2,1) should return #DIV/0! not #NUM!
2024-12-14 22:08:44 +01:00
Daniel
8c6aaf2af0 FIX: Replace inline styles with StyledMenu component for improved readability 2024-12-14 18:34:13 +01:00
Daniel González-Albo
ed24e57555 Merge pull request #185 from ironcalc/fix/dani-webapp-navigator
FIX: remove legacy scrollbar styles
2024-12-14 18:14:17 +01:00
Daniel González-Albo
e8ced73b9c Merge pull request #186 from ironcalc/fix/dani-rename-sheet-dialog
FIX: Makes styling in Rename Sheet Dialog consistent with rest of dia…
2024-12-14 18:13:50 +01:00
Daniel
7ba002aca4 FIX: Makes footer/navigator scrollable when there are too many sheets 2024-12-14 18:04:35 +01:00
Daniel
b0e72321b4 FIX: Makes styling in Rename Sheet Dialog consistent with rest of dialogs 2024-12-13 20:43:38 +01:00
Nicolás Hatcher
41350fbd73 FIX: Run tests again and disable GEOMEAN test 2024-12-13 16:08:46 +01:00
Gian Hancock
17cd1fee96 Validate arg count for OR function
To be compatible with Excel, at least 1 argument is required.

Fixes #175
2024-12-12 13:06:12 +01:00
Gian Hancock
c59148bdf6 Add tests for OR function
Tests currently fail due to issue #175
2024-12-12 13:06:12 +01:00
Andrew Fillmore
d4a2289826 Add GEOMEAN .xlsx, Update .md files, and Run cargo fmt 2024-12-12 05:58:20 +01:00
Andrew Fillmore
e5aff48e36 Add GEOMEAN Tests 2024-12-12 05:58:20 +01:00
Andrew Fillmore
4c3374c0de Add GEOMEAN to Functions 2024-12-12 05:58:20 +01:00
Sinan Yumurtacı
5f3856350b ugh 2024-12-12 05:49:25 +01:00
Sinan Yumurtaci
7058a59c45 two other typos 2024-12-12 05:49:25 +01:00
Sinan Yumurtaci
075760b7ba typo 2024-12-12 05:49:25 +01:00
Nicolás Hatcher
8669962904 FIX: Apply biome "usafe" changes 2024-12-12 05:34:49 +01:00
Nicolás Hatcher
7b30736792 FIX: Make biome happy 2024-12-12 05:34:49 +01:00
Daniel
40c7fc8f80 FIX: Makes styling in Format Dialog consistent with Figmas 2024-12-12 05:34:49 +01:00
Daniel González-Albo
d8d694dd4a FIX: Makes styling in Format Menu consistent with Figmas (#179) 2024-12-12 05:13:07 +01:00
Daniel González-Albo
4b848f26dd FIX: Makes footer/navigator scrollable when there are too many sheets (#180) 2024-12-12 05:09:30 +01:00
Gian Hancock
2f660f85a7 Fix clippy lint (#177) 2024-12-11 11:45:24 +01:00
Sinan Yumurtacı
eee40c1b9a FIX: Prevent negative column width, row height (#167)
* Prevent negative column width / row height in rust

* prevent in front-end
2024-12-11 00:07:06 +01:00
Nicolás Hatcher Andrés
56915ce0b1 Merge pull request #173 from ironcalc/bugfix/nicolas-format-issue
Bugfix/nicolas format issue
2024-12-10 00:39:30 +01:00
Nicolás Hatcher
a4a3b11858 FIX: Avoid freezing the app on frozen columns or rows 2024-12-09 22:33:22 +01:00
Nicolás Hatcher
65f1738473 FIX: Use unicode code points in getFormulaHTML function 2024-12-09 21:46:03 +01:00
Nicolás Hatcher
a05ff18e40 FIX: Stop propagation in Rename window
We will need to do this in every widget in a more efficient manner
2024-12-09 19:51:28 +01:00
Nicolás Hatcher
4ef8a6882f FIX[Format-parser]: Parse [$€]#,##0.00 correctly
We will need to have a look at the format parser sooner rather
than later though
2024-12-09 19:50:41 +01:00
Nicolás Hatcher Andrés
2f2a5e4fba Merge pull request #172 from ironcalc/feature/dani-color-picker
UPDATE: Color picker changes
2024-12-08 22:32:14 +01:00
Daniel
c39540a747 UPDATE: Changing the color picker to look like in the Figmas 2024-12-08 22:22:17 +01:00
Daniel
0fa69045f9 UPDATE: WIP 2024-12-08 22:22:17 +01:00
Nicolás Hatcher Andrés
23e958af0c Merge pull request #171 from ironcalc/bugfix/dani-button-borders
UPDATE: Moved Border button next to Background color button
2024-12-08 22:16:25 +01:00
Daniel
057835627b UPDATE: Moved Border button next to Background color button 2024-12-08 21:54:34 +01:00
Nicolás Hatcher Andrés
4d6fdf9a4a Merge pull request #170 from ironcalc/feature/nicolas-disable-coverage
FIX: Disable coverage as it is failing for all
2024-12-08 17:15:23 +01:00
Nicolás Hatcher
5731b5cc27 FIX: Disable coverage as it is failing for all
I have send them an email, maybe I can fix this
2024-12-08 17:08:25 +01:00
Nicolás Hatcher Andrés
592ef2415d Merge pull request #169 from ironcalc/update/nicolas-fv-documentation
Update/nicolas fv documentation
2024-12-08 16:21:56 +01:00
Nicolás Hatcher
cb6685f72a FIX: Fv documentation. Some updates to the text 2024-12-08 11:11:48 +01:00
Nicolás Hatcher
8402bb0935 FIX: correct links in fv documentation 2024-12-08 01:55:34 +01:00
Nicolás Hatcher
91df91c425 FIX: Make biome happy 2024-12-07 12:45:49 +01:00
Daniel
4aa770c118 UPDATE: Makes 'File' button the same as 'Help', makes manu consistent 2024-12-07 12:45:49 +01:00
Daniel
c92c065073 UPDATE: Added a Help button 2024-12-07 12:45:49 +01:00
Daniel
6f124185b2 UPDATE: Cosmetic changes on file bar 2024-12-07 12:45:49 +01:00
stevethesleeve
17cf519d41 Update fv files (#162)
* Files updated for FV function documentation.

* Further FV changes
2024-12-07 12:25:57 +01:00
Sylvain Zimmer
bd1a1e3c97 Fix typo 2024-12-06 20:51:32 +01:00
Andrew Grosser
3d517a4af4 added updates from cargo fmt 2024-12-05 23:29:40 +01:00
Andrew Grosser
1734fd5740 added missing api functions for get_cell* 2024-12-05 23:29:40 +01:00
Daniel
d9dbd3bf14 UPDATE: Small fixes in Upload file Dialog 2024-12-05 00:11:41 +01:00
Nicolás Hatcher
d8a5c29e2f FIX: Make Biome happy 2024-12-01 20:14:38 +01:00
Daniel
85cd7ab6a3 UPDATE: design adjustments in 'File' menu 2024-12-01 20:14:38 +01:00
Nicolás Hatcher
4b806c357a FIX[parser]: Check the order (row, column) in range before transforming
Fixes #155
2024-11-30 14:37:06 +01:00
Steve Fanning
3270d587ac Minor update to the README file in docs. 2024-11-29 21:22:00 +01:00
Nicolás Hatcher
e065477b5a FIX: Make clippy happy
This is mostly Rust 1.83
2024-11-29 19:55:12 +01:00
Nicolás Hatcher
1f5f575e7a UPDATE: Update chrono-tzand bitcode crate versions 2024-11-29 19:55:12 +01:00
Nicolás Hatcher
472740f296 UPDATE: Use regex-lite crate instead of of regex
This removes almost 1Mb form the generated wasm(!!!)
2024-11-29 19:55:12 +01:00
Jonathan S
430b420435 Update about.md 2024-11-29 19:00:01 +01:00
Steve Fanning
48fd51fc8a Minor improvements to CONTRIBUTING.md 2024-11-29 11:49:02 +01:00
Nicolás Hatcher
614d71b61c UPDATE: Adds CONTRIBUTING guide an documentation 2024-11-28 22:42:14 +01:00
Nicolás Hatcher
38e21b9639 FIX: Small fix in app routing 2024-11-28 22:42:14 +01:00
Nicolás Hatcher
d6a462dbe3 FIX: Added changelog entry 2024-11-28 22:42:14 +01:00
Nicolás Hatcher
c1df2cec0b FIX: Issues with paste
Copy/Paste is always with tab separated values not "," or others.
Maybe we can add that?
2024-11-27 22:55:39 +01:00
Nicolás Hatcher
283a44e109 FIX: Aaahhh! Make the linter happy 2024-11-27 22:55:39 +01:00
Nicolás Hatcher
f7dac8b015 FIX: fmt issues 2024-11-27 22:55:39 +01:00
Nicolás Hatcher
df0aa51d14 FIX: Adds some tests 2024-11-27 22:55:39 +01:00
Nicolás Hatcher
411c5de59b FIX: Another round a borders
Only test are missing(!)
2024-11-27 22:55:39 +01:00
Nicolás Hatcher
0df132d5c2 FIX: Work undone
We are not going to follow this path, but I will leave this commit as
part of the git history
2024-11-27 22:55:39 +01:00
Nicolás Hatcher
8cdb3b8c60 FIX: Removes ununsed borders code 2024-11-27 22:55:39 +01:00
Nicolás Hatcher
d08fe32f97 FIX: color picker and border issues 2024-11-27 22:55:39 +01:00
Nicolás Hatcher
47acd0d600 FIX: Some fixes to the doumentation 2024-11-27 22:28:07 +01:00
Nicolás Hatcher
d70ab85396 FIX: Adds some more tests fro FORMULATEXT 2024-11-27 22:28:07 +01:00
Nicolás Hatcher
65b959cb1c FIX: Forgotten file :S, adds documentation and CHANGELOG 2024-11-27 22:28:07 +01:00
Nicolás Hatcher
726fc1399d UPDATE: Adds FORMULATEXT for Steve 2024-11-27 22:28:07 +01:00
Nicolás Hatcher
8ed0ab25f6 FIX: Fix run of python examples
This needs to be properly fixed
2024-11-26 20:13:41 +01:00
Nicolás Hatcher
949eafc97f FIX: Removes the csv-sniffer in favour of a simple guess
This removes 500Kb form the was build so it is worth it.
We were using a very old version of the sniffer, the last one might not
have this bug though
2024-11-26 20:13:41 +01:00
Nicolás Hatcher
1f1fd24334 UPDATE: Reorder links. Add desktop app 2024-11-25 19:10:41 +01:00
Nicolás Hatcher
2b0c24de55 UPDATE: Adds bluesky link 2024-11-25 19:10:41 +01:00
Nicolás Hatcher
1aa9b6a220 FIX: Proper path for "Edit GitHub" 2024-11-25 19:10:41 +01:00
Nicolás Hatcher
94ebf33656 UPDATE: Remove docs artifacts that are not needed anymore
generate_docs.rs was used to bootstrap the documentatio effords

Unlikely to be used on a regular basis
2024-11-25 19:10:41 +01:00
Nicolás Hatcher
b3d4c479f6 UPDATE: CHANGELOG for the document server 2024-11-25 19:10:41 +01:00
Nicolás Hatcher
67aaa85a9a FIX: Rename folder (fix typo) 2024-11-25 19:10:41 +01:00
Nicolás Hatcher
5d2953b894 FIX: Incorrect filename.
Fixes #141
2024-11-25 08:32:21 +01:00
Nicolás Hatcher
b84aeb8bb9 FIX: Make Biome happy 2024-11-24 23:08:52 +01:00
Nicolás Hatcher
c5576af81b FIX: Fixes according to design 2024-11-24 23:08:52 +01:00
Daniel
cb38eff899 UPDATE: text changes in some sections, adds placeholder pages 2024-11-24 23:08:52 +01:00
Nicolás Hatcher
16212b1518 FIX: Fixes broken links, rewording 2024-11-24 23:08:52 +01:00
Nicolás Hatcher
8243e231ab UPDATE: Add placeholders for all functions 2024-11-24 23:08:52 +01:00
Nicolás Hatcher
48afb45eb9 UPDATE: Documents reorganization and some additions 2024-11-24 23:08:52 +01:00
Daniel
97846041e5 UPDATE: Adjusted settings, styling and basic content pages 2024-11-24 23:08:52 +01:00
Daniel
c1be1e47cb UPDATE: First commit with the new documentation 2024-11-24 23:08:52 +01:00
Nicolás Hatcher
7efdbede3c UPDATE: Add entry in CHANGELOG for UNICODE function
🚀
2024-11-17 17:08:22 +01:00
Bruno Carvalhal
be6819fec3 Remove unwrap logic 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
e130f784fd add missing test case for multiple arguments in UNICODE 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
c2a2983937 Add missing UNICODE to to_xlsx_string 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
3c49f9a606 Add error cases 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
63817e2d50 Also accept _xlfn.UNICODE 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
028ae1ce98 Change output formatting of UNICODE to Number 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
a4a40c6fd0 Add test workbook for UNICODE 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
0ed2984358 Add code coverage 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
0b1199056f Remove unneeded comment 2024-11-17 16:49:41 +01:00
Bruno Carvalhal
726bf677ed Implement UNICODE function 2024-11-17 16:49:41 +01:00
Nicolás Hatcher
d681f63b25 FIX: Test also full rows and columns in IRR function
This means functions like `=IRR(A:A)` or `=IRR(1:1)`
2024-11-17 11:59:10 +01:00
Nicolás Hatcher
ba5869420b FIX: Run covergae also on merge in main 2024-11-17 11:59:10 +01:00
Nicolás Hatcher
420ea9829c FIX: Refactor some finatial functions to use common code 2024-11-17 11:59:10 +01:00
Nicolás Hatcher
49ae2d8915 FIX: Forbid unwrap, expect and panic in the base code 2024-11-17 11:59:10 +01:00
Nicolás Hatcher
bdd2c8fe04 FIX: Fix several indentation issues in comments
Thanks clippy!
2024-11-17 11:59:10 +01:00
Nicolás Hatcher
24dd63b261 FIX: Correct tag number (oops) 2024-11-15 01:07:02 +01:00
Nicolás Hatcher
861700cb45 UPDATE: Adds CHANGELOG 2024-11-15 00:59:13 +01:00
Mehdi Armachi
98dc557a01 Adds navigation labels for sheet management 2024-11-11 10:36:54 +01:00
Nicolás Hatcher
2c2228c2c2 FIX: [App]: Borders done right 2024-10-31 22:43:43 +01:00
Nicolás Hatcher
494a315cbd FIX: Do geometry right 2024-10-31 22:43:43 +01:00
Nicolás Hatcher
0c69889832 FIX: Column/Row width/height UI issues 2024-10-31 22:43:43 +01:00
Nicolás Hatcher
04d8c658ab UPDATE: Adds cut/paste 2024-10-31 22:43:43 +01:00
Nicolás Hatcher
dad4755b16 FIX: Fixes from Dani's design 2024-10-26 11:04:52 +02:00
Nicolás Hatcher
75d8a5282e FIX: Slightly better behaviour for increase/decrease decimal places
The general solution must be done in Rust and it is a bit more complex.
2024-10-26 11:04:52 +02:00
Nicolás Hatcher
f78027247b FIX: Make biome happy 2024-10-26 11:04:52 +02:00
Nicolás Hatcher
ee6a41c4f4 FIX: Nicer loading image 2024-10-26 11:04:52 +02:00
Nicolás Hatcher
b7336f70d6 FIX[App]: Font-size of menu is 12px 2024-10-26 11:04:52 +02:00
Nicolás Hatcher
dae37f14ba FIX[App]: Over scroll issues 2024-10-26 11:04:52 +02:00
Nicolás Hatcher
7ffbfac432 FIX[WebApp]: fixes in formula bar
* fx is not clickable
* Removed chevron
* Show slecting/ed area in address
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
f9ea4fd757 FIX[WebApp]: Only show the active ranges in the correct sheet
Fixes #104
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
7446932519 FIX[WebApp]: Keep the area extended as selected
Fixes #110
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
d55845e69f FIX[WebApp]: PreventDefault when clicking on the Format Editor
Fixes #112
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
9e5b959ccc FIX[WebApp]: Pass the name along to the serve
Fixes #111
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
ffa93309e2 FIX: Renaming a sheet with the same name doesn't do anything
Fixes #103
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
79216b286b FIX: Caret color is IronCalc 2024-10-24 21:54:34 +02:00
Nicolás Hatcher
411d4a3780 FIX: Make biome happy 2024-10-24 21:54:34 +02:00
Nicolás Hatcher
3a7aa15347 FIX: Mark code as ununsed for now 2024-10-24 21:54:34 +02:00
Nicolás Hatcher
090e852054 FIX: Make selected sheet bold 2024-10-24 21:54:34 +02:00
Nicolás Hatcher
3e54ad5b3c FIX: Rename sheet dialog with correct default name
Fixes #103
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
7b12c2682e FIX: Headers height show be the same as the default row height 2024-10-24 21:54:34 +02:00
Nicolás Hatcher
80273a88ec FIX: When creating a new sheet, select it
Fixes #100
2024-10-24 21:54:34 +02:00
Nicolás Hatcher
3d951c5c50 FIX: Several UI fixes from Dani
* Toast has Inter font-family
* Share button has Inter font family
* More accurate menu list to design
* Removes unused navigation
* Adds link to IronCalc
* Removes line=black :O
2024-10-23 22:42:22 +02:00
Nicolás Hatcher
cd54389e91 UPDATE: Implement copy/paste in the UI 2024-10-23 21:43:18 +02:00
Nicolás Hatcher
843d8beb02 FIX: Once again more
apparently I don't have anything better to do :D
2024-10-15 19:29:21 +02:00
Nicolás Hatcher
09ac29785d FIX[wasm]: Fixes failing test 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
2b530423c8 FIX[base]: Adds test for names and row heigh 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
51c41900d7 FIX: Fix broken tests 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
730a815729 FIX[Editor]: More simplifications and fixes 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
9805d0c518 FIX: Set the color of the refe range to be the next from the active ranges 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
10a9d36f3d FIX: Make biome happy 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
480640dc98 UPDATE[WebApp]: we can now delete models on the localStorage 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
3058a63e4f FIX: Correct default for vertical align 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
8275d73b64 FIX: Set default row height to 22
This matches the line height. So far a magic number
2024-10-15 19:29:21 +02:00
Nicolás Hatcher
072abb2240 FIX: Vertical Align by default is bottom 2024-10-15 19:29:21 +02:00
Nicolás Hatcher
9a46e5ccc7 FIX: More fixes to the cell editor
* Font family is Inter, font size 13, line-width 22
* Correct vertical align for multiline text
* Entering multiline text sets the height of the row (!)
2024-10-15 19:29:21 +02:00
Nicolás Hatcher
585e594d8d FIX: Diverse fixes to the editor
* Editor now expands as you write
* You can switch between the formula bar and cell editor
* While editing in the formula bar you see the results in the editor
* Give Mateusz more credit
2024-10-15 19:29:21 +02:00
Nicolás Hatcher
248ef66e7c FIX: Make biome happy 2024-10-11 21:08:16 +02:00
Nicolás Hatcher
15da2e5785 FIX: Close the sheet list menu when a sheet is selected 2024-10-11 21:08:16 +02:00
Nicolás Hatcher
39174add1f FIX: number format menu closes when selected 2024-10-11 21:08:16 +02:00
Nicolás Hatcher
e412f5fc22 FIX: Delete the selected area correctly
Previously it was deleting one extra row and column
2024-10-11 21:08:16 +02:00
Nicolás Hatcher
42c1a39131 FIX: Cell editor correct behaviour 2024-10-11 21:08:16 +02:00
Nicolás Hatcher
f26cdd3a4b FIX: Sets the patternFill to solid when changing the background color 2024-10-11 21:08:16 +02:00
Nicolás Hatcher
4016eb5944 FIX: Better support for mobile phones 2024-10-11 21:08:16 +02:00
Nicolás Hatcher
58dfdd329e FIX: Fix broken build 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
4a290aec7c FIX: Forgotten file :S 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
3966dbc790 FIX: Correct font-size in navigation bar 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
abd4ce4ea5 FIX: Let’s move the outline handle to left and top 1px 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
02da1eb388 FIX: Make default cells 25% larger 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
1131234531 FIX: Slightly better widths in the row headers
I'm afraid this nees to be completely redone
2024-10-08 23:10:34 +02:00
Nicolás Hatcher
b495397b5f FIX: Proper imports 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
8c0a566995 FIX: Set grid color to grey-300 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
dd62dd2dc6 FIX: Set format menu font-size to 12px 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
79b7b9b817 FIX: Correct paddings in formula bar 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
06ae1a1d6d FIX: Fix tooltips on buttons
* Strike through,
* Hide grid lines
* all vertical/horizontal align buttons
2024-10-08 23:10:34 +02:00
Nicolás Hatcher
6390739fd4 FIX: Correct height of toolbar (48) and formula bar (40) 2024-10-08 23:10:34 +02:00
Nicolás Hatcher
e41741cf77 FIX: Change border color between toolbox and formula bar to grey-300 2024-10-08 23:10:34 +02:00
Nicolás Hatcher Andrés
48719b6416 UPDATE: Adds cell and formula editing (#92)
* UPDATE: Adds cell and formula editing

* FIX: Do not loose focus when clicking on the formula we are editing

* FIX: Minimal implementation of browse mode

* FIX: Initial browse mode within sheets

* UPDATE: Webapp

Minimal Web Application
2024-10-08 19:44:27 +02:00
Nicolás Hatcher Andrés
53d3d5144c UPDATE: point documentation to app instead of playground (#93) 2024-09-28 19:23:32 +02:00
Nicolás Hatcher Andrés
bf9a1ed9f4 FIX: Add infrastructure for python tests (#91)
Also integrated with CI and runs tests in documentation
2024-09-21 15:46:32 +02:00
Nicolás Hatcher Andrés
11df4a55c7 UPDATE: Adds python bindings (#90)
* UPDATE: Adds python bindings

Exclude pyroncalc and wasm from test coverage
2024-09-21 11:58:50 +02:00
Nicolás Hatcher Andrés
00b5b65588 UPDATE: bump version to 0.2.0 (#89) 2024-09-17 00:27:06 +02:00
Varun Hegde
2b03b3e3b9 Error Handling of public Set functions (#88)
What are we trying to achieve ?

++ Currently all the major public set functions is panic prone and does not handle and return error. This PR tries to address to all those functions.

What major errors that could happen in these functions ?

++ All the functions which are being made as error safe is being tested against invalid sheet, row and column values, which could given by user

What are the list of functions whose return type has been altered ?

**base/src/model.rs**
1. update_cell_with_text
2. update_cell_with_bool
3. update_cell_with_number
4. set_user_input
5. get_cell_style_index
6. get_style_for_cell
7. set_cell_with_string

++> New functions being added

1. set_cell_with_boolean
2. set_cell_with_number

**base/src/styles.rs**

1. get_style_with_quote_prefix
3. get_style_with_format
4. get_style_without_quote_prefix
5. get_style

**base/src/worksheet.rs**

1. update_cell
2. set_cell_style
3. set_cell_with_formula
4. set_cell_with_number
6. set_cell_with_string
8. set_cell_with_boolean
9. set_cell_with_error
10. cell_clear_contents
11. cell_clear_contents_with_style

++> Above is the comprehensive list of all functions being ( most are public, some are private ) altered for better error handling. As a side effect of changing function signature, there are many changes being done to other functions ( mostly adding "?" to enable to error propagation further )
2024-09-14 17:37:31 +02:00
Nicolás Hatcher Andrés
83a4431417 FIX[WebApp]: Simplify loading logic of workbook (#87)
Note that previously the model would have been fetched twice
2024-08-24 06:59:19 +02:00
Nicolás Hatcher Andrés
40aa8bebaf UPDATE: Adds load_from_xlsx_bytes (#86)
This is usefull if we are transfering bytes, for instance over the internet
2024-08-24 06:50:31 +02:00
Nicolás Hatcher Andrés
b9bf485379 Update README.md (#80)
We don't need a logo anymore
2024-08-18 12:50:34 +02:00
Nicolás Hatcher Andrés
dc23a7f29c UPDATE: Adds web app (#79)
Things missing:

* Browse mode
* Front end tests
* Storybook
2024-08-18 11:44:16 +02:00
Varun Hegde
083548608e Exporting the Merged Cell : Exports the merged cell properly with testcase to verify the scenario (#76)
* adding the functionality of exporting the merged cell with testcase to vreify the above enhancement

* addressing review comments : 1) moving the testing to appropriate folder 2) fixing lint errors 3) fixing the scenario when merge cell count is 0

* addressing 2nd round review comments : cosmetic fixes

* addressing review comments : taking reference instead of cloning
2024-07-21 16:13:58 +02:00
Nicolás Hatcher Andrés
0ba80035d2 FIX: Run test coverage only on Pull Request (#77) 2024-07-16 07:48:41 +02:00
Nicolás Hatcher Andrés
55a043366a FIX: Fixes TypeScript types correctly (#75)
We really need a better way of doing this :/
2024-06-02 19:03:59 +02:00
Nicolás Hatcher Andrés
864a37f1e6 UPDATE: Adds autofill_columns (#74) 2024-06-02 18:43:43 +02:00
Nicolás Hatcher Andrés
72c7c94f3d UPDATE: Autofill by rows (#73) 2024-06-02 10:01:46 +02:00
Nicolás Hatcher Andrés
c3a9b006d2 FIX: Export views and showGridLines properly (#72) 2024-05-28 21:52:10 +02:00
Nicolás Hatcher Andrés
b37397acb8 UPDATE: Adds ability to show/hide grid lines (#71) 2024-05-26 21:28:00 +02:00
Nicolás Hatcher Andrés
49c3b14bf0 UPDATE: Adds get/set views to the user model API (#69) 2024-05-20 17:51:09 +02:00
Nicolás Hatcher Andrés
d2cba48f8e FIX: Fixes incorrect result in M1 Apple silicom (#68) 2024-05-19 10:34:57 +02:00
Nicolás Hatcher Andrés
f752c90058 UPDATE: Parses selected row/column/range and selected sheet (#67)
* FIX: Update to Rust 1.78.0

* UPDATE: Parses selected row/column/range and selected sheet
2024-05-09 11:46:26 +02:00
Daniel González-Albo
a78d5593f2 Merge pull request #60 from ironcalc/feature/dani-logo
UPDATE: adds missing favicons
2024-04-27 18:11:29 +02:00
Daniel
079208a1bd UPDATE: adds missing favicons 2024-04-27 18:02:04 +02:00
Daniel González-Albo
4721582dfe Merge pull request #42 from ironcalc/feature/dani-logo
UPDATE: adds logo
2024-04-25 19:49:43 +02:00
Daniel
1746eec5da UPDATE: adds logo 2024-04-25 19:42:10 +02:00
Nicolás Hatcher Andrés
f9cf86a17c Bugfix/nicolas more fixes (#36)
* FIX: Remove the serde_json depndendency

* UPDATE: Use binary representation also for languages and locales
2024-04-15 19:25:38 +02:00
Nicolás Hatcher Andrés
49ef846ebd FIX: small diverse fixes (#35) 2024-04-14 21:50:14 +02:00
1019 changed files with 75724 additions and 7712 deletions

446
.github/workflows/npm.yml vendored Normal file
View File

@@ -0,0 +1,446 @@
name: nodejs
env:
DEBUG: napi:*
APP_NAME: nodejs
MACOSX_DEPLOYMENT_TARGET: '10.13'
permissions:
contents: write
id-token: write
'on':
workflow_dispatch:
inputs:
publish:
description: "Publish to npm"
required: true
type: boolean
defaults:
run:
working-directory: ./bindings/nodejs
jobs:
build:
strategy:
fail-fast: false
matrix:
settings:
- host: macos-latest
target: x86_64-apple-darwin
build: yarn build --target x86_64-apple-darwin
- host: windows-latest
build: yarn build --target x86_64-pc-windows-msvc
target: x86_64-pc-windows-msvc
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: yarn build --target x86_64-unknown-linux-gnu
- host: ubuntu-latest
target: x86_64-unknown-linux-musl
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
build: yarn build --target x86_64-unknown-linux-musl
- host: macos-latest
target: aarch64-apple-darwin
build: yarn build --target aarch64-apple-darwin
- host: ubuntu-latest
target: aarch64-unknown-linux-gnu
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
build: yarn build --target aarch64-unknown-linux-gnu
- host: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
setup: |
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf -y
build: yarn build --target armv7-unknown-linux-gnueabihf
- host: ubuntu-latest
target: armv7-unknown-linux-musleabihf
build: yarn build --target armv7-unknown-linux-musleabihf
- host: ubuntu-latest
target: aarch64-linux-android
build: yarn build --target aarch64-linux-android
- host: ubuntu-latest
target: armv7-linux-androideabi
build: yarn build --target armv7-linux-androideabi
- host: ubuntu-latest
target: aarch64-unknown-linux-musl
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
build: |-
set -e &&
rustup target add aarch64-unknown-linux-musl &&
yarn build --target aarch64-unknown-linux-musl
- host: windows-latest
target: aarch64-pc-windows-msvc
build: yarn build --target aarch64-pc-windows-msvc
- host: ubuntu-latest
target: riscv64gc-unknown-linux-gnu
setup: |
sudo apt-get update
sudo apt-get install gcc-riscv64-linux-gnu -y
build: yarn build --target riscv64gc-unknown-linux-gnu
name: stable - ${{ matrix.settings.target }} - node@20
runs-on: ${{ matrix.settings.host }}
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
if: ${{ !matrix.settings.docker }}
with:
node-version: 20
cache: yarn
cache-dependency-path: "bindings/nodejs"
- name: Install
uses: dtolnay/rust-toolchain@stable
if: ${{ !matrix.settings.docker }}
with:
toolchain: stable
targets: ${{ matrix.settings.target }}
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
.cargo-cache
target/
key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}
- uses: goto-bus-stop/setup-zig@v2
if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' || matrix.settings.target == 'armv7-unknown-linux-musleabihf' }}
with:
version: 0.13.0
- name: Setup toolchain
run: ${{ matrix.settings.setup }}
if: ${{ matrix.settings.setup }}
shell: bash
- name: Install dependencies
run: yarn install
- name: Build in docker
uses: addnab/docker-run-action@v3
if: ${{ matrix.settings.docker }}
with:
image: ${{ matrix.settings.docker }}
options: '--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
run: ${{ matrix.settings.build }}
- name: Build
run: ${{ matrix.settings.build }}
if: ${{ !matrix.settings.docker }}
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.settings.target }}
path: bindings/nodejs/${{ env.APP_NAME }}.*.node
if-no-files-found: error
test-macOS-windows-binding:
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
needs:
- build
strategy:
fail-fast: false
matrix:
settings:
- host: macos-latest
target: x86_64-apple-darwin
- host: windows-latest
target: x86_64-pc-windows-msvc
node:
- '18'
- '20'
runs-on: ${{ matrix.settings.host }}
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: yarn
cache-dependency-path: "bindings/nodejs"
architecture: x64
- name: Install dependencies
run: yarn install
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-${{ matrix.settings.target }}
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Test bindings
run: yarn test
test-linux-x64-gnu-binding:
name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }}
needs:
- build
strategy:
fail-fast: false
matrix:
node:
- '18'
- '20'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: yarn
cache-dependency-path: "bindings/nodejs"
- name: Install dependencies
run: yarn install
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-x86_64-unknown-linux-gnu
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Test bindings
run: docker run --rm -v $(pwd):/build -w /build/bindings/nodejs node:${{ matrix.node }}-slim yarn test
test-linux-x64-musl-binding:
name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }}
needs:
- build
strategy:
fail-fast: false
matrix:
node:
- '18'
- '20'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: yarn
cache-dependency-path: "bindings/nodejs"
- name: Install dependencies
run: |
yarn config set supportedArchitectures.libc "musl"
yarn install
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-x86_64-unknown-linux-musl
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Test bindings
run: docker run --rm -v $(pwd):/build -w /build/bindings/nodejs node:${{ matrix.node }}-alpine yarn test
test-linux-aarch64-gnu-binding:
name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }}
needs:
- build
strategy:
fail-fast: false
matrix:
node:
- '18'
- '20'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-aarch64-unknown-linux-gnu
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Install dependencies
run: |
yarn config set supportedArchitectures.cpu "arm64"
yarn config set supportedArchitectures.libc "glibc"
yarn install
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- name: Setup and run tests
uses: addnab/docker-run-action@v3
with:
image: node:${{ matrix.node }}-slim
options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
run: |
set -e
yarn test
ls -la
test-linux-aarch64-musl-binding:
name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }}
needs:
- build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-aarch64-unknown-linux-musl
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Install dependencies
run: |
yarn config set supportedArchitectures.cpu "arm64"
yarn config set supportedArchitectures.libc "musl"
yarn install
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- name: Setup and run tests
uses: addnab/docker-run-action@v3
with:
image: node:lts-alpine
options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
run: |
set -e
yarn test
test-linux-arm-gnueabihf-binding:
name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }}
needs:
- build
strategy:
fail-fast: false
matrix:
node:
- '18'
- '20'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-armv7-unknown-linux-gnueabihf
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Install dependencies
run: |
yarn config set supportedArchitectures.cpu "arm"
yarn install
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- name: Setup and run tests
uses: addnab/docker-run-action@v3
with:
image: node:${{ matrix.node }}-bullseye-slim
options: '--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
run: |
set -e
yarn test
ls -la
universal-macOS:
name: Build universal macOS binary
needs:
- build
runs-on: macos-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: "bindings/nodejs"
- name: Install dependencies
run: yarn install
- name: Download macOS x64 artifact
uses: actions/download-artifact@v4
with:
name: bindings-x86_64-apple-darwin
path: bindings/nodejs/artifacts
- name: Download macOS arm64 artifact
uses: actions/download-artifact@v4
with:
name: bindings-aarch64-apple-darwin
path: bindings/nodejs/artifacts
- name: Combine binaries
run: yarn universal
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: bindings-universal-apple-darwin
path: bindings/nodejs/${{ env.APP_NAME }}.*.node
if-no-files-found: error
publish:
name: Publish
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
needs:
- test-macOS-windows-binding
- test-linux-x64-gnu-binding
- test-linux-x64-musl-binding
- test-linux-aarch64-gnu-binding
- test-linux-aarch64-musl-binding
- test-linux-arm-gnueabihf-binding
- universal-macOS
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: "bindings/nodejs"
- name: Install dependencies
run: yarn install
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: bindings/nodejs/artifacts
- name: Move artifacts
run: yarn artifacts
- name: List packages
run: ls -R ./npm
shell: bash
- name: Publish
run: |
echo "${{ github.event.inputs.publish }}"
if [ "${{ github.event.inputs.publish }}" = "true" ]; then
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
npm publish --access public
echo "Published to npm"
else
echo "Not a release, skipping publish"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,18 +0,0 @@
name: Publish wiki
on:
push:
branches: [main]
paths:
- wiki/**
- .github/workflows/publish-wiki.yml
concurrency:
group: publish-wiki
cancel-in-progress: true
permissions:
contents: write
jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Andrew-Chen-Wang/github-wiki-action@v4

141
.github/workflows/pypi.yml vendored Normal file
View File

@@ -0,0 +1,141 @@
name: Upload component to Python Package Index
on:
workflow_dispatch:
inputs:
release:
type: boolean
default: false
required: false
description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org"
permissions:
contents: read
jobs:
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
manylinux: auto
working-directory: bindings/python
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
working-directory: bindings/python
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist
macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
working-directory: bindings/python
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
working-directory: bindings/python
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-sdist
path: bindings/python/dist
publish-to-test-pypi:
if: ${{ github.event.inputs.release != 'true' }}
name: >-
Publish Python 🐍 distribution 📦 to Test PyPI
runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
with:
path: bindings/python/
- name: Publish distribution 📦 to Test PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TEST_API_TOKEN }}
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
with:
command: upload
args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python
publish-pypi:
if: ${{ github.event.inputs.release == 'true' }}
name: >-
Publish Python 🐍 distribution 📦 to PyPI
runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
with:
path: bindings/python/
- name: Publish distribution 📦 to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
with:
command: upload
args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python

View File

@@ -1,6 +1,7 @@
name: Coverage name: Coverage
on: [pull_request, push] on:
workflow_dispatch:
jobs: jobs:
coverage: coverage:
@@ -14,10 +15,11 @@ jobs:
- name: Install cargo-llvm-cov - name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate code coverage - name: Generate code coverage
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info run: cargo llvm-cov --all-features --workspace --exclude pyroncalc --exclude wasm --lcov --output-path lcov.info
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info files: lcov.info
fail_ci_if_error: true fail_ci_if_error: true

2
.gitignore vendored
View File

@@ -1 +1,3 @@
target/* target/*
**/node_modules/**
.DS_Store

32
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,32 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "19"
# rust: "1.64"
# golang: "1.19"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: bindings/python/docs/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt

36
CHANGELOG.md Normal file
View File

@@ -0,0 +1,36 @@
# CHANGELOG
## Unreleased
### Added
- New function UNICODE ([#128](https://github.com/ironcalc/IronCalc/pull/128))
- New document server (Thanks Dani!)
- New function FORMULATEXT
- Name Manager ([#212](https://github.com/ironcalc/IronCalc/pull/212) [#220](https://github.com/ironcalc/IronCalc/pull/220))
- Add context menu. We can now insert rows and columns. Freeze and unfreeze rows and columns. Delete rows and columns [#271]
- Add nodejs bindings [#254]
- Add python bindings for all platforms
- Add is split into the product and widget
- Add Python documentation [#260]
### Fixed
- Fixed several issues with pasting content
- Fixed several issues with borders
- Fixed bug where columns and rows could be resized to negative width and height, respectively
- Undo/redo when add/delete sheet now works [#270]
- Numerous small fixes
- Multiple fixes to the documentation
## [0.2.0] - 2024-11-06 (The HN release)
### Added
- Rust crate ironcalc_base
- Rust crate ironcalc
- Minimal Python bindings (only Linux)
- JavaScript bindings
- React WebApp
[0.2.0]: https://github.com/IronCalc/ironcalc/releases/tag/v0.2.0

73
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,73 @@
# Contributing to IronCalc
Whether you are a seasoned developer or a rookie, welcome to IronCalc!
🎉 We appreciate your interest in contributing to our project.
Before starting any work it is best if you get in touch to make sure your work is relevant.
Please be patient, I am only one and this is a side project.
---
## 🛠 Changes to the main repo
If are comfortable working with GitHub and Git, the following steps should be straightforward. For more general information visit [GitHub Docs](https://docs.github.com/en) and [Git Documentation](https://git-scm.com/doc).
1. **Fork the repository**
Start by forking the repository to your own GitHub account. You can do this by clicking the "Fork" button on the top right of the repository page.
2. **Clone the original repository**
Clone the original repository to your local machine:
```bash
git clone https://github.com/ironcalc/IronCalc.git
cd IronCalc
```
3. **Add your fork as a remote**
Add your forked repository as a remote named fork:
```bash
git remote add fork https://github.com/<your-username>/IronCalc.git
```
4. **Create a new branch**
Always create a new branch for your changes to keep your work isolated:
```bash
git checkout -b your-feature-name
```
5. **Make changes**
Implement your changes, improvements, or bug fixes. Make sure to follow any coding style or project-specific guidelines.
6. **Commit your changes**
Write clear and concise commit messages:
```bash
git add .
git commit -m "Brief description of your changes"
```
7. **Push to your fork**
Push your branch to your forked repository:
```bash
git push fork your-feature-name
````
8. **Create a Pull Request (PR)**
Follow the steps on the terminal or go to the orig IronCalc repository, and click on "New Pull Request."
Ensure your PR has a clear title and description explaining the purpose of your changes.
Always start from the main branch in a clean state. `git pull` will generally get the lastest changes form the original repo.
You should make sure that your changes are properly tested.
# 🤝 Community and Support
Feel free to reach out if you have questions or need help. Via GitHub, email, our discord server or bluesky.
* Open an issue to report a bug or discuss a feature before implementing it.
* Engage with the community to share ideas or seek guidance.
Note that not all contributors need to be coding. To contribute testing, bug reports, typos, ideas of all kinds, you can just send us an email.
Thank you for your contributions! 💪 Together, we can make IronCalc even better.

379
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "adler" name = "adler"
@@ -43,6 +43,21 @@ dependencies = [
"libc", "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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.2.0" version = "1.2.0"
@@ -57,25 +72,34 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bitcode" name = "bitcode"
version = "0.6.0" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48bc1c27654127a24c476d40198746860ef56475f41a601bfa5c4d0f832968f0" checksum = "ee1bce7608560cd4bf0296a4262d0dbf13e6bcec5ff2105724c8ab88cc7fc784"
dependencies = [ dependencies = [
"arrayvec",
"bitcode_derive", "bitcode_derive",
"bytemuck", "bytemuck",
"glam",
"serde",
] ]
[[package]] [[package]]
name = "bitcode_derive" name = "bitcode_derive"
version = "0.6.0" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2966755a19aad59ee2aae91e2d48842c667a99d818ec72168efdab07200701cc" checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@@ -142,9 +166,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.37" version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
@@ -156,9 +180,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono-tz" name = "chrono-tz"
version = "0.9.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz-build", "chrono-tz-build",
@@ -167,12 +191,11 @@ dependencies = [
[[package]] [[package]]
name = "chrono-tz-build" name = "chrono-tz-build"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
dependencies = [ dependencies = [
"parse-zoneinfo", "parse-zoneinfo",
"phf",
"phf_codegen", "phf_codegen",
] ]
@@ -202,6 +225,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@@ -242,6 +274,37 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "ctor"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -299,6 +362,18 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "glam"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@@ -331,6 +406,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]] [[package]]
name = "inout" name = "inout"
version = "0.1.3" version = "0.1.3"
@@ -342,7 +423,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.1.3" version = "0.6.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -358,19 +439,31 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.1.3" version = "0.6.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"csv",
"js-sys", "js-sys",
"once_cell",
"rand", "rand",
"regex", "regex",
"regex-lite",
"ryu", "ryu",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "statrs",
]
[[package]]
name = "ironcalc_nodejs"
version = "0.6.0"
dependencies = [
"ironcalc",
"napi",
"napi-build",
"napi-derive",
"serde",
] ]
[[package]] [[package]]
@@ -412,6 +505,16 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.21" version = "0.4.21"
@@ -424,6 +527,15 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.2" version = "0.7.2"
@@ -433,6 +545,65 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "napi"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
"napi-derive",
"napi-sys",
"once_cell",
"serde",
"serde_json",
]
[[package]]
name = "napi-build"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
dependencies = [
"libloading",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -456,9 +627,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "parse-zoneinfo" name = "parse-zoneinfo"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [ dependencies = [
"regex", "regex",
] ]
@@ -530,6 +701,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "portable-atomic"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@@ -544,13 +721,86 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.79" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pyo3"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
dependencies = [
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "pyroncalc"
version = "0.6.0"
dependencies = [
"bitcode",
"ironcalc",
"pyo3",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.35"
@@ -613,6 +863,12 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-lite"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.3" version = "0.8.3"
@@ -625,6 +881,12 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.17" version = "1.0.17"
@@ -637,6 +899,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "semver"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.197"
@@ -679,17 +947,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_repr"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@@ -718,6 +975,16 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 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]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"
@@ -726,15 +993,21 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.58" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "target-lexicon"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.58" version = "1.0.58"
@@ -786,6 +1059,18 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unindent"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.8.0" version = "1.8.0"
@@ -810,7 +1095,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.1.3" version = "0.6.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",
@@ -821,23 +1106,24 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -858,9 +1144,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -868,9 +1154,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -881,9 +1167,12 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"

View File

@@ -5,10 +5,13 @@ members = [
"base", "base",
"xlsx", "xlsx",
"bindings/wasm", "bindings/wasm",
"bindings/python",
"bindings/nodejs",
] ]
exclude = [ exclude = [
"generate_locale", "generate_locale",
"webapp/app.ironcalc.com/server",
] ]
[profile.release] [profile.release]

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

@@ -1,41 +1,46 @@
.PHONY: lint
lint: lint:
cargo fmt -- --check cargo fmt -- --check
cargo clippy --all-targets --all-features cargo clippy --all-targets --all-features -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings
cd webapp/IronCalc/ && npm install && npm run check
cd webapp/app.ironcalc.com/frontend/ && npm install && npm run check
.PHONY: format
format: format:
cargo fmt cargo fmt
.PHONY: tests
tests: lint tests: lint
cargo test cargo test
./target/debug/documentation
cmp functions.md wiki/functions.md || exit 1
make remove-artifacts make remove-artifacts
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs # Regretabbly we need to build the wasm twice, once for the nodejs tests
# and a second one for the vitest.
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs && make
cd webapp/IronCalc/ && npm run test
cd bindings/python && ./run_tests.sh && ./run_examples.sh
.PHONY: remove-artifacts
remove-artifacts: remove-artifacts:
rm -f xlsx/hello-calc.xlsx rm -f xlsx/hello-calc.xlsx
rm -f xlsx/hello-styles.xlsx rm -f xlsx/hello-styles.xlsx
rm -f xlsx/widths-and-heights.xlsx rm -f xlsx/widths-and-heights.xlsx
rm -f functions.md
.PHONY: clean
clean: remove-artifacts clean: remove-artifacts
cargo clean cargo clean
rm -r -f base/target rm -r -f base/target
rm -r -f xlsx/target rm -r -f xlsx/target
rm -r -f bindings/python/target
rm -r -f bindings/wasm/targets
rm -f cargo-test-* rm -f cargo-test-*
rm -f base/cargo-test-* rm -f base/cargo-test-*
rm -f xlsx/cargo-test-* rm -f xlsx/cargo-test-*
.PHONY: coverage
coverage: coverage:
CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
update-docs: .PHONY: docs
cargo build
./target/debug/documentation -o wiki/functions.md
docs: docs:
cargo doc --no-deps cargo doc --no-deps
.PHONY: lint format tests docs coverage all

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. 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 # Building
@@ -77,14 +87,14 @@ And visit <http://0.0.0.0:8000/ironcalc/>
Add the dependency to `Cargo.toml`: Add the dependency to `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"} ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.5"}
``` ```
And then use this code in `main.rs`: And then use this code in `main.rs`:
```rust ```rust
use ironcalc::{ use ironcalc::{
base::{expressions::utils::number_to_column, model::Model}, base::{expressions::utils::number_to_column, Model},
export::save_to_xlsx, export::save_to_xlsx,
}; };
@@ -123,12 +133,12 @@ See https://github.com/ironcalc
An early preview of the technology running entirely in your browser: An early preview of the technology running entirely in your browser:
https://playground.ironcalc.com https://app.ironcalc.com
# Collaborators needed!. Call to action # Collaborators needed!. Call to action
We don't have a vibrant community just yet. This is the very stages of the project. But if you are passionate about code with high standards and no compromises, if you are looking for a project with high impact, if you are interested in a better, more open infrastructure for spreadsheets, whether you are a developer (rust, python, TypeScript, electron/tauri/anything else native app, React, you name it), a designer (we need a logo desperately!), an Excel power user who wants features, a business looking to integrate a MIT/Apache licensed spreadsheet in your own SaaS application join us! We don't have a vibrant community just yet. This is the very stages of the project. But if you are passionate about code with high standards and no compromises, if you are looking for a project with high impact, if you are interested in a better, more open infrastructure for spreadsheets, whether you are a developer (rust, python, TypeScript, electron/tauri/anything else native app, React, you name it), a designer, an Excel power user who wants features, a business looking to integrate a MIT/Apache licensed spreadsheet in your own SaaS application join us!
The best place to start will be to join or [discord channel](https://discord.gg/zZYWfh3RHJ) or send us an email at hello@ironcalc.com. The best place to start will be to join or [discord channel](https://discord.gg/zZYWfh3RHJ) or send us an email at hello@ironcalc.com.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
assets/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

BIN
assets/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/logo/png/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/logo/png/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,25 +1,33 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.1.3" version = "0.6.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021" edition = "2021"
homepage = "https://www.ironcalc.com" homepage = "https://www.ironcalc.com"
repository = "https://github.com/ironcalc/ironcalc/" repository = "https://github.com/ironcalc/ironcalc/"
description = "The democratization of spreadsheets" description = "Open source spreadsheet engine"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
ryu = "1.0" ryu = "1.0"
chrono = "0.4" chrono = "0.4"
chrono-tz = "0.9" chrono-tz = "0.10"
regex = "1.0" regex = { version = "1.0", optional = true}
once_cell = "1.16.0" regex-lite = { version = "0.1.6", optional = true}
bitcode = "0.6.0" bitcode = "0.6.3"
csv = "1.3.0"
statrs = { version = "0.18.0", default-features = false, features = [] }
[features]
default = ["use_regex_full"]
use_regex_full = ["regex"]
use_regex_lite = ["regex-lite"]
[dev-dependencies]
serde_json = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.69" }

View File

@@ -3,15 +3,15 @@ use ironcalc_base::{types::CellType, Model};
fn main() -> Result<(), Box<dyn std::error::Error>> { 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")?;
// A1 // A1
model.set_user_input(0, 1, 1, "1".to_string()); model.set_user_input(0, 1, 1, "1".to_string())?;
// A2 // A2
model.set_user_input(0, 2, 1, "2".to_string()); model.set_user_input(0, 2, 1, "2".to_string())?;
// A3 // A3
model.set_user_input(0, 3, 1, "3".to_string()); model.set_user_input(0, 3, 1, "3".to_string())?;
// B1 // B1
model.set_user_input(0, 1, 2, "=SUM(A1:A3)".to_string()); model.set_user_input(0, 1, 2, "=SUM(A1:A3)".to_string())?;
// B2 // B2
model.set_user_input(0, 2, 2, "=B1/0".to_string()); model.set_user_input(0, 2, 2, "=B1/0".to_string())?;
// Evaluate // Evaluate
model.evaluate(); model.evaluate();

View File

@@ -3,11 +3,11 @@ use ironcalc_base::{cell::CellValue, Model};
fn main() -> Result<(), Box<dyn std::error::Error>> { 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")?;
// A1 // A1
model.set_user_input(0, 1, 1, "Hello".to_string()); model.set_user_input(0, 1, 1, "Hello".to_string())?;
// B1 // B1
model.set_user_input(0, 1, 2, "world!".to_string()); model.set_user_input(0, 1, 2, "world!".to_string())?;
// C1 // C1
model.set_user_input(0, 1, 3, "=CONCAT(A1, \" \", B1".to_string()); model.set_user_input(0, 1, 3, "=CONCAT(A1, \" \", B1".to_string())?;
// evaluates // evaluates
model.evaluate(); model.evaluate();

View File

@@ -1,5 +1,6 @@
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::stringify::DisplaceData; use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData};
use crate::expressions::types::CellReferenceRC;
use crate::model::Model; use crate::model::Model;
// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns // NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
@@ -8,16 +9,45 @@ use crate::model::Model;
// I feel this is unimportant for now. // I feel this is unimportant for now.
impl Model { impl Model {
fn shift_cell_formula(
&mut self,
sheet: u32,
row: i32,
column: i32,
displace_data: &DisplaceData,
) -> Result<(), String> {
if let Some(f) = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.and_then(|c| c.get_formula())
{
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row,
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
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}"))?;
}
}
Ok(())
}
/// This function iterates over all cells in the model and shifts their formulas according to the displacement data. /// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude. /// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
fn displace_cells(&mut self, displace_data: &DisplaceData) { fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> {
let cells = self.get_all_cells(); let cells = self.get_all_cells();
for cell in cells { for cell in cells {
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data); self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?;
} }
Ok(())
} }
/// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order. /// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
@@ -79,12 +109,11 @@ impl Model {
let formula_or_value = self let formula_or_value = self
.get_cell_formula(sheet, source_row, source_column)? .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_text(&self.workbook.shared_strings, &self.language));
self.set_user_input(sheet, target_row, target_column, formula_or_value); self.set_user_input(sheet, target_row, target_column, formula_or_value)?;
self.workbook self.workbook
.worksheet_mut(sheet)? .worksheet_mut(sheet)?
.set_cell_style(target_row, target_column, style); .set_cell_style(target_row, target_column, style)?;
self.cell_clear_all(sheet, source_row, source_column)?; self.cell_clear_all(sheet, source_row, source_column)
Ok(())
} }
/// Inserts one or more new columns into the model at the specified index. /// Inserts one or more new columns into the model at the specified index.
@@ -135,7 +164,34 @@ impl Model {
column, column,
delta: column_count, delta: column_count,
}), }),
); )?;
// In the list of columns:
// * Keep all the columns to the left
// * Displace all the columns to the right
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_columns = Vec::new();
for col in worksheet.cols.iter_mut() {
// range under study
let min = col.min;
let max = col.max;
if column > max {
// If the range under study is to our left, this is a noop
} else if column <= min {
// If the range under study is to our right, we displace it
col.min = min + column_count;
col.max = max + column_count;
} else {
// If the range under study is in the middle we augment it
col.max = max + column_count;
}
new_columns.push(col.clone());
}
// TODO: If in a row the cell to the right and left have the same style we should copy it
worksheet.cols = new_columns;
Ok(()) Ok(())
} }
@@ -156,6 +212,12 @@ impl Model {
if column_count <= 0 { if column_count <= 0 {
return Err("Please use insert columns instead".to_string()); 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 // first column being deleted
let column_start = column; let column_start = column;
@@ -188,7 +250,7 @@ impl Model {
column, column,
delta: -column_count, delta: -column_count,
}), }),
); )?;
let worksheet = &mut self.workbook.worksheet_mut(sheet)?; let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
// deletes all the column styles // deletes all the column styles
@@ -312,7 +374,7 @@ impl Model {
row, row,
delta: row_count, delta: row_count,
}), }),
); )?;
Ok(()) Ok(())
} }
@@ -328,6 +390,13 @@ impl Model {
if row_count <= 0 { if row_count <= 0 {
return Err("Please use insert rows instead".to_string()); 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 // Move cells
let worksheet = &self.workbook.worksheet(sheet)?; let worksheet = &self.workbook.worksheet(sheet)?;
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect(); let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
@@ -373,7 +442,7 @@ impl Model {
row, row,
delta: -row_count, delta: -row_count,
}), }),
); )?;
Ok(()) Ok(())
} }
@@ -388,23 +457,86 @@ impl Model {
/// * Column is one of the extremes of the range. The new extreme would be target_column. /// * Column is one of the extremes of the range. The new extreme would be target_column.
/// Range is then normalized /// Range is then normalized
/// * Any other case, range is left unchanged. /// * 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( pub fn move_column_action(
&mut self, &mut self,
sheet: u32, sheet: u32,
column: i32, column: i32,
delta: i32, delta: i32,
) -> Result<(), &'static str> { ) -> Result<(), String> {
// Check boundaries // Check boundaries
let target_column = column + delta; let target_column = column + delta;
if !(1..=LAST_COLUMN).contains(&target_column) { if !(1..=LAST_COLUMN).contains(&target_column) {
return Err("Target column out of boundaries"); return Err("Target column out of boundaries".to_string());
} }
if !(1..=LAST_COLUMN).contains(&column) { if !(1..=LAST_COLUMN).contains(&column) {
return Err("Initial column out of boundaries"); 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_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 // Update all formulas in the workbook
self.displace_cells( self.displace_cells(
@@ -413,7 +545,91 @@ impl Model {
column, column,
delta, delta,
}), }),
); )?;
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_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(()) Ok(())
} }

158
base/src/arithmetic.rs Normal file
View File

@@ -0,0 +1,158 @@
use crate::{
calc_result::CalcResult,
cast::NumberOrArray,
expressions::{
parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
model::Model,
};
/// Unify how we map booleans/strings to f64
fn to_f64(value: &ArrayNode) -> Result<f64, Error> {
match value {
ArrayNode::Number(f) => Ok(*f),
ArrayNode::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
ArrayNode::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(f),
Err(_) => Err(Error::VALUE),
},
ArrayNode::Error(err) => Err(err.clone()),
}
}
impl Model {
/// Applies `op` elementwise for arrays/numbers.
pub(crate) fn handle_arithmetic(
&mut self,
left: &Node,
right: &Node,
cell: CellReferenceIndex,
op: &dyn Fn(f64, f64) -> Result<f64, Error>,
) -> CalcResult {
let l = match self.get_number_or_array(left, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number_or_array(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
match (l, r) {
// -----------------------------------------------------
// Case 1: Both are numbers
// -----------------------------------------------------
(NumberOrArray::Number(f1), NumberOrArray::Number(f2)) => match op(f1, f2) {
Ok(x) => CalcResult::Number(x),
Err(Error::DIV) => CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Divide by 0".to_string(),
},
Err(Error::VALUE) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid number".to_string(),
},
Err(e) => CalcResult::Error {
error: e,
origin: cell,
message: "Unknown error".to_string(),
},
},
// -----------------------------------------------------
// Case 2: left is Number, right is Array
// -----------------------------------------------------
(NumberOrArray::Number(f1), NumberOrArray::Array(a2)) => {
let mut array = Vec::new();
for row in a2 {
let mut data_row = Vec::new();
for node in row {
match to_f64(&node) {
Ok(f2) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
Err(e) => data_row.push(ArrayNode::Error(e)),
},
Err(err) => data_row.push(ArrayNode::Error(err)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------------------
// Case 3: left is Array, right is Number
// -----------------------------------------------------
(NumberOrArray::Array(a1), NumberOrArray::Number(f2)) => {
let mut array = Vec::new();
for row in a1 {
let mut data_row = Vec::new();
for node in row {
match to_f64(&node) {
Ok(f1) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
Err(e) => data_row.push(ArrayNode::Error(e)),
},
Err(err) => data_row.push(ArrayNode::Error(err)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------------------
// Case 4: Both are arrays
// -----------------------------------------------------
(NumberOrArray::Array(a1), NumberOrArray::Array(a2)) => {
let n1 = a1.len();
let m1 = a1.first().map(|r| r.len()).unwrap_or(0);
let n2 = a2.len();
let m2 = a2.first().map(|r| r.len()).unwrap_or(0);
let n = n1.max(n2);
let m = m1.max(m2);
let mut array = Vec::new();
for i in 0..n {
let row1 = a1.get(i);
let row2 = a2.get(i);
let mut data_row = Vec::new();
for j in 0..m {
let val1 = row1.and_then(|r| r.get(j));
let val2 = row2.and_then(|r| r.get(j));
match (val1, val2) {
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
(Ok(f1), Ok(f2)) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
},
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),
},
// Mismatched dimensions => #VALUE!
_ => data_row.push(ArrayNode::Error(Error::VALUE)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
}
}
}

View File

@@ -1,7 +1,8 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::expressions::{token::Error, types::CellReferenceIndex}; use crate::expressions::{parser::ArrayNode, token::Error, types::CellReferenceIndex};
#[derive(Clone)]
pub struct Range { pub struct Range {
pub left: CellReferenceIndex, pub left: CellReferenceIndex,
pub right: CellReferenceIndex, pub right: CellReferenceIndex,
@@ -23,6 +24,7 @@ pub(crate) enum CalcResult {
}, },
EmptyCell, EmptyCell,
EmptyArg, EmptyArg,
Array(Vec<Vec<ArrayNode>>),
} }
impl CalcResult { impl CalcResult {

View File

@@ -1,11 +1,103 @@
use crate::{ use crate::{
calc_result::{CalcResult, Range}, calc_result::{CalcResult, Range},
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{
implicit_intersection::implicit_intersection, parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
formatter::format::parse_formatted_number,
model::Model, model::Model,
}; };
pub(crate) enum NumberOrArray {
Number(f64),
Array(Vec<Vec<ArrayNode>>),
}
impl Model { impl Model {
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);
}
// Try to parse as a formatted number (e.g., dates, currencies, percentages)
if let Ok((v, _number_format)) = parse_formatted_number(s, &currencies) {
return Some(v);
}
None
}
}
}
pub(crate) fn get_number_or_array(
&mut self,
node: &Node,
cell: CellReferenceIndex,
) -> Result<NumberOrArray, CalcResult> {
match self.evaluate_node_in_context(node, cell) {
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
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(),
)),
},
CalcResult::Boolean(f) => {
if f {
Ok(NumberOrArray::Number(1.0))
} else {
Ok(NumberOrArray::Number(0.0))
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(NumberOrArray::Number(0.0)),
CalcResult::Range { left, right } => {
let sheet = left.sheet;
if sheet != right.sheet {
return Err(CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "3D ranges are not allowed".to_string(),
});
}
// we need to convert the range into an array
let mut array = Vec::new();
for row in left.row..=right.row {
let mut row_data = Vec::new();
for column in left.column..=right.column {
let value =
match self.evaluate_cell(CellReferenceIndex { sheet, column, row }) {
CalcResult::String(s) => ArrayNode::String(s),
CalcResult::Number(f) => ArrayNode::Number(f),
CalcResult::Boolean(b) => ArrayNode::Boolean(b),
CalcResult::Error { error, .. } => ArrayNode::Error(error),
CalcResult::Range { .. } => {
// if we do things right this can never happen.
// the evaluation of a cell should never return a range
ArrayNode::Number(0.0)
}
CalcResult::EmptyCell => ArrayNode::Number(0.0),
CalcResult::EmptyArg => ArrayNode::Number(0.0),
CalcResult::Array(_) => {
// if we do things right this can never happen.
// the evaluation of a cell should never return an array
ArrayNode::Number(0.0)
}
};
row_data.push(value);
}
array.push(row_data);
}
Ok(NumberOrArray::Array(array))
}
CalcResult::Array(s) => Ok(NumberOrArray::Array(s)),
error @ CalcResult::Error { .. } => Err(error),
}
}
pub(crate) fn get_number( pub(crate) fn get_number(
&mut self, &mut self,
node: &Node, node: &Node,
@@ -15,16 +107,16 @@ impl Model {
self.cast_to_number(result, cell) self.cast_to_number(result, cell)
} }
fn cast_to_number( pub(crate) fn cast_to_number(
&mut self, &mut self,
result: CalcResult, result: CalcResult,
cell: CellReferenceIndex, cell: CellReferenceIndex,
) -> Result<f64, CalcResult> { ) -> Result<f64, CalcResult> {
match result { match result {
CalcResult::Number(f) => Ok(f), CalcResult::Number(f) => Ok(f),
CalcResult::String(s) => match s.parse::<f64>() { CalcResult::String(s) => match self.cast_number(&s) {
Ok(f) => Ok(f), Some(f) => Ok(f),
_ => Err(CalcResult::new_error( None => Err(CalcResult::new_error(
Error::VALUE, Error::VALUE,
cell, cell,
"Expecting number".to_string(), "Expecting number".to_string(),
@@ -39,19 +131,16 @@ impl Model {
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => { CalcResult::Range { .. } => Err(CalcResult::Error {
match implicit_intersection(&cell, &Range { left, right }) { error: Error::NIMPL,
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_number(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell, origin: cell,
message: "Invalid reference (number)".to_string(), message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}), }),
}
}
} }
} }
@@ -88,7 +177,7 @@ impl Model {
// FIXME: I think when casting a number we should convert it to_precision(x, 15) // FIXME: I think when casting a number we should convert it to_precision(x, 15)
// See function Exact // See function Exact
match result { match result {
CalcResult::Number(f) => Ok(format!("{}", f)), CalcResult::Number(f) => Ok(format!("{f}")),
CalcResult::String(s) => Ok(s), CalcResult::String(s) => Ok(s),
CalcResult::Boolean(f) => { CalcResult::Boolean(f) => {
if f { if f {
@@ -99,19 +188,16 @@ impl Model {
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => { CalcResult::Range { .. } => Err(CalcResult::Error {
match implicit_intersection(&cell, &Range { left, right }) { error: Error::NIMPL,
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_string(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell, origin: cell,
message: "Invalid reference (string)".to_string(), message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}), }),
}
}
} }
} }
@@ -151,19 +237,16 @@ impl Model {
CalcResult::Boolean(b) => Ok(b), CalcResult::Boolean(b) => Ok(b),
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => { CalcResult::Range { .. } => Err(CalcResult::Error {
match implicit_intersection(&cell, &Range { left, right }) { error: Error::NIMPL,
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_bool(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell, origin: cell,
message: "Invalid reference (bool)".to_string(), message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}), }),
}
}
} }
} }

View File

@@ -1,12 +1,9 @@
use crate::{ use crate::{
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*, expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
}; };
use serde::{Deserialize, Serialize};
use serde_json::json;
/// A CellValue is the representation of the cell content. /// A CellValue is the representation of the cell content.
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Debug, PartialEq)]
#[serde(untagged)]
pub enum CellValue { pub enum CellValue {
None, None,
String(String), String(String),
@@ -14,17 +11,6 @@ pub enum CellValue {
Boolean(bool), Boolean(bool),
} }
impl CellValue {
pub fn to_json_str(&self) -> String {
match &self {
CellValue::None => "null".to_string(),
CellValue::String(s) => json!(s).to_string(),
CellValue::Number(f) => json!(f).to_string(),
CellValue::Boolean(b) => json!(b).to_string(),
}
}
}
impl From<f64> for CellValue { impl From<f64> for CellValue {
fn from(value: f64) -> Self { fn from(value: f64) -> Self {
Self::Number(value) Self::Number(value)

View File

@@ -2,10 +2,12 @@
/// COLUMN_WIDTH and ROW_HEIGHT are pixel values /// COLUMN_WIDTH and ROW_HEIGHT are pixel values
/// A column width of Excel value `w` will result in `w * COLUMN_WIDTH_FACTOR` pixels /// A column width of Excel value `w` will result in `w * COLUMN_WIDTH_FACTOR` pixels
/// Note that these constants are inlined /// Note that these constants are inlined
pub(crate) const DEFAULT_COLUMN_WIDTH: f64 = 100.0; pub(crate) const DEFAULT_COLUMN_WIDTH: f64 = 125.0;
pub(crate) const DEFAULT_ROW_HEIGHT: f64 = 21.0; pub(crate) const DEFAULT_ROW_HEIGHT: f64 = 28.0;
pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0; pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0;
pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0; pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0;
pub(crate) const DEFAULT_WINDOW_HEIGHT: i64 = 600;
pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
pub(crate) const LAST_COLUMN: i32 = 16_384; pub(crate) const LAST_COLUMN: i32 = 16_384;
pub(crate) const LAST_ROW: i32 = 1_048_576; pub(crate) const LAST_ROW: i32 = 1_048_576;
@@ -14,3 +16,10 @@ pub(crate) const LAST_ROW: i32 = 1_048_576;
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2 // NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug // The 2 days offset is because of Excel 1900 bug
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594; pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
// We do not support dates before 1899-12-31.
pub(crate) const MINIMUM_DATE_SERIAL_NUMBER: i32 = 1;
// Excel can handle dates until the year 9999-12-31
// 2958465 is the number of days from 1900-01-01 to 9999-12-31
pub(crate) const MAXIMUM_DATE_SERIAL_NUMBER: i32 = 2_958_465;

View File

@@ -1,136 +0,0 @@
use crate::{
expressions::{
parser::{
move_formula::ref_is_in_area,
stringify::{to_string, to_string_displaced, DisplaceData},
walk::forward_references,
},
types::{Area, CellReferenceIndex, CellReferenceRC},
},
model::Model,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged, deny_unknown_fields)]
pub enum CellValue {
Value(String),
None,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SetCellValue {
cell: CellReferenceIndex,
new_value: CellValue,
old_value: CellValue,
}
impl Model {
pub(crate) fn shift_cell_formula(
&mut self,
sheet: u32,
row: i32,
column: i32,
displace_data: &DisplaceData,
) {
if let Some(f) = self
.workbook
.worksheet(sheet)
.expect("Worksheet must exist")
.cell(row, column)
.expect("Cell must exist")
.get_formula()
{
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row,
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
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}"))
.expect("Failed to shift cell formula");
}
}
}
pub fn forward_references(
&mut self,
source_area: &Area,
target: &CellReferenceIndex,
) -> Result<Vec<SetCellValue>, String> {
let mut diff_list: Vec<SetCellValue> = Vec::new();
let target_area = &Area {
sheet: target.sheet,
row: target.row,
column: target.column,
width: source_area.width,
height: source_area.height,
};
// Walk over every formula
let cells = self.get_all_cells();
for cell in cells {
if let Some(f) = self
.workbook
.worksheet(cell.index)
.expect("Worksheet must exist")
.cell(cell.row, cell.column)
.expect("Cell must exist")
.get_formula()
{
let sheet = cell.index;
let row = cell.row;
let column = cell.column;
// If cell is in the source or target area, skip
if ref_is_in_area(sheet, row, column, source_area)
|| ref_is_in_area(sheet, row, column, target_area)
{
continue;
}
// Get the formula
// Get a copy of the AST
let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
column: cell.column,
row: cell.row,
};
let context = CellReferenceIndex { sheet, column, row };
let formula = to_string(node, &cell_reference);
let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
forward_references(
node,
&context,
source_area,
target.sheet,
target_sheet_name,
target.row,
target.column,
);
// If the string representation of the formula has changed update the cell
let updated_formula = to_string(node, &cell_reference);
if formula != updated_formula {
self.update_cell_with_formula(
sheet,
row,
column,
format!("={updated_formula}"),
)?;
// Update the diff list
diff_list.push(SetCellValue {
cell: CellReferenceIndex { sheet, column, row },
new_value: CellValue::Value(format!("={}", updated_formula)),
old_value: CellValue::Value(format!("={}", formula)),
});
}
}
}
Ok(diff_list)
}
}

View File

@@ -142,7 +142,7 @@ impl Lexer {
pub fn expect(&mut self, tk: TokenType) -> Result<()> { pub fn expect(&mut self, tk: TokenType) -> Result<()> {
let nt = self.next_token(); let nt = self.next_token();
if mem::discriminant(&nt) != mem::discriminant(&tk) { 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(()) Ok(())
} }
@@ -187,6 +187,7 @@ impl Lexer {
']' => TokenType::RightBracket, ']' => TokenType::RightBracket,
':' => TokenType::Colon, ':' => TokenType::Colon,
';' => TokenType::Semicolon, ';' => TokenType::Semicolon,
'@' => TokenType::At,
',' => { ',' => {
if self.locale.numbers.symbols.decimal == "," { if self.locale.numbers.symbols.decimal == "," {
match self.consume_number(',') { match self.consume_number(',') {
@@ -308,11 +309,14 @@ impl Lexer {
return self.consume_range(None); return self.consume_range(None);
} }
let name_upper = name.to_ascii_uppercase(); let name_upper = name.to_ascii_uppercase();
if name_upper == self.language.booleans.true_value { if name_upper == self.language.booleans.r#true {
return TokenType::Boolean(true); return TokenType::Boolean(true);
} else if name_upper == self.language.booleans.false_value { } else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false); return TokenType::Boolean(false);
} }
if self.peek_char() == Some('(') {
return TokenType::Ident(name);
}
if self.mode == LexerMode::A1 { if self.mode == LexerMode::A1 {
let parsed_reference = utils::parse_reference_a1(&name_upper); let parsed_reference = utils::parse_reference_a1(&name_upper);
if parsed_reference.is_some() if parsed_reference.is_some()
@@ -510,7 +514,7 @@ impl Lexer {
self.position = position; self.position = position;
chars.parse::<i32>().map_err(|_| LexerError { chars.parse::<i32>().map_err(|_| LexerError {
position, position,
message: format!("Failed to parse to int: {}", chars), message: format!("Failed to parse to int: {chars}"),
}) })
} }
@@ -571,9 +575,7 @@ impl Lexer {
} }
self.position = position; self.position = position;
match chars.parse::<f64>() { match chars.parse::<f64>() {
Err(_) => { Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)),
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
}
Ok(v) => Ok(v), Ok(v) => Ok(v),
} }
} }
@@ -660,8 +662,8 @@ impl Lexer {
fn consume_error(&mut self) -> TokenType { fn consume_error(&mut self) -> TokenType {
let errors = &self.language.errors; let errors = &self.language.errors;
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect(); let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
if rest_of_formula.starts_with(&errors.ref_value) { if rest_of_formula.starts_with(&errors.r#ref) {
self.position += errors.ref_value.chars().count() - 1; self.position += errors.r#ref.chars().count() - 1;
return TokenType::Error(Error::REF); return TokenType::Error(Error::REF);
} else if rest_of_formula.starts_with(&errors.name) { } else if rest_of_formula.starts_with(&errors.name) {
self.position += errors.name.chars().count() - 1; self.position += errors.name.chars().count() - 1;

View File

@@ -148,15 +148,16 @@ impl Lexer {
let row_left = match row_left.parse::<i32>() { let row_left = match row_left.parse::<i32>() {
Ok(n) => n, Ok(n) => n,
Err(_) => { Err(_) => {
return Err(self return Err(
.set_error(&format!("Failed parsing row {}", row_left), position)) self.set_error(&format!("Failed parsing row {row_left}"), position)
)
} }
}; };
let row_right = match row_right.parse::<i32>() { let row_right = match row_right.parse::<i32>() {
Ok(n) => n, Ok(n) => n,
Err(_) => { Err(_) => {
return Err(self 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 { if row_left > LAST_ROW {

View File

@@ -23,19 +23,19 @@ impl Lexer {
// TODO(TD): There are better ways of doing this :) // TODO(TD): There are better ways of doing this :)
let rest_of_formula: String = self.chars[self.position..self.len].iter().collect(); let rest_of_formula: String = self.chars[self.position..self.len].iter().collect();
let specifier = if rest_of_formula.starts_with("#This Row]") { let specifier = if rest_of_formula.starts_with("#This Row]") {
self.position += "#This Row]".bytes().len(); self.position += "#This Row]".len();
TableSpecifier::ThisRow TableSpecifier::ThisRow
} else if rest_of_formula.starts_with("#All]") { } else if rest_of_formula.starts_with("#All]") {
self.position += "#All]".bytes().len(); self.position += "#All]".len();
TableSpecifier::All TableSpecifier::All
} else if rest_of_formula.starts_with("#Data]") { } else if rest_of_formula.starts_with("#Data]") {
self.position += "#Data]".bytes().len(); self.position += "#Data]".len();
TableSpecifier::Data TableSpecifier::Data
} else if rest_of_formula.starts_with("#Headers]") { } else if rest_of_formula.starts_with("#Headers]") {
self.position += "#Headers]".bytes().len(); self.position += "#Headers]".len();
TableSpecifier::Headers TableSpecifier::Headers
} else if rest_of_formula.starts_with("#Totals]") { } else if rest_of_formula.starts_with("#Totals]") {
self.position += "#Totals]".bytes().len(); self.position += "#Totals]".len();
TableSpecifier::Totals TableSpecifier::Totals
} else { } else {
return Err(LexerError { return Err(LexerError {

View File

@@ -1,4 +1,5 @@
mod test_common; mod test_common;
mod test_implicit_intersection;
mod test_language; mod test_language;
mod test_locale; mod test_locale;
mod test_ranges; mod test_ranges;

View File

@@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::expressions::utils::column_to_number;
use crate::language::get_language; use crate::language::get_language;
use crate::locale::get_locale; use crate::locale::get_locale;
@@ -685,3 +686,29 @@ fn test_comparisons() {
assert_eq!(lx.next_token(), Number(7.0)); assert_eq!(lx.next_token(), Number(7.0));
assert_eq!(lx.next_token(), EOF); 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

@@ -0,0 +1,25 @@
#![allow(clippy::unwrap_used)]
use crate::expressions::{
lexer::{Lexer, LexerMode},
token::TokenType::*,
};
use crate::language::get_language;
use crate::locale::get_locale;
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)
}
#[test]
fn sum_implicit_intersection() {
let mut lx = new_lexer("sum(@A1:A3)");
assert_eq!(lx.next_token(), Ident("sum".to_string()));
assert_eq!(lx.next_token(), LeftParenthesis);
assert_eq!(lx.next_token(), At);
assert!(matches!(lx.next_token(), Range { .. }));
assert_eq!(lx.next_token(), RightParenthesis);
assert_eq!(lx.next_token(), EOF);
}

View File

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

View File

@@ -1,3 +1,5 @@
#![allow(clippy::expect_used)]
use crate::expressions::{ use crate::expressions::{
lexer::util::get_tokens, lexer::util::get_tokens,
token::{OpCompare, OpSum, TokenType}, token::{OpCompare, OpSum, TokenType},
@@ -22,6 +24,25 @@ fn test_get_tokens() {
assert_eq!(l.end, 10); assert_eq!(l.end, 10);
} }
#[test]
fn get_tokens_unicode() {
let formula = "'🇵🇭 Philippines'!A1";
let t = get_tokens(formula);
assert_eq!(t.len(), 1);
let expected = TokenType::Reference {
sheet: Some("🇵🇭 Philippines".to_string()),
row: 1,
column: 1,
absolute_column: false,
absolute_row: false,
};
let l = t.first().expect("expected token");
assert_eq!(l.token, expected);
assert_eq!(l.start, 0);
assert_eq!(l.end, 19);
}
#[test] #[test]
fn test_simple_tokens() { fn test_simple_tokens() {
assert_eq!( assert_eq!(

View File

@@ -52,7 +52,9 @@ pub fn get_tokens(formula: &str) -> Vec<MarkedToken> {
let mut lexer = Lexer::new( let mut lexer = Lexer::new(
formula, formula,
LexerMode::A1, LexerMode::A1,
#[allow(clippy::expect_used)]
get_locale("en").expect(""), get_locale("en").expect(""),
#[allow(clippy::expect_used)]
get_language("en").expect(""), get_language("en").expect(""),
); );
let mut start = lexer.get_position(); let mut start = lexer.get_position();

View File

@@ -1,5 +1,5 @@
/*! /*!
# GRAMAR # GRAMMAR
<pre class="rust"> <pre class="rust">
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>' opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
@@ -12,7 +12,8 @@ term => factor (opFactor factor)*
factor => prod (opProd prod)* factor => prod (opProd prod)*
prod => power ('^' power)* prod => power ('^' power)*
power => (unaryOp)* range '%'* power => (unaryOp)* range '%'*
range => primary (':' primary)? range => implicit (':' primary)?
implicit=> '@' primary | primary
primary => '(' expr ')' primary => '(' expr ')'
=> number => number
=> function '(' f_args ')' => function '(' f_args ')'
@@ -45,25 +46,19 @@ use super::utils::number_to_column;
use token::OpCompare; use token::OpCompare;
pub mod move_formula; pub mod move_formula;
pub mod static_analysis;
pub mod stringify; pub mod stringify;
pub mod walk;
#[cfg(test)] #[cfg(test)]
mod test; mod tests;
#[cfg(test)]
mod test_ranges;
#[cfg(test)]
mod test_move_formula;
#[cfg(test)]
mod test_tables;
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> { pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
let mut lexer = lexer::Lexer::new( let mut lexer = lexer::Lexer::new(
formula, formula,
lexer::LexerMode::A1, lexer::LexerMode::A1,
#[allow(clippy::expect_used)]
get_locale("en").expect(""), get_locale("en").expect(""),
#[allow(clippy::expect_used)]
get_language("en").expect(""), get_language("en").expect(""),
); );
if let TokenType::Range { if let TokenType::Range {
@@ -87,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
None None
} }
// DefinedNameS is a tuple with the name of the defined name, the index of the sheet and the formula
pub type DefinedNameS = (String, Option<u32>, String);
pub(crate) struct Reference<'a> { pub(crate) struct Reference<'a> {
sheet_name: &'a Option<String>, sheet_name: &'a Option<String>,
sheet_index: u32, sheet_index: u32,
@@ -96,6 +94,14 @@ pub(crate) struct Reference<'a> {
column: i32, column: i32,
} }
#[derive(PartialEq, Clone, Debug)]
pub enum ArrayNode {
Boolean(bool),
Number(f64),
String(String),
Error(token::Error),
}
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub enum Node { pub enum Node {
BooleanKind(bool), BooleanKind(bool),
@@ -169,8 +175,14 @@ pub enum Node {
name: String, name: String,
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Node>), ArrayKind(Vec<Vec<ArrayNode>>),
VariableKind(String), DefinedNameKind(DefinedNameS),
TableNameKind(String),
WrongVariableKind(String),
ImplicitIntersection {
automatic: bool,
child: Box<Node>,
},
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -193,22 +205,35 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
context: Option<CellReferenceRC>, defined_names: Vec<DefinedNameS>,
context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
impl Parser { impl Parser {
pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser { pub fn new(
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
"", "",
lexer::LexerMode::A1, lexer::LexerMode::A1,
#[allow(clippy::expect_used)]
get_locale("en").expect(""), get_locale("en").expect(""),
#[allow(clippy::expect_used)]
get_language("en").expect(""), get_language("en").expect(""),
); );
let context = CellReferenceRC {
sheet: worksheets.first().map_or("", |v| v).to_string(),
column: 1,
row: 1,
};
Parser { Parser {
lexer, lexer,
worksheets, worksheets,
context: None, defined_names,
context,
tables, tables,
} }
} }
@@ -216,11 +241,16 @@ impl Parser {
self.lexer.set_lexer_mode(mode) self.lexer.set_lexer_mode(mode)
} }
pub fn set_worksheets(&mut self, worksheets: Vec<String>) { pub fn set_worksheets_and_names(
&mut self,
worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>,
) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names;
} }
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node { pub fn parse(&mut self, formula: &str, context: &CellReferenceRC) -> Node {
self.lexer.set_formula(formula); self.lexer.set_formula(formula);
self.context = context.clone(); self.context = context.clone();
self.parse_expr() self.parse_expr()
@@ -236,6 +266,24 @@ impl Parser {
None None
} }
// Returns:
// * None: If there is no defined name by that name
// * Some((Some(index), formula)): If there is a defined name local to that sheet
// * Some(None): If there is a global defined name
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some((*df_scope, df_formula.to_owned()));
}
}
for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some((None, df_formula.to_owned()));
}
}
None
}
fn parse_expr(&mut self) -> Node { fn parse_expr(&mut self) -> Node {
let mut t = self.parse_concat(); let mut t = self.parse_concat();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
@@ -379,7 +427,7 @@ impl Parser {
} }
fn parse_range(&mut self) -> Node { fn parse_range(&mut self) -> Node {
let t = self.parse_primary(); let t = self.parse_implicit();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
return t; return t;
} }
@@ -398,6 +446,65 @@ impl Parser {
t t
} }
fn parse_implicit(&mut self) -> Node {
let next_token = self.lexer.peek_token();
if next_token == TokenType::At {
self.lexer.advance_token();
let t = self.parse_primary();
if let Node::ParseErrorKind { .. } = t {
return t;
}
return Node::ImplicitIntersection {
automatic: false,
child: Box::new(t),
};
}
self.parse_primary()
}
fn parse_array_row(&mut self) -> Result<Vec<ArrayNode>, Node> {
let mut row = Vec::new();
// and array can only have numbers, string or booleans
// otherwise it is a syntax error
let first_element = 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),
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
};
row.push(first_element);
let mut next_token = self.lexer.peek_token();
// FIXME: this is not respecting the locale
while next_token == TokenType::Comma {
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),
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
};
row.push(value);
next_token = self.lexer.peek_token();
}
Ok(row)
}
fn parse_primary(&mut self) -> Node { fn parse_primary(&mut self) -> Node {
let next_token = self.lexer.next_token(); let next_token = self.lexer.next_token();
match next_token { match next_token {
@@ -419,21 +526,35 @@ impl Parser {
TokenType::Number(s) => Node::NumberKind(s), TokenType::Number(s) => Node::NumberKind(s),
TokenType::String(s) => Node::StringKind(s), TokenType::String(s) => Node::StringKind(s),
TokenType::LeftBrace => { TokenType::LeftBrace => {
let t = self.parse_expr(); // It's an array. It's a collection of rows all of the same dimension
if let Node::ParseErrorKind { .. } = t {
return t; let first_row = match self.parse_array_row() {
} Ok(s) => s,
Err(error) => return error,
};
let length = first_row.len();
let mut matrix = Vec::new();
matrix.push(first_row);
// FIXME: this is not respecting the locale
let mut next_token = self.lexer.peek_token(); let mut next_token = self.lexer.peek_token();
let mut args: Vec<Node> = vec![t];
while next_token == TokenType::Semicolon { while next_token == TokenType::Semicolon {
self.lexer.advance_token(); self.lexer.advance_token();
let p = self.parse_expr(); let row = match self.parse_array_row() {
if let Node::ParseErrorKind { .. } = p { Ok(s) => s,
return p; Err(error) => return error,
} };
next_token = self.lexer.peek_token(); next_token = self.lexer.peek_token();
args.push(p); if row.len() != length {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "All rows in an array should be the same length".to_string(),
};
} }
matrix.push(row);
}
if let Err(err) = self.lexer.expect(TokenType::RightBrace) { if let Err(err) = self.lexer.expect(TokenType::RightBrace) {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
@@ -441,7 +562,7 @@ impl Parser {
message: err.message, message: err.message,
}; };
} }
Node::ArrayKind(args) Node::ArrayKind(matrix)
} }
TokenType::Reference { TokenType::Reference {
sheet, sheet,
@@ -450,16 +571,7 @@ impl Parser {
absolute_column, absolute_column,
absolute_row, absolute_row,
} => { } => {
let context = match &self.context { let context = &self.context;
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -494,16 +606,7 @@ impl Parser {
} }
} }
TokenType::Range { sheet, left, right } => { TokenType::Range { sheet, left, right } => {
let context = match &self.context { let context = &self.context;
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -518,20 +621,6 @@ impl Parser {
let mut absolute_row1 = left.absolute_row; let mut absolute_row1 = left.absolute_row;
let mut absolute_row2 = right.absolute_row; let mut absolute_row2 = right.absolute_row;
if self.lexer.is_a1_mode() {
if !left.absolute_row {
row1 -= context.row
};
if !left.absolute_column {
column1 -= context.column
};
if !right.absolute_row {
row2 -= context.row
};
if !right.absolute_column {
column2 -= context.column
};
}
if row1 > row2 { if row1 > row2 {
(row2, row1) = (row1, row2); (row2, row1) = (row1, row2);
(absolute_row2, absolute_row1) = (absolute_row1, absolute_row2); (absolute_row2, absolute_row1) = (absolute_row1, absolute_row2);
@@ -540,6 +629,22 @@ impl Parser {
(column2, column1) = (column1, column2); (column2, column1) = (column1, column2);
(absolute_column2, absolute_column1) = (absolute_column1, absolute_column2); (absolute_column2, absolute_column1) = (absolute_column1, absolute_column2);
} }
if self.lexer.is_a1_mode() {
if !absolute_row1 {
row1 -= context.row
};
if !absolute_column1 {
column1 -= context.column
};
if !absolute_row2 {
row2 -= context.row
};
if !absolute_column2 {
column2 -= context.column
};
}
match sheet_index { match sheet_index {
Some(index) => Node::RangeKind { Some(index) => Node::RangeKind {
sheet_name: sheet, sheet_name: sheet,
@@ -587,11 +692,47 @@ impl Parser {
kind: function_kind, kind: function_kind,
args, args,
}; };
} else { }
if &name == "_xlfn.SINGLE" {
if args.len() != 1 {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Implicit Intersection requires just one argument"
.to_string(),
};
}
return Node::ImplicitIntersection {
automatic: false,
child: Box::new(args[0].clone()),
};
}
return Node::InvalidFunctionKind { name, args }; return Node::InvalidFunctionKind { name, args };
} }
let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: format!("sheet not found: {}", context.sheet),
};
} }
Node::VariableKind(name) };
// Could be a defined name or a table
if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope, formula));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
if table_name.to_lowercase() == name_lower {
return Node::TableNameKind(name);
}
}
Node::WrongVariableKind(name)
} }
TokenType::Error(kind) => Node::ErrorKind(kind), TokenType::Error(kind) => Node::ErrorKind(kind),
TokenType::Illegal(error) => Node::ParseErrorKind { TokenType::Illegal(error) => Node::ParseErrorKind {
@@ -604,7 +745,38 @@ impl Parser {
position: 0, position: 0,
message: "Unexpected end of input.".to_string(), message: "Unexpected end of input.".to_string(),
}, },
TokenType::Boolean(value) => Node::BooleanKind(value), TokenType::Boolean(value) => {
// Could be a function call "TRUE()"
let next_token = self.lexer.peek_token();
if next_token == TokenType::LeftParenthesis {
self.lexer.advance_token();
// We parse all the arguments, although technically this is moot
// But is has the upside of transforming `=TRUE( 4 )` into `=TRUE(4)`
let args = match self.parse_function_args() {
Ok(s) => s,
Err(e) => return e,
};
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: err.position,
message: err.message,
};
}
if value {
return Node::FunctionKind {
kind: Function::True,
args,
};
} else {
return Node::FunctionKind {
kind: Function::False,
args,
};
}
}
Node::BooleanKind(value)
}
TokenType::Compare(_) => { TokenType::Compare(_) => {
// A primary Node cannot start with an operator // A primary Node cannot start with an operator
Node::ParseErrorKind { Node::ParseErrorKind {
@@ -637,6 +809,14 @@ impl Parser {
message: "Unexpected token: 'POWER'".to_string(), message: "Unexpected token: 'POWER'".to_string(),
} }
} }
TokenType::At => {
// A primary Node cannot start with an operator
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Unexpected token: '@'".to_string(),
}
}
TokenType::RightParenthesis TokenType::RightParenthesis
| TokenType::RightBracket | TokenType::RightBracket
| TokenType::Colon | TokenType::Colon
@@ -648,7 +828,7 @@ impl Parser {
| TokenType::Percent => Node::ParseErrorKind { | TokenType::Percent => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("Unexpected token: '{:?}'", next_token), message: format!("Unexpected token: '{next_token:?}'"),
}, },
TokenType::LeftBracket => Node::ParseErrorKind { TokenType::LeftBracket => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
@@ -663,33 +843,42 @@ impl Parser {
// We will try to convert to a normal reference // We will try to convert to a normal reference
// table_name[column_name] => cell1:cell2 // table_name[column_name] => cell1:cell2
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2 // table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
if let Some(context) = &self.context { let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) { let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i, Some(i) => i,
None => { None => {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: format!("sheet not found: {}", context.sheet),
}; };
} }
}; };
// table-name => table // table-name => table
let table = self.tables.get(&table_name).unwrap_or_else(|| { let table = match self.tables.get(&table_name) {
panic!( Some(t) => t,
None => {
let message = format!(
"Table not found: '{table_name}' at '{}!{}{}'", "Table not found: '{table_name}' at '{}!{}{}'",
context.sheet, context.sheet,
number_to_column(context.column).expect(""), number_to_column(context.column)
.unwrap_or(format!("{}", context.column)),
context.row context.row
) );
}); return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message,
};
}
};
let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) { let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
Some(i) => i, Some(i) => i,
None => { None => {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: format!("table sheet not found: {}", table.sheet_name),
}; };
} }
}; };
@@ -701,6 +890,7 @@ impl Parser {
}; };
// context must be with tables.reference // context must be with tables.reference
#[allow(clippy::expect_used)]
let (column_start, mut row_start, column_end, mut row_end) = let (column_start, mut row_start, column_end, mut row_end) =
parse_range(&table.reference).expect("Failed parsing range"); parse_range(&table.reference).expect("Failed parsing range");
@@ -739,8 +929,7 @@ impl Parser {
} }
} }
match table_reference { match table_reference {
None => { None => Node::RangeKind {
return Node::RangeKind {
sheet_name, sheet_name,
sheet_index: table_sheet_index, sheet_index: table_sheet_index,
absolute_row1: true, absolute_row1: true,
@@ -751,8 +940,7 @@ impl Parser {
absolute_column2: true, absolute_column2: true,
row2: row_end, row2: row_end,
column2: column_end, column2: column_end,
}; },
}
Some(TableReference::ColumnReference(s)) => { Some(TableReference::ColumnReference(s)) => {
let column_index = match get_table_column_by_name(&s, table) { let column_index = match get_table_column_by_name(&s, table) {
Some(s) => s + column_start, Some(s) => s + column_start,
@@ -760,9 +948,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize, position: self.lexer.get_position() as usize,
message: format!( message: format!("Expecting column: {s} in table {table_name}"),
"Expecting column: {s} in table {table_name}"
),
}; };
} }
}; };
@@ -776,7 +962,7 @@ impl Parser {
column: column_index, column: column_index,
}; };
} }
return Node::RangeKind { Node::RangeKind {
sheet_name, sheet_name,
sheet_index: table_sheet_index, sheet_index: table_sheet_index,
absolute_row1: true, absolute_row1: true,
@@ -787,7 +973,7 @@ impl Parser {
absolute_column2: true, absolute_column2: true,
row2: row_end, row2: row_end,
column2: column_index, column2: column_index,
}; }
} }
Some(TableReference::RangeReference((left, right))) => { Some(TableReference::RangeReference((left, right))) => {
let left_column_index = match get_table_column_by_name(&left, table) { let left_column_index = match get_table_column_by_name(&left, table) {
@@ -815,7 +1001,7 @@ impl Parser {
}; };
} }
}; };
return Node::RangeKind { Node::RangeKind {
sheet_name, sheet_name,
sheet_index: table_sheet_index, sheet_index: table_sheet_index,
absolute_row1: true, absolute_row1: true,
@@ -826,15 +1012,9 @@ impl Parser {
absolute_column2: true, absolute_column2: true,
row2: row_end, row2: row_end,
column2: right_column_index, column2: right_column_index,
};
} }
} }
} }
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Structured references not supported in R1C1 mode".to_string(),
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use super::{ use super::{
stringify::{stringify_reference, DisplaceData}, stringify::{stringify_reference, DisplaceData},
Node, Reference, ArrayNode, Node, Reference,
}; };
use crate::{ use crate::{
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW},
@@ -53,15 +53,24 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
arguments = to_string_moved(el, move_context); arguments = to_string_moved(el, move_context);
} }
} }
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 to_string_moved(node: &Node, move_context: &MoveContext) -> String { fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(), BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value), StringKind(value) => format!("\"{value}\""),
ReferenceKind { ReferenceKind {
sheet_name, sheet_name,
sheet_index, sheet_index,
@@ -232,7 +241,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
@@ -316,7 +325,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
@@ -349,7 +358,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
_ => to_string_moved(right, move_context), _ => to_string_moved(right, move_context),
}; };
format!("{}{}{}", x, kind, y) format!("{x}{kind}{y}")
} }
OpPowerKind { left, right } => format!( OpPowerKind { left, right } => format!(
"{}^{}", "{}^{}",
@@ -362,20 +371,43 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
move_function(name, args, move_context) move_function(name, args, move_context)
} }
ArrayKind(args) => { ArrayKind(args) => {
// This code is a placeholder. Arrays are not yet implemented let mut first_row = true;
let mut first = true; let mut matrix_string = String::new();
let mut arguments = "".to_string();
for el in args { // Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
if !first { for row in args {
arguments = format!("{},{}", arguments, to_string_moved(el, move_context)); if !first_row {
matrix_string.push(',');
} else { } else {
first = false; first_row = false;
arguments = to_string_moved(el, move_context);
} }
// Build the string for the current row
let mut first_col = true;
let mut row_string = String::new();
for el in row {
if !first_col {
row_string.push(',');
} else {
first_col = false;
} }
format!("{{{}}}", arguments)
// Reuse your existing element-stringification function
row_string.push_str(&to_string_array_node(el));
} }
VariableKind(value) => value.to_string(),
// Enclose the row in braces
matrix_string.push('{');
matrix_string.push_str(&row_string);
matrix_string.push('}');
}
// Enclose the whole matrix in braces
format!("{{{matrix_string}}}")
}
DefinedNameKind((name, ..)) => name.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
to_string_moved(left, move_context), to_string_moved(left, move_context),
@@ -386,12 +418,18 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)), OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)), OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
}, },
ErrorKind(kind) => format!("{}", kind), ErrorKind(kind) => format!("{kind}"),
ParseErrorKind { ParseErrorKind {
formula, formula,
message: _, message: _,
position: _, position: _,
} => formula.to_string(), } => formula.to_string(),
EmptyArgKind => "".to_string(), EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
format!("@{}", to_string_moved(child, move_context))
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
use super::{super::utils::quote_name, Node, Reference}; use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::token::OpUnary; use crate::expressions::parser::move_formula::to_string_array_node;
use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::{OpSum, OpUnary};
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str}; use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
pub enum DisplaceData { pub enum DisplaceData {
@@ -26,6 +28,11 @@ pub enum DisplaceData {
column: i32, column: i32,
delta: i32, delta: i32,
}, },
RowMove {
sheet: u32,
row: i32,
delta: i32,
},
ColumnMove { ColumnMove {
sheet: u32, sheet: u32,
column: i32, column: i32,
@@ -34,10 +41,21 @@ pub enum DisplaceData {
None, None,
} }
/// This is the internal mode in IronCalc
pub fn to_rc_format(node: &Node) -> String { pub fn to_rc_format(node: &Node) -> String {
stringify(node, None, &DisplaceData::None, false) stringify(node, None, &DisplaceData::None, false)
} }
/// 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)
}
/// 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)
}
pub fn to_string_displaced( pub fn to_string_displaced(
node: &Node, node: &Node,
context: &CellReferenceRC, context: &CellReferenceRC,
@@ -46,18 +64,10 @@ pub fn to_string_displaced(
stringify(node, Some(context), displace_data, false) stringify(node, Some(context), displace_data, false)
} }
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false)
}
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true)
}
/// Converts a local reference to a string applying some displacement if needed. /// Converts a local reference to a string applying some displacement if needed.
/// It uses A1 style if context is not None. If context is None it uses R1C1 style /// It uses A1 style if context is not None. If context is None it uses R1C1 style
/// If full_row is true then the row details will be omitted in the A1 case /// If full_row is true then the row details will be omitted in the A1 case
/// If full_colum is true then column details will be omitted. /// If full_column is true then column details will be omitted.
pub(crate) fn stringify_reference( pub(crate) fn stringify_reference(
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
@@ -154,6 +164,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 { DisplaceData::ColumnMove {
sheet, sheet,
column: move_column, column: move_column,
@@ -162,14 +195,18 @@ pub(crate) fn stringify_reference(
if sheet_index == *sheet { if sheet_index == *sheet {
if column == *move_column { if column == *move_column {
column += *delta; column += *delta;
} else if (*delta > 0 } else if *delta > 0 {
&& column > *move_column // Moving the column to the right
&& column <= *move_column + *delta) if column > *move_column && column <= *move_column + *delta {
|| (*delta < 0 // Intermediate columns move left by one position
&& column < *move_column column -= 1;
&& column >= *move_column + *delta) }
{ } else if *delta < 0 {
column -= *delta; // Moving the column to the left
if column < *move_column && column >= *move_column + *delta {
// Intermediate columns move right by one position
column += 1;
}
} }
} }
} }
@@ -179,16 +216,16 @@ pub(crate) fn stringify_reference(
return "#REF!".to_string(); return "#REF!".to_string();
} }
let mut row_abs = if absolute_row { let mut row_abs = if absolute_row {
format!("${}", row) format!("${row}")
} else { } else {
format!("{}", row) format!("{row}")
}; };
let column = match crate::expressions::utils::number_to_column(column) { let column = match crate::expressions::utils::number_to_column(column) {
Some(s) => s, Some(s) => s,
None => return "#REF!".to_string(), None => return "#REF!".to_string(),
}; };
let mut col_abs = if absolute_column { let mut col_abs = if absolute_column {
format!("${}", column) format!("${column}")
} else { } else {
column column
}; };
@@ -203,27 +240,27 @@ pub(crate) fn stringify_reference(
format!("{}!{}{}", quote_name(name), col_abs, row_abs) format!("{}!{}{}", quote_name(name), col_abs, row_abs)
} }
None => { None => {
format!("{}{}", col_abs, row_abs) format!("{col_abs}{row_abs}")
} }
} }
} }
None => { None => {
let row_abs = if absolute_row { let row_abs = if absolute_row {
format!("R{}", row) format!("R{row}")
} else { } else {
format!("R[{}]", row) format!("R[{row}]")
}; };
let col_abs = if absolute_column { let col_abs = if absolute_column {
format!("C{}", column) format!("C{column}")
} else { } else {
format!("C[{}]", column) format!("C[{column}]")
}; };
match &sheet_name { match &sheet_name {
Some(name) => { Some(name) => {
format!("{}!{}{}", quote_name(name), row_abs, col_abs) format!("{}!{}{}", quote_name(name), row_abs, col_abs)
} }
None => { None => {
format!("{}{}", row_abs, col_abs) format!("{row_abs}{col_abs}")
} }
} }
} }
@@ -235,7 +272,7 @@ fn format_function(
args: &Vec<Node>, args: &Vec<Node>,
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
use_original_name: bool, export_to_excel: bool,
) -> String { ) -> String {
let mut first = true; let mut first = true;
let mut arguments = "".to_string(); let mut arguments = "".to_string();
@@ -244,27 +281,52 @@ fn format_function(
arguments = format!( arguments = format!(
"{},{}", "{},{}",
arguments, arguments,
stringify(el, context, displace_data, use_original_name) stringify(el, context, displace_data, export_to_excel)
); );
} else { } else {
first = false; first = false;
arguments = stringify(el, context, displace_data, use_original_name); arguments = stringify(el, context, displace_data, export_to_excel);
} }
} }
format!("{}({})", name, arguments) format!("{name}({arguments})")
} }
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
// But three different ways to convert it to a string.
//
// To stringify a formula we need a "context", that is in which cell are we doing the "stringifying"
//
// But there are three ways to stringify a formula:
//
// * To show it to the IronCalc user
// * To store internally
// * To export to Excel
//
// There are, of course correspondingly three "modes" when parsing a formula.
//
// The internal representation is the more different as references are stored in the RC representation.
// The the AST of the formula is kept close to this representation we don't need a context
//
// In the export to Excel representation certain things are different:
// * We add a _xlfn. in front of some (more modern) functions
// * We remove the Implicit Intersection operator when it is automatic and add _xlfn.SINGLE when it is not
//
// Examples:
// * =A1+B2
// * =RC+R1C1
// * =A1+B1
fn stringify( fn stringify(
node: &Node, node: &Node,
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
use_original_name: bool, export_to_excel: bool,
) -> String { ) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(), BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value), StringKind(value) => format!("\"{value}\""),
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
column, column,
@@ -354,7 +416,7 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
WrongRangeKind { WrongRangeKind {
sheet_name, sheet_name,
@@ -403,114 +465,234 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
OpConcatenateKind { left, right } => format!( OpConcatenateKind { left, right } => format!(
"{}&{}", "{}&{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
kind, kind,
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
),
OpSumKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, use_original_name),
kind,
stringify(right, context, displace_data, use_original_name)
), ),
OpSumKind { kind, left, right } => {
let left_str = stringify(left, context, displace_data, export_to_excel);
// 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)
)
} else {
stringify(right, context, displace_data, export_to_excel)
};
format!("{left_str}{kind}{right_str}")
}
OpProductKind { kind, left, right } => { OpProductKind { kind, left, right } => {
let x = match **left { let x = match **left {
OpSumKind { .. } => format!( OpSumKind { .. } | CompareKind { .. } => format!(
"({})", "({})",
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, export_to_excel)
), ),
CompareKind { .. } => format!( _ => stringify(left, context, displace_data, export_to_excel),
"({})",
stringify(left, context, displace_data, use_original_name)
),
_ => stringify(left, context, displace_data, use_original_name),
}; };
let y = match **right { let y = match **right {
OpSumKind { .. } => format!( OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
CompareKind { .. } => format!( _ => stringify(right, context, displace_data, export_to_excel),
"({})",
stringify(right, context, displace_data, use_original_name)
),
OpProductKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
),
_ => stringify(right, context, displace_data, use_original_name),
}; };
format!("{}{}{}", x, kind, y) format!("{x}{kind}{y}")
} }
OpPowerKind { left, right } => format!( OpPowerKind { left, right } => {
"{}^{}", let x = match **left {
stringify(left, context, displace_data, use_original_name), BooleanKind(_)
stringify(right, context, displace_data, use_original_name) | NumberKind(_)
| UnaryKind { .. }
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| ErrorKind(_)
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(left, context, displace_data, export_to_excel)
), ),
};
let y = match **right {
BooleanKind(_)
| NumberKind(_)
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(right, context, displace_data, export_to_excel)
}
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| UnaryKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
),
};
format!("{x}^{y}")
}
InvalidFunctionKind { name, args } => { InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, use_original_name) format_function(name, args, context, displace_data, export_to_excel)
} }
FunctionKind { kind, args } => { FunctionKind { kind, args } => {
let name = if use_original_name { let name = if export_to_excel {
kind.to_xlsx_string() kind.to_xlsx_string()
} else { } else {
kind.to_string() kind.to_string()
}; };
format_function(&name, args, context, displace_data, use_original_name) format_function(&name, args, context, displace_data, export_to_excel)
} }
ArrayKind(args) => { ArrayKind(args) => {
let mut first = true; let mut first_row = true;
let mut arguments = "".to_string(); let mut matrix_string = String::new();
for el in args {
if !first { for row in args {
arguments = format!( if !first_row {
"{},{}", matrix_string.push(';');
arguments,
stringify(el, context, displace_data, use_original_name)
);
} else { } else {
first = false; first_row = false;
arguments = stringify(el, context, displace_data, use_original_name);
} }
let mut first_column = true;
let mut row_string = String::new();
for el in row {
if !first_column {
row_string.push(',');
} else {
first_column = false;
} }
format!("{{{}}}", arguments) row_string.push_str(&to_string_array_node(el));
} }
VariableKind(value) => value.to_string(), matrix_string.push_str(&row_string);
}
format!("{{{matrix_string}}}")
}
TableNameKind(value) => value.to_string(),
DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
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)
)
} else {
format!( format!(
"-{}", "-{}",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
) )
} }
}
OpUnary::Percentage => { OpUnary::Percentage => {
format!( format!(
"{}%", "{}%",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
) )
} }
}, },
ErrorKind(kind) => format!("{}", kind), ErrorKind(kind) => format!("{kind}"),
ParseErrorKind { ParseErrorKind {
formula, formula,
position: _, position: _,
message: _, message: _,
} => formula.to_string(), } => formula.to_string(),
EmptyArgKind => "".to_string(), EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
if export_to_excel {
// We need to check wether the II can be automatic or not
let mut new_node = child.as_ref().clone();
add_implicit_intersection(&mut new_node, true);
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
return stringify(child, context, displace_data, export_to_excel);
}
return format!(
"_xlfn.SINGLE({})",
stringify(child, context, displace_data, export_to_excel)
);
}
format!(
"@{}",
stringify(child, context, displace_data, export_to_excel)
)
}
} }
} }
@@ -598,6 +780,12 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::UnaryKind { kind: _, right } => { Node::UnaryKind { kind: _, right } => {
rename_sheet_in_node(right, sheet_index, new_name); rename_sheet_in_node(right, sheet_index, new_name);
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_sheet_in_node(child, sheet_index, new_name);
}
// Do nothing // Do nothing
Node::BooleanKind(_) => {} Node::BooleanKind(_) => {}
@@ -606,7 +794,96 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::ErrorKind(_) => {} Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {} Node::ArrayKind(_) => {}
Node::VariableKind(_) => {} Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::EmptyArgKind => {} Node::EmptyArgKind => {}
} }
} }
pub(crate) fn rename_defined_name_in_node(
node: &mut Node,
name: &str,
scope: Option<u32>,
new_name: &str,
) {
match node {
// Rename
Node::DefinedNameKind((n, s, _)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string();
}
}
// Go next level
Node::OpRangeKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpConcatenateKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpPowerKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_defined_name_in_node(child, name, scope, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {}
Node::EmptyArgKind => {}
Node::ReferenceKind { .. } => {}
Node::RangeKind { .. } => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
}
}

View File

@@ -0,0 +1,10 @@
mod test_add_implicit_intersection;
mod test_arrays;
mod test_general;
mod test_implicit_intersection;
mod test_issue_155;
mod test_issue_483;
mod test_move_formula;
mod test_ranges;
mod test_stringify;
mod test_tables;

View File

@@ -0,0 +1,80 @@
use std::collections::HashMap;
use crate::expressions::{
parser::{
stringify::{to_excel_string, to_string},
Parser,
},
types::CellReferenceRC,
};
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());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let cases = vec![
("A1:A10*SUM(A1:A10)", "@A1:A10*SUM(A1:A10)"),
("A1:A10", "@A1:A10"),
// Math and trigonometry functions
("SUM(A1:A10)", "SUM(A1:A10)"),
("SIN(A1:A10)", "SIN(@A1:A10)"),
("COS(A1:A10)", "COS(@A1:A10)"),
("TAN(A1:A10)", "TAN(@A1:A10)"),
("ASIN(A1:A10)", "ASIN(@A1:A10)"),
("ACOS(A1:A10)", "ACOS(@A1:A10)"),
("ATAN(A1:A10)", "ATAN(@A1:A10)"),
("SINH(A1:A10)", "SINH(@A1:A10)"),
("COSH(A1:A10)", "COSH(@A1:A10)"),
("TANH(A1:A10)", "TANH(@A1:A10)"),
("ASINH(A1:A10)", "ASINH(@A1:A10)"),
("ACOSH(A1:A10)", "ACOSH(@A1:A10)"),
("ATANH(A1:A10)", "ATANH(@A1:A10)"),
("ATAN2(A1:A10,B1:B10)", "ATAN2(@A1:A10,@B1:B10)"),
("ATAN2(A1:A10,A1)", "ATAN2(@A1:A10,A1)"),
("SQRT(A1:A10)", "SQRT(@A1:A10)"),
("SQRTPI(A1:A10)", "SQRTPI(@A1:A10)"),
("POWER(A1:A10,A1)", "POWER(@A1:A10,A1)"),
("POWER(A1:A10,B1:B10)", "POWER(@A1:A10,@B1:B10)"),
("MAX(A1:A10)", "MAX(A1:A10)"),
("MIN(A1:A10)", "MIN(A1:A10)"),
("ABS(A1:A10)", "ABS(@A1:A10)"),
("FALSE()", "FALSE()"),
("TRUE()", "TRUE()"),
// Defined names
("BADNMAE", "@BADNMAE"),
// Logical
("AND(A1:A10)", "AND(A1:A10)"),
("OR(A1:A10)", "OR(A1:A10)"),
("NOT(A1:A10)", "NOT(@A1:A10)"),
("IF(A1:A10,B1:B10,C1:C10)", "IF(@A1:A10,@B1:B10,@C1:C10)"),
// Information
// ("ISBLANK(A1:A10)", "ISBLANK(A1:A10)"),
// ("ISERR(A1:A10)", "ISERR(A1:A10)"),
// ("ISERROR(A1:A10)", "ISERROR(A1:A10)"),
// ("ISEVEN(A1:A10)", "ISEVEN(A1:A10)"),
// ("ISLOGICAL(A1:A10)", "ISLOGICAL(A1:A10)"),
// ("ISNA(A1:A10)", "ISNA(A1:A10)"),
// ("ISNONTEXT(A1:A10)", "ISNONTEXT(A1:A10)"),
// ("ISNUMBER(A1:A10)", "ISNUMBER(A1:A10)"),
// ("ISODD(A1:A10)", "ISODD(A1:A10)"),
// ("ISREF(A1:A10)", "ISREF(A1:A10)"),
// ("ISTEXT(A1:A10)", "ISTEXT(A1:A10)"),
];
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);
assert_eq!(r, expected);
let excel_formula = to_excel_string(&t, &cell_reference);
assert_eq!(excel_formula, formula);
}
}

View File

@@ -0,0 +1,92 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::{ArrayNode, Node, Parser};
use crate::expressions::types::CellReferenceRC;
#[test]
fn simple_horizontal() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let horizontal = parser.parse("{1, 2, 3}", &cell_reference);
assert_eq!(
horizontal,
Node::ArrayKind(vec![vec![
ArrayNode::Number(1.0),
ArrayNode::Number(2.0),
ArrayNode::Number(3.0)
]])
);
assert_eq!(to_rc_format(&horizontal), "{1,2,3}");
assert_eq!(to_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());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let vertical = parser.parse("{1;2; 3}", &cell_reference);
assert_eq!(
vertical,
Node::ArrayKind(vec![
vec![ArrayNode::Number(1.0)],
vec![ArrayNode::Number(2.0)],
vec![ArrayNode::Number(3.0)]
])
);
assert_eq!(to_rc_format(&vertical), "{1;2;3}");
assert_eq!(to_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());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let matrix = parser.parse("{1,2,3; 4, 5, 6; 7,8,9}", &cell_reference);
assert_eq!(
matrix,
Node::ArrayKind(vec![
vec![
ArrayNode::Number(1.0),
ArrayNode::Number(2.0),
ArrayNode::Number(3.0)
],
vec![
ArrayNode::Number(4.0),
ArrayNode::Number(5.0),
ArrayNode::Number(6.0)
],
vec![
ArrayNode::Number(7.0),
ArrayNode::Number(8.0),
ArrayNode::Number(9.0)
]
])
);
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}");
}

View File

@@ -1,17 +1,13 @@
#![allow(clippy::panic)]
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::lexer::LexerMode; use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::DisplaceData; use crate::expressions::parser::stringify::{
to_rc_format, to_string, to_string_displaced, DisplaceData,
use super::super::types::CellReferenceRC;
use super::Parser;
use super::{
super::parser::{
stringify::{to_rc_format, to_string},
Node,
},
stringify::to_string_displaced,
}; };
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> { struct Formula<'a> {
initial: &'a str, initial: &'a str,
@@ -21,7 +17,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_reference() { fn test_parser_reference() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -29,14 +25,14 @@ fn test_parser_reference() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("A2", &Some(cell_reference)); let t = parser.parse("A2", &cell_reference);
assert_eq!(to_rc_format(&t), "R[1]C[0]"); assert_eq!(to_rc_format(&t), "R[1]C[0]");
} }
#[test] #[test]
fn test_parser_absolute_column() { fn test_parser_absolute_column() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -44,14 +40,14 @@ fn test_parser_absolute_column() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A1", &Some(cell_reference)); let t = parser.parse("$A1", &cell_reference);
assert_eq!(to_rc_format(&t), "R[0]C1"); assert_eq!(to_rc_format(&t), "R[0]C1");
} }
#[test] #[test]
fn test_parser_absolute_row_col() { fn test_parser_absolute_row_col() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -59,14 +55,14 @@ fn test_parser_absolute_row_col() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$C$5", &Some(cell_reference)); let t = parser.parse("$C$5", &cell_reference);
assert_eq!(to_rc_format(&t), "R5C3"); assert_eq!(to_rc_format(&t), "R5C3");
} }
#[test] #[test]
fn test_parser_absolute_row_col_1() { fn test_parser_absolute_row_col_1() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -74,14 +70,14 @@ fn test_parser_absolute_row_col_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A$1", &Some(cell_reference)); let t = parser.parse("$A$1", &cell_reference);
assert_eq!(to_rc_format(&t), "R1C1"); assert_eq!(to_rc_format(&t), "R1C1");
} }
#[test] #[test]
fn test_parser_simple_formula() { fn test_parser_simple_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -90,14 +86,14 @@ fn test_parser_simple_formula() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference)); let t = parser.parse("C3+Sheet2!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_boolean() { fn test_parser_boolean() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -106,14 +102,14 @@ fn test_parser_boolean() {
column: 1, column: 1,
}; };
let t = parser.parse("true", &Some(cell_reference)); let t = parser.parse("true", &cell_reference);
assert_eq!(to_rc_format(&t), "TRUE"); assert_eq!(to_rc_format(&t), "TRUE");
} }
#[test] #[test]
fn test_parser_bad_formula() { fn test_parser_bad_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -121,7 +117,7 @@ fn test_parser_bad_formula() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("#Value", &Some(cell_reference)); let t = parser.parse("#Value", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -142,7 +138,7 @@ fn test_parser_bad_formula() {
#[test] #[test]
fn test_parser_bad_formula_1() { fn test_parser_bad_formula_1() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -150,7 +146,7 @@ fn test_parser_bad_formula_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("<5", &Some(cell_reference)); let t = parser.parse("<5", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -171,7 +167,7 @@ fn test_parser_bad_formula_1() {
#[test] #[test]
fn test_parser_bad_formula_2() { fn test_parser_bad_formula_2() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -179,7 +175,7 @@ fn test_parser_bad_formula_2() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("*5", &Some(cell_reference)); let t = parser.parse("*5", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -200,7 +196,7 @@ fn test_parser_bad_formula_2() {
#[test] #[test]
fn test_parser_bad_formula_3() { fn test_parser_bad_formula_3() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -208,7 +204,7 @@ fn test_parser_bad_formula_3() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference)); let t = parser.parse("SUM(#VALVE!)", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -229,7 +225,7 @@ fn test_parser_bad_formula_3() {
#[test] #[test]
fn test_parser_formulas() { fn test_parser_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -263,11 +259,11 @@ fn test_parser_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_rc_format(&t), formula.expected); assert_eq!(to_rc_format(&t), formula.expected);
assert_eq!(to_string(&t, &cell_reference), formula.initial); assert_eq!(to_string(&t, &cell_reference), formula.initial);
@@ -277,7 +273,7 @@ fn test_parser_formulas() {
#[test] #[test]
fn test_parser_r1c1_formulas() { fn test_parser_r1c1_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
parser.set_lexer_mode(LexerMode::R1C1); parser.set_lexer_mode(LexerMode::R1C1);
let formulas = vec![ let formulas = vec![
@@ -328,11 +324,11 @@ fn test_parser_r1c1_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_string(&t, &cell_reference), formula.expected); assert_eq!(to_string(&t, &cell_reference), formula.expected);
assert_eq!(to_rc_format(&t), formula.initial); assert_eq!(to_rc_format(&t), formula.initial);
@@ -342,7 +338,7 @@ fn test_parser_r1c1_formulas() {
#[test] #[test]
fn test_parser_quotes() { fn test_parser_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -351,14 +347,14 @@ fn test_parser_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference)); let t = parser.parse("C3+'Second Sheet'!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_escape_quotes() { fn test_parser_escape_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -367,14 +363,14 @@ fn test_parser_escape_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference)); let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_parenthesis() { fn test_parser_parenthesis() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -383,14 +379,14 @@ fn test_parser_parenthesis() {
column: 1, column: 1,
}; };
let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference)); let t = parser.parse("(C3=\"Yes\")*5", &cell_reference);
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5"); assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
} }
#[test] #[test]
fn test_parser_excel_xlfn() { fn test_parser_excel_xlfn() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -399,7 +395,7 @@ fn test_parser_excel_xlfn() {
column: 1, column: 1,
}; };
let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference)); let t = parser.parse("_xlfn.CONCAT(C3)", &cell_reference);
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])"); assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
} }
@@ -411,9 +407,9 @@ fn test_to_string_displaced() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone())); let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -431,9 +427,9 @@ fn test_to_string_displaced_full_ranges() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("SUM(3:3)", &Some(context.clone())); let node = parser.parse("SUM(3:3)", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -444,7 +440,7 @@ fn test_to_string_displaced_full_ranges() {
"SUM(3:3)".to_string() "SUM(3:3)".to_string()
); );
let node = parser.parse("SUM(D:D)", &Some(context.clone())); let node = parser.parse("SUM(D:D)", context);
let displace_data = DisplaceData::Row { let displace_data = DisplaceData::Row {
sheet: 0, sheet: 0,
row: 3, row: 3,
@@ -464,9 +460,9 @@ fn test_to_string_displaced_too_low() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone())); let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -484,9 +480,9 @@ fn test_to_string_displaced_too_high() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone())); let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,

View File

@@ -0,0 +1,75 @@
#![allow(clippy::panic)]
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
use std::collections::HashMap;
#[test]
fn simple() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 3,
column: 2,
};
let t = parser.parse("@A1:A10", &cell_reference);
let child = Node::RangeKind {
sheet_name: None,
sheet_index: 0,
absolute_row1: false,
absolute_column1: false,
row1: -2,
column1: -1,
absolute_row2: false,
absolute_column2: false,
row2: 7,
column2: -1,
};
assert_eq!(
t,
Node::ImplicitIntersection {
automatic: false,
child: Box::new(child)
}
)
}
#[test]
fn simple_add() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 3,
column: 2,
};
let t = parser.parse("@A1:A10+12", &cell_reference);
let child = Node::RangeKind {
sheet_name: None,
sheet_index: 0,
absolute_row1: false,
absolute_column1: false,
row1: -2,
column1: -1,
absolute_row2: false,
absolute_column2: false,
row2: 7,
column2: -1,
};
assert_eq!(
t,
Node::OpSumKind {
kind: crate::expressions::token::OpSum::Add,
left: Box::new(Node::ImplicitIntersection {
automatic: false,
child: Box::new(child)
}),
right: Box::new(Node::NumberKind(12.0))
}
)
}

View File

@@ -0,0 +1,69 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
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());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 2,
column: 2,
};
let t = parser.parse("A$1:A2", &cell_reference);
assert_eq!(to_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());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
let t = parser.parse("C$1:D2", &cell_reference);
assert_eq!(to_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());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
// 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");
}
#[test]
fn issue_155_parser_only_column() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
// 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");
}

View File

@@ -0,0 +1,27 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_483_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(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_string(&t, &cell_reference), "-(A1^1.22)");
let t = parser.parse("-A1^1.22", &cell_reference);
assert!(matches!(t, Node::OpPowerKind { .. }));
assert_eq!(to_string(&t, &cell_reference), "-A1^1.22");
}

View File

@@ -1,10 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::parser::move_formula::{move_formula, MoveContext}; use crate::expressions::parser::move_formula::{move_formula, MoveContext};
use crate::expressions::types::Area; use crate::expressions::parser::Parser;
use crate::expressions::types::{Area, CellReferenceRC};
use super::super::types::CellReferenceRC;
use super::Parser;
#[test] #[test]
fn test_move_formula() { fn test_move_formula() {
@@ -17,7 +15,7 @@ fn test_move_formula() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -29,7 +27,7 @@ fn test_move_formula() {
}; };
// formula AB31 will not change // formula AB31 will not change
let node = parser.parse("AB31", &Some(context.clone())); let node = parser.parse("AB31", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -45,7 +43,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31"); assert_eq!(t, "AB31");
// formula $AB$31 will not change // formula $AB$31 will not change
let node = parser.parse("AB31", &Some(context.clone())); let node = parser.parse("AB31", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -61,7 +59,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31"); assert_eq!(t, "AB31");
// but formula D5 will change to N15 (N = D + 10) // but formula D5 will change to N15 (N = D + 10)
let node = parser.parse("D5", &Some(context.clone())); let node = parser.parse("D5", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -77,7 +75,7 @@ fn test_move_formula() {
assert_eq!(t, "N15"); assert_eq!(t, "N15");
// Also formula $D$5 will change to N15 (N = D + 10) // Also formula $D$5 will change to N15 (N = D + 10)
let node = parser.parse("$D$5", &Some(context.clone())); let node = parser.parse("$D$5", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -104,7 +102,7 @@ fn test_move_formula_context_offset() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -115,7 +113,7 @@ fn test_move_formula_context_offset() {
height: 5, height: 5,
}; };
let node = parser.parse("-X9+C2%", &Some(context.clone())); let node = parser.parse("-X9+C2%", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -142,7 +140,7 @@ fn test_move_formula_area_limits() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -154,7 +152,7 @@ fn test_move_formula_area_limits() {
}; };
// Outside of the area. Not moved // Outside of the area. Not moved
let node = parser.parse("B2+B3+C1+G6+H5", &Some(context.clone())); let node = parser.parse("B2+B3+C1+G6+H5", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -170,7 +168,7 @@ fn test_move_formula_area_limits() {
assert_eq!(t, "B2+B3+C1+G6+H5"); assert_eq!(t, "B2+B3+C1+G6+H5");
// In the area. Moved // In the area. Moved
let node = parser.parse("C2+F4+F5+F6", &Some(context.clone())); let node = parser.parse("C2+F4+F5+F6", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -197,7 +195,7 @@ fn test_move_formula_ranges() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let area = &Area { let area = &Area {
sheet: 0, sheet: 0,
@@ -207,7 +205,7 @@ fn test_move_formula_ranges() {
height: 5, height: 5,
}; };
// Ranges inside the area are fully displaced (absolute or not) // Ranges inside the area are fully displaced (absolute or not)
let node = parser.parse("SUM(C2:F5)", &Some(context.clone())); let node = parser.parse("SUM(C2:F5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -222,7 +220,7 @@ fn test_move_formula_ranges() {
); );
assert_eq!(t, "SUM(M12:P15)"); assert_eq!(t, "SUM(M12:P15)");
let node = parser.parse("SUM($C$2:$F$5)", &Some(context.clone())); let node = parser.parse("SUM($C$2:$F$5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -238,7 +236,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($M$12:$P$15)"); assert_eq!(t, "SUM($M$12:$P$15)");
// Ranges completely outside of the area are not touched // Ranges completely outside of the area are not touched
let node = parser.parse("SUM(A1:B3)", &Some(context.clone())); let node = parser.parse("SUM(A1:B3)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -253,7 +251,7 @@ fn test_move_formula_ranges() {
); );
assert_eq!(t, "SUM(A1:B3)"); assert_eq!(t, "SUM(A1:B3)");
let node = parser.parse("SUM($A$1:$B$3)", &Some(context.clone())); let node = parser.parse("SUM($A$1:$B$3)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -269,7 +267,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($A$1:$B$3)"); assert_eq!(t, "SUM($A$1:$B$3)");
// Ranges that overlap with the area are also NOT displaced // Ranges that overlap with the area are also NOT displaced
let node = parser.parse("SUM(A1:F5)", &Some(context.clone())); let node = parser.parse("SUM(A1:F5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -285,7 +283,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM(A1:F5)"); assert_eq!(t, "SUM(A1:F5)");
// Ranges that contain the area are also NOT displaced // Ranges that contain the area are also NOT displaced
let node = parser.parse("SUM(A1:X50)", &Some(context.clone())); let node = parser.parse("SUM(A1:X50)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -320,10 +318,10 @@ fn test_move_formula_wrong_reference() {
height: 5, height: 5,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Wrong formulas will NOT be displaced // Wrong formulas will NOT be displaced
let node = parser.parse("Sheet3!AB31", &Some(context.clone())); let node = parser.parse("Sheet3!AB31", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -337,7 +335,7 @@ fn test_move_formula_wrong_reference() {
}, },
); );
assert_eq!(t, "Sheet3!AB31"); assert_eq!(t, "Sheet3!AB31");
let node = parser.parse("Sheet3!$X$9", &Some(context.clone())); let node = parser.parse("Sheet3!$X$9", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -352,7 +350,7 @@ fn test_move_formula_wrong_reference() {
); );
assert_eq!(t, "Sheet3!$X$9"); assert_eq!(t, "Sheet3!$X$9");
let node = parser.parse("SUM(Sheet3!D2:D3)", &Some(context.clone())); let node = parser.parse("SUM(Sheet3!D2:D3)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -379,7 +377,7 @@ fn test_move_formula_misc() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -389,7 +387,7 @@ fn test_move_formula_misc() {
width: 4, width: 4,
height: 5, height: 5,
}; };
let node = parser.parse("X9^C2-F4*H2", &Some(context.clone())); let node = parser.parse("X9^C2-F4*H2+SUM(F2:H4)+SUM(C2:F6)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -402,9 +400,9 @@ fn test_move_formula_misc() {
column_delta: 10, column_delta: 10,
}, },
); );
assert_eq!(t, "X9^M12-P14*H2"); assert_eq!(t, "X9^M12-P14*H2+SUM(F2:H4)+SUM(M12:P16)");
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone())); let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -419,7 +417,7 @@ fn test_move_formula_misc() {
); );
assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)"); assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)");
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", &Some(context.clone())); let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -447,7 +445,7 @@ fn test_move_formula_another_sheet() {
}; };
// we add two sheets and we cut/paste from Sheet1 to Sheet2 // we add two sheets and we cut/paste from Sheet1 to Sheet2
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -459,10 +457,7 @@ fn test_move_formula_another_sheet() {
}; };
// Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4 // Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4
let node = parser.parse( let node = parser.parse("AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)", context);
"AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)",
&Some(context.clone()),
);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -480,3 +475,77 @@ fn test_move_formula_another_sheet() {
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)" "Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
); );
} }
#[test]
fn move_formula_implicit_intersetion() {
// context is E4
let row = 4;
let column = 5;
let context = &CellReferenceRC {
sheet: "Sheet1".to_string(),
row,
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
sheet: 0,
row: 2,
column: 3,
width: 4,
height: 5,
};
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)", context);
let t = move_formula(
&node,
&MoveContext {
source_sheet_name: "Sheet1",
row,
column,
area,
target_sheet_name: "Sheet1",
row_delta: 10,
column_delta: 10,
},
);
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)");
}
#[test]
fn move_formula_implicit_intersetion_with_ranges() {
// context is E4
let row = 4;
let column = 5;
let context = &CellReferenceRC {
sheet: "Sheet1".to_string(),
row,
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
sheet: 0,
row: 2,
column: 3,
width: 4,
height: 5,
};
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)+SUM(@A1, @X9, @$D$5)", context);
let t = move_formula(
&node,
&MoveContext {
source_sheet_name: "Sheet1",
row,
column,
area,
target_sheet_name: "Sheet1",
row_delta: 10,
column_delta: 10,
},
);
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)+SUM(@A1,@X9,@$N$15)");
}

View File

@@ -2,9 +2,9 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode; use crate::expressions::lexer::LexerMode;
use super::super::parser::stringify::{to_rc_format, to_string}; use crate::expressions::parser::stringify::{to_rc_format, to_string};
use super::super::types::CellReferenceRC; use crate::expressions::parser::Parser;
use super::Parser; use crate::expressions::types::CellReferenceRC;
struct Formula<'a> { struct Formula<'a> {
formula_a1: &'a str, formula_a1: &'a str,
@@ -14,7 +14,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_formulas_with_full_ranges() { fn test_parser_formulas_with_full_ranges() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -52,11 +52,11 @@ fn test_parser_formulas_with_full_ranges() {
for formula in &formulas { for formula in &formulas {
let t = parser.parse( let t = parser.parse(
formula.formula_a1, formula.formula_a1,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_rc_format(&t), formula.formula_r1c1); assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1); assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
@@ -67,11 +67,11 @@ fn test_parser_formulas_with_full_ranges() {
for formula in &formulas { for formula in &formulas {
let t = parser.parse( let t = parser.parse(
formula.formula_r1c1, formula.formula_r1c1,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_rc_format(&t), formula.formula_r1c1); assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1); assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
@@ -81,7 +81,7 @@ fn test_parser_formulas_with_full_ranges() {
#[test] #[test]
fn test_range_inverse_order() { fn test_range_inverse_order() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -93,7 +93,7 @@ fn test_range_inverse_order() {
// D4:C2 => C2:D4 // D4:C2 => C2:D4
let t = parser.parse( let t = parser.parse(
"SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)", "SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)",
&Some(cell_reference.clone()), &cell_reference,
); );
assert_eq!( assert_eq!(
to_string(&t, &cell_reference), to_string(&t, &cell_reference),

View File

@@ -0,0 +1,70 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
#[test]
fn exp_order() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(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("(1 + 2)^3 + 4", &cell_reference);
assert_eq!(to_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");
let t = parser.parse("(C5 + 3)^(R4*6)", &cell_reference);
assert_eq!(to_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");
let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4");
}
#[test]
fn correct_parenthesis() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(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_string(&t, &cell_reference), "-(1+1)");
let t = parser.parse("1 - (3 + 4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-(3+4)");
let t = parser.parse("-(1.05*(0.0284 + 0.0046) - 0.0284)", &cell_reference);
assert_eq!(
to_string(&t, &cell_reference),
"-(1.05*(0.0284+0.0046)-0.0284)"
);
let t = parser.parse("1 + (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1+3+5");
let t = parser.parse("1 - (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-(3+5)");
let t = parser.parse("(1 - 3) - (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-3-(3+5)");
let t = parser.parse("1 + (3<5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1+(3<5)");
}

View File

@@ -3,12 +3,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string; use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
use crate::expressions::utils::{number_to_column, parse_reference_a1}; use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo}; use crate::types::{Table, TableColumn, TableStyleInfo};
use super::super::types::CellReferenceRC;
use super::Parser;
fn create_test_table( fn create_test_table(
table_name: &str, table_name: &str,
column_names: &[&str], column_names: &[&str],
@@ -63,7 +62,7 @@ fn simple_table() {
let row_count = 3; let row_count = 3;
let tables = create_test_table("tblIncome", &column_names, "A1", row_count); let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
let mut parser = Parser::new(worksheets, tables); let mut parser = Parser::new(worksheets, vec![], tables);
// Reference cell is 'Sheet One'!F2 // Reference cell is 'Sheet One'!F2
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: "Sheet One".to_string(), sheet: "Sheet One".to_string(),
@@ -72,7 +71,7 @@ fn simple_table() {
}; };
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])"; let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
let t = parser.parse(formula, &Some(cell_reference.clone())); let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)"); assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
// Cell A3 // Cell A3
@@ -82,7 +81,7 @@ fn simple_table() {
column: 1, column: 1,
}; };
let formula = "SUBTOTAL(109, tblIncome[Jan])"; let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &Some(cell_reference.clone())); let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)"); assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
// Cell A3 in 'Second Sheet' // Cell A3 in 'Second Sheet'
@@ -92,7 +91,7 @@ fn simple_table() {
column: 1, column: 1,
}; };
let formula = "SUBTOTAL(109, tblIncome[Jan])"; let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &Some(cell_reference.clone())); let t = parser.parse(formula, &cell_reference);
assert_eq!( assert_eq!(
to_string(&t, &cell_reference), to_string(&t, &cell_reference),
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)" "SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"

View File

@@ -1,276 +0,0 @@
use super::{move_formula::ref_is_in_area, Node};
use crate::expressions::types::{Area, CellReferenceIndex};
pub(crate) fn forward_references(
node: &mut Node,
context: &CellReferenceIndex,
source_area: &Area,
target_sheet: u32,
target_sheet_name: &str,
target_row: i32,
target_column: i32,
) {
match node {
Node::ReferenceKind {
sheet_name,
sheet_index: reference_sheet,
absolute_row,
absolute_column,
row: reference_row,
column: reference_column,
} => {
let reference_row_absolute = if *absolute_row {
*reference_row
} else {
*reference_row + context.row
};
let reference_column_absolute = if *absolute_column {
*reference_column
} else {
*reference_column + context.column
};
if ref_is_in_area(
*reference_sheet,
reference_row_absolute,
reference_column_absolute,
source_area,
) {
if *reference_sheet != target_sheet {
*sheet_name = Some(target_sheet_name.to_string());
*reference_sheet = target_sheet;
}
*reference_row = target_row + *reference_row - source_area.row;
*reference_column = target_column + *reference_column - source_area.column;
}
}
Node::RangeKind {
sheet_name,
sheet_index,
absolute_row1,
absolute_column1,
row1,
column1,
absolute_row2,
absolute_column2,
row2,
column2,
} => {
let reference_row1 = if *absolute_row1 {
*row1
} else {
*row1 + context.row
};
let reference_column1 = if *absolute_column1 {
*column1
} else {
*column1 + context.column
};
let reference_row2 = if *absolute_row2 {
*row2
} else {
*row2 + context.row
};
let reference_column2 = if *absolute_column2 {
*column2
} else {
*column2 + context.column
};
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
{
if *sheet_index != target_sheet {
*sheet_index = target_sheet;
*sheet_name = Some(target_sheet_name.to_string());
}
*row1 = target_row + *row1 - source_area.row;
*column1 = target_column + *column1 - source_area.column;
*row2 = target_row + *row2 - source_area.row;
*column2 = target_column + *column2 - source_area.column;
}
}
// Recurse
Node::OpRangeKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpConcatenateKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpPowerKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
forward_references(
arg,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
forward_references(
arg,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::UnaryKind { kind: _, right } => {
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
// TODO: Not implemented
Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {}
Node::VariableKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {}
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
}
}

View File

@@ -2,7 +2,6 @@ use std::fmt;
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::language::Language; use crate::language::Language;
@@ -81,8 +80,7 @@ impl fmt::Display for OpProduct {
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+") /// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc /// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
/// Note that they are serialized/deserialized by index /// Note that they are serialized/deserialized by index
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
pub enum Error { pub enum Error {
REF, REF,
NAME, NAME,
@@ -120,7 +118,7 @@ impl Error {
pub fn to_localized_error_string(&self, language: &Language) -> String { pub fn to_localized_error_string(&self, language: &Language) -> String {
match self { match self {
Error::NULL => language.errors.null.to_string(), Error::NULL => language.errors.null.to_string(),
Error::REF => language.errors.ref_value.to_string(), Error::REF => language.errors.r#ref.to_string(),
Error::NAME => language.errors.name.to_string(), Error::NAME => language.errors.name.to_string(),
Error::VALUE => language.errors.value.to_string(), Error::VALUE => language.errors.value.to_string(),
Error::DIV => language.errors.div.to_string(), Error::DIV => language.errors.div.to_string(),
@@ -137,7 +135,7 @@ impl Error {
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> { pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
let errors = &language.errors; let errors = &language.errors;
if name == errors.ref_value { if name == errors.r#ref {
return Some(Error::REF); return Some(Error::REF);
} else if name == errors.name { } else if name == errors.name {
return Some(Error::NAME); return Some(Error::NAME);
@@ -199,7 +197,7 @@ pub fn is_english_error_string(name: &str) -> bool {
"#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!", "#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!",
"#CALC!", "#CIRC!", "#NULL!", "#CALC!", "#CIRC!", "#NULL!",
]; ];
names.iter().any(|e| *e == name) names.contains(&name)
} }
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -242,6 +240,7 @@ pub enum TokenType {
Bang, // ! Bang, // !
Percent, // % Percent, // %
And, // & And, // &
At, // @
Reference { Reference {
sheet: Option<String>, sheet: Option<String>,
row: i32, 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 { pub fn is_valid_identifier(name: &str) -> bool {
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1 // https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
// https://github.com/MartinTrummer/excel-names/ // 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 upper = name.to_ascii_uppercase();
let bytes = upper.as_bytes(); // length of chars
let len = bytes.len(); let len = upper.chars().count();
let mut chars = upper.chars();
if len > 255 || len == 0 { if len > 255 || len == 0 {
return false; 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 (\). // The first character of a name must be a letter, an underscore character (_), or a backslash (\).
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') { if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
return false; return false;
@@ -237,21 +241,11 @@ pub fn is_valid_identifier(name: &str) -> bool {
if parse_reference_r1c1(name).is_some() { if parse_reference_r1c1(name).is_some() {
return false; return false;
} }
let mut i = 1; for ch in chars {
while i < len { if !(ch.is_alphanumeric() || ch == '_' || ch == '.') {
let ch = bytes[i] as char;
match ch {
'a'..='z' => {}
'A'..='Z' => {}
'0'..='9' => {}
'_' => {}
'.' => {}
_ => {
return false; return false;
} }
} }
i += 1;
}
true true
} }
@@ -259,15 +253,23 @@ pub fn is_valid_identifier(name: &str) -> bool {
fn name_needs_quoting(name: &str) -> bool { fn name_needs_quoting(name: &str) -> bool {
let chars = name.chars(); let chars = name.chars();
// it contains any of these characters: ()'$,;-+{} or space // it contains any of these characters: ()'$,;-+{} or space
for char in chars { for (i, char) in chars.enumerate() {
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) { if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
return true; return true;
} }
// if it starts with a number
if i == 0 && char.is_ascii_digit() {
return true;
} }
// TODO: }
if parse_reference_a1(name).is_some() {
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not // 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 // cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
// integers return true;
}
false false
} }
@@ -279,3 +281,32 @@ pub fn quote_name(name: &str) -> String {
}; };
name.to_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("_."));
assert!(is_valid_identifier("_1")); assert!(is_valid_identifier("_1"));
assert!(is_valid_identifier("\\.")); assert!(is_valid_identifier("\\."));
assert!(is_valid_identifier("truñe"));
// invalid // invalid
assert!(!is_valid_identifier("true")); assert!(!is_valid_identifier("true"));
@@ -209,6 +210,7 @@ fn test_names() {
assert!(!is_valid_identifier("1true")); assert!(!is_valid_identifier("1true"));
assert!(!is_valid_identifier("test€")); assert!(!is_valid_identifier("test€"));
assert!(!is_valid_identifier("truñe"));
assert!(!is_valid_identifier("tr&ue")); assert!(!is_valid_identifier("tr&ue"));
assert!(!is_valid_identifier("LOG10"));
} }

View File

@@ -1,17 +1,158 @@
use chrono::Datelike; use chrono::Datelike;
use chrono::Days;
use chrono::Duration; use chrono::Duration;
use chrono::Months;
use chrono::NaiveDate; use chrono::NaiveDate;
use crate::constants::EXCEL_DATE_BASE; use crate::constants::EXCEL_DATE_BASE;
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
pub fn from_excel_date(days: i64) -> NaiveDate { 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
}
fn is_date_within_range(date: NaiveDate) -> bool {
convert_to_serial_number(date) >= MINIMUM_DATE_SERIAL_NUMBER
&& convert_to_serial_number(date) <= MAXIMUM_DATE_SERIAL_NUMBER
}
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}"
));
};
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!(
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
));
};
#[allow(clippy::expect_used)]
let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate"); let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate");
dt + Duration::days(days - 2) Ok(dt + Duration::days(days - 2))
} }
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> { pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
match NaiveDate::from_ymd_opt(year, month, day) { match NaiveDate::from_ymd_opt(year, month, day) {
Some(native_date) => Ok(native_date.num_days_from_ce() - EXCEL_DATE_BASE), 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()),
}
}
pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Result<i32, String> {
// Excel parses `DATE` very permissively. It allows not just for valid date values, but it
// allows for invalid dates as well. If you for example enter `DATE(1900, 1, 32)` it will
// return the date `1900-02-01`. Despite giving a day that is out of range it will just
// wrap the month and year around.
//
// This function applies that same logic to dates. And does it in the most compatible way as
// possible.
// Special case for the minimum date
if year == 1899 && month == 12 && day == 31 {
return Ok(MINIMUM_DATE_SERIAL_NUMBER);
}
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
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
// but tried to increment the months so that it wraps around into within range, excel
// would still return an error.
//
// I.E. DATE(0,13,-1) will return an error, despite it being equivalent to DATE(1,1,0) which
// is within range.
//
// 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(DATE_OUT_OF_RANGE_MESSAGE.to_string());
}
date = {
let month_diff = month - 1;
let abs_month = month_diff.unsigned_abs();
if month_diff <= 0 {
date = date - Months::new(abs_month);
} else {
date = date + Months::new(abs_month);
}
if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
}
date
};
date = {
let day_diff = day - 1;
let abs_day = day_diff.unsigned_abs() as u64;
if day_diff <= 0 {
date = date - Days::new(abs_day);
} else {
date = date + Days::new(abs_day);
}
if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
}
date
};
Ok(convert_to_serial_number(date))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permissive_date_to_serial_number() {
assert_eq!(
permissive_date_to_serial_number(42, 42, 2002),
date_to_serial_number(12, 7, 2005)
);
assert_eq!(
permissive_date_to_serial_number(1, 42, 2002),
date_to_serial_number(1, 6, 2005)
);
assert_eq!(
permissive_date_to_serial_number(1, 15, 2000),
date_to_serial_number(1, 3, 2001)
);
assert_eq!(
permissive_date_to_serial_number(1, 49, 2000),
date_to_serial_number(1, 1, 2004)
);
assert_eq!(
permissive_date_to_serial_number(1, 49, 2000),
date_to_serial_number(1, 1, 2004)
);
assert_eq!(
permissive_date_to_serial_number(31, 49, 2000),
date_to_serial_number(31, 1, 2004)
);
assert_eq!(
permissive_date_to_serial_number(256, 49, 2000),
date_to_serial_number(12, 9, 2004)
);
assert_eq!(
permissive_date_to_serial_number(256, 1, 2004),
date_to_serial_number(12, 9, 2004)
);
}
#[test]
fn test_max_and_min_dates() {
assert_eq!(
permissive_date_to_serial_number(31, 12, 9999),
Ok(MAXIMUM_DATE_SERIAL_NUMBER),
);
assert_eq!(
permissive_date_to_serial_number(31, 12, 1899),
Ok(MINIMUM_DATE_SERIAL_NUMBER),
);
} }
} }

View File

@@ -15,7 +15,7 @@ pub struct Formatted {
/// Returns the vector of chars of the fractional part of a *positive* number: /// Returns the vector of chars of the fractional part of a *positive* number:
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6'] /// 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) let b = format!("{:.1$}", value.fract(), precision as usize)
.chars() .chars()
.collect::<Vec<char>>(); .collect::<Vec<char>>();
@@ -30,6 +30,12 @@ fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
if last_non_zero < 2 { if last_non_zero < 2 {
return vec![]; 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() b[2..last_non_zero].to_vec()
} }
@@ -120,7 +126,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
// We should have different codepaths for general formatting and errors // We should have different codepaths for general formatting and errors
let value_abs = value.abs(); let value_abs = value.abs();
if (1.0e-8..1.0e+11).contains(&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(); text = text.trim_end_matches('0').trim_end_matches('.').to_string();
Formatted { Formatted {
text, text,
@@ -138,7 +144,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let exponent = value_abs.log10().floor(); let exponent = value_abs.log10().floor();
value /= 10.0_f64.powf(exponent); value /= 10.0_f64.powf(exponent);
let sign = if exponent < 0.0 { '-' } else { '+' }; let sign = if exponent < 0.0 { '-' } else { '+' };
let s = format!("{:.5}", value); let s = format!("{value:.5}");
Formatted { Formatted {
text: format!( text: format!(
"{}E{}{:02}", "{}E{}{:02}",
@@ -154,47 +160,72 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Date(p) => { ParsePart::Date(p) => {
let tokens = &p.tokens; let tokens = &p.tokens;
let mut text = "".to_string(); let mut text = "".to_string();
if !(1.0..=2_958_465.0).contains(&value) { let time_fract = value.fract();
// 2_958_465 is 31 December 9999 let hours = (time_fract * 24.0).floor();
return Formatted { let minutes = ((time_fract * 24.0 - hours) * 60.0).floor();
text: "#VALUE!".to_owned(), let seconds = ((((time_fract * 24.0 - hours) * 60.0) - minutes) * 60.0).round();
color: None, let date = from_excel_date(value as i64).ok();
error: Some("Date negative or too long".to_owned()),
};
}
let date = from_excel_date(value as i64);
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
text = format!("{}{}", text, c); text = format!("{text}{c}");
} }
TextToken::Text(t) => { TextToken::Text(t) => {
text = format!("{}{}", text, t); text = format!("{text}{t}");
} }
TextToken::Ghost(_) => { TextToken::Ghost(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{} ", text); text = format!("{text} ");
} }
TextToken::Spacer(_) => { TextToken::Spacer(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{} ", text); text = format!("{text} ");
} }
TextToken::Raw => { TextToken::Raw => {
text = format!("{}{}", text, value); text = format!("{text}{value}");
} }
TextToken::Digit(_) => {} TextToken::Digit(_) => {}
TextToken::Period => {} TextToken::Period => {}
TextToken::Day => { TextToken::Day => match date {
Some(date) => {
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{}{}", text, day); text = format!("{text}{day}");
} }
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
},
TextToken::DayPadded => { 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; let day = date.day() as usize;
text = format!("{}{:02}", text, day); text = format!("{text}{day:02}");
} }
TextToken::DayNameShort => { 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; let mut day = date.weekday().number_from_monday() as usize;
if day == 7 { if day == 7 {
day = 0; day = 0;
@@ -202,6 +233,16 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names_short[day]); text = format!("{}{}", text, &locale.dates.day_names_short[day]);
} }
TextToken::DayName => { 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; let mut day = date.weekday().number_from_monday() as usize;
if day == 7 { if day == 7 {
day = 0; day = 0;
@@ -209,32 +250,144 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names[day]); text = format!("{}{}", text, &locale.dates.day_names[day]);
} }
TextToken::Month => { 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; let month = date.month() as usize;
text = format!("{}{}", text, month); text = format!("{text}{month}");
} }
TextToken::MonthPadded => { 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; let month = date.month() as usize;
text = format!("{}{:02}", text, month); text = format!("{text}{month:02}");
} }
TextToken::MonthNameShort => { 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; let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months_short[month - 1]); text = format!("{}{}", text, &locale.dates.months_short[month - 1]);
} }
TextToken::MonthName => { 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; let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months[month - 1]); text = format!("{}{}", text, &locale.dates.months[month - 1]);
} }
TextToken::MonthLetter => { 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 month = date.month() as usize;
let months_letter = &locale.dates.months_letter[month - 1]; let months_letter = &locale.dates.months_letter[month - 1];
text = format!("{}{}", text, months_letter); text = format!("{text}{months_letter}");
} }
TextToken::YearShort => { 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")); text = format!("{}{}", text, date.format("%y"));
} }
TextToken::Year => { 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()); 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 { Formatted {
@@ -245,6 +398,9 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} }
ParsePart::Number(p) => { ParsePart::Number(p) => {
let mut text = "".to_string(); let mut text = "".to_string();
if let Some(c) = p.currency {
text = format!("{c}");
}
let tokens = &p.tokens; let tokens = &p.tokens;
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma)); value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
// p.precision is the number of significant digits _after_ the decimal point // p.precision is the number of significant digits _after_ the decimal point
@@ -273,7 +429,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if value_abs as i64 == 0 { if value_abs as i64 == 0 {
int_part = vec![]; 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 // ln is the number of digits of the integer part of the value
let ln = int_part.len() as i32; let ln = int_part.len() as i32;
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point // digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
@@ -291,26 +447,26 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
text = format!("{}{}", text, c); text = format!("{text}{c}");
} }
TextToken::Text(t) => { TextToken::Text(t) => {
text = format!("{}{}", text, t); text = format!("{text}{t}");
} }
TextToken::Ghost(_) => { TextToken::Ghost(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{} ", text); text = format!("{text} ");
} }
TextToken::Spacer(_) => { TextToken::Spacer(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{} ", text); text = format!("{text} ");
} }
TextToken::Raw => { TextToken::Raw => {
text = format!("{}{}", text, value); text = format!("{text}{value}");
} }
TextToken::Period => { TextToken::Period => {
text = format!("{}{}", text, decimal_separator); text = format!("{text}{decimal_separator}");
} }
TextToken::Digit(digit) => { TextToken::Digit(digit) => {
if digit.number == 'i' { if digit.number == 'i' {
@@ -318,7 +474,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let index = digit.index; let index = digit.index;
let number_index = ln - digit_count + index; let number_index = ln - digit_count + index;
if index == 0 && is_negative { if index == 0 && is_negative {
text = format!("-{}", text); text = format!("-{text}");
} }
if ln <= digit_count { if ln <= digit_count {
// The number of digits is less or equal than the number of digit tokens // The number of digits is less or equal than the number of digit tokens
@@ -343,7 +499,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} else { } else {
"" ""
}; };
text = format!("{}{}{}", text, c, sep); text = format!("{text}{c}{sep}");
} }
digit_index += 1; digit_index += 1;
} else { } else {
@@ -369,18 +525,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if index < fract_part.len() { if index < fract_part.len() {
text = format!("{}{}", text, fract_part[index]); text = format!("{}{}", text, fract_part[index]);
} else if digit.kind == '0' { } else if digit.kind == '0' {
text = format!("{}0", text); text = format!("{text}0");
} else if digit.kind == '?' { } else if digit.kind == '?' {
text = format!("{} ", text); text = format!("{text} ");
} }
} else if digit.number == 'e' { } else if digit.number == 'e' {
// 3. Exponent part // 3. Exponent part
let index = digit.index; let index = digit.index;
if index == 0 { if index == 0 {
if exponent_is_negative { if exponent_is_negative {
text = format!("{}E-", text); text = format!("{text}E-");
} else { } else {
text = format!("{}E+", text); text = format!("{text}E+");
} }
} }
let number_index = l_exp - (p.exponent_digit_count - index); let number_index = l_exp - (p.exponent_digit_count - index);
@@ -396,7 +552,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
exponent_part[number_index as usize] exponent_part[number_index as usize]
}; };
text = format!("{}{}", text, c); text = format!("{text}{c}");
} }
} else { } else {
for i in 0..number_index + 1 { for i in 0..number_index + 1 {
@@ -418,6 +574,13 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => {} TextToken::MonthLetter => {}
TextToken::YearShort => {} TextToken::YearShort => {}
TextToken::Year => {} TextToken::Year => {}
TextToken::Hour => {}
TextToken::HourPadded => {}
TextToken::Minute => {}
TextToken::MinutePadded => {}
TextToken::Second => {}
TextToken::SecondPadded => {}
TextToken::AMPM => {}
} }
} }
Formatted { Formatted {
@@ -587,10 +750,10 @@ fn parse_date(value: &str) -> Result<(i32, String), String> {
/// "30.34%" => (0.3034, "0.00%") /// "30.34%" => (0.3034, "0.00%")
/// 100€ => (100, "100€") /// 100€ => (100, "100€")
pub(crate) fn parse_formatted_number( pub(crate) fn parse_formatted_number(
value: &str, original: &str,
currencies: &[&str], currencies: &[&str],
) -> Result<(f64, Option<String>), String> { ) -> Result<(f64, Option<String>), String> {
let value = value.trim(); let value = original.trim();
let scientific_format = "0.00E+00"; let scientific_format = "0.00E+00";
// Check if it is a percentage // Check if it is a percentage
@@ -610,7 +773,7 @@ pub(crate) fn parse_formatted_number(
// check if it is a currency in currencies // check if it is a currency in currencies
for currency in currencies { for currency in currencies {
if let Some(p) = value.strip_prefix(&format!("-{}", currency)) { if let Some(p) = value.strip_prefix(&format!("-{currency}")) {
let (f, options) = parse_number(p.trim())?; let (f, options) = parse_number(p.trim())?;
if options.is_scientific { if options.is_scientific {
return Ok((f, Some(scientific_format.to_string()))); return Ok((f, Some(scientific_format.to_string())));
@@ -642,7 +805,8 @@ 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))); return Ok((serial_number as f64, Some(format)));
} }

View File

@@ -10,6 +10,7 @@ pub struct Lexer {
pub enum Token { pub enum Token {
Color(i32), // [Red] or [Color 23] Color(i32), // [Red] or [Color 23]
Condition(Compare, f64), // [<=100] (Comparator, number) Condition(Compare, f64), // [<=100] (Comparator, number)
Currency(char), // [$€] ($ currency symbol)
Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X
Spacer(char), // *X Spacer(char), // *X
Ghost(char), // _X Ghost(char), // _X
@@ -25,19 +26,23 @@ pub enum Token {
Scientific, // E+ Scientific, // E+
ScientificMinus, // E- ScientificMinus, // E-
General, // General General, // General
// Dates // Dates and time
Day, // d Day, // d
DayPadded, // dd DayPadded, // dd
DayNameShort, // ddd DayNameShort, // ddd
DayName, // dddd+ DayName, // dddd+
Month, // m Month, // m (or minute)
MonthPadded, // mm MonthPadded, // mm (or minute padded)
MonthNameShort, // mmm MonthNameShort, // mmm
MonthName, // mmmm or mmmmmm+ MonthName, // mmmm or mmmmmm+
MonthLetter, // mmmmm MonthLetter, // mmmmm
YearShort, // y or yy YearShort, // y or yy
Year, // yyy+ Year, // yyy+
// TODO: Hours Minutes and Seconds Hour, // h
HourPadded, // hh
Second, // s
SecondPadded, // ss
AMPM, // AM/PM (or A/P)
ILLEGAL, ILLEGAL,
EOF, EOF,
} }
@@ -177,10 +182,7 @@ impl Lexer {
} }
} }
self.position = position; self.position = position;
match chars.parse::<f64>() { chars.parse::<f64>().ok()
Err(_) => None,
Ok(v) => Some(v),
}
} }
fn consume_condition(&mut self) -> Option<(Compare, f64)> { fn consume_condition(&mut self) -> Option<(Compare, f64)> {
@@ -274,6 +276,15 @@ impl Lexer {
self.set_error("Failed to parse condition"); self.set_error("Failed to parse condition");
Token::ILLEGAL Token::ILLEGAL
} }
} else if c == '$' {
// currency
self.read_next_char();
if let Some(currency) = self.read_next_char() {
self.read_next_char();
return Token::Currency(currency);
}
self.set_error("Failed to parse currency");
Token::ILLEGAL
} else { } else {
// Color // Color
if let Some(index) = self.consume_color() { if let Some(index) = self.consume_color() {
@@ -326,7 +337,7 @@ impl Lexer {
} else if s == '-' { } else if s == '-' {
Token::ScientificMinus Token::ScientificMinus
} else { } else {
self.set_error(&format!("Unexpected char: {}. Expected + or -", s)); self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
Token::ILLEGAL Token::ILLEGAL
} }
} else { } else {
@@ -354,8 +365,8 @@ impl Lexer {
self.read_next_char(); self.read_next_char();
} }
match m { match m {
1 => Token::Month, 1 => Token::Month, // (or minute)
2 => Token::MonthPadded, 2 => Token::MonthPadded, // (or minute padded)
3 => Token::MonthNameShort, 3 => Token::MonthNameShort,
4 => Token::MonthName, 4 => Token::MonthName,
5 => Token::MonthLetter, 5 => Token::MonthLetter,
@@ -374,18 +385,75 @@ impl Lexer {
Token::Year 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' => { 'g' | 'G' => {
for c in "eneral".chars() { for c in "eneral".chars() {
let cc = self.read_next_char(); let cc = self.read_next_char();
if Some(c) != cc { if Some(c) != cc {
self.set_error(&format!("Unexpected character: {}", x)); self.set_error(&format!("Unexpected character: {x}"));
return Token::ILLEGAL; return Token::ILLEGAL;
} }
} }
Token::General Token::General
} }
_ => { _ => {
self.set_error(&format!("Unexpected character: {}", x)); self.set_error(&format!("Unexpected character: {x}"));
Token::ILLEGAL Token::ILLEGAL
} }
}, },

View File

@@ -74,6 +74,7 @@ mod test;
// //
// * Color [Red] or [Color 23] or [Color23] // * Color [Red] or [Color 23] or [Color23]
// * Conditions [<100] // * Conditions [<100]
// * Currency [$€]
// * Space _X when X is any given char // * Space _X when X is any given char
// * A spacer of chars: *X where X is repeated as much as possible // * A spacer of chars: *X where X is repeated as much as possible
// * Literals: $, (, ), :, +, - and space // * Literals: $, (, ), :, +, - and space

View File

@@ -27,6 +27,13 @@ pub enum TextToken {
MonthLetter, MonthLetter,
YearShort, YearShort,
Year, Year,
Hour,
HourPadded,
Minute,
MinutePadded,
Second,
SecondPadded,
AMPM,
} }
pub struct NumberPart { pub struct NumberPart {
pub color: Option<i32>, pub color: Option<i32>,
@@ -40,10 +47,12 @@ pub struct NumberPart {
pub is_scientific: bool, pub is_scientific: bool,
pub scientific_minus: bool, pub scientific_minus: bool,
pub exponent_digit_count: i32, pub exponent_digit_count: i32,
pub currency: Option<char>,
} }
pub struct DatePart { pub struct DatePart {
pub color: Option<i32>, pub color: Option<i32>,
pub use_ampm: bool,
pub tokens: Vec<TextToken>, pub tokens: Vec<TextToken>,
} }
@@ -100,6 +109,7 @@ impl Parser {
let mut digit_count = 0; let mut digit_count = 0;
let mut precision = 0; let mut precision = 0;
let mut is_date = false; let mut is_date = false;
let mut use_ampm = false;
let mut is_number = false; let mut is_number = false;
let mut found_decimal_dot = false; let mut found_decimal_dot = false;
let mut use_thousands = false; let mut use_thousands = false;
@@ -114,6 +124,8 @@ impl Parser {
let mut exponent_digit_count = 0; let mut exponent_digit_count = 0;
let mut number = 'i'; let mut number = 'i';
let mut index = 0; let mut index = 0;
let mut currency = None;
let mut is_time = false;
while token != Token::EOF && token != Token::Separator { while token != Token::EOF && token != Token::Separator {
let next_token = self.lexer.next_token(); let next_token = self.lexer.next_token();
@@ -170,6 +182,9 @@ impl Parser {
Token::Condition(cmp, value) => { Token::Condition(cmp, value) => {
condition = Some((cmp, value)); condition = Some((cmp, value));
} }
Token::Currency(c) => {
currency = Some(c);
}
Token::QuestionMark => { Token::QuestionMark => {
tokens.push(TextToken::Digit(Digit { tokens.push(TextToken::Digit(Digit {
kind: '?', kind: '?',
@@ -195,6 +210,9 @@ impl Parser {
index += 1; index += 1;
} }
Token::Literal(value) => { Token::Literal(value) => {
if value == ':' {
is_time = true;
}
tokens.push(TextToken::Literal(value)); tokens.push(TextToken::Literal(value));
} }
Token::Text(value) => { Token::Text(value) => {
@@ -231,13 +249,23 @@ impl Parser {
tokens.push(TextToken::MonthName); tokens.push(TextToken::MonthName);
} }
Token::Month => { Token::Month => {
if is_time {
// minute
tokens.push(TextToken::Minute);
} else {
is_date = true; is_date = true;
tokens.push(TextToken::Month); tokens.push(TextToken::Month);
} }
}
Token::MonthPadded => { Token::MonthPadded => {
if is_time {
// minute padded
tokens.push(TextToken::MinutePadded);
} else {
is_date = true; is_date = true;
tokens.push(TextToken::MonthPadded); tokens.push(TextToken::MonthPadded);
} }
}
Token::MonthLetter => { Token::MonthLetter => {
is_date = true; is_date = true;
tokens.push(TextToken::MonthLetter); tokens.push(TextToken::MonthLetter);
@@ -250,6 +278,32 @@ impl Parser {
is_date = true; is_date = true;
tokens.push(TextToken::Year); 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 => { Token::Scientific => {
if !is_scientific { if !is_scientific {
index = 0; index = 0;
@@ -277,7 +331,11 @@ impl Parser {
if is_number { if is_number {
return ParsePart::Error(ErrorPart {}); return ParsePart::Error(ErrorPart {});
} }
ParsePart::Date(DatePart { color, tokens }) ParsePart::Date(DatePart {
color,
use_ampm,
tokens,
})
} else { } else {
ParsePart::Number(NumberPart { ParsePart::Number(NumberPart {
color, color,
@@ -291,6 +349,7 @@ impl Parser {
is_scientific, is_scientific,
scientific_minus, scientific_minus,
exponent_digit_count, exponent_digit_count,
currency,
}) })
} }
} }

View File

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

View File

@@ -76,6 +76,14 @@ fn test_color() {
assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4)); assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4));
} }
#[test]
fn dollar_euro() {
let locale = get_default_locale();
let format = "[$€]#,##0.00";
let t = format_number(3.1, format, locale);
assert_eq!(t.text, "€3.10");
}
#[test] #[test]
fn test_parts() { fn test_parts() {
let locale = get_default_locale(); let locale = get_default_locale();

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

@@ -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 Model {
// =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,10 +1,12 @@
use statrs::function::erf::{erf, erfc};
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{parser::Node, token::Error, types::CellReferenceIndex},
model::Model, 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 // https://root.cern/doc/v610/TMath_8cxx_source.html
// Notice that the parameters for Bessel functions in Excel and here have inverted order // Notice that the parameters for Bessel functions in Excel and here have inverted order
@@ -160,7 +162,7 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, 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 { pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -171,6 +173,6 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
CalcResult::Number(1.0 - erf(x)) CalcResult::Number(erfc(x))
} }
} }

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 // 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 // numbers > 1e-20 and scientific notation for the rest
let y_str = if y.abs() <= 9e-20 { let y_str = if y.abs() <= 9e-20 {
format!("{:E}", y) format!("{y:E}")
} else if y == 1.0 { } else if y == 1.0 {
"".to_string() "".to_string()
} else if y == -1.0 { } else if y == -1.0 {
"-".to_string() "-".to_string()
} else { } else {
format!("{}", y) format!("{y}")
}; };
let x_str = if x.abs() <= 9e-20 { let x_str = if x.abs() <= 9e-20 {
format!("{:E}", x) format!("{x:E}")
} else { } else {
format!("{}", x) format!("{x}")
}; };
if y == 0.0 && x == 0.0 { if y == 0.0 && x == 0.0 {
write!(f, "0") write!(f, "0")
@@ -188,10 +188,7 @@ impl Model {
node: &Node, node: &Node,
cell: CellReferenceIndex, cell: CellReferenceIndex,
) -> Result<(f64, f64, Suffix), CalcResult> { ) -> Result<(f64, f64, Suffix), CalcResult> {
let value = match self.get_string(node, cell) { let value = self.get_string(node, cell)?;
Ok(s) => s,
Err(s) => return Err(s),
};
if value.is_empty() { if value.is_empty() {
return Ok((0.0, 0.0, Suffix::I)); return Ok((0.0, 0.0, Suffix::I));
} }

View File

@@ -76,7 +76,7 @@ impl Model {
if value < 0 { if value < 0 {
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9)) CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
} else { } else {
let result = format!("{:X}", value); let result = format!("{value:X}");
if let Some(places) = places { if let Some(places) = places {
if places < result.len() as i32 { if places < result.len() as i32 {
return CalcResult::new_error( return CalcResult::new_error(
@@ -120,7 +120,7 @@ impl Model {
if value < 0 { if value < 0 {
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9)) CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
} else { } else {
let result = format!("{:o}", value); let result = format!("{value:o}");
if let Some(places) = places { if let Some(places) = places {
if places < result.len() as i32 { if places < result.len() as i32 {
return CalcResult::new_error( return CalcResult::new_error(
@@ -163,7 +163,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{:b}", value); let result = format!("{value:b}");
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -202,7 +202,7 @@ impl Model {
if value < 0 { if value < 0 {
value += HEX_MAX; value += HEX_MAX;
} }
let result = format!("{:X}", value); let result = format!("{value:X}");
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -242,7 +242,7 @@ impl Model {
if value < 0 { if value < 0 {
value += OCT_MAX; value += OCT_MAX;
} }
let result = format!("{:o}", value); let result = format!("{value:o}");
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -301,7 +301,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{:b}", value); let result = format!("{value:b}");
if let Some(places) = places { if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) { if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -391,7 +391,7 @@ impl Model {
if value < 0 { if value < 0 {
value += OCT_MAX; value += OCT_MAX;
} }
let result = format!("{:o}", value); let result = format!("{value:o}");
if let Some(places) = places { if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) { if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -446,7 +446,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{:b}", value); let result = format!("{value:b}");
if let Some(places) = places { if let Some(places) = places {
if value < 512 && places < result.len() as i32 { if value < 512 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -532,7 +532,7 @@ impl Model {
if value < 0 { if value < 0 {
value += HEX_MAX; value += HEX_MAX;
} }
let result = format!("{:X}", value); let result = format!("{value:X}");
if let Some(places) = places { if let Some(places) = places {
if value < HEX_MAX_HALF && places < result.len() as i32 { if value < HEX_MAX_HALF && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); 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_jn_yn;
mod bessel_k; mod bessel_k;
mod bessel_util; mod bessel_util;
mod erf;
#[cfg(test)] #[cfg(test)]
mod test_bessel; 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::jn as bessel_j;
pub(crate) use bessel_jn_yn::yn as bessel_y; pub(crate) use bessel_jn_yn::yn as bessel_y;
pub(crate) use bessel_k::bessel_k; pub(crate) use bessel_k::bessel_k;
pub(crate) use erf::erf;

View File

@@ -12,7 +12,7 @@ const EPS_LOW: f64 = 1e-6;
// Known values computed with Arb via Nemo.jl in Julia // Known values computed with Arb via Nemo.jl in Julia
// You can also use Mathematica // You can also use Mathematica
/// But please do not use Excel or any other software without arbitrary precision // But please do not use Excel or any other software without arbitrary precision
fn numbers_are_close(a: f64, b: f64) -> bool { fn numbers_are_close(a: f64, b: f64) -> bool {
if a == b { if a == b {

View File

@@ -2,7 +2,7 @@ use chrono::Datelike;
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW, MAXIMUM_DATE_SERIAL_NUMBER, MINIMUM_DATE_SERIAL_NUMBER},
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{parser::Node, token::Error, types::CellReferenceIndex},
formatter::dates::from_excel_date, formatter::dates::from_excel_date,
model::Model, model::Model,
@@ -13,37 +13,32 @@ use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr
// See: // See:
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx // https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
// FIXME: Is this enough? fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> {
fn is_valid_date(date: f64) -> bool { let end = from_excel_date(end_date)?;
date > 0.0 let start = from_excel_date(start_date)?;
}
fn is_less_than_one_year(start_date: i64, end_date: i64) -> bool {
if end_date - start_date < 365 { if end_date - start_date < 365 {
return true; return Ok(true);
} }
let end = from_excel_date(end_date);
let start = from_excel_date(start_date);
let end_year = end.year(); let end_year = end.year();
let start_year = start.year(); let start_year = start.year();
if end_year == start_year { if end_year == start_year {
return true; return Ok(true);
} }
if end_year != start_year + 1 { if end_year != start_year + 1 {
return false; return Ok(false);
} }
let start_month = start.month(); let start_month = start.month();
let end_month = end.month(); let end_month = end.month();
if end_month < start_month { if end_month < start_month {
return true; return Ok(true);
} }
if end_month > start_month { if end_month > start_month {
return false; return Ok(false);
} }
// we are one year later same month // we are one year later same month
let start_day = start.day(); let start_day = start.day();
let end_day = end.day(); let end_day = end.day();
end_day <= start_day Ok(end_day <= start_day)
} }
fn compute_payment( fn compute_payment(
@@ -89,6 +84,9 @@ fn compute_future_value(
if rate == 0.0 { if rate == 0.0 {
return Ok(-pv - pmt * nper); return Ok(-pv - pmt * nper);
} }
if rate == -1.0 && nper < 0.0 {
return Err((Error::DIV, "Divide by zero".to_string()));
}
let rate_nper = (1.0 + rate).powf(nper); let rate_nper = (1.0 + rate).powf(nper);
let fv = if period_start { let fv = if period_start {
@@ -194,16 +192,24 @@ fn compute_ppmt(
// In these formulas the payment (pmt) is normally negative // In these formulas the payment (pmt) is normally negative
impl Model { impl Model {
// FIXME: These three functions (get_array_of_numbers..) need to be refactored fn get_array_of_numbers_generic(
// They are really similar expect for small issues
fn get_array_of_numbers(
&mut self, &mut self,
arg: &Node, arg: &Node,
cell: &CellReferenceIndex, cell: &CellReferenceIndex,
accept_number_node: bool,
handle_empty_cell: impl Fn() -> Result<Option<f64>, CalcResult>,
handle_non_number_cell: impl Fn() -> Result<Option<f64>, CalcResult>,
) -> Result<Vec<f64>, CalcResult> { ) -> Result<Vec<f64>, CalcResult> {
let mut values = Vec::new(); let mut values = Vec::new();
match self.evaluate_node_in_context(arg, *cell) { match self.evaluate_node_in_context(arg, *cell) {
CalcResult::Number(value) => values.push(value), CalcResult::Number(value) if accept_number_node => values.push(value),
CalcResult::Number(_) => {
return Err(CalcResult::new_error(
Error::VALUE,
*cell,
"Expected range of numbers".to_string(),
));
}
CalcResult::Range { left, right } => { CalcResult::Range { left, right } => {
if left.sheet != right.sheet { if left.sheet != right.sheet {
return Err(CalcResult::new_error( return Err(CalcResult::new_error(
@@ -212,6 +218,7 @@ impl Model {
"Ranges are in different sheets".to_string(), "Ranges are in different sheets".to_string(),
)); ));
} }
let sheet = left.sheet;
let row1 = left.row; let row1 = left.row;
let mut row2 = right.row; let mut row2 = right.row;
let column1 = left.column; let column1 = left.column;
@@ -219,32 +226,46 @@ impl Model {
if row1 == 1 && row2 == LAST_ROW { if row1 == 1 && row2 == LAST_ROW {
row2 = self row2 = self
.workbook .workbook
.worksheet(left.sheet) .worksheet(sheet)
.expect("Sheet expected during evaluation.") .map_err(|_| {
CalcResult::new_error(
Error::ERROR,
*cell,
format!("Invalid worksheet index: '{sheet}'"),
)
})?
.dimension() .dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
column2 = self column2 = self
.workbook .workbook
.worksheet(left.sheet) .worksheet(sheet)
.expect("Sheet expected during evaluation.") .map_err(|_| {
CalcResult::new_error(
Error::ERROR,
*cell,
format!("Invalid worksheet index: '{sheet}'"),
)
})?
.dimension() .dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..=row2 {
for column in column1..(column2 + 1) { for column in column1..=column2 {
match self.evaluate_cell(CellReferenceIndex { let cell_ref = CellReferenceIndex { sheet, row, column };
sheet: left.sheet, match self.evaluate_cell(cell_ref) {
row, CalcResult::Number(value) => values.push(value),
column, error @ CalcResult::Error { .. } => return Err(error),
}) { CalcResult::EmptyCell => {
CalcResult::Number(value) => { if let Some(value) = handle_empty_cell()? {
values.push(value); values.push(value);
} }
error @ CalcResult::Error { .. } => return Err(error), }
_ => { _ => {
// We ignore booleans and strings if let Some(value) = handle_non_number_cell()? {
values.push(value);
}
} }
} }
} }
@@ -252,88 +273,51 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return Err(error), error @ CalcResult::Error { .. } => return Err(error),
_ => { _ => {
// We ignore booleans and strings handle_non_number_cell()?;
}
} }
};
Ok(values) Ok(values)
} }
fn get_array_of_numbers(
&mut self,
arg: &Node,
cell: &CellReferenceIndex,
) -> Result<Vec<f64>, CalcResult> {
self.get_array_of_numbers_generic(
arg,
cell,
true, // accept_number_node
|| Ok(None), // Ignore empty cells
|| Ok(None), // Ignore non-number cells
)
}
fn get_array_of_numbers_xpnv( fn get_array_of_numbers_xpnv(
&mut self, &mut self,
arg: &Node, arg: &Node,
cell: &CellReferenceIndex, cell: &CellReferenceIndex,
error: Error, error: Error,
) -> Result<Vec<f64>, CalcResult> { ) -> Result<Vec<f64>, CalcResult> {
let mut values = Vec::new(); self.get_array_of_numbers_generic(
match self.evaluate_node_in_context(arg, *cell) { arg,
CalcResult::Number(value) => values.push(value), cell,
CalcResult::Range { left, right } => { true, // accept_number_node
if left.sheet != right.sheet { || {
return Err(CalcResult::new_error( Err(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 = self
.workbook
.worksheet(left.sheet)
.expect("Sheet expected during evaluation.")
.dimension()
.max_row;
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = self
.workbook
.worksheet(left.sheet)
.expect("Sheet expected during evaluation.")
.dimension()
.max_column;
}
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) => {
values.push(value);
}
error @ CalcResult::Error { .. } => return Err(error),
CalcResult::EmptyCell => {
return Err(CalcResult::new_error(
Error::NUM, Error::NUM,
*cell, *cell,
"Expected number".to_string(), "Expected number".to_string(),
)); ))
} },
_ => { || {
return Err(CalcResult::new_error( Err(CalcResult::new_error(
error, error.clone(),
*cell, *cell,
"Expected number".to_string(), "Expected number".to_string(),
)); ))
} },
} )
}
}
}
error @ CalcResult::Error { .. } => return Err(error),
_ => {
return Err(CalcResult::new_error(
error,
*cell,
"Expected number".to_string(),
));
}
};
Ok(values)
} }
fn get_array_of_numbers_xirr( fn get_array_of_numbers_xirr(
@@ -341,69 +325,19 @@ impl Model {
arg: &Node, arg: &Node,
cell: &CellReferenceIndex, cell: &CellReferenceIndex,
) -> Result<Vec<f64>, CalcResult> { ) -> Result<Vec<f64>, CalcResult> {
let mut values = Vec::new(); self.get_array_of_numbers_generic(
match self.evaluate_node_in_context(arg, *cell) { arg,
CalcResult::Range { left, right } => { cell,
if left.sheet != right.sheet { false, // Do not accept a single number node
return Err(CalcResult::new_error( || Ok(Some(0.0)), // Treat empty cells as zero
Error::VALUE, || {
*cell, Err(CalcResult::new_error(
"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 = self
.workbook
.worksheet(left.sheet)
.expect("Sheet expected during evaluation.")
.dimension()
.max_row;
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = self
.workbook
.worksheet(left.sheet)
.expect("Sheet expected during evaluation.")
.dimension()
.max_column;
}
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) => {
values.push(value);
}
error @ CalcResult::Error { .. } => return Err(error),
CalcResult::EmptyCell => values.push(0.0),
_ => {
return Err(CalcResult::new_error(
Error::VALUE, Error::VALUE,
*cell, *cell,
"Expected number".to_string(), "Expected number".to_string(),
)); ))
} },
} )
}
}
}
error @ CalcResult::Error { .. } => return Err(error),
_ => {
return Err(CalcResult::new_error(
Error::VALUE,
*cell,
"Expected number".to_string(),
));
}
};
Ok(values)
} }
/// PMT(rate, nper, pv, [fv], [type]) /// PMT(rate, nper, pv, [fv], [type])
@@ -497,7 +431,7 @@ impl Model {
} }
if rate == -1.0 { if rate == -1.0 {
return CalcResult::Error { return CalcResult::Error {
error: Error::NUM, error: Error::DIV,
origin: cell, origin: cell,
message: "Rate must be != -1".to_string(), message: "Rate must be != -1".to_string(),
}; };
@@ -862,20 +796,28 @@ impl Model {
let column1 = left.column; let column1 = left.column;
let mut column2 = right.column; let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW { if row1 == 1 && row2 == LAST_ROW {
row2 = self row2 = match self.workbook.worksheet(left.sheet) {
.workbook Ok(s) => s.dimension().max_row,
.worksheet(left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension() Error::ERROR,
.max_row; cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
column2 = self column2 = match self.workbook.worksheet(left.sheet) {
.workbook Ok(s) => s.dimension().max_column,
.worksheet(left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension() Error::ERROR,
.max_column; cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {
for column in column1..(column2 + 1) { for column in column1..(column2 + 1) {
@@ -976,7 +918,9 @@ impl Model {
} }
let first_date = dates[0]; let first_date = dates[0];
for date in &dates { for date in &dates {
if !is_valid_date(*date) { if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
// Excel docs claim that if any number in dates is not a valid date, // Excel docs claim that if any number in dates is not a valid date,
// XNPV returns the #VALUE! error value, but it seems to return #VALUE! // XNPV returns the #VALUE! error value, but it seems to return #VALUE!
return CalcResult::new_error( return CalcResult::new_error(
@@ -1042,7 +986,9 @@ impl Model {
} }
let first_date = dates[0]; let first_date = dates[0];
for date in &dates { for date in &dates {
if !is_valid_date(*date) { if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1393,21 +1339,21 @@ impl Model {
CalcResult::Number(result) CalcResult::Number(result)
} }
/// This next three functions deal with Treasure Bills or T-Bills for short // This next three functions deal with Treasure Bills or T-Bills for short
/// They are zero-coupon that mature in one year or less. // They are zero-coupon that mature in one year or less.
/// Definitions: // Definitions:
/// $r$ be the discount rate // $r$ be the discount rate
/// $v$ the face value of the Bill // $v$ the face value of the Bill
/// $p$ the price of the Bill // $p$ the price of the Bill
/// $d_m$ is the number of days from the settlement to maturity // $d_m$ is the number of days from the settlement to maturity
/// Then: // Then:
/// $$ p = v \times\left(1-\frac{d_m}{r}\right) $$ // $$ p = v \times\left(1-\frac{d_m}{r}\right) $$
/// If d_m is less than 183 days the he Bond Equivalent Yield (BEY, here $y$) is given by: // If d_m is less than 183 days the he Bond Equivalent Yield (BEY, here $y$) is given by:
/// $$ y = \frac{F - B}{M}\times \frac{365}{d_m} = \frac{365\times r}{360-r\times d_m} // $$ y = \frac{F - B}{M}\times \frac{365}{d_m} = \frac{365\times r}{360-r\times d_m}
/// If d_m>= 183 days things are a bit more complicated. // If d_m>= 183 days things are a bit more complicated.
/// Let $d_e = d_m - 365/2$ if $d_m <= 365$ or $d_e = 183$ if $d_m = 366$. // Let $d_e = d_m - 365/2$ if $d_m <= 365$ or $d_e = 183$ if $d_m = 366$.
/// $$ v = p\times \left(1+\frac{y}{2}\right)\left(1+d_e\times\frac{y}{365}\right) $$ // $$ v = p\times \left(1+\frac{y}{2}\right)\left(1+d_e\times\frac{y}{365}\right) $$
/// Together with the previous relation of $p$ and $v$ gives us a quadratic equation for $y$. // Together with the previous relation of $p$ and $v$ gives us a quadratic equation for $y$.
// TBILLEQ(settlement, maturity, discount) // TBILLEQ(settlement, maturity, discount)
pub(crate) fn fn_tbilleq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_tbilleq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -1426,9 +1372,10 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
if !is_valid_date(settlement) || !is_valid_date(maturity) { let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()); Ok(f) => f,
} Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
};
if settlement > maturity { if settlement > maturity {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
@@ -1436,7 +1383,7 @@ impl Model {
"settlement should be <= maturity".to_string(), "settlement should be <= maturity".to_string(),
); );
} }
if !is_less_than_one_year(settlement as i64, maturity as i64) { if !less_than_one_year {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1490,9 +1437,10 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
if !is_valid_date(settlement) || !is_valid_date(maturity) { let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()); Ok(f) => f,
} Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
};
if settlement > maturity { if settlement > maturity {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
@@ -1500,7 +1448,7 @@ impl Model {
"settlement should be <= maturity".to_string(), "settlement should be <= maturity".to_string(),
); );
} }
if !is_less_than_one_year(settlement as i64, maturity as i64) { if !less_than_one_year {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1540,9 +1488,10 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
if !is_valid_date(settlement) || !is_valid_date(maturity) { let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()); Ok(f) => f,
} Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
};
if settlement > maturity { if settlement > maturity {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
@@ -1550,7 +1499,7 @@ impl Model {
"settlement should be <= maturity".to_string(), "settlement should be <= maturity".to_string(),
); );
} }
if !is_less_than_one_year(settlement as i64, maturity as i64) { if !less_than_one_year {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{parser::Node, token::Error, types::CellReferenceIndex, utils::number_to_column},
model::{Model, ParsedDefinedName}, model::{Model, ParsedDefinedName},
}; };
@@ -235,6 +235,11 @@ impl Model {
// This cannot happen // This cannot happen
CalcResult::Number(1.0) CalcResult::Number(1.0)
} }
CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
} }
} }
pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -247,10 +252,13 @@ impl Model {
return CalcResult::Number(cell.sheet as f64 + 1.0); return CalcResult::Number(cell.sheet as f64 + 1.0);
} }
// The arg could be a defined name or a table // The arg could be a defined name or a table
let arg = &args[0]; // let = &args[0];
if let Node::VariableKind(name) = arg { match &args[0] {
Node::DefinedNameKind((name, scope, _)) => {
// Let's see if it is a defined name // Let's see if it is a defined name
if let Some(defined_name) = self.parsed_defined_names.get(&(None, name.to_lowercase())) if let Some(defined_name) = self
.parsed_defined_names
.get(&(*scope, name.to_lowercase()))
{ {
match defined_name { match defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
@@ -261,13 +269,22 @@ impl Model {
} }
ParsedDefinedName::InvalidDefinedNameFormula => { ParsedDefinedName::InvalidDefinedNameFormula => {
return CalcResult::Error { return CalcResult::Error {
error: Error::NA, error: Error::ERROR,
origin: cell, origin: cell,
message: "Invalid name".to_string(), message: "Invalid name".to_string(),
}; };
} }
} }
} else {
// This should never happen
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid name".to_string(),
};
} }
}
Node::TableNameKind(name) => {
// Now let's see if it is a table // Now let's see if it is a table
for (table_name, table) in &self.workbook.tables { for (table_name, table) in &self.workbook.tables {
if table_name == name { if table_name == name {
@@ -279,6 +296,14 @@ impl Model {
} }
} }
} }
Node::WrongVariableKind(name) => {
return CalcResult::Error {
error: Error::NAME,
origin: cell,
message: format!("Name not found: {name}"),
}
}
arg => {
// Now it should be the name of a sheet // Now it should be the name of a sheet
let sheet_name = match self.get_string(arg, cell) { let sheet_name = match self.get_string(arg, cell) {
Ok(s) => s, Ok(s) => s,
@@ -287,10 +312,158 @@ impl Model {
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) { if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0); return CalcResult::Number(sheet_index as f64 + 1.0);
} }
}
}
CalcResult::Error { CalcResult::Error {
error: Error::NA, error: Error::NA,
origin: cell, origin: cell,
message: "Invalid name".to_string(), 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)
}
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

@@ -7,6 +7,22 @@ use crate::{
use super::util::compare_values; use super::util::compare_values;
impl Model { impl Model {
pub(crate) fn fn_true(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
CalcResult::Boolean(true)
} else {
CalcResult::new_args_number_error(cell)
}
}
pub(crate) fn fn_false(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
CalcResult::Boolean(false)
} else {
CalcResult::new_args_number_error(cell)
}
}
pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 2 || args.len() == 3 { if args.len() == 2 || args.len() == 3 {
let cond_result = self.get_boolean(&args[0], cell); let cond_result = self.get_boolean(&args[0], cell);
@@ -66,150 +82,61 @@ impl Model {
} }
pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut true_count = 0; self.logical_nary(
for arg in args { args,
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(b) => {
if !b {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::Number(value) => {
if value == 0.0 {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::String(_value) => {
true_count += 1;
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell, cell,
"Ranges are in different sheets".to_string(), |acc, value| acc.unwrap_or(true) && value,
); Some(false),
} )
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::Boolean(b) => {
if !b {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::Number(value) => {
if value == 0.0 {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::String(_value) => {
true_count += 1;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {}
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
};
}
if true_count == 0 {
return CalcResult::new_error(
Error::VALUE,
cell,
"Boolean values not found".to_string(),
);
}
CalcResult::Boolean(true)
} }
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut result = false; self.logical_nary(
for arg in args { args,
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(value) => result = value || result,
CalcResult::Number(value) => {
if value != 0.0 {
return CalcResult::Boolean(true);
}
}
CalcResult::String(_value) => {
return CalcResult::Boolean(true);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell, cell,
"Ranges are in different sheets".to_string(), |acc, value| acc.unwrap_or(false) || value,
); Some(true),
} )
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::Boolean(value) => {
result = value || result;
}
CalcResult::Number(value) => {
if value != 0.0 {
return CalcResult::Boolean(true);
}
}
CalcResult::String(_value) => {
return CalcResult::Boolean(true);
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {}
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
};
}
CalcResult::Boolean(result)
} }
/// XOR(logical1, [logical]*,...)
/// Logical1 is required, subsequent logical values are optional. Can be logical values, arrays, or references.
/// The result of XOR is TRUE when the number of TRUE inputs is odd and FALSE when the number of TRUE inputs is even.
pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut true_count = 0; self.logical_nary(args, cell, |acc, value| acc.unwrap_or(false) ^ value, None)
let mut false_count = 0; }
/// Base function for AND, OR, XOR. These are all n-ary functions that perform a boolean operation on a series of
/// boolean values. These boolean values are sourced from `args`. Note that there is not a 1-1 relationship between
/// arguments and boolean values evaluated (see how Ranges are handled for example).
///
/// Each argument in `args` is evaluated and the resulting value is interpreted as a boolean as follows:
/// - Boolean: The value is used directly.
/// - Number: 0 is FALSE, all other values are TRUE.
/// - Range: Each cell in the range is evaluated as if they were individual arguments with some caveats
/// - Empty arg: FALSE
/// - Empty cell & String: Ignored, behaves exactly like the argument wasn't passed in at all
/// - Error: Propagated
///
/// If no arguments are provided, or all arguments are ignored, the function returns a #VALUE! error
///
/// **`fold_fn`:** The function that combines the running result with the next value boolean value. The running result
/// starts as `None`.
///
/// **`short_circuit_value`:** If the running result reaches `short_circuit_value`, the function returns early.
fn logical_nary(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
fold_fn: fn(Option<bool>, bool) -> bool,
short_circuit_value: Option<bool>,
) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut result = None;
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(b) => { CalcResult::Boolean(value) => result = Some(fold_fn(result, value)),
if b { CalcResult::Number(value) => result = Some(fold_fn(result, value != 0.0)),
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Number(value) => {
if value != 0.0 {
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Range { left, right } => { CalcResult::Range { left, right } => {
if left.sheet != right.sheet { if left.sheet != right.sheet {
return CalcResult::new_error( return CalcResult::new_error(
@@ -225,33 +152,73 @@ impl Model {
row, row,
column, column,
}) { }) {
CalcResult::Boolean(b) => { CalcResult::Boolean(value) => result = Some(fold_fn(result, value)),
if b {
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Number(value) => { CalcResult::Number(value) => {
if value != 0.0 { result = Some(fold_fn(result, value != 0.0))
true_count += 1; }
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg => {} // unreachable
CalcResult::Range { .. }
| CalcResult::String { .. }
| CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value)
{
if current_result == short_circuit_value {
return CalcResult::Boolean(current_result);
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg => result = Some(result.unwrap_or(false)),
// Strings are ignored unless they are "TRUE" or "FALSE" (case insensitive). EXCEPT if the string value
// comes from a reference, in which case it is always ignored regardless of its value.
CalcResult::String(..) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
if let Ok(f) = self.get_boolean(arg, cell) {
result = Some(fold_fn(result, f));
}
}
}
// References to empty cells are ignored. If all args are ignored the result is #VALUE!
CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)
{
if current_result == short_circuit_value {
return CalcResult::Boolean(current_result);
}
}
}
if let Some(result) = result {
CalcResult::Boolean(result)
} else { } else {
false_count += 1; CalcResult::new_error(
Error::VALUE,
cell,
"No logical values in argument list".to_string(),
)
} }
} }
_ => {}
}
}
}
}
_ => {}
};
}
if true_count == 0 && false_count == 0 {
return CalcResult::new_error(Error::VALUE, cell, "No booleans found".to_string());
}
CalcResult::Boolean(true_count % 2 == 1)
}
/// =SWITCH(expression, case1, value1, [case, value]*, [default]) /// =SWITCH(expression, case1, value1, [case, value]*, [default])
pub(crate) fn fn_switch(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_switch(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -279,7 +246,7 @@ impl Model {
} }
// None of the cases matched so we return the default // 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 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); return self.evaluate_node_in_context(&args[args_count - 1], cell);
} }
CalcResult::Error { CalcResult::Error {
@@ -295,7 +262,7 @@ impl Model {
if args_count < 2 { if args_count < 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
if args_count % 2 != 0 { if !args_count.is_multiple_of(2) {
// Missing value for last condition // Missing value for last condition
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }

View File

@@ -838,4 +838,43 @@ impl Model {
}; };
CalcResult::Range { left, right } CalcResult::Range { left, right }
} }
pub(crate) fn fn_formulatext(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
if let CalcResult::Range { left, right } = self.evaluate_node_with_reference(&args[0], cell)
{
if left.sheet != right.sheet {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "3D ranges not supported".to_string(),
};
}
if left.row != right.row || left.column != right.column {
// FIXME: Implicit intersection or dynamic arrays
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
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) {
CalcResult::String(f)
} else {
CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Reference does not have a formula".to_string(),
}
}
} else {
CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Argument must be a reference".to_string(),
}
}
}
} }

View File

@@ -0,0 +1,100 @@
#[macro_export]
macro_rules! single_number_fn {
// The macro takes:
// 1) A function name to define (e.g. fn_sin)
// 2) The operation to apply (e.g. f64::sin)
($fn_name:ident, $op:expr) => {
pub(crate) fn $fn_name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// 1) Check exactly one argument
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
// 2) Try to get a "NumberOrArray"
match self.get_number_or_array(&args[0], cell) {
// -----------------------------------------
// Case A: It's a single number
// -----------------------------------------
Ok(NumberOrArray::Number(f)) => match $op(f) {
Ok(x) => CalcResult::Number(x),
Err(Error::DIV) => CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Divide by 0".to_string(),
},
Err(Error::VALUE) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid number".to_string(),
},
Err(e) => CalcResult::Error {
error: e,
origin: cell,
message: "Unknown error".to_string(),
},
},
// -----------------------------------------
// Case B: It's an array, so apply $op
// element-by-element.
// -----------------------------------------
Ok(NumberOrArray::Array(a)) => {
let mut array = Vec::new();
for row in a {
let mut data_row = Vec::with_capacity(row.len());
for value in row {
match value {
// If Boolean, treat as 0.0 or 1.0
ArrayNode::Boolean(b) => {
let n = if b { 1.0 } else { 0.0 };
match $op(n) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => {
data_row.push(ArrayNode::Error(Error::DIV))
}
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
}
}
// If Number, apply directly
ArrayNode::Number(n) => match $op(n) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
},
// If String, parse to f64 then apply or #VALUE! error
ArrayNode::String(s) => {
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),
},
None => ArrayNode::Error(Error::VALUE),
};
data_row.push(node);
}
// If Error, propagate the error
e @ ArrayNode::Error(_) => {
data_row.push(e);
}
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------
// Case C: It's an Error => just return it
// -----------------------------------------
Err(err_result) => err_result,
}
}
};
}

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

@@ -8,6 +8,7 @@ use crate::{
}; };
pub(crate) mod binary_search; pub(crate) mod binary_search;
mod database;
mod date_and_time; mod date_and_time;
mod engineering; mod engineering;
mod financial; mod financial;
@@ -15,6 +16,8 @@ mod financial_util;
mod information; mod information;
mod logical; mod logical;
mod lookup_and_reference; mod lookup_and_reference;
mod macros;
mod math_util;
mod mathematical; mod mathematical;
mod statistical; mod statistical;
mod subtotal; mod subtotal;
@@ -53,6 +56,9 @@ pub enum Function {
Columns, Columns,
Cos, Cos,
Cosh, Cosh,
Log,
Log10,
Ln,
Max, Max,
Min, Min,
Pi, Pi,
@@ -72,9 +78,47 @@ pub enum Function {
Sumifs, Sumifs,
Tan, Tan,
Tanh, Tanh,
Acot,
Acoth,
Cot,
Coth,
Csc,
Csch,
Sec,
Sech,
Exp,
Fact,
Factdouble,
Sign,
Radians,
Degrees,
Int,
Even,
Odd,
Ceiling,
CeilingMath,
CeilingPrecise,
Floor,
FloorMath,
FloorPrecise,
IsoCeiling,
Mod,
Quotient,
Mround,
Trunc,
Gcd,
Lcm,
Base,
Decimal,
Roman,
Arabic,
Combin,
Combina,
Sumsq,
// Information // Information
ErrorType, ErrorType,
Formulatext,
Isblank, Isblank,
Iserr, Iserr,
Iserror, Iserror,
@@ -91,6 +135,11 @@ pub enum Function {
Sheet, Sheet,
Type, Type,
Sheets,
N,
Cell,
Info,
// Lookup and reference // Lookup and reference
Hlookup, Hlookup,
Index, Index,
@@ -122,6 +171,7 @@ pub enum Function {
Textbefore, Textbefore,
Textjoin, Textjoin,
Trim, Trim,
Unicode,
Upper, Upper,
Value, Value,
Valuetotext, Valuetotext,
@@ -138,16 +188,34 @@ pub enum Function {
Countifs, Countifs,
Maxifs, Maxifs,
Minifs, Minifs,
Geomean,
// Date and time // Date and time
Date, Date,
Datedif,
Datevalue,
Day, Day,
Edate, Edate,
Eomonth, Eomonth,
Month, Month,
Time,
Timevalue,
Hour,
Minute,
Second,
Now, Now,
Today, Today,
Year, Year,
Networkdays,
NetworkdaysIntl,
Days,
Days360,
Weekday,
Weeknum,
Workday,
WorkdayIntl,
Yearfrac,
Isoweeknum,
// Financial // Financial
Cumipmt, Cumipmt,
@@ -243,10 +311,24 @@ pub enum Function {
Delta, Delta,
Gestep, Gestep,
Subtotal, Subtotal,
// Database
Daverage,
Dcount,
Dget,
Dmax,
Dmin,
Dsum,
Dcounta,
Dproduct,
Dstdev,
Dvar,
Dvarp,
Dstdevp,
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 192> { pub fn into_iter() -> IntoIter<Function, 268> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -273,15 +355,50 @@ impl Function {
Function::Atanh, Function::Atanh,
Function::Abs, Function::Abs,
Function::Pi, Function::Pi,
Function::Ln,
Function::Log,
Function::Log10,
Function::Sqrt, Function::Sqrt,
Function::Sqrtpi, Function::Sqrtpi,
Function::Atan2, Function::Atan2,
Function::Acot,
Function::Acoth,
Function::Cot,
Function::Coth,
Function::Csc,
Function::Csch,
Function::Sec,
Function::Sech,
Function::Power, Function::Power,
Function::Exp,
Function::Fact,
Function::Factdouble,
Function::Sign,
Function::Int,
Function::Even,
Function::Odd,
Function::Ceiling,
Function::CeilingMath,
Function::CeilingPrecise,
Function::Floor,
Function::FloorMath,
Function::FloorPrecise,
Function::IsoCeiling,
Function::Mod,
Function::Quotient,
Function::Mround,
Function::Trunc,
Function::Gcd,
Function::Lcm,
Function::Base,
Function::Decimal,
Function::Max, Function::Max,
Function::Min, Function::Min,
Function::Product, Function::Product,
Function::Rand, Function::Rand,
Function::Randbetween, Function::Randbetween,
Function::Radians,
Function::Degrees,
Function::Round, Function::Round,
Function::Rounddown, Function::Rounddown,
Function::Roundup, Function::Roundup,
@@ -316,6 +433,7 @@ impl Function {
Function::Search, Function::Search,
Function::Text, Function::Text,
Function::Trim, Function::Trim,
Function::Unicode,
Function::Upper, Function::Upper,
Function::Isnumber, Function::Isnumber,
Function::Isnontext, Function::Isnontext,
@@ -330,6 +448,7 @@ impl Function {
Function::Isodd, Function::Isodd,
Function::Iseven, Function::Iseven,
Function::ErrorType, Function::ErrorType,
Function::Formulatext,
Function::Isformula, Function::Isformula,
Function::Type, Function::Type,
Function::Sheet, Function::Sheet,
@@ -344,14 +463,32 @@ impl Function {
Function::Countifs, Function::Countifs,
Function::Maxifs, Function::Maxifs,
Function::Minifs, Function::Minifs,
Function::Geomean,
Function::Year, Function::Year,
Function::Day, Function::Day,
Function::Month, Function::Month,
Function::Eomonth, Function::Eomonth,
Function::Date, Function::Date,
Function::Datedif,
Function::Datevalue,
Function::Edate, Function::Edate,
Function::Networkdays,
Function::NetworkdaysIntl,
Function::Time,
Function::Timevalue,
Function::Hour,
Function::Minute,
Function::Second,
Function::Today, Function::Today,
Function::Now, Function::Now,
Function::Days,
Function::Days360,
Function::Weekday,
Function::Weeknum,
Function::Workday,
Function::WorkdayIntl,
Function::Yearfrac,
Function::Isoweeknum,
Function::Pmt, Function::Pmt,
Function::Pv, Function::Pv,
Function::Rate, Function::Rate,
@@ -440,6 +577,27 @@ impl Function {
Function::Delta, Function::Delta,
Function::Gestep, Function::Gestep,
Function::Subtotal, Function::Subtotal,
Function::Roman,
Function::Arabic,
Function::Combin,
Function::Combina,
Function::Sumsq,
Function::N,
Function::Cell,
Function::Info,
Function::Sheets,
Function::Daverage,
Function::Dcount,
Function::Dget,
Function::Dmax,
Function::Dmin,
Function::Dsum,
Function::Dcounta,
Function::Dproduct,
Function::Dstdev,
Function::Dvar,
Function::Dvarp,
Function::Dstdevp,
] ]
.into_iter() .into_iter()
} }
@@ -460,6 +618,7 @@ impl Function {
Function::Textbefore => "_xlfn.TEXTBEFORE".to_string(), Function::Textbefore => "_xlfn.TEXTBEFORE".to_string(),
Function::Textafter => "_xlfn.TEXTAFTER".to_string(), Function::Textafter => "_xlfn.TEXTAFTER".to_string(),
Function::Textjoin => "_xlfn.TEXTJOIN".to_string(), Function::Textjoin => "_xlfn.TEXTJOIN".to_string(),
Function::Unicode => "_xlfn.UNICODE".to_string(),
Function::Rri => "_xlfn.RRI".to_string(), Function::Rri => "_xlfn.RRI".to_string(),
Function::Pduration => "_xlfn.PDURATION".to_string(), Function::Pduration => "_xlfn.PDURATION".to_string(),
Function::Bitand => "_xlfn.BITAND".to_string(), Function::Bitand => "_xlfn.BITAND".to_string(),
@@ -479,6 +638,28 @@ impl Function {
Function::Valuetotext => "_xlfn.VALUETOTEXT".to_string(), Function::Valuetotext => "_xlfn.VALUETOTEXT".to_string(),
Function::Isformula => "_xlfn.ISFORMULA".to_string(), Function::Isformula => "_xlfn.ISFORMULA".to_string(),
Function::Sheet => "_xlfn.SHEET".to_string(), Function::Sheet => "_xlfn.SHEET".to_string(),
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
Function::Ceiling => "_xlfn.CEILING".to_string(),
Function::CeilingMath => "_xlfn.CEILING.MATH".to_string(),
Function::CeilingPrecise => "_xlfn.CEILING.PRECISE".to_string(),
Function::FloorMath => "_xlfn.FLOOR.MATH".to_string(),
Function::FloorPrecise => "_xlfn.FLOOR.PRECISE".to_string(),
Function::IsoCeiling => "_xlfn.ISO.CEILING".to_string(),
Function::Base => "_xlfn.BASE".to_string(),
Function::Decimal => "_xlfn.DECIMAL".to_string(),
Function::Arabic => "_xlfn.ARABIC".to_string(),
Function::Combina => "_xlfn.COMBINA".to_string(),
Function::Sheets => "_xlfn.SHEETS".to_string(),
Function::Acoth => "_xlfn.ACOTH".to_string(),
Function::Cot => "_xlfn.COT".to_string(),
Function::Coth => "_xlfn.COTH".to_string(),
Function::Csc => "_xlfn.CSC".to_string(),
Function::Csch => "_xlfn.CSCH".to_string(),
Function::Sec => "_xlfn.SEC".to_string(),
Function::Sech => "_xlfn.SECH".to_string(),
Function::Acot => "_xlfn.ACOT".to_string(),
_ => self.to_string(), _ => self.to_string(),
} }
} }
@@ -501,30 +682,61 @@ impl Function {
"SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch), "SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch),
"TRUE" => Some(Function::True), "TRUE" => Some(Function::True),
"XOR" | "_XLFN.XOR" => Some(Function::Xor), "XOR" | "_XLFN.XOR" => Some(Function::Xor),
"SIN" => Some(Function::Sin), "SIN" => Some(Function::Sin),
"COS" => Some(Function::Cos), "COS" => Some(Function::Cos),
"TAN" => Some(Function::Tan), "TAN" => Some(Function::Tan),
"ASIN" => Some(Function::Asin), "ASIN" => Some(Function::Asin),
"ACOS" => Some(Function::Acos), "ACOS" => Some(Function::Acos),
"ATAN" => Some(Function::Atan), "ATAN" => Some(Function::Atan),
"SINH" => Some(Function::Sinh), "SINH" => Some(Function::Sinh),
"COSH" => Some(Function::Cosh), "COSH" => Some(Function::Cosh),
"TANH" => Some(Function::Tanh), "TANH" => Some(Function::Tanh),
"ASINH" => Some(Function::Asinh), "ASINH" => Some(Function::Asinh),
"ACOSH" => Some(Function::Acosh), "ACOSH" => Some(Function::Acosh),
"ATANH" => Some(Function::Atanh), "ATANH" => Some(Function::Atanh),
"ACOT" | "_XLFN.ACOT" => Some(Function::Acot),
"COTH" | "_XLFN.COTH" => Some(Function::Coth),
"COT" | "_XLFN.COT" => Some(Function::Cot),
"CSC" | "_XLFN.CSC" => Some(Function::Csc),
"CSCH" | "_XLFN.CSCH" => Some(Function::Csch),
"SEC" | "_XLFN.SEC" => Some(Function::Sec),
"SECH" | "_XLFN.SECH" => Some(Function::Sech),
"ACOTH" | "_XLFN.ACOTH" => Some(Function::Acoth),
"FACT" => Some(Function::Fact),
"FACTDOUBLE" => Some(Function::Factdouble),
"EXP" => Some(Function::Exp),
"SIGN" => Some(Function::Sign),
"RADIANS" => Some(Function::Radians),
"DEGREES" => Some(Function::Degrees),
"INT" => Some(Function::Int),
"EVEN" => Some(Function::Even),
"ODD" => Some(Function::Odd),
"CEILING" | "_XLFN.CEILING" => Some(Function::Ceiling),
"CEILING.MATH" | "_XLFN.CEILING.MATH" => Some(Function::CeilingMath),
"CEILING.PRECISE" | "_XLFN.CEILING.PRECISE" => Some(Function::CeilingPrecise),
"FLOOR" => Some(Function::Floor),
"FLOOR.MATH" | "_XLFN.FLOOR.MATH" => Some(Function::FloorMath),
"FLOOR.PRECISE" | "_XLFN.FLOOR.PRECISE" => Some(Function::FloorPrecise),
"ISO.CEILING" | "_XLFN.ISO.CEILING" => Some(Function::IsoCeiling),
"MOD" => Some(Function::Mod),
"QUOTIENT" => Some(Function::Quotient),
"MROUND" => Some(Function::Mround),
"TRUNC" => Some(Function::Trunc),
"GCD" => Some(Function::Gcd),
"LCM" => Some(Function::Lcm),
"BASE" | "_XLFN.BASE" => Some(Function::Base),
"DECIMAL" | "_XLFN.DECIMAL" => Some(Function::Decimal),
"ROMAN" => Some(Function::Roman),
"ARABIC" | "_XLFN.ARABIC" => Some(Function::Arabic),
"PI" => Some(Function::Pi), "PI" => Some(Function::Pi),
"ABS" => Some(Function::Abs), "ABS" => Some(Function::Abs),
"SQRT" => Some(Function::Sqrt), "SQRT" => Some(Function::Sqrt),
"SQRTPI" => Some(Function::Sqrtpi), "SQRTPI" => Some(Function::Sqrtpi),
"POWER" => Some(Function::Power), "POWER" => Some(Function::Power),
"ATAN2" => Some(Function::Atan2), "ATAN2" => Some(Function::Atan2),
"LN" => Some(Function::Ln),
"LOG" => Some(Function::Log),
"LOG10" => Some(Function::Log10),
"MAX" => Some(Function::Max), "MAX" => Some(Function::Max),
"MIN" => Some(Function::Min), "MIN" => Some(Function::Min),
"PRODUCT" => Some(Function::Product), "PRODUCT" => Some(Function::Product),
@@ -536,6 +748,9 @@ impl Function {
"SUM" => Some(Function::Sum), "SUM" => Some(Function::Sum),
"SUMIF" => Some(Function::Sumif), "SUMIF" => Some(Function::Sumif),
"SUMIFS" => Some(Function::Sumifs), "SUMIFS" => Some(Function::Sumifs),
"COMBIN" => Some(Function::Combin),
"COMBINA" | "_XLFN.COMBINA" => Some(Function::Combina),
"SUMSQ" => Some(Function::Sumsq),
// Lookup and Reference // Lookup and Reference
"CHOOSE" => Some(Function::Choose), "CHOOSE" => Some(Function::Choose),
@@ -567,6 +782,7 @@ impl Function {
"SEARCH" => Some(Function::Search), "SEARCH" => Some(Function::Search),
"TEXT" => Some(Function::Text), "TEXT" => Some(Function::Text),
"TRIM" => Some(Function::Trim), "TRIM" => Some(Function::Trim),
"UNICODE" | "_XLFN.UNICODE" => Some(Function::Unicode),
"UPPER" => Some(Function::Upper), "UPPER" => Some(Function::Upper),
"REPT" => Some(Function::Rept), "REPT" => Some(Function::Rept),
@@ -588,6 +804,7 @@ impl Function {
"ISODD" => Some(Function::Isodd), "ISODD" => Some(Function::Isodd),
"ISEVEN" => Some(Function::Iseven), "ISEVEN" => Some(Function::Iseven),
"ERROR.TYPE" => Some(Function::ErrorType), "ERROR.TYPE" => Some(Function::ErrorType),
"FORMULATEXT" | "_XLFN.FORMULATEXT" => Some(Function::Formulatext),
"ISFORMULA" | "_XLFN.ISFORMULA" => Some(Function::Isformula), "ISFORMULA" | "_XLFN.ISFORMULA" => Some(Function::Isformula),
"TYPE" => Some(Function::Type), "TYPE" => Some(Function::Type),
"SHEET" | "_XLFN.SHEET" => Some(Function::Sheet), "SHEET" | "_XLFN.SHEET" => Some(Function::Sheet),
@@ -603,15 +820,33 @@ impl Function {
"COUNTIFS" => Some(Function::Countifs), "COUNTIFS" => Some(Function::Countifs),
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
"GEOMEAN" => Some(Function::Geomean),
// Date and Time // Date and Time
"YEAR" => Some(Function::Year), "YEAR" => Some(Function::Year),
"DAY" => Some(Function::Day), "DAY" => Some(Function::Day),
"EOMONTH" => Some(Function::Eomonth), "EOMONTH" => Some(Function::Eomonth),
"MONTH" => Some(Function::Month), "MONTH" => Some(Function::Month),
"DATE" => Some(Function::Date), "DATE" => Some(Function::Date),
"DATEDIF" => Some(Function::Datedif),
"DATEVALUE" => Some(Function::Datevalue),
"EDATE" => Some(Function::Edate), "EDATE" => Some(Function::Edate),
"NETWORKDAYS" => Some(Function::Networkdays),
"NETWORKDAYS.INTL" => Some(Function::NetworkdaysIntl),
"TIME" => Some(Function::Time),
"TIMEVALUE" => Some(Function::Timevalue),
"HOUR" => Some(Function::Hour),
"MINUTE" => Some(Function::Minute),
"SECOND" => Some(Function::Second),
"TODAY" => Some(Function::Today), "TODAY" => Some(Function::Today),
"NOW" => Some(Function::Now), "NOW" => Some(Function::Now),
"DAYS" | "_XLFN.DAYS" => Some(Function::Days),
"DAYS360" => Some(Function::Days360),
"WEEKDAY" => Some(Function::Weekday),
"WEEKNUM" => Some(Function::Weeknum),
"WORKDAY" => Some(Function::Workday),
"WORKDAY.INTL" => Some(Function::WorkdayIntl),
"YEARFRAC" => Some(Function::Yearfrac),
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
// Financial // Financial
"PMT" => Some(Function::Pmt), "PMT" => Some(Function::Pmt),
"PV" => Some(Function::Pv), "PV" => Some(Function::Pv),
@@ -703,6 +938,25 @@ impl Function {
"GESTEP" => Some(Function::Gestep), "GESTEP" => Some(Function::Gestep),
"SUBTOTAL" => Some(Function::Subtotal), "SUBTOTAL" => Some(Function::Subtotal),
"N" => Some(Function::N),
"CELL" => Some(Function::Cell),
"INFO" => Some(Function::Info),
"SHEETS" | "_XLFN.SHEETS" => Some(Function::Sheets),
"DAVERAGE" => Some(Function::Daverage),
"DCOUNT" => Some(Function::Dcount),
"DGET" => Some(Function::Dget),
"DMAX" => Some(Function::Dmax),
"DMIN" => Some(Function::Dmin),
"DSUM" => Some(Function::Dsum),
"DCOUNTA" => Some(Function::Dcounta),
"DPRODUCT" => Some(Function::Dproduct),
"DSTDEV" => Some(Function::Dstdev),
"DVAR" => Some(Function::Dvar),
"DVARP" => Some(Function::Dvarp),
"DSTDEVP" => Some(Function::Dstdevp),
_ => None, _ => None,
} }
} }
@@ -722,6 +976,9 @@ impl fmt::Display for Function {
Function::Switch => write!(f, "SWITCH"), Function::Switch => write!(f, "SWITCH"),
Function::True => write!(f, "TRUE"), Function::True => write!(f, "TRUE"),
Function::Xor => write!(f, "XOR"), Function::Xor => write!(f, "XOR"),
Function::Log => write!(f, "LOG"),
Function::Log10 => write!(f, "LOG10"),
Function::Ln => write!(f, "LN"),
Function::Sin => write!(f, "SIN"), Function::Sin => write!(f, "SIN"),
Function::Cos => write!(f, "COS"), Function::Cos => write!(f, "COS"),
Function::Tan => write!(f, "TAN"), Function::Tan => write!(f, "TAN"),
@@ -734,6 +991,14 @@ impl fmt::Display for Function {
Function::Asinh => write!(f, "ASINH"), Function::Asinh => write!(f, "ASINH"),
Function::Acosh => write!(f, "ACOSH"), Function::Acosh => write!(f, "ACOSH"),
Function::Atanh => write!(f, "ATANH"), Function::Atanh => write!(f, "ATANH"),
Function::Acot => write!(f, "ACOT"),
Function::Acoth => write!(f, "ACOTH"),
Function::Cot => write!(f, "COT"),
Function::Coth => write!(f, "COTH"),
Function::Csc => write!(f, "CSC"),
Function::Csch => write!(f, "CSCH"),
Function::Sec => write!(f, "SEC"),
Function::Sech => write!(f, "SECH"),
Function::Abs => write!(f, "ABS"), Function::Abs => write!(f, "ABS"),
Function::Pi => write!(f, "PI"), Function::Pi => write!(f, "PI"),
Function::Sqrt => write!(f, "SQRT"), Function::Sqrt => write!(f, "SQRT"),
@@ -779,6 +1044,7 @@ impl fmt::Display for Function {
Function::Search => write!(f, "SEARCH"), Function::Search => write!(f, "SEARCH"),
Function::Text => write!(f, "TEXT"), Function::Text => write!(f, "TEXT"),
Function::Trim => write!(f, "TRIM"), Function::Trim => write!(f, "TRIM"),
Function::Unicode => write!(f, "UNICODE"),
Function::Upper => write!(f, "UPPER"), Function::Upper => write!(f, "UPPER"),
Function::Isnumber => write!(f, "ISNUMBER"), Function::Isnumber => write!(f, "ISNUMBER"),
Function::Isnontext => write!(f, "ISNONTEXT"), Function::Isnontext => write!(f, "ISNONTEXT"),
@@ -793,10 +1059,10 @@ impl fmt::Display for Function {
Function::Isodd => write!(f, "ISODD"), Function::Isodd => write!(f, "ISODD"),
Function::Iseven => write!(f, "ISEVEN"), Function::Iseven => write!(f, "ISEVEN"),
Function::ErrorType => write!(f, "ERROR.TYPE"), Function::ErrorType => write!(f, "ERROR.TYPE"),
Function::Formulatext => write!(f, "FORMULATEXT"),
Function::Isformula => write!(f, "ISFORMULA"), Function::Isformula => write!(f, "ISFORMULA"),
Function::Type => write!(f, "TYPE"), Function::Type => write!(f, "TYPE"),
Function::Sheet => write!(f, "SHEET"), Function::Sheet => write!(f, "SHEET"),
Function::Average => write!(f, "AVERAGE"), Function::Average => write!(f, "AVERAGE"),
Function::Averagea => write!(f, "AVERAGEA"), Function::Averagea => write!(f, "AVERAGEA"),
Function::Averageif => write!(f, "AVERAGEIF"), Function::Averageif => write!(f, "AVERAGEIF"),
@@ -808,14 +1074,32 @@ impl fmt::Display for Function {
Function::Countifs => write!(f, "COUNTIFS"), Function::Countifs => write!(f, "COUNTIFS"),
Function::Maxifs => write!(f, "MAXIFS"), Function::Maxifs => write!(f, "MAXIFS"),
Function::Minifs => write!(f, "MINIFS"), Function::Minifs => write!(f, "MINIFS"),
Function::Geomean => write!(f, "GEOMEAN"),
Function::Year => write!(f, "YEAR"), Function::Year => write!(f, "YEAR"),
Function::Day => write!(f, "DAY"), Function::Day => write!(f, "DAY"),
Function::Month => write!(f, "MONTH"), Function::Month => write!(f, "MONTH"),
Function::Eomonth => write!(f, "EOMONTH"), Function::Eomonth => write!(f, "EOMONTH"),
Function::Date => write!(f, "DATE"), Function::Date => write!(f, "DATE"),
Function::Datedif => write!(f, "DATEDIF"),
Function::Datevalue => write!(f, "DATEVALUE"),
Function::Edate => write!(f, "EDATE"), Function::Edate => write!(f, "EDATE"),
Function::Networkdays => write!(f, "NETWORKDAYS"),
Function::NetworkdaysIntl => write!(f, "NETWORKDAYS.INTL"),
Function::Time => write!(f, "TIME"),
Function::Timevalue => write!(f, "TIMEVALUE"),
Function::Hour => write!(f, "HOUR"),
Function::Minute => write!(f, "MINUTE"),
Function::Second => write!(f, "SECOND"),
Function::Today => write!(f, "TODAY"), Function::Today => write!(f, "TODAY"),
Function::Now => write!(f, "NOW"), Function::Now => write!(f, "NOW"),
Function::Days => write!(f, "DAYS"),
Function::Days360 => write!(f, "DAYS360"),
Function::Weekday => write!(f, "WEEKDAY"),
Function::Weeknum => write!(f, "WEEKNUM"),
Function::Workday => write!(f, "WORKDAY"),
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
Function::Yearfrac => write!(f, "YEARFRAC"),
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
Function::Pmt => write!(f, "PMT"), Function::Pmt => write!(f, "PMT"),
Function::Pv => write!(f, "PV"), Function::Pv => write!(f, "PV"),
Function::Rate => write!(f, "RATE"), Function::Rate => write!(f, "RATE"),
@@ -903,8 +1187,53 @@ impl fmt::Display for Function {
Function::Convert => write!(f, "CONVERT"), Function::Convert => write!(f, "CONVERT"),
Function::Delta => write!(f, "DELTA"), Function::Delta => write!(f, "DELTA"),
Function::Gestep => write!(f, "GESTEP"), Function::Gestep => write!(f, "GESTEP"),
Function::Subtotal => write!(f, "SUBTOTAL"), Function::Subtotal => write!(f, "SUBTOTAL"),
Function::Exp => write!(f, "EXP"),
Function::Fact => write!(f, "FACT"),
Function::Factdouble => write!(f, "FACTDOUBLE"),
Function::Sign => write!(f, "SIGN"),
Function::Radians => write!(f, "RADIANS"),
Function::Degrees => write!(f, "DEGREES"),
Function::Int => write!(f, "INT"),
Function::Even => write!(f, "EVEN"),
Function::Odd => write!(f, "ODD"),
Function::Ceiling => write!(f, "CEILING"),
Function::CeilingMath => write!(f, "CEILING.MATH"),
Function::CeilingPrecise => write!(f, "CEILING.PRECISE"),
Function::Floor => write!(f, "FLOOR"),
Function::FloorMath => write!(f, "FLOOR.MATH"),
Function::FloorPrecise => write!(f, "FLOOR.PRECISE"),
Function::IsoCeiling => write!(f, "ISO.CEILING"),
Function::Mod => write!(f, "MOD"),
Function::Quotient => write!(f, "QUOTIENT"),
Function::Mround => write!(f, "MROUND"),
Function::Trunc => write!(f, "TRUNC"),
Function::Gcd => write!(f, "GCD"),
Function::Lcm => write!(f, "LCM"),
Function::Base => write!(f, "BASE"),
Function::Decimal => write!(f, "DECIMAL"),
Function::Roman => write!(f, "ROMAN"),
Function::Arabic => write!(f, "ARABIC"),
Function::Combin => write!(f, "COMBIN"),
Function::Combina => write!(f, "COMBINA"),
Function::Sumsq => write!(f, "SUMSQ"),
Function::N => write!(f, "N"),
Function::Cell => write!(f, "CELL"),
Function::Info => write!(f, "INFO"),
Function::Sheets => write!(f, "SHEETS"),
Function::Daverage => write!(f, "DAVERAGE"),
Function::Dcount => write!(f, "DCOUNT"),
Function::Dget => write!(f, "DGET"),
Function::Dmax => write!(f, "DMAX"),
Function::Dmin => write!(f, "DMIN"),
Function::Dsum => write!(f, "DSUM"),
Function::Dcounta => write!(f, "DCOUNTA"),
Function::Dproduct => write!(f, "DPRODUCT"),
Function::Dstdev => write!(f, "DSTDEV"),
Function::Dvar => write!(f, "DVAR"),
Function::Dvarp => write!(f, "DVARP"),
Function::Dstdevp => write!(f, "DSTDEVP"),
} }
} }
} }
@@ -933,9 +1262,8 @@ impl Model {
cell: CellReferenceIndex, cell: CellReferenceIndex,
) -> CalcResult { ) -> CalcResult {
match kind { match kind {
// Logical
Function::And => self.fn_and(args, cell), Function::And => self.fn_and(args, cell),
Function::False => CalcResult::Boolean(false), Function::False => self.fn_false(args, cell),
Function::If => self.fn_if(args, cell), Function::If => self.fn_if(args, cell),
Function::Iferror => self.fn_iferror(args, cell), Function::Iferror => self.fn_iferror(args, cell),
Function::Ifna => self.fn_ifna(args, cell), Function::Ifna => self.fn_ifna(args, cell),
@@ -943,33 +1271,29 @@ impl Model {
Function::Not => self.fn_not(args, cell), Function::Not => self.fn_not(args, cell),
Function::Or => self.fn_or(args, cell), Function::Or => self.fn_or(args, cell),
Function::Switch => self.fn_switch(args, cell), Function::Switch => self.fn_switch(args, cell),
Function::True => CalcResult::Boolean(true), Function::True => self.fn_true(args, cell),
Function::Xor => self.fn_xor(args, cell), Function::Xor => self.fn_xor(args, cell),
// Math and trigonometry Function::Log => self.fn_log(args, cell),
Function::Log10 => self.fn_log10(args, cell),
Function::Ln => self.fn_ln(args, cell),
Function::Sin => self.fn_sin(args, cell), Function::Sin => self.fn_sin(args, cell),
Function::Cos => self.fn_cos(args, cell), Function::Cos => self.fn_cos(args, cell),
Function::Tan => self.fn_tan(args, cell), Function::Tan => self.fn_tan(args, cell),
Function::Asin => self.fn_asin(args, cell), Function::Asin => self.fn_asin(args, cell),
Function::Acos => self.fn_acos(args, cell), Function::Acos => self.fn_acos(args, cell),
Function::Atan => self.fn_atan(args, cell), Function::Atan => self.fn_atan(args, cell),
Function::Sinh => self.fn_sinh(args, cell), Function::Sinh => self.fn_sinh(args, cell),
Function::Cosh => self.fn_cosh(args, cell), Function::Cosh => self.fn_cosh(args, cell),
Function::Tanh => self.fn_tanh(args, cell), Function::Tanh => self.fn_tanh(args, cell),
Function::Asinh => self.fn_asinh(args, cell), Function::Asinh => self.fn_asinh(args, cell),
Function::Acosh => self.fn_acosh(args, cell), Function::Acosh => self.fn_acosh(args, cell),
Function::Atanh => self.fn_atanh(args, cell), Function::Atanh => self.fn_atanh(args, cell),
Function::Pi => self.fn_pi(args, cell), Function::Pi => self.fn_pi(args, cell),
Function::Abs => self.fn_abs(args, cell), Function::Abs => self.fn_abs(args, cell),
Function::Sqrt => self.fn_sqrt(args, cell), Function::Sqrt => self.fn_sqrt(args, cell),
Function::Sqrtpi => self.fn_sqrtpi(args, cell), Function::Sqrtpi => self.fn_sqrtpi(args, cell),
Function::Atan2 => self.fn_atan2(args, cell), Function::Atan2 => self.fn_atan2(args, cell),
Function::Power => self.fn_power(args, cell), Function::Power => self.fn_power(args, cell),
Function::Max => self.fn_max(args, cell), Function::Max => self.fn_max(args, cell),
Function::Min => self.fn_min(args, cell), Function::Min => self.fn_min(args, cell),
Function::Product => self.fn_product(args, cell), Function::Product => self.fn_product(args, cell),
@@ -981,8 +1305,6 @@ impl Model {
Function::Sum => self.fn_sum(args, cell), Function::Sum => self.fn_sum(args, cell),
Function::Sumif => self.fn_sumif(args, cell), Function::Sumif => self.fn_sumif(args, cell),
Function::Sumifs => self.fn_sumifs(args, cell), Function::Sumifs => self.fn_sumifs(args, cell),
// Lookup and Reference
Function::Choose => self.fn_choose(args, cell), Function::Choose => self.fn_choose(args, cell),
Function::Column => self.fn_column(args, cell), Function::Column => self.fn_column(args, cell),
Function::Columns => self.fn_columns(args, cell), Function::Columns => self.fn_columns(args, cell),
@@ -996,7 +1318,6 @@ impl Model {
Function::Rows => self.fn_rows(args, cell), Function::Rows => self.fn_rows(args, cell),
Function::Vlookup => self.fn_vlookup(args, cell), Function::Vlookup => self.fn_vlookup(args, cell),
Function::Xlookup => self.fn_xlookup(args, cell), Function::Xlookup => self.fn_xlookup(args, cell),
// Text
Function::Concatenate => self.fn_concatenate(args, cell), Function::Concatenate => self.fn_concatenate(args, cell),
Function::Exact => self.fn_exact(args, cell), Function::Exact => self.fn_exact(args, cell),
Function::Value => self.fn_value(args, cell), Function::Value => self.fn_value(args, cell),
@@ -1012,8 +1333,8 @@ impl Model {
Function::Search => self.fn_search(args, cell), Function::Search => self.fn_search(args, cell),
Function::Text => self.fn_text(args, cell), Function::Text => self.fn_text(args, cell),
Function::Trim => self.fn_trim(args, cell), Function::Trim => self.fn_trim(args, cell),
Function::Unicode => self.fn_unicode(args, cell),
Function::Upper => self.fn_upper(args, cell), Function::Upper => self.fn_upper(args, cell),
// Information
Function::Isnumber => self.fn_isnumber(args, cell), Function::Isnumber => self.fn_isnumber(args, cell),
Function::Isnontext => self.fn_isnontext(args, cell), Function::Isnontext => self.fn_isnontext(args, cell),
Function::Istext => self.fn_istext(args, cell), Function::Istext => self.fn_istext(args, cell),
@@ -1027,10 +1348,10 @@ impl Model {
Function::Isodd => self.fn_isodd(args, cell), Function::Isodd => self.fn_isodd(args, cell),
Function::Iseven => self.fn_iseven(args, cell), Function::Iseven => self.fn_iseven(args, cell),
Function::ErrorType => self.fn_errortype(args, cell), Function::ErrorType => self.fn_errortype(args, cell),
Function::Formulatext => self.fn_formulatext(args, cell),
Function::Isformula => self.fn_isformula(args, cell), Function::Isformula => self.fn_isformula(args, cell),
Function::Type => self.fn_type(args, cell), Function::Type => self.fn_type(args, cell),
Function::Sheet => self.fn_sheet(args, cell), Function::Sheet => self.fn_sheet(args, cell),
// Statistical
Function::Average => self.fn_average(args, cell), Function::Average => self.fn_average(args, cell),
Function::Averagea => self.fn_averagea(args, cell), Function::Averagea => self.fn_averagea(args, cell),
Function::Averageif => self.fn_averageif(args, cell), Function::Averageif => self.fn_averageif(args, cell),
@@ -1042,16 +1363,32 @@ impl Model {
Function::Countifs => self.fn_countifs(args, cell), Function::Countifs => self.fn_countifs(args, cell),
Function::Maxifs => self.fn_maxifs(args, cell), Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell), Function::Minifs => self.fn_minifs(args, cell),
// Date and Time Function::Geomean => self.fn_geomean(args, cell),
Function::Year => self.fn_year(args, cell), Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell), Function::Day => self.fn_day(args, cell),
Function::Eomonth => self.fn_eomonth(args, cell), Function::Eomonth => self.fn_eomonth(args, cell),
Function::Month => self.fn_month(args, cell), Function::Month => self.fn_month(args, cell),
Function::Date => self.fn_date(args, cell), Function::Date => self.fn_date(args, cell),
Function::Datedif => self.fn_datedif(args, cell),
Function::Datevalue => self.fn_datevalue(args, cell),
Function::Edate => self.fn_edate(args, cell), Function::Edate => self.fn_edate(args, cell),
Function::Networkdays => self.fn_networkdays(args, cell),
Function::NetworkdaysIntl => self.fn_networkdays_intl(args, cell),
Function::Time => self.fn_time(args, cell),
Function::Timevalue => self.fn_timevalue(args, cell),
Function::Hour => self.fn_hour(args, cell),
Function::Minute => self.fn_minute(args, cell),
Function::Second => self.fn_second(args, cell),
Function::Today => self.fn_today(args, cell), Function::Today => self.fn_today(args, cell),
Function::Now => self.fn_now(args, cell), Function::Now => self.fn_now(args, cell),
// Financial Function::Days => self.fn_days(args, cell),
Function::Days360 => self.fn_days360(args, cell),
Function::Weekday => self.fn_weekday(args, cell),
Function::Weeknum => self.fn_weeknum(args, cell),
Function::Workday => self.fn_workday(args, cell),
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
Function::Yearfrac => self.fn_yearfrac(args, cell),
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
Function::Pmt => self.fn_pmt(args, cell), Function::Pmt => self.fn_pmt(args, cell),
Function::Pv => self.fn_pv(args, cell), Function::Pv => self.fn_pv(args, cell),
Function::Rate => self.fn_rate(args, cell), Function::Rate => self.fn_rate(args, cell),
@@ -1085,7 +1422,6 @@ impl Model {
Function::Db => self.fn_db(args, cell), Function::Db => self.fn_db(args, cell),
Function::Cumprinc => self.fn_cumprinc(args, cell), Function::Cumprinc => self.fn_cumprinc(args, cell),
Function::Cumipmt => self.fn_cumipmt(args, cell), Function::Cumipmt => self.fn_cumipmt(args, cell),
// Engineering
Function::Besseli => self.fn_besseli(args, cell), Function::Besseli => self.fn_besseli(args, cell),
Function::Besselj => self.fn_besselj(args, cell), Function::Besselj => self.fn_besselj(args, cell),
Function::Besselk => self.fn_besselk(args, cell), Function::Besselk => self.fn_besselk(args, cell),
@@ -1140,14 +1476,67 @@ impl Model {
Function::Convert => self.fn_convert(args, cell), Function::Convert => self.fn_convert(args, cell),
Function::Delta => self.fn_delta(args, cell), Function::Delta => self.fn_delta(args, cell),
Function::Gestep => self.fn_gestep(args, cell), Function::Gestep => self.fn_gestep(args, cell),
Function::Subtotal => self.fn_subtotal(args, cell), Function::Subtotal => self.fn_subtotal(args, cell),
Function::Acot => self.fn_acot(args, cell),
Function::Acoth => self.fn_acoth(args, cell),
Function::Cot => self.fn_cot(args, cell),
Function::Coth => self.fn_coth(args, cell),
Function::Csc => self.fn_csc(args, cell),
Function::Csch => self.fn_csch(args, cell),
Function::Sec => self.fn_sec(args, cell),
Function::Sech => self.fn_sech(args, cell),
Function::Exp => self.fn_exp(args, cell),
Function::Fact => self.fn_fact(args, cell),
Function::Factdouble => self.fn_factdouble(args, cell),
Function::Sign => self.fn_sign(args, cell),
Function::Radians => self.fn_radians(args, cell),
Function::Degrees => self.fn_degrees(args, cell),
Function::Int => self.fn_int(args, cell),
Function::Even => self.fn_even(args, cell),
Function::Odd => self.fn_odd(args, cell),
Function::Ceiling => self.fn_ceiling(args, cell),
Function::CeilingMath => self.fn_ceiling_math(args, cell),
Function::CeilingPrecise => self.fn_ceiling_precise(args, cell),
Function::Floor => self.fn_floor(args, cell),
Function::FloorMath => self.fn_floor_math(args, cell),
Function::FloorPrecise => self.fn_floor_precise(args, cell),
Function::IsoCeiling => self.fn_iso_ceiling(args, cell),
Function::Mod => self.fn_mod(args, cell),
Function::Quotient => self.fn_quotient(args, cell),
Function::Mround => self.fn_mround(args, cell),
Function::Trunc => self.fn_trunc(args, cell),
Function::Gcd => self.fn_gcd(args, cell),
Function::Lcm => self.fn_lcm(args, cell),
Function::Base => self.fn_base(args, cell),
Function::Decimal => self.fn_decimal(args, cell),
Function::Roman => self.fn_roman(args, cell),
Function::Arabic => self.fn_arabic(args, cell),
Function::Combin => self.fn_combin(args, cell),
Function::Combina => self.fn_combina(args, cell),
Function::Sumsq => self.fn_sumsq(args, cell),
Function::N => self.fn_n(args, cell),
Function::Cell => self.fn_cell(args, cell),
Function::Info => self.fn_info(args, cell),
Function::Sheets => self.fn_sheets(args, cell),
Function::Daverage => self.fn_daverage(args, cell),
Function::Dcount => self.fn_dcount(args, cell),
Function::Dget => self.fn_dget(args, cell),
Function::Dmax => self.fn_dmax(args, cell),
Function::Dmin => self.fn_dmin(args, cell),
Function::Dsum => self.fn_dsum(args, cell),
Function::Dcounta => self.fn_dcounta(args, cell),
Function::Dproduct => self.fn_dproduct(args, cell),
Function::Dstdev => self.fn_dstdev(args, cell),
Function::Dvar => self.fn_dvar(args, cell),
Function::Dvarp => self.fn_dvarp(args, cell),
Function::Dstdevp => self.fn_dstdevp(args, cell),
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
use std::{ use std::{
fs::File, fs::File,
io::{BufRead, BufReader}, io::{BufRead, BufReader},
@@ -1195,7 +1584,7 @@ mod tests {
} }
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE // We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
let iter_list = Function::into_iter() let iter_list = Function::into_iter()
.map(|f| format!("{}", f).replace('.', "")) .map(|f| format!("{f}").replace('.', ""))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let len = iter_list.len(); let len = iter_list.len();

View File

@@ -134,6 +134,13 @@ impl Model {
); );
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -165,6 +172,13 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} 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 { if count == 0.0 {
@@ -336,7 +350,7 @@ impl Model {
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them? // 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 { pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len(); let args_count = args.len();
if args_count < 2 || args_count % 2 == 1 { if args_count < 2 || !args_count.is_multiple_of(2) {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
@@ -381,11 +395,16 @@ impl Model {
let right_row = first_range.right.row; let right_row = first_range.right.row;
let right_column = first_range.right.column; let right_column = first_range.right.column;
let dimension = self let dimension = match self.workbook.worksheet(first_range.left.sheet) {
.workbook Ok(s) => s.dimension(),
.worksheet(first_range.left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension(); Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
)
}
};
let max_row = dimension.max_row; let max_row = dimension.max_row;
let max_column = dimension.max_column; let max_column = dimension.max_column;
@@ -457,7 +476,7 @@ impl Model {
F: FnMut(f64), F: FnMut(f64),
{ {
let args_count = args.len(); let args_count = args.len();
if args_count < 3 || args_count % 2 == 0 { if args_count < 3 || args_count.is_multiple_of(2) {
return Err(CalcResult::new_args_number_error(cell)); return Err(CalcResult::new_args_number_error(cell));
} }
let arg_0 = self.evaluate_node_in_context(&args[0], cell); let arg_0 = self.evaluate_node_in_context(&args[0], cell);
@@ -526,20 +545,28 @@ impl Model {
let mut right_column = sum_range.right.column; let mut right_column = sum_range.right.column;
if left_row == 1 && right_row == LAST_ROW { if left_row == 1 && right_row == LAST_ROW {
right_row = self right_row = match self.workbook.worksheet(sum_range.left.sheet) {
.workbook Ok(s) => s.dimension().max_row,
.worksheet(sum_range.left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return Err(CalcResult::new_error(
.dimension() Error::ERROR,
.max_row; cell,
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
));
}
};
} }
if left_column == 1 && right_column == LAST_COLUMN { if left_column == 1 && right_column == LAST_COLUMN {
right_column = self right_column = match self.workbook.worksheet(sum_range.left.sheet) {
.workbook Ok(s) => s.dimension().max_column,
.worksheet(sum_range.left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return Err(CalcResult::new_error(
.dimension() Error::ERROR,
.max_column; cell,
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
));
}
};
} }
for row in left_row..right_row + 1 { for row in left_row..right_row + 1 {
@@ -622,4 +649,85 @@ impl Model {
} }
CalcResult::Number(max) 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

@@ -53,8 +53,13 @@ impl Model {
false false
} }
fn cell_hidden_status(&self, sheet_index: u32, row: i32, column: i32) -> CellTableStatus { fn cell_hidden_status(
let worksheet = self.workbook.worksheet(sheet_index).expect(""); &self,
sheet_index: u32,
row: i32,
column: i32,
) -> Result<CellTableStatus, String> {
let worksheet = self.workbook.worksheet(sheet_index)?;
let mut hidden = false; let mut hidden = false;
for row_style in &worksheet.rows { for row_style in &worksheet.rows {
if row_style.r == row { if row_style.r == row {
@@ -63,13 +68,13 @@ impl Model {
} }
} }
if !hidden { if !hidden {
return CellTableStatus::Normal; return Ok(CellTableStatus::Normal);
} }
// The row is hidden we need to know if the table has filters // The row is hidden we need to know if the table has filters
if self.get_table_for_cell(sheet_index, row, column) { if self.get_table_for_cell(sheet_index, row, column) {
CellTableStatus::Filtered Ok(CellTableStatus::Filtered)
} else { } else {
CellTableStatus::Hidden Ok(CellTableStatus::Hidden)
} }
} }
@@ -143,7 +148,11 @@ impl Model {
let column2 = right.column; let column2 = right.column;
for row in row1..=row2 { for row in row1..=row2 {
let cell_status = self.cell_hidden_status(left.sheet, row, column1); let cell_status = self
.cell_hidden_status(left.sheet, row, column1)
.map_err(|message| {
CalcResult::new_error(Error::ERROR, cell, message)
})?;
if cell_status == CellTableStatus::Filtered { if cell_status == CellTableStatus::Filtered {
continue; continue;
} }
@@ -173,6 +182,13 @@ impl Model {
} }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0),
CalcResult::Array(_) => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
} }
} }
} }
@@ -380,7 +396,14 @@ impl Model {
let column2 = right.column; let column2 = right.column;
for row in row1..=row2 { for row in row1..=row2 {
let cell_status = self.cell_hidden_status(left.sheet, row, column1); let cell_status = match self
.cell_hidden_status(left.sheet, row, column1)
{
Ok(s) => s,
Err(message) => {
return CalcResult::new_error(Error::ERROR, cell, message);
}
};
if cell_status == CellTableStatus::Filtered { if cell_status == CellTableStatus::Filtered {
continue; continue;
} }
@@ -410,6 +433,13 @@ impl Model {
| CalcResult::Number(_) | CalcResult::Number(_)
| CalcResult::Boolean(_) | CalcResult::Boolean(_)
| CalcResult::Error { .. } => counta += 1, | CalcResult::Error { .. } => counta += 1,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -449,7 +479,14 @@ impl Model {
let column2 = right.column; let column2 = right.column;
for row in row1..=row2 { for row in row1..=row2 {
let cell_status = self.cell_hidden_status(left.sheet, row, column1); let cell_status = match self
.cell_hidden_status(left.sheet, row, column1)
{
Ok(s) => s,
Err(message) => {
return CalcResult::new_error(Error::ERROR, cell, message);
}
};
if cell_status == CellTableStatus::Filtered { if cell_status == CellTableStatus::Filtered {
continue; continue;
} }

View File

@@ -55,14 +55,14 @@ impl Model {
let mut result = "".to_string(); let mut result = "".to_string();
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::String(value) => result = format!("{}{}", result, value), CalcResult::String(value) => result = format!("{result}{value}"),
CalcResult::Number(value) => result = format!("{}{}", result, value), CalcResult::Number(value) => result = format!("{result}{value}"),
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{}TRUE", result); result = format!("{result}TRUE");
} else { } else {
result = format!("{}FALSE", result); result = format!("{result}FALSE");
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
@@ -82,25 +82,37 @@ impl Model {
column, column,
}) { }) {
CalcResult::String(value) => { CalcResult::String(value) => {
result = format!("{}{}", result, value); result = format!("{result}{value}");
}
CalcResult::Number(value) => {
result = format!("{}{}", result, value)
} }
CalcResult::Number(value) => result = format!("{result}{value}"),
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{}TRUE", result); result = format!("{result}TRUE");
} else { } else {
result = format!("{}FALSE", result); result = format!("{result}FALSE");
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Range { .. } => {} CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
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::String(result) CalcResult::String(result)
@@ -125,6 +137,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let format_code = match self.get_string(&args[1], cell) { let format_code = match self.get_string(&args[1], cell) {
Ok(s) => s, Ok(s) => s,
@@ -261,7 +280,7 @@ impl Model {
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -280,6 +299,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::Number(s.chars().count() as f64); return CalcResult::Number(s.chars().count() as f64);
} }
@@ -289,7 +315,7 @@ impl Model {
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -308,6 +334,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.trim().to_owned()); return CalcResult::String(s.trim().to_owned());
} }
@@ -317,7 +350,7 @@ impl Model {
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -336,16 +369,77 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.to_lowercase()); return CalcResult::String(s.to_lowercase());
} }
CalcResult::new_args_number_error(cell) CalcResult::new_args_number_error(cell)
} }
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v,
CalcResult::Boolean(b) => {
if b {
"TRUE".to_string()
} else {
"FALSE".to_string()
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
// Implicit Intersection not implemented
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Implicit Intersection not implemented".to_string(),
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Empty cell".to_string(),
}
}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
match s.chars().next() {
Some(c) => {
let unicode_number = c as u32;
return CalcResult::Number(unicode_number as f64);
}
None => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Empty cell".to_string(),
};
}
}
}
CalcResult::new_args_number_error(cell)
}
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -364,6 +458,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.to_uppercase()); return CalcResult::String(s.to_uppercase());
} }
@@ -375,7 +476,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -394,6 +495,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let num_chars = if args.len() == 2 { let num_chars = if args.len() == 2 {
match self.evaluate_node_in_context(&args[1], cell) { match self.evaluate_node_in_context(&args[1], cell) {
@@ -424,6 +532,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} else { } else {
1 1
@@ -443,7 +558,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -462,6 +577,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let num_chars = if args.len() == 2 { let num_chars = if args.len() == 2 {
match self.evaluate_node_in_context(&args[1], cell) { match self.evaluate_node_in_context(&args[1], cell) {
@@ -492,6 +614,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} else { } else {
1 1
@@ -503,7 +632,7 @@ impl Model {
} }
result.push(ch); result.push(ch);
} }
return CalcResult::String(result.chars().rev().collect::<String>()); CalcResult::String(result.chars().rev().collect::<String>())
} }
pub(crate) fn fn_mid(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_mid(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -511,7 +640,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -530,6 +659,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let start_num = match self.evaluate_node_in_context(&args[1], cell) { let start_num = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Number(v) => { CalcResult::Number(v) => {
@@ -594,6 +730,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let mut result = "".to_string(); let mut result = "".to_string();
let mut count: usize = 0; let mut count: usize = 0;
@@ -888,20 +1031,28 @@ impl Model {
let column1 = left.column; let column1 = left.column;
let mut column2 = right.column; let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW { if row1 == 1 && row2 == LAST_ROW {
row2 = self row2 = match self.workbook.worksheet(left.sheet) {
.workbook Ok(s) => s.dimension().max_row,
.worksheet(left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension() Error::ERROR,
.max_row; cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
column2 = self column2 = match self.workbook.worksheet(left.sheet) {
.workbook Ok(s) => s.dimension().max_column,
.worksheet(left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension() Error::ERROR,
.max_column; cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {
for column in column1..(column2 + 1) { for column in column1..(column2 + 1) {
@@ -928,6 +1079,13 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg | CalcResult::Range { .. } => {} CalcResult::EmptyArg | CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -947,6 +1105,13 @@ impl Model {
} }
} }
CalcResult::EmptyArg => {} CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
let result = values.join(&delimiter); let result = values.join(&delimiter);
@@ -1070,6 +1235,11 @@ impl Model {
} }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0),
CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
} }
} }

View File

@@ -1,6 +1,10 @@
use regex::{escape, Regex}; #[cfg(feature = "use_regex_lite")]
use regex_lite as regex;
use crate::{calc_result::CalcResult, expressions::token::is_english_error_string}; use crate::{
calc_result::CalcResult, expressions::token::is_english_error_string,
number_format::to_excel_precision,
};
/// This test for exact match (modulo case). /// This test for exact match (modulo case).
/// * strings are not cast into bools or numbers /// * strings are not cast into bools or numbers
@@ -25,14 +29,16 @@ pub(crate) fn values_are_equal(left: &CalcResult, right: &CalcResult) -> bool {
} }
} }
/// In Excel there are two ways of comparing cell values. // In Excel there are two ways of comparing cell values.
/// The old school comparison valid in formulas like D3 < D4 or HLOOKUP,... cast empty cells into empty strings or 0 // The old school comparison valid in formulas like D3 < D4 or HLOOKUP,... cast empty cells into empty strings or 0
/// For the new formulas like XLOOKUP or SORT an empty cell is always larger than anything else. // For the new formulas like XLOOKUP or SORT an empty cell is always larger than anything else.
// ..., -2, -1, 0, 1, 2, ..., A-Z, FALSE, TRUE; // ..., -2, -1, 0, 1, 2, ..., A-Z, FALSE, TRUE;
pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 { pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
match (left, right) { match (left, right) {
(CalcResult::Number(value1), CalcResult::Number(value2)) => { (CalcResult::Number(value1), CalcResult::Number(value2)) => {
let value1 = to_excel_precision(*value1, 15);
let value2 = to_excel_precision(*value2, 15);
if (value2 - value1).abs() < f64::EPSILON { if (value2 - value1).abs() < f64::EPSILON {
return 0; return 0;
} }
@@ -86,7 +92,7 @@ pub(crate) fn from_wildcard_to_regex(
exact: bool, exact: bool,
) -> Result<regex::Regex, regex::Error> { ) -> Result<regex::Regex, regex::Error> {
// 1. Escape all // 1. Escape all
let reg = &escape(wildcard); let reg = &regex::escape(wildcard);
// 2. We convert the escaped '?' into '.' (matches a single character) // 2. We convert the escaped '?' into '.' (matches a single character)
let reg = &reg.replace("\\?", "."); let reg = &reg.replace("\\?", ".");
@@ -109,13 +115,13 @@ pub(crate) fn from_wildcard_to_regex(
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?") // And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
if exact { if exact {
return Regex::new(&format!("^{}$", reg)); return regex::Regex::new(&format!("^{reg}$"));
} }
Regex::new(reg) regex::Regex::new(reg)
} }
/// NUMBERS /// // NUMBERS ///
///*********/// //*********///
// It could be either the number or a string representation of the number // It could be either the number or a string representation of the number
// In the rest of the cases calc_result needs to be a number (cannot be the string "23", for instance) // In the rest of the cases calc_result needs to be a number (cannot be the string "23", for instance)
@@ -180,8 +186,8 @@ fn result_is_not_equal_to_number(calc_result: &CalcResult, target: f64) -> bool
} }
} }
/// BOOLEANS /// // BOOLEANS ///
///**********/// //**********///
// Booleans have to be "exactly" equal // Booleans have to be "exactly" equal
fn result_is_equal_to_bool(calc_result: &CalcResult, target: bool) -> bool { fn result_is_equal_to_bool(calc_result: &CalcResult, target: bool) -> bool {
@@ -198,12 +204,12 @@ fn result_is_not_equal_to_bool(calc_result: &CalcResult, target: bool) -> bool {
} }
} }
/// STRINGS /// // STRINGS ///
///*********/// //*********///
/// Note that strings are case insensitive. `target` must always be lower case. // Note that strings are case insensitive. `target` must always be lower case.
pub(crate) fn result_matches_regex(calc_result: &CalcResult, reg: &Regex) -> bool { pub(crate) fn result_matches_regex(calc_result: &CalcResult, reg: &regex::Regex) -> bool {
match calc_result { match calc_result {
CalcResult::String(s) => reg.is_match(&s.to_lowercase()), CalcResult::String(s) => reg.is_match(&s.to_lowercase()),
_ => false, _ => false,
@@ -269,8 +275,8 @@ fn result_is_greater_or_equal_than_string(calc_result: &CalcResult, target: &str
} }
} }
/// ERRORS /// // ERRORS ///
///********/// //********///
fn result_is_equal_to_error(calc_result: &CalcResult, target: &str) -> bool { fn result_is_equal_to_error(calc_result: &CalcResult, target: &str) -> bool {
match calc_result { match calc_result {
@@ -286,8 +292,8 @@ fn result_is_not_equal_to_error(calc_result: &CalcResult, target: &str) -> bool
} }
} }
/// EMPTY /// // EMPTY ///
///*******/// //*******///
// Note that these two are not inverse of each other. // Note that these two are not inverse of each other.
// In particular, you can never match an empty cell. // In particular, you can never match an empty cell.
@@ -392,10 +398,8 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResul
// An error will match an error (never a string that is an error) // An error will match an error (never a string that is an error)
Box::new(move |x| result_is_equal_to_error(x, &error.to_string())) Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
} }
CalcResult::Range { left: _, right: _ } => { CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false),
// TODO: Implicit Intersection CalcResult::Array(_) => Box::new(move |_x| false),
Box::new(move |_x| false)
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty), CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
} }
} }

View File

@@ -251,20 +251,28 @@ impl Model {
let column1 = left.column; let column1 = left.column;
if row1 == 1 && row2 == LAST_ROW { if row1 == 1 && row2 == LAST_ROW {
row2 = self row2 = match self.workbook.worksheet(left.sheet) {
.workbook Ok(s) => s.dimension().max_row,
.worksheet(left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension() Error::ERROR,
.max_row; cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
column2 = self column2 = match self.workbook.worksheet(left.sheet) {
.workbook Ok(s) => s.dimension().max_column,
.worksheet(left.sheet) Err(_) => {
.expect("Sheet expected during evaluation.") return CalcResult::new_error(
.dimension() Error::ERROR,
.max_column; cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
} }
let left = CellReferenceIndex { let left = CellReferenceIndex {
sheet: left.sheet, sheet: left.sheet,

View File

@@ -0,0 +1 @@
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!

View File

@@ -1,20 +1,16 @@
use once_cell::sync::Lazy; use std::{collections::HashMap, sync::OnceLock};
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use bitcode::{Decode, Encode};
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Booleans { pub struct Booleans {
#[serde(rename = "true")] pub r#true: String,
pub true_value: String, pub r#false: String,
#[serde(rename = "false")]
pub false_value: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Errors { pub struct Errors {
#[serde(rename = "ref")] pub r#ref: String,
pub ref_value: String,
pub name: String, pub name: String,
pub value: String, pub value: String,
pub div: String, pub div: String,
@@ -28,19 +24,23 @@ pub struct Errors {
pub null: String, pub null: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Language { pub struct Language {
pub booleans: Booleans, pub booleans: Booleans,
pub errors: Errors, pub errors: Errors,
} }
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| { static LANGUAGES: OnceLock<HashMap<String, Language>> = OnceLock::new();
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file")
}); #[allow(clippy::expect_used)]
fn get_languages() -> &'static HashMap<String, Language> {
LANGUAGES.get_or_init(|| {
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
})
}
pub fn get_language(id: &str) -> Result<&Language, String> { pub fn get_language(id: &str) -> Result<&Language, String> {
let language = LANGUAGES get_languages()
.get(id) .get(id)
.ok_or(format!("Language is not supported: '{}'", id))?; .ok_or_else(|| format!("Language is not supported: '{id}'"))
Ok(language)
} }

View File

@@ -25,6 +25,8 @@
#![doc = include_str!("../examples/formulas_and_errors.rs")] #![doc = include_str!("../examples/formulas_and_errors.rs")]
//! ``` //! ```
#![warn(clippy::print_stdout)]
pub mod calc_result; pub mod calc_result;
pub mod cell; pub mod cell;
pub mod expressions; pub mod expressions;
@@ -37,9 +39,9 @@ pub mod types;
pub mod worksheet; pub mod worksheet;
mod actions; mod actions;
mod arithmetic;
mod cast; mod cast;
mod constants; mod constants;
mod diffs;
mod functions; mod functions;
mod implicit_intersection; mod implicit_intersection;
mod model; mod model;
@@ -57,4 +59,6 @@ pub mod mock_time;
pub use model::get_milliseconds_since_epoch; pub use model::get_milliseconds_since_epoch;
pub use model::Model; pub use model::Model;
pub use user_model::BorderArea;
pub use user_model::ClipboardData;
pub use user_model::UserModel; pub use user_model::UserModel;

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