Compare commits

..

164 Commits

Author SHA1 Message Date
Nicolás Hatcher
443ff6808d UPDATE: WIP 2025-10-12 17:01:52 +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
207 changed files with 12644 additions and 6546 deletions

View File

@@ -32,9 +32,9 @@ jobs:
manylinux: auto manylinux: auto
working-directory: bindings/python working-directory: bindings/python
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist path: bindings/python/dist
windows: windows:
@@ -56,9 +56,9 @@ jobs:
sccache: 'true' sccache: 'true'
working-directory: bindings/python working-directory: bindings/python
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist path: bindings/python/dist
macos: macos:
@@ -79,9 +79,9 @@ jobs:
sccache: 'true' sccache: 'true'
working-directory: bindings/python working-directory: bindings/python
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist path: bindings/python/dist
sdist: sdist:
@@ -95,9 +95,9 @@ jobs:
args: --out dist args: --out dist
working-directory: bindings/python working-directory: bindings/python
- name: Upload sdist - name: Upload sdist
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-sdist
path: bindings/python/dist path: bindings/python/dist
publish-to-test-pypi: publish-to-test-pypi:
@@ -107,9 +107,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist] needs: [linux, windows, macos, sdist]
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: wheels
path: bindings/python/ path: bindings/python/
- name: Publish distribution 📦 to Test PyPI - name: Publish distribution 📦 to Test PyPI
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
@@ -118,7 +117,7 @@ jobs:
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/" MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
with: with:
command: upload command: upload
args: --skip-existing * args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python working-directory: bindings/python
publish-pypi: publish-pypi:
@@ -128,9 +127,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist] needs: [linux, windows, macos, sdist]
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: wheels
path: bindings/python/ path: bindings/python/
- name: Publish distribution 📦 to PyPI - name: Publish distribution 📦 to PyPI
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
@@ -139,5 +137,5 @@ jobs:
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/" MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
with: with:
command: upload command: upload
args: --skip-existing * args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python working-directory: bindings/python

69
Cargo.lock generated
View File

@@ -414,7 +414,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -430,14 +430,13 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"csv", "csv",
"js-sys", "js-sys",
"once_cell",
"rand", "rand",
"regex", "regex",
"regex-lite", "regex-lite",
@@ -448,7 +447,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.3.1" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"napi", "napi",
@@ -721,11 +720,10 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.23.4" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
dependencies = [ dependencies = [
"cfg-if",
"indoc", "indoc",
"libc", "libc",
"memoffset", "memoffset",
@@ -739,9 +737,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.23.4" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@@ -749,9 +747,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.23.4" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@@ -759,9 +757,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.23.4" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -771,9 +769,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.23.4" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -784,8 +782,9 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.3.0" version = "0.5.7"
dependencies = [ dependencies = [
"bitcode",
"ironcalc", "ironcalc",
"pyo3", "pyo3",
"serde", "serde",
@@ -872,6 +871,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"
@@ -979,9 +984,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
@@ -1070,7 +1075,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.3.2" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",
@@ -1081,23 +1086,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",
@@ -1118,9 +1124,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",
@@ -1128,9 +1134,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",
@@ -1141,9 +1147,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

@@ -77,14 +77,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,
}; };

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.3.0" version = "0.5.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"
@@ -17,7 +17,6 @@ chrono = "0.4"
chrono-tz = "0.10" chrono-tz = "0.10"
regex = { version = "1.0", optional = true} regex = { version = "1.0", optional = true}
regex-lite = { version = "0.1.6", optional = true} regex-lite = { version = "0.1.6", optional = true}
once_cell = "1.16.0"
bitcode = "0.6.3" bitcode = "0.6.3"
csv = "1.3.0" csv = "1.3.0"

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.
@@ -134,7 +164,7 @@ impl Model {
column, column,
delta: column_count, delta: column_count,
}), }),
); )?;
// In the list of columns: // In the list of columns:
// * Keep all the columns to the left // * Keep all the columns to the left
@@ -182,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;
@@ -214,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
@@ -338,7 +374,7 @@ impl Model {
row, row,
delta: row_count, delta: row_count,
}), }),
); )?;
Ok(()) Ok(())
} }
@@ -354,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();
@@ -399,7 +442,7 @@ impl Model {
row, row,
delta: -row_count, delta: -row_count,
}), }),
); )?;
Ok(()) Ok(())
} }
@@ -414,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(
@@ -439,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,6 +1,6 @@
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)] #[derive(Clone)]
pub struct Range { pub struct Range {
@@ -24,6 +24,7 @@ pub(crate) enum CalcResult {
}, },
EmptyCell, EmptyCell,
EmptyArg, EmptyArg,
Array(Vec<Vec<ArrayNode>>),
} }
impl CalcResult { impl CalcResult {

View File

@@ -1,11 +1,85 @@
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,
},
model::Model, model::Model,
}; };
pub(crate) enum NumberOrArray {
Number(f64),
Array(Vec<Vec<ArrayNode>>),
}
impl Model { impl Model {
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 s.parse::<f64>() {
Ok(f) => Ok(NumberOrArray::Number(f)),
_ => 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,
@@ -39,19 +113,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) => { origin: cell,
let result = self.evaluate_cell(cell_reference); message: "Arrays not supported yet".to_string(),
self.cast_to_number(result, cell_reference) }),
} CalcResult::Array(_) => Err(CalcResult::Error {
None => Err(CalcResult::Error { error: Error::NIMPL,
error: Error::VALUE, origin: cell,
origin: cell, message: "Arrays not supported yet".to_string(),
message: "Invalid reference (number)".to_string(), }),
}),
}
}
} }
} }
@@ -88,7 +159,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 +170,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) => { origin: cell,
let result = self.evaluate_cell(cell_reference); message: "Arrays not supported yet".to_string(),
self.cast_to_string(result, cell_reference) }),
} CalcResult::Array(_) => Err(CalcResult::Error {
None => Err(CalcResult::Error { error: Error::NIMPL,
error: Error::VALUE, origin: cell,
origin: cell, message: "Arrays not supported yet".to_string(),
message: "Invalid reference (string)".to_string(), }),
}),
}
}
} }
} }
@@ -151,19 +219,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) => { origin: cell,
let result = self.evaluate_cell(cell_reference); message: "Arrays not supported yet".to_string(),
self.cast_to_bool(result, cell_reference) }),
} CalcResult::Array(_) => Err(CalcResult::Error {
None => Err(CalcResult::Error { error: Error::NIMPL,
error: Error::VALUE, origin: cell,
origin: cell, message: "Arrays not supported yet".to_string(),
message: "Invalid reference (bool)".to_string(), }),
}),
}
}
} }
} }

View File

@@ -1,138 +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 {
#[allow(clippy::expect_used)]
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");
}
}
}
#[allow(clippy::expect_used)]
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(',') {
@@ -313,6 +314,9 @@ impl Lexer {
} else if name_upper == self.language.booleans.r#false { } 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),
} }
} }

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

@@ -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,8 +46,8 @@ 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 tests; mod tests;
@@ -81,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,
@@ -90,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),
@@ -163,10 +175,14 @@ pub enum Node {
name: String, name: String,
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Node>), ArrayKind(Vec<Vec<ArrayNode>>),
DefinedNameKind((String, Option<u32>)), DefinedNameKind(DefinedNameS),
TableNameKind(String), TableNameKind(String),
WrongVariableKind(String), WrongVariableKind(String),
ImplicitIntersection {
automatic: bool,
child: Box<Node>,
},
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -189,7 +205,7 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, defined_names: Vec<DefinedNameS>,
context: CellReferenceRC, context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
@@ -197,7 +213,7 @@ pub struct Parser {
impl Parser { impl Parser {
pub fn new( pub fn new(
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
) -> Parser { ) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
@@ -228,7 +244,7 @@ impl Parser {
pub fn set_worksheets_and_names( pub fn set_worksheets_and_names(
&mut self, &mut self,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, defined_names: Vec<DefinedNameS>,
) { ) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names; self.defined_names = defined_names;
@@ -252,17 +268,17 @@ impl Parser {
// Returns: // Returns:
// * None: If there is no defined name by that name // * None: If there is no defined name by that name
// * Some(Some(index)): If there is a defined name local to that sheet // * Some((Some(index), formula)): If there is a defined name local to that sheet
// * Some(None): If there is a global defined name // * Some(None): If there is a global defined name
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> { fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
for (df_name, df_scope) in &self.defined_names { for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) { if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope); return Some((*df_scope, df_formula.to_owned()));
} }
} }
for (df_name, df_scope) in &self.defined_names { for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() { if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some(None); return Some((None, df_formula.to_owned()));
} }
} }
None None
@@ -411,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;
} }
@@ -430,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 {
@@ -451,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(),
@@ -473,7 +562,7 @@ impl Parser {
message: err.message, message: err.message,
}; };
} }
Node::ArrayKind(args) Node::ArrayKind(matrix)
} }
TokenType::Reference { TokenType::Reference {
sheet, sheet,
@@ -604,6 +693,20 @@ impl Parser {
args, args,
}; };
} }
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 = &self.context;
@@ -614,14 +717,14 @@ impl Parser {
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),
}; };
} }
}; };
// Could be a defined name or a table // Could be a defined name or a table
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) { if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope)); return Node::DefinedNameKind((name, scope, formula));
} }
let name_lower = name.to_lowercase(); let name_lower = name.to_lowercase();
for table_name in self.tables.keys() { for table_name in self.tables.keys() {
@@ -706,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
@@ -717,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(),
@@ -739,7 +850,7 @@ impl Parser {
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),
}; };
} }
}; };
@@ -767,7 +878,7 @@ impl Parser {
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),
}; };
} }
}; };

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,41 @@ 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;
}
// Reuse your existing element-stringification function
row_string.push_str(&to_string_array_node(el));
}
// Enclose the row in braces
matrix_string.push('{');
matrix_string.push_str(&row_string);
matrix_string.push('}');
} }
format!("{{{}}}", arguments)
// Enclose the whole matrix in braces
format!("{{{matrix_string}}}")
} }
DefinedNameKind((name, _)) => name.to_string(), DefinedNameKind((name, ..)) => name.to_string(),
TableNameKind(name) => name.to_string(), TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
@@ -388,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))
}
} }
} }

View File

