Compare commits

..

1 Commits

Author SHA1 Message Date
varuntumbe
e7858f7aa9 adding merge cell logic processing
formatting commit

addressing testcase failures

adding one more scenario to case

adding one more scenario to case

Adding update and unmerge functions for merge cell handling

adding one more case to testcase

adding testcases to base code

adding testcase for import/export

adding documentation to some of the PUB function

fixing warnings and test warnings

adding merge cell part cell update restriction to public sytle set fns

addressing reviwers comment : Changed Mergedcell structure and its side effercts

reverting it back to non pub.

renaming update_merge_cells to just merge_cells in model

renaming *unmerge_merged_cell* to *unmerge_cells*

addressing other reviewer's comment + cosmetica naming adjustments

cosmetic changes
2024-11-29 23:13:10 +01:00
370 changed files with 8024 additions and 30350 deletions

View File

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

View File

@@ -1,143 +0,0 @@
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@v3
with:
name: wheels
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@v3
with:
name: wheels
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@v3
with:
name: wheels
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@v3
with:
name: wheels
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@v3
with:
name: wheels
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 *
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@v3
with:
name: wheels
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 *
working-directory: bindings/python

View File

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

3
.gitignore vendored
View File

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

View File

@@ -7,21 +7,11 @@
- New function UNICODE ([#128](https://github.com/ironcalc/IronCalc/pull/128)) - New function UNICODE ([#128](https://github.com/ironcalc/IronCalc/pull/128))
- New document server (Thanks Dani!) - New document server (Thanks Dani!)
- New function FORMULATEXT - 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
- Fixed several issues with pasting content - Fixed several issues with pasting content
- Fixed several issues with borders - 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) ## [0.2.0] - 2024-11-06 (The HN release)

145
Cargo.lock generated
View File

@@ -85,12 +85,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@@ -216,15 +210,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@@ -286,16 +271,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ctor"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -414,7 +389,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.3.0" version = "0.2.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -430,7 +405,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.3.0" version = "0.2.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -446,17 +421,6 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "ironcalc_nodejs"
version = "0.3.1"
dependencies = [
"ironcalc",
"napi",
"napi-build",
"napi-derive",
"serde",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@@ -496,16 +460,6 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.21" version = "0.4.21"
@@ -536,65 +490,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "napi"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
"napi-derive",
"napi-sys",
"once_cell",
"serde",
"serde_json",
]
[[package]]
name = "napi-build"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
dependencies = [
"libloading",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -721,9 +616,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.23.4" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"indoc", "indoc",
@@ -739,9 +634,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.23.4" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@@ -749,9 +644,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.23.4" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@@ -759,9 +654,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.23.4" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -771,9 +666,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.23.4" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -784,7 +679,7 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.3.0" version = "0.1.2"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"pyo3", "pyo3",
@@ -884,12 +779,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "semver"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.197"
@@ -1034,12 +923,6 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unindent" name = "unindent"
version = "0.2.3" version = "0.2.3"
@@ -1070,7 +953,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.3.2" version = "0.1.3"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
use ironcalc_base::{Model, types::CellType}; use ironcalc_base::{types::CellType, Model};
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?; let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;

View File

@@ -1,4 +1,4 @@
use ironcalc_base::{Model, cell::CellValue}; use ironcalc_base::{cell::CellValue, Model};
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut model = Model::new_empty("hello-world", "en", "UTC")?; let mut model = Model::new_empty("hello-world", "en", "UTC")?;

View File

@@ -136,33 +136,6 @@ impl Model {
}), }),
); );
// In the list of columns:
// * Keep all the columns to the left
// * Displace all the columns to the right
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_columns = Vec::new();
for col in worksheet.cols.iter_mut() {
// range under study
let min = col.min;
let max = col.max;
if column > max {
// If the range under study is to our left, this is a noop
} else if column <= min {
// If the range under study is to our right, we displace it
col.min = min + column_count;
col.max = max + column_count;
} else {
// If the range under study is in the middle we augment it
col.max = max + column_count;
}
new_columns.push(col.clone());
}
// TODO: If in a row the cell to the right and left have the same style we should copy it
worksheet.cols = new_columns;
Ok(()) Ok(())
} }

View File

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

View File

@@ -176,3 +176,18 @@ impl Cell {
} }
} }
} }
// Implementing methods for MergedCells struct
impl MergedCells {
pub fn is_cell_part_of_merged_cells(&self, row: i32, col: i32) -> bool {
// This is merge Mother cell so do not include this cell as part of Merged Cells
if row == self.0 && col == self.1 {
return false;
}
let result: bool = (row >= self.0 && row <= self.2) && (col >= self.1 && col <= self.3);
result
}
}

View File

@@ -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 DEFAULT_ROW_HEIGHT: f64 = 28.0;
pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0; pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0;
pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0; pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0;
pub(crate) const DEFAULT_WINDOW_HEIGHT: i64 = 600; pub(crate) const DEFAULT_WINDOW_HEIGH: i64 = 600;
pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800; pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
pub(crate) const LAST_COLUMN: i32 = 16_384; pub(crate) const LAST_COLUMN: i32 = 16_384;
@@ -16,10 +16,3 @@ pub(crate) const LAST_ROW: i32 = 1_048_576;
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2 // NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug // The 2 days offset is because of Excel 1900 bug
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594; pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
// We do not support dates before 1899-12-31.
pub(crate) const MINIMUM_DATE_SERIAL_NUMBER: i32 = 1;
// Excel can handle dates until the year 9999-12-31
// 2958465 is the number of days from 1900-01-01 to 9999-12-31
pub(crate) const MAXIMUM_DATE_SERIAL_NUMBER: i32 = 2_958_465;

View File

@@ -2,7 +2,7 @@ use crate::{
expressions::{ expressions::{
parser::{ parser::{
move_formula::ref_is_in_area, move_formula::ref_is_in_area,
stringify::{DisplaceData, to_string, to_string_displaced}, stringify::{to_string, to_string_displaced, DisplaceData},
walk::forward_references, walk::forward_references,
}, },
types::{Area, CellReferenceIndex, CellReferenceRC}, types::{Area, CellReferenceIndex, CellReferenceRC},

View File

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

View File

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

View File

@@ -49,7 +49,15 @@ pub mod stringify;
pub mod walk; pub mod walk;
#[cfg(test)] #[cfg(test)]
mod tests; mod test;
#[cfg(test)]
mod test_ranges;
#[cfg(test)]
mod test_move_formula;
#[cfg(test)]
mod test_tables;
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> { pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
let mut lexer = lexer::Lexer::new( let mut lexer = lexer::Lexer::new(
@@ -164,9 +172,7 @@ pub enum Node {
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Node>), ArrayKind(Vec<Node>),
DefinedNameKind((String, Option<u32>)), VariableKind(String),
TableNameKind(String),
WrongVariableKind(String),
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -189,17 +195,12 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, context: Option<CellReferenceRC>,
context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
impl Parser { impl Parser {
pub fn new( pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser {
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
"", "",
lexer::LexerMode::A1, lexer::LexerMode::A1,
@@ -208,16 +209,10 @@ impl Parser {
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
get_language("en").expect(""), get_language("en").expect(""),
); );
let context = CellReferenceRC {
sheet: worksheets.first().map_or("", |v| v).to_string(),
column: 1,
row: 1,
};
Parser { Parser {
lexer, lexer,
worksheets, worksheets,
defined_names, context: None,
context,
tables, tables,
} }
} }
@@ -225,18 +220,13 @@ impl Parser {
self.lexer.set_lexer_mode(mode) self.lexer.set_lexer_mode(mode)
} }
pub fn set_worksheets_and_names( pub fn set_worksheets(&mut self, worksheets: Vec<String>) {
&mut self,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names;
} }
pub fn parse(&mut self, formula: &str, context: &CellReferenceRC) -> Node { pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
self.lexer.set_formula(formula); self.lexer.set_formula(formula);
self.context = context.clone(); self.context.clone_from(context);
self.parse_expr() self.parse_expr()
} }
@@ -250,24 +240,6 @@ impl Parser {
None None
} }
// Returns:
// * None: If there is no defined name by that name
// * Some(Some(index)): If there is a defined name local to that sheet
// * Some(None): If there is a global defined name
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> {
for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope);
}
}
for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some(None);
}
}
None
}
fn parse_expr(&mut self) -> Node { fn parse_expr(&mut self) -> Node {
let mut t = self.parse_concat(); let mut t = self.parse_concat();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
@@ -482,7 +454,16 @@ impl Parser {
absolute_column, absolute_column,
absolute_row, absolute_row,
} => { } => {
let context = &self.context; 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 sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -517,7 +498,16 @@ impl Parser {
} }
} }
TokenType::Range { sheet, left, right } => { TokenType::Range { sheet, left, right } => {
let context = &self.context; 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 sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -532,6 +522,20 @@ impl Parser {
let mut absolute_row1 = left.absolute_row; let mut absolute_row1 = left.absolute_row;
let mut absolute_row2 = right.absolute_row; let mut absolute_row2 = right.absolute_row;
if self.lexer.is_a1_mode() {
if !left.absolute_row {
row1 -= context.row
};
if !left.absolute_column {
column1 -= context.column
};
if !right.absolute_row {
row2 -= context.row
};
if !right.absolute_column {
column2 -= context.column
};
}
if row1 > row2 { if row1 > row2 {
(row2, row1) = (row1, row2); (row2, row1) = (row1, row2);
(absolute_row2, absolute_row1) = (absolute_row1, absolute_row2); (absolute_row2, absolute_row1) = (absolute_row1, absolute_row2);
@@ -540,22 +544,6 @@ impl Parser {
(column2, column1) = (column1, column2); (column2, column1) = (column1, column2);
(absolute_column2, absolute_column1) = (absolute_column1, absolute_column2); (absolute_column2, absolute_column1) = (absolute_column1, absolute_column2);
} }
if self.lexer.is_a1_mode() {
if !absolute_row1 {
row1 -= context.row
};
if !absolute_column1 {
column1 -= context.column
};
if !absolute_row2 {
row2 -= context.row
};
if !absolute_column2 {
column2 -= context.column
};
}
match sheet_index { match sheet_index {
Some(index) => Node::RangeKind { Some(index) => Node::RangeKind {
sheet_name: sheet, sheet_name: sheet,
@@ -603,33 +591,11 @@ impl Parser {
kind: function_kind, kind: function_kind,
args, args,
}; };
} } else {
return Node::InvalidFunctionKind { name, args }; return Node::InvalidFunctionKind { name, args };
}
let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
// Could be a defined name or a table
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
if table_name.to_lowercase() == name_lower {
return Node::TableNameKind(name);
} }
} }
Node::WrongVariableKind(name) Node::VariableKind(name)
} }
TokenType::Error(kind) => Node::ErrorKind(kind), TokenType::Error(kind) => Node::ErrorKind(kind),
TokenType::Illegal(error) => Node::ParseErrorKind { TokenType::Illegal(error) => Node::ParseErrorKind {
@@ -642,38 +608,7 @@ impl Parser {
position: 0, position: 0,
message: "Unexpected end of input.".to_string(), message: "Unexpected end of input.".to_string(),
}, },
TokenType::Boolean(value) => { TokenType::Boolean(value) => Node::BooleanKind(value),
// Could be a function call "TRUE()"
let next_token = self.lexer.peek_token();
if next_token == TokenType::LeftParenthesis {
self.lexer.advance_token();
// We parse all the arguments, although technically this is moot
// But is has the upside of transforming `=TRUE( 4 )` into `=TRUE(4)`
let args = match self.parse_function_args() {
Ok(s) => s,
Err(e) => return e,
};
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: err.position,
message: err.message,
};
}
if value {
return Node::FunctionKind {
kind: Function::True,
args,
};
} else {
return Node::FunctionKind {
kind: Function::False,
args,
};
}
}
Node::BooleanKind(value)
}
TokenType::Compare(_) => { TokenType::Compare(_) => {
// A primary Node cannot start with an operator // A primary Node cannot start with an operator
Node::ParseErrorKind { Node::ParseErrorKind {
@@ -732,177 +667,187 @@ impl Parser {
// We will try to convert to a normal reference // We will try to convert to a normal reference
// table_name[column_name] => cell1:cell2 // table_name[column_name] => cell1:cell2
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2 // table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
let context = &self.context; if let Some(context) = &self.context {
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) { let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i, Some(i) => i,
None => { None => {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: "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())
};
// 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;
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) => {
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;
}
}
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, // table-name => table
sheet_index: table_sheet_index, let table = match self.tables.get(&table_name) {
absolute_row1: true, Some(t) => t,
absolute_column1: true, None => {
row1: row_start, let message = format!(
column1: column_index, "Table not found: '{table_name}' at '{}!{}{}'",
absolute_row2: true, context.sheet,
absolute_column2: true, number_to_column(context.column)
row2: row_end, .unwrap_or(format!("{}", context.column)),
column2: column_index, context.row
);
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message,
};
} }
} };
Some(TableReference::RangeReference((left, right))) => { let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
let left_column_index = match get_table_column_by_name(&left, table) { Some(i) => i,
Some(f) => f + column_start, None => {
None => { return Node::ParseErrorKind {
return Node::ParseErrorKind { formula: self.lexer.get_formula(),
formula: self.lexer.get_formula(), position: 0,
position: self.lexer.get_position() as usize, message: "sheet not found".to_string(),
message: format!( };
"Expecting column: {left} in table {table_name}" }
), };
};
}
};
let right_column_index = match get_table_column_by_name(&right, table) { let sheet_name = if table_sheet_index == context_sheet_index {
Some(f) => f + column_start, None
None => { } else {
return Node::ParseErrorKind { Some(table.sheet_name.clone())
formula: self.lexer.get_formula(), };
position: self.lexer.get_position() as usize,
message: format!( // context must be with tables.reference
"Expecting column: {right} in table {table_name}" #[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;
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);
} }
}; }
Node::RangeKind { Some(token::TableSpecifier::Headers) => {
sheet_name, row_end = row_start;
sheet_index: table_sheet_index, }
absolute_row1: true, Some(token::TableSpecifier::Data) => {
absolute_column1: true, row_start += header_row_count;
row1: row_start, }
column1: left_column_index, Some(token::TableSpecifier::All) => {
absolute_row2: true, if totals_row_count != 0 {
absolute_column2: true, row_end += 1;
row2: row_end, }
column2: right_column_index, }
None => {
// skip the headers
row_start += header_row_count;
} }
} }
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,
};
}
}
}
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Structured references not supported in R1C1 mode".to_string(),
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use super::{ use super::{
stringify::{stringify_reference, DisplaceData},
Node, Reference, Node, Reference,
stringify::{DisplaceData, stringify_reference},
}; };
use crate::{ use crate::{
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW},
@@ -375,9 +375,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
format!("{{{}}}", arguments) format!("{{{}}}", arguments)
} }
DefinedNameKind((name, _)) => name.to_string(), VariableKind(value) => value.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
to_string_moved(left, move_context), to_string_moved(left, move_context),

