Compare commits

..

159 Commits

Author SHA1 Message Date
Nicolás Hatcher
9effd8e4b5 FIX: adds create user_model_from_bytes 2025-06-03 11:49:09 +02:00
Nicolás Hatcher
be66af8e16 FIX: Adds to_bytes in the user API 2025-06-03 11:04:12 +02:00
Nicolás Hatcher
8c5fe019b8 FIX: Add source files for alpine 2025-06-02 21:38:48 +02:00
Nicolás Hatcher
7bf36959ca FIX: Python add load/from bytes 2025-06-02 21:27:47 +02:00
Nicolás Hatcher
abaeb3284a FIX: users :) 2025-06-02 21:27:47 +02:00
Nicolás Hatcher
7635cbe1d1 FIX: bump PyO3 version 2025-06-02 21:27:47 +02:00
Nicolás Hatcher
293303b59c UPDATE: Add PyUserModel for the Hackaton 2025-06-02 21:27:47 +02:00
Nicolás Hatcher
2a809e0bd0 UPDATE: bump versions 2025-06-02 21:27:47 +02:00
Nicolás Hatcher
af49d7ad96 FIX: Download to png off by one errors 2025-06-02 21:11:18 +02:00
Nicolás Hatcher
3e015bf13a FIX: control+shitf selects area 2025-06-02 20:59:18 +02:00
Nicolás Hatcher
a5d8ee9ef0 FIX: Download all selected area
We were previously downloading only the bounds of the visible cells,
without taking into account the frozen rows/colums.

Fixes #343
2025-05-17 11:49:42 +02:00
Nicolás Hatcher
c554d929f4 FIX: Renaming a sheet updates formula in defined name 2025-04-16 09:56:17 +02:00
Nicolás Hatcher
acdf85dbc3 FIX: thick line should be 3px in the picker 2025-04-15 15:56:25 +02:00
Nicolás Hatcher
6ac8f7e948 FIX: Use local typescript instead of global 2025-04-15 15:52:07 +02:00
Nicolás Hatcher
9a4e798313 FIX: Make clippy happy. Manual fixes 2025-04-15 15:44:56 +02:00
Nicolás Hatcher
7756ef7f48 FIX: Make clippy happy. automatic update 2025-04-15 15:44:56 +02:00
Daniel
793534b190 fix: cleanup code 2025-03-26 18:13:38 +01:00
Nicolás Hatcher
efc925a046 FIX: Adds border to default swatch 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
155f891f8b FIX: Split the color picker in two 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
5683d02b93 FIX: Clean up (anchorOrigin/transformOrigin) logic 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
475c3e9d49 FIX: Add defaultColor to color picker
NB: Dani will have to pass the color from the theme
2025-03-23 16:02:14 +01:00
Nicolás Hatcher
9e65ea3024 FIX: Add 'title' to ColorPicker 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
03ad87cd8f FIX: Removes unused file 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
e2a466c500 FIX: Simplify ColorPicker code a bit
* Removes "renderMenuItem"
* Color is no longer optional
* Removes 'index' unused variable
2025-03-23 16:02:14 +01:00
Daniel
08b3d71e9e update: new color picker 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
e5ec75495a UPDATE: Introducing Arrays
# This PR introduces:

## Parsing arrays:

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

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

## Evaluating arrays in the SUM function

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

## Evaluating arithmetic operation with arrays

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

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

## Some mathematical functions implement arrays

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

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


# Not done in this PR

## Most functions are not supporting arrays

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

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

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

## Exporting arrays to Excel might not work correctly

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

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

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

## Parsing arrays:

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

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

## Evaluating arrays in the SUM function

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

## Evaluating arithmetic operation with arrays

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

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

## Some mathematical functions implement arrays

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

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


# Not done in this PR

## Most functions are not supporting arrays

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

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

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

## Exporting arrays to Excel might not work correctly

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

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

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

## The array parsing does not respect the locale

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

## The might introduce a small performance penalty

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

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

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

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

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

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

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

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

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

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

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

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

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

Note that the friendly giant implements this only for
compatibility reasons
2024-12-29 19:17:54 +01:00
Nicolás Hatcher
d3af994866 FIX[docs]: Fixes broken links 2024-12-29 13:28:56 +01:00
Nicolás Hatcher
b859af1dc4 FIX: Disable TAN from being tested 2024-12-29 13:28:56 +01:00
Steve Fanning
f9cfdeb35b Add TAN function description 2024-12-29 11:03:07 +01:00
Nicolás Hatcher
669a5eec39 FIX: Removes types.js after used 2024-12-28 14:49:32 +01:00
Steve Fanning
e268dda9e8 Add SIN function description 2024-12-28 12:28:16 +01:00
Nicolás Hatcher
e0205d6c9a FIX: Increments the dum counter in the number of documented functions 2024-12-28 10:55:09 +01:00
Steve Fanning
81ad724348 Add COS function description 2024-12-28 10:46:32 +01:00
Nicolás Hatcher
dc3bf8826b FIX: PV Return #DIV/0! instead of #NUM! if rate = -1 2024-12-27 20:19:32 +01:00
Steve Fanning
38023d3156 Include PV function description 2024-12-27 20:19:32 +01:00
335 changed files with 30367 additions and 8609 deletions

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

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

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

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

3
.gitignore vendored
View File

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

View File

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

151
Cargo.lock generated
View File

@@ -85,6 +85,12 @@ dependencies = [
"syn",
]
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -210,6 +216,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@@ -271,6 +286,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "ctor"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
@@ -389,7 +414,7 @@ dependencies = [
[[package]]
name = "ironcalc"
version = "0.2.0"
version = "0.5.0"
dependencies = [
"bitcode",
"chrono",
@@ -405,7 +430,7 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.2.0"
version = "0.5.0"
dependencies = [
"bitcode",
"chrono",
@@ -421,6 +446,17 @@ dependencies = [
"serde_json",
]
[[package]]
name = "ironcalc_nodejs"
version = "0.5.0"
dependencies = [
"ironcalc",
"napi",
"napi-build",
"napi-derive",
"serde",
]
[[package]]
name = "itertools"
version = "0.12.1"
@@ -460,6 +496,16 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "log"
version = "0.4.21"
@@ -490,6 +536,65 @@ dependencies = [
"adler",
]
[[package]]
name = "napi"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
"napi-derive",
"napi-sys",
"once_cell",
"serde",
"serde_json",
]
[[package]]
name = "napi-build"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
dependencies = [
"libloading",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -616,11 +721,10 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.22.3"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
@@ -634,9 +738,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.22.3"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
dependencies = [
"once_cell",
"target-lexicon",
@@ -644,9 +748,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.22.3"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
dependencies = [
"libc",
"pyo3-build-config",
@@ -654,9 +758,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.22.3"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -666,9 +770,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.22.3"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
dependencies = [
"heck",
"proc-macro2",
@@ -679,8 +783,9 @@ dependencies = [
[[package]]
name = "pyroncalc"
version = "0.1.2"
version = "0.5.6"
dependencies = [
"bitcode",
"ironcalc",
"pyo3",
"serde",
@@ -779,6 +884,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "semver"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]]
name = "serde"
version = "1.0.197"
@@ -868,9 +979,9 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.16"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
@@ -923,6 +1034,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unindent"
version = "0.2.3"
@@ -953,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.1.3"
version = "0.5.3"
dependencies = [
"ironcalc_base",
"serde",

View File

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

View File

@@ -2,7 +2,8 @@
lint:
cargo fmt -- --check
cargo clippy --all-targets --all-features -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings
cd webapp && npm install && npm run check
cd webapp/IronCalc/ && npm install && npm run check
cd webapp/app.ironcalc.com/frontend/ && npm install && npm run check
.PHONY: format
format:
@@ -15,7 +16,7 @@ tests: lint
# Regretabbly we need to build the wasm twice, once for the nodejs tests
# and a second one for the vitest.
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs && make
cd webapp && npm run test
cd webapp/IronCalc/ && npm run test
cd bindings/python && ./run_tests.sh && ./run_examples.sh
.PHONY: remove-artifacts

View File

@@ -77,7 +77,7 @@ And visit <http://0.0.0.0:8000/ironcalc/>
Add the dependency to `Cargo.toml`:
```toml
[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`:

View File

@@ -1,11 +1,11 @@
[package]
name = "ironcalc_base"
version = "0.2.0"
version = "0.5.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
homepage = "https://www.ironcalc.com"
repository = "https://github.com/ironcalc/ironcalc/"
description = "The democratization of spreadsheets"
description = "Open source spreadsheet engine"
license = "MIT OR Apache-2.0"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,5 +1,6 @@
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;
// 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.
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.
///
/// # Arguments
///
/// * `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();
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.
@@ -134,7 +164,34 @@ impl Model {
column,
delta: column_count,
}),
);
)?;
// In the list of columns:
// * Keep all the columns to the left
// * Displace all the columns to the right
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_columns = Vec::new();
for col in worksheet.cols.iter_mut() {
// range under study
let min = col.min;
let max = col.max;
if column > max {
// If the range under study is to our left, this is a noop
} else if column <= min {
// If the range under study is to our right, we displace it
col.min = min + column_count;
col.max = max + column_count;
} else {
// If the range under study is in the middle we augment it
col.max = max + column_count;
}
new_columns.push(col.clone());
}
// TODO: If in a row the cell to the right and left have the same style we should copy it
worksheet.cols = new_columns;
Ok(())
}
@@ -187,7 +244,7 @@ impl Model {
column,
delta: -column_count,
}),
);
)?;
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
// deletes all the column styles
@@ -311,7 +368,7 @@ impl Model {
row,
delta: row_count,
}),
);
)?;
Ok(())
}
@@ -372,7 +429,7 @@ impl Model {
row,
delta: -row_count,
}),
);
)?;
Ok(())
}
@@ -393,14 +450,14 @@ impl Model {
sheet: u32,
column: i32,
delta: i32,
) -> Result<(), &'static str> {
) -> Result<(), String> {
// Check boundaries
let target_column = column + delta;
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) {
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
@@ -412,7 +469,7 @@ impl Model {
column,
delta,
}),
);
)?;
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 crate::expressions::{token::Error, types::CellReferenceIndex};
use crate::expressions::{parser::ArrayNode, token::Error, types::CellReferenceIndex};
#[derive(Clone)]
pub struct Range {
@@ -24,6 +24,7 @@ pub(crate) enum CalcResult {
},
EmptyCell,
EmptyArg,
Array(Vec<Vec<ArrayNode>>),
}
impl CalcResult {

View File

@@ -1,11 +1,85 @@
use crate::{
calc_result::{CalcResult, Range},
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
implicit_intersection::implicit_intersection,
expressions::{
parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
model::Model,
};
pub(crate) enum NumberOrArray {
Number(f64),
Array(Vec<Vec<ArrayNode>>),
}
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(
&mut self,
node: &Node,
@@ -39,19 +113,16 @@ impl Model {
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_number(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid reference (number)".to_string(),
}),
}
}
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
}
}
@@ -99,19 +170,16 @@ impl Model {
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_string(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid reference (string)".to_string(),
}),
}
}
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
}
}
@@ -151,19 +219,16 @@ impl Model {
CalcResult::Boolean(b) => Ok(b),
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_bool(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid reference (bool)".to_string(),
}),
}
}
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
}
}

View File

@@ -16,3 +16,10 @@ pub(crate) const LAST_ROW: i32 = 1_048_576;
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
// We do not support dates before 1899-12-31.
pub(crate) const MINIMUM_DATE_SERIAL_NUMBER: i32 = 1;
// Excel can handle dates until the year 9999-12-31
// 2958465 is the number of days from 1900-01-01 to 9999-12-31
pub(crate) const MAXIMUM_DATE_SERIAL_NUMBER: i32 = 2_958_465;

View File

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