@@ -0,0 +1,994 @@
use crate::functions::Function;
use super::Node;
use regex::Regex;
use std::sync::OnceLock;
static RANGE_REFERENCE_REGEX: OnceLock<Regex> = OnceLock::new();
#[allow(clippy::expect_used)]
fn get_re() -> &'static Regex {
RANGE_REFERENCE_REGEX
.get_or_init(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"))
}
fn is_range_reference(s: &str) -> bool {
get_re().is_match(s)
}
/*
# NOTES on the Implicit Intersection operator: @
Sometimes we obtain a range where we expected a single argument. This can happen:
* As an argument of a function, eg: `SIN(A1:A5)`
* As the result of a computation of a formula `=A1:A5`
In previous versions of the Friendly Giant the spreadsheet engine would perform an operation called _implicit intersection_
that tries to find a single cell within the range. It works by picking a cell in the range that is the same row or the same column
as the cell. If there is just one we return that otherwise we return the `#REF!` error.
Examples:
* Siting on `C3` the formula `=D1:D5` will return `D3`
* Sitting on `C3` the formula `=D:D` will return `D3`
* Sitting on `C3` the formula `=A1:A7` will return `A3`
* Sitting on `C3` the formula `=A5:A8` will return `#REF!`
* Sitting on `C3` the formula `D1:G7` will return `#REF!`
Today's version of the engine will result in a dynamic array spilling the result through several cells.
To force the old behaviour we can use the _implicit intersection operator_: @
* `=@A1:A7` or `=SIN(@A1:A7)
When parsing formulas that come form old workbooks this is done automatically.
We call this version of the II operator the _automatic_ II operator.
We can also insert the II operator in places where before was impossible:
* `=SUM(@A1:A7)`
This formulas will not be compatible with old versions of the engine. The FG will stringify this as `=SUM(_xlfn.SIMPLE(A1:A7))`.
*/
/// Transverses the formula tree adding the implicit intersection operator in all arguments of functions that
/// expect a scalar but get a range.
/// * A:A => @A:A
/// * SIN(A1:D1) => SIN(@A1:D1)
///
/// Assumes formula return a scalar
pub fn add_implicit_intersection(node: &mut Node, add: bool) {
match node {
Node::BooleanKind(_)
| Node::NumberKind(_)
| Node::StringKind(_)
| Node::ErrorKind(_)
| Node::EmptyArgKind
| Node::ParseErrorKind { .. }
| Node::WrongReferenceKind { .. }
| Node::WrongRangeKind { .. }
| Node::InvalidFunctionKind { .. }
| Node::ArrayKind(_)
| Node::ReferenceKind { .. } => {}
Node::ImplicitIntersection { child, .. } => {
// 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, add);
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
*node = new_node
}
}
Node::RangeKind {
row1,
column1,
row2,
column2,
sheet_name,
sheet_index,
absolute_row1,
absolute_column1,
absolute_row2,
absolute_column2,
} => {
if add {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::RangeKind {
sheet_name: sheet_name.clone(),
sheet_index: *sheet_index,
absolute_row1: *absolute_row1,
absolute_column1: *absolute_column1,
row1: *row1,
column1: *column1,
absolute_row2: *absolute_row2,
absolute_column2: *absolute_column2,
row2: *row2,
column2: *column2,
}),
};
}
}
Node::OpRangeKind { left, right } => {
if add {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::OpRangeKind {
left: left.clone(),
right: right.clone(),
}),
}
}
}
// operations
Node::UnaryKind { right, .. } => add_implicit_intersection(right, add),
Node::OpConcatenateKind { left, right }
| Node::OpSumKind { left, right, .. }
| Node::OpProductKind { left, right, .. }
| Node::OpPowerKind { left, right, .. }
| Node::CompareKind { left, right, .. } => {
add_implicit_intersection(left, add);
add_implicit_intersection(right, add);
}
Node::DefinedNameKind(v) => {
if add {
// Not all defined names deserve the II operator
// For instance =Sheet1!A1 doesn't need to be intersected
if is_range_reference(&v.2) {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::DefinedNameKind(v.to_owned())),
}
}
}
}
Node::WrongVariableKind(v) => {
if add {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::WrongVariableKind(v.to_owned())),
}
}
}
Node::TableNameKind(_) => {
// noop for now
}
Node::FunctionKind { kind, args } => {
let arg_count = args.len();
let signature = get_function_args_signature(kind, arg_count);
for index in 0..arg_count {
if matches!(signature[index], Signature::Scalar)
&& matches!(
run_static_analysis_on_node(&args[index]),
StaticResult::Range(_, _) | StaticResult::Unknown
)
{
add_implicit_intersection(&mut args[index], true);
} else {
add_implicit_intersection(&mut args[index], false);
}
}
if add
&& matches!(
run_static_analysis_on_node(node),
StaticResult::Range(_, _) | StaticResult::Unknown
)
{
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(node.clone()),
}
}
}
};
}
pub(crate) enum StaticResult {
Scalar,
Array(i32, i32),
Range(i32, i32),
Unknown,
// TODO: What if one of the dimensions is known?
// what if the dimensions are unknown but bounded?
}
fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
let lhs = run_static_analysis_on_node(left);
let rhs = run_static_analysis_on_node(right);
match (lhs, rhs) {
(StaticResult::Scalar, StaticResult::Scalar) => StaticResult::Scalar,
(StaticResult::Scalar, StaticResult::Array(a, b) | StaticResult::Range(a, b)) => {
StaticResult::Array(a, b)
}
(StaticResult::Array(a, b) | StaticResult::Range(a, b), StaticResult::Scalar) => {
StaticResult::Array(a, b)
}
(
StaticResult::Array(a1, b1) | StaticResult::Range(a1, b1),
StaticResult::Array(a2, b2) | StaticResult::Range(a2, b2),
) => StaticResult::Array(a1.max(a2), b1.max(b2)),
(_, StaticResult::Unknown) => StaticResult::Unknown,
(StaticResult::Unknown, _) => StaticResult::Unknown,
}
}
// Returns:
// * Scalar if we can proof the result of the evaluation is a scalar
// * Array(a, b) if we know it will be an a x b array.
// * Range(a, b) if we know it will be a a x b range.
// * Unknown if we cannot guaranty either
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
match node {
Node::BooleanKind(_)
| Node::NumberKind(_)
| Node::StringKind(_)
| Node::ErrorKind(_)
| Node::EmptyArgKind => StaticResult::Scalar,
Node::UnaryKind { right, .. } => run_static_analysis_on_node(right),
Node::ParseErrorKind { .. } => {
// StaticResult::Unknown is also valid
StaticResult::Scalar
}
Node::WrongReferenceKind { .. } => {
// StaticResult::Unknown is also valid
StaticResult::Scalar
}
Node::WrongRangeKind { .. } => {
// StaticResult::Unknown or Array is also valid
StaticResult::Scalar
}
Node::InvalidFunctionKind { .. } => {
// StaticResult::Unknown is also valid
StaticResult::Scalar
}
Node::ArrayKind(array) => {
let n = array.len() as i32;
// FIXME: This is a placeholder until we implement arrays
StaticResult::Array(n, 1)
}
Node::RangeKind {
row1,
column1,
row2,
column2,
..
} => StaticResult::Range(row2 - row1, column2 - column1),
Node::OpRangeKind { .. } => {
// TODO: We could do a bit better here
StaticResult::Unknown
}
Node::ReferenceKind { .. } => StaticResult::Scalar,
// binary operations
Node::OpConcatenateKind { left, right } => static_analysis_op_nodes(left, right),
Node::OpSumKind { left, right, .. } => static_analysis_op_nodes(left, right),
Node::OpProductKind { left, right, .. } => static_analysis_op_nodes(left, right),
Node::OpPowerKind { left, right, .. } => static_analysis_op_nodes(left, right),
Node::CompareKind { left, right, .. } => static_analysis_op_nodes(left, right),
// defined names
Node::DefinedNameKind(_) => StaticResult::Unknown,
Node::WrongVariableKind(_) => StaticResult::Unknown,
Node::TableNameKind(_) => StaticResult::Unknown,
Node::FunctionKind { kind, args } => static_analysis_on_function(kind, args),
Node::ImplicitIntersection { .. } => StaticResult::Scalar,
}
}
// If all the arguments are scalars the function will return a scalar
// If any of the arguments is a range or an array it will return an array
fn scalar_arguments(args: &[Node]) -> StaticResult {
let mut n = 0;
let mut m = 0;
for arg in args {
match run_static_analysis_on_node(arg) {
StaticResult::Scalar => {
// noop
}
StaticResult::Array(a, b) | StaticResult::Range(a, b) => {
n = n.max(a);
m = m.max(b);
}
StaticResult::Unknown => return StaticResult::Unknown,
}
}
if n == 0 && m == 0 {
return StaticResult::Scalar;
}
StaticResult::Array(n, m)
}
// We only care if the function can return a range or not
fn not_implemented(_args: &[Node]) -> StaticResult {
StaticResult::Scalar
}
fn static_analysis_offset(args: &[Node]) -> StaticResult {
// If first argument is a single cell reference and there are no4th and 5th argument,
// or they are 1, then it is a scalar
let arg_count = args.len();
if arg_count < 3 {
// Actually an error
return StaticResult::Scalar;
}
if !matches!(args[0], Node::ReferenceKind { .. }) {
return StaticResult::Unknown;
}
if arg_count == 3 {
return StaticResult::Scalar;
}
match args[3] {
Node::NumberKind(f) => {
if f != 1.0 {
return StaticResult::Unknown;
}
}
_ => return StaticResult::Unknown,
};
if arg_count == 4 {
return StaticResult::Scalar;
}
match args[4] {
Node::NumberKind(f) => {
if f != 1.0 {
return StaticResult::Unknown;
}
}
_ => return StaticResult::Unknown,
};
StaticResult::Unknown
}
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
// // We will always insert the @ in CHOOSE, but technically it is only needed if one of the elements is a range
// StaticResult::Unknown
// }
fn static_analysis_indirect(_args: &[Node]) -> StaticResult {
// We will always insert the @, but we don't need to do that in every scenario`
StaticResult::Unknown
}
fn static_analysis_index(_args: &[Node]) -> StaticResult {
// INDEX has two forms, but they are indistinguishable at parse time.
StaticResult::Unknown
}
#[derive(Clone)]
enum Signature {
Scalar,
Vector,
Error,
}
fn args_signature_no_args(arg_count: usize) -> Vec<Signature> {
if arg_count == 0 {
vec![]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_scalars(
arg_count: usize,
required_count: usize,
optional_count: usize,
) -> Vec<Signature> {
if arg_count >= required_count && arg_count <= required_count + optional_count {
vec![Signature::Scalar; arg_count]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_one_vector(arg_count: usize) -> Vec<Signature> {
if arg_count == 1 {
vec![Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_sumif(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
// 1 or none scalars
fn args_signature_sheet(arg_count: usize) -> Vec<Signature> {
if arg_count == 0 {
vec![]
} else if arg_count == 1 {
vec![Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_hlookup(arg_count: usize) -> Vec<Signature> {
if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Vector,
Signature::Vector,
Signature::Scalar,
Signature::Vector,
]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_index(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_lookup(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_match(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_offset(arg_count: usize) -> Vec<Signature> {
if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
]
} else if arg_count == 5 {
vec![
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_row(arg_count: usize) -> Vec<Signature> {
if arg_count == 0 {
vec![]
} else if arg_count == 1 {
vec![Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_xlookup(arg_count: usize) -> Vec<Signature> {
if !(3..=6).contains(&arg_count) {
return vec![Signature::Error; arg_count];
}
let mut result = vec![Signature::Scalar; arg_count];
result[0] = Signature::Vector;
result[1] = Signature::Vector;
result[2] = Signature::Vector;
result
}
fn args_signature_textafter(arg_count: usize) -> Vec<Signature> {
if !(2..=6).contains(&arg_count) {
vec![Signature::Scalar; arg_count]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_textjoin(arg_count: usize) -> Vec<Signature> {
if arg_count >= 3 {
let mut result = vec![Signature::Vector; arg_count];
result[0] = Signature::Scalar;
result[1] = Signature::Scalar;
result
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_npv(arg_count: usize) -> Vec<Signature> {
if arg_count < 2 {
return vec![Signature::Error; arg_count];
}
let mut result = vec![Signature::Vector; arg_count];
result[0] = Signature::Scalar;
result
}
fn args_signature_irr(arg_count: usize) -> Vec<Signature> {
if arg_count > 2 {
vec![Signature::Error; arg_count]
} else if arg_count == 1 {
vec![Signature::Vector]
} else {
vec![Signature::Vector, Signature::Scalar]
}
}
fn args_signature_xirr(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector; arg_count]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_mirr(arg_count: usize) -> Vec<Signature> {
if arg_count != 3 {
vec![Signature::Error; arg_count]
} else {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
}
}
fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
if arg_count != 3 {
vec![Signature::Error; arg_count]
} else {
vec![Signature::Scalar, Signature::Vector, Signature::Vector]
}
}
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
// 1. When computing the function
// 2. Checking the arguments to see if we need to insert the implicit intersection operator
// 3. Understanding the return value
//
// The signature of the functions should be defined only once
// Given a function and a number of arguments this returns the arguments at each position
// are expected to be scalars or vectors (array/ranges).
// Sets signature::Error to all arguments if the number of arguments is incorrect.
fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signature> {
match kind {
Function::And => vec![Signature::Vector; arg_count],
Function::False => args_signature_no_args(arg_count),
Function::If => args_signature_scalars(arg_count, 2, 1),
Function::Iferror => args_signature_scalars(arg_count, 2, 0),
Function::Ifna => args_signature_scalars(arg_count, 2, 0),
Function::Ifs => vec![Signature::Scalar; arg_count],
Function::Not => args_signature_scalars(arg_count, 1, 0),
Function::Or => vec![Signature::Vector; arg_count],
Function::Switch => vec![Signature::Scalar; arg_count],
Function::True => args_signature_no_args(arg_count),
Function::Xor => vec![Signature::Vector; arg_count],
Function::Abs => args_signature_scalars(arg_count, 1, 0),
Function::Acos => args_signature_scalars(arg_count, 1, 0),
Function::Acosh => args_signature_scalars(arg_count, 1, 0),
Function::Asin => args_signature_scalars(arg_count, 1, 0),
Function::Asinh => args_signature_scalars(arg_count, 1, 0),
Function::Atan => args_signature_scalars(arg_count, 1, 0),
Function::Atan2 => args_signature_scalars(arg_count, 2, 0),
Function::Atanh => args_signature_scalars(arg_count, 1, 0),
Function::Choose => vec![Signature::Scalar; arg_count],
Function::Column => args_signature_row(arg_count),
Function::Columns => args_signature_one_vector(arg_count),
Function::Ln => args_signature_scalars(arg_count, 1, 0),
Function::Log => args_signature_scalars(arg_count, 1, 1),
Function::Log10 => args_signature_scalars(arg_count, 1, 0),
Function::Cos => args_signature_scalars(arg_count, 1, 0),
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
Function::Max => vec![Signature::Vector; arg_count],
Function::Min => vec![Signature::Vector; arg_count],
Function::Pi => args_signature_no_args(arg_count),
Function::Power => args_signature_scalars(arg_count, 2, 0),
Function::Product => vec![Signature::Vector; arg_count],
Function::Round => args_signature_scalars(arg_count, 2, 0),
Function::Rounddown => args_signature_scalars(arg_count, 2, 0),
Function::Roundup => args_signature_scalars(arg_count, 2, 0),
Function::Sin => args_signature_scalars(arg_count, 1, 0),
Function::Sinh => args_signature_scalars(arg_count, 1, 0),
Function::Sqrt => args_signature_scalars(arg_count, 1, 0),
Function::Sqrtpi => args_signature_scalars(arg_count, 1, 0),
Function::Sum => vec![Signature::Vector; arg_count],
Function::Sumif => args_signature_sumif(arg_count),
Function::Sumifs => vec![Signature::Vector; arg_count],
Function::Tan => args_signature_scalars(arg_count, 1, 0),
Function::Tanh => args_signature_scalars(arg_count, 1, 0),
Function::ErrorType => args_signature_scalars(arg_count, 1, 0),
Function::Isblank => args_signature_scalars(arg_count, 1, 0),
Function::Iserr => args_signature_scalars(arg_count, 1, 0),
Function::Iserror => args_signature_scalars(arg_count, 1, 0),
Function::Iseven => args_signature_scalars(arg_count, 1, 0),
Function::Isformula => args_signature_scalars(arg_count, 1, 0),
Function::Islogical => args_signature_scalars(arg_count, 1, 0),
Function::Isna => args_signature_scalars(arg_count, 1, 0),
Function::Isnontext => args_signature_scalars(arg_count, 1, 0),
Function::Isnumber => args_signature_scalars(arg_count, 1, 0),
Function::Isodd => args_signature_scalars(arg_count, 1, 0),
Function::Isref => args_signature_one_vector(arg_count),
Function::Istext => args_signature_scalars(arg_count, 1, 0),
Function::Na => args_signature_no_args(arg_count),
Function::Sheet => args_signature_sheet(arg_count),
Function::Type => args_signature_one_vector(arg_count),
Function::Hlookup => args_signature_hlookup(arg_count),
Function::Index => args_signature_index(arg_count),
Function::Indirect => args_signature_scalars(arg_count, 1, 0),
Function::Lookup => args_signature_lookup(arg_count),
Function::Match => args_signature_match(arg_count),
Function::Offset => args_signature_offset(arg_count),
Function::Row => args_signature_row(arg_count),
Function::Rows => args_signature_one_vector(arg_count),
Function::Vlookup => args_signature_hlookup(arg_count),
Function::Xlookup => args_signature_xlookup(arg_count),
Function::Concat => vec![Signature::Vector; arg_count],
Function::Concatenate => vec![Signature::Scalar; arg_count],
Function::Exact => args_signature_scalars(arg_count, 2, 0),
Function::Find => args_signature_scalars(arg_count, 2, 1),
Function::Left => args_signature_scalars(arg_count, 1, 1),
Function::Len => args_signature_scalars(arg_count, 1, 0),
Function::Lower => args_signature_scalars(arg_count, 1, 0),
Function::Mid => args_signature_scalars(arg_count, 3, 0),
Function::Rept => args_signature_scalars(arg_count, 2, 0),
Function::Right => args_signature_scalars(arg_count, 2, 1),
Function::Search => args_signature_scalars(arg_count, 2, 1),
Function::Substitute => args_signature_scalars(arg_count, 3, 1),
Function::T => args_signature_scalars(arg_count, 1, 0),
Function::Text => args_signature_scalars(arg_count, 2, 0),
Function::Textafter => args_signature_textafter(arg_count),
Function::Textbefore => args_signature_textafter(arg_count),
Function::Textjoin => args_signature_textjoin(arg_count),
Function::Trim => args_signature_scalars(arg_count, 1, 0),
Function::Upper => args_signature_scalars(arg_count, 1, 0),
Function::Value => args_signature_scalars(arg_count, 1, 0),
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
Function::Average => vec![Signature::Vector; arg_count],
Function::Averagea => vec![Signature::Vector; arg_count],
Function::Averageif => args_signature_sumif(arg_count),
Function::Averageifs => vec![Signature::Vector; arg_count],
Function::Count => vec![Signature::Vector; arg_count],
Function::Counta => vec![Signature::Vector; arg_count],
Function::Countblank => vec![Signature::Vector; arg_count],
Function::Countif => args_signature_sumif(arg_count),
Function::Countifs => vec![Signature::Vector; arg_count],
Function::Maxifs => vec![Signature::Vector; arg_count],
Function::Minifs => vec![Signature::Vector; arg_count],
Function::Date => args_signature_scalars(arg_count, 3, 0),
Function::Day => args_signature_scalars(arg_count, 1, 0),
Function::Edate => args_signature_scalars(arg_count, 2, 0),
Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
Function::Month => args_signature_scalars(arg_count, 1, 0),
Function::Now => args_signature_no_args(arg_count),
Function::Today => args_signature_no_args(arg_count),
Function::Year => args_signature_scalars(arg_count, 1, 0),
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
Function::Db => args_signature_scalars(arg_count, 4, 1),
Function::Ddb => args_signature_scalars(arg_count, 4, 1),
Function::Dollarde => args_signature_scalars(arg_count, 2, 0),
Function::Dollarfr => args_signature_scalars(arg_count, 2, 0),
Function::Effect => args_signature_scalars(arg_count, 2, 0),
Function::Fv => args_signature_scalars(arg_count, 3, 2),
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
Function::Irr => args_signature_irr(arg_count),
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
Function::Mirr => args_signature_mirr(arg_count),
Function::Nominal => args_signature_scalars(arg_count, 2, 0),
Function::Nper => args_signature_scalars(arg_count, 3, 2),
Function::Npv => args_signature_npv(arg_count),
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
Function::Pv => args_signature_scalars(arg_count, 3, 2),
Function::Rate => args_signature_scalars(arg_count, 3, 3),
Function::Rri => args_signature_scalars(arg_count, 3, 0),
Function::Sln => args_signature_scalars(arg_count, 3, 0),
Function::Syd => args_signature_scalars(arg_count, 4, 0),
Function::Tbilleq => args_signature_scalars(arg_count, 3, 0),
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
Function::Xirr => args_signature_xirr(arg_count),
Function::Xnpv => args_signature_xnpv(arg_count),
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
Function::Besselj => args_signature_scalars(arg_count, 2, 0),
Function::Besselk => args_signature_scalars(arg_count, 2, 0),
Function::Bessely => args_signature_scalars(arg_count, 2, 0),
Function::Erf => args_signature_scalars(arg_count, 1, 1),
Function::Erfc => args_signature_scalars(arg_count, 1, 0),
Function::ErfcPrecise => args_signature_scalars(arg_count, 1, 0),
Function::ErfPrecise => args_signature_scalars(arg_count, 1, 0),
Function::Bin2dec => args_signature_scalars(arg_count, 1, 0),
Function::Bin2hex => args_signature_scalars(arg_count, 1, 0),
Function::Bin2oct => args_signature_scalars(arg_count, 1, 0),
Function::Dec2Bin => args_signature_scalars(arg_count, 1, 0),
Function::Dec2hex => args_signature_scalars(arg_count, 1, 0),
Function::Dec2oct => args_signature_scalars(arg_count, 1, 0),
Function::Hex2bin => args_signature_scalars(arg_count, 1, 0),
Function::Hex2dec => args_signature_scalars(arg_count, 1, 0),
Function::Hex2oct => args_signature_scalars(arg_count, 1, 0),
Function::Oct2bin => args_signature_scalars(arg_count, 1, 0),
Function::Oct2dec => args_signature_scalars(arg_count, 1, 0),
Function::Oct2hex => args_signature_scalars(arg_count, 1, 0),
Function::Bitand => args_signature_scalars(arg_count, 2, 0),
Function::Bitlshift => args_signature_scalars(arg_count, 2, 0),
Function::Bitor => args_signature_scalars(arg_count, 2, 0),
Function::Bitrshift => args_signature_scalars(arg_count, 2, 0),
Function::Bitxor => args_signature_scalars(arg_count, 2, 0),
Function::Complex => args_signature_scalars(arg_count, 2, 1),
Function::Imabs => args_signature_scalars(arg_count, 1, 0),
Function::Imaginary => args_signature_scalars(arg_count, 1, 0),
Function::Imargument => args_signature_scalars(arg_count, 1, 0),
Function::Imconjugate => args_signature_scalars(arg_count, 1, 0),
Function::Imcos => args_signature_scalars(arg_count, 1, 0),
Function::Imcosh => args_signature_scalars(arg_count, 1, 0),
Function::Imcot => args_signature_scalars(arg_count, 1, 0),
Function::Imcsc => args_signature_scalars(arg_count, 1, 0),
Function::Imcsch => args_signature_scalars(arg_count, 1, 0),
Function::Imdiv => args_signature_scalars(arg_count, 2, 0),
Function::Imexp => args_signature_scalars(arg_count, 1, 0),
Function::Imln => args_signature_scalars(arg_count, 1, 0),
Function::Imlog10 => args_signature_scalars(arg_count, 1, 0),
Function::Imlog2 => args_signature_scalars(arg_count, 1, 0),
Function::Impower => args_signature_scalars(arg_count, 2, 0),
Function::Improduct => args_signature_scalars(arg_count, 2, 0),
Function::Imreal => args_signature_scalars(arg_count, 1, 0),
Function::Imsec => args_signature_scalars(arg_count, 1, 0),
Function::Imsech => args_signature_scalars(arg_count, 1, 0),
Function::Imsin => args_signature_scalars(arg_count, 1, 0),
Function::Imsinh => args_signature_scalars(arg_count, 1, 0),
Function::Imsqrt => args_signature_scalars(arg_count, 1, 0),
Function::Imsub => args_signature_scalars(arg_count, 2, 0),
Function::Imsum => args_signature_scalars(arg_count, 2, 0),
Function::Imtan => args_signature_scalars(arg_count, 1, 0),
Function::Convert => args_signature_scalars(arg_count, 3, 0),
Function::Delta => args_signature_scalars(arg_count, 1, 1),
Function::Gestep => args_signature_scalars(arg_count, 1, 1),
Function::Subtotal => args_signature_npv(arg_count),
Function::Rand => args_signature_no_args(arg_count),
Function::Randbetween => args_signature_scalars(arg_count, 2, 0),
Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
Function::Unicode => args_signature_scalars(arg_count, 1, 0),
Function::Geomean => vec![Signature::Vector; arg_count],
}
}
// Returns the type of the result (Scalar, Array or Range) depending on the arguments
fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
match kind {
Function::And => StaticResult::Scalar,
Function::False => StaticResult::Scalar,
Function::If => scalar_arguments(args),
Function::Iferror => scalar_arguments(args),
Function::Ifna => scalar_arguments(args),
Function::Ifs => not_implemented(args),
Function::Not => StaticResult::Scalar,
Function::Or => StaticResult::Scalar,
Function::Switch => not_implemented(args),
Function::True => StaticResult::Scalar,
Function::Xor => StaticResult::Scalar,
Function::Abs => scalar_arguments(args),
Function::Acos => scalar_arguments(args),
Function::Acosh => scalar_arguments(args),
Function::Asin => scalar_arguments(args),
Function::Asinh => scalar_arguments(args),
Function::Atan => scalar_arguments(args),
Function::Atan2 => scalar_arguments(args),
Function::Atanh => scalar_arguments(args),
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
Function::Column => not_implemented(args),
Function::Columns => not_implemented(args),
Function::Cos => scalar_arguments(args),
Function::Cosh => scalar_arguments(args),
Function::Max => StaticResult::Scalar,
Function::Min => StaticResult::Scalar,
Function::Pi => StaticResult::Scalar,
Function::Power => scalar_arguments(args),
Function::Product => not_implemented(args),
Function::Round => scalar_arguments(args),
Function::Rounddown => scalar_arguments(args),
Function::Roundup => scalar_arguments(args),
Function::Ln => scalar_arguments(args),
Function::Log => scalar_arguments(args),
Function::Log10 => scalar_arguments(args),
Function::Sin => scalar_arguments(args),
Function::Sinh => scalar_arguments(args),
Function::Sqrt => scalar_arguments(args),
Function::Sqrtpi => StaticResult::Scalar,
Function::Sum => StaticResult::Scalar,
Function::Sumif => not_implemented(args),
Function::Sumifs => not_implemented(args),
Function::Tan => scalar_arguments(args),
Function::Tanh => scalar_arguments(args),
Function::ErrorType => not_implemented(args),
Function::Isblank => not_implemented(args),
Function::Iserr => not_implemented(args),
Function::Iserror => not_implemented(args),
Function::Iseven => not_implemented(args),
Function::Isformula => not_implemented(args),
Function::Islogical => not_implemented(args),
Function::Isna => not_implemented(args),
Function::Isnontext => not_implemented(args),
Function::Isnumber => not_implemented(args),
Function::Isodd => not_implemented(args),
Function::Isref => not_implemented(args),
Function::Istext => not_implemented(args),
Function::Na => StaticResult::Scalar,
Function::Sheet => StaticResult::Scalar,
Function::Type => not_implemented(args),
Function::Hlookup => not_implemented(args),
Function::Index => static_analysis_index(args),
Function::Indirect => static_analysis_indirect(args),
Function::Lookup => not_implemented(args),
Function::Match => not_implemented(args),
Function::Offset => static_analysis_offset(args),
// FIXME: Row could return an array
Function::Row => StaticResult::Scalar,
Function::Rows => not_implemented(args),
Function::Vlookup => not_implemented(args),
Function::Xlookup => not_implemented(args),
Function::Concat => not_implemented(args),
Function::Concatenate => not_implemented(args),
Function::Exact => not_implemented(args),
Function::Find => not_implemented(args),
Function::Left => not_implemented(args),
Function::Len => not_implemented(args),
Function::Lower => not_implemented(args),
Function::Mid => not_implemented(args),
Function::Rept => not_implemented(args),
Function::Right => not_implemented(args),
Function::Search => not_implemented(args),
Function::Substitute => not_implemented(args),
Function::T => not_implemented(args),
Function::Text => not_implemented(args),
Function::Textafter => not_implemented(args),
Function::Textbefore => not_implemented(args),
Function::Textjoin => not_implemented(args),
Function::Trim => not_implemented(args),
Function::Unicode => not_implemented(args),
Function::Upper => not_implemented(args),
Function::Value => not_implemented(args),
Function::Valuetotext => not_implemented(args),
Function::Average => not_implemented(args),
Function::Averagea => not_implemented(args),
Function::Averageif => not_implemented(args),
Function::Averageifs => not_implemented(args),
Function::Count => not_implemented(args),
Function::Counta => not_implemented(args),
Function::Countblank => not_implemented(args),
Function::Countif => not_implemented(args),
Function::Countifs => not_implemented(args),
Function::Maxifs => not_implemented(args),
Function::Minifs => not_implemented(args),
Function::Date => not_implemented(args),
Function::Day => not_implemented(args),
Function::Edate => not_implemented(args),
Function::Month => not_implemented(args),
Function::Now => not_implemented(args),
Function::Today => not_implemented(args),
Function::Year => not_implemented(args),
Function::Cumipmt => not_implemented(args),
Function::Cumprinc => not_implemented(args),
Function::Db => not_implemented(args),
Function::Ddb => not_implemented(args),
Function::Dollarde => not_implemented(args),
Function::Dollarfr => not_implemented(args),
Function::Effect => not_implemented(args),
Function::Fv => not_implemented(args),
Function::Ipmt => not_implemented(args),
Function::Irr => not_implemented(args),
Function::Ispmt => not_implemented(args),
Function::Mirr => not_implemented(args),
Function::Nominal => not_implemented(args),
Function::Nper => not_implemented(args),
Function::Npv => not_implemented(args),
Function::Pduration => not_implemented(args),
Function::Pmt => not_implemented(args),
Function::Ppmt => not_implemented(args),
Function::Pv => not_implemented(args),
Function::Rate => not_implemented(args),
Function::Rri => not_implemented(args),
Function::Sln => not_implemented(args),
Function::Syd => not_implemented(args),
Function::Tbilleq => not_implemented(args),
Function::Tbillprice => not_implemented(args),
Function::Tbillyield => not_implemented(args),
Function::Xirr => not_implemented(args),
Function::Xnpv => not_implemented(args),
Function::Besseli => scalar_arguments(args),
Function::Besselj => scalar_arguments(args),
Function::Besselk => scalar_arguments(args),
Function::Bessely => scalar_arguments(args),
Function::Erf => scalar_arguments(args),
Function::Erfc => scalar_arguments(args),
Function::ErfcPrecise => scalar_arguments(args),
Function::ErfPrecise => scalar_arguments(args),
Function::Bin2dec => scalar_arguments(args),
Function::Bin2hex => scalar_arguments(args),
Function::Bin2oct => scalar_arguments(args),
Function::Dec2Bin => scalar_arguments(args),
Function::Dec2hex => scalar_arguments(args),
Function::Dec2oct => scalar_arguments(args),
Function::Hex2bin => scalar_arguments(args),
Function::Hex2dec => scalar_arguments(args),
Function::Hex2oct => scalar_arguments(args),
Function::Oct2bin => scalar_arguments(args),
Function::Oct2dec => scalar_arguments(args),
Function::Oct2hex => scalar_arguments(args),
Function::Bitand => scalar_arguments(args),
Function::Bitlshift => scalar_arguments(args),
Function::Bitor => scalar_arguments(args),
Function::Bitrshift => scalar_arguments(args),
Function::Bitxor => scalar_arguments(args),
Function::Complex => scalar_arguments(args),
Function::Imabs => scalar_arguments(args),
Function::Imaginary => scalar_arguments(args),
Function::Imargument => scalar_arguments(args),
Function::Imconjugate => scalar_arguments(args),
Function::Imcos => scalar_arguments(args),
Function::Imcosh => scalar_arguments(args),
Function::Imcot => scalar_arguments(args),
Function::Imcsc => scalar_arguments(args),
Function::Imcsch => scalar_arguments(args),
Function::Imdiv => scalar_arguments(args),
Function::Imexp => scalar_arguments(args),
Function::Imln => scalar_arguments(args),
Function::Imlog10 => scalar_arguments(args),
Function::Imlog2 => scalar_arguments(args),
Function::Impower => scalar_arguments(args),
Function::Improduct => scalar_arguments(args),
Function::Imreal => scalar_arguments(args),
Function::Imsec => scalar_arguments(args),
Function::Imsech => scalar_arguments(args),
Function::Imsin => scalar_arguments(args),
Function::Imsinh => scalar_arguments(args),
Function::Imsqrt => scalar_arguments(args),
Function::Imsub => scalar_arguments(args),
Function::Imsum => scalar_arguments(args),
Function::Imtan => scalar_arguments(args),
Function::Convert => not_implemented(args),
Function::Delta => not_implemented(args),
Function::Gestep => not_implemented(args),
Function::Subtotal => not_implemented(args),
Function::Rand => not_implemented(args),
Function::Randbetween => scalar_arguments(args),
Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args),
}
}

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,58 +465,56 @@ 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 } => { OpPowerKind { left, right } => {
let x = match **left { let x = match **left {
@@ -467,9 +527,7 @@ fn stringify(
| DefinedNameKind(_) | DefinedNameKind(_)
| TableNameKind(_) | TableNameKind(_)
| WrongVariableKind(_) | WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
stringify(left, context, displace_data, use_original_name)
}
OpRangeKind { .. } OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
| OpProductKind { .. } | OpProductKind { .. }
@@ -482,9 +540,10 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
| CompareKind { .. } | CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!( | EmptyArgKind => format!(
"({})", "({})",
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, export_to_excel)
), ),
}; };
let y = match **right { let y = match **right {
@@ -498,7 +557,7 @@ fn stringify(
| TableNameKind(_) | TableNameKind(_)
| WrongVariableKind(_) | WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
} }
OpRangeKind { .. } OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
@@ -512,65 +571,129 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
| CompareKind { .. } | CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!( | EmptyArgKind => format!(
"({})", "({})",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
}; };
format!("{}^{}", x, y) 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;
}
row_string.push_str(&to_string_array_node(el));
}
matrix_string.push_str(&row_string);
} }
format!("{{{}}}", arguments) format!("{{{matrix_string}}}")
} }
TableNameKind(value) => value.to_string(), TableNameKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(), DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
format!( let needs_parentheses = match **right {
"-{}", BooleanKind(_)
stringify(right, context, displace_data, use_original_name) | NumberKind(_)
) | StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| WrongRangeKind { .. }
| OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| ImplicitIntersection { .. }
| CompareKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| EmptyArgKind => false,
OpSumKind { .. } | UnaryKind { .. } => true,
};
if needs_parentheses {
format!(
"-({})",
stringify(right, context, displace_data, export_to_excel)
)
} else {
format!(
"-{}",
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)
)
}
} }
} }
@@ -658,6 +781,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(_) => {}
@@ -681,7 +810,7 @@ pub(crate) fn rename_defined_name_in_node(
) { ) {
match node { match node {
// Rename // Rename
Node::DefinedNameKind((n, s)) => { Node::DefinedNameKind((n, s, _)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope { if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string(); *n = new_name.to_string();
} }
@@ -736,6 +865,12 @@ pub(crate) fn rename_defined_name_in_node(
Node::UnaryKind { kind: _, right } => { Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name); 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 // Do nothing
Node::BooleanKind(_) => {} Node::BooleanKind(_) => {}

View File

@@ -1,4 +1,7 @@
mod test_add_implicit_intersection;
mod test_arrays;
mod test_general; mod test_general;
mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_move_formula; mod test_move_formula;
mod test_ranges; mod test_ranges;

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

@@ -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

@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
width: 4, width: 4,
height: 5, height: 5,
}; };
let node = parser.parse("X9^C2-F4*H2", context); 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 {
@@ -400,7 +400,7 @@ 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)", context); let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
let t = move_formula( let t = move_formula(
@@ -475,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

@@ -32,3 +32,39 @@ fn exp_order() {
let t = parser.parse("(5)^(4)", &cell_reference); let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4"); 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,11 +3,10 @@
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::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
use crate::expressions::parser::Parser; use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC; use crate::expressions::types::CellReferenceRC;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
fn create_test_table( fn create_test_table(
table_name: &str, table_name: &str,

View File

@@ -1,278 +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::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {}
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
}
}

View File

@@ -197,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)]
@@ -240,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,4 +211,6 @@ fn test_names() {
assert!(!is_valid_identifier("test€")); assert!(!is_valid_identifier("test€"));
assert!(!is_valid_identifier("truñe")); 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

@@ -21,14 +21,12 @@ fn is_date_within_range(date: NaiveDate) -> bool {
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> { pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 { if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!( return Err(format!(
"Excel date must be greater than {}", "Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
MINIMUM_DATE_SERIAL_NUMBER
)); ));
}; };
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 { if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!( return Err(format!(
"Excel date must be less than {}", "Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
MAXIMUM_DATE_SERIAL_NUMBER
)); ));
}; };
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]

View File

@@ -120,7 +120,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 +138,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}",
@@ -167,33 +167,33 @@ 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::Digit(_) => {} TextToken::Digit(_) => {}
TextToken::Period => {} TextToken::Period => {}
TextToken::Day => { TextToken::Day => {
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{}{}", text, day); text = format!("{text}{day}");
} }
TextToken::DayPadded => { TextToken::DayPadded => {
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 mut day = date.weekday().number_from_monday() as usize; let mut day = date.weekday().number_from_monday() as usize;
@@ -211,11 +211,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} }
TextToken::Month => { TextToken::Month => {
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 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 month = date.month() as usize; let month = date.month() as usize;
@@ -228,7 +228,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => { TextToken::MonthLetter => {
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 => {
text = format!("{}{}", text, date.format("%y")); text = format!("{}{}", text, date.format("%y"));
@@ -247,7 +247,7 @@ 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 { if let Some(c) = p.currency {
text = format!("{}", c); 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));
@@ -295,26 +295,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' {
@@ -322,7 +322,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
@@ -347,7 +347,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 {
@@ -373,18 +373,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);
@@ -400,7 +400,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 {
@@ -614,7 +614,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())));

View File

@@ -178,10 +178,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)> {
@@ -336,7 +333,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 {
@@ -388,14 +385,14 @@ impl Lexer {
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

@@ -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")

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

@@ -231,7 +231,7 @@ impl Model {
CalcResult::new_error( CalcResult::new_error(
Error::ERROR, Error::ERROR,
*cell, *cell,
format!("Invalid worksheet index: '{}'", sheet), format!("Invalid worksheet index: '{sheet}'"),
) )
})? })?
.dimension() .dimension()
@@ -245,7 +245,7 @@ impl Model {
CalcResult::new_error( CalcResult::new_error(
Error::ERROR, Error::ERROR,
*cell, *cell,
format!("Invalid worksheet index: '{}'", sheet), format!("Invalid worksheet index: '{sheet}'"),
) )
})? })?
.dimension() .dimension()

View File

@@ -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 {
@@ -249,7 +254,7 @@ impl Model {
// The arg could be a defined name or a table // The arg could be a defined name or a table
// let = &args[0]; // let = &args[0];
match &args[0] { match &args[0] {
Node::DefinedNameKind((name, scope)) => { 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 if let Some(defined_name) = self
.parsed_defined_names .parsed_defined_names

View File

@@ -161,6 +161,13 @@ impl Model {
CalcResult::Range { .. } CalcResult::Range { .. }
| CalcResult::String { .. } | CalcResult::String { .. }
| CalcResult::EmptyCell => {} | 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)) = if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value) (result, short_circuit_value)
@@ -185,6 +192,13 @@ impl Model {
} }
// References to empty cells are ignored. If all args are ignored the result is #VALUE! // References to empty cells are ignored. If all args are ignored the result is #VALUE!
CalcResult::EmptyCell => {} 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 let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)
@@ -232,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 {
@@ -248,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

@@ -855,7 +855,7 @@ impl Model {
if left.row != right.row || left.column != right.column { if left.row != right.row || left.column != right.column {
// FIXME: Implicit intersection or dynamic arrays // FIXME: Implicit intersection or dynamic arrays
return CalcResult::Error { return CalcResult::Error {
error: Error::ERROR, error: Error::NIMPL,
origin: cell, origin: cell,
message: "argument must be a reference to a single cell".to_string(), message: "argument must be a reference to a single cell".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 s.parse::<f64>() {
Ok(f) => match $op(f) {
Ok(x) => ArrayNode::Number(x),
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
Err(e) => ArrayNode::Error(e),
},
Err(_) => ArrayNode::Error(Error::VALUE),
};
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

@@ -1,5 +1,9 @@
use crate::cast::NumberOrArray;
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex; use crate::expressions::types::CellReferenceIndex;
use crate::number_format::to_precision;
use crate::single_number_fn;
use crate::{ use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
}; };
@@ -169,6 +173,27 @@ impl Model {
} }
} }
} }
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
result += value;
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// We ignore booleans and strings
}
}
}
}
}
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
_ => { _ => {
// We ignore booleans and strings // We ignore booleans and strings
@@ -287,7 +312,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let value = match self.get_number(&args[0], cell) { let value = match self.get_number(&args[0], cell) {
Ok(f) => f, Ok(f) => to_precision(f, 15),
Err(s) => return s, Err(s) => return s,
}; };
let number_of_digits = match self.get_number(&args[1], cell) { let number_of_digits = match self.get_number(&args[1], cell) {
@@ -303,12 +328,13 @@ impl Model {
let scale = 10.0_f64.powf(number_of_digits); let scale = 10.0_f64.powf(number_of_digits);
CalcResult::Number((value * scale).round() / scale) CalcResult::Number((value * scale).round() / scale)
} }
pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let value = match self.get_number(&args[0], cell) { let value = match self.get_number(&args[0], cell) {
Ok(f) => f, Ok(f) => to_precision(f, 15),
Err(s) => return s, Err(s) => return s,
}; };
let number_of_digits = match self.get_number(&args[1], cell) { let number_of_digits = match self.get_number(&args[1], cell) {
@@ -328,12 +354,13 @@ impl Model {
CalcResult::Number((value * scale).floor() / scale) CalcResult::Number((value * scale).floor() / scale)
} }
} }
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let value = match self.get_number(&args[0], cell) { let value = match self.get_number(&args[0], cell) {
Ok(f) => f, Ok(f) => to_precision(f, 15),
Err(s) => return s, Err(s) => return s,
}; };
let number_of_digits = match self.get_number(&args[1], cell) { let number_of_digits = match self.get_number(&args[1], cell) {
@@ -354,187 +381,39 @@ impl Model {
} }
} }
pub(crate) fn fn_sin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { single_number_fn!(fn_log10, |f| if f <= 0.0 {
if args.len() != 1 { Err(Error::NUM)
return CalcResult::new_args_number_error(cell); } else {
} Ok(f64::log10(f))
let value = match self.get_number(&args[0], cell) { });
Ok(f) => f, single_number_fn!(fn_ln, |f| if f <= 0.0 {
Err(s) => return s, Err(Error::NUM)
}; } else {
let result = value.sin(); Ok(f64::ln(f))
CalcResult::Number(result) });
} single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
pub(crate) fn fn_cos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
if args.len() != 1 { single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
return CalcResult::new_args_number_error(cell); single_number_fn!(fn_sinh, |f| Ok(f64::sinh(f)));
} single_number_fn!(fn_cosh, |f| Ok(f64::cosh(f)));
let value = match self.get_number(&args[0], cell) { single_number_fn!(fn_tanh, |f| Ok(f64::tanh(f)));
Ok(f) => f, single_number_fn!(fn_asin, |f| Ok(f64::asin(f)));
Err(s) => return s, single_number_fn!(fn_acos, |f| Ok(f64::acos(f)));
}; single_number_fn!(fn_atan, |f| Ok(f64::atan(f)));
let result = value.cos(); single_number_fn!(fn_asinh, |f| Ok(f64::asinh(f)));
CalcResult::Number(result) single_number_fn!(fn_acosh, |f| Ok(f64::acosh(f)));
} single_number_fn!(fn_atanh, |f| Ok(f64::atanh(f)));
single_number_fn!(fn_abs, |f| Ok(f64::abs(f)));
pub(crate) fn fn_tan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { single_number_fn!(fn_sqrt, |f| if f < 0.0 {
if args.len() != 1 { Err(Error::NUM)
return CalcResult::new_args_number_error(cell); } else {
} Ok(f64::sqrt(f))
let value = match self.get_number(&args[0], cell) { });
Ok(f) => f, single_number_fn!(fn_sqrtpi, |f: f64| if f < 0.0 {
Err(s) => return s, Err(Error::NUM)
}; } else {
let result = value.tan(); Ok((f * PI).sqrt())
CalcResult::Number(result) });
}
pub(crate) fn fn_sinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.sinh();
CalcResult::Number(result)
}
pub(crate) fn fn_cosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.cosh();
CalcResult::Number(result)
}
pub(crate) fn fn_tanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.tanh();
CalcResult::Number(result)
}
pub(crate) fn fn_asin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.asin();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ASIN".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_acos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.acos();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for COS".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_atan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.atan();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ATAN".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_asinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.asinh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ASINH".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_acosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.acosh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ACOSH".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_atanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.atanh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ATANH".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() { if !args.is_empty() {
@@ -543,53 +422,6 @@ impl Model {
CalcResult::Number(PI) CalcResult::Number(PI)
} }
pub(crate) fn fn_abs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
CalcResult::Number(value.abs())
}
pub(crate) fn fn_sqrtpi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
if value < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Argument of SQRTPI should be >= 0".to_string(),
};
}
CalcResult::Number((value * PI).sqrt())
}
pub(crate) fn fn_sqrt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
if value < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Argument of SQRT should be >= 0".to_string(),
};
}
CalcResult::Number(value.sqrt())
}
pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
@@ -612,6 +444,47 @@ impl Model {
CalcResult::Number(f64::atan2(y, x)) CalcResult::Number(f64::atan2(y, x))
} }
pub(crate) fn fn_log(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let n_args = args.len();
if !(1..=2).contains(&n_args) {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let y = if n_args == 1 {
10.0
} else {
match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(s) => return s,
}
};
if x <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Number must be positive".to_string(),
};
}
if y == 1.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Logarithm base cannot be 1".to_string(),
};
}
if y <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Logarithm base must be positive".to_string(),
};
}
CalcResult::Number(f64::log(x, y))
}
pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);