View File

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

View File

@@ -3,11 +3,17 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::lexer::LexerMode; use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::{ use crate::expressions::parser::stringify::DisplaceData;
DisplaceData, to_rc_format, to_string, to_string_displaced,
use super::super::types::CellReferenceRC;
use super::Parser;
use super::{
super::parser::{
stringify::{to_rc_format, to_string},
Node,
},
stringify::to_string_displaced,
}; };
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> { struct Formula<'a> {
initial: &'a str, initial: &'a str,
@@ -17,7 +23,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_reference() { fn test_parser_reference() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -25,14 +31,14 @@ fn test_parser_reference() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("A2", &cell_reference); let t = parser.parse("A2", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[1]C[0]"); assert_eq!(to_rc_format(&t), "R[1]C[0]");
} }
#[test] #[test]
fn test_parser_absolute_column() { fn test_parser_absolute_column() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -40,14 +46,14 @@ fn test_parser_absolute_column() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A1", &cell_reference); let t = parser.parse("$A1", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[0]C1"); assert_eq!(to_rc_format(&t), "R[0]C1");
} }
#[test] #[test]
fn test_parser_absolute_row_col() { fn test_parser_absolute_row_col() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -55,14 +61,14 @@ fn test_parser_absolute_row_col() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$C$5", &cell_reference); let t = parser.parse("$C$5", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R5C3"); assert_eq!(to_rc_format(&t), "R5C3");
} }
#[test] #[test]
fn test_parser_absolute_row_col_1() { fn test_parser_absolute_row_col_1() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -70,14 +76,14 @@ fn test_parser_absolute_row_col_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A$1", &cell_reference); let t = parser.parse("$A$1", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R1C1"); assert_eq!(to_rc_format(&t), "R1C1");
} }
#[test] #[test]
fn test_parser_simple_formula() { fn test_parser_simple_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -86,14 +92,14 @@ fn test_parser_simple_formula() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+Sheet2!D4", &cell_reference); let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_boolean() { fn test_parser_boolean() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -102,14 +108,14 @@ fn test_parser_boolean() {
column: 1, column: 1,
}; };
let t = parser.parse("true", &cell_reference); let t = parser.parse("true", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "TRUE"); assert_eq!(to_rc_format(&t), "TRUE");
} }
#[test] #[test]
fn test_parser_bad_formula() { fn test_parser_bad_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -117,7 +123,7 @@ fn test_parser_bad_formula() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("#Value", &cell_reference); let t = parser.parse("#Value", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -138,7 +144,7 @@ fn test_parser_bad_formula() {
#[test] #[test]
fn test_parser_bad_formula_1() { fn test_parser_bad_formula_1() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -146,7 +152,7 @@ fn test_parser_bad_formula_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("<5", &cell_reference); let t = parser.parse("<5", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -167,7 +173,7 @@ fn test_parser_bad_formula_1() {
#[test] #[test]
fn test_parser_bad_formula_2() { fn test_parser_bad_formula_2() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -175,7 +181,7 @@ fn test_parser_bad_formula_2() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("*5", &cell_reference); let t = parser.parse("*5", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -196,7 +202,7 @@ fn test_parser_bad_formula_2() {
#[test] #[test]
fn test_parser_bad_formula_3() { fn test_parser_bad_formula_3() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -204,7 +210,7 @@ fn test_parser_bad_formula_3() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("SUM(#VALVE!)", &cell_reference); let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -225,7 +231,7 @@ fn test_parser_bad_formula_3() {
#[test] #[test]
fn test_parser_formulas() { fn test_parser_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -259,11 +265,11 @@ fn test_parser_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&CellReferenceRC { &Some(CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}, }),
); );
assert_eq!(to_rc_format(&t), formula.expected); assert_eq!(to_rc_format(&t), formula.expected);
assert_eq!(to_string(&t, &cell_reference), formula.initial); assert_eq!(to_string(&t, &cell_reference), formula.initial);
@@ -273,7 +279,7 @@ fn test_parser_formulas() {
#[test] #[test]
fn test_parser_r1c1_formulas() { fn test_parser_r1c1_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
parser.set_lexer_mode(LexerMode::R1C1); parser.set_lexer_mode(LexerMode::R1C1);
let formulas = vec![ let formulas = vec![
@@ -324,11 +330,11 @@ fn test_parser_r1c1_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&CellReferenceRC { &Some(CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}, }),
); );
assert_eq!(to_string(&t, &cell_reference), formula.expected); assert_eq!(to_string(&t, &cell_reference), formula.expected);
assert_eq!(to_rc_format(&t), formula.initial); assert_eq!(to_rc_format(&t), formula.initial);
@@ -338,7 +344,7 @@ fn test_parser_r1c1_formulas() {
#[test] #[test]
fn test_parser_quotes() { fn test_parser_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -347,14 +353,14 @@ fn test_parser_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second Sheet'!D4", &cell_reference); let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_escape_quotes() { fn test_parser_escape_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -363,14 +369,14 @@ fn test_parser_escape_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &cell_reference); let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_parenthesis() { fn test_parser_parenthesis() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -379,14 +385,14 @@ fn test_parser_parenthesis() {
column: 1, column: 1,
}; };
let t = parser.parse("(C3=\"Yes\")*5", &cell_reference); let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5"); assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
} }
#[test] #[test]
fn test_parser_excel_xlfn() { fn test_parser_excel_xlfn() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -395,7 +401,7 @@ fn test_parser_excel_xlfn() {
column: 1, column: 1,
}; };
let t = parser.parse("_xlfn.CONCAT(C3)", &cell_reference); let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])"); assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
} }
@@ -407,9 +413,9 @@ fn test_to_string_displaced() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("C3", context); let node = parser.parse("C3", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -427,9 +433,9 @@ fn test_to_string_displaced_full_ranges() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("SUM(3:3)", context); let node = parser.parse("SUM(3:3)", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -440,7 +446,7 @@ fn test_to_string_displaced_full_ranges() {
"SUM(3:3)".to_string() "SUM(3:3)".to_string()
); );
let node = parser.parse("SUM(D:D)", context); let node = parser.parse("SUM(D:D)", &Some(context.clone()));
let displace_data = DisplaceData::Row { let displace_data = DisplaceData::Row {
sheet: 0, sheet: 0,
row: 3, row: 3,
@@ -460,9 +466,9 @@ fn test_to_string_displaced_too_low() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("C3", context); let node = parser.parse("C3", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -480,9 +486,9 @@ fn test_to_string_displaced_too_high() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("C3", context); let node = parser.parse("C3", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
mod test_general;
mod test_issue_155;
mod test_move_formula;
mod test_ranges;
mod test_stringify;
mod test_tables;

View File

@@ -1,69 +0,0 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::Parser;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_155_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 2,
column: 2,
};
let t = parser.parse("A$1:A2", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "A$1:A2");
}
#[test]
fn issue_155_parser_case_2() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
let t = parser.parse("C$1:D2", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "C$1:D2");
}
#[test]
fn issue_155_parser_only_row() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
// This is tricky, I am not sure what to do in these cases
let t = parser.parse("A$2:B1", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "A1:B$2");
}
#[test]
fn issue_155_parser_only_column() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
// This is tricky, I am not sure what to do in these cases
let t = parser.parse("D1:$A3", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "$A1:D3");
}

View File

@@ -1,34 +0,0 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::Parser;
use crate::expressions::parser::stringify::to_string;
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");
}

View File

@@ -1,4 +1,4 @@
use super::{Node, move_formula::ref_is_in_area}; use super::{move_formula::ref_is_in_area, Node};
use crate::expressions::types::{Area, CellReferenceIndex}; use crate::expressions::types::{Area, CellReferenceIndex};
@@ -263,9 +263,7 @@ pub(crate) fn forward_references(
// TODO: Not implemented // TODO: Not implemented
Node::ArrayKind(_) => {} Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {} // Do nothing. Note: we could do a blanket _ => {}
Node::DefinedNameKind(_) => {} Node::VariableKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {} Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {} Node::EmptyArgKind => {}

View File

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

View File

@@ -154,16 +154,15 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Date(p) => { ParsePart::Date(p) => {
let tokens = &p.tokens; let tokens = &p.tokens;
let mut text = "".to_string(); let mut text = "".to_string();
let date = match from_excel_date(value as i64) { if !(1.0..=2_958_465.0).contains(&value) {
Ok(d) => d, // 2_958_465 is 31 December 9999
Err(e) => { return Formatted {
return Formatted { text: "#VALUE!".to_owned(),
text: "#VALUE!".to_owned(), color: None,
color: None, error: Some("Date negative or too long".to_owned()),
error: Some(e), };
}; }
} let date = from_excel_date(value as i64);
};
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
@@ -246,9 +245,6 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} }
ParsePart::Number(p) => { ParsePart::Number(p) => {
let mut text = "".to_string(); let mut text = "".to_string();
if let Some(c) = p.currency {
text = format!("{}", c);
}
let tokens = &p.tokens; let tokens = &p.tokens;
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma)); value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
// p.precision is the number of significant digits _after_ the decimal point // p.precision is the number of significant digits _after_ the decimal point
@@ -391,7 +387,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if l_exp <= p.exponent_digit_count { if l_exp <= p.exponent_digit_count {
if !(number_index < 0 && digit.kind == '#') { if !(number_index < 0 && digit.kind == '#') {
let c = if number_index < 0 { let c = if number_index < 0 {
if digit.kind == '?' { ' ' } else { '0' } if digit.kind == '?' {
' '
} else {
'0'
}
} else { } else {
exponent_part[number_index as usize] exponent_part[number_index as usize]
}; };

View File

@@ -10,7 +10,6 @@ pub struct Lexer {
pub enum Token { pub enum Token {
Color(i32), // [Red] or [Color 23] Color(i32), // [Red] or [Color 23]
Condition(Compare, f64), // [<=100] (Comparator, number) Condition(Compare, f64), // [<=100] (Comparator, number)
Currency(char), // [$€] ($ currency symbol)
Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X
Spacer(char), // *X Spacer(char), // *X
Ghost(char), // _X Ghost(char), // _X
@@ -275,15 +274,6 @@ impl Lexer {
self.set_error("Failed to parse condition"); self.set_error("Failed to parse condition");
Token::ILLEGAL Token::ILLEGAL
} }
} else if c == '$' {
// currency
self.read_next_char();
if let Some(currency) = self.read_next_char() {
self.read_next_char();
return Token::Currency(currency);
}
self.set_error("Failed to parse currency");
Token::ILLEGAL
} else { } else {
// Color // Color
if let Some(index) = self.consume_color() { if let Some(index) = self.consume_color() {

View File

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

View File

@@ -40,7 +40,6 @@ pub struct NumberPart {
pub is_scientific: bool, pub is_scientific: bool,
pub scientific_minus: bool, pub scientific_minus: bool,
pub exponent_digit_count: i32, pub exponent_digit_count: i32,
pub currency: Option<char>,
} }
pub struct DatePart { pub struct DatePart {
@@ -115,7 +114,6 @@ impl Parser {
let mut exponent_digit_count = 0; let mut exponent_digit_count = 0;
let mut number = 'i'; let mut number = 'i';
let mut index = 0; let mut index = 0;
let mut currency = None;
while token != Token::EOF && token != Token::Separator { while token != Token::EOF && token != Token::Separator {
let next_token = self.lexer.next_token(); let next_token = self.lexer.next_token();
@@ -172,9 +170,6 @@ impl Parser {
Token::Condition(cmp, value) => { Token::Condition(cmp, value) => {
condition = Some((cmp, value)); condition = Some((cmp, value));
} }
Token::Currency(c) => {
currency = Some(c);
}
Token::QuestionMark => { Token::QuestionMark => {
tokens.push(TextToken::Digit(Digit { tokens.push(TextToken::Digit(Digit {
kind: '?', kind: '?',
@@ -296,7 +291,6 @@ impl Parser {
is_scientific, is_scientific,
scientific_minus, scientific_minus,
exponent_digit_count, exponent_digit_count,
currency,
}) })
} }
} }

View File

@@ -2,7 +2,7 @@
use crate::{ use crate::{
formatter::format::format_number, formatter::format::format_number,
locale::{Locale, get_locale}, locale::{get_locale, Locale},
}; };
fn get_default_locale() -> &'static Locale { fn get_default_locale() -> &'static Locale {
@@ -76,14 +76,6 @@ fn test_color() {
assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4)); assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4));
} }
#[test]
fn dollar_euro() {
let locale = get_default_locale();
let format = "[$€]#,##0.00";
let t = format_number(3.1, format, locale);
assert_eq!(t.text, "€3.10");
}
#[test] #[test]
fn test_parts() { fn test_parts() {
let locale = get_default_locale(); let locale = get_default_locale();

View File

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

View File

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

View File

@@ -57,7 +57,7 @@
use std::f64::consts::FRAC_2_PI; use std::f64::consts::FRAC_2_PI;
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words}; use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
// R0/S0 on [0, 2.00] // R0/S0 on [0, 2.00]
const R02: f64 = 1.562_499_999_999_999_5e-2; // 0x3F8FFFFF, 0xFFFFFFFD const R02: f64 = 1.562_499_999_999_999_5e-2; // 0x3F8FFFFF, 0xFFFFFFFD

View File

@@ -56,7 +56,7 @@
use std::f64::consts::FRAC_2_PI; use std::f64::consts::FRAC_2_PI;
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words}; use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
// R0/S0 on [0,2] // R0/S0 on [0,2]
const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000 const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000

View File

@@ -40,7 +40,7 @@
use super::{ use super::{
bessel_j0_y0::{j0, y0}, bessel_j0_y0::{j0, y0},
bessel_j1_y1::{j1, y1}, bessel_j1_y1::{j1, y1},
bessel_util::{FRAC_2_SQRT_PI, split_words}, bessel_util::{split_words, FRAC_2_SQRT_PI},
}; };
// Special cases are: // Special cases are:
@@ -232,7 +232,11 @@ pub(crate) fn jn(n: i32, x: f64) -> f64 {
} }
} }
}; };
if sign == 1 { -b } else { b } if sign == 1 {
-b
} else {
b
}
} }
// Yn returns the order-n Bessel function of the second kind. // Yn returns the order-n Bessel function of the second kind.
@@ -317,5 +321,9 @@ pub(crate) fn yn(n: i32, x: f64) -> f64 {
} }
b b
}; };
if sign > 0 { b } else { -b } if sign > 0 {
b
} else {
-b
}
} }

View File

@@ -45,5 +45,9 @@ pub(crate) fn erf(x: f64) -> f64 {
} }
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd); let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
if x < 0.0 { res - 1.0 } else { 1.0 - res } if x < 0.0 {
res - 1.0
} else {
1.0 - res
}
} }

