Compare commits

...

95 Commits

Author SHA1 Message Date
Nicolás Hatcher
610b899f66 UPDATE: Add raw API to nodejs bindings 2025-01-28 20:26:39 +01:00
Nicolás Hatcher Andrés
24fb87721f Update npm.yml 2025-01-28 08:09:17 +01:00
Nicolás Hatcher
d3bc8b135c FIX: Try a boolean argument for the CI instead 2025-01-28 08:09:17 +01:00
Daniel González-Albo
0f6d311de2 Merge pull request #252 from ironcalc/fix/dani-delete-sheet
UPDATE: Deleting a sheet prompts a confirmation dialog
2025-01-28 01:11:46 +01:00
Daniel
0c15ae194d Fix: simplified code by having only 1 dynamic message 2025-01-26 19:00:00 +01:00
Nicolás Hatcher
5d429b1660 UPDATE: Add node bindings 2025-01-26 11:01:42 +01:00
Daniel
20c4a596bf UPDATE: Localization 2025-01-21 01:21:03 +01:00
Daniel
f07a69260f UPDATE: Deleting a sheet prompts a confirmation dialog 2025-01-21 01:20:42 +01:00
Nicolás Hatcher
ec4e7b1ca3 FIX: Fix ERF.PRECISE and ERFC.PRECISE links in documentation 2025-01-20 17:22:33 +01:00
Steve Fanning
81d25b6ec9 Add ERFC, ERF.PRECISE and ERFC.PRECISE functions 2025-01-20 17:22:33 +01:00
Nicolás Hatcher
3a001d96b8 FIX: Can cut and paste in Custom Format Dialog
Fixes #240

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

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

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

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

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

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

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

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

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

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

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

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

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

You at least need the sheet in which you are parsing
For instance toknow if a defined name is local
2024-12-26 10:21:21 +01:00
Nicolás Hatcher
86213a8434 FIX: Add test for get_defined_name_list
Also uses the scope instead of the opaque sheet_id
2024-12-26 10:21:21 +01:00
Nicolás Hatcher
2ed5fb9bbc FIX: Adds some validation and tests 2024-12-26 10:21:21 +01:00
Nicolás Hatcher
e455ed14ea UPDATE: API for defined names 2024-12-26 10:21:21 +01:00
Daniel
ad2efad3ae FIX: Remove tabIndex and onKeyDown as they were not used 2024-12-25 20:04:48 +01:00
Daniel
40461b897b FIX: Adds crossRef back to UploadFileDialog 2024-12-25 20:04:48 +01:00
Daniel
2e7410552f FIX: Apply useTranslation on dialog tooltips 2024-12-25 20:04:48 +01:00
Daniel
095002710b Fix: Removed unnecessary ref 2024-12-25 20:04:48 +01:00
Daniel
8ba131011e FIX: Replace SVG "X" icons with Lucide icons for consistency in dialogs 2024-12-25 20:04:48 +01:00
Nicolás Hatcher Andrés
dbddc027fb Update test-coverage.yaml 2024-12-25 16:57:54 +01:00
Steve Fanning
de997f38f5 Minor updates to Error Types and Value Types documentation 2024-12-25 13:47:21 +01:00
Steve Fanning
df4b4ca353 Update to FV description and associated pages in the Features area. 2024-12-25 13:47:21 +01:00
Daniel
3b944cd659 FIX: Use the right icon for paste format action 2024-12-20 20:13:25 +01:00
Nicolás Hatcher Andrés
d1f2b2acdd Update test-coverage.yaml 2024-12-20 14:10:02 +01:00
Daniel González-Albo
36f915b193 Merge pull request #207 from ironcalc/feature/nicolas-hidden
UPDATE: Hide/Unhide sheets
2024-12-20 10:14:09 +01:00
Nicolás Hatcher
5d8e6255a3 UPDATE: Hide/Unhide sheets 2024-12-20 00:49:33 +01:00
Daniel González-Albo
73f3c06203 Merge pull request #205 from ironcalc/bugfix/nico-dialog-fixes
FIX[WebApp]: Some fixes to the DeleteWorkbook and Import dialogs
2024-12-20 00:16:42 +01:00
Nicolás Hatcher
13b1157c61 FIX[WebApp]: Some fixes to the DeleteWorkbook and Import dialogs 2024-12-19 21:14:05 +01:00
275 changed files with 22634 additions and 3658 deletions

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

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

View File

@@ -17,8 +17,9 @@ jobs:
- name: Generate code coverage
run: cargo llvm-cov --all-features --workspace --exclude pyroncalc --exclude wasm --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: true

3
.gitignore vendored
View File

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

View File

@@ -7,6 +7,7 @@
- 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))
### Fixed

145
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.3.0"
dependencies = [
"bitcode",
"chrono",
@@ -405,7 +430,7 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"bitcode",
"chrono",
@@ -421,6 +446,17 @@ dependencies = [
"serde_json",
]
[[package]]
name = "ironcalc_nodejs"
version = "0.3.1"
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,9 +721,9 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.22.3"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
dependencies = [
"cfg-if",
"indoc",
@@ -634,9 +739,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.22.3"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
dependencies = [
"once_cell",
"target-lexicon",
@@ -644,9 +749,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.22.3"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
dependencies = [
"libc",
"pyo3-build-config",
@@ -654,9 +759,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.22.3"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -666,9 +771,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.22.3"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
dependencies = [
"heck",
"proc-macro2",
@@ -679,7 +784,7 @@ dependencies = [
[[package]]
name = "pyroncalc"
version = "0.1.2"
version = "0.3.0"
dependencies = [
"ironcalc",
"pyo3",
@@ -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"
@@ -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.3.0"
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

@@ -1,11 +1,11 @@
[package]
name = "ironcalc_base"
version = "0.2.0"
version = "0.3.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

@@ -2,6 +2,7 @@ use std::cmp::Ordering;
use crate::expressions::{token::Error, types::CellReferenceIndex};
#[derive(Clone)]
pub struct Range {
pub left: CellReferenceIndex,
pub right: CellReferenceIndex,

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

@@ -164,7 +164,9 @@ pub enum Node {
args: Vec<Node>,
},
ArrayKind(Vec<Node>),
VariableKind(String),
DefinedNameKind((String, Option<u32>)),
TableNameKind(String),
WrongVariableKind(String),
CompareKind {
kind: OpCompare,
left: Box<Node>,
@@ -187,12 +189,17 @@ pub enum Node {
pub struct Parser {
lexer: lexer::Lexer,
worksheets: Vec<String>,
context: Option<CellReferenceRC>,
defined_names: Vec<(String, Option<u32>)>,
context: CellReferenceRC,
tables: HashMap<String, Table>,
}
impl Parser {
pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser {
pub fn new(
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new(
"",
lexer::LexerMode::A1,
@@ -201,10 +208,16 @@ impl Parser {
#[allow(clippy::expect_used)]
get_language("en").expect(""),
);
let context = CellReferenceRC {
sheet: worksheets.first().map_or("", |v| v).to_string(),
column: 1,
row: 1,
};
Parser {
lexer,
worksheets,
context: None,
defined_names,
context,
tables,
}
}
@@ -212,13 +225,18 @@ impl Parser {
self.lexer.set_lexer_mode(mode)
}
pub fn set_worksheets(&mut self, worksheets: Vec<String>) {
pub fn set_worksheets_and_names(
&mut self,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
) {
self.worksheets = worksheets;
self.defined_names = defined_names;
}
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
pub fn parse(&mut self, formula: &str, context: &CellReferenceRC) -> Node {
self.lexer.set_formula(formula);
self.context.clone_from(context);
self.context = context.clone();
self.parse_expr()
}
@@ -232,6 +250,24 @@ impl Parser {
None
}
// 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(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 {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope);
}
}
for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some(None);
}
}
None
}
fn parse_expr(&mut self) -> Node {
let mut t = self.parse_concat();
if let Node::ParseErrorKind { .. } = t {
@@ -446,16 +482,7 @@ impl Parser {
absolute_column,
absolute_row,
} => {
let context = match &self.context {
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let context = &self.context;
let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet),
@@ -490,16 +517,7 @@ impl Parser {
}
}
TokenType::Range { sheet, left, right } => {
let context = match &self.context {
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let context = &self.context;
let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet),
@@ -585,11 +603,33 @@ impl Parser {
kind: function_kind,
args,
};
} else {
return Node::InvalidFunctionKind { name, args };
}
return Node::InvalidFunctionKind { name, args };
}
let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
// 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));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
if table_name.to_lowercase() == name_lower {
return Node::TableNameKind(name);
}
}
Node::VariableKind(name)
Node::WrongVariableKind(name)
}
TokenType::Error(kind) => Node::ErrorKind(kind),
TokenType::Illegal(error) => Node::ParseErrorKind {
@@ -602,7 +642,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 {
@@ -661,187 +732,177 @@ impl Parser {
// We will try to convert to a normal reference
// table_name[column_name] => cell1:cell2
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
if let Some(context) = &self.context {
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
// table-name => table
let table = match self.tables.get(&table_name) {
Some(t) => t,
None => {
let message = format!(
"Table not found: '{table_name}' at '{}!{}{}'",
context.sheet,
number_to_column(context.column)
.unwrap_or(format!("{}", context.column)),
context.row
);
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message,
};
}
};
let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
// table-name => table
let table = match self.tables.get(&table_name) {
Some(t) => t,
None => {
let message = format!(
"Table not found: '{table_name}' at '{}!{}{}'",
context.sheet,
number_to_column(context.column)
.unwrap_or(format!("{}", context.column)),
context.row
);
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message,
};
}
};
let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
let sheet_name = if table_sheet_index == context_sheet_index {
None
} else {
Some(table.sheet_name.clone())
};
let sheet_name = if table_sheet_index == context_sheet_index {
None
} else {
Some(table.sheet_name.clone())
};
// context must be with tables.reference
#[allow(clippy::expect_used)]
let (column_start, mut row_start, column_end, mut row_end) =
parse_range(&table.reference).expect("Failed parsing range");
// context must be with tables.reference
#[allow(clippy::expect_used)]
let (column_start, mut row_start, column_end, mut row_end) =
parse_range(&table.reference).expect("Failed parsing range");
let totals_row_count = table.totals_row_count as i32;
let header_row_count = table.header_row_count as i32;
row_end -= totals_row_count;
let totals_row_count = table.totals_row_count as i32;
let header_row_count = table.header_row_count as i32;
row_end -= totals_row_count;
match specifier {
Some(token::TableSpecifier::ThisRow) => {
row_start = context.row;
row_end = context.row;
}
Some(token::TableSpecifier::Totals) => {
if totals_row_count != 0 {
row_start = row_end + 1;
row_end = row_start;
} else {
// Table1[#Totals] is #REF! if Table1 does not have totals
return Node::ErrorKind(token::Error::REF);
}
}
Some(token::TableSpecifier::Headers) => {
match specifier {
Some(token::TableSpecifier::ThisRow) => {
row_start = context.row;
row_end = context.row;
}
Some(token::TableSpecifier::Totals) => {
if totals_row_count != 0 {
row_start = row_end + 1;
row_end = row_start;
}
Some(token::TableSpecifier::Data) => {
row_start += header_row_count;
}
Some(token::TableSpecifier::All) => {
if totals_row_count != 0 {
row_end += 1;
}
}
None => {
// skip the headers
row_start += header_row_count;
} else {
// Table1[#Totals] is #REF! if Table1 does not have totals
return Node::ErrorKind(token::Error::REF);
}
}
match table_reference {
None => {
return Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_start,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_end,
};
}
Some(TableReference::ColumnReference(s)) => {
let column_index = match get_table_column_by_name(&s, table) {
Some(s) => s + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {s} in table {table_name}"
),
};
}
};
if row_start == row_end {
return Node::ReferenceKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row: true,
absolute_column: true,
row: row_start,
column: column_index,
};
}
return Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_index,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_index,
};
}
Some(TableReference::RangeReference((left, right))) => {
let left_column_index = match get_table_column_by_name(&left, table) {
Some(f) => f + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {left} in table {table_name}"
),
};
}
};
let right_column_index = match get_table_column_by_name(&right, table) {
Some(f) => f + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {right} in table {table_name}"
),
};
}
};
return Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: left_column_index,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: right_column_index,
};
Some(token::TableSpecifier::Headers) => {
row_end = row_start;
}
Some(token::TableSpecifier::Data) => {
row_start += header_row_count;
}
Some(token::TableSpecifier::All) => {
if totals_row_count != 0 {
row_end += 1;
}
}
None => {
// skip the headers
row_start += header_row_count;
}
}
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Structured references not supported in R1C1 mode".to_string(),
match table_reference {
None => Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_start,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_end,
},
Some(TableReference::ColumnReference(s)) => {
let column_index = match get_table_column_by_name(&s, table) {
Some(s) => s + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!("Expecting column: {s} in table {table_name}"),
};
}
};
if row_start == row_end {
return Node::ReferenceKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row: true,
absolute_column: true,
row: row_start,
column: column_index,
};
}
Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_index,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_index,
}
}
Some(TableReference::RangeReference((left, right))) => {
let left_column_index = match get_table_column_by_name(&left, table) {
Some(f) => f + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {left} in table {table_name}"
),
};
}
};
let right_column_index = match get_table_column_by_name(&right, table) {
Some(f) => f + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {right} in table {table_name}"
),
};
}
};
Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: left_column_index,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: right_column_index,
}
}
}
}
}