View File

@@ -15,6 +15,7 @@ mod financial_util;
mod information; mod information;
mod logical; mod logical;
mod lookup_and_reference; mod lookup_and_reference;
mod macros;
mod mathematical; mod mathematical;
mod statistical; mod statistical;
mod subtotal; mod subtotal;
@@ -53,6 +54,9 @@ pub enum Function {
Columns, Columns,
Cos, Cos,
Cosh, Cosh,
Log,
Log10,
Ln,
Max, Max,
Min, Min,
Pi, Pi,
@@ -249,7 +253,7 @@ pub enum Function {
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 195> { pub fn into_iter() -> IntoIter<Function, 198> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -276,6 +280,9 @@ 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,
@@ -533,6 +540,10 @@ impl Function {
"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),
@@ -733,6 +744,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"),
@@ -960,6 +974,9 @@ impl Model {
Function::True => self.fn_true(args, cell), Function::True => self.fn_true(args, cell),
Function::Xor => self.fn_xor(args, cell), Function::Xor => self.fn_xor(args, cell),
// Math and trigonometry // 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),
@@ -1213,7 +1230,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);
} }
@@ -462,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);

View File

@@ -182,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(),
})
}
} }
} }
} }
@@ -426,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(),
}
}
} }
} }
} }

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,6 +369,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_lowercase()); return CalcResult::String(s.to_lowercase());
} }
@@ -345,7 +385,7 @@ impl Model {
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_unicode(&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 {
@@ -370,6 +410,13 @@ impl Model {
message: "Empty cell".to_string(), 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() { match s.chars().next() {
@@ -392,7 +439,7 @@ impl Model {
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 {
@@ -411,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());
} }
@@ -422,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 {
@@ -441,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) {
@@ -471,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
@@ -490,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 {
@@ -509,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) {
@@ -539,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
@@ -558,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 {
@@ -577,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) => {
@@ -641,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;
@@ -983,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(),
}
}
} }
} }
} }
@@ -1002,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);
@@ -1125,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

@@ -110,7 +110,7 @@ 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::Regex::new(&format!("^{}$", reg)); return regex::Regex::new(&format!("^{reg}$"));
} }
regex::Regex::new(reg) regex::Regex::new(reg)
} }
@@ -393,10 +393,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

@@ -141,9 +141,9 @@ impl Model {
/// * 1 - Perform a search starting at the first item. This is the default. /// * 1 - Perform a search starting at the first item. This is the default.
/// * -1 - Perform a reverse search starting at the last item. /// * -1 - Perform a reverse search starting at the last item.
/// * 2 - Perform a binary search that relies on lookup_array being sorted /// * 2 - Perform a binary search that relies on lookup_array being sorted
/// in ascending order. If not sorted, invalid results will be returned. /// in ascending order. If not sorted, invalid results will be returned.
/// * -2 - Perform a binary search that relies on lookup_array being sorted /// * -2 - Perform a binary search that relies on lookup_array being sorted
/// in descending order. If not sorted, invalid results will be returned. /// in descending order. If not sorted, invalid results will be returned.
pub(crate) fn fn_xlookup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_xlookup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() < 3 || args.len() > 6 { if args.len() < 3 || args.len() > 6 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);

View File

@@ -1,7 +1,6 @@
use std::collections::HashMap; use std::{collections::HashMap, sync::OnceLock};
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Booleans { pub struct Booleans {
@@ -31,14 +30,17 @@ pub struct Language {
pub errors: Errors, pub errors: Errors,
} }
static LANGUAGES: OnceLock<HashMap<String, Language>> = OnceLock::new();
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| { fn get_languages() -> &'static HashMap<String, Language> {
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file") 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

@@ -39,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;

View File

@@ -1,7 +1,6 @@
use bitcode::{Decode, Encode}; use std::{collections::HashMap, sync::OnceLock};
use once_cell::sync::Lazy;
use std::collections::HashMap; use bitcode::{Decode, Encode};
#[derive(Encode, Decode, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Locale { pub struct Locale {
@@ -65,12 +64,17 @@ pub struct DecimalFormats {
pub standard: String, pub standard: String,
} }
static LOCALES: OnceLock<HashMap<String, Locale>> = OnceLock::new();
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
static LOCALES: Lazy<HashMap<String, Locale>> = fn get_locales() -> &'static HashMap<String, Locale> {
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale")); LOCALES.get_or_init(|| {
bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale")
})
}
pub fn get_locale(id: &str) -> Result<&Locale, String> { pub fn get_locale(id: &str) -> Result<&Locale, String> {
// TODO: pass the locale once we implement locales in Rust get_locales()
let locale = LOCALES.get(id).ok_or("Invalid locale")?; .get(id)
Ok(locale) .ok_or_else(|| format!("Invalid locale: '{id}'"))
} }