View File

@@ -2,7 +2,7 @@ use chrono::Datelike;
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
constants::{LAST_COLUMN, LAST_ROW, MAXIMUM_DATE_SERIAL_NUMBER, MINIMUM_DATE_SERIAL_NUMBER}, constants::{LAST_COLUMN, LAST_ROW},
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{parser::Node, token::Error, types::CellReferenceIndex},
formatter::dates::from_excel_date, formatter::dates::from_excel_date,
model::Model, model::Model,
@@ -13,32 +13,37 @@ use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr
// See: // See:
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx // https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> { // FIXME: Is this enough?
let end = from_excel_date(end_date)?; fn is_valid_date(date: f64) -> bool {
let start = from_excel_date(start_date)?; date > 0.0
}
fn is_less_than_one_year(start_date: i64, end_date: i64) -> bool {
if end_date - start_date < 365 { if end_date - start_date < 365 {
return Ok(true); return true;
} }
let end = from_excel_date(end_date);
let start = from_excel_date(start_date);
let end_year = end.year(); let end_year = end.year();
let start_year = start.year(); let start_year = start.year();
if end_year == start_year { if end_year == start_year {
return Ok(true); return true;
} }
if end_year != start_year + 1 { if end_year != start_year + 1 {
return Ok(false); return false;
} }
let start_month = start.month(); let start_month = start.month();
let end_month = end.month(); let end_month = end.month();
if end_month < start_month { if end_month < start_month {
return Ok(true); return true;
} }
if end_month > start_month { if end_month > start_month {
return Ok(false); return false;
} }
// we are one year later same month // we are one year later same month
let start_day = start.day(); let start_day = start.day();
let end_day = end.day(); let end_day = end.day();
Ok(end_day <= start_day) end_day <= start_day
} }
fn compute_payment( fn compute_payment(
@@ -84,9 +89,6 @@ fn compute_future_value(
if rate == 0.0 { if rate == 0.0 {
return Ok(-pv - pmt * nper); return Ok(-pv - pmt * nper);
} }
if rate == -1.0 && nper < 0.0 {
return Err((Error::DIV, "Divide by zero".to_string()));
}
let rate_nper = (1.0 + rate).powf(nper); let rate_nper = (1.0 + rate).powf(nper);
let fv = if period_start { let fv = if period_start {
@@ -431,7 +433,7 @@ impl Model {
} }
if rate == -1.0 { if rate == -1.0 {
return CalcResult::Error { return CalcResult::Error {
error: Error::DIV, error: Error::NUM,
origin: cell, origin: cell,
message: "Rate must be != -1".to_string(), message: "Rate must be != -1".to_string(),
}; };
@@ -698,7 +700,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
}; };
CalcResult::Number(ipmt) CalcResult::Number(ipmt)
@@ -762,7 +764,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
}; };
CalcResult::Number(ppmt) CalcResult::Number(ppmt)
@@ -918,9 +920,7 @@ impl Model {
} }
let first_date = dates[0]; let first_date = dates[0];
for date in &dates { for date in &dates {
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64 if !is_valid_date(*date) {
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
// Excel docs claim that if any number in dates is not a valid date, // Excel docs claim that if any number in dates is not a valid date,
// XNPV returns the #VALUE! error value, but it seems to return #VALUE! // XNPV returns the #VALUE! error value, but it seems to return #VALUE!
return CalcResult::new_error( return CalcResult::new_error(
@@ -986,9 +986,7 @@ impl Model {
} }
let first_date = dates[0]; let first_date = dates[0];
for date in &dates { for date in &dates {
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64 if !is_valid_date(*date) {
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1075,7 +1073,7 @@ impl Model {
error, error,
origin: cell, origin: cell,
message, message,
}; }
} }
} }
}; };
@@ -1096,7 +1094,7 @@ impl Model {
error, error,
origin: cell, origin: cell,
message, message,
}; }
} }
} }
}; };
@@ -1372,10 +1370,9 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) { if !is_valid_date(settlement) || !is_valid_date(maturity) {
Ok(f) => f, return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), }
};
if settlement > maturity { if settlement > maturity {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
@@ -1383,7 +1380,7 @@ impl Model {
"settlement should be <= maturity".to_string(), "settlement should be <= maturity".to_string(),
); );
} }
if !less_than_one_year { if !is_less_than_one_year(settlement as i64, maturity as i64) {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1437,10 +1434,9 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) { if !is_valid_date(settlement) || !is_valid_date(maturity) {
Ok(f) => f, return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), }
};
if settlement > maturity { if settlement > maturity {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
@@ -1448,7 +1444,7 @@ impl Model {
"settlement should be <= maturity".to_string(), "settlement should be <= maturity".to_string(),
); );
} }
if !less_than_one_year { if !is_less_than_one_year(settlement as i64, maturity as i64) {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1488,10 +1484,9 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) { if !is_valid_date(settlement) || !is_valid_date(maturity) {
Ok(f) => f, return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), }
};
if settlement > maturity { if settlement > maturity {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
@@ -1499,7 +1494,7 @@ impl Model {
"settlement should be <= maturity".to_string(), "settlement should be <= maturity".to_string(),
); );
} }
if !less_than_one_year { if !is_less_than_one_year(settlement as i64, maturity as i64) {
return CalcResult::new_error( return CalcResult::new_error(
Error::NUM, Error::NUM,
cell, cell,
@@ -1634,7 +1629,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
} }
} }
@@ -1702,7 +1697,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
} }
} }
@@ -1750,7 +1745,11 @@ impl Model {
rate = 1.0 rate = 1.0
}; };
let value = if rate == 1.0 { let value = if rate == 1.0 {
if period == 1.0 { cost } else { 0.0 } if period == 1.0 {
cost
} else {
0.0
}
} else { } else {
cost * (1.0 - rate).powf(period - 1.0) cost * (1.0 - rate).powf(period - 1.0)
}; };

View File

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

View File

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

View File