@@ -187,6 +187,7 @@ impl Lexer {
']' => TokenType::RightBracket,
':' => TokenType::Colon,
';' => TokenType::Semicolon,
'@' => TokenType::At,
',' => {
if self.locale.numbers.symbols.decimal == "," {
match self.consume_number(',') {

View File

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

View File

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

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">
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
@@ -12,7 +12,8 @@ term => factor (opFactor factor)*
factor => prod (opProd prod)*
prod => power ('^' power)*
power => (unaryOp)* range '%'*
range => primary (':' primary)?
range => implicit (':' primary)?
implicit=> '@' primary | primary
primary => '(' expr ')'
=> number
=> function '(' f_args ')'
@@ -45,8 +46,8 @@ use super::utils::number_to_column;
use token::OpCompare;
pub mod move_formula;
pub mod static_analysis;
pub mod stringify;
pub mod walk;
#[cfg(test)]
mod tests;
@@ -81,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
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> {
sheet_name: &'a Option<String>,
sheet_index: u32,
@@ -90,6 +94,14 @@ pub(crate) struct Reference<'a> {
column: i32,
}
#[derive(PartialEq, Clone, Debug)]
pub enum ArrayNode {
Boolean(bool),
Number(f64),
String(String),
Error(token::Error),
}
#[derive(PartialEq, Clone, Debug)]
pub enum Node {
BooleanKind(bool),
@@ -163,10 +175,14 @@ pub enum Node {
name: String,
args: Vec<Node>,
},
ArrayKind(Vec<Node>),
DefinedNameKind((String, Option<u32>)),
ArrayKind(Vec<Vec<ArrayNode>>),
DefinedNameKind(DefinedNameS),
TableNameKind(String),
WrongVariableKind(String),
ImplicitIntersection {
automatic: bool,
child: Box<Node>,
},
CompareKind {
kind: OpCompare,
left: Box<Node>,
@@ -189,7 +205,7 @@ pub enum Node {
pub struct Parser {
lexer: lexer::Lexer,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
context: CellReferenceRC,
tables: HashMap<String, Table>,
}
@@ -197,7 +213,7 @@ pub struct Parser {
impl Parser {
pub fn new(
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new(
@@ -228,7 +244,7 @@ impl Parser {
pub fn set_worksheets_and_names(
&mut self,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
) {
self.worksheets = worksheets;
self.defined_names = defined_names;
@@ -252,17 +268,17 @@ impl Parser {
// Returns:
// * 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
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> {
for (df_name, df_scope) in &self.defined_names {
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope);
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() {
return Some(None);
return Some((None, df_formula.to_owned()));
}
}
None
@@ -411,7 +427,7 @@ impl Parser {
}
fn parse_range(&mut self) -> Node {
let t = self.parse_primary();
let t = self.parse_implicit();
if let Node::ParseErrorKind { .. } = t {
return t;
}
@@ -430,6 +446,65 @@ impl Parser {
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 {
let next_token = self.lexer.next_token();
match next_token {
@@ -451,21 +526,35 @@ impl Parser {
TokenType::Number(s) => Node::NumberKind(s),
TokenType::String(s) => Node::StringKind(s),
TokenType::LeftBrace => {
let t = self.parse_expr();
if let Node::ParseErrorKind { .. } = t {
return t;
}
// It's an array. It's a collection of rows all of the same dimension
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 args: Vec<Node> = vec![t];
while next_token == TokenType::Semicolon {
self.lexer.advance_token();
let p = self.parse_expr();
if let Node::ParseErrorKind { .. } = p {
return p;
}
let row = match self.parse_array_row() {
Ok(s) => s,
Err(error) => return error,
};
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) {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
@@ -473,7 +562,7 @@ impl Parser {
message: err.message,
};
}
Node::ArrayKind(args)
Node::ArrayKind(matrix)
}
TokenType::Reference {
sheet,
@@ -604,6 +693,20 @@ impl Parser {
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 };
}
let context = &self.context;
@@ -620,8 +723,8 @@ impl Parser {
};
// Could be a defined name or a table
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope));
if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope, formula));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
@@ -642,7 +745,38 @@ impl Parser {
position: 0,
message: "Unexpected end of input.".to_string(),
},
TokenType::Boolean(value) => Node::BooleanKind(value),
TokenType::Boolean(value) => {
// Could be a function call "TRUE()"
let next_token = self.lexer.peek_token();
if next_token == TokenType::LeftParenthesis {
self.lexer.advance_token();
// We parse all the arguments, although technically this is moot
// But is has the upside of transforming `=TRUE( 4 )` into `=TRUE(4)`
let args = match self.parse_function_args() {
Ok(s) => s,
Err(e) => return e,
};
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: err.position,
message: err.message,
};
}
if value {
return Node::FunctionKind {
kind: Function::True,
args,
};
} else {
return Node::FunctionKind {
kind: Function::False,
args,
};
}
}
Node::BooleanKind(value)
}
TokenType::Compare(_) => {
// A primary Node cannot start with an operator
Node::ParseErrorKind {
@@ -675,6 +809,14 @@ impl Parser {
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::RightBracket
| TokenType::Colon

View File

@@ -1,6 +1,6 @@
use super::{
stringify::{stringify_reference, DisplaceData},
Node, Reference,
ArrayNode, Node, Reference,
};
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
@@ -56,6 +56,15 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
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 {
use self::Node::*;
match node {
@@ -362,20 +371,41 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
move_function(name, args, move_context)
}
ArrayKind(args) => {
// This code is a placeholder. Arrays are not yet implemented
let mut first = true;
let mut arguments = "".to_string();
for el in args {
if !first {
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
let mut first_row = true;
let mut matrix_string = String::new();
// Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
for row in args {
if !first_row {
matrix_string.push(',');
} else {
first = false;
arguments = to_string_moved(el, move_context);
first_row = false;
}
// 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(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!(
@@ -395,5 +425,11 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
position: _,
} => formula.to_string(),
EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
format!("@{}", to_string_moved(child, move_context))
}
}
}

View File

@@ -0,0 +1,984 @@
use crate::functions::Function;
use super::Node;
use once_cell::sync::Lazy;
use regex::Regex;
#[allow(clippy::expect_used)]
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
fn is_range_reference(s: &str) -> bool {
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::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::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,5 +1,7 @@
use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::move_formula::to_string_array_node;
use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::OpUnary;
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
@@ -34,10 +36,21 @@ pub enum DisplaceData {
None,
}
/// This is the internal mode in IronCalc
pub fn to_rc_format(node: &Node) -> String {
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(
node: &Node,
context: &CellReferenceRC,
@@ -46,18 +59,10 @@ pub fn to_string_displaced(
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.
/// 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_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(
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
@@ -235,7 +240,7 @@ fn format_function(
args: &Vec<Node>,
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
use_original_name: bool,
export_to_excel: bool,
) -> String {
let mut first = true;
let mut arguments = "".to_string();
@@ -244,21 +249,46 @@ fn format_function(
arguments = format!(
"{},{}",
arguments,
stringify(el, context, displace_data, use_original_name)
stringify(el, context, displace_data, export_to_excel)
);
} else {
first = false;
arguments = stringify(el, context, displace_data, use_original_name);
arguments = stringify(el, context, displace_data, export_to_excel);
}
}
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(
node: &Node,
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
use_original_name: bool,
export_to_excel: bool,
) -> String {
use self::Node::*;
match node {
@@ -407,52 +437,52 @@ fn stringify(
}
OpRangeKind { left, right } => format!(
"{}:{}",
stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, export_to_excel)
),
OpConcatenateKind { left, right } => format!(
"{}&{}",
stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, export_to_excel)
),
CompareKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, use_original_name),
stringify(left, context, displace_data, export_to_excel),
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),
stringify(left, context, displace_data, export_to_excel),
kind,
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
OpProductKind { kind, left, right } => {
let x = match **left {
OpSumKind { .. } => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel)
),
CompareKind { .. } => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel)
),
_ => stringify(left, context, displace_data, use_original_name),
_ => stringify(left, context, displace_data, export_to_excel),
};
let y = match **right {
OpSumKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
CompareKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
OpProductKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
_ => stringify(right, context, displace_data, use_original_name),
_ => stringify(right, context, displace_data, export_to_excel),
};
format!("{}{}{}", x, kind, y)
}
@@ -467,9 +497,7 @@ fn stringify(
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name)
}
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
@@ -482,9 +510,10 @@ fn stringify(
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel)
),
};
let y = match **right {
@@ -498,7 +527,7 @@ fn stringify(
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
}
OpRangeKind { .. }
| OpConcatenateKind { .. }
@@ -512,55 +541,63 @@ fn stringify(
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
};
format!("{}^{}", x, y)
}
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 } => {
let name = if use_original_name {
let name = if export_to_excel {
kind.to_xlsx_string()
} else {
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) => {
let mut first = true;
let mut arguments = "".to_string();
for el in args {
if !first {
arguments = format!(
"{},{}",
arguments,
stringify(el, context, displace_data, use_original_name)
);
let mut first_row = true;
let mut matrix_string = String::new();
for row in args {
if !first_row {
matrix_string.push(';');
} else {
first = false;
arguments = stringify(el, context, displace_data, use_original_name);
first_row = false;
}
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(),
DefinedNameKind((name, _)) => name.to_string(),
DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind {
OpUnary::Minus => {
format!(
"-{}",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
)
}
OpUnary::Percentage => {
format!(
"{}%",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
)
}
},
@@ -571,6 +608,29 @@ fn stringify(
message: _,
} => formula.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 +718,12 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::UnaryKind { kind: _, right } => {
rename_sheet_in_node(right, sheet_index, new_name);
}
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_sheet_in_node(child, sheet_index, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}
@@ -681,7 +747,7 @@ pub(crate) fn rename_defined_name_in_node(
) {
match node {
// Rename
Node::DefinedNameKind((n, s)) => {
Node::DefinedNameKind((n, s, _)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string();
}
@@ -736,6 +802,12 @@ pub(crate) fn rename_defined_name_in_node(
Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_defined_name_in_node(child, name, scope, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}

View File

@@ -1,4 +1,7 @@
mod test_add_implicit_intersection;
mod test_arrays;
mod test_general;
mod test_implicit_intersection;
mod test_issue_155;
mod test_move_formula;
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,
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(
&node,
&MoveContext {
@@ -400,7 +400,7 @@ fn test_move_formula_misc() {
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 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)"
);
}
#[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

@@ -3,11 +3,10 @@
use std::collections::HashMap;
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::types::CellReferenceRC;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
fn create_test_table(
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!",
"#CALC!", "#CIRC!", "#NULL!",
];
names.iter().any(|e| *e == name)
names.contains(&name)
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -240,6 +240,7 @@ pub enum TokenType {
Bang, // !
Percent, // %
And, // &
At, // @
Reference {
sheet: Option<String>,
row: i32,

View File

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

View File

@@ -154,15 +154,16 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Date(p) => {
let tokens = &p.tokens;
let mut text = "".to_string();
if !(1.0..=2_958_465.0).contains(&value) {
// 2_958_465 is 31 December 9999
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some("Date negative or too long".to_owned()),
};
}
let date = from_excel_date(value as i64);
let date = match from_excel_date(value as i64) {
Ok(d) => d,
Err(e) => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(e),
}
}
};
for token in tokens {
match token {
TextToken::Literal(c) => {

View File

@@ -178,10 +178,7 @@ impl Lexer {
}
}
self.position = position;
match chars.parse::<f64>() {
Err(_) => None,
Ok(v) => Some(v),
}
chars.parse::<f64>().ok()
}
fn consume_condition(&mut self) -> Option<(Compare, f64)> {

View File

@@ -3,8 +3,11 @@ use chrono::Datelike;
use chrono::Months;
use chrono::Timelike;
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
use crate::expressions::types::CellReferenceIndex;
use crate::formatter::dates::date_to_serial_number;
use crate::formatter::dates::permissive_date_to_serial_number;
use crate::model::get_milliseconds_since_epoch;
use crate::{
calc_result::CalcResult, constants::EXCEL_DATE_BASE, expressions::parser::Node,
@@ -18,20 +21,19 @@ impl Model {
return CalcResult::new_args_number_error(cell);
}
let serial_number = match self.get_number(&args[0], cell) {
Ok(c) => {
let t = c.floor() as i64;
if t < 0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Function DAY parameter 1 value is negative. It should be positive or zero.".to_string(),
};
}
t
}
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = from_excel_date(serial_number);
let date = match from_excel_date(serial_number) {
Ok(date) => date,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
}
};
let day = date.day() as f64;
CalcResult::Number(day)
}
@@ -42,20 +44,19 @@ impl Model {
return CalcResult::new_args_number_error(cell);
}
let serial_number = match self.get_number(&args[0], cell) {
Ok(c) => {
let t = c.floor() as i64;
if t < 0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Function MONTH parameter 1 value is negative. It should be positive or zero.".to_string(),
};
}
t
}
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = from_excel_date(serial_number);
let date = match from_excel_date(serial_number) {
Ok(date) => date,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
}
};
let month = date.month() as f64;
CalcResult::Number(month)
}
@@ -79,6 +80,23 @@ impl Model {
}
Err(s) => return s,
};
let date = match from_excel_date(serial_number) {
Ok(date) => date,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
}
};
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Function DAY parameter 1 value is too large.".to_string(),
};
}
let months = match self.get_number_no_bools(&args[1], cell) {
Ok(c) => {
@@ -91,9 +109,9 @@ impl Model {
let months_abs = months.unsigned_abs();
let native_date = if months > 0 {
from_excel_date(serial_number) + Months::new(months_abs)
date + Months::new(months_abs)
} else {
from_excel_date(serial_number) - Months::new(months_abs)
date - Months::new(months_abs)
};
// Instead of calculating the end of month we compute the first day of the following month
@@ -137,32 +155,18 @@ impl Model {
let month = match self.get_number(&args[1], cell) {
Ok(c) => {
let t = c.floor();
if t < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
};
}
t as u32
t as i32
}
Err(s) => return s,
};
let day = match self.get_number(&args[2], cell) {
Ok(c) => {
let t = c.floor();
if t < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
};
}
t as u32
t as i32
}
Err(s) => return s,
};
match date_to_serial_number(day, month, year) {
match permissive_date_to_serial_number(day, month, year) {
Ok(serial_number) => CalcResult::Number(serial_number as f64),
Err(message) => CalcResult::Error {
error: Error::NUM,
@@ -178,20 +182,19 @@ impl Model {
return CalcResult::new_args_number_error(cell);
}
let serial_number = match self.get_number(&args[0], cell) {
Ok(c) => {
let t = c.floor() as i64;
if t < 0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Function YEAR parameter 1 value is negative. It should be positive or zero.".to_string(),
};
}
t
}
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = from_excel_date(serial_number);
let date = match from_excel_date(serial_number) {
Ok(date) => date,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
}
};
let year = date.year() as f64;
CalcResult::Number(year)
}
@@ -203,20 +206,19 @@ impl Model {
return CalcResult::new_args_number_error(cell);
}
let serial_number = match self.get_number(&args[0], cell) {
Ok(c) => {
let t = c.floor() as i64;
if t < 0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Parameter 1 value is negative. It should be positive or zero."
.to_string(),
};
}
t
}
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = match from_excel_date(serial_number) {
Ok(date) => date,
Err(_) => {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
}
};
let months = match self.get_number(&args[1], cell) {
Ok(c) => {
@@ -229,13 +231,13 @@ impl Model {
let months_abs = months.unsigned_abs();
let native_date = if months > 0 {
from_excel_date(serial_number) + Months::new(months_abs)
date + Months::new(months_abs)
} else {
from_excel_date(serial_number) - Months::new(months_abs)
date - Months::new(months_abs)
};
let serial_number = native_date.num_days_from_ce() - EXCEL_DATE_BASE;
if serial_number < 0 {
if serial_number < MINIMUM_DATE_SERIAL_NUMBER {
return CalcResult::Error {
error: Error::NUM,
origin: cell,

View File

@@ -188,10 +188,7 @@ impl Model {
node: &Node,
cell: CellReferenceIndex,
) -> Result<(f64, f64, Suffix), CalcResult> {
let value = match self.get_string(node, cell) {
Ok(s) => s,
Err(s) => return Err(s),
};
let value = self.get_string(node, cell)?;
if value.is_empty() {
return Ok((0.0, 0.0, Suffix::I));
}

View File

@@ -2,7 +2,7 @@ use chrono::Datelike;
use crate::{
calc_result::CalcResult,
constants::{LAST_COLUMN, LAST_ROW},
constants::{LAST_COLUMN, LAST_ROW, MAXIMUM_DATE_SERIAL_NUMBER, MINIMUM_DATE_SERIAL_NUMBER},
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
formatter::dates::from_excel_date,
model::Model,
@@ -13,37 +13,32 @@ use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr
// See:
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
// FIXME: Is this enough?
fn is_valid_date(date: f64) -> bool {
date > 0.0
}
fn is_less_than_one_year(start_date: i64, end_date: i64) -> bool {
fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> {
let end = from_excel_date(end_date)?;
let start = from_excel_date(start_date)?;
if end_date - start_date < 365 {
return true;
return Ok(true);
}
let end = from_excel_date(end_date);
let start = from_excel_date(start_date);
let end_year = end.year();
let start_year = start.year();
if end_year == start_year {
return true;
return Ok(true);
}
if end_year != start_year + 1 {
return false;
return Ok(false);
}
let start_month = start.month();
let end_month = end.month();
if end_month < start_month {
return true;
return Ok(true);
}
if end_month > start_month {
return false;
return Ok(false);
}
// we are one year later same month
let start_day = start.day();
let end_day = end.day();
end_day <= start_day
Ok(end_day <= start_day)
}
fn compute_payment(
@@ -436,7 +431,7 @@ impl Model {
}
if rate == -1.0 {
return CalcResult::Error {
error: Error::NUM,
error: Error::DIV,
origin: cell,
message: "Rate must be != -1".to_string(),
};
@@ -923,7 +918,9 @@ impl Model {
}
let first_date = dates[0];
for date in &dates {
if !is_valid_date(*date) {
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
// Excel docs claim that if any number in dates is not a valid date,
// XNPV returns the #VALUE! error value, but it seems to return #VALUE!
return CalcResult::new_error(
@@ -989,7 +986,9 @@ impl Model {
}
let first_date = dates[0];
for date in &dates {
if !is_valid_date(*date) {
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
return CalcResult::new_error(
Error::NUM,
cell,
@@ -1373,9 +1372,10 @@ impl Model {
Ok(f) => f,
Err(s) => return s,
};
if !is_valid_date(settlement) || !is_valid_date(maturity) {
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
}
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
Ok(f) => f,
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
};
if settlement > maturity {
return CalcResult::new_error(
Error::NUM,
@@ -1383,7 +1383,7 @@ impl Model {
"settlement should be <= maturity".to_string(),
);
}
if !is_less_than_one_year(settlement as i64, maturity as i64) {
if !less_than_one_year {
return CalcResult::new_error(
Error::NUM,
cell,
@@ -1437,9 +1437,10 @@ impl Model {
Ok(f) => f,
Err(s) => return s,
};
if !is_valid_date(settlement) || !is_valid_date(maturity) {
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
}
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
Ok(f) => f,
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
};
if settlement > maturity {
return CalcResult::new_error(
Error::NUM,
@@ -1447,7 +1448,7 @@ impl Model {
"settlement should be <= maturity".to_string(),
);
}
if !is_less_than_one_year(settlement as i64, maturity as i64) {
if !less_than_one_year {
return CalcResult::new_error(
Error::NUM,
cell,
@@ -1487,9 +1488,10 @@ impl Model {
Ok(f) => f,
Err(s) => return s,
};
if !is_valid_date(settlement) || !is_valid_date(maturity) {
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
}
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
Ok(f) => f,
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
};
if settlement > maturity {
return CalcResult::new_error(
Error::NUM,
@@ -1497,7 +1499,7 @@ impl Model {
"settlement should be <= maturity".to_string(),
);
}
if !is_less_than_one_year(settlement as i64, maturity as i64) {
if !less_than_one_year {
return CalcResult::new_error(
Error::NUM,
cell,

View File

@@ -235,6 +235,11 @@ impl Model {
// This cannot happen
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 {
@@ -249,7 +254,7 @@ impl Model {
// The arg could be a defined name or a table
// let = &args[0];
match &args[0] {
Node::DefinedNameKind((name, scope)) => {
Node::DefinedNameKind((name, scope, _)) => {
// Let's see if it is a defined name
if let Some(defined_name) = self
.parsed_defined_names

View File

@@ -7,6 +7,22 @@ use crate::{
use super::util::compare_values;
impl Model {
pub(crate) fn fn_true(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
CalcResult::Boolean(true)
} else {
CalcResult::new_args_number_error(cell)
}
}
pub(crate) fn fn_false(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
CalcResult::Boolean(false)
} else {
CalcResult::new_args_number_error(cell)
}
}
pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 2 || args.len() == 3 {
let cond_result = self.get_boolean(&args[0], cell);
@@ -145,6 +161,13 @@ impl Model {
CalcResult::Range { .. }
| CalcResult::String { .. }
| CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value)
@@ -169,6 +192,13 @@ impl Model {
}
// References to empty cells are ignored. If all args are ignored the result is #VALUE!
CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)

View File

@@ -855,7 +855,7 @@ impl Model {
if left.row != right.row || left.column != right.column {
// FIXME: Implicit intersection or dynamic arrays
return CalcResult::Error {
error: Error::ERROR,
error: Error::NIMPL,
origin: cell,
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,8 @@
use crate::cast::NumberOrArray;
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::single_number_fn;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
@@ -169,6 +172,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,
_ => {
// We ignore booleans and strings
@@ -354,187 +378,29 @@ impl Model {
}
}
pub(crate) fn fn_sin(&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.sin();
CalcResult::Number(result)
}
pub(crate) fn fn_cos(&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.cos();
CalcResult::Number(result)
}
pub(crate) fn fn_tan(&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.tan();
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)
}
single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
single_number_fn!(fn_sinh, |f| Ok(f64::sinh(f)));
single_number_fn!(fn_cosh, |f| Ok(f64::cosh(f)));
single_number_fn!(fn_tanh, |f| Ok(f64::tanh(f)));
single_number_fn!(fn_asin, |f| Ok(f64::asin(f)));
single_number_fn!(fn_acos, |f| Ok(f64::acos(f)));
single_number_fn!(fn_atan, |f| Ok(f64::atan(f)));
single_number_fn!(fn_asinh, |f| Ok(f64::asinh(f)));
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)));
single_number_fn!(fn_sqrt, |f| if f < 0.0 {
Err(Error::NUM)
} else {
Ok(f64::sqrt(f))
});
single_number_fn!(fn_sqrtpi, |f: f64| if f < 0.0 {
Err(Error::NUM)
} else {
Ok((f * PI).sqrt())
});
pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() {
@@ -543,53 +409,6 @@ impl Model {
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 {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);

View File

@@ -15,6 +15,7 @@ mod financial_util;
mod information;
mod logical;
mod lookup_and_reference;
mod macros;
mod mathematical;
mod statistical;
mod subtotal;
@@ -949,7 +950,7 @@ impl Model {
match kind {
// Logical
Function::And => self.fn_and(args, cell),
Function::False => CalcResult::Boolean(false),
Function::False => self.fn_false(args, cell),
Function::If => self.fn_if(args, cell),
Function::Iferror => self.fn_iferror(args, cell),
Function::Ifna => self.fn_ifna(args, cell),
@@ -957,7 +958,7 @@ impl Model {
Function::Not => self.fn_not(args, cell),
Function::Or => self.fn_or(args, cell),
Function::Switch => self.fn_switch(args, cell),
Function::True => CalcResult::Boolean(true),
Function::True => self.fn_true(args, cell),
Function::Xor => self.fn_xor(args, cell),
// Math and trigonometry
Function::Sin => self.fn_sin(args, cell),

View File

@@ -134,6 +134,13 @@ impl Model {
);
}
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,
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 {

View File

@@ -182,6 +182,13 @@ impl Model {
}
}
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::Boolean(_)
| CalcResult::Error { .. } => counta += 1,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
}
}

View File

@@ -97,10 +97,24 @@ impl Model {
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
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)
@@ -125,6 +139,13 @@ impl Model {
};
}
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) {
Ok(s) => s,
@@ -280,6 +301,13 @@ impl Model {
};
}
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);
}
@@ -308,6 +336,13 @@ impl Model {
};
}
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());
}
@@ -336,6 +371,13 @@ impl Model {
};
}
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());
}
@@ -370,6 +412,13 @@ impl Model {
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() {
@@ -411,6 +460,13 @@ impl Model {
};
}
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());
}
@@ -441,6 +497,13 @@ impl Model {
};
}
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 {
match self.evaluate_node_in_context(&args[1], cell) {
@@ -471,6 +534,13 @@ impl Model {
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
} else {
1
@@ -509,6 +579,13 @@ impl Model {
};
}
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 {
match self.evaluate_node_in_context(&args[1], cell) {
@@ -539,6 +616,13 @@ impl Model {
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
} else {
1
@@ -577,6 +661,13 @@ impl Model {
};
}
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) {
CalcResult::Number(v) => {
@@ -641,6 +732,13 @@ impl Model {
};
}
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 count: usize = 0;
@@ -983,6 +1081,13 @@ impl Model {
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg | CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
}
}
@@ -1002,6 +1107,13 @@ impl Model {
}
}
CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
}
let result = values.join(&delimiter);
@@ -1125,6 +1237,11 @@ impl Model {
}
}
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

@@ -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)
Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
}
CalcResult::Range { left: _, right: _ } => {
// TODO: Implicit Intersection
Box::new(move |_x| false)
}
CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false),
CalcResult::Array(_) => Box::new(move |_x| false),
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 reverse search starting at the last item.
/// * 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
/// 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 {
if args.len() < 3 || args.len() > 6 {
return CalcResult::new_args_number_error(cell);

View File

@@ -39,9 +39,9 @@ pub mod types;
pub mod worksheet;
mod actions;
mod arithmetic;
mod cast;
mod constants;
mod diffs;
mod functions;
mod implicit_intersection;
mod model;

View File

@@ -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),
}
}
@@ -256,27 +267,10 @@ impl Model {
) -> CalcResult {
use Node::*;
match node {
OpSumKind { kind, left, right } => {
// In the future once the feature try trait stabilizes we could use the '?' operator for this :)
// See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5
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)
}
OpSumKind { kind, left, right } => match kind {
OpSum::Add => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 + f2)),
OpSum::Minus => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 - f2)),
},
NumberKind(value) => CalcResult::Number(*value),
StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)),
BooleanKind(value) => CalcResult::Boolean(*value),
@@ -364,59 +358,27 @@ impl Model {
let result = format!("{}{}", l, r);
CalcResult::String(result)
}
OpProductKind { kind, left, right } => {
let l = match self.get_number(left, cell) {
Ok(f) => f,
Err(s) => {
return s;
OpProductKind { kind, left, right } => match kind {
OpProduct::Times => {
self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 * f2))
}
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 } => {
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;
}
};
// Deal with errors properly
CalcResult::Number(l.powf(r))
self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1.powf(f2)))
}
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
InvalidFunctionKind { name, args: _ } => {
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
}
ArrayKind(_) => {
// TODO: NOT IMPLEMENTED
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
}
DefinedNameKind((name, scope)) => {
ArrayKind(s) => CalcResult::Array(s.to_owned()),
DefinedNameKind((name, scope, _)) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => {
@@ -528,6 +490,22 @@ impl Model {
format!("Error parsing {}: {}", formula, message),
),
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 } => {
let range = Range {
left: *left,
right: *right,
};
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
if left.sheet == right.sheet
&& left.row == right.row
&& left.column == right.column
{
let intersection_cell = CellReferenceIndex {
sheet: left.sheet,
column: left.column,
row: left.row,
};
let v = self.evaluate_cell(intersection_cell);
self.set_cell_value(cell_reference, &v);
} else {
@@ -639,10 +620,32 @@ impl Model {
f,
s,
o,
m: "Invalid reference".to_string(),
ei: Error::VALUE,
m: "Implicit Intersection not implemented".to_string(),
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 => {
*self.workbook.worksheets[sheet as usize]
@@ -652,6 +655,20 @@ impl Model {
.get_mut(&column)
.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,
};
}
}
}
}
@@ -865,11 +882,7 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
let defined_names = workbook.get_defined_names_with_scope();
// add all tables
// let mut tables = Vec::new();
// for worksheet in worksheets {
@@ -960,10 +973,7 @@ impl Model {
}
}
}
let sheet = match self.get_sheet_index_by_name(&sheet_name) {
Some(s) => s,
None => return None,
};
let sheet = self.get_sheet_index_by_name(&sheet_name)?;
let row = match row.parse::<i32>() {
Ok(r) => r,
Err(_) => return None,
@@ -1875,12 +1885,29 @@ impl Model {
}
/// Returns the style for cell (`sheet`, `row`, `column`)
/// If the cell does not have a style defined we check the row, otherwise the column and finally a default
pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
let style_index = self.get_cell_style_index(sheet, row, column)?;
let style = self.workbook.styles.get_style(style_index)?;
Ok(style)
}
/// Returns the style defined in a cell if any.
pub fn get_cell_style_or_none(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<Style>, String> {
let style = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.map(|c| self.workbook.styles.get_style(c.get_style()))
.transpose();
style
}
/// Returns an internal binary representation of the workbook
///
/// See also:
@@ -2164,6 +2191,73 @@ impl Model {
Err("Defined name not found".to_string())
}
}
/// Returns the style object of a column, if any
pub fn get_column_style(&self, sheet: u32, column: i32) -> Result<Option<Style>, String> {
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
let cols = &worksheet.cols;
for col in cols {
if column >= col.min && column <= col.max {
if let Some(style_index) = col.style {
let style = self.workbook.styles.get_style(style_index)?;
return Ok(Some(style));
}
return Ok(None);
}
}
Ok(None)
} else {
Err("Invalid sheet".to_string())
}
}
/// Returns the style object of a row, if any
pub fn get_row_style(&self, sheet: u32, row: i32) -> Result<Option<Style>, String> {
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
let rows = &worksheet.rows;
for r in rows {
if row == r.r {
let style = self.workbook.styles.get_style(r.s)?;
return Ok(Some(style));
}
}
Ok(None)
} else {
Err("Invalid sheet".to_string())
}
}
/// Sets a column with style
pub fn set_column_style(
&mut self,
sheet: u32,
column: i32,
style: &Style,
) -> Result<(), String> {
let style_index = self.workbook.styles.get_style_index_or_create(style);
self.workbook
.worksheet_mut(sheet)?
.set_column_style(column, style_index)
}
/// Sets a row with style
pub fn set_row_style(&mut self, sheet: u32, row: i32, style: &Style) -> Result<(), String> {
let style_index = self.workbook.styles.get_style_index_or_create(style);
self.workbook
.worksheet_mut(sheet)?
.set_row_style(row, style_index)
}
/// Deletes the style of a column if the is any
pub fn delete_column_style(&mut self, sheet: u32, column: i32) -> Result<(), String> {
self.workbook
.worksheet_mut(sheet)?
.delete_column_style(column)
}
/// Deletes the style of a row if there is any
pub fn delete_row_style(&mut self, sheet: u32, row: i32) -> Result<(), String> {
self.workbook.worksheet_mut(sheet)?.delete_row_style(row)
}
}
#[cfg(test)]

View File

@@ -8,7 +8,7 @@ use crate::{
expressions::{
lexer::LexerMode,
parser::{
stringify::{rename_sheet_in_node, to_rc_format},
stringify::{rename_sheet_in_node, to_rc_format, to_string},
Parser,
},
types::CellReferenceRC,
@@ -17,7 +17,8 @@ use crate::{
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
WorksheetView,
},
utils::ParsedReference,
};
@@ -144,12 +145,7 @@ impl Model {
/// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) {
let defined_names = self
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
let defined_names = self.workbook.get_defined_names_with_scope();
self.parser
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
self.parsed_formulas = vec![];
@@ -243,7 +239,7 @@ impl Model {
/// Renames a sheet and updates all existing references to that sheet.
/// 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 is invalid
pub fn rename_sheet_by_index(
@@ -257,17 +253,15 @@ impl Model {
if self.get_sheet_index_by_name(new_name).is_some() {
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
// All internal formulas are 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 worksheets {
for worksheet in &mut self.workbook.worksheets {
// R1C1 formulas are not tied to a cell (but are tied to a cell)
let cell_reference = &CellReferenceRC {
sheet: worksheet.get_name(),
row: 1,
@@ -281,11 +275,32 @@ impl Model {
}
worksheet.shared_formulas = formulas;
}
// Se the mode back to A1
// Set the mode back to 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
let worksheets = &mut self.workbook.worksheets;
worksheets[sheet_index as usize].set_name(new_name);
self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
self.reset_parsed_structures();
Ok(())
}
@@ -301,7 +316,7 @@ impl Model {
};
if sheet_index >= sheet_count {
return Err("Sheet index too large".to_string());
}
};
self.workbook.worksheets.remove(sheet_index as usize);
self.reset_parsed_structures();
Ok(())
@@ -390,6 +405,7 @@ impl Model {
},
tables: HashMap::new(),
views,
users: Vec::new(),
};
let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets;

View File

@@ -4,8 +4,6 @@ use crate::{
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles},
};
// TODO: Move Styles and all related types from crate::types here
// Not doing it right now to not have conflicts with exporter branch
impl Styles {
fn get_font_index(&self, font: &Font) -> Option<i32> {
for (font_index, item) in self.fonts.iter().enumerate() {

View File

@@ -13,6 +13,7 @@ mod test_fn_averageifs;
mod test_fn_choose;
mod test_fn_concatenate;
mod test_fn_count;
mod test_fn_day;
mod test_fn_exact;
mod test_fn_financial;
mod test_fn_formulatext;
@@ -27,7 +28,6 @@ mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_fn_unicode;
mod test_forward_references;
mod test_frozen_rows_columns;
mod test_general;
mod test_math;
@@ -36,11 +36,13 @@ mod test_model_cell_clear_all;
mod test_model_is_empty_cell;
mod test_move_formula;
mod test_quote_prefix;
mod test_row_column_styles;
mod test_set_user_input;
mod test_sheet_markup;
mod test_sheets;
mod test_styles;
mod test_trigonometric;
mod test_true_false;
mod test_workbook;
mod test_worksheet;
pub(crate) mod util;
@@ -49,6 +51,7 @@ mod engineering;
mod test_fn_offset;
mod test_number_format;
mod test_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_fv;
@@ -56,6 +59,7 @@ mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_geomean;
mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155;
mod test_percentage;
mod test_set_functions_error_handling;

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

@@ -37,12 +37,12 @@ fn test_fn_date_arguments() {
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#NUM!");
assert_eq!(model._get_text("A6"), *"#NUM!");
assert_eq!(model._get_text("A7"), *"#NUM!");
assert_eq!(model._get_text("A8"), *"#NUM!");
assert_eq!(model._get_text("A5"), *"10/10/1974");
assert_eq!(model._get_text("A6"), *"21/01/1975");
assert_eq!(model._get_text("A7"), *"10/02/1976");
assert_eq!(model._get_text("A8"), *"02/03/1975");
assert_eq!(model._get_text("A9"), *"#NUM!");
assert_eq!(model._get_text("A9"), *"01/03/1975");
assert_eq!(model._get_text("A10"), *"29/02/1976");
assert_eq!(
model.get_cell_value_by_ref("Sheet1!A10"),
@@ -64,15 +64,18 @@ fn test_date_out_of_range() {
// year (actually years < 1900 don't really make sense)
model._set("C1", "=DATE(-1, 5, 5)");
// excel is not compatible with years past 9999
model._set("C2", "=DATE(10000, 5, 5)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("A1"), *"10/12/2021");
assert_eq!(model._get_text("A2"), *"10/01/2023");
assert_eq!(model._get_text("B1"), *"30/04/2042");
assert_eq!(model._get_text("B2"), *"01/06/2025");
assert_eq!(model._get_text("C1"), *"#NUM!");
assert_eq!(model._get_text("C2"), *"#NUM!");
}
#[test]
@@ -129,8 +132,7 @@ fn test_day_small_serial() {
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
// This agrees with Google Docs and disagrees with Excel
assert_eq!(model._get_text("A2"), *"30");
assert_eq!(model._get_text("A2"), *"#NUM!");
// Excel thinks is Feb 29, 1900
assert_eq!(model._get_text("A3"), *"28");
@@ -150,8 +152,7 @@ fn test_month_small_serial() {
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
// This agrees with Google Docs and disagrees with Excel
assert_eq!(model._get_text("A2"), *"12");
assert_eq!(model._get_text("A2"), *"#NUM!");
// We agree with Excel here (We are both in Feb)
assert_eq!(model._get_text("A3"), *"2");
@@ -171,8 +172,7 @@ fn test_year_small_serial() {
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
// This agrees with Google Docs and disagrees with Excel
assert_eq!(model._get_text("A2"), *"1899");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"1900");
@@ -204,7 +204,10 @@ fn test_date_early_dates() {
model.get_cell_value_by_ref("Sheet1!A2"),
Ok(CellValue::Number(60.0))
);
assert_eq!(model._get_text("B2"), *"#NUM!");
// This does not agree with Excel, instead of mistakenly allowing
// for Feb 29, it will auto-wrap to the next day after Feb 28.
assert_eq!(model._get_text("B2"), *"01/03/1900");
// This agrees with Excel from he onward
assert_eq!(model._get_text("A3"), *"01/03/1900");

View File

@@ -22,13 +22,14 @@ fn fn_concatenate() {
model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#);
// This will break once we implement the implicit intersection operator
// 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("B3", r#"=CONCAT(A1:A3, "!")"#);
model.evaluate();
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("C2"), *" my !");
}

View File

@@ -0,0 +1,15 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn test_fn_date_arguments() {
let mut model = new_empty_model();
model._set("A1", "=DAY(95051806)");
model._set("A2", "=DAY(2958465)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
assert_eq!(model._get_text("A2"), *"31");
}

View File

@@ -30,8 +30,18 @@ fn implicit_intersection() {
model._set("A2", "=FORMULATEXT(D1:E1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A1"), *"#N/IMPL!");
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]

View File

@@ -17,3 +17,19 @@ fn test_fn_sum_arguments() {
assert_eq!(model._get_text("A3"), *"1");
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!");
}

View File

@@ -0,0 +1,32 @@
#![allow(clippy::unwrap_used)]
use crate::{constants::DEFAULT_COLUMN_WIDTH, test::util::new_empty_model};
#[test]
fn test_model_set_cells_with_values_styles() {
let mut model = new_empty_model();
let style_base = model.get_style_for_cell(0, 1, 1).unwrap();
let mut style = style_base.clone();
style.font.b = true;
model.set_column_style(0, 10, &style).unwrap();
assert!(model.get_style_for_cell(0, 21, 10).unwrap().font.b);
model.delete_column_style(0, 10).unwrap();
// There are no styles in the column
assert!(model.workbook.worksheets[0].cols.is_empty());
// lets change the column width and check it does not affect the style
model
.set_column_width(0, 10, DEFAULT_COLUMN_WIDTH * 2.0)
.unwrap();
model.set_column_style(0, 10, &style).unwrap();
model.delete_column_style(0, 10).unwrap();
// There are no styles in the column
assert!(model.workbook.worksheets[0].cols.len() == 1);
}

View File

@@ -0,0 +1,25 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn true_false_arguments() {
let mut model = new_empty_model();
model._set("A1", "=TRUE( )");
model._set("A2", "=FALSE( )");
model._set("A3", "=TRUE( 4 )");
model._set("A4", "=FALSE( 4 )");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_formula("A1"), *"=TRUE()");
assert_eq!(model._get_formula("A2"), *"=FALSE()");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_formula("A3"), *"=TRUE(4)");
assert_eq!(model._get_formula("A4"), *"=FALSE(4)");
}

View File

@@ -3,7 +3,9 @@ mod test_autofill_columns;
mod test_autofill_rows;
mod test_border;
mod test_clear_cells;
mod test_column_style;
mod test_defined_names;
mod test_delete_row_column_formatting;
mod test_diff_queue;
mod test_evaluation;
mod test_general;
@@ -13,9 +15,11 @@ mod test_on_area_selection;
mod test_on_expand_selected_range;
mod test_on_paste_styles;
mod test_paste_csv;
mod test_recursive;
mod test_rename_sheet;
mod test_row_column;
mod test_sheet_state;
mod test_sheets_undo_redo;
mod test_styles;
mod test_to_from_bytes;
mod test_undo_redo;

View File

@@ -9,7 +9,7 @@ fn add_undo_redo() {
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
model
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
.set_columns_width(1, 5, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
.unwrap();
model.new_sheet().unwrap();
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
@@ -25,9 +25,6 @@ fn add_undo_redo() {
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
model.delete_sheet(1).unwrap();
assert!(!model.can_undo());
assert!(!model.can_redo());
}
#[test]

View File

@@ -520,14 +520,19 @@ fn borders_top() {
.unwrap();
model.set_area_with_border(range, &border_area).unwrap();
check_borders(&model);
for row in 5..9 {
for row in 4..9 {
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let bottom = if row == 8 {
let bottom = if row != 4 {
None
} else {
Some(border_item.clone())
};
let top = if row != 5 {
None
} else {
Some(border_item.clone())
@@ -537,7 +542,7 @@ fn borders_top() {
diagonal_down: false,
left: None,
right: None,
top: Some(border_item.clone()),
top,
bottom,
diagonal: None,
};
@@ -647,12 +652,12 @@ fn borders_right() {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let left = if column == 6 {
let left = if column != 9 {
None
} else {
Some(border_item.clone())
};
let right = if column == 9 {
let right = if column != 8 {
None
} else {
Some(border_item.clone())
@@ -705,7 +710,7 @@ fn borders_bottom() {
color: Some("#FF5566".to_string()),
};
// The top will also have a value for all but the first one
let top = if row == 5 {
let bottom = if row != 8 {
None
} else {
Some(border_item.clone())
@@ -715,8 +720,8 @@ fn borders_bottom() {
diagonal_down: false,
left: None,
right: None,
top,
bottom: Some(border_item.clone()),
top: None,
bottom,
diagonal: None,
};
assert_eq!(style.border, expected_border);
@@ -751,18 +756,13 @@ fn borders_left() {
model.set_area_with_border(range, &border_area).unwrap();
for row in 5..9 {
for column in 5..9 {
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let left = if column == 5 {
None
} else {
Some(border_item.clone())
};
let right = if column == 8 {
let left = if column != 6 {
None
} else {
Some(border_item.clone())
@@ -771,13 +771,29 @@ fn borders_left() {
diagonal_up: false,
diagonal_down: false,
left,
right,
right: None,
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
// Column 5 has a border to the right, of course:
let style = model.get_cell_style(0, row, 5).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: Some(border_item.clone()),
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
@@ -1018,10 +1034,7 @@ fn border_top() {
style: BorderStyle::Thin,
color: Some("#F2F2F2".to_string()),
};
assert_eq!(
model._get_cell_actual_border("C4").bottom,
Some(border_item)
);
assert_eq!(model._get_cell_actual_border("C4").top, Some(border_item));
model.undo().unwrap();

View File

@@ -0,0 +1,504 @@
#![allow(clippy::unwrap_used)]
use crate::constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
use crate::expressions::types::Area;
use crate::UserModel;
#[test]
fn column_width() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.i);
assert!(!style.font.b);
assert!(!style.font.u);
assert!(!style.font.strike);
assert_eq!(style.font.color, Some("#000000".to_owned()));
// Set the whole column style and check it works
model.update_range_style(&range, "font.b", "true").unwrap();
let style = model.get_cell_style(0, 109, 7).unwrap();
assert!(style.font.b);
// undo and check it works
model.undo().unwrap();
let style = model.get_cell_style(0, 109, 7).unwrap();
assert!(!style.font.b);
// redo and check it works
model.redo().unwrap();
let style = model.get_cell_style(0, 109, 7).unwrap();
assert!(style.font.b);
// change the column width and check it does not affect the style
model
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
.unwrap();
let style = model.get_cell_style(0, 109, 7).unwrap();
assert!(style.font.b);
}
#[test]
fn existing_style() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let cell_g123 = Area {
sheet: 0,
row: 123,
column: 7,
width: 1,
height: 1,
};
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
// Set G123 background to red
model
.update_range_style(&cell_g123, "fill.bg_color", "#333444")
.unwrap();
// Now set the style of the whole column
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
// Get the style of G123
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
model.undo().unwrap();
// Check the style of G123 is now what it was before
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
model.redo().unwrap();
// Check G123 has the column style now
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
}
#[test]
fn row_column() {
// We set the row style, then a column style
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let row_3_range = Area {
sheet: 0,
row: 3,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// update the row style
model
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
.unwrap();
// update the column style
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
// Check G3 has the column style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
// undo twice. Color must be default
model.undo().unwrap();
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
model.undo().unwrap();
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
}
#[test]
fn column_row() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let default_style = model.get_cell_style(0, 3, 7).unwrap();
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let row_3_range = Area {
sheet: 0,
row: 3,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// update the column style
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
// update the row style
model
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
.unwrap();
// Check G3 has the row style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
model.undo().unwrap();
// Check G3 has the column style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
model.undo().unwrap();
// Check G3 has the default_style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, default_style.fill.bg_color);
}
#[test]
fn row_column_column() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let column_c_range = Area {
sheet: 0,
row: 1,
column: 3,
width: 1,
height: LAST_ROW,
};
let column_e_range = Area {
sheet: 0,
row: 1,
column: 5,
width: 1,
height: LAST_ROW,
};
let row_5_range = Area {
sheet: 0,
row: 5,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// update the row style
model
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
.unwrap();
// update the column style
model
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
.unwrap();
model
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
.unwrap();
model.undo().unwrap();
model.undo().unwrap();
model.undo().unwrap();
// Test E5 has the default style
let style = model.get_cell_style(0, 5, 5).unwrap();
assert_eq!(style.fill.bg_color, None);
}
#[test]
fn width_column_undo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
.unwrap();
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", "#CCC111")
.unwrap();
model.undo().unwrap();
assert_eq!(
model.get_column_width(0, 7).unwrap(),
DEFAULT_COLUMN_WIDTH * 2.0
);
}
#[test]
fn height_row_undo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_rows_height(0, 10, 10, DEFAULT_ROW_HEIGHT * 2.0)
.unwrap();
let row_10_range = Area {
sheet: 0,
row: 10,
column: 1,
width: LAST_COLUMN,
height: 1,
};
model
.update_range_style(&row_10_range, "fill.bg_color", "#CCC111")
.unwrap();
assert_eq!(
model.get_row_height(0, 10).unwrap(),
2.0 * DEFAULT_ROW_HEIGHT
);
model.undo().unwrap();
assert_eq!(
model.get_row_height(0, 10).unwrap(),
2.0 * DEFAULT_ROW_HEIGHT
);
model.undo().unwrap();
assert_eq!(model.get_row_height(0, 10).unwrap(), DEFAULT_ROW_HEIGHT);
}
#[test]
fn cell_row_undo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let cell_g12 = Area {
sheet: 0,
row: 12,
column: 7,
width: 1,
height: 1,
};
let row_12_range = Area {
sheet: 0,
row: 12,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// Set G12 background to red
model
.update_range_style(&cell_g12, "fill.bg_color", "#333444")
.unwrap();
model
.update_range_style(&row_12_range, "fill.bg_color", "#CCC111")
.unwrap();
let style = model.get_cell_style(0, 12, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
model.undo().unwrap();
let style = model.get_cell_style(0, 12, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
}
#[test]
fn set_column_style_then_cell() {
// We check that if we set a cell style in a column that already has a style
// the styles compound
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let cell_g12 = Area {
sheet: 0,
row: 12,
column: 7,
width: 1,
height: 1,
};
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
// Set G12 background to red
model
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
.unwrap();
model
.update_range_style(&cell_g12, "alignment.horizontal", "center")
.unwrap();
let style = model.get_cell_style(0, 12, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
model.undo().unwrap();
model.undo().unwrap();
let style = model.get_cell_style(0, 12, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
}
#[test]
fn set_row_style_then_cell() {
// We check that if we set a cell style in a column that already has a style
// the styles compound
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let cell_g12 = Area {
sheet: 0,
row: 12,
column: 7,
width: 1,
height: 1,
};
let row_12_range = Area {
sheet: 0,
row: 12,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// Set G12 background to red
model
.update_range_style(&row_12_range, "fill.bg_color", "#333444")
.unwrap();
model
.update_range_style(&cell_g12, "alignment.horizontal", "center")
.unwrap();
let style = model.get_cell_style(0, 12, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
}
#[test]
fn column_style_then_row_alignment() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let row_3_range = Area {
sheet: 0,
row: 3,
column: 1,
width: LAST_COLUMN,
height: 1,
};
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
model
.update_range_style(&row_3_range, "alignment.horizontal", "center")
.unwrap();
// check the row alignment does not affect the column style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
}
#[test]
fn column_style_then_width() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
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", "#555666")
.unwrap();
model
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
.unwrap();
// Check column width worked:
assert_eq!(
model.get_column_width(0, 7).unwrap(),
DEFAULT_COLUMN_WIDTH * 2.0
);
}
#[test]
fn test_row_column_column() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let column_c_range = Area {
sheet: 0,
row: 1,
column: 3,
width: 1,
height: LAST_ROW,
};
let column_e_range = Area {
sheet: 0,
row: 1,
column: 5,
width: 1,
height: LAST_ROW,
};
let row_5_range = Area {
sheet: 0,
row: 5,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// update the row style
model
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
.unwrap();
// update the column style
model
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
.unwrap();
model
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
.unwrap();
// test E5 has the column style
let style = model.get_cell_style(0, 5, 5).unwrap();
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
}

View File

@@ -396,3 +396,57 @@ fn undo_redo() {
Ok("Hola!".to_string())
);
}
#[test]
fn change_scope_to_first_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
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(1, 2, 1),
Ok("Hello world!".to_string())
);
model
.update_defined_name("myName", None, "myName", Some(0), "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(1, 2, 1),
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

@@ -0,0 +1,243 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW},
expressions::types::Area,
UserModel,
};
#[test]
fn delete_column_formatting() {
// We are going to delete formatting in column G (7)
// There are cells with their own styles
// There are rows with their own styles
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let cell_g123 = Area {
sheet: 0,
row: 123,
column: 7,
width: 1,
height: 1,
};
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let row_3_range = Area {
sheet: 0,
row: 3,
column: 1,
width: LAST_COLUMN,
height: 1,
};
// Set the style of the whole column
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
// Set G123 background to red
model
.update_range_style(&cell_g123, "fill.bg_color", "#FF5533")
.unwrap();
// Set the style of the whole row
model
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
.unwrap();
// Delete the column formatting
model.range_clear_formatting(&column_g_range).unwrap();
// Check the style of G123 is now what it was before
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
// Check the style of the whole row is still there
let style = model.get_cell_style(0, 3, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
// Check the style of the whole column is now gone
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
let style = model.get_cell_style(0, 40, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
model.undo().unwrap();
// Check the style of G123 is now what it was before
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#FF5533".to_owned()));
// Check G3 is the row style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
// Check G40 is the column style
let style = model.get_cell_style(0, 40, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
model.redo().unwrap();
// Check the style of G123 is now what it was before
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
// Check the style of the whole row is still there
let style = model.get_cell_style(0, 3, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
// Check the style of the whole column is now gone
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
let style = model.get_cell_style(0, 40, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
}
#[test]
fn column_width() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
.unwrap();
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
// Set the style of the whole column
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
// Delete the column formatting
model.range_clear_formatting(&column_g_range).unwrap();
// This does not change the column width
assert_eq!(
model.get_column_width(0, 7).unwrap(),
2.0 * DEFAULT_COLUMN_WIDTH
);
model.undo().unwrap();
assert_eq!(
model.get_column_width(0, 7).unwrap(),
2.0 * DEFAULT_COLUMN_WIDTH
);
model.redo().unwrap();
assert_eq!(
model.get_column_width(0, 7).unwrap(),
2.0 * DEFAULT_COLUMN_WIDTH
);
}
#[test]
fn column_row_style_undo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
.unwrap();
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let row_123_range = Area {
sheet: 0,
row: 123,
column: 1,
width: LAST_COLUMN,
height: 1,
};
let delete_range = Area {
sheet: 0,
row: 120,
column: 5,
width: 20,
height: 20,
};
// Set the style of the whole column
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
model
.update_range_style(&row_123_range, "fill.bg_color", "#111222")
.unwrap();
model.range_clear_formatting(&delete_range).unwrap();
// check G123 is empty
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
// uno clear formatting
model.undo().unwrap();
// G123 has the row style
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#111222".to_owned()));
// undo twice
model.undo().unwrap();
model.undo().unwrap();
// check G123 is empty
let style = model.get_cell_style(0, 123, 7).unwrap();
assert_eq!(style.fill.bg_color, None);
}
#[test]
fn column_row_row_height_undo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
let row_3_range = Area {
sheet: 0,
row: 3,
column: 1,
width: LAST_COLUMN,
height: 1,
};
model
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
.unwrap();
model
.set_rows_height(0, 3, 3, DEFAULT_ROW_HEIGHT * 2.0)
.unwrap();
model
.update_range_style(&row_3_range, "fill.bg_color", "#111222")
.unwrap();
model.undo().unwrap();
// check G3 has the column style
let style = model.get_cell_style(0, 3, 7).unwrap();
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
}

View File

@@ -10,7 +10,7 @@ use crate::{
fn send_queue() {
let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_column_width(0, 3, width).unwrap();
model1.set_columns_width(0, 3, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
let send_queue = model1.flush_send_queue();
@@ -34,7 +34,7 @@ fn apply_external_diffs_wrong_str() {
fn queue_undo_redo() {
let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_column_width(0, 3, width).unwrap();
model1.set_columns_width(0, 3, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
assert!(model1.undo().is_ok());
assert!(model1.redo().is_ok());
@@ -57,8 +57,8 @@ fn queue_undo_redo_multiple() {
// do a bunch of things
model1.set_frozen_columns_count(0, 5).unwrap();
model1.set_frozen_rows_count(0, 6).unwrap();
model1.set_column_width(0, 7, 300.0).unwrap();
model1.set_row_height(0, 23, 123.0).unwrap();
model1.set_columns_width(0, 7, 7, 300.0).unwrap();
model1.set_rows_height(0, 23, 23, 123.0).unwrap();
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
for row in 1..5 {

View File

@@ -59,7 +59,7 @@ fn insert_remove_rows() {
// Insert some data in row 5 (and change the style)
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
// Change the height of the column
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
// remove the row
assert!(model.delete_row(0, 5).is_ok());
@@ -95,7 +95,7 @@ fn insert_remove_columns() {
// Insert some data in row 5 (and change the style) in E1
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
// Change the width of the column
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
assert!(model.set_columns_width(0, 5, 5, 3.0 * column_width).is_ok());
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
// remove the column

View File

@@ -0,0 +1,42 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::LAST_ROW, expressions::types::Area, test::util::new_empty_model, UserModel,
};
#[test]
fn two_columns() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Set style in column C (column 3)
let column_c_range = Area {
sheet: 0,
row: 1,
column: 3,
width: 1,
height: LAST_ROW,
};
model
.update_range_style(&column_c_range, "fill.bg_color", "#333444")
.unwrap();
model.set_user_input(0, 5, 3, "2").unwrap();
// Set Style in column G (column 7)
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();
model.set_user_input(0, 5, 6, "42").unwrap();
// Set formula in G5: =F5*C5
model.set_user_input(0, 5, 7, "=F5*C5").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 7).unwrap(), "84");
}

View File

@@ -59,7 +59,7 @@ fn simple_delete_column() {
model.set_user_input(0, 1, 5, "3").unwrap();
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
model
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
.unwrap();
model.delete_column(0, 5).unwrap();
@@ -116,7 +116,7 @@ fn simple_delete_row() {
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
model
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
.unwrap();
model.delete_row(0, 15).unwrap();
@@ -170,5 +170,44 @@ fn row_heigh_increases_automatically() {
model
.set_user_input(0, 1, 1, "My home in Canada had horses\nAnd monkeys!")
.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]
fn insert_row_evaluates() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
assert!(model.insert_row(0, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.delete_row(0, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
}
#[test]
fn insert_column_evaluates() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
assert!(model.insert_column(0, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.delete_column(0, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
}

View File

@@ -0,0 +1,52 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_undo_redo() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(model.get_selected_sheet(), 0);
model.new_sheet().unwrap();
assert_eq!(model.get_selected_sheet(), 1);
model.undo().unwrap();
assert_eq!(model.get_selected_sheet(), 0);
{
let props = model.get_worksheets_properties();
assert_eq!(props.len(), 1);
let view = model.get_selected_view();
assert_eq!(view.sheet, 0);
}
model.redo().unwrap();
assert_eq!(model.get_selected_sheet(), 1);
{
let props = model.get_worksheets_properties();
assert_eq!(props.len(), 2);
let view = model.get_selected_view();
assert_eq!(view.sheet, 1);
}
}
#[test]
fn delete_undo() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(model.get_selected_sheet(), 0);
model.new_sheet().unwrap();
assert_eq!(model.get_selected_sheet(), 1);
model.set_user_input(1, 1, 1, "42").unwrap();
model.set_user_input(1, 1, 2, "=A1*2").unwrap();
model.delete_sheet(1).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
model.undo().unwrap();
assert_eq!(model.get_selected_sheet(), 1);
model.redo().unwrap();
assert_eq!(model.get_selected_sheet(), 0);
}

View File

@@ -436,3 +436,47 @@ fn false_removes_value() {
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.b);
}
#[test]
fn cell_clear_formatting() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
// bold
model.update_range_style(&range, "font.b", "true").unwrap();
model
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.b);
assert_eq!(
style.alignment.unwrap().horizontal,
HorizontalAlignment::CenterContinuous
);
model.range_clear_all(&range).unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.b);
assert_eq!(style.alignment, None);
model.undo().unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.b);
assert_eq!(
style.alignment.unwrap().horizontal,
HorizontalAlignment::CenterContinuous
);
model.redo().unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.b);
assert_eq!(style.alignment, None);
}

View File

@@ -6,7 +6,7 @@ use crate::{test::util::new_empty_model, UserModel};
fn basic() {
let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_column_width(0, 3, width).unwrap();
model1.set_columns_width(0, 3, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
let model_bytes = model1.to_bytes();

View File

@@ -39,6 +39,14 @@ pub struct WorkbookView {
pub window_height: i64,
}
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct WebUser {
pub id: String,
pub sheet: u32,
pub row: i32,
pub column: i32,
}
/// An internal representation of an IronCalc Workbook
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Workbook {
@@ -51,6 +59,7 @@ pub struct Workbook {
pub metadata: Metadata,
pub tables: HashMap<String, Table>,
pub views: HashMap<u32, WorkbookView>,
pub users: Vec<WebUser>
}
/// A defined name. The `sheet_id` is the sheet index in case the name is local
@@ -62,8 +71,8 @@ pub struct DefinedName {
}
/// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum SheetState {
Visible,
@@ -323,6 +332,19 @@ pub struct Style {
pub quote_prefix: bool,
}
impl Default for Style {
fn default() -> Self {
Style {
alignment: None,
num_fmt: "general".to_string(),
fill: Fill::default(),
font: Font::default(),
border: Border::default(),
quote_prefix: false,
}
}
}
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct NumFmt {
pub num_fmt_id: i32,
@@ -394,7 +416,7 @@ impl Default for Font {
u: false,
b: false,
i: false,
sz: 11,
sz: 13,
color: Some("#000000".to_string()),
name: "Calibri".to_string(),
family: 2,

View File

@@ -50,8 +50,9 @@ impl Units {
fn get_units_from_format_string(num_fmt: &str) -> Option<Units> {
let mut parser = Parser::new(num_fmt);
parser.parse();
let parts = parser.parts.first()?;
// We only care about the first part (positive number)
match &parser.parts[0] {
match parts {
ParsePart::Number(part) => {
if part.percent > 0 {
Some(Units::Percentage {
@@ -298,6 +299,7 @@ impl Model {
Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None,
Node::ImplicitIntersection { .. } => None,
}
}

View File

@@ -0,0 +1,507 @@
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
expressions::types::Area,
};
use super::{
border_utils::is_max_border, common::BorderType, history::Diff, BorderArea, UserModel,
};
impl UserModel {
fn update_single_cell_border(
&mut self,
border_area: &BorderArea,
cell: (u32, i32, i32),
range: (i32, i32, i32, i32),
diff_list: &mut Vec<Diff>,
) -> Result<(), String> {
let (sheet, row, column) = cell;
let (first_row, first_column, last_row, last_column) = range;
let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
let mut new_value = match &old_value {
Some(value) => value.clone(),
None => Default::default(),
};
match border_area.r#type {
BorderType::All => {
new_value.border.top = Some(border_area.item.clone());
new_value.border.right = Some(border_area.item.clone());
new_value.border.bottom = Some(border_area.item.clone());
new_value.border.left = Some(border_area.item.clone());
}
BorderType::Inner => {
if row != first_row {
new_value.border.top = Some(border_area.item.clone());
}
if row != last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
if column != first_column {
new_value.border.left = Some(border_area.item.clone());
}
if column != last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::Outer => {
if row == first_row {
new_value.border.top = Some(border_area.item.clone());
}
if row == last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
if column == first_column {
new_value.border.left = Some(border_area.item.clone());
}
if column == last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::Top => {
if row == first_row {
new_value.border.top = Some(border_area.item.clone());
}
}
BorderType::Right => {
if column == last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::Bottom => {
if row == last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
}
BorderType::Left => {
if column == first_column {
new_value.border.left = Some(border_area.item.clone());
}
}
BorderType::CenterH => {
if row != first_row {
new_value.border.top = Some(border_area.item.clone());
}
if row != last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
}
BorderType::CenterV => {
if column != first_column {
new_value.border.left = Some(border_area.item.clone());
}
if column != last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::None => {
new_value.border.top = None;
new_value.border.right = None;
new_value.border.bottom = None;
new_value.border.left = None;
}
}
self.model.set_cell_style(sheet, row, column, &new_value)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(old_value),
new_value: Box::new(new_value),
});
Ok(())
}
fn set_rows_with_border(
&mut self,
sheet: u32,
first_row: i32,
last_row: i32,
border_area: &BorderArea,
) -> Result<(), String> {
let mut diff_list = Vec::new();
for row in first_row..=last_row {
let old_value = self.model.get_row_style(sheet, row)?;
let mut new_value = match &old_value {
Some(value) => value.clone(),
None => Default::default(),
};
match border_area.r#type {
BorderType::All => {
new_value.border.top = Some(border_area.item.clone());
new_value.border.right = Some(border_area.item.clone());
new_value.border.bottom = Some(border_area.item.clone());
new_value.border.left = Some(border_area.item.clone());
}
BorderType::Inner => {
if row != first_row {
new_value.border.top = Some(border_area.item.clone());
}
if row != last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
}
BorderType::Outer => {
if row == first_row {
new_value.border.top = Some(border_area.item.clone());
}
if row == last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
}
BorderType::Top => {
if row == first_row {
new_value.border.top = Some(border_area.item.clone());
}
}
BorderType::Right => {
// noop
}
BorderType::Bottom => {
if row == last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
}
BorderType::Left => {
// noop
}
BorderType::CenterH => {
if row != first_row {
new_value.border.top = Some(border_area.item.clone());
}
if row != last_row {
new_value.border.bottom = Some(border_area.item.clone());
}
}
BorderType::CenterV => {
new_value.border.left = Some(border_area.item.clone());
new_value.border.right = Some(border_area.item.clone());
}
BorderType::None => {
new_value.border.top = None;
new_value.border.right = None;
new_value.border.bottom = None;
new_value.border.left = None;
}
}
// We need to go throw each non-empty cell in the row
let columns: Vec<i32> = self
.model
.workbook
.worksheet(sheet)?
.sheet_data
.get(&row)
.map(|row_data| row_data.keys().copied().collect())
.unwrap_or_default();
for column in columns {
self.update_single_cell_border(
border_area,
(sheet, row, column),
(first_row, 1, last_row, LAST_COLUMN),
&mut diff_list,
)?;
}
self.model.set_row_style(sheet, row, &new_value)?;
diff_list.push(Diff::SetRowStyle {
sheet,
row,
old_value: Box::new(old_value),
new_value: Box::new(new_value),
});
}
// TODO: We need to check the rows above and below. also any non empty cell in the rows above and below.
self.push_diff_list(diff_list);
Ok(())
}
fn set_columns_with_border(
&mut self,
sheet: u32,
first_column: i32,
last_column: i32,
border_area: &BorderArea,
) -> Result<(), String> {
let mut diff_list = Vec::new();
// We need all the rows in the column to update the style
// NB: This is too much, this is all the rows that have values
let data_rows: Vec<i32> = self
.model
.workbook
.worksheet(sheet)?
.sheet_data
.keys()
.copied()
.collect();
let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
for column in first_column..=last_column {
let old_value = self.model.get_column_style(sheet, column)?;
let mut new_value = match &old_value {
Some(value) => value.clone(),
None => Default::default(),
};
match border_area.r#type {
BorderType::All => {
new_value.border.top = Some(border_area.item.clone());
new_value.border.right = Some(border_area.item.clone());
new_value.border.bottom = Some(border_area.item.clone());
new_value.border.left = Some(border_area.item.clone());
}
BorderType::Inner => {
if column != first_column {
new_value.border.left = Some(border_area.item.clone());
}
if column != last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::Outer => {
if column == first_column {
new_value.border.left = Some(border_area.item.clone());
}
if column == last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::Top => {
// noop
}
BorderType::Right => {
if column == last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::Bottom => {
// noop
}
BorderType::Left => {
if column == first_column {
new_value.border.left = Some(border_area.item.clone());
}
}
BorderType::CenterH => {
new_value.border.top = Some(border_area.item.clone());
new_value.border.bottom = Some(border_area.item.clone());
}
BorderType::CenterV => {
if column != first_column {
new_value.border.left = Some(border_area.item.clone());
}
if column != last_column {
new_value.border.right = Some(border_area.item.clone());
}
}
BorderType::None => {
new_value.border.top = None;
new_value.border.right = None;
new_value.border.bottom = None;
new_value.border.left = None;
}
}
// We need to go through each non empty cell in the column
for &row in &data_rows {
if let Some(data_row) = self.model.workbook.worksheet(sheet)?.sheet_data.get(&row) {
if data_row.get(&column).is_some() {
self.update_single_cell_border(
border_area,
(sheet, row, column),
(1, first_column, LAST_ROW, last_column),
&mut diff_list,
)?;
}
}
}
// We also need to overwrite those that have a row style
for row_s in styled_rows.iter() {
let row = row_s.r;
self.update_single_cell_border(
border_area,
(sheet, row, column),
(1, first_column, LAST_ROW, last_column),
&mut diff_list,
)?;
}
self.model.set_column_style(sheet, column, &new_value)?;
diff_list.push(Diff::SetColumnStyle {
sheet,
column,
old_value: Box::new(old_value),
new_value: Box::new(new_value),
});
}
// We need to check the borders of the column to the left and the column to the right
// We also need to check every non-empty cell in the columns to the left and right
self.push_diff_list(diff_list);
Ok(())
}
/// Sets the border in an area of cells.
/// When setting the border we need to check if the adjacent cells have a "heavier" border
/// If that is the case we need to change it
pub fn set_area_with_border(
&mut self,
range: &Area,
border_area: &BorderArea,
) -> Result<(), String> {
let sheet = range.sheet;
let first_row = range.row;
let first_column = range.column;
let last_row = first_row + range.height - 1;
let last_column = first_column + range.width - 1;
if first_row == 1 && last_row == LAST_ROW {
// full columns
self.set_columns_with_border(sheet, first_column, last_column, border_area)?;
return Ok(());
}
if first_column == 1 && last_column == LAST_COLUMN {
// full rows
self.set_rows_with_border(sheet, first_row, last_row, border_area)?;
return Ok(());
}
let mut diff_list = Vec::new();
for row in first_row..=last_row {
for column in first_column..=last_column {
self.update_single_cell_border(
border_area,
(sheet, row, column),
(first_row, first_column, last_row, last_column),
&mut diff_list,
)?;
}
}
// bottom of the cells above the first
if first_row > 1
&& [
BorderType::All,
BorderType::None,
BorderType::Outer,
BorderType::Top,
]
.contains(&border_area.r#type)
{
let row = first_row - 1;
for column in first_column..=last_column {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
if is_max_border(Some(&border_area.item), old_value.border.bottom.as_ref()) {
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.bottom = None;
} else {
style.border.bottom = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(Some(old_value)),
new_value: Box::new(style),
});
}
}
}
// Cells to the right
if last_column < LAST_COLUMN
&& [
BorderType::All,
BorderType::None,
BorderType::Outer,
BorderType::Right,
]
.contains(&border_area.r#type)
{
let column = last_column + 1;
for row in first_row..=last_row {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
// If the border in the adjacent cell is "heavier" we change it
if is_max_border(Some(&border_area.item), old_value.border.left.as_ref()) {
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.left = None;
} else {
style.border.left = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(Some(old_value)),
new_value: Box::new(style),
});
}
}
}
// Cells bellow
if last_row < LAST_ROW
&& [
BorderType::All,
BorderType::None,
BorderType::Outer,
BorderType::Bottom,
]
.contains(&border_area.r#type)
{
let row = last_row + 1;
for column in first_column..=last_column {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
if is_max_border(Some(&border_area.item), old_value.border.top.as_ref()) {
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.top = None;
} else {
style.border.top = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(Some(old_value)),
new_value: Box::new(style),
});
}
}
}
// Cells to the left
if first_column > 1
&& [
BorderType::All,
BorderType::None,
BorderType::Outer,
BorderType::Left,
]
.contains(&border_area.r#type)
{
let column = first_column - 1;
for row in first_row..=last_row {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
if is_max_border(Some(&border_area.item), old_value.border.right.as_ref()) {
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.right = None;
} else {
style.border.right = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(Some(old_value)),
new_value: Box::new(style),
});
}
}
}
self.push_diff_list(diff_list);
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use bitcode::{Decode, Encode};
use crate::types::{Cell, Col, Row, SheetState, Style};
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
#[derive(Clone, Encode, Decode)]
pub(crate) struct RowData {
@@ -39,11 +39,17 @@ pub(crate) enum Diff {
old_value: Box<Option<Cell>>,
old_style: Box<Style>,
},
CellClearFormatting {
sheet: u32,
row: i32,
column: i32,
old_style: Box<Option<Style>>,
},
SetCellStyle {
sheet: u32,
row: i32,
column: i32,
old_value: Box<Style>,
old_value: Box<Option<Style>>,
new_value: Box<Style>,
},
// Column and Row diffs
@@ -59,6 +65,28 @@ pub(crate) enum Diff {
new_value: f64,
old_value: f64,
},
SetColumnStyle {
sheet: u32,
column: i32,
old_value: Box<Option<Style>>,
new_value: Box<Style>,
},
SetRowStyle {
sheet: u32,
row: i32,
old_value: Box<Option<Style>>,
new_value: Box<Style>,
},
DeleteColumnStyle {
sheet: u32,
column: i32,
old_value: Box<Option<Style>>,
},
DeleteRowStyle {
sheet: u32,
row: i32,
old_value: Box<Option<Style>>,
},
InsertRow {
sheet: u32,
row: i32,
@@ -77,6 +105,10 @@ pub(crate) enum Diff {
column: i32,
old_data: Box<ColumnData>,
},
DeleteSheet {
sheet: u32,
old_data: Box<Worksheet>,
},
SetFrozenRowsCount {
sheet: u32,
new_value: i32,
@@ -87,9 +119,6 @@ pub(crate) enum Diff {
new_value: i32,
old_value: i32,
},
DeleteSheet {
sheet: u32,
},
NewSheet {
index: u32,
name: String,
@@ -168,11 +197,6 @@ impl History {
None => None,
}
}
pub fn clear(&mut self) {
self.redo_stack = vec![];
self.undo_stack = vec![];
}
}
#[derive(Clone, Encode, Decode)]

View File

@@ -1,5 +1,6 @@
#![deny(missing_docs)]
mod border;
mod border_utils;
mod common;
mod history;

View File

@@ -1,6 +1,6 @@
use std::vec::Vec;
use crate::types::*;
use crate::{expressions::parser::DefinedNameS, types::*};
impl Workbook {
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
pub(crate) 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 defined_names = self

View File

@@ -108,37 +108,120 @@ impl Worksheet {
self.cols = vec![Col {
min: 1,
max: constants::LAST_COLUMN,
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
custom_width: true,
width: constants::DEFAULT_COLUMN_WIDTH,
custom_width: false,
style: Some(style_index),
}];
Ok(())
}
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
let width = constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR;
let width = self
.get_column_width(column)
.unwrap_or(constants::DEFAULT_COLUMN_WIDTH);
self.set_column_width_and_style(column, width, Some(style_index))
}
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
// FIXME: This is a HACK
let custom_format = style_index != 0;
for r in self.rows.iter_mut() {
if r.r == row {
r.s = style_index;
r.custom_format = true;
r.custom_format = custom_format;
return Ok(());
}
}
self.rows.push(Row {
height: constants::DEFAULT_ROW_HEIGHT / constants::ROW_HEIGHT_FACTOR,
r: row,
custom_format: true,
custom_height: true,
custom_format,
custom_height: false,
s: style_index,
hidden: false,
});
Ok(())
}
pub fn delete_row_style(&mut self, row: i32) -> Result<(), String> {
let mut index = None;
for (i, r) in self.rows.iter().enumerate() {
if r.r == row {
index = Some(i);
break;
}
}
if let Some(i) = index {
if let Some(r) = self.rows.get_mut(i) {
r.s = 0;
r.custom_format = false;
}
}
Ok(())
}
pub fn delete_column_style(&mut self, column: i32) -> Result<(), String> {
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
let cols = &mut self.cols;
let mut index = 0;
let mut split = false;
for c in cols.iter_mut() {
let min = c.min;
let max = c.max;
if min <= column && column <= max {
//
split = true;
break;
}
if column < min {
// We passed, there is nothing to delete
break;
}
index += 1;
}
if split {
let min = cols[index].min;
let max = cols[index].max;
let custom_width = cols[index].custom_width;
let width = cols[index].width;
let pre = Col {
min,
max: column - 1,
width,
custom_width,
style: cols[index].style,
};
let col = Col {
min: column,
max: column,
width,
custom_width,
style: None,
};
let post = Col {
min: column + 1,
max,
width,
custom_width,
style: cols[index].style,
};
cols.remove(index);
if column != max {
cols.insert(index, post);
}
if custom_width {
cols.insert(index, col);
}
if column != min {
cols.insert(index, pre);
}
}
Ok(())
}
pub fn set_cell_style(
&mut self,
row: i32,
@@ -285,11 +368,12 @@ impl Worksheet {
/// Changes the width of a column.
/// * If the column does not a have a width we simply add it
/// * If it has, it might be part of a range and we ned to split the range.
/// * If it has, it might be part of a range and we need to split the range.
///
/// Fails if column index is outside allowed range or width is negative.
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
self.set_column_width_and_style(column, width, None)
let style = self.get_column_style(column)?;
self.set_column_width_and_style(column, width, style)
}
pub(crate) fn set_column_width_and_style(
@@ -309,7 +393,7 @@ impl Worksheet {
min: column,
max: column,
width: width / constants::COLUMN_WIDTH_FACTOR,
custom_width: true,
custom_width: width != constants::DEFAULT_COLUMN_WIDTH,
style,
};
let mut index = 0;
@@ -319,7 +403,9 @@ impl Worksheet {
let max = c.max;
if min <= column && column <= max {
if min == column && max == column {
c.style = style;
c.width = width / constants::COLUMN_WIDTH_FACTOR;
c.custom_width = width != constants::DEFAULT_COLUMN_WIDTH;
return Ok(());
}
split = true;
@@ -383,6 +469,23 @@ impl Worksheet {
Ok(constants::DEFAULT_COLUMN_WIDTH)
}
/// Returns the column style index if present
pub fn get_column_style(&self, column: i32) -> Result<Option<i32>, String> {
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
let cols = &self.cols;
for col in cols {
let min = col.min;
let max = col.max;
if column >= min && column <= max {
return Ok(col.style);
}
}
Ok(None)
}
// Returns non empty cells in a column
pub fn column_cell_references(&self, column: i32) -> Result<Vec<CellReferenceIndex>, String> {
let mut column_cell_references: Vec<CellReferenceIndex> = Vec::new();

View File

@@ -0,0 +1,5 @@
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
rustflags = ["-C", "target-feature=-crt-static"]
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]

197
bindings/nodejs/.gitignore vendored Normal file
View File

@@ -0,0 +1,197 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# End of https://www.toptal.com/developers/gitignore/api/node
# Created by https://www.toptal.com/developers/gitignore/api/macos
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
# End of https://www.toptal.com/developers/gitignore/api/macos
# Created by https://www.toptal.com/developers/gitignore/api/windows
# Edit at https://www.toptal.com/developers/gitignore?templates=windows
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows
#Added by cargo
/target
Cargo.lock
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
*.node

View File

@@ -0,0 +1,13 @@
target
Cargo.lock
.cargo
.github
npm
.eslintrc
.prettierignore
rustfmt.toml
yarn.lock
*.node
.yarn
__test__
renovate.json

View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -0,0 +1,18 @@
[package]
edition = "2021"
name = "ironcalc_nodejs"
version = "0.5.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
# 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-derive = "2.12.2"
ironcalc = { path = "../../xlsx", version = "0.5.0" }
serde = { version = "1.0", features = ["derive"] }
[build-dependencies]
napi-build = "2.0.1"

18
bindings/nodejs/README.md Normal file
View File

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

View File

@@ -0,0 +1,20 @@
import test from 'ava'
import { UserModel, Model } from '../index.js';
test('User Model smoke test', (t) => {
const model = new UserModel("Workbook1", "en", "UTC");
model.setUserInput(0, 1, 1, "=1+1");
t.is(model.getFormattedCellValue(0, 1, 1), '2');
});
test('Raw API smoke test', (t) => {
const model = new Model("Workbook1", "en", "UTC");
model.setUserInput(0, 1, 1, "=1+1");
model.evaluate();
t.is(model.getFormattedCellValue(0, 1, 1), '2');
});

5
bindings/nodejs/build.rs Normal file
View File

@@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

Binary file not shown.

115
bindings/nodejs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,115 @@
/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export declare class Model {
constructor(name: string, locale: string, timezone: string)
static fromXlsx(filePath: string, locale: string, tz: string): Model
static fromIcalc(fileName: string): Model
saveToXlsx(file: string): void
saveToIcalc(file: string): void
evaluate(): void
setUserInput(sheet: number, row: number, column: number, value: string): void
clearCellContents(sheet: number, row: number, column: number): void
getCellContent(sheet: number, row: number, column: number): string
getCellType(sheet: number, row: number, column: number): number
getFormattedCellValue(sheet: number, row: number, column: number): string
setCellStyle(sheet: number, row: number, column: number, style: unknown): void
getCellStyle(sheet: number, row: number, column: number): unknown
insertRows(sheet: number, row: number, rowCount: number): void
insertColumns(sheet: number, column: number, columnCount: number): void
deleteRows(sheet: number, row: number, rowCount: number): void
deleteColumns(sheet: number, column: number, columnCount: number): void
getColumnWidth(sheet: number, column: number): number
getRowHeight(sheet: number, row: number): number
setColumnWidth(sheet: number, column: number, width: number): void
setRowHeight(sheet: number, row: number, height: number): void
getFrozenColumnsCount(sheet: number): number
getFrozenRowsCount(sheet: number): number
setFrozenColumnsCount(sheet: number, columnCount: number): void
setFrozenRowsCount(sheet: number, rowCount: number): void
getWorksheetsProperties(): unknown
setSheetColor(sheet: number, color: string): void
addSheet(sheetName: string): void
newSheet(): void
deleteSheet(sheet: number): void
renameSheet(sheet: number, newName: string): void
getDefinedNameList(): unknown
newDefinedName(name: string, scope: number | undefined | null, formula: string): void
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
deleteDefinedName(name: string, scope?: number | undefined | null): void
testPanic(): void
}
export declare class UserModel {
constructor(name: string, locale: string, timezone: string)
static fromBytes(bytes: Uint8Array): UserModel
canUndo(): boolean
canRedo(): boolean
pauseEvaluation(): void
resumeEvaluation(): void
flushSendQueue(): Array<number>
applyExternalDiffs(diffs: Uint8Array): void
getCellContent(sheet: number, row: number, column: number): string
newSheet(): void
deleteSheet(sheet: number): void
hideSheet(sheet: number): void
unhideSheet(sheet: number): void
renameSheet(sheet: number, name: string): void
setSheetColor(sheet: number, color: string): 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
insertRow(sheet: number, row: number): void
insertColumn(sheet: number, column: number): void
deleteRow(sheet: number, row: number): void
deleteColumn(sheet: number, column: number): void
setRowHeight(sheet: number, row: number, height: number): void
setColumnWidth(sheet: number, column: number, width: number): void
getRowHeight(sheet: number, row: number): number
getColumnWidth(sheet: number, column: number): number
setUserInput(sheet: number, row: number, column: number, input: string): void
getFormattedCellValue(sheet: number, row: number, column: number): string
getFrozenRowsCount(sheet: number): number
getFrozenColumnsCount(sheet: number): number
setFrozenRowsCount(sheet: number, count: number): void
setFrozenColumnsCount(sheet: number, count: number): void
updateRangeStyle(range: unknown, stylePath: string, value: string): void
getCellStyle(sheet: number, row: number, column: number): unknown
onPasteStyles(styles: unknown): void
getCellType(sheet: number, row: number, column: number): number
getWorksheetsProperties(): unknown
getSelectedSheet(): number
getSelectedCell(): Array<number>
getSelectedView(): unknown
setSelectedSheet(sheet: number): void
setSelectedCell(row: number, column: number): void
setSelectedRange(startRow: number, startColumn: number, endRow: number, endColumn: number): void
setTopLeftVisibleCell(topRow: number, topColumn: number): void
setShowGridLines(sheet: number, showGridLines: boolean): void
getShowGridLines(sheet: number): boolean
autoFillRows(sourceArea: unknown, toRow: number): void
autoFillColumns(sourceArea: unknown, toColumn: number): void
onArrowRight(): void
onArrowLeft(): void
onArrowUp(): void
onArrowDown(): void
onPageDown(): void
onPageUp(): void
setWindowWidth(windowWidth: number): void
setWindowHeight(windowHeight: number): void
getScrollX(): number
getScrollY(): number
onExpandSelectedRange(key: string): void
onAreaSelecting(targetRow: number, targetColumn: number): void
setAreaWithBorder(area: unknown, borderArea: unknown): void
toBytes(): Array<number>
getName(): string
setName(name: string): void
copyToClipboard(): unknown
pasteFromClipboard(sourceSheet: number, sourceRange: unknown, clipboard: unknown, isCut: boolean): void
pasteCsvText(area: unknown, csv: string): void
getDefinedNameList(): unknown
newDefinedName(name: string, scope: number | undefined | null, formula: string): void
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
deleteDefinedName(name: string, scope?: number | undefined | null): void
}

316
bindings/nodejs/index.js Normal file
View File

@@ -0,0 +1,316 @@
/* tslint:disable */
/* eslint-disable */
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
let nativeBinding = null
let localFileExisted = false
let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'nodejs.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.android-arm64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'nodejs.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.android-arm-eabi.node')
} else {
nativeBinding = require('@ironcalc/nodejs-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-x64-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-ia32-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-arm64-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'nodejs.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-universal.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'nodejs.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-x64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-arm64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, 'nodejs.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.freebsd-x64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-x64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-x64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm-musleabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm-musleabihf.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm-musleabihf')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-riscv64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-riscv64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-s390x-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-s390x-gnu')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(`Failed to load native binding`)
}
const { Model, UserModel } = nativeBinding
module.exports.Model = Model
module.exports.UserModel = UserModel

28
bindings/nodejs/main.mjs Normal file
View File

@@ -0,0 +1,28 @@
import { UserModel, Model } from './index.js'
function testUserModel() {
const model = new UserModel("Workbook1", "en", "UTC");
model.setUserInput(0, 1, 1, "=1+1");
let t = model.getFormattedCellValue(0, 1, 1);
console.log('From native', t);
let t2 = model.getCellStyle(0, 1, 1);
console.log('From native', t2);
}
function testModel() {
const model = Model.fromXlsx("example.xlsx", "en", "UTC");
const style = model.getCellStyle(0, 1, 6);
console.log(style);
const quantum = model.getFormattedCellValue(0, 14, 4);
console.log(quantum);
}
testUserModel();
testModel();

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-android-arm-eabi`
This is the **armv7-linux-androideabi** binary for `@ironcalc/nodejs`

View File

@@ -0,0 +1,18 @@
{
"name": "@ironcalc/nodejs-android-arm-eabi",
"version": "0.2.0",
"os": [
"android"
],
"cpu": [
"arm"
],
"main": "nodejs.android-arm-eabi.node",
"files": [
"nodejs.android-arm-eabi.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
}
}

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-android-arm64`
This is the **aarch64-linux-android** binary for `@ironcalc/nodejs`

View File

@@ -0,0 +1,18 @@
{
"name": "@ironcalc/nodejs-android-arm64",
"version": "0.2.0",
"os": [
"android"
],
"cpu": [
"arm64"
],
"main": "nodejs.android-arm64.node",
"files": [
"nodejs.android-arm64.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
}
}

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