View File

@@ -106,15 +106,15 @@ pub struct Model {
pub(crate) shared_strings: HashMap<String, usize>, pub(crate) shared_strings: HashMap<String, usize>,
/// An instance of the parser /// An instance of the parser
pub(crate) parser: Parser, pub(crate) parser: Parser,
/// The list of cells with formulas that are evaluated of being evaluated /// The list of cells with formulas that are evaluated or being evaluated
pub(crate) cells: HashMap<(u32, i32, i32), CellState>, pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The locale of the model /// The locale of the model
pub(crate) locale: Locale, pub(crate) locale: Locale,
/// Tha language used /// The language used
pub(crate) language: Language, pub(crate) language: Language,
/// The timezone used to evaluate the model /// The timezone used to evaluate the model
pub(crate) tz: Tz, pub(crate) tz: Tz,
/// The view id. A view consist of a selected sheet and ranges. /// The view id. A view consists of a selected sheet and ranges.
pub(crate) view_id: u32, pub(crate) view_id: u32,
} }
@@ -207,6 +207,17 @@ impl Model {
}, },
} }
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => match self.evaluate_node_with_reference(child, cell) {
CalcResult::Range { left, right } => CalcResult::Range { left, right },
_ => CalcResult::new_error(
Error::ERROR,
cell,
format!("Error with Implicit Intersection in cell {cell:?}"),
),
},
_ => self.evaluate_node_in_context(node, cell), _ => self.evaluate_node_in_context(node, cell),
} }
} }
@@ -256,27 +267,10 @@ impl Model {
) -> CalcResult { ) -> CalcResult {
use Node::*; use Node::*;
match node { match node {
OpSumKind { kind, left, right } => { OpSumKind { kind, left, right } => match kind {
// In the future once the feature try trait stabilizes we could use the '?' operator for this :) OpSum::Add => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 + f2)),
// See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5 OpSum::Minus => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 - f2)),
let l = match self.get_number(left, cell) { },
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
let result = match kind {
OpSum::Add => l + r,
OpSum::Minus => l - r,
};
CalcResult::Number(result)
}
NumberKind(value) => CalcResult::Number(*value), NumberKind(value) => CalcResult::Number(*value),
StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)), StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)),
BooleanKind(value) => CalcResult::Boolean(*value), BooleanKind(value) => CalcResult::Boolean(*value),
@@ -361,62 +355,30 @@ impl Model {
return s; return s;
} }
}; };
let result = format!("{}{}", l, r); let result = format!("{l}{r}");
CalcResult::String(result) CalcResult::String(result)
} }
OpProductKind { kind, left, right } => { OpProductKind { kind, left, right } => match kind {
let l = match self.get_number(left, cell) { OpProduct::Times => {
Ok(f) => f, self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 * f2))
Err(s) => { }
return s; OpProduct::Divide => self.handle_arithmetic(left, right, cell, &|f1, f2| {
if f2 == 0.0 {
Err(Error::DIV)
} else {
Ok(f1 / f2)
} }
}; }),
let r = match self.get_number(right, cell) { },
Ok(f) => f,
Err(s) => {
return s;
}
};
let result = match kind {
OpProduct::Times => l * r,
OpProduct::Divide => {
if r == 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"Divide by Zero".to_string(),
);
}
l / r
}
};
CalcResult::Number(result)
}
OpPowerKind { left, right } => { OpPowerKind { left, right } => {
let l = match self.get_number(left, cell) { self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1.powf(f2)))
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
// Deal with errors properly
CalcResult::Number(l.powf(r))
} }
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell), FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
InvalidFunctionKind { name, args: _ } => { InvalidFunctionKind { name, args: _ } => {
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name)) CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {name}"))
} }
ArrayKind(_) => { ArrayKind(s) => CalcResult::Array(s.to_owned()),
// TODO: NOT IMPLEMENTED DefinedNameKind((name, scope, _)) => {
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
}
DefinedNameKind((name, scope)) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) { if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
match parsed_defined_name { match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
@@ -429,26 +391,26 @@ impl Model {
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" is not a reference.", name), format!("Defined name \"{name}\" is not a reference."),
), ),
} }
} else { } else {
CalcResult::new_error( CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" not found.", name), format!("Defined name \"{name}\" not found."),
) )
} }
} }
TableNameKind(s) => CalcResult::new_error( TableNameKind(s) => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("table name \"{}\" not supported.", s), format!("table name \"{s}\" not supported."),
), ),
WrongVariableKind(s) => CalcResult::new_error( WrongVariableKind(s) => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Variable name \"{}\" not found.", s), format!("Variable name \"{s}\" not found."),
), ),
CompareKind { kind, left, right } => { CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell); let l = self.evaluate_node_in_context(left, cell);
@@ -525,9 +487,25 @@ impl Model {
} => CalcResult::new_error( } => CalcResult::new_error(
Error::ERROR, Error::ERROR,
cell, cell,
format!("Error parsing {}: {}", formula, message), format!("Error parsing {formula}: {message}"),
), ),
EmptyArgKind => CalcResult::EmptyArg, EmptyArgKind => CalcResult::EmptyArg,
ImplicitIntersection {
automatic: _,
child,
} => match self.evaluate_node_with_reference(child, cell) {
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
Some(cell_reference) => self.evaluate_cell(cell_reference),
None => CalcResult::new_error(
Error::VALUE,
cell,
format!("Error with Implicit Intersection in cell {cell:?}"),
),
}
}
_ => self.evaluate_node_in_context(child, cell),
},
} }
} }
@@ -617,12 +595,15 @@ impl Model {
}; };
} }
CalcResult::Range { left, right } => { CalcResult::Range { left, right } => {
let range = Range { if left.sheet == right.sheet
left: *left, && left.row == right.row
right: *right, && left.column == right.column
};
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
{ {
let intersection_cell = CellReferenceIndex {
sheet: left.sheet,
column: left.column,
row: left.row,
};
let v = self.evaluate_cell(intersection_cell); let v = self.evaluate_cell(intersection_cell);
self.set_cell_value(cell_reference, &v); self.set_cell_value(cell_reference, &v);
} else { } else {
@@ -639,10 +620,32 @@ impl Model {
f, f,
s, s,
o, o,
m: "Invalid reference".to_string(), m: "Implicit Intersection not implemented".to_string(),
ei: Error::VALUE, ei: Error::NIMPL,
}; };
} }
// if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
// {
// let v = self.evaluate_cell(intersection_cell);
// self.set_cell_value(cell_reference, &v);
// } else {
// let o = match self.cell_reference_to_string(&cell_reference) {
// Ok(s) => s,
// Err(_) => "".to_string(),
// };
// *self.workbook.worksheets[sheet as usize]
// .sheet_data
// .get_mut(&row)
// .expect("expected a row")
// .get_mut(&column)
// .expect("expected a column") = Cell::CellFormulaError {
// f,
// s,
// o,
// m: "Invalid reference".to_string(),
// ei: Error::VALUE,
// };
// }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => { CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize] *self.workbook.worksheets[sheet as usize]
@@ -652,6 +655,20 @@ impl Model {
.get_mut(&column) .get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 }; .expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
} }
CalcResult::Array(_) => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaError {
f,
s,
o: "".to_string(),
m: "Arrays not supported yet".to_string(),
ei: Error::NIMPL,
};
}
} }
} }
} }
@@ -680,7 +697,7 @@ impl Model {
worksheet.color = Some(color.to_string()); worksheet.color = Some(color.to_string());
return Ok(()); return Ok(());
} }
Err(format!("Invalid color: {}", color)) Err(format!("Invalid color: {color}"))
} }
/// Changes the visibility of a sheet /// Changes the visibility of a sheet
@@ -865,11 +882,7 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook let defined_names = workbook.get_defined_names_with_scope();
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
// add all tables // add all tables
// let mut tables = Vec::new(); // let mut tables = Vec::new();
// for worksheet in worksheets { // for worksheet in worksheets {
@@ -1014,7 +1027,7 @@ impl Model {
let source_sheet_name = self let source_sheet_name = self
.workbook .workbook
.worksheet(source.sheet) .worksheet(source.sheet)
.map_err(|e| format!("Could not find source worksheet: {}", e))? .map_err(|e| format!("Could not find source worksheet: {e}"))?
.get_name(); .get_name();
if source.sheet != area.sheet { if source.sheet != area.sheet {
return Err("Source and area are in different sheets".to_string()); return Err("Source and area are in different sheets".to_string());
@@ -1028,7 +1041,7 @@ impl Model {
let target_sheet_name = self let target_sheet_name = self
.workbook .workbook
.worksheet(target.sheet) .worksheet(target.sheet)
.map_err(|e| format!("Could not find target worksheet: {}", e))? .map_err(|e| format!("Could not find target worksheet: {e}"))?
.get_name(); .get_name();
if let Some(formula) = value.strip_prefix('=') { if let Some(formula) = value.strip_prefix('=') {
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -1048,7 +1061,7 @@ impl Model {
column_delta: target.column - source.column, column_delta: target.column - source.column,
}, },
); );
Ok(format!("={}", formula_str)) Ok(format!("={formula_str}"))
} else { } else {
Ok(value.to_string()) Ok(value.to_string())
} }
@@ -1525,7 +1538,7 @@ impl Model {
// If the formula fails to parse try adding a parenthesis // If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3) // SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula { if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference); let new_parsed_formula = self.parser.parse(&format!("{formula})"), &cell_reference);
match new_parsed_formula { match new_parsed_formula {
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula, _ => parsed_formula = new_parsed_formula,

View File

@@ -8,7 +8,7 @@ use crate::{
expressions::{ expressions::{
lexer::LexerMode, lexer::LexerMode,
parser::{ parser::{
stringify::{rename_sheet_in_node, to_rc_format}, stringify::{rename_sheet_in_node, to_rc_format, to_string},
Parser, Parser,
}, },
types::CellReferenceRC, types::CellReferenceRC,
@@ -17,7 +17,8 @@ use crate::{
locale::get_locale, locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName}, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{ types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView, DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
WorksheetView,
}, },
utils::ParsedReference, utils::ParsedReference,
}; };
@@ -144,12 +145,7 @@ impl Model {
/// Reparses all formulas and defined names /// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) { pub(crate) fn reset_parsed_structures(&mut self) {
let defined_names = self let defined_names = self.workbook.get_defined_names_with_scope();
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
self.parser self.parser
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names); .set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
self.parsed_formulas = vec![]; self.parsed_formulas = vec![];
@@ -172,11 +168,11 @@ impl Model {
.get_worksheet_names() .get_worksheet_names()
.iter() .iter()
.map(|s| s.to_uppercase()) .map(|s| s.to_uppercase())
.any(|x| x == format!("{}{}", base_name_uppercase, index)) .any(|x| x == format!("{base_name_uppercase}{index}"))
{ {
index += 1; index += 1;
} }
let sheet_name = format!("{}{}", base_name, index); let sheet_name = format!("{base_name}{index}");
// Now we need a sheet_id // Now we need a sheet_id
let sheet_id = self.get_new_sheet_id(); let sheet_id = self.get_new_sheet_id();
let view_ids: Vec<&u32> = self.workbook.views.keys().collect(); let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
@@ -196,7 +192,7 @@ impl Model {
sheet_id: Option<u32>, sheet_id: Option<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(sheet_name) { if !is_valid_sheet_name(sheet_name) {
return Err(format!("Invalid name for a sheet: '{}'", sheet_name)); return Err(format!("Invalid name for a sheet: '{sheet_name}'"));
} }
if self if self
.workbook .workbook
@@ -238,12 +234,12 @@ impl Model {
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) { if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
return self.rename_sheet_by_index(sheet_index, new_name); return self.rename_sheet_by_index(sheet_index, new_name);
} }
Err(format!("Could not find sheet {}", old_name)) Err(format!("Could not find sheet {old_name}"))
} }
/// Renames a sheet and updates all existing references to that sheet. /// Renames a sheet and updates all existing references to that sheet.
/// It can fail if: /// It can fail if:
/// * The original index is too large /// * The original index is out of bounds
/// * The target sheet name already exists /// * The target sheet name already exists
/// * The target sheet name is invalid /// * The target sheet name is invalid
pub fn rename_sheet_by_index( pub fn rename_sheet_by_index(
@@ -252,22 +248,20 @@ impl Model {
new_name: &str, new_name: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(new_name) { if !is_valid_sheet_name(new_name) {
return Err(format!("Invalid name for a sheet: '{}'.", new_name)); return Err(format!("Invalid name for a sheet: '{new_name}'."));
} }
if self.get_sheet_index_by_name(new_name).is_some() { if self.get_sheet_index_by_name(new_name).is_some() {
return Err(format!("Sheet already exists: '{}'.", new_name)); return Err(format!("Sheet already exists: '{new_name}'."));
}
let worksheets = &self.workbook.worksheets;
let sheet_count = worksheets.len() as u32;
if sheet_index >= sheet_count {
return Err("Sheet index out of bounds".to_string());
} }
// Gets the new name and checks that a sheet with that index exists
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
// Parse all formulas with the old name // Parse all formulas with the old name
// All internal formulas are R1C1 // All internal formulas are R1C1
self.parser.set_lexer_mode(LexerMode::R1C1); self.parser.set_lexer_mode(LexerMode::R1C1);
// We use iter because the default would be a mut_iter and we don't need a mutable reference
let worksheets = &mut self.workbook.worksheets; for worksheet in &mut self.workbook.worksheets {
for worksheet in worksheets { // R1C1 formulas are not tied to a cell (but are tied to a cell)
let cell_reference = &CellReferenceRC { let cell_reference = &CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
@@ -281,11 +275,32 @@ impl Model {
} }
worksheet.shared_formulas = formulas; worksheet.shared_formulas = formulas;
} }
// Se the mode back to A1
// Set the mode back to A1
self.parser.set_lexer_mode(LexerMode::A1); self.parser.set_lexer_mode(LexerMode::A1);
// We reparse all the defined names formulas
let mut defined_names = Vec::new();
// Defined names do not have a context, we can use anything
let cell_reference = &CellReferenceRC {
sheet: old_name.clone(),
row: 1,
column: 1,
};
for defined_name in &mut self.workbook.defined_names {
let mut t = self.parser.parse(&defined_name.formula, cell_reference);
rename_sheet_in_node(&mut t, sheet_index, new_name);
let formula = to_string(&t, cell_reference);
defined_names.push(DefinedName {
name: defined_name.name.clone(),
formula,
sheet_id: defined_name.sheet_id,
});
}
self.workbook.defined_names = defined_names;
// Update the name of the worksheet // Update the name of the worksheet
let worksheets = &mut self.workbook.worksheets; self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
worksheets[sheet_index as usize].set_name(new_name);
self.reset_parsed_structures(); self.reset_parsed_structures();
Ok(()) Ok(())
} }
@@ -347,14 +362,14 @@ impl Model {
}; };
let locale = match get_locale(locale_id) { let locale = match get_locale(locale_id) {
Ok(l) => l.clone(), Ok(l) => l.clone(),
Err(_) => return Err(format!("Invalid locale: {}", locale_id)), Err(_) => return Err(format!("Invalid locale: {locale_id}")),
}; };
let milliseconds = get_milliseconds_since_epoch(); let milliseconds = get_milliseconds_since_epoch();
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match DateTime::from_timestamp(seconds, 0) { let dt = match DateTime::from_timestamp(seconds, 0) {
Some(s) => s, Some(s) => s,
None => return Err(format!("Invalid timestamp: {}", milliseconds)), None => return Err(format!("Invalid timestamp: {milliseconds}")),
}; };
// "2020-08-06T21:20:53Z // "2020-08-06T21:20:53Z
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();

View File

@@ -126,7 +126,7 @@ pub fn to_precision_str(value: f64, precision: usize) -> String {
let exponent = value.abs().log10().floor(); let exponent = value.abs().log10().floor();
let base = value / 10.0_f64.powf(exponent); let base = value / 10.0_f64.powf(exponent);
let base = format!("{0:.1$}", base, precision - 1); let base = format!("{0:.1$}", base, precision - 1);
let value = format!("{}e{}", base, exponent).parse::<f64>().unwrap_or({ let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
// TODO: do this in a way that does not require a possible error // TODO: do this in a way that does not require a possible error
0.0 0.0
}); });

View File

@@ -154,7 +154,7 @@ impl Styles {
return Ok(cell_style.xf_id); return Ok(cell_style.xf_id);
} }
} }
Err(format!("Style '{}' not found", style_name)) Err(format!("Style '{style_name}' not found"))
} }
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> { pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {

View File

@@ -28,7 +28,6 @@ mod test_fn_sumifs;
mod test_fn_textbefore; mod test_fn_textbefore;
mod test_fn_textjoin; mod test_fn_textjoin;
mod test_fn_unicode; mod test_fn_unicode;
mod test_forward_references;
mod test_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
mod test_math; mod test_math;
@@ -52,14 +51,20 @@ mod engineering;
mod test_fn_offset; mod test_fn_offset;
mod test_number_format; mod test_number_format;
mod test_arrays;
mod test_escape_quotes; mod test_escape_quotes;
mod test_extend; mod test_extend;
mod test_fn_fv; mod test_fn_fv;
mod test_fn_round;
mod test_fn_type; mod test_fn_type;
mod test_frozen_rows_and_columns; mod test_frozen_rows_and_columns;
mod test_geomean; mod test_geomean;
mod test_get_cell_content; mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_ln;
mod test_log;
mod test_log10;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;
mod test_today; mod test_today;

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN}; use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
use crate::model::Model; use crate::model::Model;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::types::Col; use crate::types::Col;
@@ -508,6 +508,10 @@ fn test_move_column_right() {
assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)"); assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)");
assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)"); assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)");
assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)"); assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)");
// Data moved as well
assert_eq!(model._get_text("G1"), "1");
assert_eq!(model._get_text("H1"), "3");
} }
#[test] #[test]
@@ -532,5 +536,249 @@ fn tets_move_column_error() {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test]
fn test_move_row_down() {
let mut model = new_empty_model();
populate_table(&mut model);
// Formulas referencing rows 3 and 4
model._set("E3", "=G3");
model._set("E4", "=G4");
model._set("E5", "=SUM(G3:J3)");
model._set("E6", "=SUM(G3:G3)");
model._set("E7", "=SUM(G4:G4)");
model.evaluate();
// Move row 3 down by one position
let result = model.move_row_action(0, 3, 1);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=G3");
assert_eq!(model._get_formula("E4"), "=G4");
assert_eq!(model._get_formula("E5"), "=SUM(G4:J4)");
assert_eq!(model._get_formula("E6"), "=SUM(G4:G4)");
assert_eq!(model._get_formula("E7"), "=SUM(G3:G3)");
// Data moved as well
assert_eq!(model._get_text("G4"), "-2");
assert_eq!(model._get_text("G3"), "");
}
#[test]
fn test_move_row_up() {
let mut model = new_empty_model();
populate_table(&mut model);
// Formulas referencing rows 4 and 5
model._set("E4", "=G4");
model._set("E5", "=G5");
model._set("E6", "=SUM(G4:J4)");
model._set("E7", "=SUM(G4:G4)");
model._set("E8", "=SUM(G5:G5)");
model.evaluate();
// Move row 5 up by one position
let result = model.move_row_action(0, 5, -1);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("E4"), "=G4");
assert_eq!(model._get_formula("E5"), "=G5");
assert_eq!(model._get_formula("E6"), "=SUM(G5:J5)");
assert_eq!(model._get_formula("E7"), "=SUM(G5:G5)");
assert_eq!(model._get_formula("E8"), "=SUM(G4:G4)");
// Data moved as well
assert_eq!(model._get_text("G4"), "");
assert_eq!(model._get_text("G5"), "");
}
#[test]
fn test_move_row_error() {
let mut model = new_empty_model();
model.evaluate();
let result = model.move_row_action(0, 7, -10);
assert!(result.is_err());
let result = model.move_row_action(0, -7, 20);
assert!(result.is_err());
let result = model.move_row_action(0, LAST_ROW, 1);
assert!(result.is_err());
let result = model.move_row_action(0, LAST_ROW + 1, -10);
assert!(result.is_err());
// This works
let result = model.move_row_action(0, LAST_ROW, -1);
assert!(result.is_ok());
}
#[test]
fn test_move_row_down_absolute_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
// Absolute references
model._set("E3", "=$G$3");
model._set("E4", "=$G$4");
model._set("E5", "=SUM($G$3:$J$3)");
model._set("E6", "=SUM($G$3:$G$3)");
model._set("E7", "=SUM($G$4:$G$4)");
model.evaluate();
assert!(model.move_row_action(0, 3, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$G$3");
assert_eq!(model._get_formula("E4"), "=$G$4");
assert_eq!(model._get_formula("E5"), "=SUM($G$4:$J$4)");
assert_eq!(model._get_formula("E6"), "=SUM($G$4:$G$4)");
assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$3)");
}
#[test]
fn test_move_column_right_absolute_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
// Absolute references
model._set("E3", "=$G$3");
model._set("E4", "=$H$3");
model._set("E5", "=SUM($G$3:$J$7)");
model._set("E6", "=SUM($G$3:$G$7)");
model._set("E7", "=SUM($H$3:$H$7)");
model.evaluate();
assert!(model.move_column_action(0, 7, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$H$3");
assert_eq!(model._get_formula("E4"), "=$G$3");
assert_eq!(model._get_formula("E5"), "=SUM($H$3:$J$7)");
assert_eq!(model._get_formula("E6"), "=SUM($H$3:$H$7)");
assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$7)");
}
#[test]
fn test_move_row_down_mixed_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
model._set("E3", "=$G3"); // absolute col, relative row
model._set("E4", "=$G4");
model._set("E5", "=SUM($G3:$J3)");
model._set("E6", "=SUM($G3:$G3)");
model._set("E7", "=SUM($G4:$G4)");
model._set("F3", "=H$3"); // relative col, absolute row
model._set("F4", "=G$3");
model.evaluate();
assert!(model.move_row_action(0, 3, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$G3");
assert_eq!(model._get_formula("E4"), "=$G4");
assert_eq!(model._get_formula("E5"), "=SUM($G4:$J4)");
assert_eq!(model._get_formula("E6"), "=SUM($G4:$G4)");
assert_eq!(model._get_formula("E7"), "=SUM($G3:$G3)");
assert_eq!(model._get_formula("F3"), "=G$4");
assert_eq!(model._get_formula("F4"), "=H$4");
}
#[test]
fn test_move_column_right_mixed_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
model._set("E3", "=$G3");
model._set("E4", "=$H3");
model._set("E5", "=SUM($G3:$J7)");
model._set("E6", "=SUM($G3:$G7)");
model._set("E7", "=SUM($H3:$H7)");
model._set("F3", "=H$3");
model._set("F4", "=H$3");
model.evaluate();
assert!(model.move_column_action(0, 7, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$H3");
assert_eq!(model._get_formula("E4"), "=$G3");
assert_eq!(model._get_formula("E5"), "=SUM($H3:$J7)");
assert_eq!(model._get_formula("E6"), "=SUM($H3:$H7)");
assert_eq!(model._get_formula("E7"), "=SUM($G3:$G7)");
assert_eq!(model._get_formula("F3"), "=G$3");
assert_eq!(model._get_formula("F4"), "=G$3");
}
#[test]
fn test_move_row_height() {
let mut model = new_empty_model();
let sheet = 0;
let custom_height = DEFAULT_ROW_HEIGHT * 2.0;
// Set a custom height for row 3
model
.workbook
.worksheet_mut(sheet)
.unwrap()
.set_row_height(3, custom_height)
.unwrap();
// Record the original height of row 4 (should be the default)
let original_row4_height = model.get_row_height(sheet, 4).unwrap();
// Move row 3 down by one position
assert!(model.move_row_action(sheet, 3, 1).is_ok());
// The custom height should now be on row 4
assert_eq!(model.get_row_height(sheet, 4), Ok(custom_height));
// Row 3 should now have the previous height of row 4
assert_eq!(model.get_row_height(sheet, 3), Ok(original_row4_height));
}
/// Moving a row down by two positions should shift formulas on intermediate
/// rows by only one (the row that gets skipped), not by the full delta this
/// guards against the regression fixed in the RowMove displacement logic.
#[test]
fn test_row_move_down_two_updates_intermediate_refs_by_one() {
let mut model = new_empty_model();
populate_table(&mut model);
// Set up formulas to verify intermediate rows shift by 1 (not full delta).
model._set("E3", "=G3"); // target row
model._set("E4", "=G4"); // intermediate row
model._set("E5", "=SUM(G3:J3)");
model.evaluate();
// Move row 3 down by two positions (row 3 -> row 5)
assert!(model.move_row_action(0, 3, 2).is_ok());
model.evaluate();
// Assert that references for the moved row and intermediate row are correct.
assert_eq!(model._get_formula("E3"), "=G3");
assert_eq!(model._get_formula("E5"), "=G5");
assert_eq!(model._get_formula("E4"), "=SUM(G5:J5)");
}
/// Moving a column right by two positions should shift formulas on
/// intermediate columns by only one, ensuring the ColumnMove displacement
/// logic handles multi-position moves correctly.
#[test]
fn test_column_move_right_two_updates_intermediate_refs_by_one() {
let mut model = new_empty_model();
populate_table(&mut model);
// Set up formulas to verify intermediate columns shift by 1 (not full delta).
model._set("E3", "=$G3"); // target column
model._set("E4", "=$H3"); // intermediate column
model._set("E5", "=SUM($G3:$J7)");
model.evaluate();
// Move column G (7) right by two positions (G -> I)
assert!(model.move_column_action(0, 7, 2).is_ok());
model.evaluate();
// Assert that references for moved and intermediate columns are correct.
assert_eq!(model._get_formula("E3"), "=$I3");
assert_eq!(model._get_formula("E4"), "=$G3");
assert_eq!(model._get_formula("E5"), "=SUM($I3:$J7)");
}
// A B C D E F G H I J K L M N O P Q R // A B C D E F G H I J K L M N O P Q R
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

View File

@@ -0,0 +1,13 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn sum_arrays() {
let mut model = new_empty_model();
model._set("A1", "=SUM({1,2,3}+{3,4,5})");
model.evaluate();
assert_eq!(model._get_text("A1"), *"18");
}

View File

@@ -22,13 +22,14 @@ fn fn_concatenate() {
model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#); model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#);
// This will break once we implement the implicit intersection operator // This will break once we implement the implicit intersection operator
// It should be: // It should be:
// model._set("B2", r#"=CONCATENATE(@A1:A3, "!")"#); model._set("C2", r#"=CONCATENATE(@A1:A3, "!")"#);
model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#); model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#);
model._set("B3", r#"=CONCAT(A1:A3, "!")"#); model._set("B3", r#"=CONCAT(A1:A3, "!")"#);
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("B1"), *"Hello my World!"); assert_eq!(model._get_text("B1"), *"Hello my World!");
assert_eq!(model._get_text("B2"), *" my !"); assert_eq!(model._get_text("B2"), *"#N/IMPL!");
assert_eq!(model._get_text("B3"), *"Hello my World!"); assert_eq!(model._get_text("B3"), *"Hello my World!");
assert_eq!(model._get_text("C2"), *" my !");
} }

View File

@@ -30,8 +30,18 @@ fn implicit_intersection() {
model._set("A2", "=FORMULATEXT(D1:E1)"); model._set("A2", "=FORMULATEXT(D1:E1)");
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A1"), *"#N/IMPL!");
assert_eq!(model._get_text("A2"), *"#ERROR!"); assert_eq!(model._get_text("A2"), *"#N/IMPL!");
}
#[test]
fn implicit_intersection_operator() {
let mut model = new_empty_model();
model._set("A1", "=1 + 2");
model._set("B1", "=FORMULATEXT(@A:A)");
model.evaluate();
assert_eq!(model._get_text("B1"), *"#N/IMPL!");
} }
#[test] #[test]