View File

@@ -375,7 +375,9 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
}
format!("{{{}}}", arguments)
}
VariableKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!(
"{}{}{}",
to_string_moved(left, move_context),

View File

@@ -464,7 +464,9 @@ fn stringify(
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| VariableKind(_)
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name)
}
@@ -492,7 +494,9 @@ fn stringify(
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| VariableKind(_)
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name)
}
@@ -543,7 +547,9 @@ fn stringify(
}
format!("{{{}}}", arguments)
}
VariableKind(value) => value.to_string(),
TableNameKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind {
OpUnary::Minus => {
format!(
@@ -660,7 +666,90 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {}
Node::VariableKind(_) => {}
Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::EmptyArgKind => {}
}
}
pub(crate) fn rename_defined_name_in_node(
node: &mut Node,
name: &str,
scope: Option<u32>,
new_name: &str,
) {
match node {
// Rename
Node::DefinedNameKind((n, s)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string();
}
}
// Go next level
Node::OpRangeKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpConcatenateKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpPowerKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {}
Node::EmptyArgKind => {}
Node::ReferenceKind { .. } => {}
Node::RangeKind { .. } => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
}
}

View File

@@ -17,7 +17,7 @@ struct Formula<'a> {
#[test]
fn test_parser_reference() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -25,14 +25,14 @@ fn test_parser_reference() {
row: 1,
column: 1,
};
let t = parser.parse("A2", &Some(cell_reference));
let t = parser.parse("A2", &cell_reference);
assert_eq!(to_rc_format(&t), "R[1]C[0]");
}
#[test]
fn test_parser_absolute_column() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -40,14 +40,14 @@ fn test_parser_absolute_column() {
row: 1,
column: 1,
};
let t = parser.parse("$A1", &Some(cell_reference));
let t = parser.parse("$A1", &cell_reference);
assert_eq!(to_rc_format(&t), "R[0]C1");
}
#[test]
fn test_parser_absolute_row_col() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -55,14 +55,14 @@ fn test_parser_absolute_row_col() {
row: 1,
column: 1,
};
let t = parser.parse("$C$5", &Some(cell_reference));
let t = parser.parse("$C$5", &cell_reference);
assert_eq!(to_rc_format(&t), "R5C3");
}
#[test]
fn test_parser_absolute_row_col_1() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -70,14 +70,14 @@ fn test_parser_absolute_row_col_1() {
row: 1,
column: 1,
};
let t = parser.parse("$A$1", &Some(cell_reference));
let t = parser.parse("$A$1", &cell_reference);
assert_eq!(to_rc_format(&t), "R1C1");
}
#[test]
fn test_parser_simple_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -86,14 +86,14 @@ fn test_parser_simple_formula() {
column: 1,
};
let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference));
let t = parser.parse("C3+Sheet2!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
}
#[test]
fn test_parser_boolean() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -102,14 +102,14 @@ fn test_parser_boolean() {
column: 1,
};
let t = parser.parse("true", &Some(cell_reference));
let t = parser.parse("true", &cell_reference);
assert_eq!(to_rc_format(&t), "TRUE");
}
#[test]
fn test_parser_bad_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -117,7 +117,7 @@ fn test_parser_bad_formula() {
row: 1,
column: 1,
};
let t = parser.parse("#Value", &Some(cell_reference));
let t = parser.parse("#Value", &cell_reference);
match &t {
Node::ParseErrorKind {
formula,
@@ -138,7 +138,7 @@ fn test_parser_bad_formula() {
#[test]
fn test_parser_bad_formula_1() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -146,7 +146,7 @@ fn test_parser_bad_formula_1() {
row: 1,
column: 1,
};
let t = parser.parse("<5", &Some(cell_reference));
let t = parser.parse("<5", &cell_reference);
match &t {
Node::ParseErrorKind {
formula,
@@ -167,7 +167,7 @@ fn test_parser_bad_formula_1() {
#[test]
fn test_parser_bad_formula_2() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -175,7 +175,7 @@ fn test_parser_bad_formula_2() {
row: 1,
column: 1,
};
let t = parser.parse("*5", &Some(cell_reference));
let t = parser.parse("*5", &cell_reference);
match &t {
Node::ParseErrorKind {
formula,
@@ -196,7 +196,7 @@ fn test_parser_bad_formula_2() {
#[test]
fn test_parser_bad_formula_3() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -204,7 +204,7 @@ fn test_parser_bad_formula_3() {
row: 1,
column: 1,
};
let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference));
let t = parser.parse("SUM(#VALVE!)", &cell_reference);
match &t {
Node::ParseErrorKind {
formula,
@@ -225,7 +225,7 @@ fn test_parser_bad_formula_3() {
#[test]
fn test_parser_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let formulas = vec![
Formula {
@@ -259,11 +259,11 @@ fn test_parser_formulas() {
for formula in formulas {
let t = parser.parse(
formula.initial,
&Some(CellReferenceRC {
&CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
}),
},
);
assert_eq!(to_rc_format(&t), formula.expected);
assert_eq!(to_string(&t, &cell_reference), formula.initial);
@@ -273,7 +273,7 @@ fn test_parser_formulas() {
#[test]
fn test_parser_r1c1_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
parser.set_lexer_mode(LexerMode::R1C1);
let formulas = vec![
@@ -324,11 +324,11 @@ fn test_parser_r1c1_formulas() {
for formula in formulas {
let t = parser.parse(
formula.initial,
&Some(CellReferenceRC {
&CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
}),
},
);
assert_eq!(to_string(&t, &cell_reference), formula.expected);
assert_eq!(to_rc_format(&t), formula.initial);
@@ -338,7 +338,7 @@ fn test_parser_r1c1_formulas() {
#[test]
fn test_parser_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -347,14 +347,14 @@ fn test_parser_quotes() {
column: 1,
};
let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference));
let t = parser.parse("C3+'Second Sheet'!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
}
#[test]
fn test_parser_escape_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -363,14 +363,14 @@ fn test_parser_escape_quotes() {
column: 1,
};
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference));
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
}
#[test]
fn test_parser_parenthesis() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -379,14 +379,14 @@ fn test_parser_parenthesis() {
column: 1,
};
let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference));
let t = parser.parse("(C3=\"Yes\")*5", &cell_reference);
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
}
#[test]
fn test_parser_excel_xlfn() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -395,7 +395,7 @@ fn test_parser_excel_xlfn() {
column: 1,
};
let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference));
let t = parser.parse("_xlfn.CONCAT(C3)", &cell_reference);
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
}
@@ -407,9 +407,9 @@ fn test_to_string_displaced() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone()));
let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column {
sheet: 0,
column: 1,
@@ -427,9 +427,9 @@ fn test_to_string_displaced_full_ranges() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("SUM(3:3)", &Some(context.clone()));
let node = parser.parse("SUM(3:3)", context);
let displace_data = DisplaceData::Column {
sheet: 0,
column: 1,
@@ -440,7 +440,7 @@ fn test_to_string_displaced_full_ranges() {
"SUM(3:3)".to_string()
);
let node = parser.parse("SUM(D:D)", &Some(context.clone()));
let node = parser.parse("SUM(D:D)", context);
let displace_data = DisplaceData::Row {
sheet: 0,
row: 3,
@@ -460,9 +460,9 @@ fn test_to_string_displaced_too_low() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone()));
let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column {
sheet: 0,
column: 1,
@@ -480,9 +480,9 @@ fn test_to_string_displaced_too_high() {
column: 1,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone()));
let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column {
sheet: 0,
column: 1,

View File

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

View File

@@ -15,7 +15,7 @@ fn test_move_formula() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -27,7 +27,7 @@ fn test_move_formula() {
};
// formula AB31 will not change
let node = parser.parse("AB31", &Some(context.clone()));
let node = parser.parse("AB31", context);
let t = move_formula(
&node,
&MoveContext {
@@ -43,7 +43,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31");
// formula $AB$31 will not change
let node = parser.parse("AB31", &Some(context.clone()));
let node = parser.parse("AB31", context);
let t = move_formula(
&node,
&MoveContext {
@@ -59,7 +59,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31");
// but formula D5 will change to N15 (N = D + 10)
let node = parser.parse("D5", &Some(context.clone()));
let node = parser.parse("D5", context);
let t = move_formula(
&node,
&MoveContext {
@@ -75,7 +75,7 @@ fn test_move_formula() {
assert_eq!(t, "N15");
// Also formula $D$5 will change to N15 (N = D + 10)
let node = parser.parse("$D$5", &Some(context.clone()));
let node = parser.parse("$D$5", context);
let t = move_formula(
&node,
&MoveContext {
@@ -102,7 +102,7 @@ fn test_move_formula_context_offset() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -113,7 +113,7 @@ fn test_move_formula_context_offset() {
height: 5,
};
let node = parser.parse("-X9+C2%", &Some(context.clone()));
let node = parser.parse("-X9+C2%", context);
let t = move_formula(
&node,
&MoveContext {
@@ -140,7 +140,7 @@ fn test_move_formula_area_limits() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -152,7 +152,7 @@ fn test_move_formula_area_limits() {
};
// Outside of the area. Not moved
let node = parser.parse("B2+B3+C1+G6+H5", &Some(context.clone()));
let node = parser.parse("B2+B3+C1+G6+H5", context);
let t = move_formula(
&node,
&MoveContext {
@@ -168,7 +168,7 @@ fn test_move_formula_area_limits() {
assert_eq!(t, "B2+B3+C1+G6+H5");
// In the area. Moved
let node = parser.parse("C2+F4+F5+F6", &Some(context.clone()));
let node = parser.parse("C2+F4+F5+F6", context);
let t = move_formula(
&node,
&MoveContext {
@@ -195,7 +195,7 @@ fn test_move_formula_ranges() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let area = &Area {
sheet: 0,
@@ -205,7 +205,7 @@ fn test_move_formula_ranges() {
height: 5,
};
// Ranges inside the area are fully displaced (absolute or not)
let node = parser.parse("SUM(C2:F5)", &Some(context.clone()));
let node = parser.parse("SUM(C2:F5)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -220,7 +220,7 @@ fn test_move_formula_ranges() {
);
assert_eq!(t, "SUM(M12:P15)");
let node = parser.parse("SUM($C$2:$F$5)", &Some(context.clone()));
let node = parser.parse("SUM($C$2:$F$5)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -236,7 +236,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($M$12:$P$15)");
// Ranges completely outside of the area are not touched
let node = parser.parse("SUM(A1:B3)", &Some(context.clone()));
let node = parser.parse("SUM(A1:B3)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -251,7 +251,7 @@ fn test_move_formula_ranges() {
);
assert_eq!(t, "SUM(A1:B3)");
let node = parser.parse("SUM($A$1:$B$3)", &Some(context.clone()));
let node = parser.parse("SUM($A$1:$B$3)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -267,7 +267,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($A$1:$B$3)");
// Ranges that overlap with the area are also NOT displaced
let node = parser.parse("SUM(A1:F5)", &Some(context.clone()));
let node = parser.parse("SUM(A1:F5)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -283,7 +283,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM(A1:F5)");
// Ranges that contain the area are also NOT displaced
let node = parser.parse("SUM(A1:X50)", &Some(context.clone()));
let node = parser.parse("SUM(A1:X50)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -318,10 +318,10 @@ fn test_move_formula_wrong_reference() {
height: 5,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Wrong formulas will NOT be displaced
let node = parser.parse("Sheet3!AB31", &Some(context.clone()));
let node = parser.parse("Sheet3!AB31", context);
let t = move_formula(
&node,
&MoveContext {
@@ -335,7 +335,7 @@ fn test_move_formula_wrong_reference() {
},
);
assert_eq!(t, "Sheet3!AB31");
let node = parser.parse("Sheet3!$X$9", &Some(context.clone()));
let node = parser.parse("Sheet3!$X$9", context);
let t = move_formula(
&node,
&MoveContext {
@@ -350,7 +350,7 @@ fn test_move_formula_wrong_reference() {
);
assert_eq!(t, "Sheet3!$X$9");
let node = parser.parse("SUM(Sheet3!D2:D3)", &Some(context.clone()));
let node = parser.parse("SUM(Sheet3!D2:D3)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -377,7 +377,7 @@ fn test_move_formula_misc() {
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
width: 4,
height: 5,
};
let node = parser.parse("X9^C2-F4*H2", &Some(context.clone()));
let node = parser.parse("X9^C2-F4*H2", context);
let t = move_formula(
&node,
&MoveContext {
@@ -402,7 +402,7 @@ fn test_move_formula_misc() {
);
assert_eq!(t, "X9^M12-P14*H2");
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone()));
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -417,7 +417,7 @@ fn test_move_formula_misc() {
);
assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)");
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", &Some(context.clone()));
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", context);
let t = move_formula(
&node,
&MoveContext {
@@ -445,7 +445,7 @@ fn test_move_formula_another_sheet() {
};
// we add two sheets and we cut/paste from Sheet1 to Sheet2
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
@@ -457,10 +457,7 @@ fn test_move_formula_another_sheet() {
};
// Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4
let node = parser.parse(
"AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)",
&Some(context.clone()),
);
let node = parser.parse("AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)", context);
let t = move_formula(
&node,
&MoveContext {

View File

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

View File

@@ -9,7 +9,7 @@ use crate::expressions::types::CellReferenceRC;
#[test]
fn exp_order() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
@@ -17,18 +17,18 @@ fn exp_order() {
row: 1,
column: 1,
};
let t = parser.parse("(1 + 2)^3 + 4", &Some(cell_reference.clone()));
let t = parser.parse("(1 + 2)^3 + 4", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4");
let t = parser.parse("(C5 + 3)^R4", &Some(cell_reference.clone()));
let t = parser.parse("(C5 + 3)^R4", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4");
let t = parser.parse("(C5 + 3)^(R4*6)", &Some(cell_reference.clone()));
let t = parser.parse("(C5 + 3)^(R4*6)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)");
let t = parser.parse("(C5)^(R4)", &Some(cell_reference.clone()));
let t = parser.parse("(C5)^(R4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "C5^R4");
let t = parser.parse("(5)^(4)", &Some(cell_reference.clone()));
let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4");
}

View File

@@ -63,7 +63,7 @@ fn simple_table() {
let row_count = 3;
let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
let mut parser = Parser::new(worksheets, tables);
let mut parser = Parser::new(worksheets, vec![], tables);
// Reference cell is 'Sheet One'!F2
let cell_reference = CellReferenceRC {
sheet: "Sheet One".to_string(),
@@ -72,7 +72,7 @@ fn simple_table() {
};
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
let t = parser.parse(formula, &Some(cell_reference.clone()));
let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
// Cell A3
@@ -82,7 +82,7 @@ fn simple_table() {
column: 1,
};
let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &Some(cell_reference.clone()));
let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
// Cell A3 in 'Second Sheet'
@@ -92,7 +92,7 @@ fn simple_table() {
column: 1,
};
let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &Some(cell_reference.clone()));
let t = parser.parse(formula, &cell_reference);
assert_eq!(
to_string(&t, &cell_reference),
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"

View File

@@ -263,7 +263,9 @@ pub(crate) fn forward_references(
// TODO: Not implemented
Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {}
Node::VariableKind(_) => {}
Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {}

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

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

@@ -247,45 +247,67 @@ impl Model {
return CalcResult::Number(cell.sheet as f64 + 1.0);
}
// The arg could be a defined name or a table
let arg = &args[0];
if let Node::VariableKind(name) = arg {
// Let's see if it is a defined name
if let Some(defined_name) = self.parsed_defined_names.get(&(None, name.to_lowercase()))
{
match defined_name {
ParsedDefinedName::CellReference(reference) => {
return CalcResult::Number(reference.sheet as f64 + 1.0)
// let = &args[0];
match &args[0] {
Node::DefinedNameKind((name, scope)) => {
// Let's see if it is a defined name
if let Some(defined_name) = self
.parsed_defined_names
.get(&(*scope, name.to_lowercase()))
{
match defined_name {
ParsedDefinedName::CellReference(reference) => {
return CalcResult::Number(reference.sheet as f64 + 1.0)
}
ParsedDefinedName::RangeReference(range) => {
return CalcResult::Number(range.left.sheet as f64 + 1.0)
}
ParsedDefinedName::InvalidDefinedNameFormula => {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid name".to_string(),
};
}
}
ParsedDefinedName::RangeReference(range) => {
return CalcResult::Number(range.left.sheet as f64 + 1.0)
}
ParsedDefinedName::InvalidDefinedNameFormula => {
return CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Invalid name".to_string(),
};
} else {
// This should never happen
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid name".to_string(),
};
}
}
Node::TableNameKind(name) => {
// Now let's see if it is a table
for (table_name, table) in &self.workbook.tables {
if table_name == name {
if let Some(sheet_index) = self.get_sheet_index_by_name(&table.sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
} else {
break;
}
}
}
}
// Now let's see if it is a table
for (table_name, table) in &self.workbook.tables {
if table_name == name {
if let Some(sheet_index) = self.get_sheet_index_by_name(&table.sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
} else {
break;
}
Node::WrongVariableKind(name) => {
return CalcResult::Error {
error: Error::NAME,
origin: cell,
message: format!("Name not found: {name}"),
}
}
arg => {
// Now it should be the name of a sheet
let sheet_name = match self.get_string(arg, cell) {
Ok(s) => s,
Err(e) => return e,
};
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
}
}
}
// Now it should be the name of a sheet
let sheet_name = match self.get_string(arg, cell) {
Ok(s) => s,
Err(e) => return e,
};
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
}
CalcResult::Error {
error: Error::NA,

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

View File

@@ -949,7 +949,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 +957,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

@@ -25,6 +25,8 @@
#![doc = include_str!("../examples/formulas_and_errors.rs")]
//! ```
#![warn(clippy::print_stdout)]
pub mod calc_result;
pub mod cell;
pub mod expressions;

View File

@@ -8,14 +8,15 @@ use crate::{
cell::CellValue,
constants::{self, LAST_COLUMN, LAST_ROW},
expressions::{
lexer::LexerMode,
parser::{
move_formula::{move_formula, MoveContext},
stringify::{to_rc_format, to_string},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
Node, Parser,
},
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*,
utils::{self, is_valid_column_number, is_valid_row},
utils::{self, is_valid_column_number, is_valid_identifier, is_valid_row},
},
formatter::{
format::{format_number, parse_formatted_number},
@@ -72,6 +73,7 @@ pub(crate) enum CellState {
}
/// A parsed formula for a defined name
#[derive(Clone)]
pub(crate) enum ParsedDefinedName {
/// CellReference (`=C4`)
CellReference(CellReferenceIndex),
@@ -79,9 +81,6 @@ pub(crate) enum ParsedDefinedName {
RangeReference(Range),
/// `=SomethingElse`
InvalidDefinedNameFormula,
// TODO: Support constants in defined names
// TODO: Support formulas in defined names
// TODO: Support tables in defined names
}
/// A dynamical IronCalc model.
@@ -417,38 +416,40 @@ impl Model {
// TODO: NOT IMPLEMENTED
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
}
VariableKind(defined_name) => {
let parsed_defined_name = self
.parsed_defined_names
.get(&(Some(cell.sheet), defined_name.to_lowercase())) // try getting local defined name
.or_else(|| {
self.parsed_defined_names
.get(&(None, defined_name.to_lowercase()))
}); // fallback to global
if let Some(parsed_defined_name) = parsed_defined_name {
DefinedNameKind((name, scope)) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => {
self.evaluate_cell(*reference)
self.evaluate_cell(reference)
}
ParsedDefinedName::RangeReference(range) => CalcResult::Range {
left: range.left,
right: range.right,
},
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NIMPL,
Error::NAME,
cell,
format!("Defined name \"{}\" is not a reference.", defined_name),
format!("Defined name \"{}\" is not a reference.", name),
),
}
} else {
CalcResult::new_error(
Error::NAME,
cell,
format!("Defined name \"{}\" not found.", defined_name),
format!("Defined name \"{}\" not found.", name),
)
}
}
TableNameKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("table name \"{}\" not supported.", s),
),
WrongVariableKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("Variable name \"{}\" not found.", s),
),
CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell);
if l.is_error() {
@@ -682,6 +683,13 @@ impl Model {
Err(format!("Invalid color: {}", color))
}
/// Changes the visibility of a sheet
pub fn set_sheet_state(&mut self, sheet: u32, state: SheetState) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
worksheet.state = state;
Ok(())
}
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
@@ -857,6 +865,11 @@ 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();
// add all tables
// let mut tables = Vec::new();
// for worksheet in worksheets {
@@ -866,7 +879,7 @@ impl Model {
// }
// tables.push(tables_in_sheet);
// }
let parser = Parser::new(worksheet_names, workbook.tables.clone());
let parser = Parser::new(worksheet_names, defined_names, workbook.tables.clone());
let cells = HashMap::new();
let locale = get_locale(&workbook.settings.locale)
.map_err(|_| "Invalid locale".to_string())?
@@ -947,10 +960,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,
@@ -1027,7 +1037,7 @@ impl Model {
column: source.column,
};
let formula_str = move_formula(
&self.parser.parse(formula, &Some(cell_reference)),
&self.parser.parse(formula, &cell_reference),
&MoveContext {
source_sheet_name: &source_sheet_name,
row: source.row,
@@ -1135,7 +1145,7 @@ impl Model {
row: source.row,
column: source.column,
};
let formula = &self.parser.parse(formula_str, &Some(cell_reference));
let formula = &self.parser.parse(formula_str, &cell_reference);
let cell_reference = CellReferenceRC {
sheet: target_sheet_name,
row: target.row,
@@ -1511,13 +1521,11 @@ impl Model {
column,
};
let shared_formulas = &mut worksheet.shared_formulas;
let mut parsed_formula = self.parser.parse(formula, &Some(cell_reference.clone()));
let mut parsed_formula = self.parser.parse(formula, &cell_reference);
// If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self
.parser
.parse(&format!("{})", formula), &Some(cell_reference));
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference);
match new_parsed_formula {
Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula,
@@ -1596,6 +1604,42 @@ impl Model {
.set_cell_with_number(row, column, value, style)
}
// Helper function that returns a defined name given the name and scope
fn get_parsed_defined_name(
&self,
name: &str,
scope: Option<u32>,
) -> Result<Option<ParsedDefinedName>, String> {
let name_upper = name.to_uppercase();
for (key, df) in &self.parsed_defined_names {
if key.1.to_uppercase() == name_upper && key.0 == scope {
return Ok(Some(df.clone()));
}
}
Ok(None)
}
// Returns the formula for a defined name
pub(crate) fn get_defined_name_formula(
&self,
name: &str,
scope: Option<u32>,
) -> Result<String, String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Ok(df.formula.clone());
}
}
Err("Defined name not found".to_string())
}
/// Gets the Excel Value (Bool, Number, String) of a cell
///
/// See also:
@@ -1986,6 +2030,137 @@ impl Model {
.worksheet_mut(sheet)?
.set_row_height(column, height)
}
/// Adds a new defined name
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), String> {
if !is_valid_identifier(name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
// if the defined name already exist return error
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Err("Defined name already exists".to_string());
}
}
self.workbook.defined_names.push(DefinedName {
name: name.to_string(),
formula: formula.to_string(),
sheet_id,
});
self.reset_parsed_structures();
Ok(())
}
/// Delete defined name of name and scope
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let mut index = None;
for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
index = Some(i);
}
}
if let Some(i) = index {
self.workbook.defined_names.remove(i);
self.reset_parsed_structures();
Ok(())
} else {
Err("Defined name not found".to_string())
}
}
/// Update defined name
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), String> {
if !is_valid_identifier(new_name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase();
let new_name_upper = new_name.to_uppercase();
if name_upper != new_name_upper || scope != new_scope {
for key in self.parsed_defined_names.keys() {
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
return Err("Defined name already exists".to_string());
}
}
}
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let new_sheet_id = match new_scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let mut index = None;
for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
index = Some(i);
}
}
if let Some(i) = index {
if let Some(df) = self.workbook.defined_names.get_mut(i) {
if new_name != df.name {
// We need to rename the name in every formula:
// Parse all formulas with the old name
// All internal formulas are R1C1
self.parser.set_lexer_mode(LexerMode::R1C1);
let worksheets = &mut self.workbook.worksheets;
for worksheet in worksheets {
let cell_reference = CellReferenceRC {
sheet: worksheet.get_name(),
row: 1,
column: 1,
};
let mut formulas = Vec::new();
for formula in &worksheet.shared_formulas {
let mut t = self.parser.parse(formula, &cell_reference);
rename_defined_name_in_node(&mut t, name, scope, new_name);
formulas.push(to_rc_format(&t));
}
worksheet.shared_formulas = formulas;
}
// Se the mode back to A1
self.parser.set_lexer_mode(LexerMode::A1);
}
df.name = new_name.to_string();
df.sheet_id = new_sheet_id;
df.formula = new_formula.to_string();
self.reset_parsed_structures();
}
Ok(())
} else {
Err("Defined name not found".to_string())
}
}
}
#[cfg(test)]

View File

@@ -85,14 +85,14 @@ impl Model {
let worksheets = &self.workbook.worksheets;
for worksheet in worksheets {
let shared_formulas = &worksheet.shared_formulas;
let cell_reference = &Some(CellReferenceRC {
let cell_reference = CellReferenceRC {
sheet: worksheet.get_name(),
row: 1,
column: 1,
});
};
let mut parse_formula = Vec::new();
for formula in shared_formulas {
let t = self.parser.parse(formula, cell_reference);
let t = self.parser.parse(formula, &cell_reference);
parse_formula.push(t);
}
self.parsed_formulas.push(parse_formula);
@@ -144,8 +144,14 @@ 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();
self.parser
.set_worksheets(self.workbook.get_worksheet_names());
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
self.parsed_formulas = vec![];
self.parse_formulas();
self.parsed_defined_names = HashMap::new();
@@ -262,11 +268,11 @@ impl Model {
// 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 {
let cell_reference = &Some(CellReferenceRC {
let cell_reference = &CellReferenceRC {
sheet: worksheet.get_name(),
row: 1,
column: 1,
});
};
let mut formulas = Vec::new();
for formula in &worksheet.shared_formulas {
let mut t = self.parser.parse(formula, cell_reference);
@@ -388,7 +394,7 @@ impl Model {
let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets;
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let parser = Parser::new(worksheet_names, HashMap::new());
let parser = Parser::new(worksheet_names, vec![], HashMap::new());
let cells = HashMap::new();
// FIXME: Add support for display languages

View File

@@ -13,12 +13,14 @@ 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;
mod test_fn_if;
mod test_fn_maxifs;
mod test_fn_minifs;
mod test_fn_or_xor;
mod test_fn_product;
mod test_fn_rept;
mod test_fn_sum;
@@ -40,13 +42,13 @@ 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;
mod engineering;
mod test_fn_offset;
mod test_fn_or;
mod test_number_format;
mod test_escape_quotes;

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

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

@@ -1,36 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 0)");
model._set("A2", "=OR(0, 0)");
model._set("A3", "=OR(true, false)");
model._set("A4", "=OR(false, false)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
}
#[test]
fn fn_or_no_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR()");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
#[test]
fn fn_or_missing_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR(,)");
model._set("A2", "=OR(,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
}

View File

@@ -0,0 +1,204 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::print_stdout)]
use crate::test::util::new_empty_model;
// These tests are grouped because in many cases XOR and OR have similar behaviour.
// Test specific to xor
#[test]
fn fn_xor() {
let mut model = new_empty_model();
model._set("A1", "=XOR(1, 1, 1, 0, 0)");
model._set("A2", "=XOR(1, 1, 0, 0, 0)");
model._set("A3", "=XOR(TRUE, TRUE, TRUE, FALSE, FALSE)");
model._set("A4", "=XOR(TRUE, TRUE, FALSE, FALSE, FALSE)");
model._set("A5", "=XOR(FALSE, FALSE, FALSE, FALSE, FALSE)");
model._set("A6", "=XOR(TRUE, TRUE)");
model._set("A7", "=XOR(0,0,0)");
model._set("A8", "=XOR(0,0,1)");
model._set("A9", "=XOR(0,1,0)");
model._set("A10", "=XOR(0,1,1)");
model._set("A11", "=XOR(1,0,0)");
model._set("A12", "=XOR(1,0,1)");
model._set("A13", "=XOR(1,1,0)");
model._set("A14", "=XOR(1,1,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"FALSE");
assert_eq!(model._get_text("A6"), *"FALSE");
assert_eq!(model._get_text("A7"), *"FALSE");
assert_eq!(model._get_text("A8"), *"TRUE");
assert_eq!(model._get_text("A9"), *"TRUE");
assert_eq!(model._get_text("A10"), *"FALSE");
assert_eq!(model._get_text("A11"), *"TRUE");
assert_eq!(model._get_text("A12"), *"FALSE");
assert_eq!(model._get_text("A13"), *"FALSE");
assert_eq!(model._get_text("A14"), *"TRUE");
}
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 1, 1, 0, 0)");
model._set("A2", "=OR(1, 1, 0, 0, 0)");
model._set("A3", "=OR(TRUE, TRUE, TRUE, FALSE, FALSE)");
model._set("A4", "=OR(TRUE, TRUE, FALSE, FALSE, FALSE)");
model._set("A5", "=OR(FALSE, FALSE, FALSE, FALSE, FALSE)");
model._set("A6", "=OR(TRUE, TRUE)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"TRUE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"TRUE");
assert_eq!(model._get_text("A5"), *"FALSE");
assert_eq!(model._get_text("A6"), *"TRUE");
}
#[test]
fn fn_or_xor() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
// Text args
model._set("A1", &format!(r#"={func}("")"#));
model._set("A2", &format!(r#"={func}("", "")"#));
model._set("A3", &format!(r#"={func}("", TRUE)"#));
model._set("A4", &format!(r#"={func}("", FALSE)"#));
model._set("A5", &format!("={func}(FALSE, TRUE)"));
model._set("A6", &format!("={func}(FALSE, FALSE)"));
model._set("A7", &format!("={func}(TRUE, FALSE)"));
// Reference to empty cell, plus true argument
model._set("A8", &format!("={func}(Z99, 1)"));
// Reference to empty cell/range
model._set("A9", &format!("={func}(Z99)"));
model._set("A10", &format!("={func}(X99:Z99"));
// Reference to cell with reference to empty range
model._set("B11", "=X99:Z99");
model._set("A11", &format!("={func}(B11)"));
// Reference to cell with non-empty range
model._set("X12", "1");
model._set("B12", "=X12:Z12");
model._set("A12", &format!("={func}(B12)"));
// Reference to text cell
model._set("B13", "some_text");
model._set("A13", &format!("={func}(B13)"));
model._set("A14", &format!("={func}(B13, 0)"));
model._set("A15", &format!("={func}(B13, 1)"));
// Reference to Implicit intersection
model._set("X16", "1");
model._set("B16", "=@X15:X16");
model._set("A16", &format!("={func}(B16)"));
// Non-empty range
model._set("B17", "1");
model._set("A17", &format!("={func}(B17:C17)"));
// Non-empty range with text
model._set("B18", "text");
model._set("A18", &format!("={func}(B18:C18)"));
// Non-empty range with text and number
model._set("B19", "text");
model._set("C19", "1");
model._set("A19", &format!("={func}(B19:C19)"));
// range with error
model._set("B20", "=1/0");
model._set("A20", &format!("={func}(B20:C20)"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
assert_eq!(model._get_text("A2"), *"#VALUE!");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"TRUE");
assert_eq!(model._get_text("A6"), *"FALSE");
assert_eq!(model._get_text("A7"), *"TRUE");
assert_eq!(model._get_text("A8"), *"TRUE");
assert_eq!(model._get_text("A9"), *"#VALUE!");
assert_eq!(model._get_text("A10"), *"#VALUE!");
assert_eq!(model._get_text("A11"), *"#VALUE!");
// TODO: This one depends on spill behaviour which isn't implemented yet
// assert_eq!(model._get_text("A12"), *"TRUE");
assert_eq!(model._get_text("A13"), *"#VALUE!");
assert_eq!(model._get_text("A14"), *"FALSE");
assert_eq!(model._get_text("A15"), *"TRUE");
// TODO: This one depends on @ implicit intersection behaviour which isn't implemented yet
// assert_eq!(model._get_text("A16"), *"TRUE");
assert_eq!(model._get_text("A17"), *"TRUE");
assert_eq!(model._get_text("A18"), *"#VALUE!");
assert_eq!(model._get_text("A19"), *"TRUE");
assert_eq!(model._get_text("A20"), *"#DIV/0!");
}
}
#[test]
fn fn_or_xor_no_arguments() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
model._set("A1", &format!("={}()", func));
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
}
#[test]
fn fn_or_xor_missing_arguments() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
model._set("A1", &format!("={func}(,)"));
model._set("A2", &format!("={func}(,1)"));
model._set("A3", &format!("={func}(1,)"));
model._set("A4", &format!("={func}(,B1)"));
model._set("A5", &format!("={func}(,B1:B4)"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"FALSE");
}
}

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,6 +3,7 @@ mod test_autofill_columns;
mod test_autofill_rows;
mod test_border;
mod test_clear_cells;
mod test_defined_names;
mod test_diff_queue;
mod test_evaluation;
mod test_general;
@@ -14,6 +15,7 @@ mod test_on_paste_styles;
mod test_paste_csv;
mod test_rename_sheet;
mod test_row_column;
mod test_sheet_state;
mod test_styles;
mod test_to_from_bytes;
mod test_undo_redo;

View File

@@ -0,0 +1,398 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
#[test]
fn create_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
assert_eq!(
model.get_defined_name_list(),
vec![("myName".to_string(), None, "Sheet1!$A$1".to_string())]
);
// delete it
model.delete_defined_name("myName", None).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("#NAME?".to_string())
);
assert_eq!(model.get_defined_name_list().len(), 0);
model.undo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
}
#[test]
fn scopes() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
// Global
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap();
// Local to Sheet2
model.new_sheet().unwrap();
model.set_user_input(1, 2, 1, "145").unwrap();
model
.new_defined_name("myName", Some(1), "Sheet2!$A$2")
.unwrap();
model.set_user_input(1, 8, 8, "=myName").unwrap();
// Sheet 3
model.new_sheet().unwrap();
model.set_user_input(2, 2, 2, "=myName").unwrap();
// Global
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
assert_eq!(
model.get_formatted_cell_value(1, 8, 8),
Ok("145".to_string())
);
assert_eq!(
model.get_formatted_cell_value(2, 2, 2),
Ok("42".to_string())
);
}
#[test]
fn delete_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", Some(1), "Sheet1!$A$1")
.unwrap();
model
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " my world!")"#)
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
assert_eq!(
model.get_formatted_cell_value(1, 2, 1),
Ok("Hello my world!".to_string())
);
model.delete_sheet(0).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok(r#"=CONCATENATE(MyName," my world!")"#.to_string())
);
}
#[test]
fn change_scope() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", Some(1), "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
model
.update_defined_name("myName", Some(1), "myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
}
#[test]
fn rename_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
model
.update_defined_name("myName", None, "newName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok(r#"=CONCATENATE(newName," world!")"#.to_string())
);
}
#[test]
fn rename_defined_name_operations() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "123").unwrap();
model
.new_defined_name("answer", None, "Sheet1!$A$1")
.unwrap();
model
.set_user_input(0, 2, 1, "=IF(answer<2, answer*2, answer^2)")
.unwrap();
model
.set_user_input(0, 3, 1, "=badDunction(-answer)")
.unwrap();
model.new_sheet().unwrap();
model.set_user_input(1, 1, 1, "78").unwrap();
model
.new_defined_name("answer", Some(1), "Sheet1!$A$1")
.unwrap();
model.set_user_input(1, 3, 1, "=answer").unwrap();
model
.update_defined_name("answer", None, "respuesta", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok("=IF(respuesta<2,respuesta*2,respuesta^2)".to_string())
);
assert_eq!(
model.get_cell_content(0, 3, 1),
Ok("=badDunction(-respuesta)".to_string())
);
// A defined name with the same name but different scope
assert_eq!(model.get_cell_content(1, 3, 1), Ok("=answer".to_string()));
}
#[test]
fn rename_defined_name_string_operations() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.set_user_input(0, 1, 2, "World").unwrap();
model
.new_defined_name("hello", None, "Sheet1!$A$1")
.unwrap();
model
.new_defined_name("world", None, "Sheet1!$B$1")
.unwrap();
model.set_user_input(0, 2, 1, "=hello&world").unwrap();
model
.update_defined_name("hello", None, "HolaS", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok("=HolaS&world".to_string())
);
}
#[test]
fn invalid_names() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
// spaces
assert_eq!(
model.new_defined_name("A real", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
// Starts with number
assert_eq!(
model.new_defined_name("2real", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
// Updating also fails
assert_eq!(
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
}
#[test]
fn already_existing() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
model
.new_defined_name("Another", None, "Sheet1!$A$1")
.unwrap();
// Can't create a new name with the same name
assert_eq!(
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
Err("Defined name already exists".to_string())
);
// Can't update one into an existing
assert_eq!(
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
Err("Defined name already exists".to_string())
);
}
#[test]
fn invalid_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
}
#[test]
fn invalid_formula() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.new_defined_name("MyName", None, "A1").unwrap();
model.set_user_input(0, 1, 2, "=MyName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("#NAME?".to_string())
);
}
#[test]
fn undo_redo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.set_user_input(0, 2, 1, "Hola").unwrap();
model.set_user_input(0, 1, 2, r#"=MyName&"!""#).unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 0);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("#NAME?".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model
.update_defined_name("MyName", None, "MyName", None, "Sheet1!$A$2")
.unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(model2.get_defined_name_list().len(), 1);
assert_eq!(
model2.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
}

View File

@@ -91,7 +91,6 @@ fn insert_remove_columns() {
let mut model = UserModel::from_model(model);
// column E
let column_width = model.get_column_width(0, 5).unwrap();
println!("{column_width}");
// Insert some data in row 5 (and change the style) in E1
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());

View File

@@ -0,0 +1,57 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_tests() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// add three more sheets
model.new_sheet().unwrap();
model.new_sheet().unwrap();
model.new_sheet().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.len(), 4);
for sheet in &info {
assert_eq!(sheet.state, "visible".to_string());
}
model.set_selected_sheet(2).unwrap();
assert_eq!(info.get(2).unwrap().name, "Sheet3".to_string());
model.hide_sheet(2).unwrap();
let info = model.get_worksheets_properties();
assert_eq!(model.get_selected_sheet(), 3);
assert_eq!(info.get(2).unwrap().state, "hidden".to_string());
model.undo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(2).unwrap().state, "visible".to_string());
model.redo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(2).unwrap().state, "hidden".to_string());
model.set_selected_sheet(3).unwrap();
model.hide_sheet(3).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
model.unhide_sheet(2).unwrap();
model.unhide_sheet(3).unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.len(), 4);
for sheet in &info {
assert_eq!(sheet.state, "visible".to_string());
}
model.undo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(3).unwrap().state, "hidden".to_string());
model.redo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(3).unwrap().state, "visible".to_string());
}

View File

@@ -293,7 +293,9 @@ impl Model {
Node::EmptyArgKind => None,
Node::InvalidFunctionKind { .. } => None,
Node::ArrayKind(_) => None,
Node::VariableKind(_) => None,
Node::DefinedNameKind(_) => None,
Node::TableNameKind(_) => None,
Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None,
}

View File

@@ -13,8 +13,8 @@ use crate::{
},
model::Model,
types::{
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, Style,
VerticalAlignment,
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
Style, VerticalAlignment,
},
utils::is_valid_hex_color,
};
@@ -440,6 +440,48 @@ impl UserModel {
Ok(())
}
/// Hides sheet by index
///
/// See also:
/// * [Model::set_sheet_state]
/// * [UserModel::unhide_sheet]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), String> {
let sheet_count = self.model.workbook.worksheets.len() as u32;
for index in 1..sheet_count {
let sheet_index = (sheet + index) % sheet_count;
if self.model.workbook.worksheet(sheet_index)?.state == SheetState::Visible {
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
view.sheet = sheet_index;
};
break;
}
}
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
self.push_diff_list(vec![Diff::SetSheetState {
index: sheet,
new_value: SheetState::Hidden,
old_value,
}]);
self.model.set_sheet_state(sheet, SheetState::Hidden)?;
Ok(())
}
/// Un hides sheet by index
///
/// See also:
/// * [Model::set_sheet_state]
/// * [UserModel::hide_sheet]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), String> {
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
self.push_diff_list(vec![Diff::SetSheetState {
index: sheet,
new_value: SheetState::Visible,
old_value,
}]);
self.model.set_sheet_state(sheet, SheetState::Visible)?;
Ok(())
}
/// Sets sheet color
///
/// Note: an empty string will remove the color
@@ -1692,6 +1734,68 @@ impl UserModel {
Ok(())
}
/// Returns the list of defined names
pub fn get_defined_name_list(&self) -> Vec<(String, Option<u32>, String)> {
self.model.workbook.get_defined_names_with_scope()
}
/// Delete an existing defined name
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let old_value = self.model.get_defined_name_formula(name, scope)?;
let diff_list = vec![Diff::DeleteDefinedName {
name: name.to_string(),
scope,
old_value,
}];
self.push_diff_list(diff_list);
self.model.delete_defined_name(name, scope)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Create a new defined name
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), String> {
self.model.new_defined_name(name, scope, formula)?;
let diff_list = vec![Diff::CreateDefinedName {
name: name.to_string(),
scope,
value: formula.to_string(),
}];
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Updates a defined name
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), String> {
let old_formula = self.model.get_defined_name_formula(name, scope)?;
let diff_list = vec![Diff::UpdateDefinedName {
name: name.to_string(),
scope,
old_formula: old_formula.to_string(),
new_name: new_name.to_string(),
new_scope,
new_formula: new_formula.to_string(),
}];
self.push_diff_list(diff_list);
self.model
.update_defined_name(name, scope, new_name, new_scope, new_formula)?;
self.evaluate_if_not_paused();
Ok(())
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {
@@ -1862,6 +1966,41 @@ impl UserModel {
} => {
self.model.set_show_grid_lines(*sheet, *old_value)?;
}
Diff::CreateDefinedName {
name,
scope,
value: _,
} => {
self.model.delete_defined_name(name, *scope)?;
}
Diff::DeleteDefinedName {
name,
scope,
old_value,
} => {
self.model.new_defined_name(name, *scope, old_value)?;
}
Diff::UpdateDefinedName {
name,
scope,
old_formula,
new_name,
new_scope,
new_formula: _,
} => {
self.model.update_defined_name(
new_name,
*new_scope,
name,
*scope,
old_formula,
)?;
}
Diff::SetSheetState {
index,
old_value,
new_value: _,
} => self.model.set_sheet_state(*index, old_value.clone())?,
}
}
if needs_evaluation {
@@ -1989,6 +2128,33 @@ impl UserModel {
} => {
self.model.set_show_grid_lines(*sheet, *new_value)?;
}
Diff::CreateDefinedName { name, scope, value } => {
self.model.new_defined_name(name, *scope, value)?
}
Diff::DeleteDefinedName {
name,
scope,
old_value: _,
} => self.model.delete_defined_name(name, *scope)?,
Diff::UpdateDefinedName {
name,
scope,
old_formula: _,
new_name,
new_scope,
new_formula,
} => self.model.update_defined_name(
name,
*scope,
new_name,
*new_scope,
new_formula,
)?,
Diff::SetSheetState {
index,
old_value: _,
new_value,
} => self.model.set_sheet_state(*index, new_value.clone())?,
}
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use bitcode::{Decode, Encode};
use crate::types::{Cell, Col, Row, Style};
use crate::types::{Cell, Col, Row, SheetState, Style};
#[derive(Clone, Encode, Decode)]
pub(crate) struct RowData {
@@ -104,11 +104,35 @@ pub(crate) enum Diff {
old_value: String,
new_value: String,
},
SetSheetState {
index: u32,
old_value: SheetState,
new_value: SheetState,
},
SetShowGridLines {
sheet: u32,
old_value: bool,
new_value: bool,
}, // FIXME: we are missing SetViewDiffs
},
CreateDefinedName {
name: String,
scope: Option<u32>,
value: String,
},
DeleteDefinedName {
name: String,
scope: Option<u32>,
old_value: String,
},
UpdateDefinedName {
name: String,
scope: Option<u32>,
old_formula: String,
new_name: String,
new_scope: Option<u32>,
new_formula: String,
},
// FIXME: we are missing SetViewDiffs
}
pub(crate) type DiffList = Vec<Diff>;

View File

@@ -27,4 +27,27 @@ impl Workbook {
.get_mut(worksheet_index as usize)
.ok_or_else(|| "Invalid sheet index".to_string())
}
/// Returns the a list of defined names in the workbook with their scope
pub fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self
.defined_names
.iter()
.map(|dn| {
let index = dn
.sheet_id
.and_then(|sheet_id| {
// returns an Option<usize>
sheet_id_index.iter().position(|&x| x == sheet_id)
})
// convert Option<usize> to Option<u32>
.map(|pos| pos as u32);
(dn.name.clone(), index, dn.formula.clone())
})
.collect::<Vec<_>>();
defined_names
}
}

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.3.1"
[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.3.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"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-darwin-universal`
This is the **universal-apple-darwin** binary for `@ironcalc/nodejs`

View File

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

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-darwin-x64`
This is the **x86_64-apple-darwin** binary for `@ironcalc/nodejs`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-linux-riscv64-gnu`
This is the **riscv64gc-unknown-linux-gnu** binary for `@ironcalc/nodejs`

View File

@@ -0,0 +1,21 @@
{
"name": "@ironcalc/nodejs-linux-riscv64-gnu",
"version": "0.2.0",
"os": [
"linux"
],
"cpu": [
"riscv64"
],
"main": "nodejs.linux-riscv64-gnu.node",
"files": [
"nodejs.linux-riscv64-gnu.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"glibc"
]
}

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-linux-x64-gnu`
This is the **x86_64-unknown-linux-gnu** binary for `@ironcalc/nodejs`

View File

@@ -0,0 +1,21 @@
{
"name": "@ironcalc/nodejs-linux-x64-gnu",
"version": "0.2.0",
"os": [
"linux"
],
"cpu": [
"x64"
],
"main": "nodejs.linux-x64-gnu.node",
"files": [
"nodejs.linux-x64-gnu.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"glibc"
]
}

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-linux-x64-musl`
This is the **x86_64-unknown-linux-musl** binary for `@ironcalc/nodejs`

View File

@@ -0,0 +1,21 @@
{
"name": "@ironcalc/nodejs-linux-x64-musl",
"version": "0.2.0",
"os": [
"linux"
],
"cpu": [
"x64"
],
"main": "nodejs.linux-x64-musl.node",
"files": [
"nodejs.linux-x64-musl.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"musl"
]
}

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-win32-arm64-msvc`
This is the **aarch64-pc-windows-msvc** binary for `@ironcalc/nodejs`

View File

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

View File

@@ -0,0 +1,3 @@
# `@ironcalc/nodejs-win32-x64-msvc`
This is the **x86_64-pc-windows-msvc** binary for `@ironcalc/nodejs`

View File

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

2215
bindings/nodejs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
{
"name": "@ironcalc/nodejs",
"version": "0.3.1",
"main": "index.js",
"types": "index.d.ts",
"napi": {
"name": "nodejs",
"triples": {
"additional": [
"aarch64-apple-darwin",
"aarch64-linux-android",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"aarch64-pc-windows-msvc",
"armv7-unknown-linux-gnueabihf",
"armv7-unknown-linux-musleabihf",
"x86_64-unknown-linux-musl",
"armv7-linux-androideabi",
"universal-apple-darwin",
"riscv64gc-unknown-linux-gnu"
]
}
},
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "^2.18.4",
"ava": "^6.0.1"
},
"ava": {
"timeout": "3m"
},
"engines": {
"node": ">= 10"
},
"scripts": {
"artifacts": "napi artifacts",
"build": "napi build --platform --release",
"build:debug": "napi build --platform",
"prepublishOnly": "napi prepublish -t npm",
"test": "ava",
"universal": "napi universal",
"version": "napi version"
}
}

1359
bindings/nodejs/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
tab_spaces = 2
edition = "2021"

View File

@@ -0,0 +1,8 @@
#[macro_use]
extern crate napi_derive;
mod model;
mod user_model;
pub use model::Model;
pub use user_model::UserModel;

View File

@@ -0,0 +1,343 @@
#![deny(clippy::all)]
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use serde::Serialize;
use ironcalc::{
base::{
types::{CellType, Style},
Model as BaseModel,
},
error::XlsxError,
export::{save_to_icalc, save_to_xlsx},
import::{load_from_icalc, load_from_xlsx},
};
#[derive(Serialize)]
struct DefinedName {
name: String,
scope: Option<u32>,
formula: String,
}
fn to_js_error(error: String) -> Error {
Error::new(Status::Unknown, error)
}
fn to_node_error(error: XlsxError) -> Error {
Error::new(Status::Unknown, error.to_string())
}
#[napi]
pub struct Model {
model: BaseModel,
}
#[napi]
impl Model {
#[napi(constructor)]
pub fn new(name: String, locale: String, timezone: String) -> Result<Self> {
let model = BaseModel::new_empty(&name, &locale, &timezone).map_err(to_js_error)?;
Ok(Self { model })
}
#[napi(factory)]
pub fn from_xlsx(file_path: String, locale: String, tz: String) -> Result<Model> {
let model = load_from_xlsx(&file_path, &locale, &tz)
.map_err(|error| Error::new(Status::Unknown, error.to_string()))?;
Ok(Self { model })
}
#[napi(factory)]
pub fn from_icalc(file_name: String) -> Result<Model> {
let model = load_from_icalc(&file_name)
.map_err(|error| Error::new(Status::Unknown, error.to_string()))?;
Ok(Self { model })
}
#[napi]
pub fn save_to_xlsx(&self, file: String) -> Result<()> {
save_to_xlsx(&self.model, &file).map_err(to_node_error)
}
#[napi]
pub fn save_to_icalc(&self, file: String) -> Result<()> {
save_to_icalc(&self.model, &file).map_err(to_node_error)
}
#[napi]
pub fn evaluate(&mut self) {
self.model.evaluate();
}
#[napi]
pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, value: String) -> Result<()> {
self
.model
.set_user_input(sheet, row, column, value)
.map_err(to_js_error)
}
#[napi]
pub fn clear_cell_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<()> {
self
.model
.cell_clear_contents(sheet, row, column)
.map_err(to_js_error)
}
#[napi]
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
self
.model
.get_cell_content(sheet, row, column)
.map_err(to_js_error)
}
#[napi(js_name = "getCellType")]
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32> {
Ok(
match self
.model
.get_cell_type(sheet, row, column)
.map_err(to_js_error)?
{
CellType::Number => 1,
CellType::Text => 2,
CellType::LogicalValue => 4,
CellType::ErrorValue => 16,
CellType::Array => 64,
CellType::CompoundData => 128,
},
)
}
#[napi]
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
self
.model
.get_formatted_cell_value(sheet, row, column)
.map_err(to_js_error)
}
#[napi]
pub fn set_cell_style(
&mut self,
env: Env,
sheet: u32,
row: i32,
column: i32,
style: JsUnknown,
) -> Result<()> {
let style: Style = env
.from_js_value(style)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.set_cell_style(sheet, row, column, &style)
.map_err(to_js_error)
}
#[napi(js_name = "getCellStyle")]
pub fn get_cell_style(
&mut self,
env: Env,
sheet: u32,
row: i32,
column: i32,
) -> Result<JsUnknown> {
let style = self
.model
.get_style_for_cell(sheet, row, column)
.map_err(to_js_error)?;
env
.to_js_value(&style)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi]
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
self
.model
.insert_rows(sheet, row, row_count)
.map_err(to_js_error)
}
#[napi]
pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
self
.model
.insert_columns(sheet, column, column_count)
.map_err(to_js_error)
}
#[napi]
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
self
.model
.delete_rows(sheet, row, row_count)
.map_err(to_js_error)
}
#[napi]
pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
self
.model
.delete_columns(sheet, column, column_count)
.map_err(to_js_error)
}
#[napi]
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64> {
self
.model
.get_column_width(sheet, column)
.map_err(to_js_error)
}
#[napi]
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64> {
self.model.get_row_height(sheet, row).map_err(to_js_error)
}
#[napi]
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<()> {
self
.model
.set_column_width(sheet, column, width)
.map_err(to_js_error)
}
#[napi]
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<()> {
self
.model
.set_row_height(sheet, row, height)
.map_err(to_js_error)
}
#[napi]
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32> {
self
.model
.get_frozen_columns_count(sheet)
.map_err(to_js_error)
}
#[napi]
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32> {
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
}
#[napi]
pub fn set_frozen_columns_count(&mut self, sheet: u32, column_count: i32) -> Result<()> {
self
.model
.set_frozen_columns(sheet, column_count)
.map_err(to_js_error)
}
#[napi]
pub fn set_frozen_rows_count(&mut self, sheet: u32, row_count: i32) -> Result<()> {
self
.model
.set_frozen_rows(sheet, row_count)
.map_err(to_js_error)
}
// I don't _think_ serializing to JsUnknown can't fail
// FIXME: Remove this clippy directive
#[napi(js_name = "getWorksheetsProperties")]
#[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
env
.to_js_value(&self.model.get_worksheets_properties())
.unwrap()
}
#[napi]
pub fn set_sheet_color(&mut self, sheet: u32, color: String) -> Result<()> {
self
.model
.set_sheet_color(sheet, &color)
.map_err(to_js_error)
}
#[napi]
pub fn add_sheet(&mut self, sheet_name: String) -> Result<()> {
self.model.add_sheet(&sheet_name).map_err(to_js_error)
}
#[napi]
pub fn new_sheet(&mut self) {
self.model.new_sheet();
}
#[napi]
pub fn delete_sheet(&mut self, sheet: u32) -> Result<()> {
self.model.delete_sheet(sheet).map_err(to_js_error)
}
#[napi]
pub fn rename_sheet(&mut self, sheet: u32, new_name: String) -> Result<()> {
self
.model
.rename_sheet_by_index(sheet, &new_name)
.map_err(to_js_error)
}
#[napi(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
let data: Vec<DefinedName> = self
.model
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| DefinedName {
name: s.0.to_owned(),
scope: s.1,
formula: s.2.to_owned(),
})
.collect();
env
.to_js_value(&data)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "newDefinedName")]
pub fn new_defined_name(
&mut self,
name: String,
scope: Option<u32>,
formula: String,
) -> Result<()> {
self
.model
.new_defined_name(&name, scope, &formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "updateDefinedName")]
pub fn update_defined_name(
&mut self,
name: String,
scope: Option<u32>,
new_name: String,
new_scope: Option<u32>,
new_formula: String,
) -> Result<()> {
self
.model
.update_defined_name(&name, scope, &new_name, new_scope, &new_formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "deleteDefinedName")]
pub fn delete_definedname(&mut self, name: String, scope: Option<u32>) -> Result<()> {
self
.model
.delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
}

View File

@@ -0,0 +1,620 @@
#![deny(clippy::all)]
use serde::Serialize;
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use ironcalc::base::{
expressions::types::Area,
types::{CellType, Style},
BorderArea, ClipboardData, UserModel as BaseModel,
};
#[derive(Serialize)]
struct DefinedName {
name: String,
scope: Option<u32>,
formula: String,
}
fn to_js_error(error: String) -> Error {
Error::new(Status::Unknown, error)
}
#[napi]
pub struct UserModel {
model: BaseModel,
}
#[napi]
impl UserModel {
#[napi(constructor)]
pub fn new(name: String, locale: String, timezone: String) -> Result<Self> {
let model = BaseModel::new_empty(&name, &locale, &timezone).map_err(to_js_error)?;
Ok(Self { model })
}
#[napi(factory)]
pub fn from_bytes(bytes: &[u8]) -> Result<UserModel> {
let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?;
Ok(UserModel { model })
}
pub fn undo(&mut self) -> Result<()> {
self.model.undo().map_err(to_js_error)
}
pub fn redo(&mut self) -> Result<()> {
self.model.redo().map_err(to_js_error)
}
#[napi(js_name = "canUndo")]
pub fn can_undo(&self) -> bool {
self.model.can_undo()
}
#[napi(js_name = "canRedo")]
pub fn can_redo(&self) -> bool {
self.model.can_redo()
}
#[napi(js_name = "pauseEvaluation")]
pub fn pause_evaluation(&mut self) {
self.model.pause_evaluation()
}
#[napi(js_name = "resumeEvaluation")]
pub fn resume_evaluation(&mut self) {
self.model.resume_evaluation()
}
pub fn evaluate(&mut self) {
self.model.evaluate();
}
#[napi(js_name = "flushSendQueue")]
pub fn flush_send_queue(&mut self) -> Vec<u8> {
self.model.flush_send_queue()
}
#[napi(js_name = "applyExternalDiffs")]
pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<()> {
self.model.apply_external_diffs(diffs).map_err(to_js_error)
}
#[napi(js_name = "getCellContent")]
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
self
.model
.get_cell_content(sheet, row, column)
.map_err(to_js_error)
}
#[napi(js_name = "newSheet")]
pub fn new_sheet(&mut self) -> Result<()> {
self.model.new_sheet().map_err(to_js_error)
}
#[napi(js_name = "deleteSheet")]
pub fn delete_sheet(&mut self, sheet: u32) -> Result<()> {
self.model.delete_sheet(sheet).map_err(to_js_error)
}
#[napi(js_name = "hideSheet")]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<()> {
self.model.hide_sheet(sheet).map_err(to_js_error)
}
#[napi(js_name = "unhideSheet")]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<()> {
self.model.unhide_sheet(sheet).map_err(to_js_error)
}
#[napi(js_name = "renameSheet")]
pub fn rename_sheet(&mut self, sheet: u32, name: String) -> Result<()> {
self.model.rename_sheet(sheet, &name).map_err(to_js_error)
}
#[napi(js_name = "setSheetColor")]
pub fn set_sheet_color(&mut self, sheet: u32, color: String) -> Result<()> {
self
.model
.set_sheet_color(sheet, &color)
.map_err(to_js_error)
}
#[napi(js_name = "rangeClearAll")]
pub fn range_clear_all(
&mut self,
sheet: u32,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<()> {
let range = Area {
sheet,
row: start_row,
column: start_column,
width: end_column - start_column + 1,
height: end_row - start_row + 1,
};
self.model.range_clear_all(&range).map_err(to_js_error)
}
#[napi(js_name = "rangeClearContents")]
pub fn range_clear_contents(
&mut self,
sheet: u32,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<()> {
let range = Area {
sheet,
row: start_row,
column: start_column,
width: end_column - start_column + 1,
height: end_row - start_row + 1,
};
self.model.range_clear_contents(&range).map_err(to_js_error)
}
#[napi(js_name = "insertRow")]
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
self.model.insert_row(sheet, row).map_err(to_js_error)
}
#[napi(js_name = "insertColumn")]
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
self.model.insert_column(sheet, column).map_err(to_js_error)
}
#[napi(js_name = "deleteRow")]
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
self.model.delete_row(sheet, row).map_err(to_js_error)
}
#[napi(js_name = "deleteColumn")]
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
self.model.delete_column(sheet, column).map_err(to_js_error)
}
#[napi(js_name = "setRowHeight")]
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<()> {
self
.model
.set_row_height(sheet, row, height)
.map_err(to_js_error)
}
#[napi(js_name = "setColumnWidth")]
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<()> {
self
.model
.set_column_width(sheet, column, width)
.map_err(to_js_error)
}
#[napi(js_name = "getRowHeight")]
pub fn get_row_height(&mut self, sheet: u32, row: i32) -> Result<f64> {
self.model.get_row_height(sheet, row).map_err(to_js_error)
}
#[napi(js_name = "getColumnWidth")]
pub fn get_column_width(&mut self, sheet: u32, column: i32) -> Result<f64> {
self
.model
.get_column_width(sheet, column)
.map_err(to_js_error)
}
#[napi(js_name = "setUserInput")]
pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, input: String) -> Result<()> {
self
.model
.set_user_input(sheet, row, column, &input)
.map_err(to_js_error)
}
#[napi(js_name = "getFormattedCellValue")]
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
self
.model
.get_formatted_cell_value(sheet, row, column)
.map_err(to_js_error)
}
#[napi(js_name = "getFrozenRowsCount")]
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32> {
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
}
#[napi(js_name = "getFrozenColumnsCount")]
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32> {
self
.model
.get_frozen_columns_count(sheet)
.map_err(to_js_error)
}
#[napi(js_name = "setFrozenRowsCount")]
pub fn set_frozen_rows_count(&mut self, sheet: u32, count: i32) -> Result<()> {
self
.model
.set_frozen_rows_count(sheet, count)
.map_err(to_js_error)
}
#[napi(js_name = "setFrozenColumnsCount")]
pub fn set_frozen_columns_count(&mut self, sheet: u32, count: i32) -> Result<()> {
self
.model
.set_frozen_columns_count(sheet, count)
.map_err(to_js_error)
}
#[napi(js_name = "updateRangeStyle")]
pub fn update_range_style(
&mut self,
env: Env,
range: JsUnknown,
style_path: String,
value: String,
) -> Result<()> {
let range: Area = env
.from_js_value(range)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.update_range_style(&range, &style_path, &value)
.map_err(to_js_error)
}
#[napi(js_name = "getCellStyle")]
pub fn get_cell_style(
&mut self,
env: Env,
sheet: u32,
row: i32,
column: i32,
) -> Result<JsUnknown> {
let style = self
.model
.get_cell_style(sheet, row, column)
.map_err(to_js_error)?;
env
.to_js_value(&style)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "onPasteStyles")]
pub fn on_paste_styles(&mut self, env: Env, styles: JsUnknown) -> Result<()> {
let styles: &Vec<Vec<Style>> = &env
.from_js_value(styles)
.map_err(|e| to_js_error(e.to_string()))?;
self.model.on_paste_styles(styles).map_err(to_js_error)
}
#[napi(js_name = "getCellType")]
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32> {
Ok(
match self
.model
.get_cell_type(sheet, row, column)
.map_err(to_js_error)?
{
CellType::Number => 1,
CellType::Text => 2,
CellType::LogicalValue => 4,
CellType::ErrorValue => 16,
CellType::Array => 64,
CellType::CompoundData => 128,
},
)
}
// I don't _think_ serializing to JsUnknown can't fail
// FIXME: Remove this clippy directive
#[napi(js_name = "getWorksheetsProperties")]
#[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
env
.to_js_value(&self.model.get_worksheets_properties())
.unwrap()
}
#[napi(js_name = "getSelectedSheet")]
pub fn get_selected_sheet(&self) -> u32 {
self.model.get_selected_sheet()
}
#[napi(js_name = "getSelectedCell")]
pub fn get_selected_cell(&self) -> Vec<i32> {
let (sheet, row, column) = self.model.get_selected_cell();
vec![sheet as i32, row, column]
}
// I don't _think_ serializing to JsUnknown can't fail
// FIXME: Remove this clippy directive
#[napi(js_name = "getSelectedView")]
#[allow(clippy::unwrap_used)]
pub fn get_selected_view(&self, env: Env) -> JsUnknown {
env.to_js_value(&self.model.get_selected_view()).unwrap()
}
#[napi(js_name = "setSelectedSheet")]
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<()> {
self.model.set_selected_sheet(sheet).map_err(to_js_error)
}
#[napi(js_name = "setSelectedCell")]
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<()> {
self
.model
.set_selected_cell(row, column)
.map_err(to_js_error)
}
#[napi(js_name = "setSelectedRange")]
pub fn set_selected_range(
&mut self,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<()> {
self
.model
.set_selected_range(start_row, start_column, end_row, end_column)
.map_err(to_js_error)
}
#[napi(js_name = "setTopLeftVisibleCell")]
pub fn set_top_left_visible_cell(&mut self, top_row: i32, top_column: i32) -> Result<()> {
self
.model
.set_top_left_visible_cell(top_row, top_column)
.map_err(to_js_error)
}
#[napi(js_name = "setShowGridLines")]
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<()> {
self
.model
.set_show_grid_lines(sheet, show_grid_lines)
.map_err(to_js_error)
}
#[napi(js_name = "getShowGridLines")]
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool> {
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
}
#[napi(js_name = "autoFillRows")]
pub fn auto_fill_rows(&mut self, env: Env, source_area: JsUnknown, to_row: i32) -> Result<()> {
let area: Area = env
.from_js_value(source_area)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.auto_fill_rows(&area, to_row)
.map_err(to_js_error)
}
#[napi(js_name = "autoFillColumns")]
pub fn auto_fill_columns(
&mut self,
env: Env,
source_area: JsUnknown,
to_column: i32,
) -> Result<()> {
let area: Area = env
.from_js_value(source_area)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.auto_fill_columns(&area, to_column)
.map_err(to_js_error)
}
#[napi(js_name = "onArrowRight")]
pub fn on_arrow_right(&mut self) -> Result<()> {
self.model.on_arrow_right().map_err(to_js_error)
}
#[napi(js_name = "onArrowLeft")]
pub fn on_arrow_left(&mut self) -> Result<()> {
self.model.on_arrow_left().map_err(to_js_error)
}
#[napi(js_name = "onArrowUp")]
pub fn on_arrow_up(&mut self) -> Result<()> {
self.model.on_arrow_up().map_err(to_js_error)
}
#[napi(js_name = "onArrowDown")]
pub fn on_arrow_down(&mut self) -> Result<()> {
self.model.on_arrow_down().map_err(to_js_error)
}
#[napi(js_name = "onPageDown")]
pub fn on_page_down(&mut self) -> Result<()> {
self.model.on_page_down().map_err(to_js_error)
}
#[napi(js_name = "onPageUp")]
pub fn on_page_up(&mut self) -> Result<()> {
self.model.on_page_up().map_err(to_js_error)
}
#[napi(js_name = "setWindowWidth")]
pub fn set_window_width(&mut self, window_width: f64) {
self.model.set_window_width(window_width);
}
#[napi(js_name = "setWindowHeight")]
pub fn set_window_height(&mut self, window_height: f64) {
self.model.set_window_height(window_height);
}
#[napi(js_name = "getScrollX")]
pub fn get_scroll_x(&self) -> Result<f64> {
self.model.get_scroll_x().map_err(to_js_error)
}
#[napi(js_name = "getScrollY")]
pub fn get_scroll_y(&self) -> Result<f64> {
self.model.get_scroll_y().map_err(to_js_error)
}
#[napi(js_name = "onExpandSelectedRange")]
pub fn on_expand_selected_range(&mut self, key: String) -> Result<()> {
self
.model
.on_expand_selected_range(&key)
.map_err(to_js_error)
}
#[napi(js_name = "onAreaSelecting")]
pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<()> {
self
.model
.on_area_selecting(target_row, target_column)
.map_err(to_js_error)
}
#[napi(js_name = "setAreaWithBorder")]
pub fn set_area_with_border(
&mut self,
env: Env,
area: JsUnknown,
border_area: JsUnknown,
) -> Result<()> {
let range: Area = env
.from_js_value(area)
.map_err(|e| to_js_error(e.to_string()))?;
let border: BorderArea = env
.from_js_value(border_area)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.set_area_with_border(&range, &border)
.map_err(|e| to_js_error(e.to_string()))?;
Ok(())
}
#[napi(js_name = "toBytes")]
pub fn to_bytes(&self) -> Vec<u8> {
self.model.to_bytes()
}
#[napi(js_name = "getName")]
pub fn get_name(&self) -> String {
self.model.get_name()
}
#[napi(js_name = "setName")]
pub fn set_name(&mut self, name: String) {
self.model.set_name(&name);
}
#[napi(js_name = "copyToClipboard")]
pub fn copy_to_clipboard(&self, env: Env) -> Result<JsUnknown> {
let data = self
.model
.copy_to_clipboard()
.map_err(|e| to_js_error(e.to_string()))?;
env
.to_js_value(&data)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "pasteFromClipboard")]
pub fn paste_from_clipboard(
&mut self,
env: Env,
source_sheet: u32,
source_range: JsUnknown,
clipboard: JsUnknown,
is_cut: bool,
) -> Result<()> {
let source_range: (i32, i32, i32, i32) = env
.from_js_value(source_range)
.map_err(|e| to_js_error(e.to_string()))?;
let clipboard: ClipboardData = env
.from_js_value(clipboard)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.paste_from_clipboard(source_sheet, source_range, &clipboard, is_cut)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "pasteCsvText")]
pub fn paste_csv_string(&mut self, env: Env, area: JsUnknown, csv: String) -> Result<()> {
let range: Area = env
.from_js_value(area)
.map_err(|e| to_js_error(e.to_string()))?;
self
.model
.paste_csv_string(&range, &csv)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
let data: Vec<DefinedName> = self
.model
.get_defined_name_list()
.iter()
.map(|s| DefinedName {
name: s.0.to_owned(),
scope: s.1,
formula: s.2.to_owned(),
})
.collect();
env
.to_js_value(&data)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "newDefinedName")]
pub fn new_defined_name(
&mut self,
name: String,
scope: Option<u32>,
formula: String,
) -> Result<()> {
self
.model
.new_defined_name(&name, scope, &formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "updateDefinedName")]
pub fn update_defined_name(
&mut self,
name: String,
scope: Option<u32>,
new_name: String,
new_scope: Option<u32>,
new_formula: String,
) -> Result<()> {
self
.model
.update_defined_name(&name, scope, &new_name, new_scope, &new_formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[napi(js_name = "deleteDefinedName")]
pub fn delete_definedname(&mut self, name: String, scope: Option<u32>) -> Result<()> {
self
.model
.delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
}

1575
bindings/nodejs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,884 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "chrono-tz"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc529705a6e0028189c83f0a5dd9fb214105116f7e3c0eeab7ff0369766b0d1"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "getrandom"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indoc"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "ironcalc"
version = "0.1.0"
dependencies = [
"chrono",
"ironcalc_base",
"itertools",
"roxmltree",
"serde",
"serde_json",
"thiserror",
"zip",
]
[[package]]
name = "ironcalc_base"
version = "0.1.0"
dependencies = [
"chrono",
"chrono-tz",
"js-sys",
"once_cell",
"rand",
"regex",
"ryu",
"serde",
"serde_json",
"serde_repr",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "lock_api"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
"regex",
]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"parking_lot",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pyroncalc"
version = "0.1.0"
dependencies = [
"ironcalc",
"pyo3",
"serde",
"serde_json",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "roxmltree"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671"
dependencies = [
"xmlparser",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "serde_json"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "smallvec"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
[[package]]
name = "thiserror"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unindent"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.39",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "zip"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
dependencies = [
"byteorder",
"bzip2",
"crc32fast",
"flate2",
"thiserror",
"time",
]

View File

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

View File

@@ -1,6 +1,6 @@
[project]
name = "ironcalc"
version = "0.1.2"
version = "0.3.0"
description = "Create, edit and evaluate Excel spreadsheets"
requires-python = ">=3.10"
keywords = [

View File

@@ -1,10 +1,10 @@
[package]
name = "wasm"
version = "0.1.3"
version = "0.3.0"
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
description = "IronCalc Web bindings"
license = "MIT/Apache-2.0"
repository = "https://github.com/ironcalc/web-bindings"
repository = "https://github.com/ironcalc/ironcalc"
edition = "2021"
[lib]
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
# Uses `../ironcalc/base` when used locally, and uses
# the inicated version from crates.io when published.
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../../base", version = "0.2", features = ["use_regex_lite"] }
ironcalc_base = { path = "../../base", version = "0.3", features = ["use_regex_lite"] }
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2.92"
serde-wasm-bindgen = "0.4"

View File

@@ -1,8 +1,17 @@
# In some platforms, python is called python3
PYTHON := $(shell command -v python 2>/dev/null || command -v python3 2>/dev/null)
# If neither is found, fail immediately
ifeq ($(PYTHON),)
$(error No python found. Please install python.)
endif
all:
wasm-pack build --target web --scope ironcalc --release
cp README.pkg.md pkg/README.md
tsc types.ts --target esnext --module esnext
python fix_types.py
$(PYTHON) fix_types.py
rm -f types.js
tests:
wasm-pack build --target nodejs && node tests/test.mjs

View File

@@ -187,6 +187,20 @@ paste_from_clipboard_types = r"""
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
"""
defined_name_list = r"""
/**
* @returns {any}
*/
getDefinedNameList(): any;
"""
defined_name_list_types = r"""
/**
* @returns {DefinedName[]}
*/
getDefinedNameList(): DefinedName[];
"""
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
@@ -200,6 +214,7 @@ def fix_types(text):
text = text.replace(paste_csv_string, paste_csv_string_types)
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
with open("types.ts") as f:
types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str)

View File

@@ -1,3 +1,4 @@
use serde::Serialize;
use wasm_bindgen::{
prelude::{wasm_bindgen, JsError},
JsValue,
@@ -29,6 +30,13 @@ pub fn column_name_from_number(column: i32) -> Result<String, JsError> {
}
}
#[derive(Serialize)]
struct DefinedName {
name: String,
scope: Option<u32>,
formula: String,
}
#[wasm_bindgen]
pub struct Model {
model: BaseModel,
@@ -106,6 +114,16 @@ impl Model {
self.model.delete_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "hideSheet")]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.hide_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "unhideSheet")]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.unhide_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "renameSheet")]
pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> {
self.model.rename_sheet(sheet, name).map_err(to_js_error)
@@ -542,4 +560,52 @@ impl Model {
.paste_csv_string(&range, csv)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
let data: Vec<DefinedName> = self
.model
.get_defined_name_list()
.iter()
.map(|s| DefinedName {
name: s.0.to_owned(),
scope: s.1,
formula: s.2.to_owned(),
})
.collect();
serde_wasm_bindgen::to_value(&data).map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "newDefinedName")]
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), JsError> {
self.model
.new_defined_name(name, scope, formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "updateDefinedName")]
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), JsError> {
self.model
.update_defined_name(name, scope, new_name, new_scope, new_formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "deleteDefinedName")]
pub fn delete_definedname(&mut self, name: &str, scope: Option<u32>) -> Result<(), JsError> {
self.model
.delete_defined_name(name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
}

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