@@ -140,7 +140,6 @@ pub enum Function {
Countifs, Countifs,
Maxifs, Maxifs,
Minifs, Minifs,
Geomean,
// Date and time // Date and time
Date, Date,
@@ -249,7 +248,7 @@ pub enum Function {
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 195> { pub fn into_iter() -> IntoIter<Function, 194> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -349,7 +348,6 @@ impl Function {
Function::Countifs, Function::Countifs,
Function::Maxifs, Function::Maxifs,
Function::Minifs, Function::Minifs,
Function::Geomean,
Function::Year, Function::Year,
Function::Day, Function::Day,
Function::Month, Function::Month,
@@ -613,7 +611,6 @@ impl Function {
"COUNTIFS" => Some(Function::Countifs), "COUNTIFS" => Some(Function::Countifs),
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
"GEOMEAN" => Some(Function::Geomean),
// Date and Time // Date and Time
"YEAR" => Some(Function::Year), "YEAR" => Some(Function::Year),
"DAY" => Some(Function::Day), "DAY" => Some(Function::Day),
@@ -821,7 +818,6 @@ impl fmt::Display for Function {
Function::Countifs => write!(f, "COUNTIFS"), Function::Countifs => write!(f, "COUNTIFS"),
Function::Maxifs => write!(f, "MAXIFS"), Function::Maxifs => write!(f, "MAXIFS"),
Function::Minifs => write!(f, "MINIFS"), Function::Minifs => write!(f, "MINIFS"),
Function::Geomean => write!(f, "GEOMEAN"),
Function::Year => write!(f, "YEAR"), Function::Year => write!(f, "YEAR"),
Function::Day => write!(f, "DAY"), Function::Day => write!(f, "DAY"),
Function::Month => write!(f, "MONTH"), Function::Month => write!(f, "MONTH"),
@@ -949,7 +945,7 @@ impl Model {
match kind { match kind {
// Logical // Logical
Function::And => self.fn_and(args, cell), Function::And => self.fn_and(args, cell),
Function::False => self.fn_false(args, cell), Function::False => CalcResult::Boolean(false),
Function::If => self.fn_if(args, cell), Function::If => self.fn_if(args, cell),
Function::Iferror => self.fn_iferror(args, cell), Function::Iferror => self.fn_iferror(args, cell),
Function::Ifna => self.fn_ifna(args, cell), Function::Ifna => self.fn_ifna(args, cell),
@@ -957,7 +953,7 @@ impl Model {
Function::Not => self.fn_not(args, cell), Function::Not => self.fn_not(args, cell),
Function::Or => self.fn_or(args, cell), Function::Or => self.fn_or(args, cell),
Function::Switch => self.fn_switch(args, cell), Function::Switch => self.fn_switch(args, cell),
Function::True => self.fn_true(args, cell), Function::True => CalcResult::Boolean(true),
Function::Xor => self.fn_xor(args, cell), Function::Xor => self.fn_xor(args, cell),
// Math and trigonometry // Math and trigonometry
Function::Sin => self.fn_sin(args, cell), Function::Sin => self.fn_sin(args, cell),
@@ -1058,7 +1054,6 @@ impl Model {
Function::Countifs => self.fn_countifs(args, cell), Function::Countifs => self.fn_countifs(args, cell),
Function::Maxifs => self.fn_maxifs(args, cell), Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell), Function::Minifs => self.fn_minifs(args, cell),
Function::Geomean => self.fn_geomean(args, cell),
// Date and Time // Date and Time
Function::Year => self.fn_year(args, cell), Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell), Function::Day => self.fn_day(args, cell),

View File

@@ -388,7 +388,7 @@ impl Model {
Error::ERROR, Error::ERROR,
cell, cell,
format!("Invalid worksheet index: '{}'", first_range.left.sheet), format!("Invalid worksheet index: '{}'", first_range.left.sheet),
); )
} }
}; };
let max_row = dimension.max_row; let max_row = dimension.max_row;
@@ -635,85 +635,4 @@ impl Model {
} }
CalcResult::Number(max) CalcResult::Number(max)
} }
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut product = 1.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
} else {
product *= if b { 1.0 } else { 0.0 };
count += 1.0;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
} else if let Ok(t) = s.parse::<f64>() {
product *= t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
}
}
_ => {
// Ignore everything else
}
};
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(product.powf(1.0 / count))
}
} }

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
expressions::{ expressions::{
parser::{Node, parse_range}, parser::{parse_range, Node},
token::Error, token::Error,
types::CellReferenceIndex, types::CellReferenceIndex,
}, },

View File