View File

@@ -174,7 +174,7 @@ fn fn_or_xor_no_arguments() {
println!("Testing function: {func}"); println!("Testing function: {func}");
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", &format!("={}()", func)); model._set("A1", &format!("={func}()"));
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A1"), *"#ERROR!");
} }

View File

@@ -0,0 +1,15 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_round_approximation() {
let mut model = new_empty_model();
model._set("A1", "=ROUND(1.05*(0.0284+0.0046)-0.0284,4)");
model._set("A2", "=ROUNDDOWN(1.05*(0.0284+0.0046)-0.0284,5)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0.0063");
assert_eq!(model._get_text("A2"), *"0.00625");
}

View File

@@ -17,3 +17,19 @@ fn test_fn_sum_arguments() {
assert_eq!(model._get_text("A3"), *"1"); assert_eq!(model._get_text("A3"), *"1");
assert_eq!(model._get_text("A4"), *"4"); assert_eq!(model._get_text("A4"), *"4");
} }
#[test]
fn arrays() {
let mut model = new_empty_model();
model._set("A1", "=SUM({1, 2, 3})");
model._set("A2", "=SUM({1; 2; 3})");
model._set("A3", "=SUM({1, 2; 3, 4})");
model._set("A4", "=SUM({1, 2; 3, 4; 5, 6})");
model.evaluate();
assert_eq!(model._get_text("A1"), *"6");
assert_eq!(model._get_text("A2"), *"6");
assert_eq!(model._get_text("A3"), *"10");
assert_eq!(model._get_text("A4"), *"21");
}

View File

@@ -1,121 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::expressions::types::{Area, CellReferenceIndex};
use crate::test::util::new_empty_model;
#[test]
fn test_forward_references() {
let mut model = new_empty_model();
// test single ref changed nd not changed
model._set("H8", "=F6*G9");
// tests areas
model._set("H9", "=SUM(D4:F6)");
// absolute coordinates
model._set("H10", "=$F$6");
// area larger than the source area
model._set("H11", "=SUM(D3:F6)");
// Test arguments and concat
model._set("H12", "=SUM(F6, D4:F6) & D4");
// Test range operator. This is syntax error for now.
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
// Test operations
model._set("H14", "=-D4+D5*F6/F5");
model.evaluate();
// Source Area is D4:F6
let source_area = &Area {
sheet: 0,
row: 4,
column: 4,
width: 3,
height: 3,
};
// We paste in B10
let target_row = 10;
let target_column = 2;
let result = model.forward_references(
source_area,
&CellReferenceIndex {
sheet: 0,
row: target_row,
column: target_column,
},
);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("H8"), "=D12*G9");
assert_eq!(model._get_formula("H9"), "=SUM(B10:D12)");
assert_eq!(model._get_formula("H10"), "=$D$12");
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
assert_eq!(model._get_formula("H12"), "=SUM(D12,B10:D12)&B10");
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
assert_eq!(model._get_formula("H14"), "=-B10+B11*D12/D11");
}
#[test]
fn test_different_sheet() {
let mut model = new_empty_model();
// test single ref changed not changed
model._set("H8", "=F6*G9");
// tests areas
model._set("H9", "=SUM(D4:F6)");
// absolute coordinates
model._set("H10", "=$F$6");
// area larger than the source area
model._set("H11", "=SUM(D3:F6)");
// Test arguments and concat
model._set("H12", "=SUM(F6, D4:F6) & D4");
// Test range operator. This is syntax error for now.
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
// Test operations
model._set("H14", "=-D4+D5*F6/F5");
// Adds a new sheet
assert!(model.add_sheet("Sheet2").is_ok());
model.evaluate();
// Source Area is D4:F6
let source_area = &Area {
sheet: 0,
row: 4,
column: 4,
width: 3,
height: 3,
};
// We paste in Sheet2!B10
let target_row = 10;
let target_column = 2;
let result = model.forward_references(
source_area,
&CellReferenceIndex {
sheet: 1,
row: target_row,
column: target_column,
},
);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("H8"), "=Sheet2!D12*G9");
assert_eq!(model._get_formula("H9"), "=SUM(Sheet2!B10:D12)");
assert_eq!(model._get_formula("H10"), "=Sheet2!$D$12");
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
assert_eq!(
model._get_formula("H12"),
"=SUM(Sheet2!D12,Sheet2!B10:D12)&Sheet2!B10"
);
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
assert_eq!(
model._get_formula("H14"),
"=-Sheet2!B10+Sheet2!B11*Sheet2!D12/Sheet2!D11"
);
}

View File

@@ -0,0 +1,50 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn simple_colum() {
let mut model = new_empty_model();
// We populate cells A1 to A3
model._set("A1", "1");
model._set("A2", "2");
model._set("A3", "3");
model._set("C2", "=@A1:A3");
model.evaluate();
assert_eq!(model._get_text("C2"), "2".to_string());
}
#[test]
fn return_of_array_is_n_impl() {
let mut model = new_empty_model();
// We populate cells A1 to A3
model._set("A1", "1");
model._set("A2", "2");
model._set("A3", "3");
model._set("C2", "=A1:A3");
model._set("D2", "=SUM(SIN(A:A)");
model.evaluate();
assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string());
assert_eq!(model._get_text("D2"), "1.89188842".to_string());
}
#[test]
fn concat() {
let mut model = new_empty_model();
model._set("A1", "=CONCAT(@B1:B3)");
model._set("A2", "=CONCAT(B1:B3)");
model._set("B1", "Hello");
model._set("B2", " ");
model._set("B3", "world!");
model.evaluate();
assert_eq!(model._get_text("A1"), *"Hello");
assert_eq!(model._get_text("A2"), *"Hello world!");
}

17
base/src/test/test_ln.rs Normal file
View File

@@ -0,0 +1,17 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=LN(100)");
model._set("A2", "=LN()");
model._set("A3", "=LN(100, 10)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"4.605170186");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
}

19
base/src/test/test_log.rs Normal file
View File

@@ -0,0 +1,19 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=LOG(100)");
model._set("A2", "=LOG()");
model._set("A3", "=LOG(10000, 10)");
model._set("A4", "=LOG(100, 10, 1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"4");
assert_eq!(model._get_text("A4"), *"#ERROR!");
}

View File

@@ -0,0 +1,35 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=LOG10(100)");
model._set("A2", "=LOG10()");
model._set("A3", "=LOG10(100, 10)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
}
#[test]
fn cell_and_function() {
let mut model = new_empty_model();
model._set("A1", "=LOG10");
model.evaluate();
// This is the cell LOG10
assert_eq!(model._get_text("A1"), *"0");
model._set("LOG10", "1000");
model._set("A2", "=LOG10(LOG10)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"1000");
assert_eq!(model._get_text("A2"), *"3");
}

View File

@@ -62,3 +62,17 @@ fn test_create_named_style() {
let style = model.get_style_for_cell(0, 1, 1).unwrap(); let style = model.get_style_for_cell(0, 1, 1).unwrap();
assert!(style.font.b); assert!(style.font.b);
} }
#[test]
fn empty_models_have_two_fills() {
let model = new_empty_model();
assert_eq!(model.workbook.styles.fills.len(), 2);
assert_eq!(
model.workbook.styles.fills[0].pattern_type,
"none".to_string()
);
assert_eq!(
model.workbook.styles.fills[1].pattern_type,
"gray125".to_string()
);
}

View File

@@ -1,6 +1,7 @@
mod test_add_delete_sheets; mod test_add_delete_sheets;
mod test_autofill_columns; mod test_autofill_columns;
mod test_autofill_rows; mod test_autofill_rows;
mod test_batch_row_column_diff;
mod test_border; mod test_border;
mod test_clear_cells; mod test_clear_cells;
mod test_column_style; mod test_column_style;
@@ -11,6 +12,8 @@ mod test_evaluation;
mod test_general; mod test_general;
mod test_grid_lines; mod test_grid_lines;
mod test_keyboard_navigation; mod test_keyboard_navigation;
mod test_last_empty_cell;
mod test_multi_row_column;
mod test_on_area_selection; mod test_on_area_selection;
mod test_on_expand_selected_range; mod test_on_expand_selected_range;
mod test_on_paste_styles; mod test_on_paste_styles;

View File

