Compare commits
234 Commits
varum-chan
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48727b1b39 | ||
|
|
c554d929f4 | ||
|
|
acdf85dbc3 | ||
|
|
6ac8f7e948 | ||
|
|
9a4e798313 | ||
|
|
7756ef7f48 | ||
|
|
793534b190 | ||
|
|
efc925a046 | ||
|
|
155f891f8b | ||
|
|
5683d02b93 | ||
|
|
475c3e9d49 | ||
|
|
9e65ea3024 | ||
|
|
03ad87cd8f | ||
|
|
e2a466c500 | ||
|
|
08b3d71e9e | ||
|
|
e5ec75495a | ||
|
|
e07fdd2091 | ||
|
|
cde6f0e49f | ||
|
|
da017b6113 | ||
|
|
90763048bc | ||
|
|
532386b448 | ||
|
|
84b2bdd7c9 | ||
|
|
25bb1ab8dc | ||
|
|
5c13f241c6 | ||
|
|
26b20eea43 | ||
|
|
b62256963a | ||
|
|
4f627b4363 | ||
|
|
a9a8c4f615 | ||
|
|
f9c9467e6c | ||
|
|
409b77c210 | ||
|
|
eecf6f3c3b | ||
|
|
ce7318840d | ||
|
|
7bc563ef29 | ||
|
|
8ed88e1445 | ||
|
|
a1353e0817 | ||
|
|
c0fa55c5f7 | ||
|
|
1ff0c38aa5 | ||
|
|
e5a2db4d8c | ||
|
|
fc7335707a | ||
|
|
4095b7db6e | ||
|
|
dd9ca4224d | ||
|
|
5aa7617e97 | ||
|
|
a10d1f4615 | ||
|
|
1e8441a674 | ||
|
|
b2c5027f56 | ||
|
|
91984dc920 | ||
|
|
74be62823d | ||
|
|
edd00096b6 | ||
|
|
d764752f16 | ||
|
|
ce6c908dc7 | ||
|
|
6ee450709a | ||
|
|
23ab5dfef2 | ||
|
|
7e54cb6aa2 | ||
|
|
857ebabf16 | ||
|
|
f0af3048b7 | ||
|
|
99125f1fea | ||
|
|
f96481feb8 | ||
|
|
dc8bb6da21 | ||
|
|
d866e283e9 | ||
|
|
8a54f45d75 | ||
|
|
42d557d485 | ||
|
|
293f7c6de6 | ||
|
|
38325b0bb9 | ||
|
|
282ed16f0d | ||
|
|
fd744d28a3 | ||
|
|
9a717daf04 | ||
|
|
84bf859c2c | ||
|
|
e57101f279 | ||
|
|
264fcac63c | ||
|
|
7777f8e5d6 | ||
|
|
6aa73171c7 | ||
|
|
8051913b2d | ||
|
|
cfa38548d5 | ||
|
|
9787721c5a | ||
|
|
610b899f66 | ||
|
|
24fb87721f | ||
|
|
d3bc8b135c | ||
|
|
0f6d311de2 | ||
|
|
0c15ae194d | ||
|
|
5d429b1660 | ||
|
|
20c4a596bf | ||
|
|
f07a69260f | ||
|
|
ec4e7b1ca3 | ||
|
|
81d25b6ec9 | ||
|
|
3a001d96b8 | ||
|
|
69ca1f178c | ||
|
|
feb22cced3 | ||
|
|
c88304ba96 | ||
|
|
fa0b386abc | ||
|
|
ff0d05e3a0 | ||
|
|
1b7389fd23 | ||
|
|
263bab2cf9 | ||
|
|
2e0722f9b5 | ||
|
|
fd72bca141 | ||
|
|
8215cfc9fb | ||
|
|
378f8351d3 | ||
|
|
c770b3229c | ||
|
|
b0f57b20c2 | ||
|
|
912fcae0a3 | ||
|
|
cc72d031b5 | ||
|
|
e8c18ebc5e | ||
|
|
576c358e2a | ||
|
|
eb03efba3e | ||
|
|
43e9cb3523 | ||
|
|
b95c0642da | ||
|
|
185a70224c | ||
|
|
a4c3233253 | ||
|
|
ac9dc22972 | ||
|
|
68b5364bbd | ||
|
|
02e726b445 | ||
|
|
f63d307fec | ||
|
|
99e1110261 | ||
|
|
91eb66993d | ||
|
|
87e8b7a20b | ||
|
|
97b27006cf | ||
|
|
b7f7e73824 | ||
|
|
ea194ee730 | ||
|
|
cbb413f100 | ||
|
|
a4cf93c49a | ||
|
|
70366ea60c | ||
|
|
9aa1b4574e | ||
|
|
82b2d28663 | ||
|
|
d2ba34166b | ||
|
|
99d42cb1e2 | ||
|
|
ddc785e7a6 | ||
|
|
8ab1382e75 | ||
|
|
ec5714e3ec | ||
|
|
4660f0e456 | ||
|
|
f2757e7d76 | ||
|
|
5ca15033f7 | ||
|
|
75e04696b5 | ||
|
|
832ca02e16 | ||
|
|
cbda30f951 | ||
|
|
564d4bac7a | ||
|
|
0dd26e8fee | ||
|
|
f6fbb4b303 | ||
|
|
c6adf8449b | ||
|
|
d04691b790 | ||
|
|
7c32088480 | ||
|
|
6326c44941 | ||
|
|
d3af994866 | ||
|
|
b859af1dc4 | ||
|
|
f9cfdeb35b | ||
|
|
669a5eec39 | ||
|
|
e268dda9e8 | ||
|
|
e0205d6c9a | ||
|
|
81ad724348 | ||
|
|
dc3bf8826b | ||
|
|
38023d3156 | ||
|
|
655d663590 | ||
|
|
8ba30fde33 | ||
|
|
690032c811 | ||
|
|
86213a8434 | ||
|
|
2ed5fb9bbc | ||
|
|
e455ed14ea | ||
|
|
ad2efad3ae | ||
|
|
40461b897b | ||
|
|
2e7410552f | ||
|
|
095002710b | ||
|
|
8ba131011e | ||
|
|
dbddc027fb | ||
|
|
de997f38f5 | ||
|
|
df4b4ca353 | ||
|
|
3b944cd659 | ||
|
|
d1f2b2acdd | ||
|
|
36f915b193 | ||
|
|
5d8e6255a3 | ||
|
|
73f3c06203 | ||
|
|
13b1157c61 | ||
|
|
44f7929f4e | ||
|
|
23643f0fae | ||
|
|
ad91d47db0 | ||
|
|
8f36a1f750 | ||
|
|
8ad407432f | ||
|
|
ebc31780ab | ||
|
|
6e8c47d4f6 | ||
|
|
ed42667e87 | ||
|
|
0cd3470a97 | ||
|
|
ab3f9c276d | ||
|
|
e098105531 | ||
|
|
a5919d837f | ||
|
|
f214070299 | ||
|
|
0b2de92053 | ||
|
|
98dc1f3b06 | ||
|
|
fb764fed1c | ||
|
|
23814ec18c | ||
|
|
8c6aaf2af0 | ||
|
|
ed24e57555 | ||
|
|
e8ced73b9c | ||
|
|
7ba002aca4 | ||
|
|
b0e72321b4 | ||
|
|
41350fbd73 | ||
|
|
17cd1fee96 | ||
|
|
c59148bdf6 | ||
|
|
d4a2289826 | ||
|
|
e5aff48e36 | ||
|
|
4c3374c0de | ||
|
|
5f3856350b | ||
|
|
7058a59c45 | ||
|
|
075760b7ba | ||
|
|
8669962904 | ||
|
|
7b30736792 | ||
|
|
40c7fc8f80 | ||
|
|
d8d694dd4a | ||
|
|
4b848f26dd | ||
|
|
2f660f85a7 | ||
|
|
eee40c1b9a | ||
|
|
56915ce0b1 | ||
|
|
a4a3b11858 | ||
|
|
65f1738473 | ||
|
|
a05ff18e40 | ||
|
|
4ef8a6882f | ||
|
|
2f2a5e4fba | ||
|
|
c39540a747 | ||
|
|
0fa69045f9 | ||
|
|
23e958af0c | ||
|
|
057835627b | ||
|
|
4d6fdf9a4a | ||
|
|
5731b5cc27 | ||
|
|
592ef2415d | ||
|
|
cb6685f72a | ||
|
|
8402bb0935 | ||
|
|
91df91c425 | ||
|
|
4aa770c118 | ||
|
|
c92c065073 | ||
|
|
6f124185b2 | ||
|
|
17cf519d41 | ||
|
|
bd1a1e3c97 | ||
|
|
3d517a4af4 | ||
|
|
1734fd5740 | ||
|
|
d9dbd3bf14 | ||
|
|
d8a5c29e2f | ||
|
|
85cd7ab6a3 | ||
|
|
4b806c357a |
446
.github/workflows/npm.yml
vendored
Normal file
446
.github/workflows/npm.yml
vendored
Normal file
@@ -0,0 +1,446 @@
|
||||
name: nodejs
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
APP_NAME: nodejs
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
'on':
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: "Publish to npm"
|
||||
required: true
|
||||
type: boolean
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
build: yarn build --target x86_64-apple-darwin
|
||||
- host: windows-latest
|
||||
build: yarn build --target x86_64-pc-windows-msvc
|
||||
target: x86_64-pc-windows-msvc
|
||||
- host: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
build: yarn build --target x86_64-unknown-linux-gnu
|
||||
- host: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
build: yarn build --target x86_64-unknown-linux-musl
|
||||
- host: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
build: yarn build --target aarch64-apple-darwin
|
||||
- host: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
build: yarn build --target aarch64-unknown-linux-gnu
|
||||
- host: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
setup: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf -y
|
||||
build: yarn build --target armv7-unknown-linux-gnueabihf
|
||||
- host: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabihf
|
||||
build: yarn build --target armv7-unknown-linux-musleabihf
|
||||
- host: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
build: yarn build --target aarch64-linux-android
|
||||
- host: ubuntu-latest
|
||||
target: armv7-linux-androideabi
|
||||
build: yarn build --target armv7-linux-androideabi
|
||||
- host: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
build: |-
|
||||
set -e &&
|
||||
rustup target add aarch64-unknown-linux-musl &&
|
||||
yarn build --target aarch64-unknown-linux-musl
|
||||
- host: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
build: yarn build --target aarch64-pc-windows-msvc
|
||||
- host: ubuntu-latest
|
||||
target: riscv64gc-unknown-linux-gnu
|
||||
setup: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-riscv64-linux-gnu -y
|
||||
build: yarn build --target riscv64gc-unknown-linux-gnu
|
||||
name: stable - ${{ matrix.settings.target }} - node@20
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
if: ${{ !matrix.settings.docker }}
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
cache-dependency-path: "bindings/nodejs"
|
||||
- name: Install
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ !matrix.settings.docker }}
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.settings.target }}
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
.cargo-cache
|
||||
target/
|
||||
key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}
|
||||
- uses: goto-bus-stop/setup-zig@v2
|
||||
if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' || matrix.settings.target == 'armv7-unknown-linux-musleabihf' }}
|
||||
with:
|
||||
version: 0.13.0
|
||||
- name: Setup toolchain
|
||||
run: ${{ matrix.settings.setup }}
|
||||
if: ${{ matrix.settings.setup }}
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build in docker
|
||||
uses: addnab/docker-run-action@v3
|
||||
if: ${{ matrix.settings.docker }}
|
||||
with:
|
||||
image: ${{ matrix.settings.docker }}
|
||||
options: '--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
|
||||
run: ${{ matrix.settings.build }}
|
||||
- name: Build
|
||||
run: ${{ matrix.settings.build }}
|
||||
if: ${{ !matrix.settings.docker }}
|
||||
shell: bash
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.settings.target }}
|
||||
path: bindings/nodejs/${{ env.APP_NAME }}.*.node
|
||||
if-no-files-found: error
|
||||
test-macOS-windows-binding:
|
||||
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
|
||||
needs:
|
||||
- build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- host: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
node:
|
||||
- '18'
|
||||
- '20'
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: yarn
|
||||
cache-dependency-path: "bindings/nodejs"
|
||||
architecture: x64
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.settings.target }}
|
||||
path: bindings/nodejs/
|
||||
- name: List packages
|
||||
run: ls -R .
|
||||
shell: bash
|
||||
- name: Test bindings
|
||||
run: yarn test
|
||||
test-linux-x64-gnu-binding:
|
||||
name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }}
|
||||
needs:
|
||||
- build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node:
|
||||
- '18'
|
||||
- '20'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: yarn
|
||||
cache-dependency-path: "bindings/nodejs"
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-linux-gnu
|
||||
path: bindings/nodejs/
|
||||
- name: List packages
|
||||
run: ls -R .
|
||||
shell: bash
|
||||
- name: Test bindings
|
||||
run: docker run --rm -v $(pwd):/build -w /build/bindings/nodejs node:${{ matrix.node }}-slim yarn test
|
||||
test-linux-x64-musl-binding:
|
||||
name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }}
|
||||
needs:
|
||||
- build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node:
|
||||
- '18'
|
||||
- '20'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: yarn
|
||||
cache-dependency-path: "bindings/nodejs"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn config set supportedArchitectures.libc "musl"
|
||||
yarn install
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-linux-musl
|
||||
path: bindings/nodejs/
|
||||
- name: List packages
|
||||
run: ls -R .
|
||||
shell: bash
|
||||
- name: Test bindings
|
||||
run: docker run --rm -v $(pwd):/build -w /build/bindings/nodejs node:${{ matrix.node }}-alpine yarn test
|
||||
test-linux-aarch64-gnu-binding:
|
||||
name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }}
|
||||
needs:
|
||||
- build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node:
|
||||
- '18'
|
||||
- '20'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-aarch64-unknown-linux-gnu
|
||||
path: bindings/nodejs/
|
||||
- name: List packages
|
||||
run: ls -R .
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn config set supportedArchitectures.cpu "arm64"
|
||||
yarn config set supportedArchitectures.libc "glibc"
|
||||
yarn install
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- name: Setup and run tests
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:${{ matrix.node }}-slim
|
||||
options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
|
||||
run: |
|
||||
set -e
|
||||
yarn test
|
||||
ls -la
|
||||
test-linux-aarch64-musl-binding:
|
||||
name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }}
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-aarch64-unknown-linux-musl
|
||||
path: bindings/nodejs/
|
||||
- name: List packages
|
||||
run: ls -R .
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn config set supportedArchitectures.cpu "arm64"
|
||||
yarn config set supportedArchitectures.libc "musl"
|
||||
yarn install
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- name: Setup and run tests
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:lts-alpine
|
||||
options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
|
||||
run: |
|
||||
set -e
|
||||
yarn test
|
||||
test-linux-arm-gnueabihf-binding:
|
||||
name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }}
|
||||
needs:
|
||||
- build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node:
|
||||
- '18'
|
||||
- '20'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-armv7-unknown-linux-gnueabihf
|
||||
path: bindings/nodejs/
|
||||
- name: List packages
|
||||
run: ls -R .
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn config set supportedArchitectures.cpu "arm"
|
||||
yarn install
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm
|
||||
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- name: Setup and run tests
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:${{ matrix.node }}-bullseye-slim
|
||||
options: '--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
|
||||
run: |
|
||||
set -e
|
||||
yarn test
|
||||
ls -la
|
||||
universal-macOS:
|
||||
name: Build universal macOS binary
|
||||
needs:
|
||||
- build
|
||||
runs-on: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
cache-dependency-path: "bindings/nodejs"
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download macOS x64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-apple-darwin
|
||||
path: bindings/nodejs/artifacts
|
||||
- name: Download macOS arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bindings-aarch64-apple-darwin
|
||||
path: bindings/nodejs/artifacts
|
||||
- name: Combine binaries
|
||||
run: yarn universal
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-universal-apple-darwin
|
||||
path: bindings/nodejs/${{ env.APP_NAME }}.*.node
|
||||
if-no-files-found: error
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./bindings/nodejs
|
||||
needs:
|
||||
- test-macOS-windows-binding
|
||||
- test-linux-x64-gnu-binding
|
||||
- test-linux-x64-musl-binding
|
||||
- test-linux-aarch64-gnu-binding
|
||||
- test-linux-aarch64-musl-binding
|
||||
- test-linux-arm-gnueabihf-binding
|
||||
- universal-macOS
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
cache-dependency-path: "bindings/nodejs"
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: bindings/nodejs/artifacts
|
||||
- name: Move artifacts
|
||||
run: yarn artifacts
|
||||
- name: List packages
|
||||
run: ls -R ./npm
|
||||
shell: bash
|
||||
- name: Publish
|
||||
run: |
|
||||
echo "${{ github.event.inputs.publish }}"
|
||||
if [ "${{ github.event.inputs.publish }}" = "true" ]; then
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
npm publish --access public
|
||||
echo "Published to npm"
|
||||
else
|
||||
echo "Not a release, skipping publish"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
141
.github/workflows/pypi.yml
vendored
Normal file
141
.github/workflows/pypi.yml
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
name: Upload component to Python Package Index
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
manylinux: auto
|
||||
working-directory: bindings/python
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-${{ runner.os }}-${{ matrix.target }}
|
||||
path: bindings/python/dist
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x64, x86]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
architecture: ${{ matrix.target }}
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
working-directory: bindings/python
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-${{ runner.os }}-${{ matrix.target }}
|
||||
path: bindings/python/dist
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, aarch64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
working-directory: bindings/python
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-${{ runner.os }}-${{ matrix.target }}
|
||||
path: bindings/python/dist
|
||||
|
||||
sdist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
working-directory: bindings/python
|
||||
- name: Upload sdist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-${{ runner.os }}-sdist
|
||||
path: bindings/python/dist
|
||||
|
||||
publish-to-test-pypi:
|
||||
if: ${{ github.event.inputs.release != 'true' }}
|
||||
name: >-
|
||||
Publish Python 🐍 distribution 📦 to Test PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs: [linux, windows, macos, sdist]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: bindings/python/
|
||||
- name: Publish distribution 📦 to Test PyPI
|
||||
uses: PyO3/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TEST_API_TOKEN }}
|
||||
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
|
||||
with:
|
||||
command: upload
|
||||
args: "--skip-existing **/*.whl"
|
||||
working-directory: bindings/python
|
||||
|
||||
publish-pypi:
|
||||
if: ${{ github.event.inputs.release == 'true' }}
|
||||
name: >-
|
||||
Publish Python 🐍 distribution 📦 to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs: [linux, windows, macos, sdist]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: bindings/python/
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: PyO3/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
|
||||
with:
|
||||
command: upload
|
||||
args: "--skip-existing **/*.whl"
|
||||
working-directory: bindings/python
|
||||
10
.github/workflows/test-coverage.yaml
vendored
10
.github/workflows/test-coverage.yaml
vendored
@@ -1,10 +1,7 @@
|
||||
name: Coverage
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
@@ -20,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
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
target/*
|
||||
.DS_Store
|
||||
**/node_modules/**
|
||||
.DS_Store
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -7,11 +7,21 @@
|
||||
- New function UNICODE ([#128](https://github.com/ironcalc/IronCalc/pull/128))
|
||||
- New document server (Thanks Dani!)
|
||||
- New function FORMULATEXT
|
||||
- Name Manager ([#212](https://github.com/ironcalc/IronCalc/pull/212) [#220](https://github.com/ironcalc/IronCalc/pull/220))
|
||||
- Add context menu. We can now insert rows and columns. Freeze and unfreeze rows and columns. Delete rows and columns [#271]
|
||||
- Add nodejs bindings [#254]
|
||||
- Add python bindings for all platforms
|
||||
- Add is split into the product and widget
|
||||
- Add Python documentation [#260]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed several issues with pasting content
|
||||
- Fixed several issues with borders
|
||||
- Fixed bug where columns and rows could be resized to negative width and height, respectively
|
||||
- Undo/redo when add/delete sheet now works [#270]
|
||||
- Numerous small fixes
|
||||
- Multiple fixes to the documentation
|
||||
|
||||
## [0.2.0] - 2024-11-06 (The HN release)
|
||||
|
||||
|
||||
145
Cargo.lock
generated
145
Cargo.lock
generated
@@ -85,6 +85,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -210,6 +216,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
@@ -271,6 +286,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@@ -389,7 +414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc"
|
||||
version = "0.2.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bitcode",
|
||||
"chrono",
|
||||
@@ -405,7 +430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc_base"
|
||||
version = "0.2.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bitcode",
|
||||
"chrono",
|
||||
@@ -421,6 +446,17 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc_nodejs"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"ironcalc",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@@ -460,6 +496,16 @@ version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
@@ -490,6 +536,65 @@ dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.16.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"napi-derive",
|
||||
"napi-sys",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "2.16.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -616,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.5.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.5.0"
|
||||
dependencies = [
|
||||
"ironcalc_base",
|
||||
"serde",
|
||||
|
||||
@@ -6,10 +6,12 @@ members = [
|
||||
"xlsx",
|
||||
"bindings/wasm",
|
||||
"bindings/python",
|
||||
"bindings/nodejs",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"generate_locale",
|
||||
"webapp/app.ironcalc.com/server",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
||||
6
Makefile
6
Makefile
@@ -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:
|
||||
@@ -10,11 +11,12 @@ format:
|
||||
|
||||
.PHONY: tests
|
||||
tests: lint
|
||||
cargo test
|
||||
make remove-artifacts
|
||||
# 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
|
||||
|
||||
@@ -77,7 +77,7 @@ And visit <http://0.0.0.0:8000/ironcalc/>
|
||||
Add the dependency to `Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"}
|
||||
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.5"}
|
||||
```
|
||||
|
||||
And then use this code in `main.rs`:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "ironcalc_base"
|
||||
version = "0.2.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
homepage = "https://www.ironcalc.com"
|
||||
repository = "https://github.com/ironcalc/ironcalc/"
|
||||
description = "The democratization of spreadsheets"
|
||||
description = "Open source spreadsheet engine"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use crate::model::Model;
|
||||
|
||||
// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
|
||||
@@ -8,16 +9,45 @@ use crate::model::Model;
|
||||
// I feel this is unimportant for now.
|
||||
|
||||
impl Model {
|
||||
fn shift_cell_formula(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
displace_data: &DisplaceData,
|
||||
) -> Result<(), String> {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.and_then(|c| c.get_formula())
|
||||
{
|
||||
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// FIXME: This is not a very performant way if the formula has changed :S.
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
|
||||
if formula != formula_displaced {
|
||||
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
|
||||
fn displace_cells(&mut self, displace_data: &DisplaceData) {
|
||||
fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> {
|
||||
let cells = self.get_all_cells();
|
||||
for cell in cells {
|
||||
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data);
|
||||
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
|
||||
@@ -134,7 +164,34 @@ impl Model {
|
||||
column,
|
||||
delta: column_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
|
||||
// In the list of columns:
|
||||
// * Keep all the columns to the left
|
||||
// * Displace all the columns to the right
|
||||
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
|
||||
let mut new_columns = Vec::new();
|
||||
for col in worksheet.cols.iter_mut() {
|
||||
// range under study
|
||||
let min = col.min;
|
||||
let max = col.max;
|
||||
if column > max {
|
||||
// If the range under study is to our left, this is a noop
|
||||
} else if column <= min {
|
||||
// If the range under study is to our right, we displace it
|
||||
col.min = min + column_count;
|
||||
col.max = max + column_count;
|
||||
} else {
|
||||
// If the range under study is in the middle we augment it
|
||||
col.max = max + column_count;
|
||||
}
|
||||
new_columns.push(col.clone());
|
||||
}
|
||||
// TODO: If in a row the cell to the right and left have the same style we should copy it
|
||||
|
||||
worksheet.cols = new_columns;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -187,7 +244,7 @@ impl Model {
|
||||
column,
|
||||
delta: -column_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
|
||||
// deletes all the column styles
|
||||
@@ -311,7 +368,7 @@ impl Model {
|
||||
row,
|
||||
delta: row_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -372,7 +429,7 @@ impl Model {
|
||||
row,
|
||||
delta: -row_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -393,14 +450,14 @@ impl Model {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
) -> Result<(), &'static str> {
|
||||
) -> Result<(), String> {
|
||||
// Check boundaries
|
||||
let target_column = column + delta;
|
||||
if !(1..=LAST_COLUMN).contains(&target_column) {
|
||||
return Err("Target column out of boundaries");
|
||||
return Err("Target column out of boundaries".to_string());
|
||||
}
|
||||
if !(1..=LAST_COLUMN).contains(&column) {
|
||||
return Err("Initial column out of boundaries");
|
||||
return Err("Initial column out of boundaries".to_string());
|
||||
}
|
||||
|
||||
// TODO: Add the actual displacement of data and styles
|
||||
@@ -412,7 +469,7 @@ impl Model {
|
||||
column,
|
||||
delta,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
158
base/src/arithmetic.rs
Normal file
158
base/src/arithmetic.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
cast::NumberOrArray,
|
||||
expressions::{
|
||||
parser::{ArrayNode, Node},
|
||||
token::Error,
|
||||
types::CellReferenceIndex,
|
||||
},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
/// Unify how we map booleans/strings to f64
|
||||
fn to_f64(value: &ArrayNode) -> Result<f64, Error> {
|
||||
match value {
|
||||
ArrayNode::Number(f) => Ok(*f),
|
||||
ArrayNode::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
|
||||
ArrayNode::String(s) => match s.parse::<f64>() {
|
||||
Ok(f) => Ok(f),
|
||||
Err(_) => Err(Error::VALUE),
|
||||
},
|
||||
ArrayNode::Error(err) => Err(err.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Applies `op` element‐wise for arrays/numbers.
|
||||
pub(crate) fn handle_arithmetic(
|
||||
&mut self,
|
||||
left: &Node,
|
||||
right: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
op: &dyn Fn(f64, f64) -> Result<f64, Error>,
|
||||
) -> CalcResult {
|
||||
let l = match self.get_number_or_array(left, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
let r = match self.get_number_or_array(right, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
match (l, r) {
|
||||
// -----------------------------------------------------
|
||||
// Case 1: Both are numbers
|
||||
// -----------------------------------------------------
|
||||
(NumberOrArray::Number(f1), NumberOrArray::Number(f2)) => match op(f1, f2) {
|
||||
Ok(x) => CalcResult::Number(x),
|
||||
Err(Error::DIV) => CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Divide by 0".to_string(),
|
||||
},
|
||||
Err(Error::VALUE) => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid number".to_string(),
|
||||
},
|
||||
Err(e) => CalcResult::Error {
|
||||
error: e,
|
||||
origin: cell,
|
||||
message: "Unknown error".to_string(),
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Case 2: left is Number, right is Array
|
||||
// -----------------------------------------------------
|
||||
(NumberOrArray::Number(f1), NumberOrArray::Array(a2)) => {
|
||||
let mut array = Vec::new();
|
||||
for row in a2 {
|
||||
let mut data_row = Vec::new();
|
||||
for node in row {
|
||||
match to_f64(&node) {
|
||||
Ok(f2) => match op(f1, f2) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
Err(err) => data_row.push(ArrayNode::Error(err)),
|
||||
}
|
||||
}
|
||||
array.push(data_row);
|
||||
}
|
||||
CalcResult::Array(array)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Case 3: left is Array, right is Number
|
||||
// -----------------------------------------------------
|
||||
(NumberOrArray::Array(a1), NumberOrArray::Number(f2)) => {
|
||||
let mut array = Vec::new();
|
||||
for row in a1 {
|
||||
let mut data_row = Vec::new();
|
||||
for node in row {
|
||||
match to_f64(&node) {
|
||||
Ok(f1) => match op(f1, f2) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
Err(err) => data_row.push(ArrayNode::Error(err)),
|
||||
}
|
||||
}
|
||||
array.push(data_row);
|
||||
}
|
||||
CalcResult::Array(array)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Case 4: Both are arrays
|
||||
// -----------------------------------------------------
|
||||
(NumberOrArray::Array(a1), NumberOrArray::Array(a2)) => {
|
||||
let n1 = a1.len();
|
||||
let m1 = a1.first().map(|r| r.len()).unwrap_or(0);
|
||||
let n2 = a2.len();
|
||||
let m2 = a2.first().map(|r| r.len()).unwrap_or(0);
|
||||
let n = n1.max(n2);
|
||||
let m = m1.max(m2);
|
||||
|
||||
let mut array = Vec::new();
|
||||
for i in 0..n {
|
||||
let row1 = a1.get(i);
|
||||
let row2 = a2.get(i);
|
||||
|
||||
let mut data_row = Vec::new();
|
||||
for j in 0..m {
|
||||
let val1 = row1.and_then(|r| r.get(j));
|
||||
let val2 = row2.and_then(|r| r.get(j));
|
||||
|
||||
match (val1, val2) {
|
||||
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
|
||||
(Ok(f1), Ok(f2)) => match op(f1, f2) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => {
|
||||
data_row.push(ArrayNode::Error(Error::VALUE))
|
||||
}
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
// Mismatched dimensions => #VALUE!
|
||||
_ => data_row.push(ArrayNode::Error(Error::VALUE)),
|
||||
}
|
||||
}
|
||||
array.push(data_row);
|
||||
}
|
||||
CalcResult::Array(array)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::expressions::{token::Error, types::CellReferenceIndex};
|
||||
use crate::expressions::{parser::ArrayNode, token::Error, types::CellReferenceIndex};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Range {
|
||||
pub left: CellReferenceIndex,
|
||||
pub right: CellReferenceIndex,
|
||||
@@ -23,6 +24,7 @@ pub(crate) enum CalcResult {
|
||||
},
|
||||
EmptyCell,
|
||||
EmptyArg,
|
||||
Array(Vec<Vec<ArrayNode>>),
|
||||
}
|
||||
|
||||
impl CalcResult {
|
||||
|
||||
147
base/src/cast.rs
147
base/src/cast.rs
@@ -1,11 +1,85 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, Range},
|
||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||
implicit_intersection::implicit_intersection,
|
||||
expressions::{
|
||||
parser::{ArrayNode, Node},
|
||||
token::Error,
|
||||
types::CellReferenceIndex,
|
||||
},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
pub(crate) enum NumberOrArray {
|
||||
Number(f64),
|
||||
Array(Vec<Vec<ArrayNode>>),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn get_number_or_array(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<NumberOrArray, CalcResult> {
|
||||
match self.evaluate_node_in_context(node, cell) {
|
||||
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
|
||||
CalcResult::String(s) => match s.parse::<f64>() {
|
||||
Ok(f) => Ok(NumberOrArray::Number(f)),
|
||||
_ => Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expecting number".to_string(),
|
||||
)),
|
||||
},
|
||||
CalcResult::Boolean(f) => {
|
||||
if f {
|
||||
Ok(NumberOrArray::Number(1.0))
|
||||
} else {
|
||||
Ok(NumberOrArray::Number(0.0))
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(NumberOrArray::Number(0.0)),
|
||||
CalcResult::Range { left, right } => {
|
||||
let sheet = left.sheet;
|
||||
if sheet != right.sheet {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "3D ranges are not allowed".to_string(),
|
||||
});
|
||||
}
|
||||
// we need to convert the range into an array
|
||||
let mut array = Vec::new();
|
||||
for row in left.row..=right.row {
|
||||
let mut row_data = Vec::new();
|
||||
for column in left.column..=right.column {
|
||||
let value =
|
||||
match self.evaluate_cell(CellReferenceIndex { sheet, column, row }) {
|
||||
CalcResult::String(s) => ArrayNode::String(s),
|
||||
CalcResult::Number(f) => ArrayNode::Number(f),
|
||||
CalcResult::Boolean(b) => ArrayNode::Boolean(b),
|
||||
CalcResult::Error { error, .. } => ArrayNode::Error(error),
|
||||
CalcResult::Range { .. } => {
|
||||
// if we do things right this can never happen.
|
||||
// the evaluation of a cell should never return a range
|
||||
ArrayNode::Number(0.0)
|
||||
}
|
||||
CalcResult::EmptyCell => ArrayNode::Number(0.0),
|
||||
CalcResult::EmptyArg => ArrayNode::Number(0.0),
|
||||
CalcResult::Array(_) => {
|
||||
// if we do things right this can never happen.
|
||||
// the evaluation of a cell should never return an array
|
||||
ArrayNode::Number(0.0)
|
||||
}
|
||||
};
|
||||
row_data.push(value);
|
||||
}
|
||||
array.push(row_data);
|
||||
}
|
||||
Ok(NumberOrArray::Array(array))
|
||||
}
|
||||
CalcResult::Array(s) => Ok(NumberOrArray::Array(s)),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
}
|
||||
}
|
||||
pub(crate) fn get_number(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
@@ -39,19 +113,16 @@ impl Model {
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_number(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid reference (number)".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
CalcResult::Range { .. } => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
CalcResult::Array(_) => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,19 +170,16 @@ impl Model {
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_string(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid reference (string)".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
CalcResult::Range { .. } => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
CalcResult::Array(_) => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,19 +219,16 @@ impl Model {
|
||||
CalcResult::Boolean(b) => Ok(b),
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_bool(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid reference (bool)".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
CalcResult::Range { .. } => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
CalcResult::Array(_) => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ impl Cell {
|
||||
Cell::CellFormulaNumber { s, .. } => *s = style,
|
||||
Cell::CellFormulaString { s, .. } => *s = style,
|
||||
Cell::CellFormulaError { s, .. } => *s = style,
|
||||
// Should we throw an error here?
|
||||
Cell::Merged { .. } => {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,6 +106,8 @@ impl Cell {
|
||||
Cell::CellFormulaNumber { s, .. } => *s,
|
||||
Cell::CellFormulaString { s, .. } => *s,
|
||||
Cell::CellFormulaError { s, .. } => *s,
|
||||
// A merged cell has no style
|
||||
Cell::Merged { .. } => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +123,7 @@ impl Cell {
|
||||
Cell::CellFormulaNumber { .. } => CellType::Number,
|
||||
Cell::CellFormulaString { .. } => CellType::Text,
|
||||
Cell::CellFormulaError { .. } => CellType::ErrorValue,
|
||||
Cell::Merged { .. } => CellType::Number,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +161,7 @@ impl Cell {
|
||||
let v = ei.to_localized_error_string(language);
|
||||
CellValue::String(v)
|
||||
}
|
||||
Cell::Merged { .. } => CellValue::None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ pub(crate) const DEFAULT_COLUMN_WIDTH: f64 = 125.0;
|
||||
pub(crate) const DEFAULT_ROW_HEIGHT: f64 = 28.0;
|
||||
pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0;
|
||||
pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0;
|
||||
pub(crate) const DEFAULT_WINDOW_HEIGH: i64 = 600;
|
||||
pub(crate) const DEFAULT_WINDOW_HEIGHT: i64 = 600;
|
||||
pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
|
||||
|
||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
use crate::{
|
||||
expressions::{
|
||||
parser::{
|
||||
move_formula::ref_is_in_area,
|
||||
stringify::{to_string, to_string_displaced, DisplaceData},
|
||||
walk::forward_references,
|
||||
},
|
||||
types::{Area, CellReferenceIndex, CellReferenceRC},
|
||||
},
|
||||
model::Model,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum CellValue {
|
||||
Value(String),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetCellValue {
|
||||
cell: CellReferenceIndex,
|
||||
new_value: CellValue,
|
||||
old_value: CellValue,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
#[allow(clippy::expect_used)]
|
||||
pub(crate) fn shift_cell_formula(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
displace_data: &DisplaceData,
|
||||
) {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(sheet)
|
||||
.expect("Worksheet must exist")
|
||||
.cell(row, column)
|
||||
.expect("Cell must exist")
|
||||
.get_formula()
|
||||
{
|
||||
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// FIXME: This is not a very performant way if the formula has changed :S.
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
|
||||
if formula != formula_displaced {
|
||||
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))
|
||||
.expect("Failed to shift cell formula");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
pub fn forward_references(
|
||||
&mut self,
|
||||
source_area: &Area,
|
||||
target: &CellReferenceIndex,
|
||||
) -> Result<Vec<SetCellValue>, String> {
|
||||
let mut diff_list: Vec<SetCellValue> = Vec::new();
|
||||
let target_area = &Area {
|
||||
sheet: target.sheet,
|
||||
row: target.row,
|
||||
column: target.column,
|
||||
width: source_area.width,
|
||||
height: source_area.height,
|
||||
};
|
||||
// Walk over every formula
|
||||
let cells = self.get_all_cells();
|
||||
for cell in cells {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(cell.index)
|
||||
.expect("Worksheet must exist")
|
||||
.cell(cell.row, cell.column)
|
||||
.expect("Cell must exist")
|
||||
.get_formula()
|
||||
{
|
||||
let sheet = cell.index;
|
||||
let row = cell.row;
|
||||
let column = cell.column;
|
||||
|
||||
// If cell is in the source or target area, skip
|
||||
if ref_is_in_area(sheet, row, column, source_area)
|
||||
|| ref_is_in_area(sheet, row, column, target_area)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the formula
|
||||
// Get a copy of the AST
|
||||
let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
column: cell.column,
|
||||
row: cell.row,
|
||||
};
|
||||
let context = CellReferenceIndex { sheet, column, row };
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
|
||||
forward_references(
|
||||
node,
|
||||
&context,
|
||||
source_area,
|
||||
target.sheet,
|
||||
target_sheet_name,
|
||||
target.row,
|
||||
target.column,
|
||||
);
|
||||
|
||||
// If the string representation of the formula has changed update the cell
|
||||
let updated_formula = to_string(node, &cell_reference);
|
||||
if formula != updated_formula {
|
||||
self.update_cell_with_formula(
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
format!("={updated_formula}"),
|
||||
)?;
|
||||
// Update the diff list
|
||||
diff_list.push(SetCellValue {
|
||||
cell: CellReferenceIndex { sheet, column, row },
|
||||
new_value: CellValue::Value(format!("={}", updated_formula)),
|
||||
old_value: CellValue::Value(format!("={}", formula)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(diff_list)
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,7 @@ impl Lexer {
|
||||
']' => TokenType::RightBracket,
|
||||
':' => TokenType::Colon,
|
||||
';' => TokenType::Semicolon,
|
||||
'@' => TokenType::At,
|
||||
',' => {
|
||||
if self.locale.numbers.symbols.decimal == "," {
|
||||
match self.consume_number(',') {
|
||||
|
||||
@@ -23,19 +23,19 @@ impl Lexer {
|
||||
// TODO(TD): There are better ways of doing this :)
|
||||
let rest_of_formula: String = self.chars[self.position..self.len].iter().collect();
|
||||
let specifier = if rest_of_formula.starts_with("#This Row]") {
|
||||
self.position += "#This Row]".bytes().len();
|
||||
self.position += "#This Row]".len();
|
||||
TableSpecifier::ThisRow
|
||||
} else if rest_of_formula.starts_with("#All]") {
|
||||
self.position += "#All]".bytes().len();
|
||||
self.position += "#All]".len();
|
||||
TableSpecifier::All
|
||||
} else if rest_of_formula.starts_with("#Data]") {
|
||||
self.position += "#Data]".bytes().len();
|
||||
self.position += "#Data]".len();
|
||||
TableSpecifier::Data
|
||||
} else if rest_of_formula.starts_with("#Headers]") {
|
||||
self.position += "#Headers]".bytes().len();
|
||||
self.position += "#Headers]".len();
|
||||
TableSpecifier::Headers
|
||||
} else if rest_of_formula.starts_with("#Totals]") {
|
||||
self.position += "#Totals]".bytes().len();
|
||||
self.position += "#Totals]".len();
|
||||
TableSpecifier::Totals
|
||||
} else {
|
||||
return Err(LexerError {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod test_common;
|
||||
mod test_implicit_intersection;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_ranges;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType::*,
|
||||
};
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
fn new_lexer(formula: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_implicit_intersection() {
|
||||
let mut lx = new_lexer("sum(@A1:A3)");
|
||||
assert_eq!(lx.next_token(), Ident("sum".to_string()));
|
||||
assert_eq!(lx.next_token(), LeftParenthesis);
|
||||
assert_eq!(lx.next_token(), At);
|
||||
assert!(matches!(lx.next_token(), Range { .. }));
|
||||
assert_eq!(lx.next_token(), RightParenthesis);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
@@ -24,6 +24,25 @@ fn test_get_tokens() {
|
||||
assert_eq!(l.end, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_tokens_unicode() {
|
||||
let formula = "'🇵🇭 Philippines'!A1";
|
||||
let t = get_tokens(formula);
|
||||
assert_eq!(t.len(), 1);
|
||||
|
||||
let expected = TokenType::Reference {
|
||||
sheet: Some("🇵🇭 Philippines".to_string()),
|
||||
row: 1,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
};
|
||||
let l = t.first().expect("expected token");
|
||||
assert_eq!(l.token, expected);
|
||||
assert_eq!(l.start, 0);
|
||||
assert_eq!(l.end, 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
# GRAMAR
|
||||
# GRAMMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
@@ -12,7 +12,8 @@ term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
range => implicit (':' primary)?
|
||||
implicit=> '@' primary | primary
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
@@ -45,19 +46,11 @@ use super::utils::number_to_column;
|
||||
use token::OpCompare;
|
||||
|
||||
pub mod move_formula;
|
||||
pub mod static_analysis;
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ranges;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_move_formula;
|
||||
#[cfg(test)]
|
||||
mod test_tables;
|
||||
mod tests;
|
||||
|
||||
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
|
||||
let mut lexer = lexer::Lexer::new(
|
||||
@@ -89,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
|
||||
None
|
||||
}
|
||||
|
||||
// DefinedNameS is a tuple with the name of the defined name, the index of the sheet and the formula
|
||||
pub type DefinedNameS = (String, Option<u32>, String);
|
||||
|
||||
pub(crate) struct Reference<'a> {
|
||||
sheet_name: &'a Option<String>,
|
||||
sheet_index: u32,
|
||||
@@ -98,6 +94,14 @@ pub(crate) struct Reference<'a> {
|
||||
column: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum ArrayNode {
|
||||
Boolean(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
Error(token::Error),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum Node {
|
||||
BooleanKind(bool),
|
||||
@@ -171,8 +175,14 @@ pub enum Node {
|
||||
name: String,
|
||||
args: Vec<Node>,
|
||||
},
|
||||
ArrayKind(Vec<Node>),
|
||||
VariableKind(String),
|
||||
ArrayKind(Vec<Vec<ArrayNode>>),
|
||||
DefinedNameKind(DefinedNameS),
|
||||
TableNameKind(String),
|
||||
WrongVariableKind(String),
|
||||
ImplicitIntersection {
|
||||
automatic: bool,
|
||||
child: Box<Node>,
|
||||
},
|
||||
CompareKind {
|
||||
kind: OpCompare,
|
||||
left: Box<Node>,
|
||||
@@ -195,12 +205,17 @@ pub enum Node {
|
||||
pub struct Parser {
|
||||
lexer: lexer::Lexer,
|
||||
worksheets: Vec<String>,
|
||||
context: Option<CellReferenceRC>,
|
||||
defined_names: Vec<DefinedNameS>,
|
||||
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<DefinedNameS>,
|
||||
tables: HashMap<String, Table>,
|
||||
) -> Parser {
|
||||
let lexer = lexer::Lexer::new(
|
||||
"",
|
||||
lexer::LexerMode::A1,
|
||||
@@ -209,10 +224,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,
|
||||
}
|
||||
}
|
||||
@@ -220,13 +241,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<DefinedNameS>,
|
||||
) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -240,6 +266,24 @@ impl Parser {
|
||||
None
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// * None: If there is no defined name by that name
|
||||
// * Some((Some(index), formula)): If there is a defined name local to that sheet
|
||||
// * Some(None): If there is a global defined name
|
||||
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
|
||||
for (df_name, df_scope, df_formula) in &self.defined_names {
|
||||
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
|
||||
return Some((*df_scope, df_formula.to_owned()));
|
||||
}
|
||||
}
|
||||
for (df_name, df_scope, df_formula) in &self.defined_names {
|
||||
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
|
||||
return Some((None, df_formula.to_owned()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_expr(&mut self) -> Node {
|
||||
let mut t = self.parse_concat();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
@@ -383,7 +427,7 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn parse_range(&mut self) -> Node {
|
||||
let t = self.parse_primary();
|
||||
let t = self.parse_implicit();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
@@ -402,6 +446,65 @@ impl Parser {
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_implicit(&mut self) -> Node {
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::At {
|
||||
self.lexer.advance_token();
|
||||
let t = self.parse_primary();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
return Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(t),
|
||||
};
|
||||
}
|
||||
self.parse_primary()
|
||||
}
|
||||
|
||||
fn parse_array_row(&mut self) -> Result<Vec<ArrayNode>, Node> {
|
||||
let mut row = Vec::new();
|
||||
// and array can only have numbers, string or booleans
|
||||
// otherwise it is a syntax error
|
||||
let first_element = match self.parse_expr() {
|
||||
Node::BooleanKind(s) => ArrayNode::Boolean(s),
|
||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||
Node::StringKind(s) => ArrayNode::String(s),
|
||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
||||
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||
_ => {
|
||||
return Err(Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
message: "Invalid value in array".to_string(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
});
|
||||
}
|
||||
};
|
||||
row.push(first_element);
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
// FIXME: this is not respecting the locale
|
||||
while next_token == TokenType::Comma {
|
||||
self.lexer.advance_token();
|
||||
let value = match self.parse_expr() {
|
||||
Node::BooleanKind(s) => ArrayNode::Boolean(s),
|
||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||
Node::StringKind(s) => ArrayNode::String(s),
|
||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
||||
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||
_ => {
|
||||
return Err(Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
message: "Invalid value in array".to_string(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
});
|
||||
}
|
||||
};
|
||||
row.push(value);
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Node {
|
||||
let next_token = self.lexer.next_token();
|
||||
match next_token {
|
||||
@@ -423,21 +526,35 @@ impl Parser {
|
||||
TokenType::Number(s) => Node::NumberKind(s),
|
||||
TokenType::String(s) => Node::StringKind(s),
|
||||
TokenType::LeftBrace => {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
// It's an array. It's a collection of rows all of the same dimension
|
||||
|
||||
let first_row = match self.parse_array_row() {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let length = first_row.len();
|
||||
|
||||
let mut matrix = Vec::new();
|
||||
matrix.push(first_row);
|
||||
// FIXME: this is not respecting the locale
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
let mut args: Vec<Node> = vec![t];
|
||||
while next_token == TokenType::Semicolon {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
let row = match self.parse_array_row() {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
args.push(p);
|
||||
if row.len() != length {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "All rows in an array should be the same length".to_string(),
|
||||
};
|
||||
}
|
||||
matrix.push(row);
|
||||
}
|
||||
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightBrace) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
@@ -445,7 +562,7 @@ impl Parser {
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
Node::ArrayKind(args)
|
||||
Node::ArrayKind(matrix)
|
||||
}
|
||||
TokenType::Reference {
|
||||
sheet,
|
||||
@@ -454,16 +571,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),
|
||||
@@ -498,16 +606,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),
|
||||
@@ -522,20 +621,6 @@ impl Parser {
|
||||
let mut absolute_row1 = left.absolute_row;
|
||||
let mut absolute_row2 = right.absolute_row;
|
||||
|
||||
if self.lexer.is_a1_mode() {
|
||||
if !left.absolute_row {
|
||||
row1 -= context.row
|
||||
};
|
||||
if !left.absolute_column {
|
||||
column1 -= context.column
|
||||
};
|
||||
if !right.absolute_row {
|
||||
row2 -= context.row
|
||||
};
|
||||
if !right.absolute_column {
|
||||
column2 -= context.column
|
||||
};
|
||||
}
|
||||
if row1 > row2 {
|
||||
(row2, row1) = (row1, row2);
|
||||
(absolute_row2, absolute_row1) = (absolute_row1, absolute_row2);
|
||||
@@ -544,6 +629,22 @@ impl Parser {
|
||||
(column2, column1) = (column1, column2);
|
||||
(absolute_column2, absolute_column1) = (absolute_column1, absolute_column2);
|
||||
}
|
||||
|
||||
if self.lexer.is_a1_mode() {
|
||||
if !absolute_row1 {
|
||||
row1 -= context.row
|
||||
};
|
||||
if !absolute_column1 {
|
||||
column1 -= context.column
|
||||
};
|
||||
if !absolute_row2 {
|
||||
row2 -= context.row
|
||||
};
|
||||
if !absolute_column2 {
|
||||
column2 -= context.column
|
||||
};
|
||||
}
|
||||
|
||||
match sheet_index {
|
||||
Some(index) => Node::RangeKind {
|
||||
sheet_name: sheet,
|
||||
@@ -591,11 +692,47 @@ impl Parser {
|
||||
kind: function_kind,
|
||||
args,
|
||||
};
|
||||
} else {
|
||||
return Node::InvalidFunctionKind { name, args };
|
||||
}
|
||||
if &name == "_xlfn.SINGLE" {
|
||||
if args.len() != 1 {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "Implicit Intersection requires just one argument"
|
||||
.to_string(),
|
||||
};
|
||||
}
|
||||
return Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(args[0].clone()),
|
||||
};
|
||||
}
|
||||
return Node::InvalidFunctionKind { name, args };
|
||||
}
|
||||
let context = &self.context;
|
||||
|
||||
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, formula)) = self.get_defined_name(&name, context_sheet_index) {
|
||||
return Node::DefinedNameKind((name, scope, formula));
|
||||
}
|
||||
let name_lower = name.to_lowercase();
|
||||
for table_name in self.tables.keys() {
|
||||
if table_name.to_lowercase() == name_lower {
|
||||
return Node::TableNameKind(name);
|
||||
}
|
||||
}
|
||||
Node::VariableKind(name)
|
||||
Node::WrongVariableKind(name)
|
||||
}
|
||||
TokenType::Error(kind) => Node::ErrorKind(kind),
|
||||
TokenType::Illegal(error) => Node::ParseErrorKind {
|
||||
@@ -608,7 +745,38 @@ impl Parser {
|
||||
position: 0,
|
||||
message: "Unexpected end of input.".to_string(),
|
||||
},
|
||||
TokenType::Boolean(value) => Node::BooleanKind(value),
|
||||
TokenType::Boolean(value) => {
|
||||
// Could be a function call "TRUE()"
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::LeftParenthesis {
|
||||
self.lexer.advance_token();
|
||||
// We parse all the arguments, although technically this is moot
|
||||
// But is has the upside of transforming `=TRUE( 4 )` into `=TRUE(4)`
|
||||
let args = match self.parse_function_args() {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
if value {
|
||||
return Node::FunctionKind {
|
||||
kind: Function::True,
|
||||
args,
|
||||
};
|
||||
} else {
|
||||
return Node::FunctionKind {
|
||||
kind: Function::False,
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
Node::BooleanKind(value)
|
||||
}
|
||||
TokenType::Compare(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
@@ -641,6 +809,14 @@ impl Parser {
|
||||
message: "Unexpected token: 'POWER'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::At => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: '@'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::RightParenthesis
|
||||
| TokenType::RightBracket
|
||||
| TokenType::Colon
|
||||
@@ -667,187 +843,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
stringify::{stringify_reference, DisplaceData},
|
||||
Node, Reference,
|
||||
ArrayNode, Node, Reference,
|
||||
};
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
@@ -56,6 +56,15 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
|
||||
match node {
|
||||
ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
ArrayNode::Number(number) => to_excel_precision_str(*number),
|
||||
ArrayNode::String(value) => format!("\"{}\"", value),
|
||||
ArrayNode::Error(kind) => format!("{}", kind),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
@@ -362,20 +371,43 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
move_function(name, args, move_context)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
// This code is a placeholder. Arrays are not yet implemented
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
|
||||
let mut first_row = true;
|
||||
let mut matrix_string = String::new();
|
||||
|
||||
// Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
|
||||
for row in args {
|
||||
if !first_row {
|
||||
matrix_string.push(',');
|
||||
} else {
|
||||
first = false;
|
||||
arguments = to_string_moved(el, move_context);
|
||||
first_row = false;
|
||||
}
|
||||
|
||||
// Build the string for the current row
|
||||
let mut first_col = true;
|
||||
let mut row_string = String::new();
|
||||
for el in row {
|
||||
if !first_col {
|
||||
row_string.push(',');
|
||||
} else {
|
||||
first_col = false;
|
||||
}
|
||||
|
||||
// Reuse your existing element-stringification function
|
||||
row_string.push_str(&to_string_array_node(el));
|
||||
}
|
||||
|
||||
// Enclose the row in braces
|
||||
matrix_string.push('{');
|
||||
matrix_string.push_str(&row_string);
|
||||
matrix_string.push('}');
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
|
||||
// Enclose the whole matrix in braces
|
||||
format!("{{{}}}", matrix_string)
|
||||
}
|
||||
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),
|
||||
@@ -393,5 +425,11 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
position: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
format!("@{}", to_string_moved(child, move_context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
984
base/src/expressions/parser/static_analysis.rs
Normal file
984
base/src/expressions/parser/static_analysis.rs
Normal file
@@ -0,0 +1,984 @@
|
||||
use crate::functions::Function;
|
||||
|
||||
use super::Node;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
|
||||
|
||||
fn is_range_reference(s: &str) -> bool {
|
||||
RE.is_match(s)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
# NOTES on the Implicit Intersection operator: @
|
||||
|
||||
Sometimes we obtain a range where we expected a single argument. This can happen:
|
||||
|
||||
* As an argument of a function, eg: `SIN(A1:A5)`
|
||||
* As the result of a computation of a formula `=A1:A5`
|
||||
|
||||
In previous versions of the Friendly Giant the spreadsheet engine would perform an operation called _implicit intersection_
|
||||
that tries to find a single cell within the range. It works by picking a cell in the range that is the same row or the same column
|
||||
as the cell. If there is just one we return that otherwise we return the `#REF!` error.
|
||||
|
||||
Examples:
|
||||
|
||||
* Siting on `C3` the formula `=D1:D5` will return `D3`
|
||||
* Sitting on `C3` the formula `=D:D` will return `D3`
|
||||
* Sitting on `C3` the formula `=A1:A7` will return `A3`
|
||||
* Sitting on `C3` the formula `=A5:A8` will return `#REF!`
|
||||
* Sitting on `C3` the formula `D1:G7` will return `#REF!`
|
||||
|
||||
Today's version of the engine will result in a dynamic array spilling the result through several cells.
|
||||
To force the old behaviour we can use the _implicit intersection operator_: @
|
||||
|
||||
* `=@A1:A7` or `=SIN(@A1:A7)
|
||||
|
||||
When parsing formulas that come form old workbooks this is done automatically.
|
||||
We call this version of the II operator the _automatic_ II operator.
|
||||
|
||||
We can also insert the II operator in places where before was impossible:
|
||||
|
||||
* `=SUM(@A1:A7)`
|
||||
|
||||
This formulas will not be compatible with old versions of the engine. The FG will stringify this as `=SUM(_xlfn.SIMPLE(A1:A7))`.
|
||||
*/
|
||||
|
||||
/// Transverses the formula tree adding the implicit intersection operator in all arguments of functions that
|
||||
/// expect a scalar but get a range.
|
||||
/// * A:A => @A:A
|
||||
/// * SIN(A1:D1) => SIN(@A1:D1)
|
||||
///
|
||||
/// Assumes formula return a scalar
|
||||
pub fn add_implicit_intersection(node: &mut Node, add: bool) {
|
||||
match node {
|
||||
Node::BooleanKind(_)
|
||||
| Node::NumberKind(_)
|
||||
| Node::StringKind(_)
|
||||
| Node::ErrorKind(_)
|
||||
| Node::EmptyArgKind
|
||||
| Node::ParseErrorKind { .. }
|
||||
| Node::WrongReferenceKind { .. }
|
||||
| Node::WrongRangeKind { .. }
|
||||
| Node::InvalidFunctionKind { .. }
|
||||
| Node::ArrayKind(_)
|
||||
| Node::ReferenceKind { .. } => {}
|
||||
Node::ImplicitIntersection { child, .. } => {
|
||||
// We need to check wether the II can be automatic or not
|
||||
let mut new_node = child.as_ref().clone();
|
||||
add_implicit_intersection(&mut new_node, add);
|
||||
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
|
||||
*node = new_node
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
} => {
|
||||
if add {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::RangeKind {
|
||||
sheet_name: sheet_name.clone(),
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row1: *absolute_row1,
|
||||
absolute_column1: *absolute_column1,
|
||||
row1: *row1,
|
||||
column1: *column1,
|
||||
absolute_row2: *absolute_row2,
|
||||
absolute_column2: *absolute_column2,
|
||||
row2: *row2,
|
||||
column2: *column2,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
Node::OpRangeKind { left, right } => {
|
||||
if add {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::OpRangeKind {
|
||||
left: left.clone(),
|
||||
right: right.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// operations
|
||||
Node::UnaryKind { right, .. } => add_implicit_intersection(right, add),
|
||||
Node::OpConcatenateKind { left, right }
|
||||
| Node::OpSumKind { left, right, .. }
|
||||
| Node::OpProductKind { left, right, .. }
|
||||
| Node::OpPowerKind { left, right, .. }
|
||||
| Node::CompareKind { left, right, .. } => {
|
||||
add_implicit_intersection(left, add);
|
||||
add_implicit_intersection(right, add);
|
||||
}
|
||||
|
||||
Node::DefinedNameKind(v) => {
|
||||
if add {
|
||||
// Not all defined names deserve the II operator
|
||||
// For instance =Sheet1!A1 doesn't need to be intersected
|
||||
if is_range_reference(&v.2) {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::DefinedNameKind(v.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::WrongVariableKind(v) => {
|
||||
if add {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::WrongVariableKind(v.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::TableNameKind(_) => {
|
||||
// noop for now
|
||||
}
|
||||
Node::FunctionKind { kind, args } => {
|
||||
let arg_count = args.len();
|
||||
let signature = get_function_args_signature(kind, arg_count);
|
||||
for index in 0..arg_count {
|
||||
if matches!(signature[index], Signature::Scalar)
|
||||
&& matches!(
|
||||
run_static_analysis_on_node(&args[index]),
|
||||
StaticResult::Range(_, _) | StaticResult::Unknown
|
||||
)
|
||||
{
|
||||
add_implicit_intersection(&mut args[index], true);
|
||||
} else {
|
||||
add_implicit_intersection(&mut args[index], false);
|
||||
}
|
||||
}
|
||||
if add
|
||||
&& matches!(
|
||||
run_static_analysis_on_node(node),
|
||||
StaticResult::Range(_, _) | StaticResult::Unknown
|
||||
)
|
||||
{
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(node.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) enum StaticResult {
|
||||
Scalar,
|
||||
Array(i32, i32),
|
||||
Range(i32, i32),
|
||||
Unknown,
|
||||
// TODO: What if one of the dimensions is known?
|
||||
// what if the dimensions are unknown but bounded?
|
||||
}
|
||||
|
||||
fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
|
||||
let lhs = run_static_analysis_on_node(left);
|
||||
let rhs = run_static_analysis_on_node(right);
|
||||
match (lhs, rhs) {
|
||||
(StaticResult::Scalar, StaticResult::Scalar) => StaticResult::Scalar,
|
||||
(StaticResult::Scalar, StaticResult::Array(a, b) | StaticResult::Range(a, b)) => {
|
||||
StaticResult::Array(a, b)
|
||||
}
|
||||
|
||||
(StaticResult::Array(a, b) | StaticResult::Range(a, b), StaticResult::Scalar) => {
|
||||
StaticResult::Array(a, b)
|
||||
}
|
||||
(
|
||||
StaticResult::Array(a1, b1) | StaticResult::Range(a1, b1),
|
||||
StaticResult::Array(a2, b2) | StaticResult::Range(a2, b2),
|
||||
) => StaticResult::Array(a1.max(a2), b1.max(b2)),
|
||||
|
||||
(_, StaticResult::Unknown) => StaticResult::Unknown,
|
||||
(StaticResult::Unknown, _) => StaticResult::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// * Scalar if we can proof the result of the evaluation is a scalar
|
||||
// * Array(a, b) if we know it will be an a x b array.
|
||||
// * Range(a, b) if we know it will be a a x b range.
|
||||
// * Unknown if we cannot guaranty either
|
||||
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
|
||||
match node {
|
||||
Node::BooleanKind(_)
|
||||
| Node::NumberKind(_)
|
||||
| Node::StringKind(_)
|
||||
| Node::ErrorKind(_)
|
||||
| Node::EmptyArgKind => StaticResult::Scalar,
|
||||
Node::UnaryKind { right, .. } => run_static_analysis_on_node(right),
|
||||
Node::ParseErrorKind { .. } => {
|
||||
// StaticResult::Unknown is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::WrongReferenceKind { .. } => {
|
||||
// StaticResult::Unknown is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::WrongRangeKind { .. } => {
|
||||
// StaticResult::Unknown or Array is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::InvalidFunctionKind { .. } => {
|
||||
// StaticResult::Unknown is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::ArrayKind(array) => {
|
||||
let n = array.len() as i32;
|
||||
// FIXME: This is a placeholder until we implement arrays
|
||||
StaticResult::Array(n, 1)
|
||||
}
|
||||
Node::RangeKind {
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
..
|
||||
} => StaticResult::Range(row2 - row1, column2 - column1),
|
||||
Node::OpRangeKind { .. } => {
|
||||
// TODO: We could do a bit better here
|
||||
StaticResult::Unknown
|
||||
}
|
||||
Node::ReferenceKind { .. } => StaticResult::Scalar,
|
||||
|
||||
// binary operations
|
||||
Node::OpConcatenateKind { left, right } => static_analysis_op_nodes(left, right),
|
||||
Node::OpSumKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
Node::OpProductKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
Node::OpPowerKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
Node::CompareKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
|
||||
// defined names
|
||||
Node::DefinedNameKind(_) => StaticResult::Unknown,
|
||||
Node::WrongVariableKind(_) => StaticResult::Unknown,
|
||||
Node::TableNameKind(_) => StaticResult::Unknown,
|
||||
Node::FunctionKind { kind, args } => static_analysis_on_function(kind, args),
|
||||
Node::ImplicitIntersection { .. } => StaticResult::Scalar,
|
||||
}
|
||||
}
|
||||
|
||||
// If all the arguments are scalars the function will return a scalar
|
||||
// If any of the arguments is a range or an array it will return an array
|
||||
fn scalar_arguments(args: &[Node]) -> StaticResult {
|
||||
let mut n = 0;
|
||||
let mut m = 0;
|
||||
for arg in args {
|
||||
match run_static_analysis_on_node(arg) {
|
||||
StaticResult::Scalar => {
|
||||
// noop
|
||||
}
|
||||
StaticResult::Array(a, b) | StaticResult::Range(a, b) => {
|
||||
n = n.max(a);
|
||||
m = m.max(b);
|
||||
}
|
||||
StaticResult::Unknown => return StaticResult::Unknown,
|
||||
}
|
||||
}
|
||||
if n == 0 && m == 0 {
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
StaticResult::Array(n, m)
|
||||
}
|
||||
|
||||
// We only care if the function can return a range or not
|
||||
fn not_implemented(_args: &[Node]) -> StaticResult {
|
||||
StaticResult::Scalar
|
||||
}
|
||||
|
||||
fn static_analysis_offset(args: &[Node]) -> StaticResult {
|
||||
// If first argument is a single cell reference and there are no4th and 5th argument,
|
||||
// or they are 1, then it is a scalar
|
||||
let arg_count = args.len();
|
||||
if arg_count < 3 {
|
||||
// Actually an error
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
return StaticResult::Unknown;
|
||||
}
|
||||
if arg_count == 3 {
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
match args[3] {
|
||||
Node::NumberKind(f) => {
|
||||
if f != 1.0 {
|
||||
return StaticResult::Unknown;
|
||||
}
|
||||
}
|
||||
_ => return StaticResult::Unknown,
|
||||
};
|
||||
if arg_count == 4 {
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
match args[4] {
|
||||
Node::NumberKind(f) => {
|
||||
if f != 1.0 {
|
||||
return StaticResult::Unknown;
|
||||
}
|
||||
}
|
||||
_ => return StaticResult::Unknown,
|
||||
};
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
|
||||
// // We will always insert the @ in CHOOSE, but technically it is only needed if one of the elements is a range
|
||||
// StaticResult::Unknown
|
||||
// }
|
||||
|
||||
fn static_analysis_indirect(_args: &[Node]) -> StaticResult {
|
||||
// We will always insert the @, but we don't need to do that in every scenario`
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
fn static_analysis_index(_args: &[Node]) -> StaticResult {
|
||||
// INDEX has two forms, but they are indistinguishable at parse time.
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Signature {
|
||||
Scalar,
|
||||
Vector,
|
||||
Error,
|
||||
}
|
||||
|
||||
fn args_signature_no_args(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 0 {
|
||||
vec![]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_scalars(
|
||||
arg_count: usize,
|
||||
required_count: usize,
|
||||
optional_count: usize,
|
||||
) -> Vec<Signature> {
|
||||
if arg_count >= required_count && arg_count <= required_count + optional_count {
|
||||
vec![Signature::Scalar; arg_count]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_one_vector(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 1 {
|
||||
vec![Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_sumif(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Scalar]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
// 1 or none scalars
|
||||
fn args_signature_sheet(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 0 {
|
||||
vec![]
|
||||
} else if arg_count == 1 {
|
||||
vec![Signature::Scalar]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_hlookup(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Vector,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_index(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Scalar]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_lookup(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Vector]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_match(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Vector]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_offset(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
]
|
||||
} else if arg_count == 5 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_row(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 0 {
|
||||
vec![]
|
||||
} else if arg_count == 1 {
|
||||
vec![Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_xlookup(arg_count: usize) -> Vec<Signature> {
|
||||
if !(3..=6).contains(&arg_count) {
|
||||
return vec![Signature::Error; arg_count];
|
||||
}
|
||||
let mut result = vec![Signature::Scalar; arg_count];
|
||||
result[0] = Signature::Vector;
|
||||
result[1] = Signature::Vector;
|
||||
result[2] = Signature::Vector;
|
||||
result
|
||||
}
|
||||
|
||||
fn args_signature_textafter(arg_count: usize) -> Vec<Signature> {
|
||||
if !(2..=6).contains(&arg_count) {
|
||||
vec![Signature::Scalar; arg_count]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_textjoin(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count >= 3 {
|
||||
let mut result = vec![Signature::Vector; arg_count];
|
||||
result[0] = Signature::Scalar;
|
||||
result[1] = Signature::Scalar;
|
||||
result
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_npv(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count < 2 {
|
||||
return vec![Signature::Error; arg_count];
|
||||
}
|
||||
let mut result = vec![Signature::Vector; arg_count];
|
||||
result[0] = Signature::Scalar;
|
||||
result
|
||||
}
|
||||
|
||||
fn args_signature_irr(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count > 2 {
|
||||
vec![Signature::Error; arg_count]
|
||||
} else if arg_count == 1 {
|
||||
vec![Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Vector, Signature::Scalar]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_xirr(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector; arg_count]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_mirr(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count != 3 {
|
||||
vec![Signature::Error; arg_count]
|
||||
} else {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count != 3 {
|
||||
vec![Signature::Error; arg_count]
|
||||
} else {
|
||||
vec![Signature::Scalar, Signature::Vector, Signature::Vector]
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
|
||||
// 1. When computing the function
|
||||
// 2. Checking the arguments to see if we need to insert the implicit intersection operator
|
||||
// 3. Understanding the return value
|
||||
//
|
||||
// The signature of the functions should be defined only once
|
||||
|
||||
// Given a function and a number of arguments this returns the arguments at each position
|
||||
// are expected to be scalars or vectors (array/ranges).
|
||||
// Sets signature::Error to all arguments if the number of arguments is incorrect.
|
||||
fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signature> {
|
||||
match kind {
|
||||
Function::And => vec![Signature::Vector; arg_count],
|
||||
Function::False => args_signature_no_args(arg_count),
|
||||
Function::If => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Iferror => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Ifna => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Ifs => vec![Signature::Scalar; arg_count],
|
||||
Function::Not => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Or => vec![Signature::Vector; arg_count],
|
||||
Function::Switch => vec![Signature::Scalar; arg_count],
|
||||
Function::True => args_signature_no_args(arg_count),
|
||||
Function::Xor => vec![Signature::Vector; arg_count],
|
||||
Function::Abs => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Acos => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Acosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Asin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Asinh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Atan => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Atan2 => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Atanh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Choose => vec![Signature::Scalar; arg_count],
|
||||
Function::Column => args_signature_row(arg_count),
|
||||
Function::Columns => args_signature_one_vector(arg_count),
|
||||
Function::Cos => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Max => vec![Signature::Vector; arg_count],
|
||||
Function::Min => vec![Signature::Vector; arg_count],
|
||||
Function::Pi => args_signature_no_args(arg_count),
|
||||
Function::Power => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Product => vec![Signature::Vector; arg_count],
|
||||
Function::Round => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Rounddown => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Roundup => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Sin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sinh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sqrt => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sqrtpi => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sum => vec![Signature::Vector; arg_count],
|
||||
Function::Sumif => args_signature_sumif(arg_count),
|
||||
Function::Sumifs => vec![Signature::Vector; arg_count],
|
||||
Function::Tan => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Tanh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::ErrorType => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isblank => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Iserr => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Iserror => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Iseven => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isformula => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Islogical => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isna => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isnontext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isnumber => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isodd => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isref => args_signature_one_vector(arg_count),
|
||||
Function::Istext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Na => args_signature_no_args(arg_count),
|
||||
Function::Sheet => args_signature_sheet(arg_count),
|
||||
Function::Type => args_signature_one_vector(arg_count),
|
||||
Function::Hlookup => args_signature_hlookup(arg_count),
|
||||
Function::Index => args_signature_index(arg_count),
|
||||
Function::Indirect => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Lookup => args_signature_lookup(arg_count),
|
||||
Function::Match => args_signature_match(arg_count),
|
||||
Function::Offset => args_signature_offset(arg_count),
|
||||
Function::Row => args_signature_row(arg_count),
|
||||
Function::Rows => args_signature_one_vector(arg_count),
|
||||
Function::Vlookup => args_signature_hlookup(arg_count),
|
||||
Function::Xlookup => args_signature_xlookup(arg_count),
|
||||
Function::Concat => vec![Signature::Vector; arg_count],
|
||||
Function::Concatenate => vec![Signature::Scalar; arg_count],
|
||||
Function::Exact => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Find => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Left => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Len => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Lower => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Mid => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Rept => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Right => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Search => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Substitute => args_signature_scalars(arg_count, 3, 1),
|
||||
Function::T => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Text => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Textafter => args_signature_textafter(arg_count),
|
||||
Function::Textbefore => args_signature_textafter(arg_count),
|
||||
Function::Textjoin => args_signature_textjoin(arg_count),
|
||||
Function::Trim => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Upper => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Value => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Average => vec![Signature::Vector; arg_count],
|
||||
Function::Averagea => vec![Signature::Vector; arg_count],
|
||||
Function::Averageif => args_signature_sumif(arg_count),
|
||||
Function::Averageifs => vec![Signature::Vector; arg_count],
|
||||
Function::Count => vec![Signature::Vector; arg_count],
|
||||
Function::Counta => vec![Signature::Vector; arg_count],
|
||||
Function::Countblank => vec![Signature::Vector; arg_count],
|
||||
Function::Countif => args_signature_sumif(arg_count),
|
||||
Function::Countifs => vec![Signature::Vector; arg_count],
|
||||
Function::Maxifs => vec![Signature::Vector; arg_count],
|
||||
Function::Minifs => vec![Signature::Vector; arg_count],
|
||||
Function::Date => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Day => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Edate => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Month => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Now => args_signature_no_args(arg_count),
|
||||
Function::Today => args_signature_no_args(arg_count),
|
||||
Function::Year => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
|
||||
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
|
||||
Function::Db => args_signature_scalars(arg_count, 4, 1),
|
||||
Function::Ddb => args_signature_scalars(arg_count, 4, 1),
|
||||
Function::Dollarde => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Dollarfr => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Effect => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Fv => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
|
||||
Function::Irr => args_signature_irr(arg_count),
|
||||
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
|
||||
Function::Mirr => args_signature_mirr(arg_count),
|
||||
Function::Nominal => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Nper => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Npv => args_signature_npv(arg_count),
|
||||
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
|
||||
Function::Pv => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Rate => args_signature_scalars(arg_count, 3, 3),
|
||||
Function::Rri => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Sln => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Syd => args_signature_scalars(arg_count, 4, 0),
|
||||
Function::Tbilleq => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Xirr => args_signature_xirr(arg_count),
|
||||
Function::Xnpv => args_signature_xnpv(arg_count),
|
||||
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Besselj => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Besselk => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bessely => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Erf => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Erfc => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::ErfcPrecise => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::ErfPrecise => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bin2dec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bin2hex => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bin2oct => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Dec2Bin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Dec2hex => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Dec2oct => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hex2bin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hex2dec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hex2oct => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Oct2bin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Oct2dec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Oct2hex => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bitand => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitlshift => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitor => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitrshift => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitxor => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Complex => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Imabs => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imaginary => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imargument => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imconjugate => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcos => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcot => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcsc => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcsch => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imdiv => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imexp => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imln => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imlog10 => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imlog2 => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Impower => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Improduct => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imreal => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsech => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsinh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsqrt => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsub => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imsum => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imtan => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Convert => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Delta => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Gestep => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Subtotal => args_signature_npv(arg_count),
|
||||
Function::Rand => args_signature_no_args(arg_count),
|
||||
Function::Randbetween => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Unicode => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Geomean => vec![Signature::Vector; arg_count],
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the type of the result (Scalar, Array or Range) depending on the arguments
|
||||
fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
match kind {
|
||||
Function::And => StaticResult::Scalar,
|
||||
Function::False => StaticResult::Scalar,
|
||||
Function::If => scalar_arguments(args),
|
||||
Function::Iferror => scalar_arguments(args),
|
||||
Function::Ifna => scalar_arguments(args),
|
||||
Function::Ifs => not_implemented(args),
|
||||
Function::Not => StaticResult::Scalar,
|
||||
Function::Or => StaticResult::Scalar,
|
||||
Function::Switch => not_implemented(args),
|
||||
Function::True => StaticResult::Scalar,
|
||||
Function::Xor => StaticResult::Scalar,
|
||||
Function::Abs => scalar_arguments(args),
|
||||
Function::Acos => scalar_arguments(args),
|
||||
Function::Acosh => scalar_arguments(args),
|
||||
Function::Asin => scalar_arguments(args),
|
||||
Function::Asinh => scalar_arguments(args),
|
||||
Function::Atan => scalar_arguments(args),
|
||||
Function::Atan2 => scalar_arguments(args),
|
||||
Function::Atanh => scalar_arguments(args),
|
||||
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
||||
Function::Column => not_implemented(args),
|
||||
Function::Columns => not_implemented(args),
|
||||
Function::Cos => scalar_arguments(args),
|
||||
Function::Cosh => scalar_arguments(args),
|
||||
Function::Max => StaticResult::Scalar,
|
||||
Function::Min => StaticResult::Scalar,
|
||||
Function::Pi => StaticResult::Scalar,
|
||||
Function::Power => scalar_arguments(args),
|
||||
Function::Product => not_implemented(args),
|
||||
Function::Round => scalar_arguments(args),
|
||||
Function::Rounddown => scalar_arguments(args),
|
||||
Function::Roundup => scalar_arguments(args),
|
||||
Function::Sin => scalar_arguments(args),
|
||||
Function::Sinh => scalar_arguments(args),
|
||||
Function::Sqrt => scalar_arguments(args),
|
||||
Function::Sqrtpi => StaticResult::Scalar,
|
||||
Function::Sum => StaticResult::Scalar,
|
||||
Function::Sumif => not_implemented(args),
|
||||
Function::Sumifs => not_implemented(args),
|
||||
Function::Tan => scalar_arguments(args),
|
||||
Function::Tanh => scalar_arguments(args),
|
||||
Function::ErrorType => not_implemented(args),
|
||||
Function::Isblank => not_implemented(args),
|
||||
Function::Iserr => not_implemented(args),
|
||||
Function::Iserror => not_implemented(args),
|
||||
Function::Iseven => not_implemented(args),
|
||||
Function::Isformula => not_implemented(args),
|
||||
Function::Islogical => not_implemented(args),
|
||||
Function::Isna => not_implemented(args),
|
||||
Function::Isnontext => not_implemented(args),
|
||||
Function::Isnumber => not_implemented(args),
|
||||
Function::Isodd => not_implemented(args),
|
||||
Function::Isref => not_implemented(args),
|
||||
Function::Istext => not_implemented(args),
|
||||
Function::Na => StaticResult::Scalar,
|
||||
Function::Sheet => StaticResult::Scalar,
|
||||
Function::Type => not_implemented(args),
|
||||
Function::Hlookup => not_implemented(args),
|
||||
Function::Index => static_analysis_index(args),
|
||||
Function::Indirect => static_analysis_indirect(args),
|
||||
Function::Lookup => not_implemented(args),
|
||||
Function::Match => not_implemented(args),
|
||||
Function::Offset => static_analysis_offset(args),
|
||||
// FIXME: Row could return an array
|
||||
Function::Row => StaticResult::Scalar,
|
||||
Function::Rows => not_implemented(args),
|
||||
Function::Vlookup => not_implemented(args),
|
||||
Function::Xlookup => not_implemented(args),
|
||||
Function::Concat => not_implemented(args),
|
||||
Function::Concatenate => not_implemented(args),
|
||||
Function::Exact => not_implemented(args),
|
||||
Function::Find => not_implemented(args),
|
||||
Function::Left => not_implemented(args),
|
||||
Function::Len => not_implemented(args),
|
||||
Function::Lower => not_implemented(args),
|
||||
Function::Mid => not_implemented(args),
|
||||
Function::Rept => not_implemented(args),
|
||||
Function::Right => not_implemented(args),
|
||||
Function::Search => not_implemented(args),
|
||||
Function::Substitute => not_implemented(args),
|
||||
Function::T => not_implemented(args),
|
||||
Function::Text => not_implemented(args),
|
||||
Function::Textafter => not_implemented(args),
|
||||
Function::Textbefore => not_implemented(args),
|
||||
Function::Textjoin => not_implemented(args),
|
||||
Function::Trim => not_implemented(args),
|
||||
Function::Unicode => not_implemented(args),
|
||||
Function::Upper => not_implemented(args),
|
||||
Function::Value => not_implemented(args),
|
||||
Function::Valuetotext => not_implemented(args),
|
||||
Function::Average => not_implemented(args),
|
||||
Function::Averagea => not_implemented(args),
|
||||
Function::Averageif => not_implemented(args),
|
||||
Function::Averageifs => not_implemented(args),
|
||||
Function::Count => not_implemented(args),
|
||||
Function::Counta => not_implemented(args),
|
||||
Function::Countblank => not_implemented(args),
|
||||
Function::Countif => not_implemented(args),
|
||||
Function::Countifs => not_implemented(args),
|
||||
Function::Maxifs => not_implemented(args),
|
||||
Function::Minifs => not_implemented(args),
|
||||
Function::Date => not_implemented(args),
|
||||
Function::Day => not_implemented(args),
|
||||
Function::Edate => not_implemented(args),
|
||||
Function::Month => not_implemented(args),
|
||||
Function::Now => not_implemented(args),
|
||||
Function::Today => not_implemented(args),
|
||||
Function::Year => not_implemented(args),
|
||||
Function::Cumipmt => not_implemented(args),
|
||||
Function::Cumprinc => not_implemented(args),
|
||||
Function::Db => not_implemented(args),
|
||||
Function::Ddb => not_implemented(args),
|
||||
Function::Dollarde => not_implemented(args),
|
||||
Function::Dollarfr => not_implemented(args),
|
||||
Function::Effect => not_implemented(args),
|
||||
Function::Fv => not_implemented(args),
|
||||
Function::Ipmt => not_implemented(args),
|
||||
Function::Irr => not_implemented(args),
|
||||
Function::Ispmt => not_implemented(args),
|
||||
Function::Mirr => not_implemented(args),
|
||||
Function::Nominal => not_implemented(args),
|
||||
Function::Nper => not_implemented(args),
|
||||
Function::Npv => not_implemented(args),
|
||||
Function::Pduration => not_implemented(args),
|
||||
Function::Pmt => not_implemented(args),
|
||||
Function::Ppmt => not_implemented(args),
|
||||
Function::Pv => not_implemented(args),
|
||||
Function::Rate => not_implemented(args),
|
||||
Function::Rri => not_implemented(args),
|
||||
Function::Sln => not_implemented(args),
|
||||
Function::Syd => not_implemented(args),
|
||||
Function::Tbilleq => not_implemented(args),
|
||||
Function::Tbillprice => not_implemented(args),
|
||||
Function::Tbillyield => not_implemented(args),
|
||||
Function::Xirr => not_implemented(args),
|
||||
Function::Xnpv => not_implemented(args),
|
||||
Function::Besseli => scalar_arguments(args),
|
||||
Function::Besselj => scalar_arguments(args),
|
||||
Function::Besselk => scalar_arguments(args),
|
||||
Function::Bessely => scalar_arguments(args),
|
||||
Function::Erf => scalar_arguments(args),
|
||||
Function::Erfc => scalar_arguments(args),
|
||||
Function::ErfcPrecise => scalar_arguments(args),
|
||||
Function::ErfPrecise => scalar_arguments(args),
|
||||
Function::Bin2dec => scalar_arguments(args),
|
||||
Function::Bin2hex => scalar_arguments(args),
|
||||
Function::Bin2oct => scalar_arguments(args),
|
||||
Function::Dec2Bin => scalar_arguments(args),
|
||||
Function::Dec2hex => scalar_arguments(args),
|
||||
Function::Dec2oct => scalar_arguments(args),
|
||||
Function::Hex2bin => scalar_arguments(args),
|
||||
Function::Hex2dec => scalar_arguments(args),
|
||||
Function::Hex2oct => scalar_arguments(args),
|
||||
Function::Oct2bin => scalar_arguments(args),
|
||||
Function::Oct2dec => scalar_arguments(args),
|
||||
Function::Oct2hex => scalar_arguments(args),
|
||||
Function::Bitand => scalar_arguments(args),
|
||||
Function::Bitlshift => scalar_arguments(args),
|
||||
Function::Bitor => scalar_arguments(args),
|
||||
Function::Bitrshift => scalar_arguments(args),
|
||||
Function::Bitxor => scalar_arguments(args),
|
||||
Function::Complex => scalar_arguments(args),
|
||||
Function::Imabs => scalar_arguments(args),
|
||||
Function::Imaginary => scalar_arguments(args),
|
||||
Function::Imargument => scalar_arguments(args),
|
||||
Function::Imconjugate => scalar_arguments(args),
|
||||
Function::Imcos => scalar_arguments(args),
|
||||
Function::Imcosh => scalar_arguments(args),
|
||||
Function::Imcot => scalar_arguments(args),
|
||||
Function::Imcsc => scalar_arguments(args),
|
||||
Function::Imcsch => scalar_arguments(args),
|
||||
Function::Imdiv => scalar_arguments(args),
|
||||
Function::Imexp => scalar_arguments(args),
|
||||
Function::Imln => scalar_arguments(args),
|
||||
Function::Imlog10 => scalar_arguments(args),
|
||||
Function::Imlog2 => scalar_arguments(args),
|
||||
Function::Impower => scalar_arguments(args),
|
||||
Function::Improduct => scalar_arguments(args),
|
||||
Function::Imreal => scalar_arguments(args),
|
||||
Function::Imsec => scalar_arguments(args),
|
||||
Function::Imsech => scalar_arguments(args),
|
||||
Function::Imsin => scalar_arguments(args),
|
||||
Function::Imsinh => scalar_arguments(args),
|
||||
Function::Imsqrt => scalar_arguments(args),
|
||||
Function::Imsub => scalar_arguments(args),
|
||||
Function::Imsum => scalar_arguments(args),
|
||||
Function::Imtan => scalar_arguments(args),
|
||||
Function::Convert => not_implemented(args),
|
||||
Function::Delta => not_implemented(args),
|
||||
Function::Gestep => not_implemented(args),
|
||||
Function::Subtotal => not_implemented(args),
|
||||
Function::Rand => not_implemented(args),
|
||||
Function::Randbetween => scalar_arguments(args),
|
||||
Function::Eomonth => scalar_arguments(args),
|
||||
Function::Formulatext => not_implemented(args),
|
||||
Function::Geomean => not_implemented(args),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::move_formula::to_string_array_node;
|
||||
use crate::expressions::parser::static_analysis::add_implicit_intersection;
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
@@ -34,10 +36,21 @@ pub enum DisplaceData {
|
||||
None,
|
||||
}
|
||||
|
||||
/// This is the internal mode in IronCalc
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// This is the mode used to display the formula in the UI
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// This is the mode used to export the formula to Excel
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
@@ -46,18 +59,10 @@ pub fn to_string_displaced(
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
|
||||
/// Converts a local reference to a string applying some displacement if needed.
|
||||
/// It uses A1 style if context is not None. If context is None it uses R1C1 style
|
||||
/// If full_row is true then the row details will be omitted in the A1 case
|
||||
/// If full_colum is true then column details will be omitted.
|
||||
/// If full_column is true then column details will be omitted.
|
||||
pub(crate) fn stringify_reference(
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
@@ -235,7 +240,7 @@ fn format_function(
|
||||
args: &Vec<Node>,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
export_to_excel: bool,
|
||||
) -> String {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
@@ -244,21 +249,46 @@ fn format_function(
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
stringify(el, context, displace_data, export_to_excel)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
arguments = stringify(el, context, displace_data, export_to_excel);
|
||||
}
|
||||
}
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
|
||||
// But three different ways to convert it to a string.
|
||||
//
|
||||
// To stringify a formula we need a "context", that is in which cell are we doing the "stringifying"
|
||||
//
|
||||
// But there are three ways to stringify a formula:
|
||||
//
|
||||
// * To show it to the IronCalc user
|
||||
// * To store internally
|
||||
// * To export to Excel
|
||||
//
|
||||
// There are, of course correspondingly three "modes" when parsing a formula.
|
||||
//
|
||||
// The internal representation is the more different as references are stored in the RC representation.
|
||||
// The the AST of the formula is kept close to this representation we don't need a context
|
||||
//
|
||||
// In the export to Excel representation certain things are different:
|
||||
// * We add a _xlfn. in front of some (more modern) functions
|
||||
// * We remove the Implicit Intersection operator when it is automatic and add _xlfn.SINGLE when it is not
|
||||
//
|
||||
// Examples:
|
||||
// * =A1+B2
|
||||
// * =RC+R1C1
|
||||
// * =A1+B1
|
||||
|
||||
fn stringify(
|
||||
node: &Node,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
export_to_excel: bool,
|
||||
) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
@@ -407,100 +437,167 @@ fn stringify(
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpConcatenateKind { left, right } => format!(
|
||||
"{}&{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
_ => stringify(left, context, displace_data, use_original_name),
|
||||
_ => stringify(left, context, displace_data, export_to_excel),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpProductKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
_ => stringify(right, context, displace_data, use_original_name),
|
||||
_ => stringify(right, context, displace_data, export_to_excel),
|
||||
};
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => format!(
|
||||
"{}^{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpPowerKind { left, right } => {
|
||||
let x = match **left {
|
||||
BooleanKind(_)
|
||||
| NumberKind(_)
|
||||
| StringKind(_)
|
||||
| ReferenceKind { .. }
|
||||
| RangeKind { .. }
|
||||
| WrongReferenceKind { .. }
|
||||
| DefinedNameKind(_)
|
||||
| TableNameKind(_)
|
||||
| WrongVariableKind(_)
|
||||
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
|
||||
OpRangeKind { .. }
|
||||
| OpConcatenateKind { .. }
|
||||
| OpProductKind { .. }
|
||||
| OpPowerKind { .. }
|
||||
| FunctionKind { .. }
|
||||
| InvalidFunctionKind { .. }
|
||||
| ArrayKind(_)
|
||||
| UnaryKind { .. }
|
||||
| ErrorKind(_)
|
||||
| ParseErrorKind { .. }
|
||||
| OpSumKind { .. }
|
||||
| CompareKind { .. }
|
||||
| ImplicitIntersection { .. }
|
||||
| EmptyArgKind => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
};
|
||||
let y = match **right {
|
||||
BooleanKind(_)
|
||||
| NumberKind(_)
|
||||
| StringKind(_)
|
||||
| ReferenceKind { .. }
|
||||
| RangeKind { .. }
|
||||
| WrongReferenceKind { .. }
|
||||
| DefinedNameKind(_)
|
||||
| TableNameKind(_)
|
||||
| WrongVariableKind(_)
|
||||
| WrongRangeKind { .. } => {
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
}
|
||||
OpRangeKind { .. }
|
||||
| OpConcatenateKind { .. }
|
||||
| OpProductKind { .. }
|
||||
| OpPowerKind { .. }
|
||||
| FunctionKind { .. }
|
||||
| InvalidFunctionKind { .. }
|
||||
| ArrayKind(_)
|
||||
| UnaryKind { .. }
|
||||
| ErrorKind(_)
|
||||
| ParseErrorKind { .. }
|
||||
| OpSumKind { .. }
|
||||
| CompareKind { .. }
|
||||
| ImplicitIntersection { .. }
|
||||
| EmptyArgKind => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
};
|
||||
format!("{}^{}", x, y)
|
||||
}
|
||||
InvalidFunctionKind { name, args } => {
|
||||
format_function(name, args, context, displace_data, use_original_name)
|
||||
format_function(name, args, context, displace_data, export_to_excel)
|
||||
}
|
||||
FunctionKind { kind, args } => {
|
||||
let name = if use_original_name {
|
||||
let name = if export_to_excel {
|
||||
kind.to_xlsx_string()
|
||||
} else {
|
||||
kind.to_string()
|
||||
};
|
||||
format_function(&name, args, context, displace_data, use_original_name)
|
||||
format_function(&name, args, context, displace_data, export_to_excel)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
);
|
||||
let mut first_row = true;
|
||||
let mut matrix_string = String::new();
|
||||
|
||||
for row in args {
|
||||
if !first_row {
|
||||
matrix_string.push(';');
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
first_row = false;
|
||||
}
|
||||
let mut first_column = true;
|
||||
let mut row_string = String::new();
|
||||
for el in row {
|
||||
if !first_column {
|
||||
row_string.push(',');
|
||||
} else {
|
||||
first_column = false;
|
||||
}
|
||||
row_string.push_str(&to_string_array_node(el));
|
||||
}
|
||||
matrix_string.push_str(&row_string);
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
format!("{{{}}}", matrix_string)
|
||||
}
|
||||
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!(
|
||||
"-{}",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
OpUnary::Percentage => {
|
||||
format!(
|
||||
"{}%",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -511,6 +608,29 @@ fn stringify(
|
||||
message: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
if export_to_excel {
|
||||
// We need to check wether the II can be automatic or not
|
||||
let mut new_node = child.as_ref().clone();
|
||||
|
||||
add_implicit_intersection(&mut new_node, true);
|
||||
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
|
||||
return stringify(child, context, displace_data, export_to_excel);
|
||||
}
|
||||
|
||||
return format!(
|
||||
"_xlfn.SINGLE({})",
|
||||
stringify(child, context, displace_data, export_to_excel)
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"@{}",
|
||||
stringify(child, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +718,12 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
rename_sheet_in_node(child, sheet_index, new_name);
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
Node::BooleanKind(_) => {}
|
||||
@@ -606,7 +732,96 @@ 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);
|
||||
}
|
||||
Node::ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
rename_defined_name_in_node(child, name, scope, new_name);
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::ArrayKind(_) => {}
|
||||
Node::EmptyArgKind => {}
|
||||
Node::ReferenceKind { .. } => {}
|
||||
Node::RangeKind { .. } => {}
|
||||
Node::WrongReferenceKind { .. } => {}
|
||||
Node::WrongRangeKind { .. } => {}
|
||||
Node::TableNameKind(_) => {}
|
||||
Node::WrongVariableKind(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
9
base/src/expressions/parser/tests/mod.rs
Normal file
9
base/src/expressions/parser/tests/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod test_add_implicit_intersection;
|
||||
mod test_arrays;
|
||||
mod test_general;
|
||||
mod test_implicit_intersection;
|
||||
mod test_issue_155;
|
||||
mod test_move_formula;
|
||||
mod test_ranges;
|
||||
mod test_stringify;
|
||||
mod test_tables;
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::{
|
||||
parser::{
|
||||
stringify::{to_excel_string, to_string},
|
||||
Parser,
|
||||
},
|
||||
types::CellReferenceRC,
|
||||
};
|
||||
|
||||
use crate::expressions::parser::static_analysis::add_implicit_intersection;
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let cases = vec![
|
||||
("A1:A10*SUM(A1:A10)", "@A1:A10*SUM(A1:A10)"),
|
||||
("A1:A10", "@A1:A10"),
|
||||
// Math and trigonometry functions
|
||||
("SUM(A1:A10)", "SUM(A1:A10)"),
|
||||
("SIN(A1:A10)", "SIN(@A1:A10)"),
|
||||
("COS(A1:A10)", "COS(@A1:A10)"),
|
||||
("TAN(A1:A10)", "TAN(@A1:A10)"),
|
||||
("ASIN(A1:A10)", "ASIN(@A1:A10)"),
|
||||
("ACOS(A1:A10)", "ACOS(@A1:A10)"),
|
||||
("ATAN(A1:A10)", "ATAN(@A1:A10)"),
|
||||
("SINH(A1:A10)", "SINH(@A1:A10)"),
|
||||
("COSH(A1:A10)", "COSH(@A1:A10)"),
|
||||
("TANH(A1:A10)", "TANH(@A1:A10)"),
|
||||
("ASINH(A1:A10)", "ASINH(@A1:A10)"),
|
||||
("ACOSH(A1:A10)", "ACOSH(@A1:A10)"),
|
||||
("ATANH(A1:A10)", "ATANH(@A1:A10)"),
|
||||
("ATAN2(A1:A10,B1:B10)", "ATAN2(@A1:A10,@B1:B10)"),
|
||||
("ATAN2(A1:A10,A1)", "ATAN2(@A1:A10,A1)"),
|
||||
("SQRT(A1:A10)", "SQRT(@A1:A10)"),
|
||||
("SQRTPI(A1:A10)", "SQRTPI(@A1:A10)"),
|
||||
("POWER(A1:A10,A1)", "POWER(@A1:A10,A1)"),
|
||||
("POWER(A1:A10,B1:B10)", "POWER(@A1:A10,@B1:B10)"),
|
||||
("MAX(A1:A10)", "MAX(A1:A10)"),
|
||||
("MIN(A1:A10)", "MIN(A1:A10)"),
|
||||
("ABS(A1:A10)", "ABS(@A1:A10)"),
|
||||
("FALSE()", "FALSE()"),
|
||||
("TRUE()", "TRUE()"),
|
||||
// Defined names
|
||||
("BADNMAE", "@BADNMAE"),
|
||||
// Logical
|
||||
("AND(A1:A10)", "AND(A1:A10)"),
|
||||
("OR(A1:A10)", "OR(A1:A10)"),
|
||||
("NOT(A1:A10)", "NOT(@A1:A10)"),
|
||||
("IF(A1:A10,B1:B10,C1:C10)", "IF(@A1:A10,@B1:B10,@C1:C10)"),
|
||||
// Information
|
||||
// ("ISBLANK(A1:A10)", "ISBLANK(A1:A10)"),
|
||||
// ("ISERR(A1:A10)", "ISERR(A1:A10)"),
|
||||
// ("ISERROR(A1:A10)", "ISERROR(A1:A10)"),
|
||||
// ("ISEVEN(A1:A10)", "ISEVEN(A1:A10)"),
|
||||
// ("ISLOGICAL(A1:A10)", "ISLOGICAL(A1:A10)"),
|
||||
// ("ISNA(A1:A10)", "ISNA(A1:A10)"),
|
||||
// ("ISNONTEXT(A1:A10)", "ISNONTEXT(A1:A10)"),
|
||||
// ("ISNUMBER(A1:A10)", "ISNUMBER(A1:A10)"),
|
||||
// ("ISODD(A1:A10)", "ISODD(A1:A10)"),
|
||||
// ("ISREF(A1:A10)", "ISREF(A1:A10)"),
|
||||
// ("ISTEXT(A1:A10)", "ISTEXT(A1:A10)"),
|
||||
];
|
||||
for (formula, expected) in cases {
|
||||
let mut t = parser.parse(formula, &cell_reference);
|
||||
add_implicit_intersection(&mut t, true);
|
||||
let r = to_string(&t, &cell_reference);
|
||||
assert_eq!(r, expected);
|
||||
let excel_formula = to_excel_string(&t, &cell_reference);
|
||||
assert_eq!(excel_formula, formula);
|
||||
}
|
||||
}
|
||||
92
base/src/expressions/parser/tests/test_arrays.rs
Normal file
92
base/src/expressions/parser/tests/test_arrays.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::parser::{ArrayNode, Node, Parser};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
#[test]
|
||||
fn simple_horizontal() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let horizontal = parser.parse("{1, 2, 3}", &cell_reference);
|
||||
assert_eq!(
|
||||
horizontal,
|
||||
Node::ArrayKind(vec![vec![
|
||||
ArrayNode::Number(1.0),
|
||||
ArrayNode::Number(2.0),
|
||||
ArrayNode::Number(3.0)
|
||||
]])
|
||||
);
|
||||
|
||||
assert_eq!(to_rc_format(&horizontal), "{1,2,3}");
|
||||
assert_eq!(to_string(&horizontal, &cell_reference), "{1,2,3}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_vertical() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let vertical = parser.parse("{1;2; 3}", &cell_reference);
|
||||
assert_eq!(
|
||||
vertical,
|
||||
Node::ArrayKind(vec![
|
||||
vec![ArrayNode::Number(1.0)],
|
||||
vec![ArrayNode::Number(2.0)],
|
||||
vec![ArrayNode::Number(3.0)]
|
||||
])
|
||||
);
|
||||
assert_eq!(to_rc_format(&vertical), "{1;2;3}");
|
||||
assert_eq!(to_string(&vertical, &cell_reference), "{1;2;3}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_matrix() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let matrix = parser.parse("{1,2,3; 4, 5, 6; 7,8,9}", &cell_reference);
|
||||
assert_eq!(
|
||||
matrix,
|
||||
Node::ArrayKind(vec![
|
||||
vec![
|
||||
ArrayNode::Number(1.0),
|
||||
ArrayNode::Number(2.0),
|
||||
ArrayNode::Number(3.0)
|
||||
],
|
||||
vec![
|
||||
ArrayNode::Number(4.0),
|
||||
ArrayNode::Number(5.0),
|
||||
ArrayNode::Number(6.0)
|
||||
],
|
||||
vec![
|
||||
ArrayNode::Number(7.0),
|
||||
ArrayNode::Number(8.0),
|
||||
ArrayNode::Number(9.0)
|
||||
]
|
||||
])
|
||||
);
|
||||
assert_eq!(to_rc_format(&matrix), "{1,2,3;4,5,6;7,8,9}");
|
||||
assert_eq!(to_string(&matrix, &cell_reference), "{1,2,3;4,5,6;7,8,9}");
|
||||
}
|
||||
@@ -3,17 +3,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use super::{
|
||||
super::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node,
|
||||
},
|
||||
stringify::to_string_displaced,
|
||||
use crate::expressions::parser::stringify::{
|
||||
to_rc_format, to_string, to_string_displaced, DisplaceData,
|
||||
};
|
||||
use crate::expressions::parser::{Node, Parser};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
initial: &'a str,
|
||||
@@ -23,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 {
|
||||
@@ -31,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 {
|
||||
@@ -46,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 {
|
||||
@@ -61,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 {
|
||||
@@ -76,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 {
|
||||
@@ -92,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 {
|
||||
@@ -108,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 {
|
||||
@@ -123,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,
|
||||
@@ -144,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 {
|
||||
@@ -152,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,
|
||||
@@ -173,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 {
|
||||
@@ -181,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,
|
||||
@@ -202,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 {
|
||||
@@ -210,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,
|
||||
@@ -231,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 {
|
||||
@@ -265,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);
|
||||
@@ -279,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![
|
||||
@@ -330,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);
|
||||
@@ -344,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 {
|
||||
@@ -353,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 {
|
||||
@@ -369,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 {
|
||||
@@ -385,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 {
|
||||
@@ -401,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])");
|
||||
}
|
||||
|
||||
@@ -413,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,
|
||||
@@ -433,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,
|
||||
@@ -446,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,
|
||||
@@ -466,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,
|
||||
@@ -486,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,
|
||||
@@ -0,0 +1,75 @@
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
use crate::expressions::parser::{Node, Parser};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!B3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 3,
|
||||
column: 2,
|
||||
};
|
||||
let t = parser.parse("@A1:A10", &cell_reference);
|
||||
let child = Node::RangeKind {
|
||||
sheet_name: None,
|
||||
sheet_index: 0,
|
||||
absolute_row1: false,
|
||||
absolute_column1: false,
|
||||
row1: -2,
|
||||
column1: -1,
|
||||
absolute_row2: false,
|
||||
absolute_column2: false,
|
||||
row2: 7,
|
||||
column2: -1,
|
||||
};
|
||||
assert_eq!(
|
||||
t,
|
||||
Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(child)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_add() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!B3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 3,
|
||||
column: 2,
|
||||
};
|
||||
let t = parser.parse("@A1:A10+12", &cell_reference);
|
||||
let child = Node::RangeKind {
|
||||
sheet_name: None,
|
||||
sheet_index: 0,
|
||||
absolute_row1: false,
|
||||
absolute_column1: false,
|
||||
row1: -2,
|
||||
column1: -1,
|
||||
absolute_row2: false,
|
||||
absolute_column2: false,
|
||||
row2: 7,
|
||||
column2: -1,
|
||||
};
|
||||
assert_eq!(
|
||||
t,
|
||||
Node::OpSumKind {
|
||||
kind: crate::expressions::token::OpSum::Add,
|
||||
left: Box::new(Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(child)
|
||||
}),
|
||||
right: Box::new(Node::NumberKind(12.0))
|
||||
}
|
||||
)
|
||||
}
|
||||
69
base/src/expressions/parser/tests/test_issue_155.rs
Normal file
69
base/src/expressions/parser/tests/test_issue_155.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
#[test]
|
||||
fn issue_155_parser() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 2,
|
||||
column: 2,
|
||||
};
|
||||
let t = parser.parse("A$1:A2", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "A$1:A2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_155_parser_case_2() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 20,
|
||||
column: 20,
|
||||
};
|
||||
let t = parser.parse("C$1:D2", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "C$1:D2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_155_parser_only_row() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 20,
|
||||
column: 20,
|
||||
};
|
||||
// This is tricky, I am not sure what to do in these cases
|
||||
let t = parser.parse("A$2:B1", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "A1:B$2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_155_parser_only_column() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 20,
|
||||
column: 20,
|
||||
};
|
||||
// This is tricky, I am not sure what to do in these cases
|
||||
let t = parser.parse("D1:$A3", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "$A1:D3");
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::{Area, CellReferenceRC};
|
||||
|
||||
#[test]
|
||||
fn test_move_formula() {
|
||||
@@ -17,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 {
|
||||
@@ -29,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 {
|
||||
@@ -45,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 {
|
||||
@@ -61,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 {
|
||||
@@ -77,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 {
|
||||
@@ -104,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 {
|
||||
@@ -115,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 {
|
||||
@@ -142,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 {
|
||||
@@ -154,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 {
|
||||
@@ -170,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 {
|
||||
@@ -197,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,
|
||||
@@ -207,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 {
|
||||
@@ -222,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 {
|
||||
@@ -238,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 {
|
||||
@@ -253,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 {
|
||||
@@ -269,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 {
|
||||
@@ -285,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 {
|
||||
@@ -320,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 {
|
||||
@@ -337,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 {
|
||||
@@ -352,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 {
|
||||
@@ -379,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 {
|
||||
@@ -389,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+SUM(F2:H4)+SUM(C2:F6)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
@@ -402,9 +400,9 @@ fn test_move_formula_misc() {
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "X9^M12-P14*H2");
|
||||
assert_eq!(t, "X9^M12-P14*H2+SUM(F2:H4)+SUM(M12:P16)");
|
||||
|
||||
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone()));
|
||||
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
@@ -419,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 {
|
||||
@@ -447,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 {
|
||||
@@ -459,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 {
|
||||
@@ -480,3 +475,77 @@ fn test_move_formula_another_sheet() {
|
||||
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_formula_implicit_intersetion() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_formula_implicit_intersetion_with_ranges() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)+SUM(@A1, @X9, @$D$5)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)+SUM(@A1,@X9,@$N$15)");
|
||||
}
|
||||
@@ -2,9 +2,9 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use super::super::parser::stringify::{to_rc_format, to_string};
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
formula_a1: &'a str,
|
||||
@@ -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),
|
||||
34
base/src/expressions/parser/tests/test_stringify.rs
Normal file
34
base/src/expressions/parser/tests/test_stringify.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
#[test]
|
||||
fn exp_order() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("(1 + 2)^3 + 4", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4");
|
||||
|
||||
let t = parser.parse("(C5 + 3)^R4", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4");
|
||||
|
||||
let t = parser.parse("(C5 + 3)^(R4*6)", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)");
|
||||
|
||||
let t = parser.parse("(C5)^(R4)", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "C5^R4");
|
||||
|
||||
let t = parser.parse("(5)^(4)", &cell_reference);
|
||||
assert_eq!(to_string(&t, &cell_reference), "5^4");
|
||||
}
|
||||
@@ -3,12 +3,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
column_names: &[&str],
|
||||
@@ -63,7 +62,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 +71,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 +81,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 +91,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)"
|
||||
@@ -1,276 +0,0 @@
|
||||
use super::{move_formula::ref_is_in_area, Node};
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
|
||||
pub(crate) fn forward_references(
|
||||
node: &mut Node,
|
||||
context: &CellReferenceIndex,
|
||||
source_area: &Area,
|
||||
target_sheet: u32,
|
||||
target_sheet_name: &str,
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) {
|
||||
match node {
|
||||
Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: reference_sheet,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row: reference_row,
|
||||
column: reference_column,
|
||||
} => {
|
||||
let reference_row_absolute = if *absolute_row {
|
||||
*reference_row
|
||||
} else {
|
||||
*reference_row + context.row
|
||||
};
|
||||
let reference_column_absolute = if *absolute_column {
|
||||
*reference_column
|
||||
} else {
|
||||
*reference_column + context.column
|
||||
};
|
||||
if ref_is_in_area(
|
||||
*reference_sheet,
|
||||
reference_row_absolute,
|
||||
reference_column_absolute,
|
||||
source_area,
|
||||
) {
|
||||
if *reference_sheet != target_sheet {
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
*reference_sheet = target_sheet;
|
||||
}
|
||||
*reference_row = target_row + *reference_row - source_area.row;
|
||||
*reference_column = target_column + *reference_column - source_area.column;
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let reference_row1 = if *absolute_row1 {
|
||||
*row1
|
||||
} else {
|
||||
*row1 + context.row
|
||||
};
|
||||
let reference_column1 = if *absolute_column1 {
|
||||
*column1
|
||||
} else {
|
||||
*column1 + context.column
|
||||
};
|
||||
|
||||
let reference_row2 = if *absolute_row2 {
|
||||
*row2
|
||||
} else {
|
||||
*row2 + context.row
|
||||
};
|
||||
let reference_column2 = if *absolute_column2 {
|
||||
*column2
|
||||
} else {
|
||||
*column2 + context.column
|
||||
};
|
||||
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
|
||||
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
|
||||
{
|
||||
if *sheet_index != target_sheet {
|
||||
*sheet_index = target_sheet;
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
}
|
||||
*row1 = target_row + *row1 - source_area.row;
|
||||
*column1 = target_column + *column1 - source_area.column;
|
||||
*row2 = target_row + *row2 - source_area.row;
|
||||
*column2 = target_column + *column2 - source_area.column;
|
||||
}
|
||||
}
|
||||
// Recurse
|
||||
Node::OpRangeKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpConcatenateKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpSumKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpProductKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpPowerKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::FunctionKind { kind: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::InvalidFunctionKind { name: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::CompareKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
// TODO: Not implemented
|
||||
Node::ArrayKind(_) => {}
|
||||
// Do nothing. Note: we could do a blanket _ => {}
|
||||
Node::VariableKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::EmptyArgKind => {}
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::WrongReferenceKind { .. } => {}
|
||||
Node::WrongRangeKind { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ pub fn is_english_error_string(name: &str) -> bool {
|
||||
"#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!",
|
||||
"#CALC!", "#CIRC!", "#NULL!",
|
||||
];
|
||||
names.iter().any(|e| *e == name)
|
||||
names.contains(&name)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
@@ -240,6 +240,7 @@ pub enum TokenType {
|
||||
Bang, // !
|
||||
Percent, // %
|
||||
And, // &
|
||||
At, // @
|
||||
Reference {
|
||||
sheet: Option<String>,
|
||||
row: i32,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
@@ -245,6 +246,9 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
}
|
||||
ParsePart::Number(p) => {
|
||||
let mut text = "".to_string();
|
||||
if let Some(c) = p.currency {
|
||||
text = format!("{}", c);
|
||||
}
|
||||
let tokens = &p.tokens;
|
||||
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
||||
// p.precision is the number of significant digits _after_ the decimal point
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Lexer {
|
||||
pub enum Token {
|
||||
Color(i32), // [Red] or [Color 23]
|
||||
Condition(Compare, f64), // [<=100] (Comparator, number)
|
||||
Currency(char), // [$€] ($ currency symbol)
|
||||
Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X
|
||||
Spacer(char), // *X
|
||||
Ghost(char), // _X
|
||||
@@ -177,10 +178,7 @@ impl Lexer {
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => None,
|
||||
Ok(v) => Some(v),
|
||||
}
|
||||
chars.parse::<f64>().ok()
|
||||
}
|
||||
|
||||
fn consume_condition(&mut self) -> Option<(Compare, f64)> {
|
||||
@@ -274,6 +272,15 @@ impl Lexer {
|
||||
self.set_error("Failed to parse condition");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else if c == '$' {
|
||||
// currency
|
||||
self.read_next_char();
|
||||
if let Some(currency) = self.read_next_char() {
|
||||
self.read_next_char();
|
||||
return Token::Currency(currency);
|
||||
}
|
||||
self.set_error("Failed to parse currency");
|
||||
Token::ILLEGAL
|
||||
} else {
|
||||
// Color
|
||||
if let Some(index) = self.consume_color() {
|
||||
|
||||
@@ -74,6 +74,7 @@ mod test;
|
||||
//
|
||||
// * Color [Red] or [Color 23] or [Color23]
|
||||
// * Conditions [<100]
|
||||
// * Currency [$€]
|
||||
// * Space _X when X is any given char
|
||||
// * A spacer of chars: *X where X is repeated as much as possible
|
||||
// * Literals: $, (, ), :, +, - and space
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct NumberPart {
|
||||
pub is_scientific: bool,
|
||||
pub scientific_minus: bool,
|
||||
pub exponent_digit_count: i32,
|
||||
pub currency: Option<char>,
|
||||
}
|
||||
|
||||
pub struct DatePart {
|
||||
@@ -114,6 +115,7 @@ impl Parser {
|
||||
let mut exponent_digit_count = 0;
|
||||
let mut number = 'i';
|
||||
let mut index = 0;
|
||||
let mut currency = None;
|
||||
|
||||
while token != Token::EOF && token != Token::Separator {
|
||||
let next_token = self.lexer.next_token();
|
||||
@@ -170,6 +172,9 @@ impl Parser {
|
||||
Token::Condition(cmp, value) => {
|
||||
condition = Some((cmp, value));
|
||||
}
|
||||
Token::Currency(c) => {
|
||||
currency = Some(c);
|
||||
}
|
||||
Token::QuestionMark => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '?',
|
||||
@@ -291,6 +296,7 @@ impl Parser {
|
||||
is_scientific,
|
||||
scientific_minus,
|
||||
exponent_digit_count,
|
||||
currency,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ fn test_color() {
|
||||
assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dollar_euro() {
|
||||
let locale = get_default_locale();
|
||||
let format = "[$€]#,##0.00";
|
||||
let t = format_number(3.1, format, locale);
|
||||
assert_eq!(t.text, "€3.10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parts() {
|
||||
let locale = get_default_locale();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -89,6 +84,9 @@ fn compute_future_value(
|
||||
if rate == 0.0 {
|
||||
return Ok(-pv - pmt * nper);
|
||||
}
|
||||
if rate == -1.0 && nper < 0.0 {
|
||||
return Err((Error::DIV, "Divide by zero".to_string()));
|
||||
}
|
||||
|
||||
let rate_nper = (1.0 + rate).powf(nper);
|
||||
let fv = if period_start {
|
||||
@@ -433,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(),
|
||||
};
|
||||
@@ -920,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(
|
||||
@@ -986,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,
|
||||
@@ -1370,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,
|
||||
@@ -1380,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,
|
||||
@@ -1434,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,
|
||||
@@ -1444,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,
|
||||
@@ -1484,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,
|
||||
@@ -1494,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,
|
||||
|
||||
@@ -235,6 +235,11 @@ impl Model {
|
||||
// This cannot happen
|
||||
CalcResult::Number(1.0)
|
||||
}
|
||||
CalcResult::Array(_) => CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
@@ -247,45 +252,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,
|
||||
|
||||
@@ -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,88 +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 {
|
||||
let mut result = false;
|
||||
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 = 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(
|
||||
@@ -163,94 +152,72 @@ 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 => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (Some(current_result), Some(short_circuit_value)) =
|
||||
(result, short_circuit_value)
|
||||
{
|
||||
if current_result == short_circuit_value {
|
||||
return CalcResult::Boolean(current_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::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 => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)
|
||||
{
|
||||
if current_result == short_circuit_value {
|
||||
return CalcResult::Boolean(current_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
if 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])
|
||||
|
||||
@@ -855,7 +855,7 @@ impl Model {
|
||||
if left.row != right.row || left.column != right.column {
|
||||
// FIXME: Implicit intersection or dynamic arrays
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "argument must be a reference to a single cell".to_string(),
|
||||
};
|
||||
|
||||
100
base/src/functions/macros.rs
Normal file
100
base/src/functions/macros.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
#[macro_export]
|
||||
macro_rules! single_number_fn {
|
||||
// The macro takes:
|
||||
// 1) A function name to define (e.g. fn_sin)
|
||||
// 2) The operation to apply (e.g. f64::sin)
|
||||
($fn_name:ident, $op:expr) => {
|
||||
pub(crate) fn $fn_name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// 1) Check exactly one argument
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
// 2) Try to get a "NumberOrArray"
|
||||
match self.get_number_or_array(&args[0], cell) {
|
||||
// -----------------------------------------
|
||||
// Case A: It's a single number
|
||||
// -----------------------------------------
|
||||
Ok(NumberOrArray::Number(f)) => match $op(f) {
|
||||
Ok(x) => CalcResult::Number(x),
|
||||
Err(Error::DIV) => CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Divide by 0".to_string(),
|
||||
},
|
||||
Err(Error::VALUE) => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid number".to_string(),
|
||||
},
|
||||
Err(e) => CalcResult::Error {
|
||||
error: e,
|
||||
origin: cell,
|
||||
message: "Unknown error".to_string(),
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------
|
||||
// Case B: It's an array, so apply $op
|
||||
// element-by-element.
|
||||
// -----------------------------------------
|
||||
Ok(NumberOrArray::Array(a)) => {
|
||||
let mut array = Vec::new();
|
||||
for row in a {
|
||||
let mut data_row = Vec::with_capacity(row.len());
|
||||
for value in row {
|
||||
match value {
|
||||
// If Boolean, treat as 0.0 or 1.0
|
||||
ArrayNode::Boolean(b) => {
|
||||
let n = if b { 1.0 } else { 0.0 };
|
||||
match $op(n) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => {
|
||||
data_row.push(ArrayNode::Error(Error::DIV))
|
||||
}
|
||||
Err(Error::VALUE) => {
|
||||
data_row.push(ArrayNode::Error(Error::VALUE))
|
||||
}
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
}
|
||||
}
|
||||
// If Number, apply directly
|
||||
ArrayNode::Number(n) => match $op(n) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => {
|
||||
data_row.push(ArrayNode::Error(Error::VALUE))
|
||||
}
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
// If String, parse to f64 then apply or #VALUE! error
|
||||
ArrayNode::String(s) => {
|
||||
let node = match s.parse::<f64>() {
|
||||
Ok(f) => match $op(f) {
|
||||
Ok(x) => ArrayNode::Number(x),
|
||||
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
|
||||
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
|
||||
Err(e) => ArrayNode::Error(e),
|
||||
},
|
||||
Err(_) => ArrayNode::Error(Error::VALUE),
|
||||
};
|
||||
data_row.push(node);
|
||||
}
|
||||
// If Error, propagate the error
|
||||
e @ ArrayNode::Error(_) => {
|
||||
data_row.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
array.push(data_row);
|
||||
}
|
||||
CalcResult::Array(array)
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Case C: It's an Error => just return it
|
||||
// -----------------------------------------
|
||||
Err(err_result) => err_result,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::cast::NumberOrArray;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::single_number_fn;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
@@ -169,6 +172,27 @@ impl Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
result += value;
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
@@ -354,187 +378,29 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.sin();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_cos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.cos();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_tan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.tan();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.sinh();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_cosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.cosh();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_tanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.tanh();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_asin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.asin();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ASIN".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_acos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.acos();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for COS".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_atan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.atan();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ATAN".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_asinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.asinh();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ASINH".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_acosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.acosh();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ACOSH".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_atanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.atanh();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ATANH".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
|
||||
single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
|
||||
single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
|
||||
single_number_fn!(fn_sinh, |f| Ok(f64::sinh(f)));
|
||||
single_number_fn!(fn_cosh, |f| Ok(f64::cosh(f)));
|
||||
single_number_fn!(fn_tanh, |f| Ok(f64::tanh(f)));
|
||||
single_number_fn!(fn_asin, |f| Ok(f64::asin(f)));
|
||||
single_number_fn!(fn_acos, |f| Ok(f64::acos(f)));
|
||||
single_number_fn!(fn_atan, |f| Ok(f64::atan(f)));
|
||||
single_number_fn!(fn_asinh, |f| Ok(f64::asinh(f)));
|
||||
single_number_fn!(fn_acosh, |f| Ok(f64::acosh(f)));
|
||||
single_number_fn!(fn_atanh, |f| Ok(f64::atanh(f)));
|
||||
single_number_fn!(fn_abs, |f| Ok(f64::abs(f)));
|
||||
single_number_fn!(fn_sqrt, |f| if f < 0.0 {
|
||||
Err(Error::NUM)
|
||||
} else {
|
||||
Ok(f64::sqrt(f))
|
||||
});
|
||||
single_number_fn!(fn_sqrtpi, |f: f64| if f < 0.0 {
|
||||
Err(Error::NUM)
|
||||
} else {
|
||||
Ok((f * PI).sqrt())
|
||||
});
|
||||
|
||||
pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !args.is_empty() {
|
||||
@@ -543,53 +409,6 @@ impl Model {
|
||||
CalcResult::Number(PI)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_abs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(value.abs())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sqrtpi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if value < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Argument of SQRTPI should be >= 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number((value * PI).sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sqrt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if value < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Argument of SQRT should be >= 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(value.sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
|
||||
@@ -15,6 +15,7 @@ mod financial_util;
|
||||
mod information;
|
||||
mod logical;
|
||||
mod lookup_and_reference;
|
||||
mod macros;
|
||||
mod mathematical;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
@@ -140,6 +141,7 @@ pub enum Function {
|
||||
Countifs,
|
||||
Maxifs,
|
||||
Minifs,
|
||||
Geomean,
|
||||
|
||||
// Date and time
|
||||
Date,
|
||||
@@ -248,7 +250,7 @@ pub enum Function {
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 194> {
|
||||
pub fn into_iter() -> IntoIter<Function, 195> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -348,6 +350,7 @@ impl Function {
|
||||
Function::Countifs,
|
||||
Function::Maxifs,
|
||||
Function::Minifs,
|
||||
Function::Geomean,
|
||||
Function::Year,
|
||||
Function::Day,
|
||||
Function::Month,
|
||||
@@ -611,6 +614,7 @@ impl Function {
|
||||
"COUNTIFS" => Some(Function::Countifs),
|
||||
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
|
||||
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
|
||||
"GEOMEAN" => Some(Function::Geomean),
|
||||
// Date and Time
|
||||
"YEAR" => Some(Function::Year),
|
||||
"DAY" => Some(Function::Day),
|
||||
@@ -818,6 +822,7 @@ impl fmt::Display for Function {
|
||||
Function::Countifs => write!(f, "COUNTIFS"),
|
||||
Function::Maxifs => write!(f, "MAXIFS"),
|
||||
Function::Minifs => write!(f, "MINIFS"),
|
||||
Function::Geomean => write!(f, "GEOMEAN"),
|
||||
Function::Year => write!(f, "YEAR"),
|
||||
Function::Day => write!(f, "DAY"),
|
||||
Function::Month => write!(f, "MONTH"),
|
||||
@@ -945,7 +950,7 @@ impl Model {
|
||||
match kind {
|
||||
// Logical
|
||||
Function::And => self.fn_and(args, cell),
|
||||
Function::False => CalcResult::Boolean(false),
|
||||
Function::False => self.fn_false(args, cell),
|
||||
Function::If => self.fn_if(args, cell),
|
||||
Function::Iferror => self.fn_iferror(args, cell),
|
||||
Function::Ifna => self.fn_ifna(args, cell),
|
||||
@@ -953,7 +958,7 @@ impl Model {
|
||||
Function::Not => self.fn_not(args, cell),
|
||||
Function::Or => self.fn_or(args, cell),
|
||||
Function::Switch => self.fn_switch(args, cell),
|
||||
Function::True => CalcResult::Boolean(true),
|
||||
Function::True => self.fn_true(args, cell),
|
||||
Function::Xor => self.fn_xor(args, cell),
|
||||
// Math and trigonometry
|
||||
Function::Sin => self.fn_sin(args, cell),
|
||||
@@ -1054,6 +1059,7 @@ impl Model {
|
||||
Function::Countifs => self.fn_countifs(args, cell),
|
||||
Function::Maxifs => self.fn_maxifs(args, cell),
|
||||
Function::Minifs => self.fn_minifs(args, cell),
|
||||
Function::Geomean => self.fn_geomean(args, cell),
|
||||
// Date and Time
|
||||
Function::Year => self.fn_year(args, cell),
|
||||
Function::Day => self.fn_day(args, cell),
|
||||
|
||||
@@ -134,6 +134,13 @@ impl Model {
|
||||
);
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +172,13 @@ impl Model {
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
@@ -635,4 +649,85 @@ impl Model {
|
||||
}
|
||||
CalcResult::Number(max)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut product = 1.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
product *= value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
product *= if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
product *= value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
product *= t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(product.powf(1.0 / count))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,13 @@ impl Model {
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0),
|
||||
CalcResult::Array(_) => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,6 +433,13 @@ impl Model {
|
||||
| CalcResult::Number(_)
|
||||
| CalcResult::Boolean(_)
|
||||
| CalcResult::Error { .. } => counta += 1,
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +97,24 @@ impl Model {
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Range { .. } => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::String(result)
|
||||
@@ -125,6 +139,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let format_code = match self.get_string(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
@@ -280,6 +301,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
return CalcResult::Number(s.chars().count() as f64);
|
||||
}
|
||||
@@ -308,6 +336,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
return CalcResult::String(s.trim().to_owned());
|
||||
}
|
||||
@@ -336,6 +371,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
return CalcResult::String(s.to_lowercase());
|
||||
}
|
||||
@@ -370,6 +412,13 @@ impl Model {
|
||||
message: "Empty cell".to_string(),
|
||||
}
|
||||
}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match s.chars().next() {
|
||||
@@ -411,6 +460,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
return CalcResult::String(s.to_uppercase());
|
||||
}
|
||||
@@ -441,6 +497,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let num_chars = if args.len() == 2 {
|
||||
match self.evaluate_node_in_context(&args[1], cell) {
|
||||
@@ -471,6 +534,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
1
|
||||
@@ -509,6 +579,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let num_chars = if args.len() == 2 {
|
||||
match self.evaluate_node_in_context(&args[1], cell) {
|
||||
@@ -539,6 +616,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
1
|
||||
@@ -577,6 +661,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let start_num = match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Number(v) => {
|
||||
@@ -641,6 +732,13 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut result = "".to_string();
|
||||
let mut count: usize = 0;
|
||||
@@ -983,6 +1081,13 @@ impl Model {
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyArg | CalcResult::Range { .. } => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1002,6 +1107,13 @@ impl Model {
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyArg => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
let result = values.join(&delimiter);
|
||||
@@ -1125,6 +1237,11 @@ impl Model {
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0),
|
||||
CalcResult::Array(_) => CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -393,10 +393,8 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResul
|
||||
// An error will match an error (never a string that is an error)
|
||||
Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
|
||||
}
|
||||
CalcResult::Range { left: _, right: _ } => {
|
||||
// TODO: Implicit Intersection
|
||||
Box::new(move |_x| false)
|
||||
}
|
||||
CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false),
|
||||
CalcResult::Array(_) => Box::new(move |_x| false),
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,9 +141,9 @@ impl Model {
|
||||
/// * 1 - Perform a search starting at the first item. This is the default.
|
||||
/// * -1 - Perform a reverse search starting at the last item.
|
||||
/// * 2 - Perform a binary search that relies on lookup_array being sorted
|
||||
/// in ascending order. If not sorted, invalid results will be returned.
|
||||
/// in ascending order. If not sorted, invalid results will be returned.
|
||||
/// * -2 - Perform a binary search that relies on lookup_array being sorted
|
||||
/// in descending order. If not sorted, invalid results will be returned.
|
||||
/// in descending order. If not sorted, invalid results will be returned.
|
||||
pub(crate) fn fn_xlookup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() < 3 || args.len() > 6 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
|
||||
@@ -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;
|
||||
@@ -37,9 +39,9 @@ pub mod types;
|
||||
pub mod worksheet;
|
||||
|
||||
mod actions;
|
||||
mod arithmetic;
|
||||
mod cast;
|
||||
mod constants;
|
||||
mod diffs;
|
||||
mod functions;
|
||||
mod implicit_intersection;
|
||||
mod model;
|
||||
@@ -57,6 +59,7 @@ pub mod mock_time;
|
||||
|
||||
pub use model::get_milliseconds_since_epoch;
|
||||
pub use model::Model;
|
||||
pub use model::CellStructure;
|
||||
pub use user_model::BorderArea;
|
||||
pub use user_model::ClipboardData;
|
||||
pub use user_model::UserModel;
|
||||
|
||||
@@ -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},
|
||||
@@ -30,6 +31,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(test)]
|
||||
pub use crate::mock_time::get_milliseconds_since_epoch;
|
||||
@@ -71,7 +73,29 @@ pub(crate) enum CellState {
|
||||
Evaluating,
|
||||
}
|
||||
|
||||
/// Cell structure indicates if the cell is part of a merged cell or not
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum CellStructure {
|
||||
/// The cell is not part of a merged cell
|
||||
Simple,
|
||||
/// The cell is part of a merged cell, and teh root cell is (row, column)
|
||||
Merged {
|
||||
/// Row of the root cell
|
||||
row: i32,
|
||||
/// Column of the root cell
|
||||
column: i32,
|
||||
},
|
||||
/// The cell is the root of a merged cell of dimensions (width, height)
|
||||
MergedRoot {
|
||||
/// Width of the merged cell
|
||||
width: i32,
|
||||
/// Height of the merged cell
|
||||
height: i32,
|
||||
},
|
||||
}
|
||||
|
||||
/// A parsed formula for a defined name
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ParsedDefinedName {
|
||||
/// CellReference (`=C4`)
|
||||
CellReference(CellReferenceIndex),
|
||||
@@ -79,9 +103,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.
|
||||
@@ -208,6 +229,17 @@ impl Model {
|
||||
},
|
||||
}
|
||||
}
|
||||
Node::ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => match self.evaluate_node_with_reference(child, cell) {
|
||||
CalcResult::Range { left, right } => CalcResult::Range { left, right },
|
||||
_ => CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||
),
|
||||
},
|
||||
_ => self.evaluate_node_in_context(node, cell),
|
||||
}
|
||||
}
|
||||
@@ -257,27 +289,10 @@ impl Model {
|
||||
) -> CalcResult {
|
||||
use Node::*;
|
||||
match node {
|
||||
OpSumKind { kind, left, right } => {
|
||||
// In the future once the feature try trait stabilizes we could use the '?' operator for this :)
|
||||
// See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5
|
||||
let l = match self.get_number(left, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
let r = match self.get_number(right, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
let result = match kind {
|
||||
OpSum::Add => l + r,
|
||||
OpSum::Minus => l - r,
|
||||
};
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
OpSumKind { kind, left, right } => match kind {
|
||||
OpSum::Add => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 + f2)),
|
||||
OpSum::Minus => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 - f2)),
|
||||
},
|
||||
NumberKind(value) => CalcResult::Number(*value),
|
||||
StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)),
|
||||
BooleanKind(value) => CalcResult::Boolean(*value),
|
||||
@@ -365,90 +380,60 @@ impl Model {
|
||||
let result = format!("{}{}", l, r);
|
||||
CalcResult::String(result)
|
||||
}
|
||||
OpProductKind { kind, left, right } => {
|
||||
let l = match self.get_number(left, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
OpProductKind { kind, left, right } => match kind {
|
||||
OpProduct::Times => {
|
||||
self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 * f2))
|
||||
}
|
||||
OpProduct::Divide => self.handle_arithmetic(left, right, cell, &|f1, f2| {
|
||||
if f2 == 0.0 {
|
||||
Err(Error::DIV)
|
||||
} else {
|
||||
Ok(f1 / f2)
|
||||
}
|
||||
};
|
||||
let r = match self.get_number(right, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
let result = match kind {
|
||||
OpProduct::Times => l * r,
|
||||
OpProduct::Divide => {
|
||||
if r == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Divide by Zero".to_string(),
|
||||
);
|
||||
}
|
||||
l / r
|
||||
}
|
||||
};
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}),
|
||||
},
|
||||
OpPowerKind { left, right } => {
|
||||
let l = match self.get_number(left, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
let r = match self.get_number(right, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
// Deal with errors properly
|
||||
CalcResult::Number(l.powf(r))
|
||||
self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1.powf(f2)))
|
||||
}
|
||||
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
||||
InvalidFunctionKind { name, args: _ } => {
|
||||
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
|
||||
}
|
||||
ArrayKind(_) => {
|
||||
// TODO: NOT IMPLEMENTED
|
||||
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
|
||||
}
|
||||
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 {
|
||||
ArrayKind(s) => CalcResult::Array(s.to_owned()),
|
||||
DefinedNameKind((name, scope, _)) => {
|
||||
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
|
||||
match parsed_defined_name {
|
||||
ParsedDefinedName::CellReference(reference) => {
|
||||
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() {
|
||||
@@ -527,6 +512,22 @@ impl Model {
|
||||
format!("Error parsing {}: {}", formula, message),
|
||||
),
|
||||
EmptyArgKind => CalcResult::EmptyArg,
|
||||
ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => match self.evaluate_node_with_reference(child, cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => self.evaluate_cell(cell_reference),
|
||||
None => CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => self.evaluate_node_in_context(child, cell),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,12 +617,15 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
let range = Range {
|
||||
left: *left,
|
||||
right: *right,
|
||||
};
|
||||
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
|
||||
if left.sheet == right.sheet
|
||||
&& left.row == right.row
|
||||
&& left.column == right.column
|
||||
{
|
||||
let intersection_cell = CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
};
|
||||
let v = self.evaluate_cell(intersection_cell);
|
||||
self.set_cell_value(cell_reference, &v);
|
||||
} else {
|
||||
@@ -638,10 +642,32 @@ impl Model {
|
||||
f,
|
||||
s,
|
||||
o,
|
||||
m: "Invalid reference".to_string(),
|
||||
ei: Error::VALUE,
|
||||
m: "Implicit Intersection not implemented".to_string(),
|
||||
ei: Error::NIMPL,
|
||||
};
|
||||
}
|
||||
// if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
|
||||
// {
|
||||
// let v = self.evaluate_cell(intersection_cell);
|
||||
// self.set_cell_value(cell_reference, &v);
|
||||
// } else {
|
||||
// let o = match self.cell_reference_to_string(&cell_reference) {
|
||||
// Ok(s) => s,
|
||||
// Err(_) => "".to_string(),
|
||||
// };
|
||||
// *self.workbook.worksheets[sheet as usize]
|
||||
// .sheet_data
|
||||
// .get_mut(&row)
|
||||
// .expect("expected a row")
|
||||
// .get_mut(&column)
|
||||
// .expect("expected a column") = Cell::CellFormulaError {
|
||||
// f,
|
||||
// s,
|
||||
// o,
|
||||
// m: "Invalid reference".to_string(),
|
||||
// ei: Error::VALUE,
|
||||
// };
|
||||
// }
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
||||
*self.workbook.worksheets[sheet as usize]
|
||||
@@ -651,6 +677,20 @@ impl Model {
|
||||
.get_mut(&column)
|
||||
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
|
||||
}
|
||||
CalcResult::Array(_) => {
|
||||
*self.workbook.worksheets[sheet as usize]
|
||||
.sheet_data
|
||||
.get_mut(&row)
|
||||
.expect("expected a row")
|
||||
.get_mut(&column)
|
||||
.expect("expected a column") = Cell::CellFormulaError {
|
||||
f,
|
||||
s,
|
||||
o: "".to_string(),
|
||||
m: "Arrays not supported yet".to_string(),
|
||||
ei: Error::NIMPL,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,6 +722,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)?;
|
||||
@@ -726,6 +773,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
Merged { .. } => CalcResult::EmptyCell,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,6 +905,7 @@ impl Model {
|
||||
|
||||
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
|
||||
|
||||
let defined_names = workbook.get_defined_names_with_scope();
|
||||
// add all tables
|
||||
// let mut tables = Vec::new();
|
||||
// for worksheet in worksheets {
|
||||
@@ -866,7 +915,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 +996,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 +1073,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 +1181,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,
|
||||
@@ -1415,6 +1461,10 @@ impl Model {
|
||||
value: String,
|
||||
) -> Result<(), String> {
|
||||
// If value starts with "'" then we force the style to be quote_prefix
|
||||
let cell = self.workbook.worksheet(sheet)?.cell(row, column);
|
||||
if matches!(cell, Some(Cell::Merged { .. })) {
|
||||
return Err("Cannot set value on merged cell".to_string());
|
||||
}
|
||||
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||
if let Some(new_value) = value.strip_prefix('\'') {
|
||||
// First check if it needs quoting
|
||||
@@ -1511,13 +1561,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 +1644,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:
|
||||
@@ -1828,12 +1912,29 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Returns the style for cell (`sheet`, `row`, `column`)
|
||||
/// If the cell does not have a style defined we check the row, otherwise the column and finally a default
|
||||
pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
|
||||
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||
let style = self.workbook.styles.get_style(style_index)?;
|
||||
Ok(style)
|
||||
}
|
||||
|
||||
/// Returns the style defined in a cell if any.
|
||||
pub fn get_cell_style_or_none(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<Style>, String> {
|
||||
let style = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.map(|c| self.workbook.styles.get_style(c.get_style()))
|
||||
.transpose();
|
||||
style
|
||||
}
|
||||
|
||||
/// Returns an internal binary representation of the workbook
|
||||
///
|
||||
/// See also:
|
||||
@@ -1986,6 +2087,289 @@ 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())
|
||||
}
|
||||
}
|
||||
/// Returns the style object of a column, if any
|
||||
pub fn get_column_style(&self, sheet: u32, column: i32) -> Result<Option<Style>, String> {
|
||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||
let cols = &worksheet.cols;
|
||||
for col in cols {
|
||||
if column >= col.min && column <= col.max {
|
||||
if let Some(style_index) = col.style {
|
||||
let style = self.workbook.styles.get_style(style_index)?;
|
||||
return Ok(Some(style));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
Err("Invalid sheet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the style object of a row, if any
|
||||
pub fn get_row_style(&self, sheet: u32, row: i32) -> Result<Option<Style>, String> {
|
||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||
let rows = &worksheet.rows;
|
||||
for r in rows {
|
||||
if row == r.r {
|
||||
let style = self.workbook.styles.get_style(r.s)?;
|
||||
return Ok(Some(style));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
Err("Invalid sheet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a column with style
|
||||
pub fn set_column_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
style: &Style,
|
||||
) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_column_style(column, style_index)
|
||||
}
|
||||
|
||||
/// Sets a row with style
|
||||
pub fn set_row_style(&mut self, sheet: u32, row: i32, style: &Style) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_row_style(row, style_index)
|
||||
}
|
||||
|
||||
/// Deletes the style of a column if the is any
|
||||
pub fn delete_column_style(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.delete_column_style(column)
|
||||
}
|
||||
|
||||
/// Deletes the style of a row if there is any
|
||||
pub fn delete_row_style(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||
self.workbook.worksheet_mut(sheet)?.delete_row_style(row)
|
||||
}
|
||||
|
||||
/// Returns the geometric structure of a cell
|
||||
pub fn get_cell_structure(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<CellStructure, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
worksheet.get_cell_structure(row, column)
|
||||
}
|
||||
|
||||
/// Merges cells
|
||||
pub fn merge_cells(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
let sheet_data = &mut worksheet.sheet_data;
|
||||
// First check that it is possible to merge the cells
|
||||
for r in row..(row + height) {
|
||||
for c in column..(column + width) {
|
||||
if let Some(Cell::Merged { .. }) =
|
||||
sheet_data.get(&r).and_then(|row_data| row_data.get(&c))
|
||||
{
|
||||
return Err("Cannot merge cells".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
worksheet
|
||||
.merged_cells
|
||||
.insert((row, column), (width, height));
|
||||
for r in row..(row + height) {
|
||||
for c in column..(column + width) {
|
||||
// We remove everything except the "root" cell:
|
||||
if r == row && c == column {
|
||||
continue;
|
||||
}
|
||||
if let Some(row_data) = sheet_data.get_mut(&r) {
|
||||
row_data.remove(&c);
|
||||
row_data.insert(c, Cell::Merged { r: row, c: column });
|
||||
} else {
|
||||
let mut row_data = HashMap::new();
|
||||
row_data.insert(c, Cell::Merged { r: row, c: column });
|
||||
sheet_data.insert(r, row_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmerges cells
|
||||
pub fn unmerge_cells(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
let s = self.get_cell_style_index(sheet, row, column)?;
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
let sheet_data = &mut worksheet.sheet_data;
|
||||
let (width, height) = match worksheet.merged_cells.get(&(row, column)) {
|
||||
Some((w, h)) => (*w, *h),
|
||||
None => return Ok(()),
|
||||
};
|
||||
worksheet.merged_cells.remove(&(row, column));
|
||||
for r in row..(row + width) {
|
||||
for c in column..(column + height) {
|
||||
// We remove everything except the "root" cell:
|
||||
if r == row && c == column {
|
||||
continue;
|
||||
}
|
||||
if let Some(row_data) = sheet_data.get_mut(&r) {
|
||||
row_data.remove(&c);
|
||||
if s != 0 {
|
||||
row_data.insert(c, Cell::EmptyCell { s });
|
||||
}
|
||||
} else if s != 0 {
|
||||
let mut row_data = HashMap::new();
|
||||
row_data.insert(c, Cell::EmptyCell { s });
|
||||
sheet_data.insert(r, row_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,11 +4,11 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
calc_result::Range,
|
||||
constants::{DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH},
|
||||
constants::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::{
|
||||
stringify::{rename_sheet_in_node, to_rc_format},
|
||||
stringify::{rename_sheet_in_node, to_rc_format, to_string},
|
||||
Parser,
|
||||
},
|
||||
types::CellReferenceRC,
|
||||
@@ -17,7 +17,8 @@ use crate::{
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{
|
||||
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
|
||||
DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
|
||||
WorksheetView,
|
||||
},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
@@ -57,10 +58,10 @@ impl Model {
|
||||
rows: vec![],
|
||||
comments: vec![],
|
||||
dimension: "A1".to_string(),
|
||||
merge_cells: vec![],
|
||||
name: name.to_string(),
|
||||
shared_formulas: vec![],
|
||||
sheet_data: Default::default(),
|
||||
merged_cells: HashMap::new(),
|
||||
sheet_id,
|
||||
state: SheetState::Visible,
|
||||
color: Default::default(),
|
||||
@@ -85,14 +86,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 +145,9 @@ 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();
|
||||
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();
|
||||
@@ -237,7 +239,7 @@ impl Model {
|
||||
|
||||
/// Renames a sheet and updates all existing references to that sheet.
|
||||
/// It can fail if:
|
||||
/// * The original index is too large
|
||||
/// * The original index is out of bounds
|
||||
/// * The target sheet name already exists
|
||||
/// * The target sheet name is invalid
|
||||
pub fn rename_sheet_by_index(
|
||||
@@ -251,22 +253,20 @@ impl Model {
|
||||
if self.get_sheet_index_by_name(new_name).is_some() {
|
||||
return Err(format!("Sheet already exists: '{}'.", new_name));
|
||||
}
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
let sheet_count = worksheets.len() as u32;
|
||||
if sheet_index >= sheet_count {
|
||||
return Err("Sheet index out of bounds".to_string());
|
||||
}
|
||||
// Gets the new name and checks that a sheet with that index exists
|
||||
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
|
||||
|
||||
// Parse all formulas with the old name
|
||||
// All internal formulas are R1C1
|
||||
self.parser.set_lexer_mode(LexerMode::R1C1);
|
||||
// We use iter because the default would be a mut_iter and we don't need a mutable reference
|
||||
let worksheets = &mut self.workbook.worksheets;
|
||||
for worksheet in worksheets {
|
||||
let cell_reference = &Some(CellReferenceRC {
|
||||
|
||||
for worksheet in &mut self.workbook.worksheets {
|
||||
// R1C1 formulas are not tied to a cell (but are tied to a cell)
|
||||
let cell_reference = &CellReferenceRC {
|
||||
sheet: worksheet.get_name(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
});
|
||||
};
|
||||
let mut formulas = Vec::new();
|
||||
for formula in &worksheet.shared_formulas {
|
||||
let mut t = self.parser.parse(formula, cell_reference);
|
||||
@@ -275,11 +275,32 @@ impl Model {
|
||||
}
|
||||
worksheet.shared_formulas = formulas;
|
||||
}
|
||||
// Se the mode back to A1
|
||||
|
||||
// Set the mode back to A1
|
||||
self.parser.set_lexer_mode(LexerMode::A1);
|
||||
|
||||
// We reparse all the defined names formulas
|
||||
let mut defined_names = Vec::new();
|
||||
// Defined names do not have a context, we can use anything
|
||||
let cell_reference = &CellReferenceRC {
|
||||
sheet: old_name.clone(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
for defined_name in &mut self.workbook.defined_names {
|
||||
let mut t = self.parser.parse(&defined_name.formula, cell_reference);
|
||||
rename_sheet_in_node(&mut t, sheet_index, new_name);
|
||||
let formula = to_string(&t, cell_reference);
|
||||
defined_names.push(DefinedName {
|
||||
name: defined_name.name.clone(),
|
||||
formula,
|
||||
sheet_id: defined_name.sheet_id,
|
||||
});
|
||||
}
|
||||
self.workbook.defined_names = defined_names;
|
||||
|
||||
// Update the name of the worksheet
|
||||
let worksheets = &mut self.workbook.worksheets;
|
||||
worksheets[sheet_index as usize].set_name(new_name);
|
||||
self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
|
||||
self.reset_parsed_structures();
|
||||
Ok(())
|
||||
}
|
||||
@@ -295,7 +316,7 @@ impl Model {
|
||||
};
|
||||
if sheet_index >= sheet_count {
|
||||
return Err("Sheet index too large".to_string());
|
||||
}
|
||||
};
|
||||
self.workbook.worksheets.remove(sheet_index as usize);
|
||||
self.reset_parsed_structures();
|
||||
Ok(())
|
||||
@@ -359,7 +380,7 @@ impl Model {
|
||||
WorkbookView {
|
||||
sheet: 0,
|
||||
window_width: DEFAULT_WINDOW_WIDTH,
|
||||
window_height: DEFAULT_WINDOW_HEIGH,
|
||||
window_height: DEFAULT_WINDOW_HEIGHT,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -388,7 +409,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
|
||||
|
||||
@@ -4,8 +4,6 @@ use crate::{
|
||||
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles},
|
||||
};
|
||||
|
||||
// TODO: Move Styles and all related types from crate::types here
|
||||
// Not doing it right now to not have conflicts with exporter branch
|
||||
impl Styles {
|
||||
fn get_font_index(&self, font: &Font) -> Option<i32> {
|
||||
for (font_index, item) in self.fonts.iter().enumerate() {
|
||||
|
||||
@@ -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;
|
||||
@@ -26,7 +28,6 @@ mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_fn_unicode;
|
||||
mod test_forward_references;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_math;
|
||||
@@ -35,11 +36,13 @@ mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_move_formula;
|
||||
mod test_quote_prefix;
|
||||
mod test_row_column_styles;
|
||||
mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_styles;
|
||||
mod test_trigonometric;
|
||||
mod test_true_false;
|
||||
mod test_workbook;
|
||||
mod test_worksheet;
|
||||
pub(crate) mod util;
|
||||
@@ -48,11 +51,16 @@ mod engineering;
|
||||
mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_arrays;
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_fv;
|
||||
mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_geomean;
|
||||
mod test_get_cell_content;
|
||||
mod test_implicit_intersection;
|
||||
mod test_issue_155;
|
||||
mod test_percentage;
|
||||
mod test_set_functions_error_handling;
|
||||
mod test_today;
|
||||
|
||||
13
base/src/test/test_arrays.rs
Normal file
13
base/src/test/test_arrays.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn sum_arrays() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=SUM({1,2,3}+{3,4,5})");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"18");
|
||||
}
|
||||
@@ -82,3 +82,21 @@ fn test_column_width_higher_edge() {
|
||||
assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 16), Ok(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_width_negative() {
|
||||
let mut model = new_empty_model();
|
||||
let result = model
|
||||
.workbook
|
||||
.worksheet_mut(0)
|
||||
.unwrap()
|
||||
.set_column_width(16, -1.0);
|
||||
assert_eq!(result, Err("Can not set a negative width: -1".to_string()));
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 0);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert_eq!(
|
||||
(worksheet.get_column_width(16).unwrap()),
|
||||
DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 16), Ok(0));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -22,13 +22,14 @@ fn fn_concatenate() {
|
||||
model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#);
|
||||
// This will break once we implement the implicit intersection operator
|
||||
// It should be:
|
||||
// model._set("B2", r#"=CONCATENATE(@A1:A3, "!")"#);
|
||||
model._set("C2", r#"=CONCATENATE(@A1:A3, "!")"#);
|
||||
model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#);
|
||||
model._set("B3", r#"=CONCAT(A1:A3, "!")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"Hello my World!");
|
||||
assert_eq!(model._get_text("B2"), *" my !");
|
||||
assert_eq!(model._get_text("B2"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("B3"), *"Hello my World!");
|
||||
assert_eq!(model._get_text("C2"), *" my !");
|
||||
}
|
||||
|
||||
15
base/src/test/test_fn_day.rs
Normal file
15
base/src/test/test_fn_day.rs
Normal 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");
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn simple_cases() {}
|
||||
|
||||
#[test]
|
||||
fn wrong_number_of_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
@@ -33,8 +30,18 @@ fn implicit_intersection() {
|
||||
model._set("A2", "=FORMULATEXT(D1:E1)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A1"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("A2"), *"#N/IMPL!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_intersection_operator() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=1 + 2");
|
||||
model._set("B1", "=FORMULATEXT(@A:A)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#N/IMPL!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
36
base/src/test/test_fn_fv.rs
Normal file
36
base/src/test/test_fn_fv.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn computation() {
|
||||
let i2 = "=-C2*(1+D2)^E2-F2*((D2+1)*((1+D2)^E2-1))/D2";
|
||||
|
||||
let mut model = new_empty_model();
|
||||
model._set("C2", "1");
|
||||
model._set("D2", "2");
|
||||
model._set("E2", "3");
|
||||
model._set("F2", "4");
|
||||
|
||||
model._set("I2", i2);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("I2"), "-183");
|
||||
assert_eq!(model._get_formula("I2"), i2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_currency() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("C2", "1");
|
||||
model._set("D2", "2");
|
||||
model._set("E2", "3");
|
||||
model._set("F2", "4");
|
||||
|
||||
model._set("I2", "=FV(D2,E2,F2,C2,1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("I2"), "-$183.00");
|
||||
}
|
||||
204
base/src/test/test_fn_or_xor.rs
Normal file
204
base/src/test/test_fn_or_xor.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -17,3 +17,19 @@ fn test_fn_sum_arguments() {
|
||||
assert_eq!(model._get_text("A3"), *"1");
|
||||
assert_eq!(model._get_text("A4"), *"4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=SUM({1, 2, 3})");
|
||||
model._set("A2", "=SUM({1; 2; 3})");
|
||||
model._set("A3", "=SUM({1, 2; 3, 4})");
|
||||
model._set("A4", "=SUM({1, 2; 3, 4; 5, 6})");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"6");
|
||||
assert_eq!(model._get_text("A2"), *"6");
|
||||
assert_eq!(model._get_text("A3"), *"10");
|
||||
assert_eq!(model._get_text("A4"), *"21");
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_forward_references() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// test single ref changed nd not changed
|
||||
model._set("H8", "=F6*G9");
|
||||
// tests areas
|
||||
model._set("H9", "=SUM(D4:F6)");
|
||||
// absolute coordinates
|
||||
model._set("H10", "=$F$6");
|
||||
// area larger than the source area
|
||||
model._set("H11", "=SUM(D3:F6)");
|
||||
// Test arguments and concat
|
||||
model._set("H12", "=SUM(F6, D4:F6) & D4");
|
||||
// Test range operator. This is syntax error for now.
|
||||
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
|
||||
// Test operations
|
||||
model._set("H14", "=-D4+D5*F6/F5");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Source Area is D4:F6
|
||||
let source_area = &Area {
|
||||
sheet: 0,
|
||||
row: 4,
|
||||
column: 4,
|
||||
width: 3,
|
||||
height: 3,
|
||||
};
|
||||
|
||||
// We paste in B10
|
||||
let target_row = 10;
|
||||
let target_column = 2;
|
||||
let result = model.forward_references(
|
||||
source_area,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
row: target_row,
|
||||
column: target_column,
|
||||
},
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("H8"), "=D12*G9");
|
||||
assert_eq!(model._get_formula("H9"), "=SUM(B10:D12)");
|
||||
assert_eq!(model._get_formula("H10"), "=$D$12");
|
||||
|
||||
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
|
||||
assert_eq!(model._get_formula("H12"), "=SUM(D12,B10:D12)&B10");
|
||||
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
|
||||
assert_eq!(model._get_formula("H14"), "=-B10+B11*D12/D11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// test single ref changed not changed
|
||||
model._set("H8", "=F6*G9");
|
||||
// tests areas
|
||||
model._set("H9", "=SUM(D4:F6)");
|
||||
// absolute coordinates
|
||||
model._set("H10", "=$F$6");
|
||||
// area larger than the source area
|
||||
model._set("H11", "=SUM(D3:F6)");
|
||||
// Test arguments and concat
|
||||
model._set("H12", "=SUM(F6, D4:F6) & D4");
|
||||
// Test range operator. This is syntax error for now.
|
||||
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
|
||||
// Test operations
|
||||
model._set("H14", "=-D4+D5*F6/F5");
|
||||
|
||||
// Adds a new sheet
|
||||
assert!(model.add_sheet("Sheet2").is_ok());
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Source Area is D4:F6
|
||||
let source_area = &Area {
|
||||
sheet: 0,
|
||||
row: 4,
|
||||
column: 4,
|
||||
width: 3,
|
||||
height: 3,
|
||||
};
|
||||
|
||||
// We paste in Sheet2!B10
|
||||
let target_row = 10;
|
||||
let target_column = 2;
|
||||
let result = model.forward_references(
|
||||
source_area,
|
||||
&CellReferenceIndex {
|
||||
sheet: 1,
|
||||
row: target_row,
|
||||
column: target_column,
|
||||
},
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("H8"), "=Sheet2!D12*G9");
|
||||
assert_eq!(model._get_formula("H9"), "=SUM(Sheet2!B10:D12)");
|
||||
assert_eq!(model._get_formula("H10"), "=Sheet2!$D$12");
|
||||
|
||||
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
|
||||
assert_eq!(
|
||||
model._get_formula("H12"),
|
||||
"=SUM(Sheet2!D12,Sheet2!B10:D12)&Sheet2!B10"
|
||||
);
|
||||
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
|
||||
assert_eq!(
|
||||
model._get_formula("H14"),
|
||||
"=-Sheet2!B10+Sheet2!B11*Sheet2!D12/Sheet2!D11"
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::DEFAULT_ROW_HEIGHT;
|
||||
|
||||
use crate::cell::CellValue;
|
||||
|
||||
use crate::number_format::to_excel_precision_str;
|
||||
@@ -113,6 +115,15 @@ fn test_set_row_height() {
|
||||
worksheet.set_row_height(5, 5.0).unwrap();
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!((5.0 - worksheet.row_height(5).unwrap()).abs() < f64::EPSILON);
|
||||
|
||||
let worksheet = model.workbook.worksheet_mut(0).unwrap();
|
||||
let result = worksheet.set_row_height(6, -1.0);
|
||||
assert_eq!(result, Err("Can not set a negative height: -1".to_string()));
|
||||
|
||||
assert_eq!(worksheet.row_height(6).unwrap(), DEFAULT_ROW_HEIGHT);
|
||||
|
||||
worksheet.set_row_height(6, 0.0).unwrap();
|
||||
assert_eq!(worksheet.row_height(6).unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
27
base/src/test/test_geomean.rs
Normal file
27
base/src/test/test_geomean.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_geomean_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=GEOMEAN()");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_geomean_minimal() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("B1", "1");
|
||||
model._set("B2", "2");
|
||||
model._set("B3", "3");
|
||||
model._set("B4", "'2");
|
||||
// B5 is empty
|
||||
model._set("B6", "true");
|
||||
model._set("A1", "=GEOMEAN(B1:B6)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1.817120593");
|
||||
}
|
||||
50
base/src/test/test_implicit_intersection.rs
Normal file
50
base/src/test/test_implicit_intersection.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn simple_colum() {
|
||||
let mut model = new_empty_model();
|
||||
// We populate cells A1 to A3
|
||||
model._set("A1", "1");
|
||||
model._set("A2", "2");
|
||||
model._set("A3", "3");
|
||||
|
||||
model._set("C2", "=@A1:A3");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C2"), "2".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_of_array_is_n_impl() {
|
||||
let mut model = new_empty_model();
|
||||
// We populate cells A1 to A3
|
||||
model._set("A1", "1");
|
||||
model._set("A2", "2");
|
||||
model._set("A3", "3");
|
||||
|
||||
model._set("C2", "=A1:A3");
|
||||
model._set("D2", "=SUM(SIN(A:A)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string());
|
||||
assert_eq!(model._get_text("D2"), "1.89188842".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concat() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CONCAT(@B1:B3)");
|
||||
model._set("A2", "=CONCAT(B1:B3)");
|
||||
model._set("B1", "Hello");
|
||||
model._set("B2", " ");
|
||||
model._set("B3", "world!");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"Hello");
|
||||
assert_eq!(model._get_text("A2"), *"Hello world!");
|
||||
}
|
||||
14
base/src/test/test_issue_155.rs
Normal file
14
base/src/test/test_issue_155.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn issue_155() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "1");
|
||||
model._set("A2", "2");
|
||||
model._set("B2", "=A$1:A2");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("B2"), "=A$1:A2".to_string());
|
||||
}
|
||||
32
base/src/test/test_row_column_styles.rs
Normal file
32
base/src/test/test_row_column_styles.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{constants::DEFAULT_COLUMN_WIDTH, test::util::new_empty_model};
|
||||
|
||||
#[test]
|
||||
fn test_model_set_cells_with_values_styles() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
let style_base = model.get_style_for_cell(0, 1, 1).unwrap();
|
||||
let mut style = style_base.clone();
|
||||
style.font.b = true;
|
||||
|
||||
model.set_column_style(0, 10, &style).unwrap();
|
||||
|
||||
assert!(model.get_style_for_cell(0, 21, 10).unwrap().font.b);
|
||||
|
||||
model.delete_column_style(0, 10).unwrap();
|
||||
|
||||
// There are no styles in the column
|
||||
assert!(model.workbook.worksheets[0].cols.is_empty());
|
||||
|
||||
// lets change the column width and check it does not affect the style
|
||||
model
|
||||
.set_column_width(0, 10, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
model.set_column_style(0, 10, &style).unwrap();
|
||||
|
||||
model.delete_column_style(0, 10).unwrap();
|
||||
|
||||
// There are no styles in the column
|
||||
assert!(model.workbook.worksheets[0].cols.len() == 1);
|
||||
}
|
||||
25
base/src/test/test_true_false.rs
Normal file
25
base/src/test/test_true_false.rs
Normal 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)");
|
||||
}
|
||||
@@ -3,6 +3,9 @@ mod test_autofill_columns;
|
||||
mod test_autofill_rows;
|
||||
mod test_border;
|
||||
mod test_clear_cells;
|
||||
mod test_column_style;
|
||||
mod test_defined_names;
|
||||
mod test_delete_row_column_formatting;
|
||||
mod test_diff_queue;
|
||||
mod test_evaluation;
|
||||
mod test_general;
|
||||
@@ -12,8 +15,11 @@ mod test_on_area_selection;
|
||||
mod test_on_expand_selected_range;
|
||||
mod test_on_paste_styles;
|
||||
mod test_paste_csv;
|
||||
mod test_recursive;
|
||||
mod test_rename_sheet;
|
||||
mod test_row_column;
|
||||
mod test_sheet_state;
|
||||
mod test_sheets_undo_redo;
|
||||
mod test_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
|
||||
@@ -9,7 +9,7 @@ fn add_undo_redo() {
|
||||
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
|
||||
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
|
||||
model
|
||||
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
||||
.set_columns_width(1, 5, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
||||
.unwrap();
|
||||
model.new_sheet().unwrap();
|
||||
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
|
||||
@@ -25,9 +25,6 @@ fn add_undo_redo() {
|
||||
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
|
||||
|
||||
model.delete_sheet(1).unwrap();
|
||||
|
||||
assert!(!model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -520,14 +520,19 @@ fn borders_top() {
|
||||
.unwrap();
|
||||
model.set_area_with_border(range, &border_area).unwrap();
|
||||
check_borders(&model);
|
||||
for row in 5..9 {
|
||||
for row in 4..9 {
|
||||
for column in 6..9 {
|
||||
let style = model.get_cell_style(0, row, column).unwrap();
|
||||
let border_item = BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let bottom = if row == 8 {
|
||||
let bottom = if row != 4 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
};
|
||||
let top = if row != 5 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -537,7 +542,7 @@ fn borders_top() {
|
||||
diagonal_down: false,
|
||||
left: None,
|
||||
right: None,
|
||||
top: Some(border_item.clone()),
|
||||
top,
|
||||
bottom,
|
||||
diagonal: None,
|
||||
};
|
||||
@@ -647,12 +652,12 @@ fn borders_right() {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let left = if column == 6 {
|
||||
let left = if column != 9 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
};
|
||||
let right = if column == 9 {
|
||||
let right = if column != 8 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -705,7 +710,7 @@ fn borders_bottom() {
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
// The top will also have a value for all but the first one
|
||||
let top = if row == 5 {
|
||||
let bottom = if row != 8 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -715,8 +720,8 @@ fn borders_bottom() {
|
||||
diagonal_down: false,
|
||||
left: None,
|
||||
right: None,
|
||||
top,
|
||||
bottom: Some(border_item.clone()),
|
||||
top: None,
|
||||
bottom,
|
||||
diagonal: None,
|
||||
};
|
||||
assert_eq!(style.border, expected_border);
|
||||
@@ -751,18 +756,13 @@ fn borders_left() {
|
||||
model.set_area_with_border(range, &border_area).unwrap();
|
||||
|
||||
for row in 5..9 {
|
||||
for column in 5..9 {
|
||||
for column in 6..9 {
|
||||
let style = model.get_cell_style(0, row, column).unwrap();
|
||||
let border_item = BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let left = if column == 5 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
};
|
||||
let right = if column == 8 {
|
||||
let left = if column != 6 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -771,13 +771,29 @@ fn borders_left() {
|
||||
diagonal_up: false,
|
||||
diagonal_down: false,
|
||||
left,
|
||||
right,
|
||||
right: None,
|
||||
top: None,
|
||||
bottom: None,
|
||||
diagonal: None,
|
||||
};
|
||||
assert_eq!(style.border, expected_border);
|
||||
}
|
||||
// Column 5 has a border to the right, of course:
|
||||
let style = model.get_cell_style(0, row, 5).unwrap();
|
||||
let border_item = BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let expected_border = Border {
|
||||
diagonal_up: false,
|
||||
diagonal_down: false,
|
||||
left: None,
|
||||
right: Some(border_item.clone()),
|
||||
top: None,
|
||||
bottom: None,
|
||||
diagonal: None,
|
||||
};
|
||||
assert_eq!(style.border, expected_border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,10 +1034,7 @@ fn border_top() {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#F2F2F2".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
model._get_cell_actual_border("C4").bottom,
|
||||
Some(border_item)
|
||||
);
|
||||
assert_eq!(model._get_cell_actual_border("C4").top, Some(border_item));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
|
||||
504
base/src/test/user_model/test_column_style.rs
Normal file
504
base/src/test/user_model/test_column_style.rs
Normal file
@@ -0,0 +1,504 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn column_width() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
assert!(!style.font.b);
|
||||
assert!(!style.font.u);
|
||||
assert!(!style.font.strike);
|
||||
assert_eq!(style.font.color, Some("#000000".to_owned()));
|
||||
|
||||
// Set the whole column style and check it works
|
||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(style.font.b);
|
||||
|
||||
// undo and check it works
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(!style.font.b);
|
||||
|
||||
// redo and check it works
|
||||
model.redo().unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(style.font.b);
|
||||
|
||||
// change the column width and check it does not affect the style
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(style.font.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_style() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let cell_g123 = Area {
|
||||
sheet: 0,
|
||||
row: 123,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
// Set G123 background to red
|
||||
model
|
||||
.update_range_style(&cell_g123, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Now set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Get the style of G123
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
// Check G123 has the column style now
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_column() {
|
||||
// We set the row style, then a column style
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Check G3 has the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
// undo twice. Color must be default
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_row() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let default_style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Check G3 has the row style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check G3 has the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check G3 has the default_style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, default_style.fill.bg_color);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_column_column() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_c_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let column_e_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_5_range = Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
|
||||
// Test E5 has the default style
|
||||
let style = model.get_cell_style(0, 5, 5).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width_column_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
DEFAULT_COLUMN_WIDTH * 2.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn height_row_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_rows_height(0, 10, 10, DEFAULT_ROW_HEIGHT * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let row_10_range = Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&row_10_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_row_height(0, 10).unwrap(),
|
||||
2.0 * DEFAULT_ROW_HEIGHT
|
||||
);
|
||||
model.undo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_row_height(0, 10).unwrap(),
|
||||
2.0 * DEFAULT_ROW_HEIGHT
|
||||
);
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_row_height(0, 10).unwrap(), DEFAULT_ROW_HEIGHT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_row_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g12 = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let row_12_range = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// Set G12 background to red
|
||||
model
|
||||
.update_range_style(&cell_g12, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&row_12_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
|
||||
model.undo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_column_style_then_cell() {
|
||||
// We check that if we set a cell style in a column that already has a style
|
||||
// the styles compound
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g12 = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
// Set G12 background to red
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&cell_g12, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_row_style_then_cell() {
|
||||
// We check that if we set a cell style in a column that already has a style
|
||||
// the styles compound
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g12 = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let row_12_range = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// Set G12 background to red
|
||||
model
|
||||
.update_range_style(&row_12_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&cell_g12, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_style_then_row_alignment() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
model
|
||||
.update_range_style(&row_3_range, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
// check the row alignment does not affect the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_style_then_width() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
// Check column width worked:
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
DEFAULT_COLUMN_WIDTH * 2.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_column_column() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_c_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let column_e_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_5_range = Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
// test E5 has the column style
|
||||
let style = model.get_cell_style(0, 5, 5).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
|
||||
}
|
||||
452
base/src/test/user_model/test_defined_names.rs
Normal file
452
base/src/test/user_model/test_defined_names.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
#![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())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_scope_to_first_sheet() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.new_sheet().unwrap();
|
||||
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
||||
model
|
||||
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
|
||||
.unwrap();
|
||||
model
|
||||
.new_defined_name("myName", None, "Sheet1!$A$1")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(1, 2, 1),
|
||||
Ok("Hello world!".to_string())
|
||||
);
|
||||
|
||||
model
|
||||
.update_defined_name("myName", None, "myName", Some(0), "Sheet1!$A$1")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(1, 2, 1),
|
||||
Ok("#NAME?".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_sheet() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.new_sheet().unwrap();
|
||||
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
||||
|
||||
model
|
||||
.new_defined_name("myName", None, "Sheet1!$A$1")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 1),
|
||||
Ok("Hello world!".to_string())
|
||||
);
|
||||
|
||||
model.rename_sheet(0, "AnotherName").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 1),
|
||||
Ok("Hello world!".to_string())
|
||||
);
|
||||
}
|
||||
243
base/src/test/user_model/test_delete_row_column_formatting.rs
Normal file
243
base/src/test/user_model/test_delete_row_column_formatting.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW},
|
||||
expressions::types::Area,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn delete_column_formatting() {
|
||||
// We are going to delete formatting in column G (7)
|
||||
// There are cells with their own styles
|
||||
// There are rows with their own styles
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g123 = Area {
|
||||
sheet: 0,
|
||||
row: 123,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// Set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Set G123 background to red
|
||||
model
|
||||
.update_range_style(&cell_g123, "fill.bg_color", "#FF5533")
|
||||
.unwrap();
|
||||
|
||||
// Set the style of the whole row
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Delete the column formatting
|
||||
model.range_clear_formatting(&column_g_range).unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
// Check the style of the whole row is still there
|
||||
let style = model.get_cell_style(0, 3, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
// Check the style of the whole column is now gone
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#FF5533".to_owned()));
|
||||
|
||||
// Check G3 is the row style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
// Check G40 is the column style
|
||||
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
// Check the style of the whole row is still there
|
||||
let style = model.get_cell_style(0, 3, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
// Check the style of the whole column is now gone
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_width() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
// Set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Delete the column formatting
|
||||
model.range_clear_formatting(&column_g_range).unwrap();
|
||||
// This does not change the column width
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
2.0 * DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
|
||||
model.undo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
2.0 * DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
2.0 * DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_row_style_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_123_range = Area {
|
||||
sheet: 0,
|
||||
row: 123,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let delete_range = Area {
|
||||
sheet: 0,
|
||||
row: 120,
|
||||
column: 5,
|
||||
width: 20,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
// Set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&row_123_range, "fill.bg_color", "#111222")
|
||||
.unwrap();
|
||||
|
||||
model.range_clear_formatting(&delete_range).unwrap();
|
||||
|
||||
// check G123 is empty
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
// uno clear formatting
|
||||
model.undo().unwrap();
|
||||
|
||||
// G123 has the row style
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#111222".to_owned()));
|
||||
|
||||
// undo twice
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
|
||||
// check G123 is empty
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_row_row_height_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.set_rows_height(0, 3, 3, DEFAULT_ROW_HEIGHT * 2.0)
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#111222")
|
||||
.unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// check G3 has the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
fn send_queue() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
let send_queue = model1.flush_send_queue();
|
||||
|
||||
@@ -34,7 +34,7 @@ fn apply_external_diffs_wrong_str() {
|
||||
fn queue_undo_redo() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
assert!(model1.undo().is_ok());
|
||||
assert!(model1.redo().is_ok());
|
||||
@@ -57,8 +57,8 @@ fn queue_undo_redo_multiple() {
|
||||
// do a bunch of things
|
||||
model1.set_frozen_columns_count(0, 5).unwrap();
|
||||
model1.set_frozen_rows_count(0, 6).unwrap();
|
||||
model1.set_column_width(0, 7, 300.0).unwrap();
|
||||
model1.set_row_height(0, 23, 123.0).unwrap();
|
||||
model1.set_columns_width(0, 7, 7, 300.0).unwrap();
|
||||
model1.set_rows_height(0, 23, 23, 123.0).unwrap();
|
||||
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
|
||||
|
||||
for row in 1..5 {
|
||||
|
||||
@@ -59,7 +59,7 @@ fn insert_remove_rows() {
|
||||
// Insert some data in row 5 (and change the style)
|
||||
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
|
||||
// Change the height of the column
|
||||
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
|
||||
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
|
||||
|
||||
// remove the row
|
||||
assert!(model.delete_row(0, 5).is_ok());
|
||||
@@ -91,12 +91,11 @@ 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());
|
||||
// Change the width of the column
|
||||
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
|
||||
assert!(model.set_columns_width(0, 5, 5, 3.0 * column_width).is_ok());
|
||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
||||
|
||||
// remove the column
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::{
|
||||
constants::{
|
||||
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH,
|
||||
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH,
|
||||
LAST_COLUMN,
|
||||
},
|
||||
test::util::new_empty_model,
|
||||
@@ -87,7 +87,7 @@ fn last_colum() {
|
||||
fn page_down() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
let window_height = DEFAULT_WINDOW_HEIGH as f64;
|
||||
let window_height = DEFAULT_WINDOW_HEIGHT as f64;
|
||||
let row_height = DEFAULT_ROW_HEIGHT;
|
||||
let row_count = f64::floor(window_height / row_height) as i32;
|
||||
model.on_page_down().unwrap();
|
||||
|
||||
@@ -99,7 +99,7 @@ fn cut_paste() {
|
||||
|
||||
// paste in cell D4 (4, 4)
|
||||
model
|
||||
.paste_from_clipboard((1, 1, 2, 2), ©.data, true)
|
||||
.paste_from_clipboard(0, (1, 1, 2, 2), ©.data, true)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
|
||||
@@ -119,6 +119,26 @@ fn cut_paste() {
|
||||
assert_eq!(model.get_cell_content(0, 2, 2), Ok("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_paste_different_sheet() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
|
||||
model.set_selected_range(1, 1, 1, 1).unwrap();
|
||||
let copy = model.copy_to_clipboard().unwrap();
|
||||
model.new_sheet().unwrap();
|
||||
model.set_selected_sheet(1).unwrap();
|
||||
model.set_selected_cell(4, 4).unwrap();
|
||||
|
||||
// paste in cell D4 (4, 4) of Sheet2
|
||||
model
|
||||
.paste_from_clipboard(0, (1, 1, 1, 1), ©.data, true)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(1, 4, 4), Ok("42".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_paste_internal() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
@@ -152,7 +172,7 @@ fn copy_paste_internal() {
|
||||
|
||||
// paste in cell D4 (4, 4)
|
||||
model
|
||||
.paste_from_clipboard((1, 1, 2, 2), ©.data, false)
|
||||
.paste_from_clipboard(0, (1, 1, 2, 2), ©.data, false)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
|
||||
|
||||
42
base/src/test/user_model/test_recursive.rs
Normal file
42
base/src/test/user_model/test_recursive.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
constants::LAST_ROW, expressions::types::Area, test::util::new_empty_model, UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn two_columns() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Set style in column C (column 3)
|
||||
let column_c_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_c_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
model.set_user_input(0, 5, 3, "2").unwrap();
|
||||
|
||||
// Set Style in column G (column 7)
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
model.set_user_input(0, 5, 6, "42").unwrap();
|
||||
// Set formula in G5: =F5*C5
|
||||
model.set_user_input(0, 5, 7, "=F5*C5").unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 7).unwrap(), "84");
|
||||
}
|
||||
@@ -59,7 +59,7 @@ fn simple_delete_column() {
|
||||
model.set_user_input(0, 1, 5, "3").unwrap();
|
||||
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
|
||||
model
|
||||
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_column(0, 5).unwrap();
|
||||
@@ -116,7 +116,7 @@ fn simple_delete_row() {
|
||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||
|
||||
model
|
||||
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_row(0, 15).unwrap();
|
||||
@@ -170,5 +170,44 @@ fn row_heigh_increases_automatically() {
|
||||
model
|
||||
.set_user_input(0, 1, 1, "My home in Canada had horses\nAnd monkeys!")
|
||||
.unwrap();
|
||||
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT));
|
||||
assert_eq!(model.get_row_height(0, 1), Ok(40.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_row_evaluates() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
|
||||
|
||||
assert!(model.insert_row(0, 1).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||
|
||||
model.delete_row(0, 1).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_column_evaluates() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
|
||||
|
||||
assert!(model.insert_column(0, 1).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||
|
||||
model.delete_column(0, 1).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
|
||||
}
|
||||
|
||||
57
base/src/test/user_model/test_sheet_state.rs
Normal file
57
base/src/test/user_model/test_sheet_state.rs
Normal 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());
|
||||
}
|
||||
52
base/src/test/user_model/test_sheets_undo_redo.rs
Normal file
52
base/src/test/user_model/test_sheets_undo_redo.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_undo_redo() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
|
||||
model.new_sheet().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
{
|
||||
let props = model.get_worksheets_properties();
|
||||
assert_eq!(props.len(), 1);
|
||||
let view = model.get_selected_view();
|
||||
assert_eq!(view.sheet, 0);
|
||||
}
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
{
|
||||
let props = model.get_worksheets_properties();
|
||||
assert_eq!(props.len(), 2);
|
||||
let view = model.get_selected_view();
|
||||
|
||||
assert_eq!(view.sheet, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_undo() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
|
||||
model.new_sheet().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
model.set_user_input(1, 1, 1, "42").unwrap();
|
||||
model.set_user_input(1, 1, 2, "=A1*2").unwrap();
|
||||
model.delete_sheet(1).unwrap();
|
||||
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
}
|
||||
@@ -436,3 +436,47 @@ fn false_removes_value() {
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_clear_formatting() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// bold
|
||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
assert_eq!(
|
||||
style.alignment.unwrap().horizontal,
|
||||
HorizontalAlignment::CenterContinuous
|
||||
);
|
||||
|
||||
model.range_clear_all(&range).unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
assert_eq!(style.alignment, None);
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
assert_eq!(
|
||||
style.alignment.unwrap().horizontal,
|
||||
HorizontalAlignment::CenterContinuous
|
||||
);
|
||||
model.redo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
assert_eq!(style.alignment, None);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{test::util::new_empty_model, UserModel};
|
||||
fn basic() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
|
||||
let model_bytes = model1.to_bytes();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
constants::{DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH},
|
||||
constants::{DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ fn basic_test() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
let window_height = model.get_window_height().unwrap();
|
||||
assert_eq!(window_height, DEFAULT_WINDOW_HEIGH);
|
||||
assert_eq!(window_height, DEFAULT_WINDOW_HEIGHT);
|
||||
|
||||
let window_width = model.get_window_width().unwrap();
|
||||
assert_eq!(window_width, DEFAULT_WINDOW_WIDTH);
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct WorkbookView {
|
||||
pub sheet: u32,
|
||||
/// The current width of the window
|
||||
pub window_width: i64,
|
||||
/// The current heigh of the window
|
||||
/// The current height of the window
|
||||
pub window_height: i64,
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ pub struct DefinedName {
|
||||
}
|
||||
|
||||
/// * state:
|
||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||
/// hidden, veryHidden, visible
|
||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||
/// hidden, veryHidden, visible
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum SheetState {
|
||||
Visible,
|
||||
@@ -110,7 +110,7 @@ pub struct Worksheet {
|
||||
pub sheet_id: u32,
|
||||
pub state: SheetState,
|
||||
pub color: Option<String>,
|
||||
pub merge_cells: Vec<String>,
|
||||
pub merged_cells: HashMap<(i32, i32), (i32, i32)>,
|
||||
pub comments: Vec<Comment>,
|
||||
pub frozen_rows: i32,
|
||||
pub frozen_columns: i32,
|
||||
@@ -217,7 +217,10 @@ pub enum Cell {
|
||||
// Error Message: "Not implemented function"
|
||||
m: String,
|
||||
},
|
||||
// TODO: Array formulas
|
||||
Merged {
|
||||
r: i32,
|
||||
c: i32,
|
||||
}, // TODO: Array formulas
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
@@ -323,6 +326,19 @@ pub struct Style {
|
||||
pub quote_prefix: bool,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Style {
|
||||
alignment: None,
|
||||
num_fmt: "general".to_string(),
|
||||
fill: Fill::default(),
|
||||
font: Font::default(),
|
||||
border: Border::default(),
|
||||
quote_prefix: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NumFmt {
|
||||
pub num_fmt_id: i32,
|
||||
@@ -394,7 +410,7 @@ impl Default for Font {
|
||||
u: false,
|
||||
b: false,
|
||||
i: false,
|
||||
sz: 11,
|
||||
sz: 13,
|
||||
color: Some("#000000".to_string()),
|
||||
name: "Calibri".to_string(),
|
||||
family: 2,
|
||||
|
||||
@@ -50,8 +50,9 @@ impl Units {
|
||||
fn get_units_from_format_string(num_fmt: &str) -> Option<Units> {
|
||||
let mut parser = Parser::new(num_fmt);
|
||||
parser.parse();
|
||||
let parts = parser.parts.first()?;
|
||||
// We only care about the first part (positive number)
|
||||
match &parser.parts[0] {
|
||||
match parts {
|
||||
ParsePart::Number(part) => {
|
||||
if part.percent > 0 {
|
||||
Some(Units::Percentage {
|
||||
@@ -293,9 +294,12 @@ 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,
|
||||
Node::ImplicitIntersection { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +313,7 @@ impl Model {
|
||||
Function::Sum => self.units_fn_sum_like(args, cell),
|
||||
Function::Average => self.units_fn_sum_like(args, cell),
|
||||
Function::Pmt => self.units_fn_currency(args, cell),
|
||||
Function::Fv => self.units_fn_currency(args, cell),
|
||||
Function::Nper => self.units_fn_currency(args, cell),
|
||||
Function::Npv => self.units_fn_currency(args, cell),
|
||||
Function::Irr => self.units_fn_percentage(args, cell),
|
||||
|
||||
507
base/src/user_model/border.rs
Normal file
507
base/src/user_model/border.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
expressions::types::Area,
|
||||
};
|
||||
|
||||
use super::{
|
||||
border_utils::is_max_border, common::BorderType, history::Diff, BorderArea, UserModel,
|
||||
};
|
||||
|
||||
impl UserModel {
|
||||
fn update_single_cell_border(
|
||||
&mut self,
|
||||
border_area: &BorderArea,
|
||||
cell: (u32, i32, i32),
|
||||
range: (i32, i32, i32, i32),
|
||||
diff_list: &mut Vec<Diff>,
|
||||
) -> Result<(), String> {
|
||||
let (sheet, row, column) = cell;
|
||||
let (first_row, first_column, last_row, last_column) = range;
|
||||
|
||||
let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||||
let mut new_value = match &old_value {
|
||||
Some(value) => value.clone(),
|
||||
None => Default::default(),
|
||||
};
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Right => {
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Bottom => {
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Left => {
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterH => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::None => {
|
||||
new_value.border.top = None;
|
||||
new_value.border.right = None;
|
||||
new_value.border.bottom = None;
|
||||
new_value.border.left = None;
|
||||
}
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &new_value)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(new_value),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_rows_with_border(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
first_row: i32,
|
||||
last_row: i32,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
for row in first_row..=last_row {
|
||||
let old_value = self.model.get_row_style(sheet, row)?;
|
||||
let mut new_value = match &old_value {
|
||||
Some(value) => value.clone(),
|
||||
None => Default::default(),
|
||||
};
|
||||
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Right => {
|
||||
// noop
|
||||
}
|
||||
BorderType::Bottom => {
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Left => {
|
||||
// noop
|
||||
}
|
||||
BorderType::CenterH => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::None => {
|
||||
new_value.border.top = None;
|
||||
new_value.border.right = None;
|
||||
new_value.border.bottom = None;
|
||||
new_value.border.left = None;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to go throw each non-empty cell in the row
|
||||
let columns: Vec<i32> = self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.sheet_data
|
||||
.get(&row)
|
||||
.map(|row_data| row_data.keys().copied().collect())
|
||||
.unwrap_or_default();
|
||||
for column in columns {
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(first_row, 1, last_row, LAST_COLUMN),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
|
||||
self.model.set_row_style(sheet, row, &new_value)?;
|
||||
diff_list.push(Diff::SetRowStyle {
|
||||
sheet,
|
||||
row,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(new_value),
|
||||
});
|
||||
}
|
||||
// TODO: We need to check the rows above and below. also any non empty cell in the rows above and below.
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_columns_with_border(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
first_column: i32,
|
||||
last_column: i32,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
// We need all the rows in the column to update the style
|
||||
// NB: This is too much, this is all the rows that have values
|
||||
let data_rows: Vec<i32> = self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.sheet_data
|
||||
.keys()
|
||||
.copied()
|
||||
.collect();
|
||||
let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
|
||||
for column in first_column..=last_column {
|
||||
let old_value = self.model.get_column_style(sheet, column)?;
|
||||
let mut new_value = match &old_value {
|
||||
Some(value) => value.clone(),
|
||||
None => Default::default(),
|
||||
};
|
||||
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => {
|
||||
// noop
|
||||
}
|
||||
BorderType::Right => {
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Bottom => {
|
||||
// noop
|
||||
}
|
||||
BorderType::Left => {
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterH => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::None => {
|
||||
new_value.border.top = None;
|
||||
new_value.border.right = None;
|
||||
new_value.border.bottom = None;
|
||||
new_value.border.left = None;
|
||||
}
|
||||
}
|
||||
// We need to go through each non empty cell in the column
|
||||
for &row in &data_rows {
|
||||
if let Some(data_row) = self.model.workbook.worksheet(sheet)?.sheet_data.get(&row) {
|
||||
if data_row.get(&column).is_some() {
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(1, first_column, LAST_ROW, last_column),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We also need to overwrite those that have a row style
|
||||
for row_s in styled_rows.iter() {
|
||||
let row = row_s.r;
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(1, first_column, LAST_ROW, last_column),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
|
||||
self.model.set_column_style(sheet, column, &new_value)?;
|
||||
diff_list.push(Diff::SetColumnStyle {
|
||||
sheet,
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(new_value),
|
||||
});
|
||||
}
|
||||
// We need to check the borders of the column to the left and the column to the right
|
||||
// We also need to check every non-empty cell in the columns to the left and right
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the border in an area of cells.
|
||||
/// When setting the border we need to check if the adjacent cells have a "heavier" border
|
||||
/// If that is the case we need to change it
|
||||
pub fn set_area_with_border(
|
||||
&mut self,
|
||||
range: &Area,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let sheet = range.sheet;
|
||||
let first_row = range.row;
|
||||
let first_column = range.column;
|
||||
let last_row = first_row + range.height - 1;
|
||||
let last_column = first_column + range.width - 1;
|
||||
if first_row == 1 && last_row == LAST_ROW {
|
||||
// full columns
|
||||
self.set_columns_with_border(sheet, first_column, last_column, border_area)?;
|
||||
return Ok(());
|
||||
}
|
||||
if first_column == 1 && last_column == LAST_COLUMN {
|
||||
// full rows
|
||||
self.set_rows_with_border(sheet, first_row, last_row, border_area)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut diff_list = Vec::new();
|
||||
for row in first_row..=last_row {
|
||||
for column in first_column..=last_column {
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(first_row, first_column, last_row, last_column),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// bottom of the cells above the first
|
||||
if first_row > 1
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Top,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let row = first_row - 1;
|
||||
for column in first_column..=last_column {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
if is_max_border(Some(&border_area.item), old_value.border.bottom.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.bottom = None;
|
||||
} else {
|
||||
style.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cells to the right
|
||||
if last_column < LAST_COLUMN
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Right,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let column = last_column + 1;
|
||||
for row in first_row..=last_row {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
// If the border in the adjacent cell is "heavier" we change it
|
||||
if is_max_border(Some(&border_area.item), old_value.border.left.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.left = None;
|
||||
} else {
|
||||
style.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cells bellow
|
||||
if last_row < LAST_ROW
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Bottom,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let row = last_row + 1;
|
||||
for column in first_column..=last_column {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
if is_max_border(Some(&border_area.item), old_value.border.top.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.top = None;
|
||||
} else {
|
||||
style.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cells to the left
|
||||
if first_column > 1
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Left,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let column = first_column - 1;
|
||||
for row in first_row..=last_row {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
if is_max_border(Some(&border_area.item), old_value.border.right.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.right = None;
|
||||
} else {
|
||||
style.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user