@@ -8,7 +8,7 @@ use crate::{
}; };
use super::{ use super::{
text_util::{Case, substitute, text_after, text_before}, text_util::{substitute, text_after, text_before, Case},
util::from_wildcard_to_regex, util::from_wildcard_to_regex,
}; };
@@ -368,7 +368,7 @@ impl Model {
error: Error::VALUE, error: Error::VALUE,
origin: cell, origin: cell,
message: "Empty cell".to_string(), message: "Empty cell".to_string(),
}; }
} }
}; };
@@ -629,7 +629,7 @@ impl Model {
error: Error::VALUE, error: Error::VALUE,
origin: cell, origin: cell,
message: "Expecting number".to_string(), message: "Expecting number".to_string(),
}; }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => { CalcResult::Range { .. } => {

View File

@@ -25,8 +25,6 @@
#![doc = include_str!("../examples/formulas_and_errors.rs")] #![doc = include_str!("../examples/formulas_and_errors.rs")]
//! ``` //! ```
#![warn(clippy::print_stdout)]
pub mod calc_result; pub mod calc_result;
pub mod cell; pub mod cell;
pub mod expressions; pub mod expressions;
@@ -57,8 +55,8 @@ mod test;
#[cfg(test)] #[cfg(test)]
pub mod mock_time; pub mod mock_time;
pub use model::Model;
pub use model::get_milliseconds_since_epoch; pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use user_model::BorderArea; pub use user_model::BorderArea;
pub use user_model::ClipboardData; pub use user_model::ClipboardData;
pub use user_model::UserModel; pub use user_model::UserModel;

View File

@@ -8,15 +8,14 @@ use crate::{
cell::CellValue, cell::CellValue,
constants::{self, LAST_COLUMN, LAST_ROW}, constants::{self, LAST_COLUMN, LAST_ROW},
expressions::{ expressions::{
lexer::LexerMode,
parser::{ parser::{
move_formula::{move_formula, MoveContext},
stringify::{to_rc_format, to_string},
Node, Parser, Node, Parser,
move_formula::{MoveContext, move_formula},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
}, },
token::{Error, OpCompare, OpProduct, OpSum, OpUnary, get_error_by_name}, token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*, types::*,
utils::{self, is_valid_column_number, is_valid_identifier, is_valid_row}, utils::{self, is_valid_column_number, is_valid_row, parse_reference_a1},
}, },
formatter::{ formatter::{
format::{format_number, parse_formatted_number}, format::{format_number, parse_formatted_number},
@@ -24,8 +23,8 @@ use crate::{
}, },
functions::util::compare_values, functions::util::compare_values,
implicit_intersection::implicit_intersection, implicit_intersection::implicit_intersection,
language::{Language, get_language}, language::{get_language, Language},
locale::{Currency, Locale, get_locale}, locale::{get_locale, Currency, Locale},
types::*, types::*,
utils as common, utils as common,
}; };
@@ -73,7 +72,6 @@ pub(crate) enum CellState {
} }
/// A parsed formula for a defined name /// A parsed formula for a defined name
#[derive(Clone)]
pub(crate) enum ParsedDefinedName { pub(crate) enum ParsedDefinedName {
/// CellReference (`=C4`) /// CellReference (`=C4`)
CellReference(CellReferenceIndex), CellReference(CellReferenceIndex),
@@ -81,6 +79,9 @@ pub(crate) enum ParsedDefinedName {
RangeReference(Range), RangeReference(Range),
/// `=SomethingElse` /// `=SomethingElse`
InvalidDefinedNameFormula, InvalidDefinedNameFormula,
// TODO: Support constants in defined names
// TODO: Support formulas in defined names
// TODO: Support tables in defined names
} }
/// A dynamical IronCalc model. /// A dynamical IronCalc model.
@@ -416,40 +417,38 @@ impl Model {
// TODO: NOT IMPLEMENTED // TODO: NOT IMPLEMENTED
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string()) CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
} }
DefinedNameKind((name, scope)) => { VariableKind(defined_name) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) { 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 {
match parsed_defined_name { match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
self.evaluate_cell(reference) self.evaluate_cell(*reference)
} }
ParsedDefinedName::RangeReference(range) => CalcResult::Range { ParsedDefinedName::RangeReference(range) => CalcResult::Range {
left: range.left, left: range.left,
right: range.right, right: range.right,
}, },
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NAME, Error::NIMPL,
cell, cell,
format!("Defined name \"{}\" is not a reference.", name), format!("Defined name \"{}\" is not a reference.", defined_name),
), ),
} }
} else { } else {
CalcResult::new_error( CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" not found.", name), format!("Defined name \"{}\" not found.", defined_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 } => { CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell); let l = self.evaluate_node_in_context(left, cell);
if l.is_error() { if l.is_error() {
@@ -683,13 +682,6 @@ impl Model {
Err(format!("Invalid color: {}", color)) 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`) /// 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> { pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?; let worksheet = self.workbook.worksheet_mut(sheet)?;
@@ -755,6 +747,29 @@ impl Model {
self.workbook.worksheet(sheet)?.is_empty_cell(row, column) self.workbook.worksheet(sheet)?.is_empty_cell(row, column)
} }
/// Returns 'true' if the cell belongs to any Merged cells
/// # Examples
///
/// ```rust
/// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?;
/// model.merge_cells(0, "A1:D5");
/// assert_eq!(model.is_part_of_merged_cells(0, 1, 2)?, true);
/// # Ok(())
/// # }
/// ```
pub fn is_part_of_merged_cells(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<bool, String> {
self.workbook
.worksheet(sheet)?
.is_part_of_merged_cells(row, column)
}
pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult { pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult {
let row_data = match self.workbook.worksheets[cell_reference.sheet as usize] let row_data = match self.workbook.worksheets[cell_reference.sheet as usize]
.sheet_data .sheet_data
@@ -865,11 +880,6 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
// add all tables // add all tables
// let mut tables = Vec::new(); // let mut tables = Vec::new();
// for worksheet in worksheets { // for worksheet in worksheets {
@@ -879,7 +889,7 @@ impl Model {
// } // }
// tables.push(tables_in_sheet); // tables.push(tables_in_sheet);
// } // }
let parser = Parser::new(worksheet_names, defined_names, workbook.tables.clone()); let parser = Parser::new(worksheet_names, workbook.tables.clone());
let cells = HashMap::new(); let cells = HashMap::new();
let locale = get_locale(&workbook.settings.locale) let locale = get_locale(&workbook.settings.locale)
.map_err(|_| "Invalid locale".to_string())? .map_err(|_| "Invalid locale".to_string())?
@@ -960,7 +970,10 @@ impl Model {
} }
} }
} }
let sheet = self.get_sheet_index_by_name(&sheet_name)?; let sheet = match self.get_sheet_index_by_name(&sheet_name) {
Some(s) => s,
None => return None,
};
let row = match row.parse::<i32>() { let row = match row.parse::<i32>() {
Ok(r) => r, Ok(r) => r,
Err(_) => return None, Err(_) => return None,
@@ -1037,7 +1050,7 @@ impl Model {
column: source.column, column: source.column,
}; };
let formula_str = move_formula( let formula_str = move_formula(
&self.parser.parse(formula, &cell_reference), &self.parser.parse(formula, &Some(cell_reference)),
&MoveContext { &MoveContext {
source_sheet_name: &source_sheet_name, source_sheet_name: &source_sheet_name,
row: source.row, row: source.row,
@@ -1145,7 +1158,7 @@ impl Model {
row: source.row, row: source.row,
column: source.column, column: source.column,
}; };
let formula = &self.parser.parse(formula_str, &cell_reference); let formula = &self.parser.parse(formula_str, &Some(cell_reference));
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: target_sheet_name, sheet: target_sheet_name,
row: target.row, row: target.row,
@@ -1235,6 +1248,14 @@ impl Model {
column: i32, column: i32,
value: &str, value: &str,
) -> Result<(), String> { ) -> Result<(), String> {
// Checking first whether cell we are updating is part of Merged cells
// if so returning with Err
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
let style_index = self.get_cell_style_index(sheet, row, column)?; let style_index = self.get_cell_style_index(sheet, row, column)?;
let new_style_index; let new_style_index;
if common::value_needs_quoting(value, &self.language) { if common::value_needs_quoting(value, &self.language) {
@@ -1285,6 +1306,15 @@ impl Model {
column: i32, column: i32,
value: bool, value: bool,
) -> Result<(), String> { ) -> Result<(), String> {
// Checking first whether cell we are updating is part of Merged cells
// if so returning with Err
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
let style_index = self.get_cell_style_index(sheet, row, column)?; let style_index = self.get_cell_style_index(sheet, row, column)?;
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) { let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
self.workbook self.workbook
@@ -1327,6 +1357,14 @@ impl Model {
column: i32, column: i32,
value: f64, value: f64,
) -> Result<(), String> { ) -> Result<(), String> {
// Checking first whether cell we are updating is part of Merged cells
// if so returning with Err
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
let style_index = self.get_cell_style_index(sheet, row, column)?; let style_index = self.get_cell_style_index(sheet, row, column)?;
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) { let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
self.workbook self.workbook
@@ -1372,6 +1410,12 @@ impl Model {
column: i32, column: i32,
formula: String, formula: String,
) -> Result<(), String> { ) -> Result<(), String> {
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
let mut style_index = self.get_cell_style_index(sheet, row, column)?; let mut style_index = self.get_cell_style_index(sheet, row, column)?;
if self.workbook.styles.style_is_quote_prefix(style_index) { if self.workbook.styles.style_is_quote_prefix(style_index) {
style_index = self style_index = self
@@ -1424,6 +1468,14 @@ impl Model {
column: i32, column: i32,
value: String, value: String,
) -> Result<(), String> { ) -> Result<(), String> {
// Checking first whether cell we are updating is part of Merged cells
// if so returning with Err
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
// If value starts with "'" then we force the style to be quote_prefix // If value starts with "'" then we force the style to be quote_prefix
let style_index = self.get_cell_style_index(sheet, row, column)?; let style_index = self.get_cell_style_index(sheet, row, column)?;
if let Some(new_value) = value.strip_prefix('\'') { if let Some(new_value) = value.strip_prefix('\'') {
@@ -1521,11 +1573,13 @@ impl Model {
column, column,
}; };
let shared_formulas = &mut worksheet.shared_formulas; let shared_formulas = &mut worksheet.shared_formulas;
let mut parsed_formula = self.parser.parse(formula, &cell_reference); let mut parsed_formula = self.parser.parse(formula, &Some(cell_reference.clone()));
// If the formula fails to parse try adding a parenthesis // If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3) // SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula { if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference); let new_parsed_formula = self
.parser
.parse(&format!("{})", formula), &Some(cell_reference));
match new_parsed_formula { match new_parsed_formula {
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula, _ => parsed_formula = new_parsed_formula,
@@ -1604,42 +1658,6 @@ impl Model {
.set_cell_with_number(row, column, value, style) .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 /// Gets the Excel Value (Bool, Number, String) of a cell
/// ///
/// See also: /// See also:
@@ -1872,29 +1890,12 @@ impl Model {
} }
/// Returns the style for cell (`sheet`, `row`, `column`) /// 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> { 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_index = self.get_cell_style_index(sheet, row, column)?;
let style = self.workbook.styles.get_style(style_index)?; let style = self.workbook.styles.get_style(style_index)?;
Ok(style) 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 /// Returns an internal binary representation of the workbook
/// ///
/// See also: /// See also:
@@ -1989,6 +1990,7 @@ impl Model {
/// Sets the number of frozen rows to `frozen_rows` in the workbook. /// Sets the number of frozen rows to `frozen_rows` in the workbook.
/// Fails if `frozen`_rows` is either too small (<0) or too large (>LAST_ROW)` /// Fails if `frozen`_rows` is either too small (<0) or too large (>LAST_ROW)`
pub fn set_frozen_rows(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> { pub fn set_frozen_rows(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> {
// TODO: What is frozen rows and do we need to take of this if row we are frozing is part of merge cells ?
if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) { if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) {
if frozen_rows < 0 { if frozen_rows < 0 {
return Err("Frozen rows cannot be negative".to_string()); return Err("Frozen rows cannot be negative".to_string());
@@ -2006,6 +2008,7 @@ impl Model {
/// Sets the number of frozen columns to `frozen_column` in the workbook. /// Sets the number of frozen columns to `frozen_column` in the workbook.
/// Fails if `frozen`_columns` is either too small (<0) or too large (>LAST_COLUMN)` /// Fails if `frozen`_columns` is either too small (<0) or too large (>LAST_COLUMN)`
pub fn set_frozen_columns(&mut self, sheet: u32, frozen_columns: i32) -> Result<(), String> { pub fn set_frozen_columns(&mut self, sheet: u32, frozen_columns: i32) -> Result<(), String> {
// TODO: What is frozen columns and do we need to take of this if column we are frozing is part of merge cells ?
if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) { if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) {
if frozen_columns < 0 { if frozen_columns < 0 {
return Err("Frozen columns cannot be negative".to_string()); return Err("Frozen columns cannot be negative".to_string());
@@ -2048,202 +2051,162 @@ impl Model {
.set_row_height(column, height) .set_row_height(column, height)
} }
/// Adds a new defined name fn parse_merged_range(&mut self, range: &str) -> Result<(i32, i32, i32, i32), String> {
pub fn new_defined_name( let parts: Vec<&str> = range.split(':').collect();
&mut self, if parts.len() == 1 {
name: &str, Err(format!("Invalid range: '{}'", range))
scope: Option<u32>, } else if parts.len() == 2 {
formula: &str, match (parse_reference_a1(parts[0]), parse_reference_a1(parts[1])) {
) -> Result<(), String> { (Some(left), Some(right)) => {
if !is_valid_identifier(name) { return Ok((left.row, left.column, right.row, right.column));
return Err("Invalid defined name".to_string()); }
}; _ => return Err(format!("Invalid range: '{}'", range)),
let name_upper = name.to_uppercase(); }
let defined_names = &self.workbook.defined_names; } else {
let sheet_id = match scope { return Err(format!("Invalid range: '{}'", range));
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id), }
None => None, }
};
// if the defined name already exist return error // Implementing public APIS related to Merge cells handling
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id { /// Merges given selected cells
return Err("Defined name already exists".to_string()); /// If no overlap, it will create that merged cells with left most top cell value representing the whole merged cells
/// If new merge cells creation overlaps with any of the existing merged cells, Overlapped merged cells gets unmerged
/// and new merge cells gets added
///
/// # Examples
///
/// ```rust
/// # use ironcalc_base::Model;
/// # use ironcalc_base::cell::CellValue;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?;
/// model.merge_cells(0, "D4:F6").unwrap();
/// model.merge_cells(0, "A1:B4").unwrap();
/// assert_eq!(model.workbook.worksheet(0).unwrap().merged_cells_list.len(), 2);
/// # Ok(())
/// # }
/// ```
///
/// See also:
/// * [Model::update_cell_with_formula()]
/// * [Model::update_cell_with_number()]
/// * [Model::update_cell_with_bool()]
/// * [Model::update_cell_with_text()]
pub fn merge_cells(&mut self, sheet: u32, range_ref: &str) -> Result<(), String> {
match self.parse_merged_range(range_ref) {
Ok(parsed_merge_cell_range) => {
// ATTENTION 2: Below thing we can support here but keeping it simple
// Web or different client needs to keep this in mind
// User can give errored parse ranges like C3:A1
// Where col_start and row_start and is greated then col_end and row_end
// Return error in these scenario
if parsed_merge_cell_range.0 > parsed_merge_cell_range.2
|| parsed_merge_cell_range.1 > parsed_merge_cell_range.3
{
return Err(
"Invalid parse range. Merge Mother cell always be top left cell"
.to_string(),
);
}
let mut merged_cells_overlaped_list: Vec<bool> = Vec::new();
// checking whether our new range overlaps with any of the already existing merged cells
// if so, need to unmerge those and create this new one
{
let worksheet = self.workbook.worksheet(sheet)?;
let merged_cells = worksheet.get_merged_cells_list();
for merge_node in merged_cells {
// checking whether any overlapping exist with this merge cell
if !(parsed_merge_cell_range.1 > merge_node.3
|| parsed_merge_cell_range.3 < merge_node.1
|| parsed_merge_cell_range.0 > merge_node.2
|| parsed_merge_cell_range.2 < merge_node.0)
{
// overlap has happened
merged_cells_overlaped_list.push(true);
} else {
merged_cells_overlaped_list.push(false);
}
}
}
if !merged_cells_overlaped_list.is_empty() {
// Lets take Mutable ref to Merge cell and deletes all those nodes which has overlapped
let worksheet = self.workbook.worksheet_mut(sheet)?;
let merged_cells_list_mut = worksheet.get_merged_cells_list_mut();
let mut merged_cells_overlaped_list_iter = merged_cells_overlaped_list.iter();
merged_cells_list_mut
.retain(|_| !(*merged_cells_overlaped_list_iter.next().unwrap()))
}
// Now need to update (n*m - 1) cells with empty cell ( except the Mother cell )
for row_index in parsed_merge_cell_range.0..=parsed_merge_cell_range.2 {
for col_index in parsed_merge_cell_range.1..=parsed_merge_cell_range.3 {
// skip Mother cell
if row_index == parsed_merge_cell_range.0
&& col_index == parsed_merge_cell_range.2
{
continue;
}
//update the node with empty cell
{
self.workbook.worksheet_mut(sheet)?.update_cell(
row_index,
col_index,
Cell::EmptyCell { s: 0 },
)?;
}
}
}
let new_merged_cells = MergedCells::new(parsed_merge_cell_range);
{
self.workbook
.worksheet_mut(sheet)?
.merged_cells_list
.push(new_merged_cells);
}
}
Err(err) => {
return Err(err);
} }
} }
self.workbook.defined_names.push(DefinedName {
name: name.to_string(),
formula: formula.to_string(),
sheet_id,
});
self.reset_parsed_structures();
Ok(()) Ok(())
} }
/// Delete defined name of name and scope /// Unmerges a given/selected merged cells
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> { /// Once unmerged, only top most left corner value gets retained and all the others will have empty cell
let name_upper = name.to_uppercase(); /// # Examples
let defined_names = &self.workbook.defined_names; ///
let sheet_id = match scope { /// ```rust
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id), /// # use ironcalc_base::Model;
None => None, /// # use ironcalc_base::cell::CellValue;
}; /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut index = None; /// let mut model = Model::new_empty("model", "en", "UTC")?;
for (i, df) in defined_names.iter().enumerate() { /// model.merge_cells(0, "D4:F6");
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id { /// model.unmerge_cells(0, "D4:F6");
index = Some(i); /// # Ok(())
/// # }
/// ```
pub fn unmerge_cells(&mut self, sheet: u32, range_ref: &str) -> Result<(), String> {
let worksheet = self.workbook.worksheet(sheet)?;
let merged_cells = worksheet.get_merged_cells_list();
for (index, merge_node) in merged_cells.iter().enumerate() {
let merge_block_range_ref = merge_node.get_merged_cells_str_ref()?;
// finding the merge cell node to be deleted
if merge_block_range_ref.as_str() == range_ref {
// Merge cell to be deleted is found
self.workbook
.worksheet_mut(sheet)?
.merged_cells_list
.remove(index);
return Ok(());
} }
} }
if let Some(i) = index { Err("Invalid merge_cell_ref, Merged cells to be deleted is not found".to_string())
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)
} }
} }

View File

@@ -4,18 +4,18 @@ use std::collections::HashMap;
use crate::{ use crate::{
calc_result::Range, calc_result::Range,
constants::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH}, constants::{DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH},
expressions::{ expressions::{
lexer::LexerMode, lexer::LexerMode,
parser::{ parser::{
Parser,
stringify::{rename_sheet_in_node, to_rc_format}, stringify::{rename_sheet_in_node, to_rc_format},
Parser,
}, },
types::CellReferenceRC, types::CellReferenceRC,
}, },
language::get_language, language::get_language,
locale::get_locale, locale::get_locale,
model::{Model, ParsedDefinedName, get_milliseconds_since_epoch}, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{ types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
}, },
@@ -57,7 +57,7 @@ impl Model {
rows: vec![], rows: vec![],
comments: vec![], comments: vec![],
dimension: "A1".to_string(), dimension: "A1".to_string(),
merge_cells: vec![], merged_cells_list: vec![],
name: name.to_string(), name: name.to_string(),
shared_formulas: vec![], shared_formulas: vec![],
sheet_data: Default::default(), sheet_data: Default::default(),
@@ -85,14 +85,14 @@ impl Model {
let worksheets = &self.workbook.worksheets; let worksheets = &self.workbook.worksheets;
for worksheet in worksheets { for worksheet in worksheets {
let shared_formulas = &worksheet.shared_formulas; let shared_formulas = &worksheet.shared_formulas;
let cell_reference = CellReferenceRC { let cell_reference = &Some(CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
column: 1, column: 1,
}; });
let mut parse_formula = Vec::new(); let mut parse_formula = Vec::new();
for formula in shared_formulas { 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); parse_formula.push(t);
} }
self.parsed_formulas.push(parse_formula); self.parsed_formulas.push(parse_formula);
@@ -144,14 +144,8 @@ impl Model {
/// Reparses all formulas and defined names /// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) { pub(crate) fn reset_parsed_structures(&mut self) {
let defined_names = self
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
self.parser self.parser
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names); .set_worksheets(self.workbook.get_worksheet_names());
self.parsed_formulas = vec![]; self.parsed_formulas = vec![];
self.parse_formulas(); self.parse_formulas();
self.parsed_defined_names = HashMap::new(); self.parsed_defined_names = HashMap::new();
@@ -268,11 +262,11 @@ impl Model {
// We use iter because the default would be a mut_iter and we don't need a mutable reference // 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; let worksheets = &mut self.workbook.worksheets;
for worksheet in worksheets { for worksheet in worksheets {
let cell_reference = &CellReferenceRC { let cell_reference = &Some(CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
column: 1, column: 1,
}; });
let mut formulas = Vec::new(); let mut formulas = Vec::new();
for formula in &worksheet.shared_formulas { for formula in &worksheet.shared_formulas {
let mut t = self.parser.parse(formula, cell_reference); let mut t = self.parser.parse(formula, cell_reference);
@@ -301,7 +295,7 @@ impl Model {
}; };
if sheet_index >= sheet_count { if sheet_index >= sheet_count {
return Err("Sheet index too large".to_string()); return Err("Sheet index too large".to_string());
}; }
self.workbook.worksheets.remove(sheet_index as usize); self.workbook.worksheets.remove(sheet_index as usize);
self.reset_parsed_structures(); self.reset_parsed_structures();
Ok(()) Ok(())
@@ -365,7 +359,7 @@ impl Model {
WorkbookView { WorkbookView {
sheet: 0, sheet: 0,
window_width: DEFAULT_WINDOW_WIDTH, window_width: DEFAULT_WINDOW_WIDTH,
window_height: DEFAULT_WINDOW_HEIGHT, window_height: DEFAULT_WINDOW_HEIGH,
}, },
); );
@@ -394,7 +388,7 @@ impl Model {
let parsed_formulas = Vec::new(); let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets; let worksheets = &workbook.worksheets;
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let parser = Parser::new(worksheet_names, vec![], HashMap::new()); let parser = Parser::new(worksheet_names, HashMap::new());
let cells = HashMap::new(); let cells = HashMap::new();
// FIXME: Add support for display languages // FIXME: Add support for display languages

View File

@@ -150,7 +150,7 @@ pub fn format_number(value: f64, format_code: &str, locale: &str) -> Formatted {
text: "#ERROR!".to_owned(), text: "#ERROR!".to_owned(),
color: None, color: None,
error: Some("Invalid locale".to_string()), error: Some("Invalid locale".to_string()),
}; }
} }
}; };
formatter::format::format_number(value, format_code, locale) formatter::format::format_number(value, format_code, locale)

View File

@@ -4,6 +4,8 @@ use crate::{
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles}, 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 { impl Styles {
fn get_font_index(&self, font: &Font) -> Option<i32> { fn get_font_index(&self, font: &Font) -> Option<i32> {
for (font_index, item) in self.fonts.iter().enumerate() { for (font_index, item) in self.fonts.iter().enumerate() {
@@ -221,6 +223,14 @@ impl Model {
column: i32, column: i32,
style: &Style, style: &Style,
) -> Result<(), String> { ) -> Result<(), String> {
// Checking first whether cell we are updating is part of Merged cells
// if so returning with Err
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
let style_index = self.workbook.styles.get_style_index_or_create(style); let style_index = self.workbook.styles.get_style_index_or_create(style);
self.workbook self.workbook
.worksheet_mut(sheet)? .worksheet_mut(sheet)?
@@ -250,6 +260,14 @@ impl Model {
column: i32, column: i32,
style_name: &str, style_name: &str,
) -> Result<(), String> { ) -> Result<(), String> {
// Checking first whether cell we are updating is part of Merged cells
// if so returning with Err
if self.is_part_of_merged_cells(sheet, row, column)? {
return Err(format!(
"Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible",
row, column
));
}
let style_index = self.workbook.styles.get_style_index_by_name(style_name)?; let style_index = self.workbook.styles.get_style_index_by_name(style_name)?;
self.workbook self.workbook
.worksheet_mut(sheet)? .worksheet_mut(sheet)?

View File

@@ -13,14 +13,12 @@ mod test_fn_averageifs;
mod test_fn_choose; mod test_fn_choose;
mod test_fn_concatenate; mod test_fn_concatenate;
mod test_fn_count; mod test_fn_count;
mod test_fn_day;
mod test_fn_exact; mod test_fn_exact;
mod test_fn_financial; mod test_fn_financial;
mod test_fn_formulatext; mod test_fn_formulatext;
mod test_fn_if; mod test_fn_if;
mod test_fn_maxifs; mod test_fn_maxifs;
mod test_fn_minifs; mod test_fn_minifs;
mod test_fn_or_xor;
mod test_fn_product; mod test_fn_product;
mod test_fn_rept; mod test_fn_rept;
mod test_fn_sum; mod test_fn_sum;
@@ -37,13 +35,11 @@ mod test_model_cell_clear_all;
mod test_model_is_empty_cell; mod test_model_is_empty_cell;
mod test_move_formula; mod test_move_formula;
mod test_quote_prefix; mod test_quote_prefix;
mod test_row_column_styles;
mod test_set_user_input; mod test_set_user_input;
mod test_sheet_markup; mod test_sheet_markup;
mod test_sheets; mod test_sheets;
mod test_styles; mod test_styles;
mod test_trigonometric; mod test_trigonometric;
mod test_true_false;
mod test_workbook; mod test_workbook;
mod test_worksheet; mod test_worksheet;
pub(crate) mod util; pub(crate) mod util;
@@ -54,12 +50,10 @@ mod test_number_format;
mod test_escape_quotes; mod test_escape_quotes;
mod test_extend; mod test_extend;
mod test_fn_fv;
mod test_fn_type; mod test_fn_type;
mod test_frozen_rows_and_columns; mod test_frozen_rows_and_columns;
mod test_geomean;
mod test_get_cell_content; mod test_get_cell_content;
mod test_issue_155; mod test_model_merge_cell_fns;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;
mod test_today; mod test_today;

View File

@@ -206,11 +206,9 @@ fn test_delete_column_width() {
let (sheet, column) = (0, 5); let (sheet, column) = (0, 5);
let normal_width = model.get_column_width(sheet, column).unwrap(); let normal_width = model.get_column_width(sheet, column).unwrap();
// Set the width of one column to 5 times the normal width // Set the width of one column to 5 times the normal width
assert!( assert!(model
model .set_column_width(sheet, column, normal_width * 5.0)
.set_column_width(sheet, column, normal_width * 5.0) .is_ok());
.is_ok()
);
// delete it // delete it
assert!(model.delete_columns(sheet, column, 1).is_ok()); assert!(model.delete_columns(sheet, column, 1).is_ok());

View File

@@ -82,21 +82,3 @@ fn test_column_width_higher_edge() {
assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); 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)); 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));
}

View File

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

View File

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

View File

@@ -2,6 +2,9 @@
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
#[test]
fn simple_cases() {}
#[test] #[test]
fn wrong_number_of_arguments() { fn wrong_number_of_arguments() {
let mut model = new_empty_model(); let mut model = new_empty_model();

View File

@@ -1,36 +0,0 @@
#![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");
}

View File

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

View File

@@ -1,7 +1,5 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::constants::DEFAULT_ROW_HEIGHT;
use crate::cell::CellValue; use crate::cell::CellValue;
use crate::number_format::to_excel_precision_str; use crate::number_format::to_excel_precision_str;
@@ -115,15 +113,6 @@ fn test_set_row_height() {
worksheet.set_row_height(5, 5.0).unwrap(); worksheet.set_row_height(5, 5.0).unwrap();
let worksheet = model.workbook.worksheet(0).unwrap(); let worksheet = model.workbook.worksheet(0).unwrap();
assert!((5.0 - worksheet.row_height(5).unwrap()).abs() < f64::EPSILON); 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] #[test]

View File

@@ -1,27 +0,0 @@
#![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");
}

View File

@@ -1,14 +0,0 @@
#![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());
}

View File

@@ -0,0 +1,155 @@
#![allow(clippy::unwrap_used)]
use crate::{test::util::new_empty_model, types::CellType};
#[test]
fn test_model_set_fns_related_to_merge_cells() {
let mut model = new_empty_model();
// creating a merge cell of D1:F2
model.merge_cells(0, "D1:F2").unwrap();
// Updating the mother cell of Merge cells and expecting the update to go through
model.set_user_input(0, 1, 4, "Hello".to_string()).unwrap();
assert_eq!(model.get_cell_content(0, 1, 4).unwrap(), "Hello");
assert_eq!(model.get_cell_type(0, 1, 4).unwrap(), CellType::Text);
// Updating cell which is not in Merge cell block
assert_eq!(model.set_user_input(0, 1, 3, "Hello".to_string()), Ok(()));
assert_eq!(model.get_cell_content(0, 1, 3), Ok("Hello".to_string()));
assert_eq!(model.get_cell_type(0, 1, 3), Ok(CellType::Text));
// 1: testing with set_user_input()
assert_eq!(
model
.set_user_input(0, 1, 5, "Hello".to_string()),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string()));
assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number));
// 2: testing with update_cell_with_bool()
assert_eq!(
model
.update_cell_with_bool(0, 1, 5, true),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string()));
assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number));
// 3: testing with update_cell_with_formula()
assert_eq!(
model
.update_cell_with_formula(0, 1, 5, "=SUM(A1+A2)".to_string()),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number));
// 4: testing with update_cell_with_number()
assert_eq!(
model
.update_cell_with_number(0, 1, 5, 10.0),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string()));
assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number));
// 5: testing with update_cell_with_text()
assert_eq!(
model
.update_cell_with_text(0, 1, 5, "new text"),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string()));
assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number));
}
#[test]
fn test_model_merge_cells_crud_api() {
let mut model = new_empty_model();
// creating a merge cell of D4:F6
model.merge_cells(0, "D4:F6").unwrap();
model
.set_user_input(0, 4, 4, "Merge Block".to_string())
.unwrap();
// CRUD APIS testing on Merge Cells
// Case1: Creating a new merge cell without overlapping
// Newly created Merge block is left to D4:F6
assert_eq!(model.merge_cells(0, "A1:B4"), Ok(()));
assert_eq!(
model.workbook.worksheet(0).unwrap().merged_cells_list.len(),
2
);
model.set_user_input(0, 1, 1, "left".to_string()).unwrap();
// Newly created Merge block is right to D4:F6
assert_eq!(model.merge_cells(0, "G1:H7"), Ok(()));
assert_eq!(
model.workbook.worksheet(0).unwrap().merged_cells_list.len(),
3
);
model.set_user_input(0, 1, 7, "right".to_string()).unwrap();
// Newly created Merge block is above to D4:F6
assert_eq!(model.merge_cells(0, "C1:D3"), Ok(()));
assert_eq!(
model.workbook.worksheet(0).unwrap().merged_cells_list.len(),
4
);
model.set_user_input(0, 1, 3, "top".to_string()).unwrap();
// Newly created Merge block is down to D4:F6
assert_eq!(model.merge_cells(0, "D8:E9"), Ok(()));
assert_eq!(
model.workbook.worksheet(0).unwrap().merged_cells_list.len(),
5
);
model.set_user_input(0, 8, 4, "down".to_string()).unwrap();
// Case2: Creating a new merge cell with overlapping with other 3 merged cell
assert_eq!(model.merge_cells(0, "C1:G4"), Ok(()));
assert_eq!(
model.workbook.worksheet(0).unwrap().merged_cells_list.len(),
3
);
model
.set_user_input(0, 1, 3, "overlapped_new_merge_block".to_string())
.unwrap();
// Case3: Giving wrong parsing range
assert_eq!(
model.merge_cells(0, "C3:A1"),
Err("Invalid parse range. Merge Mother cell always be top left cell".to_string())
);
assert_eq!(
model.merge_cells(0, "CA:A1"),
Err("Invalid range: 'CA:A1'".to_string())
);
assert_eq!(
model.merge_cells(0, "C0:A1"),
Err("Invalid range: 'C0:A1'".to_string())
);
assert_eq!(
model.merge_cells(0, "C1:A0"),
Err("Invalid range: 'C1:A0'".to_string())
);
assert_eq!(
model.merge_cells(0, "C1"),
Err("Invalid range: 'C1'".to_string())
);
assert_eq!(
model.merge_cells(0, "C1:A1:B1"),
Err("Invalid range: 'C1:A1:B1'".to_string())
);
// Case3: Giving wrong merge_ref, which would resulting in error (Merge cell to be deleted is not found)
assert_eq!(
model.unmerge_cells(0, "C1:E1"),
Err("Invalid merge_cell_ref, Merged cells to be deleted is not found".to_string())
);
// Case4: unmerge scenario
assert_eq!(model.unmerge_cells(0, "C1:G4"), Ok(()));
}

View File

@@ -179,60 +179,52 @@ fn test_move_formula_rectangle() {
width: 2, width: 2,
height: 20, height: 20,
}; };
assert!( assert!(model
model .move_cell_value_to_area(
.move_cell_value_to_area( value,
value, &CellReferenceIndex {
&CellReferenceIndex { sheet: 0,
sheet: 0, column: 3,
column: 3, row: 1,
row: 1, },
}, target,
target, area
area )
) .is_err());
.is_err() assert!(model
); .move_cell_value_to_area(
assert!( value,
model &CellReferenceIndex {
.move_cell_value_to_area( sheet: 0,
value, column: 2,
&CellReferenceIndex { row: 1,
sheet: 0, },
column: 2, target,
row: 1, area
}, )
target, .is_ok());
area assert!(model
) .move_cell_value_to_area(
.is_ok() value,
); &CellReferenceIndex {
assert!( sheet: 0,
model column: 1,
.move_cell_value_to_area( row: 20,
value, },
&CellReferenceIndex { target,
sheet: 0, area
column: 1, )
row: 20, .is_ok());
}, assert!(model
target, .move_cell_value_to_area(
area value,
) &CellReferenceIndex {
.is_ok() sheet: 0,
); column: 1,
assert!( row: 21,
model },
.move_cell_value_to_area( target,
value, area
&CellReferenceIndex { )
sheet: 0, .is_err());
column: 1,
row: 21,
},
target,
area
)
.is_err()
);
} }

View File

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

View File

@@ -62,3 +62,45 @@ fn test_create_named_style() {
let style = model.get_style_for_cell(0, 1, 1).unwrap(); let style = model.get_style_for_cell(0, 1, 1).unwrap();
assert!(style.font.b); assert!(style.font.b);
} }
#[test]
fn test_model_style_set_fns_in_merge_cell_context() {
let mut model = new_empty_model();
// creating a merge cell of D1:F2
model.merge_cells(0, "D1:F2").unwrap();
model.set_user_input(0, 1, 4, "Hello".to_string()).unwrap();
let mut style = model.get_style_for_cell(0, 1, 1).unwrap();
assert!(!style.font.b);
style.font.b = true;
// Updating the mother cell of Merge cells and expecting the update to go through
// This should make the text "Hello" in bold format
assert_eq!(model.set_cell_style(0, 1, 4, &style), Ok(()));
// 1: testing with set_cell_style()
let original_style: Style = model.get_style_for_cell(0, 1, 5).unwrap();
assert_eq!(
model
.set_cell_style(0, 1, 5, &style),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_style_for_cell(0, 1, 5), Ok(original_style));
// 2: testing with set_cell_style_by_name
let mut style = model.get_style_for_cell(0, 1, 4).unwrap();
style.font.b = true;
assert_eq!(
model.workbook.styles.create_named_style("bold", &style),
Ok(())
);
let original_style: Style = model.get_style_for_cell(0, 1, 5).unwrap();
assert_eq!(
model
.set_cell_style_by_name(0, 1, 5, "bold"),
Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string())
);
assert_eq!(model.get_style_for_cell(0, 1, 5), Ok(original_style));
}

View File

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

View File

@@ -283,3 +283,87 @@ fn test_worksheet_navigate_to_edge_in_direction() {
assert_eq!(navigate(8, 3, NavigationDirection::Up), (6, 3)); assert_eq!(navigate(8, 3, NavigationDirection::Up), (6, 3));
assert_eq!(navigate(9, 3, NavigationDirection::Up), (6, 3)); assert_eq!(navigate(9, 3, NavigationDirection::Up), (6, 3));
} }
// Tests Merge cells related functions of worksheet
#[test]
fn test_merge_cell_fns_worksheet() {
let mut model = new_empty_model();
// Adding one Merge cell
model.merge_cells(0, "D1:E3").unwrap();
// Lets check whether D1 (Mother Merge cell) is part of Merge block or not
// It should not be considered as part of Merge cell
assert!(!model
.workbook
.worksheet(0)
.unwrap()
.is_part_of_merged_cells(1, 4)
.unwrap(),);
// Lets give cell which is actually part of Merge block and expect true from fn
assert!(model
.workbook
.worksheet(0)
.unwrap()
.is_part_of_merged_cells(2, 4)
.unwrap());
// Lets give cell which is not a part of Merge block and expect false from fn
assert!(!model
.workbook
.worksheet(0)
.unwrap()
.is_part_of_merged_cells(2, 6)
.unwrap());
// Lets give an Invalid row
assert_eq!(
model
.workbook
.worksheet(0)
.unwrap()
.is_part_of_merged_cells(0, 1),
Err("Incorrect row or column".to_string())
);
//Lets give Invalid column
assert_eq!(
model
.workbook
.worksheet(0)
.unwrap()
.is_part_of_merged_cells(1, 0),
Err("Incorrect row or column".to_string())
);
// Verifying get fns of worksheet
assert_eq!(
model
.workbook
.worksheet(0)
.unwrap()
.get_merged_cells_list()
.len(),
1
);
{
let merge_cell_vec = model
.workbook
.worksheet_mut(0)
.unwrap()
.get_merged_cells_list_mut();
merge_cell_vec.remove(0);
assert_eq!(
model
.workbook
.worksheet(0)
.unwrap()
.get_merged_cells_list()
.len(),
0
);
}
}

View File

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

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{UserModel, constants::DEFAULT_COLUMN_WIDTH}; use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel};
#[test] #[test]
fn add_undo_redo() { fn add_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, 1, "=1 + 1").unwrap();
model.set_user_input(1, 1, 2, "=A1*3").unwrap(); model.set_user_input(1, 1, 2, "=A1*3").unwrap();
model model
.set_columns_width(1, 5, 5, 5.0 * DEFAULT_COLUMN_WIDTH) .set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
.unwrap(); .unwrap();
model.new_sheet().unwrap(); model.new_sheet().unwrap();
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap(); model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
@@ -25,6 +25,9 @@ fn add_undo_redo() {
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string())); assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
model.delete_sheet(1).unwrap(); model.delete_sheet(1).unwrap();
assert!(!model.can_undo());
assert!(!model.can_redo());
} }
#[test] #[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::types::Area; use crate::expressions::types::Area;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::UserModel;
#[test] #[test]
fn basic_tests() { fn basic_tests() {

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::types::Area; use crate::expressions::types::Area;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::UserModel;
#[test] #[test]
fn basic_tests() { fn basic_tests() {

View File

@@ -1,10 +1,10 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
BorderArea, UserModel,
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW},
expressions::{types::Area, utils::number_to_column}, expressions::{types::Area, utils::number_to_column},
types::{Border, BorderItem, BorderStyle}, types::{Border, BorderItem, BorderStyle},
BorderArea, UserModel,
}; };
// checks there are no borders in the sheet // checks there are no borders in the sheet
@@ -520,19 +520,14 @@ fn borders_top() {
.unwrap(); .unwrap();
model.set_area_with_border(range, &border_area).unwrap(); model.set_area_with_border(range, &border_area).unwrap();
check_borders(&model); check_borders(&model);
for row in 4..9 { for row in 5..9 {
for column in 6..9 { for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap(); let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem { let border_item = BorderItem {
style: BorderStyle::Thin, style: BorderStyle::Thin,
color: Some("#FF5566".to_string()), color: Some("#FF5566".to_string()),
}; };
let bottom = if row != 4 { let bottom = if row == 8 {
None
} else {
Some(border_item.clone())
};
let top = if row != 5 {
None None
} else { } else {
Some(border_item.clone()) Some(border_item.clone())
@@ -542,7 +537,7 @@ fn borders_top() {
diagonal_down: false, diagonal_down: false,
left: None, left: None,
right: None, right: None,
top, top: Some(border_item.clone()),
bottom, bottom,
diagonal: None, diagonal: None,
}; };
@@ -652,12 +647,12 @@ fn borders_right() {
style: BorderStyle::Thin, style: BorderStyle::Thin,
color: Some("#FF5566".to_string()), color: Some("#FF5566".to_string()),
}; };
let left = if column != 9 { let left = if column == 6 {
None None
} else { } else {
Some(border_item.clone()) Some(border_item.clone())
}; };
let right = if column != 8 { let right = if column == 9 {
None None
} else { } else {
Some(border_item.clone()) Some(border_item.clone())
@@ -710,7 +705,7 @@ fn borders_bottom() {
color: Some("#FF5566".to_string()), color: Some("#FF5566".to_string()),
}; };
// The top will also have a value for all but the first one // The top will also have a value for all but the first one
let bottom = if row != 8 { let top = if row == 5 {
None None
} else { } else {
Some(border_item.clone()) Some(border_item.clone())
@@ -720,8 +715,8 @@ fn borders_bottom() {
diagonal_down: false, diagonal_down: false,
left: None, left: None,
right: None, right: None,
top: None, top,
bottom, bottom: Some(border_item.clone()),
diagonal: None, diagonal: None,
}; };
assert_eq!(style.border, expected_border); assert_eq!(style.border, expected_border);
@@ -756,13 +751,18 @@ fn borders_left() {
model.set_area_with_border(range, &border_area).unwrap(); model.set_area_with_border(range, &border_area).unwrap();
for row in 5..9 { for row in 5..9 {
for column in 6..9 { for column in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap(); let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem { let border_item = BorderItem {
style: BorderStyle::Thin, style: BorderStyle::Thin,
color: Some("#FF5566".to_string()), color: Some("#FF5566".to_string()),
}; };
let left = if column != 6 { let left = if column == 5 {
None
} else {
Some(border_item.clone())
};
let right = if column == 8 {
None None
} else { } else {
Some(border_item.clone()) Some(border_item.clone())
@@ -771,29 +771,13 @@ fn borders_left() {
diagonal_up: false, diagonal_up: false,
diagonal_down: false, diagonal_down: false,
left, left,
right: None, right,
top: None, top: None,
bottom: None, bottom: None,
diagonal: None, diagonal: None,
}; };
assert_eq!(style.border, expected_border); 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);
} }
} }
@@ -1034,7 +1018,10 @@ fn border_top() {
style: BorderStyle::Thin, style: BorderStyle::Thin,
color: Some("#F2F2F2".to_string()), color: Some("#F2F2F2".to_string()),
}; };
assert_eq!(model._get_cell_actual_border("C4").top, Some(border_item)); assert_eq!(
model._get_cell_actual_border("C4").bottom,
Some(border_item)
);
model.undo().unwrap(); model.undo().unwrap();

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{UserModel, expressions::types::Area}; use crate::{expressions::types::Area, UserModel};
#[test] #[test]
fn basic() { fn basic() {

View File

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

View File

@@ -1,425 +0,0 @@
#![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())
);
}

View File

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

View File

@@ -1,16 +1,16 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT}, constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT},
test::util::new_empty_model, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]
fn send_queue() { fn send_queue() {
let mut model1 = UserModel::from_model(new_empty_model()); let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0; let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_columns_width(0, 3, 3, width).unwrap(); model1.set_column_width(0, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap(); model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
let send_queue = model1.flush_send_queue(); let send_queue = model1.flush_send_queue();
@@ -34,7 +34,7 @@ fn apply_external_diffs_wrong_str() {
fn queue_undo_redo() { fn queue_undo_redo() {
let mut model1 = UserModel::from_model(new_empty_model()); let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0; let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_columns_width(0, 3, 3, width).unwrap(); model1.set_column_width(0, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap(); model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
assert!(model1.undo().is_ok()); assert!(model1.undo().is_ok());
assert!(model1.redo().is_ok()); assert!(model1.redo().is_ok());
@@ -57,8 +57,8 @@ fn queue_undo_redo_multiple() {
// do a bunch of things // do a bunch of things
model1.set_frozen_columns_count(0, 5).unwrap(); model1.set_frozen_columns_count(0, 5).unwrap();
model1.set_frozen_rows_count(0, 6).unwrap(); model1.set_frozen_rows_count(0, 6).unwrap();
model1.set_columns_width(0, 7, 7, 300.0).unwrap(); model1.set_column_width(0, 7, 300.0).unwrap();
model1.set_rows_height(0, 23, 23, 123.0).unwrap(); model1.set_row_height(0, 23, 123.0).unwrap();
model1.set_user_input(0, 55, 55, "=42+8").unwrap(); model1.set_user_input(0, 55, 55, "=42+8").unwrap();
for row in 1..5 { for row in 1..5 {
@@ -157,9 +157,7 @@ fn new_sheet() {
#[test] #[test]
fn wrong_diffs_handled() { fn wrong_diffs_handled() {
let mut model = UserModel::from_model(new_empty_model()); let mut model = UserModel::from_model(new_empty_model());
assert!( assert!(model
model .apply_external_diffs("Hello world".as_bytes())
.apply_external_diffs("Hello world".as_bytes()) .is_err());
.is_err()
);
} }

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::types::CellType; use crate::types::CellType;
use crate::UserModel;
#[test] #[test]
fn set_user_input_errors() { fn set_user_input_errors() {
@@ -59,7 +59,7 @@ fn insert_remove_rows() {
// Insert some data in row 5 (and change the style) // Insert some data in row 5 (and change the style)
assert!(model.set_user_input(0, 5, 1, "100$").is_ok()); assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
// Change the height of the column // Change the height of the column
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok()); assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
// remove the row // remove the row
assert!(model.delete_row(0, 5).is_ok()); assert!(model.delete_row(0, 5).is_ok());
@@ -91,11 +91,12 @@ fn insert_remove_columns() {
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
// column E // column E
let column_width = model.get_column_width(0, 5).unwrap(); 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 // Insert some data in row 5 (and change the style) in E1
assert!(model.set_user_input(0, 1, 5, "100$").is_ok()); assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
// Change the width of the column // Change the width of the column
assert!(model.set_columns_width(0, 5, 5, 3.0 * column_width).is_ok()); assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width); assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
// remove the column // remove the column

View File

@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::UserModel;
#[test] #[test]
fn basic_tests() { fn basic_tests() {

View File

@@ -1,12 +1,12 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel,
constants::{ constants::{
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH,
LAST_COLUMN, LAST_COLUMN,
}, },
test::util::new_empty_model, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]
@@ -87,7 +87,7 @@ fn last_colum() {
fn page_down() { fn page_down() {
let model = new_empty_model(); let model = new_empty_model();
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
let window_height = DEFAULT_WINDOW_HEIGHT as f64; let window_height = DEFAULT_WINDOW_HEIGH as f64;
let row_height = DEFAULT_ROW_HEIGHT; let row_height = DEFAULT_ROW_HEIGHT;
let row_count = f64::floor(window_height / row_height) as i32; let row_count = f64::floor(window_height / row_height) as i32;
model.on_page_down().unwrap(); model.on_page_down().unwrap();

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH}, constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH},
test::util::new_empty_model, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH, LAST_COLUMN}, constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH, LAST_COLUMN},
test::util::new_empty_model, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]

View File

@@ -1,8 +1,8 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::types::Fill; use crate::types::Fill;
use crate::UserModel;
#[test] #[test]
fn simple_pasting() { fn simple_pasting() {

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{UserModel, expressions::types::Area}; use crate::{expressions::types::Area, UserModel};
#[test] #[test]
fn csv_paste() { fn csv_paste() {
@@ -99,7 +99,7 @@ fn cut_paste() {
// paste in cell D4 (4, 4) // paste in cell D4 (4, 4)
model model
.paste_from_clipboard(0, (1, 1, 2, 2), &copy.data, true) .paste_from_clipboard((1, 1, 2, 2), &copy.data, true)
.unwrap(); .unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string())); assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
@@ -119,26 +119,6 @@ fn cut_paste() {
assert_eq!(model.get_cell_content(0, 2, 2), Ok("".to_string())); 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), &copy.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] #[test]
fn copy_paste_internal() { fn copy_paste_internal() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
@@ -172,7 +152,7 @@ fn copy_paste_internal() {
// paste in cell D4 (4, 4) // paste in cell D4 (4, 4)
model model
.paste_from_clipboard(0, (1, 1, 2, 2), &copy.data, false) .paste_from_clipboard((1, 1, 2, 2), &copy.data, false)
.unwrap(); .unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string())); assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));

View File

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

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN}, constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
test::util::new_empty_model, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]
@@ -59,7 +59,7 @@ fn simple_delete_column() {
model.set_user_input(0, 1, 5, "3").unwrap(); model.set_user_input(0, 1, 5, "3").unwrap();
model.set_user_input(0, 2, 5, "=E1*2").unwrap(); model.set_user_input(0, 2, 5, "=E1*2").unwrap();
model model
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0) .set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
.unwrap(); .unwrap();
model.delete_column(0, 5).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_user_input(0, 15, 6, "=D15*2").unwrap();
model model
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0) .set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
.unwrap(); .unwrap();
model.delete_row(0, 15).unwrap(); model.delete_row(0, 15).unwrap();
@@ -172,42 +172,3 @@ fn row_heigh_increases_automatically() {
.unwrap(); .unwrap();
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT)); assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT));
} }
#[test]
fn insert_row_evaluates() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
assert!(model.insert_row(0, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.delete_row(0, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
}
#[test]
fn insert_column_evaluates() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
assert!(model.insert_column(0, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.delete_column(0, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{UserModel, test::util::new_empty_model}; use crate::{test::util::new_empty_model, UserModel};
#[test] #[test]
fn simple_undo_redo() { fn simple_undo_redo() {

View File

@@ -3,10 +3,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
UserModel,
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model, test::util::new_empty_model,
user_model::SelectedView, user_model::SelectedView,
UserModel,
}; };
#[test] #[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel, 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, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]
@@ -11,7 +11,7 @@ fn basic_test() {
let model = new_empty_model(); let model = new_empty_model();
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
let window_height = model.get_window_height().unwrap(); let window_height = model.get_window_height().unwrap();
assert_eq!(window_height, DEFAULT_WINDOW_HEIGHT); assert_eq!(window_height, DEFAULT_WINDOW_HEIGH);
let window_width = model.get_window_width().unwrap(); let window_width = model.get_window_width().unwrap();
assert_eq!(window_width, DEFAULT_WINDOW_WIDTH); assert_eq!(window_width, DEFAULT_WINDOW_WIDTH);

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{BorderArea, UserModel, expressions::types::Area, types::Border}; use crate::{expressions::types::Area, types::Border, BorderArea, UserModel};
impl UserModel { impl UserModel {
pub fn _set_cell_border(&mut self, cell: &str, color: &str) { pub fn _set_cell_border(&mut self, cell: &str, color: &str) {

View File

@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display}; use std::{collections::HashMap, fmt::Display};
use crate::expressions::token::Error; use crate::expressions::token::Error;
use crate::expressions::utils::number_to_column;
fn default_as_false() -> bool { fn default_as_false() -> bool {
false false
@@ -35,7 +36,7 @@ pub struct WorkbookView {
pub sheet: u32, pub sheet: u32,
/// The current width of the window /// The current width of the window
pub window_width: i64, pub window_width: i64,
/// The current height of the window /// The current heigh of the window
pub window_height: i64, pub window_height: i64,
} }
@@ -110,7 +111,7 @@ pub struct Worksheet {
pub sheet_id: u32, pub sheet_id: u32,
pub state: SheetState, pub state: SheetState,
pub color: Option<String>, pub color: Option<String>,
pub merge_cells: Vec<String>, pub merged_cells_list: Vec<MergedCells>,
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
pub frozen_rows: i32, pub frozen_rows: i32,
pub frozen_columns: i32, pub frozen_columns: i32,
@@ -323,19 +324,6 @@ pub struct Style {
pub quote_prefix: bool, 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)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct NumFmt { pub struct NumFmt {
pub num_fmt_id: i32, pub num_fmt_id: i32,
@@ -364,6 +352,43 @@ pub enum FontScheme {
None, None,
} }
// MergedCells type
// There will be one MergedCells struct maintained for every Merged cells that we load
// merge_cell_range : Its tuple having [row_start, column_start, row_end, column_end]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct MergedCells(pub i32, pub i32, pub i32, pub i32);
// implementing accessor function
impl MergedCells {
// Method which returns range_ref from the tuple
// ex : (3,1,4,2) is interpreted as A3:B4
pub fn get_merged_cells_str_ref(&self) -> Result<String, String> {
let start_column = number_to_column(self.1).ok_or(format!(
"Error while converting column start {} number to column string ref",
self.1
))?;
let end_column = number_to_column(self.3).ok_or(format!(
"Error while converting column end {} number to column string ref",
self.3
))?;
return Ok(start_column
+ &self.0.to_string()
+ &":".to_string()
+ &end_column
+ &self.2.to_string());
}
// Only Public function where Merge cell can be created
pub fn new(merge_cell_parsed_range: (i32, i32, i32, i32)) -> Self {
Self(
merge_cell_parsed_range.0,
merge_cell_parsed_range.1,
merge_cell_parsed_range.2,
merge_cell_parsed_range.3,
)
}
}
impl Display for FontScheme { impl Display for FontScheme {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {

View File

@@ -50,9 +50,8 @@ impl Units {
fn get_units_from_format_string(num_fmt: &str) -> Option<Units> { fn get_units_from_format_string(num_fmt: &str) -> Option<Units> {
let mut parser = Parser::new(num_fmt); let mut parser = Parser::new(num_fmt);
parser.parse(); parser.parse();
let parts = parser.parts.first()?;
// We only care about the first part (positive number) // We only care about the first part (positive number)
match parts { match &parser.parts[0] {
ParsePart::Number(part) => { ParsePart::Number(part) => {
if part.percent > 0 { if part.percent > 0 {
Some(Units::Percentage { Some(Units::Percentage {
@@ -294,9 +293,7 @@ impl Model {
Node::EmptyArgKind => None, Node::EmptyArgKind => None,
Node::InvalidFunctionKind { .. } => None, Node::InvalidFunctionKind { .. } => None,
Node::ArrayKind(_) => None, Node::ArrayKind(_) => None,
Node::DefinedNameKind(_) => None, Node::VariableKind(_) => None,
Node::TableNameKind(_) => None,
Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None, Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None, Node::OpPowerKind { .. } => None,
} }
@@ -312,7 +309,6 @@ impl Model {
Function::Sum => self.units_fn_sum_like(args, cell), Function::Sum => self.units_fn_sum_like(args, cell),
Function::Average => 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::Pmt => self.units_fn_currency(args, cell),
Function::Fv => self.units_fn_currency(args, cell),
Function::Nper => self.units_fn_currency(args, cell), Function::Nper => self.units_fn_currency(args, cell),
Function::Npv => self.units_fn_currency(args, cell), Function::Npv => self.units_fn_currency(args, cell),
Function::Irr => self.units_fn_percentage(args, cell), Function::Irr => self.units_fn_percentage(args, cell),

View File

@@ -1,507 +0,0 @@
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
expressions::types::Area,
};
use super::{
BorderArea, UserModel, border_utils::is_max_border, common::BorderType, history::Diff,
};
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(())
}
}

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