@@ -0,0 +1,675 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use bitcode::decode;
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
user_model::history::{Diff, QueueDiffs},
UserModel,
};
fn last_diff_list(model: &mut UserModel) -> Vec<Diff> {
let bytes = model.flush_send_queue();
let queue: Vec<QueueDiffs> = decode(&bytes).unwrap();
// Get the last operation's diff list
queue.last().unwrap().list.clone()
}
#[test]
fn diff_invariant_insert_rows() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
assert!(model.insert_rows(0, 5, 3).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertRows {
sheet: 0,
row: 5,
count: 3
}
));
}
#[test]
fn diff_invariant_insert_columns() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
assert!(model.insert_columns(0, 2, 4).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertColumns {
sheet: 0,
column: 2,
count: 4
}
));
}
#[test]
fn undo_redo_after_batch_delete() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Place values that will shift.
model.set_user_input(0, 20, 1, "A").unwrap();
model.set_user_input(0, 1, 20, "B").unwrap();
// Fill some of the rows we are about to delete for testing
for r in 10..15 {
model.set_user_input(0, r, 1, "tmp").unwrap();
}
// Delete rows 10..14 and columns 5..8
assert!(model.delete_rows(0, 10, 5).is_ok());
assert!(model.delete_columns(0, 5, 4).is_ok());
// Verify shift
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
// Undo
model.undo().unwrap(); // columns
model.undo().unwrap(); // rows
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "A");
assert_eq!(model.get_formatted_cell_value(0, 1, 20).unwrap(), "B");
// Redo
model.redo().unwrap(); // rows
model.redo().unwrap(); // columns
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
}
#[test]
fn diff_order_delete_rows() {
// Verifies that delete diffs are generated with all data preserved
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Populate rows to delete
for r in 5..10 {
model.set_user_input(0, r, 1, &r.to_string()).unwrap();
}
assert!(model.delete_rows(0, 5, 5).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
// Should have one bulk diff with all the row data
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 5);
assert_eq!(*count, 5);
assert_eq!(old_data.len(), 5);
// Verify the data was collected for each row
for (i, row_data) in old_data.iter().enumerate() {
let _expected_value = (5 + i).to_string();
assert!(row_data.data.contains_key(&1));
}
}
_ => panic!("Unexpected diff variant"),
}
}
#[test]
fn batch_operations_with_formulas() {
// Verifies formulas update correctly after batch ops
let base = new_empty_model();
let mut model = UserModel::from_model(base);
model.set_user_input(0, 1, 1, "10").unwrap();
model.set_user_input(0, 5, 1, "=A1*2").unwrap(); // Will become A3 after insert
assert!(model.insert_rows(0, 2, 2).is_ok());
// Formula should now reference A1 (unchanged) but be in row 7
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "20");
assert_eq!(model.get_cell_content(0, 7, 1).unwrap(), "=A1*2");
// Undo and verify formula is back at original position
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "20");
}
#[test]
fn edge_case_single_operation() {
// Single row/column operations should still work correctly
let base = new_empty_model();
let mut model = UserModel::from_model(base);
assert!(model.insert_rows(0, 1, 1).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(model.insert_columns(0, 1, 1).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
}
#[test]
fn delete_empty_rows() {
// Delete multiple empty rows and verify behavior
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set data in rows 1 and 10, leaving rows 5-8 empty
model.set_user_input(0, 1, 1, "Before").unwrap();
model.set_user_input(0, 10, 1, "After").unwrap();
// Delete empty rows 5-8
assert!(model.delete_rows(0, 5, 4).is_ok());
// Verify shift
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
// Verify diffs now use bulk operation
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 5);
assert_eq!(*count, 4);
assert_eq!(old_data.len(), 4);
// All rows should be empty
for row_data in old_data {
assert!(row_data.data.is_empty());
}
}
_ => panic!("Unexpected diff variant"),
}
// Undo/redo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
}
#[test]
fn delete_mixed_empty_and_filled_rows() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Alternating filled and empty rows
model.set_user_input(0, 5, 1, "Row5").unwrap();
model.set_user_input(0, 7, 1, "Row7").unwrap();
model.set_user_input(0, 9, 1, "Row9").unwrap();
model.set_user_input(0, 10, 1, "After").unwrap();
assert!(model.delete_rows(0, 5, 5).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "After");
// Verify mix of empty and filled row diffs
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 5);
assert_eq!(*count, 5);
assert_eq!(old_data.len(), 5);
// Count filled rows (should be 3: rows 5, 7, 9)
let filled_count = old_data
.iter()
.filter(|row_data| !row_data.data.is_empty())
.count();
assert_eq!(filled_count, 3);
}
_ => panic!("Unexpected diff variant"),
}
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "Row7");
assert_eq!(model.get_formatted_cell_value(0, 9, 1).unwrap(), "Row9");
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
}
#[test]
fn bulk_insert_rows_undo_redo() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up initial data
model.set_user_input(0, 1, 1, "A1").unwrap();
model.set_user_input(0, 2, 1, "A2").unwrap();
model.set_user_input(0, 5, 1, "A5").unwrap();
// Insert 3 rows at position 3
assert!(model.insert_rows(0, 3, 3).is_ok());
// Verify data has shifted
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "A2");
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // A5 moved to A8
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertRows {
sheet: 0,
row: 3,
count: 3
}
));
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "A5"); // Back to original position
// Redo
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // Shifted again
}
#[test]
fn bulk_insert_columns_undo_redo() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up initial data
model.set_user_input(0, 1, 1, "A1").unwrap();
model.set_user_input(0, 1, 2, "B1").unwrap();
model.set_user_input(0, 1, 5, "E1").unwrap();
// Insert 3 columns at position 3
assert!(model.insert_columns(0, 3, 3).is_ok());
// Verify data has shifted
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "B1");
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // E1 moved to H1
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertColumns {
sheet: 0,
column: 3,
count: 3
}
));
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1"); // Back to original position
// Redo
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // Shifted again
}
#[test]
fn bulk_delete_rows_round_trip() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data with styles
model.set_user_input(0, 3, 1, "Row3").unwrap();
model.set_user_input(0, 4, 1, "Row4").unwrap();
model.set_user_input(0, 5, 1, "Row5").unwrap();
model.set_user_input(0, 6, 1, "Row6").unwrap();
model.set_user_input(0, 7, 1, "After").unwrap();
// Set some row heights to verify they're preserved
model.set_rows_height(0, 4, 4, 30.0).unwrap();
model.set_rows_height(0, 5, 5, 40.0).unwrap();
// Delete rows 3-6
assert!(model.delete_rows(0, 3, 4).is_ok());
// Verify deletion
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 3);
assert_eq!(*count, 4);
assert_eq!(old_data.len(), 4);
// Verify data was preserved
assert!(old_data[0].data.contains_key(&1)); // Row3
assert!(old_data[1].data.contains_key(&1)); // Row4
assert!(old_data[2].data.contains_key(&1)); // Row5
assert!(old_data[3].data.contains_key(&1)); // Row6
}
_ => panic!("Expected DeleteRows diff"),
}
// Undo - should restore all data and row heights
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "After");
assert_eq!(model.get_row_height(0, 4).unwrap(), 30.0);
assert_eq!(model.get_row_height(0, 5).unwrap(), 40.0);
// Redo - should delete again
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
// Final undo to verify round-trip
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
}
#[test]
fn bulk_delete_columns_round_trip() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data with styles
model.set_user_input(0, 1, 3, "C1").unwrap();
model.set_user_input(0, 1, 4, "D1").unwrap();
model.set_user_input(0, 1, 5, "E1").unwrap();
model.set_user_input(0, 1, 6, "F1").unwrap();
model.set_user_input(0, 1, 7, "After").unwrap();
// Set some column widths to verify they're preserved
model.set_columns_width(0, 4, 4, 100.0).unwrap();
model.set_columns_width(0, 5, 5, 120.0).unwrap();
// Delete columns 3-6
assert!(model.delete_columns(0, 3, 4).is_ok());
// Verify deletion
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteColumns {
sheet,
column,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*column, 3);
assert_eq!(*count, 4);
assert_eq!(old_data.len(), 4);
// Verify data was preserved
assert!(old_data[0].data.contains_key(&1)); // C1
assert!(old_data[1].data.contains_key(&1)); // D1
assert!(old_data[2].data.contains_key(&1)); // E1
assert!(old_data[3].data.contains_key(&1)); // F1
}
_ => panic!("Expected DeleteColumns diff"),
}
// Undo - should restore all data and column widths
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
assert_eq!(model.get_formatted_cell_value(0, 1, 7).unwrap(), "After");
assert_eq!(model.get_column_width(0, 4).unwrap(), 100.0);
assert_eq!(model.get_column_width(0, 5).unwrap(), 120.0);
// Redo - should delete again
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
// Final undo to verify round-trip
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
}
#[test]
fn complex_bulk_operations_sequence() {
// Test a complex sequence of bulk operations
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Initial setup
model.set_user_input(0, 1, 1, "A1").unwrap();
model.set_user_input(0, 2, 2, "B2").unwrap();
model.set_user_input(0, 3, 3, "C3").unwrap();
// Operation 1: Insert 2 rows at position 2
model.insert_rows(0, 2, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2"); // B2 moved down
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3"); // C3 moved down
// Operation 2: Insert 2 columns at position 2
model.insert_columns(0, 2, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2"); // B2 moved right
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3"); // C3 moved right
// Operation 3: Delete the inserted rows
model.delete_rows(0, 2, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
// Undo all operations
model.undo().unwrap(); // Undo delete rows
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3");
model.undo().unwrap(); // Undo insert columns
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3");
model.undo().unwrap(); // Undo insert rows
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 3, 3).unwrap(), "C3");
// Redo all operations
model.redo().unwrap(); // Redo insert rows
model.redo().unwrap(); // Redo insert columns
model.redo().unwrap(); // Redo delete rows
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
}
#[test]
fn bulk_operations_with_formulas_update() {
// Test that formulas update correctly with bulk operations
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data and formulas
model.set_user_input(0, 1, 1, "10").unwrap();
model.set_user_input(0, 5, 1, "20").unwrap();
model.set_user_input(0, 10, 1, "=A1+A5").unwrap(); // Formula referencing A1 and A5
// Insert 3 rows at position 3
model.insert_rows(0, 3, 3).unwrap();
// Formula should update to reference the shifted cells
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "30"); // Formula moved down
assert_eq!(model.get_cell_content(0, 13, 1).unwrap(), "=A1+A8"); // A5 became A8
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "30");
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1+A5");
// Now test column insertion
model.set_user_input(0, 1, 5, "20").unwrap(); // Add value in E1
model.set_user_input(0, 1, 10, "=A1+E1").unwrap();
model.insert_columns(0, 3, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 12).unwrap(), "30"); // Formula moved right
assert_eq!(model.get_cell_content(0, 1, 12).unwrap(), "=A1+G1"); // E1 became G1
}
#[test]
fn bulk_delete_with_styles() {
// Test that cell and row/column styles are preserved
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data with various styles
for r in 5..10 {
model.set_user_input(0, r, 1, &format!("Row{r}")).unwrap();
model.set_rows_height(0, r, r, (r * 10) as f64).unwrap();
}
// Delete and verify style preservation
model.delete_rows(0, 5, 5).unwrap();
// Undo should restore all styles
model.undo().unwrap();
for r in 5..10 {
assert_eq!(
model.get_formatted_cell_value(0, r, 1).unwrap(),
format!("Row{r}")
);
assert_eq!(model.get_row_height(0, r).unwrap(), (r * 10) as f64);
}
}
#[test]
fn bulk_operations_large_count() {
// Test operations with large counts
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Insert a large number of rows
model.set_user_input(0, 1, 1, "Before").unwrap();
model.set_user_input(0, 100, 1, "After").unwrap();
assert!(model.insert_rows(0, 50, 100).is_ok());
// Verify shift
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After"); // Moved by 100
// Check diff
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertRows {
sheet: 0,
row: 50,
count: 100
}
));
// Undo and redo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 100, 1).unwrap(), "After");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After");
}
#[test]
fn bulk_operations_error_cases() {
// Test error conditions
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Negative count should fail
assert!(model.insert_rows(0, 1, -5).is_err());
assert!(model.insert_columns(0, 1, -5).is_err());
assert!(model.delete_rows(0, 1, -5).is_err());
assert!(model.delete_columns(0, 1, -5).is_err());
// Zero count should fail
assert!(model.insert_rows(0, 1, 0).is_err());
assert!(model.insert_columns(0, 1, 0).is_err());
assert!(model.delete_rows(0, 1, 0).is_err());
assert!(model.delete_columns(0, 1, 0).is_err());
// Out of bounds operations should fail
assert!(model.delete_rows(0, LAST_ROW - 5, 10).is_err());
assert!(model.delete_columns(0, LAST_COLUMN - 5, 10).is_err());
}
#[test]
fn bulk_diff_serialization() {
// Test that bulk diffs can be serialized/deserialized correctly
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Create some data
model.set_user_input(0, 1, 1, "Test").unwrap();
model.insert_rows(0, 2, 3).unwrap();
// Flush and get the serialized diffs
let bytes = model.flush_send_queue();
// Create a new model and apply the diffs
let base2 = new_empty_model();
let mut model2 = UserModel::from_model(base2);
assert!(model2.apply_external_diffs(&bytes).is_ok());
// Verify the state matches
assert_eq!(model2.get_formatted_cell_value(0, 1, 1).unwrap(), "Test");
}
#[test]
fn boundary_validation() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Test deleting rows beyond valid range
assert!(model.delete_rows(0, LAST_ROW, 2).is_err());
assert!(model.delete_rows(0, LAST_ROW + 1, 1).is_err());
// Test deleting columns beyond valid range
assert!(model.delete_columns(0, LAST_COLUMN, 2).is_err());
assert!(model.delete_columns(0, LAST_COLUMN + 1, 1).is_err());
// Test valid boundary deletions (should work with our empty row fix)
assert!(model.delete_rows(0, LAST_ROW, 1).is_ok());
assert!(model.delete_columns(0, LAST_COLUMN, 1).is_ok());
}

View File

@@ -50,10 +50,7 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(top_border), Some(top_border),
top_cell_style.border.bottom, top_cell_style.border.bottom,
"(Top). Sheet: {}, row: {}, column: {}", "(Top). Sheet: {sheet}, row: {row}, column: {column}"
sheet,
row,
column
); );
} }
} }
@@ -65,10 +62,7 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(right_border), Some(right_border),
right_cell_style.border.left, right_cell_style.border.left,
"(Right). Sheet: {}, row: {}, column: {}", "(Right). Sheet: {sheet}, row: {row}, column: {column}"
sheet,
row,
column
); );
} }
} }
@@ -80,10 +74,7 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(bottom_border), Some(bottom_border),
bottom_cell_style.border.top, bottom_cell_style.border.top,
"(Bottom). Sheet: {}, row: {}, column: {}", "(Bottom). Sheet: {sheet}, row: {row}, column: {column}"
sheet,
row,
column
); );
} }
} }
@@ -94,10 +85,7 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(left_border), Some(left_border),
left_cell_style.border.right, left_cell_style.border.right,
"(Left). Sheet: {}, row: {}, column: {}", "(Left). Sheet: {sheet}, row: {row}, column: {column}"
sheet,
row,
column
); );
} }
} }

View File

@@ -423,3 +423,30 @@ fn change_scope_to_first_sheet() {
Ok("#NAME?".to_string()) Ok("#NAME?".to_string())
); );
} }
#[test]
fn rename_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.new_sheet().unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
model.rename_sheet(0, "AnotherName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
}

View File

@@ -65,8 +65,8 @@ fn queue_undo_redo_multiple() {
model1.set_user_input(0, row, 17, "=ROW()").unwrap(); model1.set_user_input(0, row, 17, "=ROW()").unwrap();
} }
model1.insert_row(0, 3).unwrap(); model1.insert_rows(0, 3, 1).unwrap();
model1.insert_row(0, 3).unwrap(); model1.insert_rows(0, 3, 1).unwrap();
// undo al of them // undo al of them
while model1.can_undo() { while model1.can_undo() {

View File

@@ -26,7 +26,7 @@ fn set_user_input_errors() {
#[test] #[test]
fn user_model_debug_message() { fn user_model_debug_message() {
let model = UserModel::new_empty("model", "en", "UTC").unwrap(); let model = UserModel::new_empty("model", "en", "UTC").unwrap();
let s = &format!("{:?}", model); let s = &format!("{model:?}");
assert_eq!(s, "UserModel"); assert_eq!(s, "UserModel");
} }
@@ -62,7 +62,7 @@ fn insert_remove_rows() {
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok()); assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
// remove the row // remove the row
assert!(model.delete_row(0, 5).is_ok()); assert!(model.delete_rows(0, 5, 1).is_ok());
// Row 5 has now the normal height // Row 5 has now the normal height
assert_eq!(model.get_row_height(0, 5), Ok(height)); assert_eq!(model.get_row_height(0, 5), Ok(height));
// There is no value in A5 // There is no value in A5
@@ -99,7 +99,7 @@ fn insert_remove_columns() {
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width); assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
// remove the column // remove the column
assert!(model.delete_column(0, 5).is_ok()); assert!(model.delete_columns(0, 5, 1).is_ok());
// Column 5 has now the normal width // Column 5 has now the normal width
assert_eq!(model.get_column_width(0, 5), Ok(column_width)); assert_eq!(model.get_column_width(0, 5), Ok(column_width));
// There is no value in E5 // There is no value in E5

View File

@@ -0,0 +1,55 @@
#![allow(clippy::unwrap_used)]
use crate::constants::LAST_ROW;
use crate::expressions::types::Area;
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_tests() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// This is the first row, column 5
model.set_user_input(0, 3, 5, "todo").unwrap();
// Row 3 before column 5 should be empty
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 4)
.unwrap(),
None
);
// Row 3 before column 5 should be 5
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 7)
.unwrap(),
Some(5)
);
}
#[test]
fn test_last_empty_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
model
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
.unwrap();
// Column 7 has a style but it is empty
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 14)
.unwrap(),
None
);
}

View File

@@ -0,0 +1,173 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
UserModel,
};
#[test]
fn insert_multiple_rows_shifts_cells() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Place a value below the insertion point.
model.set_user_input(0, 10, 1, "42").unwrap();
// Insert 3 rows starting at row 5.
assert!(model.insert_rows(0, 5, 3).is_ok());
// The original value should have moved down by 3 rows.
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
// Undo / redo cycle should restore the same behaviour.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "42");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
}
#[test]
fn insert_rows_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.insert_rows(0, 1, -2),
Err("Cannot add a negative number of cells :)".to_string())
);
assert_eq!(
model.insert_rows(0, 1, 0),
Err("Cannot add a negative number of cells :)".to_string())
);
// Inserting too many rows so that the sheet would overflow.
let too_many = LAST_ROW; // This guarantees max_row + too_many > LAST_ROW.
assert_eq!(
model.insert_rows(0, 1, too_many),
Err(
"Cannot shift cells because that would delete cells at the end of a column".to_string()
)
);
}
#[test]
fn insert_multiple_columns_shifts_cells() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Place a value to the right of the insertion point.
model.set_user_input(0, 1, 10, "99").unwrap();
// Insert 3 columns starting at column 5.
assert!(model.insert_columns(0, 5, 3).is_ok());
// The original value should have moved right by 3 columns.
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
// Undo / redo cycle.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 10).unwrap(), "99");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
}
#[test]
fn insert_columns_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.insert_columns(0, 1, -2),
Err("Cannot add a negative number of cells :)".to_string())
);
assert_eq!(
model.insert_columns(0, 1, 0),
Err("Cannot add a negative number of cells :)".to_string())
);
// Overflow to the right.
let too_many = LAST_COLUMN; // Ensures max_column + too_many > LAST_COLUMN
assert_eq!(
model.insert_columns(0, 1, too_many),
Err("Cannot shift cells because that would delete cells at the end of a row".to_string())
);
}
#[test]
fn delete_multiple_rows_shifts_cells_upwards() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Populate rows 10..14 (to be deleted) so that the diff builder does not fail.
for r in 10..15 {
model.set_user_input(0, r, 1, "del").unwrap();
}
// Place a value below the deletion range.
model.set_user_input(0, 20, 1, "keep").unwrap();
// Delete 5 rows starting at row 10.
assert!(model.delete_rows(0, 10, 5).is_ok());
// The value originally at row 20 should now be at row 15.
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
// Undo / redo cycle.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "keep");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
}
#[test]
fn delete_rows_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.delete_rows(0, 1, -3),
Err("Please use insert rows instead".to_string())
);
assert_eq!(
model.delete_rows(0, 1, 0),
Err("Please use insert rows instead".to_string())
);
}
#[test]
fn delete_multiple_columns_shifts_cells_left() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Place a value to the right of the deletion range.
model.set_user_input(0, 1, 15, "88").unwrap();
// Delete 4 columns starting at column 5.
assert!(model.delete_columns(0, 5, 4).is_ok());
// The value originally at column 15 should now be at column 11.
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
// Undo / redo cycle.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 15).unwrap(), "88");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
}
#[test]
fn delete_columns_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.delete_columns(0, 1, -4),
Err("Please use insert columns instead".to_string())
);
assert_eq!(
model.delete_columns(0, 1, 0),
Err("Please use insert columns instead".to_string())
);
}

View File

@@ -24,6 +24,7 @@ fn csv_paste() {
model.get_formatted_cell_value(0, 7, 7), model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string()) Ok("21".to_string())
); );
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
} }
#[test] #[test]
@@ -45,6 +46,7 @@ fn csv_paste_formula() {
model.get_formatted_cell_value(0, 1, 1), model.get_formatted_cell_value(0, 1, 1),
Ok("2022".to_string()) Ok("2022".to_string())
); );
assert_eq!([1, 1, 1, 1], model.get_selected_view().range);
} }
#[test] #[test]
@@ -69,6 +71,7 @@ fn tsv_crlf_paste() {
model.get_formatted_cell_value(0, 7, 7), model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string()) Ok("21".to_string())
); );
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
} }
#[test] #[test]
@@ -164,7 +167,7 @@ fn copy_paste_internal() {
let copy = model.copy_to_clipboard().unwrap(); let copy = model.copy_to_clipboard().unwrap();
assert_eq!( assert_eq!(
copy.csv, copy.csv,
"42\t127\n\"A season of faith\t \"\"perfection\"\"\"\t\n" "42\t127\n\"A season of faith\t \"\"perfection\"\"\""
); );
assert_eq!(copy.range, (1, 1, 2, 2)); assert_eq!(copy.range, (1, 1, 2, 2));

View File

@@ -14,7 +14,7 @@ fn simple_insert_row() {
for row in 1..5 { for row in 1..5 {
assert!(model.set_user_input(sheet, row, column, "123").is_ok()); assert!(model.set_user_input(sheet, row, column, "123").is_ok());
} }
assert!(model.insert_row(sheet, 3).is_ok()); assert!(model.insert_rows(sheet, 3, 1).is_ok());
assert_eq!( assert_eq!(
model.get_formatted_cell_value(sheet, 3, column).unwrap(), model.get_formatted_cell_value(sheet, 3, column).unwrap(),
"" ""
@@ -40,7 +40,7 @@ fn simple_insert_column() {
for column in 1..5 { for column in 1..5 {
assert!(model.set_user_input(sheet, row, column, "123").is_ok()); assert!(model.set_user_input(sheet, row, column, "123").is_ok());
} }
assert!(model.insert_column(sheet, 3).is_ok()); assert!(model.insert_columns(sheet, 3, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), ""); assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
assert!(model.undo().is_ok()); assert!(model.undo().is_ok());
@@ -62,7 +62,7 @@ fn simple_delete_column() {
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0) .set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
.unwrap(); .unwrap();
model.delete_column(0, 5).unwrap(); model.delete_columns(0, 5, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH)); assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
@@ -92,20 +92,20 @@ fn delete_column_errors() {
let model = new_empty_model(); let model = new_empty_model();
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
assert_eq!( assert_eq!(
model.delete_column(1, 1), model.delete_columns(1, 1, 1),
Err("Invalid sheet index".to_string()) Err("Invalid sheet index".to_string())
); );
assert_eq!( assert_eq!(
model.delete_column(0, 0), model.delete_columns(0, 0, 1),
Err("Column number '0' is not valid.".to_string()) Err("Column number '0' is not valid.".to_string())
); );
assert_eq!( assert_eq!(
model.delete_column(0, LAST_COLUMN + 1), model.delete_columns(0, LAST_COLUMN + 1, 1),
Err("Column number '16385' is not valid.".to_string()) Err(format!("Column number '{}' is not valid.", LAST_COLUMN + 1))
); );
assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(())); assert_eq!(model.delete_columns(0, LAST_COLUMN, 1), Ok(()));
} }
#[test] #[test]
@@ -119,7 +119,7 @@ fn simple_delete_row() {
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0) .set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
.unwrap(); .unwrap();
model.delete_row(0, 15).unwrap(); model.delete_rows(0, 15, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT)); assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
@@ -150,7 +150,7 @@ fn simple_delete_row_no_style() {
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
model.set_user_input(0, 15, 4, "3").unwrap(); model.set_user_input(0, 15, 4, "3").unwrap();
model.set_user_input(0, 15, 6, "=D15*2").unwrap(); model.set_user_input(0, 15, 6, "=D15*2").unwrap();
model.delete_row(0, 15).unwrap(); model.delete_rows(0, 15, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
} }
@@ -170,7 +170,7 @@ fn row_heigh_increases_automatically() {
model model
.set_user_input(0, 1, 1, "My home in Canada had horses\nAnd monkeys!") .set_user_input(0, 1, 1, "My home in Canada had horses\nAnd monkeys!")
.unwrap(); .unwrap();
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT)); assert_eq!(model.get_row_height(0, 1), Ok(40.5));
} }
#[test] #[test]
@@ -180,14 +180,14 @@ fn insert_row_evaluates() {
model.set_user_input(0, 1, 1, "42").unwrap(); model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*2").unwrap(); model.set_user_input(0, 1, 2, "=A1*2").unwrap();
assert!(model.insert_row(0, 1).is_ok()); assert!(model.insert_rows(0, 1, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.undo().unwrap(); model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
model.redo().unwrap(); model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.delete_row(0, 1).unwrap(); model.delete_rows(0, 1, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2"); assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
} }
@@ -199,7 +199,7 @@ fn insert_column_evaluates() {
model.set_user_input(0, 1, 1, "42").unwrap(); model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 10, 1, "=A1*2").unwrap(); model.set_user_input(0, 10, 1, "=A1*2").unwrap();
assert!(model.insert_column(0, 1).is_ok()); assert!(model.insert_columns(0, 1, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.undo().unwrap(); model.undo().unwrap();
@@ -207,7 +207,7 @@ fn insert_column_evaluates() {
model.redo().unwrap(); model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.delete_column(0, 1).unwrap(); model.delete_columns(0, 1, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2"); assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
} }

View File

@@ -55,7 +55,7 @@ fn set_the_range_does_not_set_the_cell() {
assert_eq!( assert_eq!(
model.set_selected_range(5, 4, 10, 6), model.set_selected_range(5, 4, 10, 6),
Err( Err(
"The selected cells is not in one of the corners. Row: '1' and row range '(5, 10)'" "The selected cell is not in one of the corners. Row: '1' and row range '(5, 10)'"
.to_string() .to_string()
) )
); );

View File

@@ -11,11 +11,10 @@ impl UserModel {
r##"{{ r##"{{
"item": {{ "item": {{
"style": "thin", "style": "thin",
"color": "{}" "color": "{color}"
}}, }},
"type": "All" "type": "All"
}}"##, }}"##
color
)) ))
.unwrap(); .unwrap();
let range = &Area { let range = &Area {
@@ -40,11 +39,10 @@ impl UserModel {
r##"{{ r##"{{
"item": {{ "item": {{
"style": "thin", "style": "thin",
"color": "{}" "color": "{color}"
}}, }},
"type": "{}" "type": "{kind}"
}}"##, }}"##
color, kind
)) ))
.unwrap(); .unwrap();
let range = &Area { let range = &Area {

View File

@@ -13,7 +13,7 @@ impl Model {
if cell.contains('!') { if cell.contains('!') {
self.parse_reference(cell).unwrap() self.parse_reference(cell).unwrap()
} else { } else {
self.parse_reference(&format!("Sheet1!{}", cell)).unwrap() self.parse_reference(&format!("Sheet1!{cell}")).unwrap()
} }
} }
pub fn _set(&mut self, cell: &str, value: &str) { pub fn _set(&mut self, cell: &str, value: &str) {

View File

@@ -62,8 +62,8 @@ pub struct DefinedName {
} }
/// * state: /// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types) /// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible /// hidden, veryHidden, visible
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum SheetState { pub enum SheetState {
Visible, Visible,
@@ -303,7 +303,14 @@ impl Default for Styles {
Styles { Styles {
num_fmts: vec![], num_fmts: vec![],
fonts: vec![Default::default()], fonts: vec![Default::default()],
fills: vec![Default::default()], fills: vec![
Default::default(),
Fill {
pattern_type: "gray125".to_string(),
fg_color: None,
bg_color: None,
},
],
borders: vec![Default::default()], borders: vec![Default::default()],
cell_style_xfs: vec![Default::default()], cell_style_xfs: vec![Default::default()],
cell_xfs: vec![Default::default()], cell_xfs: vec![Default::default()],

View File

@@ -299,6 +299,7 @@ impl Model {
Node::WrongVariableKind(_) => None, Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None, Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None, Node::OpPowerKind { .. } => None,
Node::ImplicitIntersection { .. } => None,
} }
} }

View File

@@ -6,15 +6,15 @@ use csv::{ReaderBuilder, WriterBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
constants::{self, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW}, constants::{self, LAST_COLUMN, LAST_ROW},
expressions::{ expressions::{
types::{Area, CellReferenceIndex}, types::{Area, CellReferenceIndex},
utils::{is_valid_column_number, is_valid_row}, utils::{is_valid_column_number, is_valid_row},
}, },
model::Model, model::Model,
types::{ types::{
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState, Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties,
Style, VerticalAlignment, SheetState, Style, VerticalAlignment,
}, },
utils::is_valid_hex_color, utils::is_valid_hex_color,
}; };
@@ -430,10 +430,14 @@ impl UserModel {
new_value: value.to_string(), new_value: value.to_string(),
old_value: Box::new(old_value), old_value: Box::new(old_value),
}]; }];
let style = self.model.get_style_for_cell(sheet, row, column)?;
let line_count = value.split('\n').count(); let line_count = value.split('\n').count() as f64;
let row_height = self.model.get_row_height(sheet, row)?; let row_height = self.model.get_row_height(sheet, row)?;
let cell_height = (line_count as f64) * DEFAULT_ROW_HEIGHT; // This is in sync with the front-end auto fit row
let font_size = style.font.sz as f64;
let line_height = font_size * 1.5;
let cell_height = (line_count - 1.0) * line_height + 8.0 + font_size;
if cell_height > row_height { if cell_height > row_height {
diff_list.push(Diff::SetRowHeight { diff_list.push(Diff::SetRowHeight {
sheet, sheet,
@@ -655,8 +659,12 @@ impl UserModel {
Ok(()) Ok(())
} }
fn clear_column_formatting(&mut self, sheet: u32, column: i32) -> Result<(), String> { fn clear_column_formatting(
let mut diff_list = Vec::new(); &mut self,
sheet: u32,
column: i32,
diff_list: &mut Vec<Diff>,
) -> Result<(), String> {
let old_value = self.model.get_column_style(sheet, column)?; let old_value = self.model.get_column_style(sheet, column)?;
self.model.delete_column_style(sheet, column)?; self.model.delete_column_style(sheet, column)?;
diff_list.push(Diff::DeleteColumnStyle { diff_list.push(Diff::DeleteColumnStyle {
@@ -735,12 +743,15 @@ impl UserModel {
} }
} }
} }
self.push_diff_list(diff_list);
Ok(()) Ok(())
} }
fn clear_row_formatting(&mut self, sheet: u32, row: i32) -> Result<(), String> { fn clear_row_formatting(
let mut diff_list = Vec::new(); &mut self,
sheet: u32,
row: i32,
diff_list: &mut Vec<Diff>,
) -> Result<(), String> {
let old_value = self.model.get_row_style(sheet, row)?; let old_value = self.model.get_row_style(sheet, row)?;
self.model.delete_row_style(sheet, row)?; self.model.delete_row_style(sheet, row)?;
diff_list.push(Diff::DeleteRowStyle { diff_list.push(Diff::DeleteRowStyle {
@@ -787,8 +798,6 @@ impl UserModel {
} }
} }
} }
self.push_diff_list(diff_list);
Ok(()) Ok(())
} }
@@ -799,19 +808,21 @@ impl UserModel {
/// * [UserModel::range_clear_contents] /// * [UserModel::range_clear_contents]
pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> { pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
let sheet = range.sheet; let sheet = range.sheet;
let mut diff_list = Vec::new();
if range.row == 1 && range.height == LAST_ROW { if range.row == 1 && range.height == LAST_ROW {
for column in range.column..range.column + range.width { for column in range.column..range.column + range.width {
self.clear_column_formatting(sheet, column)?; self.clear_column_formatting(sheet, column, &mut diff_list)?;
} }
self.push_diff_list(diff_list);
return Ok(()); return Ok(());
} }
if range.column == 1 && range.width == LAST_COLUMN { if range.column == 1 && range.width == LAST_COLUMN {
for row in range.row..range.row + range.height { for row in range.row..range.row + range.height {
self.clear_row_formatting(sheet, row)?; self.clear_row_formatting(sheet, row, &mut diff_list)?;
} }
self.push_diff_list(diff_list);
return Ok(()); return Ok(());
} }
let mut diff_list = Vec::new();
for row in range.row..range.row + range.height { for row in range.row..range.row + range.height {
for column in range.column..range.column + range.width { for column in range.column..range.column + range.width {
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? { if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
@@ -847,105 +858,184 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Inserts a row /// Inserts `row_count` blank rows starting at `row` (both 0-based).
/// ///
/// See also: /// Parameters
/// * [Model::insert_rows] /// * `sheet` worksheet index.
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> { /// * `row` first row to insert.
let diff_list = vec![Diff::InsertRow { sheet, row }]; /// * `row_count` number of rows (> 0).
///
/// History: the method pushes `row_count` [`crate::user_model::history::Diff::InsertRow`]
/// items **all using the same `row` index**. Replaying those diffs (undo / redo)
/// is therefore immune to the row-shifts that happen after each individual
/// insertion.
///
/// See also [`Model::insert_rows`].
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
self.model.insert_rows(sheet, row, row_count)?;
let diff_list = vec![Diff::InsertRows {
sheet,
row,
count: row_count,
}];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.model.insert_rows(sheet, row, 1)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
/// Deletes a row /// Inserts `column_count` blank columns starting at `column` (0-based).
/// ///
/// See also: /// Parameters
/// * [Model::delete_rows] /// * `sheet` worksheet index.
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), String> { /// * `column` first column to insert.
let mut row_data = None; /// * `column_count` number of columns (> 0).
///
/// History: pushes one [`crate::user_model::history::Diff::InsertColumn`]
/// per inserted column, all with the same `column` value, preventing index
/// drift when the diffs are reapplied.
///
/// See also [`Model::insert_columns`].
pub fn insert_columns(
&mut self,
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), String> {
self.model.insert_columns(sheet, column, column_count)?;
let diff_list = vec![Diff::InsertColumns {
sheet,
column,
count: column_count,
}];
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Deletes `row_count` rows starting at `row`.
///
/// History: a [`crate::user_model::history::Diff::DeleteRow`] is created for
/// each row, ordered **bottom → top**. Undo therefore recreates rows from
/// top → bottom and redo removes them bottom → top, avoiding index drift.
///
/// See also [`Model::delete_rows`].
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
let worksheet = self.model.workbook.worksheet(sheet)?; let worksheet = self.model.workbook.worksheet(sheet)?;
for rd in &worksheet.rows { let mut old_data = Vec::new();
if rd.r == row { // Collect data for all rows to be deleted
row_data = Some(rd.clone()); for r in row..row + row_count {
break; let mut row_data = None;
for rd in &worksheet.rows {
if rd.r == r {
row_data = Some(rd.clone());
break;
}
} }
let data = match worksheet.sheet_data.get(&r) {
Some(s) => s.clone(),
None => HashMap::new(),
};
old_data.push(RowData {
row: row_data,
data,
});
} }
let data = match worksheet.sheet_data.get(&row) {
Some(s) => s.clone(), self.model.delete_rows(sheet, row, row_count)?;
None => return Err(format!("Row number '{row}' is not valid.")),
}; let diff_list = vec![Diff::DeleteRows {
let old_data = Box::new(RowData {
row: row_data,
data,
});
let diff_list = vec![Diff::DeleteRow {
sheet, sheet,
row, row,
count: row_count,
old_data, old_data,
}]; }];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.model.delete_rows(sheet, row, 1)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
/// Inserts a column /// Deletes `column_count` columns starting at `column`.
/// ///
/// See also: /// History: pushes one [`crate::user_model::history::Diff::DeleteColumn`]
/// * [Model::insert_columns] /// per column, **right → left**, so replaying the list is always safe with
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> { /// respect to index shifts.
let diff_list = vec![Diff::InsertColumn { sheet, column }];
self.push_diff_list(diff_list);
self.model.insert_columns(sheet, column, 1)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Deletes a column
/// ///
/// See also: /// See also [`Model::delete_columns`].
/// * [Model::delete_columns] pub fn delete_columns(
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), String> { &mut self,
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), String> {
let worksheet = self.model.workbook.worksheet(sheet)?; let worksheet = self.model.workbook.worksheet(sheet)?;
if !is_valid_column_number(column) { let mut old_data = Vec::new();
return Err(format!("Column number '{column}' is not valid.")); // Collect data for all columns to be deleted
} for c in column..column + column_count {
let mut column_data = None;
let mut column_data = None; for col in &worksheet.cols {
for col in &worksheet.cols { if c >= col.min && c <= col.max {
let min = col.min; column_data = Some(Col {
let max = col.max; min: c,
if column >= min && column <= max { max: c,
column_data = Some(Col { width: col.width,
min: column, custom_width: col.custom_width,
max: column, style: col.style,
width: col.width, });
custom_width: col.custom_width, break;
style: col.style, }
});
break;
} }
}
let mut data = HashMap::new(); let mut data = HashMap::new();
for (row, row_data) in &worksheet.sheet_data { for (row_idx, row_data) in &worksheet.sheet_data {
if let Some(cell) = row_data.get(&column) { if let Some(cell) = row_data.get(&c) {
data.insert(*row, cell.clone()); data.insert(*row_idx, cell.clone());
}
} }
}
let diff_list = vec![Diff::DeleteColumn { old_data.push(ColumnData {
sheet,
column,
old_data: Box::new(ColumnData {
column: column_data, column: column_data,
data, data,
}), });
}
self.model.delete_columns(sheet, column, column_count)?;
let diff_list = vec![Diff::DeleteColumns {
sheet,
column,
count: column_count,
old_data,
}]; }];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.model.delete_columns(sheet, column, 1)?; self.evaluate_if_not_paused();
Ok(())
}
/// Moves a column horizontally and adjusts formulas
pub fn move_column_action(
&mut self,
sheet: u32,
column: i32,
delta: i32,
) -> Result<(), String> {
let diff_list = vec![Diff::MoveColumn {
sheet,
column,
delta,
}];
self.push_diff_list(diff_list);
self.model.move_column_action(sheet, column, delta)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Moves a row vertically and adjusts formulas
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
let diff_list = vec![Diff::MoveRow { sheet, row, delta }];
self.push_diff_list(diff_list);
self.model.move_row_action(sheet, row, delta)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -1483,10 +1573,10 @@ impl UserModel {
return Err(format!("Invalid row: '{first_row}'")); return Err(format!("Invalid row: '{first_row}'"));
} }
if !is_valid_column_number(last_column) { if !is_valid_column_number(last_column) {
return Err(format!("Invalid column: '{}'", last_column)); return Err(format!("Invalid column: '{last_column}'"));
} }
if !is_valid_row(last_row) { if !is_valid_row(last_row) {
return Err(format!("Invalid row: '{}'", last_row)); return Err(format!("Invalid row: '{last_row}'"));
} }
if !is_valid_row(to_column) { if !is_valid_row(to_column) {
@@ -1591,6 +1681,66 @@ impl UserModel {
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines) Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
} }
/// Returns the largest column in the row less than a column whose cell has a non empty value.
/// If there are none it returns `None`.
/// This is useful when rendering a part of a worksheet to know which cells spill over
pub fn get_last_non_empty_in_row_before_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
let data = worksheet.sheet_data.get(&row);
if let Some(row_data) = data {
let mut last_column = None;
let mut columns: Vec<i32> = row_data.keys().copied().collect();
columns.sort_unstable();
for col in columns {
if col < column {
if let Some(cell) = worksheet.cell(row, col) {
if matches!(cell, Cell::EmptyCell { .. }) {
continue;
}
}
last_column = Some(col);
}
}
Ok(last_column)
} else {
Ok(None)
}
}
/// Returns the smallest column in the row larger than "column" whose cell has a non empty value.
/// If there are none it returns `None`.
/// This is useful when rendering a part of a worksheet to know which cells spill over
pub fn get_first_non_empty_in_row_after_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
let data = worksheet.sheet_data.get(&row);
if let Some(row_data) = data {
let mut columns: Vec<i32> = row_data.keys().copied().collect();
// We sort the keys to ensure we are going from left to right
columns.sort_unstable();
for col in columns {
if col > column {
if let Some(cell) = worksheet.cell(row, col) {
if matches!(cell, Cell::EmptyCell { .. }) {
continue;
}
}
return Ok(Some(col));
}
}
}
Ok(None)
}
/// Returns a copy of the selected area /// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> { pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view(); let selected_area = self.get_selected_view();
@@ -1619,18 +1769,18 @@ impl UserModel {
text_row.push(text); text_row.push(text);
} }
wtr.write_record(text_row) wtr.write_record(text_row)
.map_err(|e| format!("Error while processing csv: {}", e))?; .map_err(|e| format!("Error while processing csv: {e}"))?;
data.insert(row, data_row); data.insert(row, data_row);
} }
let csv = String::from_utf8( let csv = String::from_utf8(
wtr.into_inner() wtr.into_inner()
.map_err(|e| format!("Processing error: '{}'", e))?, .map_err(|e| format!("Processing error: '{e}'"))?,
) )
.map_err(|e| format!("Error converting from utf8: '{}'", e))?; .map_err(|e| format!("Error converting from utf8: '{e}'"))?;
Ok(Clipboard { Ok(Clipboard {
csv, csv: csv.trim().to_string(),
data, data,
sheet, sheet,
range: (row_start, column_start, row_end, column_end), range: (row_start, column_start, row_end, column_end),
@@ -1798,7 +1948,7 @@ impl UserModel {
} }
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
// select the pasted area // select the pasted area
self.set_selected_range(area.row, area.column, row, column)?; self.set_selected_range(area.row, area.column, row - 1, column - 1)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -1963,45 +2113,56 @@ impl UserModel {
self.model.cell_clear_all(*sheet, *row, *column)?; self.model.cell_clear_all(*sheet, *row, *column)?;
} }
} }
Diff::InsertRow { sheet, row } => { Diff::InsertRows { sheet, row, count } => {
self.model.delete_rows(*sheet, *row, 1)?; self.model.delete_rows(*sheet, *row, *count)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::DeleteRow { Diff::DeleteRows {
sheet, sheet,
row, row,
count: _,
old_data, old_data,
} => { } => {
needs_evaluation = true; needs_evaluation = true;
self.model.insert_rows(*sheet, *row, 1)?; self.model
.insert_rows(*sheet, *row, old_data.len() as i32)?;
let worksheet = self.model.workbook.worksheet_mut(*sheet)?; let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
if let Some(row_data) = old_data.row.clone() { for (i, row_data) in old_data.iter().enumerate() {
worksheet.rows.push(row_data); let r = *row + i as i32;
if let Some(row_style) = row_data.row.clone() {
worksheet.rows.push(row_style);
}
worksheet.sheet_data.insert(r, row_data.data.clone());
} }
worksheet.sheet_data.insert(*row, old_data.data.clone());
} }
Diff::InsertColumn { sheet, column } => { Diff::InsertColumns {
self.model.delete_columns(*sheet, *column, 1)?;
needs_evaluation = true;
}
Diff::DeleteColumn {
sheet, sheet,
column, column,
count,
} => {
self.model.delete_columns(*sheet, *column, *count)?;
needs_evaluation = true;
}
Diff::DeleteColumns {
sheet,
column,
count: _,
old_data, old_data,
} => { } => {
needs_evaluation = true; needs_evaluation = true;
// inserts an empty column self.model
self.model.insert_columns(*sheet, *column, 1)?; .insert_columns(*sheet, *column, old_data.len() as i32)?;
// puts all the data back
let worksheet = self.model.workbook.worksheet_mut(*sheet)?; let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
for (row, cell) in &old_data.data { for (i, col_data) in old_data.iter().enumerate() {
worksheet.update_cell(*row, *column, cell.clone())?; let c = *column + i as i32;
} for (row, cell) in &col_data.data {
// makes sure that the width and style is correct worksheet.update_cell(*row, c, cell.clone())?;
if let Some(col) = &old_data.column { }
let width = col.width * constants::COLUMN_WIDTH_FACTOR; if let Some(col) = &col_data.column {
let style = col.style; let width = col.width * constants::COLUMN_WIDTH_FACTOR;
worksheet.set_column_width_and_style(*column, width, style)?; let style = col.style;
worksheet.set_column_width_and_style(c, width, style)?;
}
} }
} }
Diff::SetFrozenRowsCount { Diff::SetFrozenRowsCount {
@@ -2159,6 +2320,21 @@ impl UserModel {
self.model.delete_row_style(*sheet, *row)?; self.model.delete_row_style(*sheet, *row)?;
} }
} }
Diff::MoveColumn {
sheet,
column,
delta,
} => {
// For undo, we apply the opposite move
self.model
.move_column_action(*sheet, *column + *delta, -*delta)?;
needs_evaluation = true;
}
Diff::MoveRow { sheet, row, delta } => {
// For undo, we apply the opposite move
self.model.move_row_action(*sheet, *row + *delta, -*delta)?;
needs_evaluation = true;
}
} }
} }
if needs_evaluation { if needs_evaluation {
@@ -2227,28 +2403,34 @@ impl UserModel {
} => self } => self
.model .model
.set_cell_style(*sheet, *row, *column, new_value)?, .set_cell_style(*sheet, *row, *column, new_value)?,
Diff::InsertRow { sheet, row } => { Diff::InsertRows { sheet, row, count } => {
self.model.insert_rows(*sheet, *row, 1)?; self.model.insert_rows(*sheet, *row, *count)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::DeleteRow { Diff::DeleteRows {
sheet, sheet,
row, row,
count,
old_data: _, old_data: _,
} => { } => {
self.model.delete_rows(*sheet, *row, 1)?; self.model.delete_rows(*sheet, *row, *count)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::InsertColumn { sheet, column } => { Diff::InsertColumns {
needs_evaluation = true;
self.model.insert_columns(*sheet, *column, 1)?;
}
Diff::DeleteColumn {
sheet, sheet,
column, column,
count,
} => {
self.model.insert_columns(*sheet, *column, *count)?;
needs_evaluation = true;
}
Diff::DeleteColumns {
sheet,
column,
count,
old_data: _, old_data: _,
} => { } => {
self.model.delete_columns(*sheet, *column, 1)?; self.model.delete_columns(*sheet, *column, *count)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::SetFrozenRowsCount { Diff::SetFrozenRowsCount {
@@ -2360,6 +2542,18 @@ impl UserModel {
} => { } => {
self.model.delete_row_style(*sheet, *row)?; self.model.delete_row_style(*sheet, *row)?;
} }
Diff::MoveColumn {
sheet,
column,
delta,
} => {
self.model.move_column_action(*sheet, *column, *delta)?;
needs_evaluation = true;
}
Diff::MoveRow { sheet, row, delta } => {
self.model.move_row_action(*sheet, *row, *delta)?;
needs_evaluation = true;
}
} }
} }
@@ -2387,7 +2581,7 @@ mod tests {
VerticalAlignment::Top, VerticalAlignment::Top,
]; ];
for a in all { for a in all {
assert_eq!(vertical(&format!("{}", a)), Ok(a)); assert_eq!(vertical(&format!("{a}")), Ok(a));
} }
} }
@@ -2404,7 +2598,7 @@ mod tests {
HorizontalAlignment::Right, HorizontalAlignment::Right,
]; ];
for a in all { for a in all {
assert_eq!(horizontal(&format!("{}", a)), Ok(a)); assert_eq!(horizontal(&format!("{a}")), Ok(a));
} }
} }
} }

View File

@@ -87,23 +87,27 @@ pub(crate) enum Diff {
row: i32, row: i32,
old_value: Box<Option<Style>>, old_value: Box<Option<Style>>,
}, },
InsertRow { InsertRows {
sheet: u32, sheet: u32,
row: i32, row: i32,
count: i32,
}, },
DeleteRow { DeleteRows {
sheet: u32, sheet: u32,
row: i32, row: i32,
old_data: Box<RowData>, count: i32,
old_data: Vec<RowData>,
}, },
InsertColumn { InsertColumns {
sheet: u32, sheet: u32,
column: i32, column: i32,
count: i32,
}, },
DeleteColumn { DeleteColumns {
sheet: u32, sheet: u32,
column: i32, column: i32,
old_data: Box<ColumnData>, count: i32,
old_data: Vec<ColumnData>,
}, },
DeleteSheet { DeleteSheet {
sheet: u32, sheet: u32,
@@ -161,6 +165,16 @@ pub(crate) enum Diff {
new_scope: Option<u32>, new_scope: Option<u32>,
new_formula: String, new_formula: String,
}, },
MoveColumn {
sheet: u32,
column: i32,
delta: i32,
},
MoveRow {
sheet: u32,
row: i32,
delta: i32,
},
// FIXME: we are missing SetViewDiffs // FIXME: we are missing SetViewDiffs
} }

View File

@@ -3,7 +3,7 @@
mod border; mod border;
mod border_utils; mod border_utils;
mod common; mod common;
mod history; pub(crate) mod history;
mod ui; mod ui;
pub use common::UserModel; pub use common::UserModel;

View File

@@ -2,7 +2,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::expressions::utils::{is_valid_column_number, is_valid_row}; use crate::{
constants::{LAST_COLUMN, LAST_ROW},
expressions::utils::{is_valid_column_number, is_valid_row},
worksheet::NavigationDirection,
};
use super::common::UserModel; use super::common::UserModel;
@@ -76,7 +80,7 @@ impl UserModel {
/// Sets the the selected sheet /// Sets the the selected sheet
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> { pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Some(view) = self.model.workbook.views.get_mut(&0) { if let Some(view) = self.model.workbook.views.get_mut(&0) {
view.sheet = sheet; view.sheet = sheet;
@@ -98,7 +102,7 @@ impl UserModel {
return Err(format!("Invalid row: '{row}'")); return Err(format!("Invalid row: '{row}'"));
} }
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {
@@ -110,7 +114,7 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Sets the selected range. Note that the selected cell must be in one of the corners. /// Sets the selected range. Note that the selected cell must be in the selected range.
pub fn set_selected_range( pub fn set_selected_range(
&mut self, &mut self,
start_row: i32, start_row: i32,
@@ -138,24 +142,38 @@ impl UserModel {
return Err(format!("Invalid row: '{end_row}'")); return Err(format!("Invalid row: '{end_row}'"));
} }
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {
let selected_row = view.row; let selected_row = view.row;
let selected_column = view.column; let selected_column = view.column;
// The selected cells must be on one of the corners of the selected range: if start_row == 1 && end_row == LAST_ROW {
if selected_row != start_row && selected_row != end_row { // full row selected. The cell must be at the top or the bottom of the range
return Err(format!( if selected_column != start_column && selected_column != end_column {
"The selected cells is not in one of the corners. Row: '{}' and row range '({}, {})'", return Err(format!(
selected_row, start_row, end_row "The selected cell is not the column edge. Column '{selected_column}' and column range '({start_column}, {end_column})'"
));
}
} else if start_column == 1 && end_column == LAST_COLUMN {
// full column selected. The cell must be at the left or the right of the range
if selected_row != start_row && selected_row != end_row {
return Err(format!(
"The selected cell is not in the row edge. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
));
}
} else {
// The selected cell must be on one of the corners of the selected range:
if selected_row != start_row && selected_row != end_row {
return Err(format!(
"The selected cell is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
)); ));
} }
if selected_column != start_column && selected_column != end_column { if selected_column != start_column && selected_column != end_column {
return Err(format!( return Err(format!(
"The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'", "The selected cell is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
selected_column, start_column, end_column
)); ));
}
} }
view.range = [start_row, start_column, end_row, end_column]; view.range = [start_row, start_column, end_row, end_column];
} }
@@ -192,6 +210,17 @@ impl UserModel {
return Ok(()); return Ok(());
}; };
let [row_start, column_start, row_end, column_end] = range; let [row_start, column_start, row_end, column_end] = range;
if ["ArrowUp", "ArrowDown"].contains(&key) && row_start == 1 && row_end == LAST_ROW {
// full column selected, nothing to do
return Ok(());
}
if ["ArrowRight", "ArrowLeft"].contains(&key)
&& column_start == 1
&& column_end == LAST_COLUMN
{
// full row selected, nothing to do
return Ok(());
}
match key { match key {
"ArrowRight" => { "ArrowRight" => {
@@ -307,7 +336,7 @@ impl UserModel {
return Err(format!("Invalid row: '{top_row}'")); return Err(format!("Invalid row: '{top_row}'"));
} }
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {
@@ -474,7 +503,7 @@ impl UserModel {
// if the row is not fully visible we 'scroll' down until it is // if the row is not fully visible we 'scroll' down until it is
let mut height = 0.0; let mut height = 0.0;
let mut row = view.top_row; let mut row = view.top_row;
while row <= new_row + 1 { while row <= new_row + 1 && row <= LAST_ROW {
height += self.model.get_row_height(sheet, row)?; height += self.model.get_row_height(sheet, row)?;
row += 1; row += 1;
} }
@@ -684,4 +713,94 @@ impl UserModel {
Ok(()) Ok(())
} }
/// User navigates to the edge in the given direction
pub fn on_navigate_to_edge_in_direction(
&mut self,
direction: NavigationDirection,
) -> Result<(), String> {
let (sheet, window_height, window_width) =
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
(view.sheet, view.window_height, view.window_width)
} else {
return Err("View not found".to_string());
};
let worksheet = match self.model.workbook.worksheet(sheet) {
Ok(s) => s,
Err(_) => return Err("Worksheet not found".to_string()),
};
let view = match worksheet.views.get(&self.model.view_id) {
Some(s) => s,
None => return Err("View not found".to_string()),
};
let row = view.row;
let column = view.column;
if !is_valid_row(row) || !is_valid_column_number(column) {
return Err("Invalid row or column".to_string());
}
let (new_row, new_column) =
worksheet.navigate_to_edge_in_direction(row, column, direction)?;
if !is_valid_row(new_row) || !is_valid_column_number(new_column) {
return Err("Invalid row or column after navigation".to_string());
}
if new_row == row && new_column == column {
return Ok(()); // No change in selection
}
let mut top_row = view.top_row;
let mut left_column = view.left_column;
match direction {
NavigationDirection::Left | NavigationDirection::Right => {
// If the new column is not fully visible we 'scroll' until it is
// We need to check two conditions:
// 1. new_column > view.left_column
// 2. right_column < new_column
if new_column < view.left_column {
left_column = new_column;
} else {
let mut c = new_column;
let mut width = self.model.get_column_width(sheet, c)?;
while c > 1 && width <= window_width as f64 {
c -= 1;
width += self.model.get_column_width(sheet, c)?;
}
if c > view.left_column {
left_column = c;
}
}
}
NavigationDirection::Up | NavigationDirection::Down => {
// If the new row is not fully visible we 'scroll' until it is
// We need to check two conditions:
// 1. new_row > view.top_row
// 2. bottom_row < new_row
if new_row < view.top_row {
top_row = new_row;
} else {
let mut r = new_row;
let mut height = self.model.get_row_height(sheet, r)?;
while r > 1 && height <= window_height as f64 {
r -= 1;
height += self.model.get_row_height(sheet, r)?;
}
if r > view.top_row {
top_row = r;
}
}
}
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
view.row = new_row;
view.column = new_column;
view.range = [new_row, new_column, new_row, new_column];
view.top_row = top_row;
view.left_column = left_column;
}
}
Ok(())
}
} }

View File

@@ -1,6 +1,6 @@
use std::vec::Vec; use std::vec::Vec;
use crate::types::*; use crate::{expressions::parser::DefinedNameS, types::*};
impl Workbook { impl Workbook {
pub fn get_worksheet_names(&self) -> Vec<String> { pub fn get_worksheet_names(&self) -> Vec<String> {
@@ -29,7 +29,7 @@ impl Workbook {
} }
/// Returns the a list of defined names in the workbook with their scope /// Returns the a list of defined names in the workbook with their scope
pub fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> { pub fn get_defined_names_with_scope(&self) -> Vec<DefinedNameS> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect(); let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self let defined_names = self

View File

@@ -1,7 +1,7 @@
[package] [package]
edition = "2021" edition = "2021"
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.3.1" version = "0.5.0"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] } napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] }
napi-derive = "2.12.2" napi-derive = "2.12.2"
ironcalc = { path = "../../xlsx", version = "0.3.0" } ironcalc = { path = "../../xlsx", version = "0.5.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[build-dependencies] [build-dependencies]

View File

@@ -4,14 +4,19 @@
Example usage: Example usage:
```javascript ```javascript
import { Model } from '@ironcalc/wasm'; import { Model } from '@ironcalc/nodejs';
const model = new Model("Workbook1", "en", "UTC"); const model = new Model("Workbook1", "en", "UTC");
model.setUserInput(0, 1, 1, "=1+1"); model.setUserInput(0, 1, 1, "=1+1");
const result1 = model.getFormattedCellValue(0, 1, 1);
console.log('Cell value', result1); const result1 = model.getFormattedCellValue(0, 1, 1);
console.log('Cell value', result1); // "#ERROR"
model.evaluate();
const resultAfterEvaluate = model.getFormattedCellValue(0, 1, 1);
console.log('Cell value', resultAfterEvaluate); // 2
let result2 = model.getCellStyle(0, 1, 1); let result2 = model.getCellStyle(0, 1, 1);
console.log('Cell style', result2); console.log('Cell style', result2);

View File

@@ -59,10 +59,10 @@ export declare class UserModel {
setSheetColor(sheet: number, color: string): void setSheetColor(sheet: number, color: string): void
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
insertRow(sheet: number, row: number): void insertRows(sheet: number, row: number, rowCount: number): void
insertColumn(sheet: number, column: number): void insertColumns(sheet: number, column: number, columnCount: number): void
deleteRow(sheet: number, row: number): void deleteRows(sheet: number, row: number, rowCount: number): void
deleteColumn(sheet: number, column: number): void deleteColumns(sheet: number, column: number, columnCount: number): void
setRowHeight(sheet: number, row: number, height: number): void setRowHeight(sheet: number, row: number, height: number): void
setColumnWidth(sheet: number, column: number, width: number): void setColumnWidth(sheet: number, column: number, width: number): void
getRowHeight(sheet: number, row: number): number getRowHeight(sheet: number, row: number): number

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs", "name": "@ironcalc/nodejs",
"version": "0.3.1", "version": "0.5.1",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"napi": { "napi": {

View File

@@ -340,4 +340,20 @@ impl Model {
.delete_defined_name(&name, scope) .delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[napi(js_name = "moveColumn")]
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
self
.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[napi(js_name = "moveRow")]
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
self
.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
}
} }

View File

@@ -183,24 +183,36 @@ impl UserModel {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[napi(js_name = "insertRow")] #[napi(js_name = "insertRows")]
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> { pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
self.model.insert_row(sheet, row).map_err(to_js_error) self
.model
.insert_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[napi(js_name = "insertColumn")] #[napi(js_name = "insertColumns")]
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> { pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
self.model.insert_column(sheet, column).map_err(to_js_error) self
.model
.insert_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[napi(js_name = "deleteRow")] #[napi(js_name = "deleteRows")]
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> { pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
self.model.delete_row(sheet, row).map_err(to_js_error) self
.model
.delete_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[napi(js_name = "deleteColumn")] #[napi(js_name = "deleteColumns")]
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> { pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
self.model.delete_column(sheet, column).map_err(to_js_error) self
.model
.delete_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[napi(js_name = "setRowsHeight")] #[napi(js_name = "setRowsHeight")]
@@ -651,4 +663,20 @@ impl UserModel {
.delete_defined_name(&name, scope) .delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[napi(js_name = "moveColumn")]
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
self
.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[napi(js_name = "moveRow")]
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
self
.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
}
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pyroncalc" name = "pyroncalc"
version = "0.3.0" version = "0.5.7"
edition = "2021" edition = "2021"
@@ -12,8 +12,9 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.3.0" } xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
pyo3 = { version = "0.23", features = ["extension-module"] } pyo3 = { version = "0.25", features = ["extension-module"] }
bitcode = "0.6.3"
[features] [features]

View File

@@ -10,9 +10,6 @@ You can add cell values, retrieve them and most importantly you can evaluate spr
pip install ironcalc pip install ironcalc
``` ```
## Compile and test ## Compile and test
To compile this and test it: To compile this and test it:
@@ -29,3 +26,17 @@ examples $ python example.py
From there if you use `python` you can `import ironcalc`. You can either create a new file, read it from a JSON string or import from Excel. From there if you use `python` you can `import ironcalc`. You can either create a new file, read it from a JSON string or import from Excel.
Hopefully the API is straightforward. Hopefully the API is straightforward.
## Creating documentation
We use sphinx
```
python -m venv venv
source venv/bin/activate
pip install maturin
pip install sphinx
maturin develop
sphinx-build -M html docs html
python -m http.server --directory html/html/
```

9
bindings/python/build_docs.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
python -m venv venv
source venv/bin/activate
pip install patchelf
pip install maturin
pip install sphinx
maturin develop
sphinx-build -M html docs html
python -m http.server --directory html/html/

View File

@@ -8,7 +8,8 @@ IronCalc
installation installation
usage_examples usage_examples
top_level_methods top_level_methods
api_reference raw_api_reference
user_api_reference
objects objects
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets. IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.

View File

@@ -1,6 +1,6 @@
API Reference Raw API Reference
------------- -----------------
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes. In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.

View File

@@ -1,6 +1,13 @@
Top Level Methods Top Level Methods
----------------- -----------------
This module provides a set of top-level methods for creating and loading IronCalc models.
.. autofunction:: ironcalc.create .. autofunction:: ironcalc.create
.. autofunction:: ironcalc.load_from_xlsx .. autofunction:: ironcalc.load_from_xlsx
.. autofunction:: ironcalc.load_from_icalc .. autofunction:: ironcalc.load_from_icalc
.. autofunction:: ironcalc.load_from_bytes
.. autofunction:: ironcalc.create_user_model
.. autofunction:: ironcalc.create_user_model_from_bytes
.. autofunction:: ironcalc.create_user_model_from_xlsx
.. autofunction:: ironcalc.create_user_model_from_icalc

View File

@@ -0,0 +1,41 @@
User API Reference
------------------
This is the "user api". Models here have history, they evaluate automatically with each change and have a "diff" history.
.. method:: save_to_xlsx(file: str)
Saves the user model to file in the XLSX format.
::param file: The file path to save the model to.
.. method:: save_to_icalc(file: str)
Saves the user model to file in the internal binary ic format.
::param file: The file path to save the model to.
.. method:: apply_external_diffs(external_diffs: bytes)
Applies external diffs to the model. This is used to apply changes from other instances of the model.
::param external_diffs: The external diffs to apply, as a byte array.
.. method:: flush_send_queue() -> bytes
Flushes the send queue and returns the bytes to be sent to the client. This is used to send changes to the client.
.. method:: set_user_input(sheet: int, row: int, column: int, value: str)
Sets an input in a cell, as would be done by a user typing into a spreadsheet cell.
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
Returns the cells value as a formatted string, taking into account any number/currency/date formatting.
.. method:: to_bytes() -> bytes
Returns the model as a byte array. This is useful for sending the model over a network or saving it to a file.

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