Compare commits

..

3 Commits

Author SHA1 Message Date
Nicolás Hatcher
ae661f8e93 WIP 2024-12-30 11:46:46 +01:00
francisco aloi
e5265ce3ad refactored NameManager logic for new design 2024-12-29 18:12:00 +01:00
francisco aloi
17ba7a5f84 NamedRanges changes 2024-12-28 12:32:40 +01:00
451 changed files with 11223 additions and 36334 deletions

View File

@@ -1,443 +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: |
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
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: |
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target x86_64-unknown-linux-musl
- host: macos-latest
target: aarch64-apple-darwin
build: |
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
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: |
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
rustup target add aarch64-unknown-linux-gnu &&
yarn build --target aarch64-unknown-linux-gnu
- host: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
setup: |
sudo apt-get update
sudo apt-get install -y gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
build: |
set -e
rustup toolchain install 1.90.0
rustup default 1.90.0
export CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
export AR_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-ar
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
export PKG_CONFIG_ALLOW_CROSS=1
yarn build --target armv7-unknown-linux-gnueabihf
# - host: ubuntu-latest
# target: armv7-unknown-linux-musleabihf
# build: |
# set -e
# rustup toolchain install 1.90.0
# rustup default 1.90.0
# # Use Zig as the MUSL cross C toolchain
# export CC_armv7_unknown_linux_musleabihf="zig cc -target armv7-linux-musleabihf -mfpu=vfpv3-d16 -mfloat-abi=hard"
# export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="zig cc -target armv7-linux-musleabihf -mfpu=vfpv3-d16 -mfloat-abi=hard"
# export AR_armv7_unknown_linux_musleabihf="zig ar"
# export PKG_CONFIG_ALLOW_CROSS=1
# yarn build --target armv7-unknown-linux-musleabihf
- host: ubuntu-latest
target: aarch64-linux-android
build: |
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target aarch64-linux-android
- host: ubuntu-latest
target: armv7-linux-androideabi
build: |
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
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 toolchain install 1.90.0 &&
rustup default 1.90.0 &&
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: 1.90.0
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:
- '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:
- '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:
- '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:
- '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
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
- 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,141 +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@v4
with:
name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
working-directory: bindings/python
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist
macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
working-directory: bindings/python
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
working-directory: bindings/python
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-${{ runner.os }}-sdist
path: bindings/python/dist
publish-to-test-pypi:
if: ${{ github.event.inputs.release != 'true' }}
name: >-
Publish Python 🐍 distribution 📦 to Test PyPI
runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
with:
path: bindings/python/
- name: Publish distribution 📦 to Test PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TEST_API_TOKEN }}
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
with:
command: upload
args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python
publish-pypi:
if: ${{ github.event.inputs.release == 'true' }}
name: >-
Publish Python 🐍 distribution 📦 to PyPI
runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
with:
path: bindings/python/
- name: Publish distribution 📦 to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
with:
command: upload
args: "--skip-existing **/*.whl **/*.tar.gz"
working-directory: bindings/python

3
.gitignore vendored
View File

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

View File

@@ -7,21 +7,12 @@
- 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 - 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)

216
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.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@@ -286,22 +271,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ctor"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
dependencies = [
"ctor-proc-macro",
"dtor",
]
[[package]]
name = "ctor-proc-macro"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -322,21 +291,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "dtor"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934"
dependencies = [
"dtor-proc-macro",
]
[[package]]
name = "dtor-proc-macro"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.10.0"
@@ -435,7 +389,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.6.0" version = "0.2.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -451,13 +405,14 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.2.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"csv", "csv",
"js-sys", "js-sys",
"once_cell",
"rand", "rand",
"regex", "regex",
"regex-lite", "regex-lite",
@@ -466,17 +421,6 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "ironcalc_nodejs"
version = "0.6.0"
dependencies = [
"ironcalc",
"napi",
"napi-build",
"napi-derive",
"serde",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@@ -516,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"
@@ -556,70 +490,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "napi"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
dependencies = [
"bitflags",
"ctor",
"napi-build",
"napi-sys",
"nohash-hasher",
"rustc-hash",
"serde",
"serde_json",
]
[[package]]
name = "napi-build"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
[[package]]
name = "napi-derive"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
dependencies = [
"convert_case",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
dependencies = [
"libloading",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -746,10 +616,11 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.25.0" 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 = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4" checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
dependencies = [ dependencies = [
"cfg-if",
"indoc", "indoc",
"libc", "libc",
"memoffset", "memoffset",
@@ -763,9 +634,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.25.0" 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 = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d" checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@@ -773,9 +644,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.25.0" 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 = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e" checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@@ -783,9 +654,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.25.0" 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 = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214" checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -795,9 +666,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.25.0" 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 = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e" checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -808,9 +679,8 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.6.0" version = "0.1.2"
dependencies = [ dependencies = [
"bitcode",
"ironcalc", "ironcalc",
"pyo3", "pyo3",
"serde", "serde",
@@ -897,18 +767,6 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.17" version = "1.0.17"
@@ -921,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"
@@ -1016,9 +868,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.13.2" version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
@@ -1071,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"
@@ -1107,7 +953,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.6.0" version = "0.1.3"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",
@@ -1118,24 +964,23 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -1156,9 +1001,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1166,9 +1011,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1179,12 +1024,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"

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

@@ -1,93 +0,0 @@
FROM rust:latest AS builder
WORKDIR /app
COPY . .
# Tools + wasm toolchain + Node via nvm
RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl ca-certificates make \
&& rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& bash -lc "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash -" \
&& bash -lc '\
export NVM_DIR="$HOME/.nvm" && \
source "$NVM_DIR/nvm.sh" && \
nvm install 22 && nvm alias default 22 && \
nroot="$NVM_DIR/versions/node/$(nvm version default)/bin" && \
ln -sf "$nroot/node" /usr/local/bin/node && \
ln -sf "$nroot/npm" /usr/local/bin/npm && \
ln -sf "$nroot/npx" /usr/local/bin/npx \
' \
&& npm install typescript \
&& rm -rf /var/lib/apt/lists/*
# build the server
RUN cargo build --release --manifest-path webapp/app.ironcalc.com/server/Cargo.toml
# build the wasm
RUN make -C bindings/wasm
# build the widget
WORKDIR /app/webapp/IronCalc
RUN npm install && npm run build
# build the frontend app
WORKDIR /app/webapp/app.ironcalc.com/frontend
RUN npm install && npm run build
# build the xlsx_2_icalc binary (we don't need the release version here)
WORKDIR /app/xlsx
RUN cargo build
WORKDIR /app
# copy the artifacts to a dist/ directory
RUN mkdir dist
RUN mkdir dist/frontend
RUN cp -r webapp/app.ironcalc.com/frontend/dist/* dist/frontend/
RUN mkdir dist/server
RUN cp webapp/app.ironcalc.com/server/target/release/ironcalc_server dist/server/
RUN cp webapp/app.ironcalc.com/server/Rocket.toml dist/server/
RUN cp webapp/app.ironcalc.com/server/ironcalc.sqlite dist/server/
# Create ic files in docs
RUN mkdir -p dist/frontend/models
# Loop over all xlsx files in xlsx/tests/docs & templates and convert them to .ic
RUN bash -lc 'set -euo pipefail; \
mkdir -p dist/frontend/models; \
shopt -s nullglob; \
for xlsx_file in xlsx/tests/docs/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done; \
for xlsx_file in xlsx/tests/templates/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done'
# ---------- server runtime ----------
FROM debian:bookworm-slim AS server-runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy EVERYTHING you put in dist/server (binary + Rocket.toml + DB)
COPY --from=builder /app/dist/server/ ./
# Make sure Rocket binds to the container IP; explicitly point to the config file
ENV ROCKET_ADDRESS=0.0.0.0 \
ROCKET_PORT=8000 \
ROCKET_CONFIG=/app/Rocket.toml
EXPOSE 8000
# Run from /app so relative paths in Rocket.toml/DB work
CMD ["./ironcalc_server"]
# ---------- caddy runtime (serves frontend + reverse-proxy /api) ----------
FROM caddy:latest AS caddy-runtime
WORKDIR /srv
# Copy the frontend build output to /srv
COPY --from=builder /app/dist/frontend/ /srv/
# Copy the Caddyfile
COPY --from=builder /app/webapp/app.ironcalc.com/Caddyfile.compose /etc/caddy/Caddyfile

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:
@@ -16,7 +15,7 @@ tests: lint
# 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

@@ -31,17 +31,7 @@ This repository contains the main engine and the xlsx reader and writer.
Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go. Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go.
We will build different _skins_: in the terminal, as a desktop application or use it in your own web application. We will build different _skins_: in the terminal, as a desktop application or use it in you own web application.
# Docker
If you have docker installed just run:
```bash
docker compose up --build
```
head over to <http://localhost:2080> to test the application.
# Building # Building
@@ -87,14 +77,14 @@ And visit <http://0.0.0.0:8000/ironcalc/>
Add the dependency to `Cargo.toml`: Add the dependency to `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.5"} ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"}
``` ```
And then use this code in `main.rs`: And then use this code in `main.rs`:
```rust ```rust
use ironcalc::{ use ironcalc::{
base::{expressions::utils::number_to_column, Model}, base::{expressions::utils::number_to_column, model::Model},
export::save_to_xlsx, export::save_to_xlsx,
}; };

View File

@@ -1,11 +1,11 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.2.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021" edition = "2021"
homepage = "https://www.ironcalc.com" homepage = "https://www.ironcalc.com"
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
@@ -17,6 +17,7 @@ chrono = "0.4"
chrono-tz = "0.10" chrono-tz = "0.10"
regex = { version = "1.0", optional = true} regex = { version = "1.0", optional = true}
regex-lite = { version = "0.1.6", optional = true} regex-lite = { version = "0.1.6", optional = true}
once_cell = "1.16.0"
bitcode = "0.6.3" bitcode = "0.6.3"
csv = "1.3.0" csv = "1.3.0"

View File

@@ -1,6 +1,5 @@
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData}; use crate::expressions::parser::stringify::DisplaceData;
use crate::expressions::types::CellReferenceRC;
use crate::model::Model; use crate::model::Model;
// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns // NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
@@ -9,45 +8,16 @@ use crate::model::Model;
// I feel this is unimportant for now. // I feel this is unimportant for now.
impl Model { impl Model {
fn shift_cell_formula(
&mut self,
sheet: u32,
row: i32,
column: i32,
displace_data: &DisplaceData,
) -> Result<(), String> {
if let Some(f) = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.and_then(|c| c.get_formula())
{
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row,
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
if formula != formula_displaced {
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
}
}
Ok(())
}
/// This function iterates over all cells in the model and shifts their formulas according to the displacement data. /// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude. /// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> { fn displace_cells(&mut self, displace_data: &DisplaceData) {
let cells = self.get_all_cells(); let cells = self.get_all_cells();
for cell in cells { for cell in cells {
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?; self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data);
} }
Ok(())
} }
/// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order. /// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
@@ -164,34 +134,7 @@ impl Model {
column, column,
delta: column_count, delta: column_count,
}), }),
)?; );
// In the list of columns:
// * Keep all the columns to the left
// * Displace all the columns to the right
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_columns = Vec::new();
for col in worksheet.cols.iter_mut() {
// range under study
let min = col.min;
let max = col.max;
if column > max {
// If the range under study is to our left, this is a noop
} else if column <= min {
// If the range under study is to our right, we displace it
col.min = min + column_count;
col.max = max + column_count;
} else {
// If the range under study is in the middle we augment it
col.max = max + column_count;
}
new_columns.push(col.clone());
}
// TODO: If in a row the cell to the right and left have the same style we should copy it
worksheet.cols = new_columns;
Ok(()) Ok(())
} }
@@ -212,12 +155,6 @@ impl Model {
if column_count <= 0 { if column_count <= 0 {
return Err("Please use insert columns instead".to_string()); return Err("Please use insert columns instead".to_string());
} }
if !(1..=LAST_COLUMN).contains(&column) {
return Err(format!("Column number '{column}' is not valid."));
}
if column + column_count - 1 > LAST_COLUMN {
return Err("Cannot delete columns beyond the last column of the sheet".to_string());
}
// first column being deleted // first column being deleted
let column_start = column; let column_start = column;
@@ -250,7 +187,7 @@ impl Model {
column, column,
delta: -column_count, delta: -column_count,
}), }),
)?; );
let worksheet = &mut self.workbook.worksheet_mut(sheet)?; let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
// deletes all the column styles // deletes all the column styles
@@ -374,7 +311,7 @@ impl Model {
row, row,
delta: row_count, delta: row_count,
}), }),
)?; );
Ok(()) Ok(())
} }
@@ -390,13 +327,6 @@ impl Model {
if row_count <= 0 { if row_count <= 0 {
return Err("Please use insert rows instead".to_string()); return Err("Please use insert rows instead".to_string());
} }
if !(1..=LAST_ROW).contains(&row) {
return Err(format!("Row number '{row}' is not valid."));
}
if row + row_count - 1 > LAST_ROW {
return Err("Cannot delete rows beyond the last row of the sheet".to_string());
}
// Move cells // Move cells
let worksheet = &self.workbook.worksheet(sheet)?; let worksheet = &self.workbook.worksheet(sheet)?;
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect(); let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
@@ -442,7 +372,7 @@ impl Model {
row, row,
delta: -row_count, delta: -row_count,
}), }),
)?; );
Ok(()) Ok(())
} }
@@ -457,86 +387,23 @@ impl Model {
/// * Column is one of the extremes of the range. The new extreme would be target_column. /// * Column is one of the extremes of the range. The new extreme would be target_column.
/// Range is then normalized /// Range is then normalized
/// * Any other case, range is left unchanged. /// * Any other case, range is left unchanged.
/// NOTE: This moves the data and column styles along with the formulas /// NOTE: This does NOT move the data in the columns or move the colum styles
pub fn move_column_action( pub fn move_column_action(
&mut self, &mut self,
sheet: u32, sheet: u32,
column: i32, column: i32,
delta: i32, delta: i32,
) -> Result<(), String> { ) -> Result<(), &'static str> {
// Check boundaries // Check boundaries
let target_column = column + delta; let target_column = column + delta;
if !(1..=LAST_COLUMN).contains(&target_column) { if !(1..=LAST_COLUMN).contains(&target_column) {
return Err("Target column out of boundaries".to_string()); return Err("Target column out of boundaries");
} }
if !(1..=LAST_COLUMN).contains(&column) { if !(1..=LAST_COLUMN).contains(&column) {
return Err("Initial column out of boundaries".to_string()); return Err("Initial column out of boundaries");
} }
if delta == 0 { // TODO: Add the actual displacement of data and styles
return Ok(());
}
// Preserve cell contents, width and style of the column being moved
let original_refs = self
.workbook
.worksheet(sheet)?
.column_cell_references(column)?;
let mut original_cells = Vec::new();
for r in &original_refs {
let cell = self
.workbook
.worksheet(sheet)?
.cell(r.row, column)
.ok_or("Expected Cell to exist")?;
let style_idx = cell.get_style();
let formula_or_value = self
.get_cell_formula(sheet, r.row, column)?
.unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language));
original_cells.push((r.row, formula_or_value, style_idx));
self.cell_clear_all(sheet, r.row, column)?;
}
let width = self.workbook.worksheet(sheet)?.get_column_width(column)?;
let style = self.workbook.worksheet(sheet)?.get_column_style(column)?;
if delta > 0 {
for c in column + 1..=target_column {
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
for r in refs {
self.move_cell(sheet, r.row, c, r.row, c - 1)?;
}
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(c - 1, w, s)?;
}
} else {
for c in (target_column..=column - 1).rev() {
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
for r in refs {
self.move_cell(sheet, r.row, c, r.row, c + 1)?;
}
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(c + 1, w, s)?;
}
}
for (r, value, style_idx) in original_cells {
self.set_user_input(sheet, r, target_column, value)?;
self.workbook
.worksheet_mut(sheet)?
.set_cell_style(r, target_column, style_idx)?;
}
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(target_column, width, style)?;
// Update all formulas in the workbook // Update all formulas in the workbook
self.displace_cells( self.displace_cells(
@@ -545,91 +412,7 @@ impl Model {
column, column,
delta, delta,
}), }),
)?; );
Ok(())
}
/// Displaces cells due to a move row action
/// from initial_row to target_row = initial_row + row_delta
/// References will be updated following the same rules as move_column_action
/// NOTE: This moves the data and row styles along with the formulas
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
// Check boundaries
let target_row = row + delta;
if !(1..=LAST_ROW).contains(&target_row) {
return Err("Target row out of boundaries".to_string());
}
if !(1..=LAST_ROW).contains(&row) {
return Err("Initial row out of boundaries".to_string());
}
if delta == 0 {
return Ok(());
}
let original_cols = self.get_columns_for_row(sheet, row, false)?;
let mut original_cells = Vec::new();
for c in &original_cols {
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, *c)
.ok_or("Expected Cell to exist")?;
let style_idx = cell.get_style();
let formula_or_value = self
.get_cell_formula(sheet, row, *c)?
.unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language));
original_cells.push((*c, formula_or_value, style_idx));
self.cell_clear_all(sheet, row, *c)?;
}
if delta > 0 {
for r in row + 1..=target_row {
let cols = self.get_columns_for_row(sheet, r, false)?;
for c in cols {
self.move_cell(sheet, r, c, r - 1, c)?;
}
}
} else {
for r in (target_row..=row - 1).rev() {
let cols = self.get_columns_for_row(sheet, r, false)?;
for c in cols {
self.move_cell(sheet, r, c, r + 1, c)?;
}
}
}
for (c, value, style_idx) in original_cells {
self.set_user_input(sheet, target_row, c, value)?;
self.workbook
.worksheet_mut(sheet)?
.set_cell_style(target_row, c, style_idx)?;
}
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_rows = Vec::new();
for r in worksheet.rows.iter() {
if r.r == row {
let mut nr = r.clone();
nr.r = target_row;
new_rows.push(nr);
} else if delta > 0 && r.r > row && r.r <= target_row {
let mut nr = r.clone();
nr.r -= 1;
new_rows.push(nr);
} else if delta < 0 && r.r < row && r.r >= target_row {
let mut nr = r.clone();
nr.r += 1;
new_rows.push(nr);
} else {
new_rows.push(r.clone());
}
}
worksheet.rows = new_rows;
// Update all formulas in the workbook
self.displace_cells(&(DisplaceData::RowMove { sheet, row, delta }))?;
Ok(()) Ok(())
} }

View File

@@ -1,158 +0,0 @@
use crate::{
calc_result::CalcResult,
cast::NumberOrArray,
expressions::{
parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
model::Model,
};
/// Unify how we map booleans/strings to f64
fn to_f64(value: &ArrayNode) -> Result<f64, Error> {
match value {
ArrayNode::Number(f) => Ok(*f),
ArrayNode::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
ArrayNode::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(f),
Err(_) => Err(Error::VALUE),
},
ArrayNode::Error(err) => Err(err.clone()),
}
}
impl Model {
/// Applies `op` elementwise for arrays/numbers.
pub(crate) fn handle_arithmetic(
&mut self,
left: &Node,
right: &Node,
cell: CellReferenceIndex,
op: &dyn Fn(f64, f64) -> Result<f64, Error>,
) -> CalcResult {
let l = match self.get_number_or_array(left, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number_or_array(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
match (l, r) {
// -----------------------------------------------------
// Case 1: Both are numbers
// -----------------------------------------------------
(NumberOrArray::Number(f1), NumberOrArray::Number(f2)) => match op(f1, f2) {
Ok(x) => CalcResult::Number(x),
Err(Error::DIV) => CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Divide by 0".to_string(),
},
Err(Error::VALUE) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid number".to_string(),
},
Err(e) => CalcResult::Error {
error: e,
origin: cell,
message: "Unknown error".to_string(),
},
},
// -----------------------------------------------------
// Case 2: left is Number, right is Array
// -----------------------------------------------------
(NumberOrArray::Number(f1), NumberOrArray::Array(a2)) => {
let mut array = Vec::new();
for row in a2 {
let mut data_row = Vec::new();
for node in row {
match to_f64(&node) {
Ok(f2) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
Err(e) => data_row.push(ArrayNode::Error(e)),
},
Err(err) => data_row.push(ArrayNode::Error(err)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------------------
// Case 3: left is Array, right is Number
// -----------------------------------------------------
(NumberOrArray::Array(a1), NumberOrArray::Number(f2)) => {
let mut array = Vec::new();
for row in a1 {
let mut data_row = Vec::new();
for node in row {
match to_f64(&node) {
Ok(f1) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
Err(e) => data_row.push(ArrayNode::Error(e)),
},
Err(err) => data_row.push(ArrayNode::Error(err)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------------------
// Case 4: Both are arrays
// -----------------------------------------------------
(NumberOrArray::Array(a1), NumberOrArray::Array(a2)) => {
let n1 = a1.len();
let m1 = a1.first().map(|r| r.len()).unwrap_or(0);
let n2 = a2.len();
let m2 = a2.first().map(|r| r.len()).unwrap_or(0);
let n = n1.max(n2);
let m = m1.max(m2);
let mut array = Vec::new();
for i in 0..n {
let row1 = a1.get(i);
let row2 = a2.get(i);
let mut data_row = Vec::new();
for j in 0..m {
let val1 = row1.and_then(|r| r.get(j));
let val2 = row2.and_then(|r| r.get(j));
match (val1, val2) {
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
(Ok(f1), Ok(f2)) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
},
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),
},
// Mismatched dimensions => #VALUE!
_ => data_row.push(ArrayNode::Error(Error::VALUE)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::expressions::{parser::ArrayNode, token::Error, types::CellReferenceIndex}; use crate::expressions::{token::Error, types::CellReferenceIndex};
#[derive(Clone)] #[derive(Clone)]
pub struct Range { pub struct Range {
@@ -24,7 +24,6 @@ pub(crate) enum CalcResult {
}, },
EmptyCell, EmptyCell,
EmptyArg, EmptyArg,
Array(Vec<Vec<ArrayNode>>),
} }
impl CalcResult { impl CalcResult {

View File

@@ -1,85 +1,11 @@
use crate::{ use crate::{
calc_result::{CalcResult, Range}, calc_result::{CalcResult, Range},
expressions::{ expressions::{parser::Node, token::Error, types::CellReferenceIndex},
parser::{ArrayNode, Node}, implicit_intersection::implicit_intersection,
token::Error,
types::CellReferenceIndex,
},
model::Model, model::Model,
}; };
pub(crate) enum NumberOrArray {
Number(f64),
Array(Vec<Vec<ArrayNode>>),
}
impl Model { impl Model {
pub(crate) fn get_number_or_array(
&mut self,
node: &Node,
cell: CellReferenceIndex,
) -> Result<NumberOrArray, CalcResult> {
match self.evaluate_node_in_context(node, cell) {
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
CalcResult::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(NumberOrArray::Number(f)),
_ => Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expecting number".to_string(),
)),
},
CalcResult::Boolean(f) => {
if f {
Ok(NumberOrArray::Number(1.0))
} else {
Ok(NumberOrArray::Number(0.0))
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(NumberOrArray::Number(0.0)),
CalcResult::Range { left, right } => {
let sheet = left.sheet;
if sheet != right.sheet {
return Err(CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "3D ranges are not allowed".to_string(),
});
}
// we need to convert the range into an array
let mut array = Vec::new();
for row in left.row..=right.row {
let mut row_data = Vec::new();
for column in left.column..=right.column {
let value =
match self.evaluate_cell(CellReferenceIndex { sheet, column, row }) {
CalcResult::String(s) => ArrayNode::String(s),
CalcResult::Number(f) => ArrayNode::Number(f),
CalcResult::Boolean(b) => ArrayNode::Boolean(b),
CalcResult::Error { error, .. } => ArrayNode::Error(error),
CalcResult::Range { .. } => {
// if we do things right this can never happen.
// the evaluation of a cell should never return a range
ArrayNode::Number(0.0)
}
CalcResult::EmptyCell => ArrayNode::Number(0.0),
CalcResult::EmptyArg => ArrayNode::Number(0.0),
CalcResult::Array(_) => {
// if we do things right this can never happen.
// the evaluation of a cell should never return an array
ArrayNode::Number(0.0)
}
};
row_data.push(value);
}
array.push(row_data);
}
Ok(NumberOrArray::Array(array))
}
CalcResult::Array(s) => Ok(NumberOrArray::Array(s)),
error @ CalcResult::Error { .. } => Err(error),
}
}
pub(crate) fn get_number( pub(crate) fn get_number(
&mut self, &mut self,
node: &Node, node: &Node,
@@ -113,16 +39,19 @@ impl Model {
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { .. } => Err(CalcResult::Error { CalcResult::Range { left, right } => {
error: Error::NIMPL, match implicit_intersection(&cell, &Range { left, right }) {
origin: cell, Some(cell_reference) => {
message: "Arrays not supported yet".to_string(), let result = self.evaluate_cell(cell_reference);
}), self.cast_to_number(result, cell_reference)
CalcResult::Array(_) => Err(CalcResult::Error { }
error: Error::NIMPL, None => Err(CalcResult::Error {
origin: cell, error: Error::VALUE,
message: "Arrays not supported yet".to_string(), origin: cell,
}), message: "Invalid reference (number)".to_string(),
}),
}
}
} }
} }
@@ -159,7 +88,7 @@ impl Model {
// FIXME: I think when casting a number we should convert it to_precision(x, 15) // FIXME: I think when casting a number we should convert it to_precision(x, 15)
// See function Exact // See function Exact
match result { match result {
CalcResult::Number(f) => Ok(format!("{f}")), CalcResult::Number(f) => Ok(format!("{}", f)),
CalcResult::String(s) => Ok(s), CalcResult::String(s) => Ok(s),
CalcResult::Boolean(f) => { CalcResult::Boolean(f) => {
if f { if f {
@@ -170,16 +99,19 @@ impl Model {
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { .. } => Err(CalcResult::Error { CalcResult::Range { left, right } => {
error: Error::NIMPL, match implicit_intersection(&cell, &Range { left, right }) {
origin: cell, Some(cell_reference) => {
message: "Arrays not supported yet".to_string(), let result = self.evaluate_cell(cell_reference);
}), self.cast_to_string(result, cell_reference)
CalcResult::Array(_) => Err(CalcResult::Error { }
error: Error::NIMPL, None => Err(CalcResult::Error {
origin: cell, error: Error::VALUE,
message: "Arrays not supported yet".to_string(), origin: cell,
}), message: "Invalid reference (string)".to_string(),
}),
}
}
} }
} }
@@ -219,16 +151,19 @@ impl Model {
CalcResult::Boolean(b) => Ok(b), CalcResult::Boolean(b) => Ok(b),
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { .. } => Err(CalcResult::Error { CalcResult::Range { left, right } => {
error: Error::NIMPL, match implicit_intersection(&cell, &Range { left, right }) {
origin: cell, Some(cell_reference) => {
message: "Arrays not supported yet".to_string(), let result = self.evaluate_cell(cell_reference);
}), self.cast_to_bool(result, cell_reference)
CalcResult::Array(_) => Err(CalcResult::Error { }
error: Error::NIMPL, None => Err(CalcResult::Error {
origin: cell, error: Error::VALUE,
message: "Arrays not supported yet".to_string(), origin: cell,
}), message: "Invalid reference (bool)".to_string(),
}),
}
}
} }
} }

View File

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

138
base/src/diffs.rs Normal file
View File

@@ -0,0 +1,138 @@
use crate::{
expressions::{
parser::{
move_formula::ref_is_in_area,
stringify::{to_string, to_string_displaced, DisplaceData},
walk::forward_references,
},
types::{Area, CellReferenceIndex, CellReferenceRC},
},
model::Model,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged, deny_unknown_fields)]
pub enum CellValue {
Value(String),
None,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SetCellValue {
cell: CellReferenceIndex,
new_value: CellValue,
old_value: CellValue,
}
impl Model {
#[allow(clippy::expect_used)]
pub(crate) fn shift_cell_formula(
&mut self,
sheet: u32,
row: i32,
column: i32,
displace_data: &DisplaceData,
) {
if let Some(f) = self
.workbook
.worksheet(sheet)
.expect("Worksheet must exist")
.cell(row, column)
.expect("Cell must exist")
.get_formula()
{
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row,
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
if formula != formula_displaced {
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))
.expect("Failed to shift cell formula");
}
}
}
#[allow(clippy::expect_used)]
pub fn forward_references(
&mut self,
source_area: &Area,
target: &CellReferenceIndex,
) -> Result<Vec<SetCellValue>, String> {
let mut diff_list: Vec<SetCellValue> = Vec::new();
let target_area = &Area {
sheet: target.sheet,
row: target.row,
column: target.column,
width: source_area.width,
height: source_area.height,
};
// Walk over every formula
let cells = self.get_all_cells();
for cell in cells {
if let Some(f) = self
.workbook
.worksheet(cell.index)
.expect("Worksheet must exist")
.cell(cell.row, cell.column)
.expect("Cell must exist")
.get_formula()
{
let sheet = cell.index;
let row = cell.row;
let column = cell.column;
// If cell is in the source or target area, skip
if ref_is_in_area(sheet, row, column, source_area)
|| ref_is_in_area(sheet, row, column, target_area)
{
continue;
}
// Get the formula
// Get a copy of the AST
let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
column: cell.column,
row: cell.row,
};
let context = CellReferenceIndex { sheet, column, row };
let formula = to_string(node, &cell_reference);
let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
forward_references(
node,
&context,
source_area,
target.sheet,
target_sheet_name,
target.row,
target.column,
);
// If the string representation of the formula has changed update the cell
let updated_formula = to_string(node, &cell_reference);
if formula != updated_formula {
self.update_cell_with_formula(
sheet,
row,
column,
format!("={updated_formula}"),
)?;
// Update the diff list
diff_list.push(SetCellValue {
cell: CellReferenceIndex { sheet, column, row },
new_value: CellValue::Value(format!("={}", updated_formula)),
old_value: CellValue::Value(format!("={}", formula)),
});
}
}
}
Ok(diff_list)
}
}

View File

@@ -142,7 +142,7 @@ impl Lexer {
pub fn expect(&mut self, tk: TokenType) -> Result<()> { pub fn expect(&mut self, tk: TokenType) -> Result<()> {
let nt = self.next_token(); let nt = self.next_token();
if mem::discriminant(&nt) != mem::discriminant(&tk) { if mem::discriminant(&nt) != mem::discriminant(&tk) {
return Err(self.set_error(&format!("Error, expected {tk:?}"), self.position)); return Err(self.set_error(&format!("Error, expected {:?}", tk), self.position));
} }
Ok(()) Ok(())
} }
@@ -187,7 +187,6 @@ impl Lexer {
']' => TokenType::RightBracket, ']' => TokenType::RightBracket,
':' => TokenType::Colon, ':' => TokenType::Colon,
';' => TokenType::Semicolon, ';' => TokenType::Semicolon,
'@' => TokenType::At,
',' => { ',' => {
if self.locale.numbers.symbols.decimal == "," { if self.locale.numbers.symbols.decimal == "," {
match self.consume_number(',') { match self.consume_number(',') {
@@ -314,9 +313,6 @@ impl Lexer {
} else if name_upper == self.language.booleans.r#false { } else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false); return TokenType::Boolean(false);
} }
if self.peek_char() == Some('(') {
return TokenType::Ident(name);
}
if self.mode == LexerMode::A1 { if self.mode == LexerMode::A1 {
let parsed_reference = utils::parse_reference_a1(&name_upper); let parsed_reference = utils::parse_reference_a1(&name_upper);
if parsed_reference.is_some() if parsed_reference.is_some()
@@ -514,7 +510,7 @@ impl Lexer {
self.position = position; self.position = position;
chars.parse::<i32>().map_err(|_| LexerError { chars.parse::<i32>().map_err(|_| LexerError {
position, position,
message: format!("Failed to parse to int: {chars}"), message: format!("Failed to parse to int: {}", chars),
}) })
} }
@@ -575,7 +571,9 @@ impl Lexer {
} }
self.position = position; self.position = position;
match chars.parse::<f64>() { match chars.parse::<f64>() {
Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)), Err(_) => {
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
}
Ok(v) => Ok(v), Ok(v) => Ok(v),
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::expressions::utils::column_to_number;
use crate::language::get_language; use crate::language::get_language;
use crate::locale::get_locale; use crate::locale::get_locale;
@@ -686,29 +685,3 @@ fn test_comparisons() {
assert_eq!(lx.next_token(), Number(7.0)); assert_eq!(lx.next_token(), Number(7.0));
assert_eq!(lx.next_token(), EOF); assert_eq!(lx.next_token(), EOF);
} }
#[test]
fn test_log10_is_cell_reference() {
let mut lx = new_lexer("LOG10", true);
assert_eq!(
lx.next_token(),
Reference {
sheet: None,
column: column_to_number("LOG").unwrap(),
row: 10,
absolute_column: false,
absolute_row: false,
}
);
assert_eq!(lx.next_token(), EOF);
}
#[test]
fn test_log10_is_function() {
let mut lx = new_lexer("LOG10(100)", true);
assert_eq!(lx.next_token(), Ident("LOG10".to_string()));
assert_eq!(lx.next_token(), LeftParenthesis);
assert_eq!(lx.next_token(), Number(100.0));
assert_eq!(lx.next_token(), RightParenthesis);
assert_eq!(lx.next_token(), EOF);
}

View File

@@ -1,25 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::expressions::{
lexer::{Lexer, LexerMode},
token::TokenType::*,
};
use crate::language::get_language;
use crate::locale::get_locale;
fn new_lexer(formula: &str) -> Lexer {
let locale = get_locale("en").unwrap();
let language = get_language("en").unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)
}
#[test]
fn sum_implicit_intersection() {
let mut lx = new_lexer("sum(@A1:A3)");
assert_eq!(lx.next_token(), Ident("sum".to_string()));
assert_eq!(lx.next_token(), LeftParenthesis);
assert_eq!(lx.next_token(), At);
assert!(matches!(lx.next_token(), Range { .. }));
assert_eq!(lx.next_token(), RightParenthesis);
assert_eq!(lx.next_token(), EOF);
}

View File

@@ -1,5 +1,5 @@
/*! /*!
# GRAMMAR # GRAMAR
<pre class="rust"> <pre class="rust">
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>' opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
@@ -12,8 +12,7 @@ term => factor (opFactor factor)*
factor => prod (opProd prod)* factor => prod (opProd prod)*
prod => power ('^' power)* prod => power ('^' power)*
power => (unaryOp)* range '%'* power => (unaryOp)* range '%'*
range => implicit (':' primary)? range => primary (':' primary)?
implicit=> '@' primary | primary
primary => '(' expr ')' primary => '(' expr ')'
=> number => number
=> function '(' f_args ')' => function '(' f_args ')'
@@ -46,8 +45,8 @@ use super::utils::number_to_column;
use token::OpCompare; use token::OpCompare;
pub mod move_formula; pub mod move_formula;
pub mod static_analysis;
pub mod stringify; pub mod stringify;
pub mod walk;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@@ -82,9 +81,6 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
None None
} }
// DefinedNameS is a tuple with the name of the defined name, the index of the sheet and the formula
pub type DefinedNameS = (String, Option<u32>, String);
pub(crate) struct Reference<'a> { pub(crate) struct Reference<'a> {
sheet_name: &'a Option<String>, sheet_name: &'a Option<String>,
sheet_index: u32, sheet_index: u32,
@@ -94,14 +90,6 @@ pub(crate) struct Reference<'a> {
column: i32, column: i32,
} }
#[derive(PartialEq, Clone, Debug)]
pub enum ArrayNode {
Boolean(bool),
Number(f64),
String(String),
Error(token::Error),
}
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub enum Node { pub enum Node {
BooleanKind(bool), BooleanKind(bool),
@@ -175,14 +163,10 @@ pub enum Node {
name: String, name: String,
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Vec<ArrayNode>>), ArrayKind(Vec<Node>),
DefinedNameKind(DefinedNameS), DefinedNameKind((String, Option<u32>)),
TableNameKind(String), TableNameKind(String),
WrongVariableKind(String), WrongVariableKind(String),
ImplicitIntersection {
automatic: bool,
child: Box<Node>,
},
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -205,7 +189,7 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>, defined_names: Vec<(String, Option<u32>)>,
context: CellReferenceRC, context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
@@ -213,7 +197,7 @@ pub struct Parser {
impl Parser { impl Parser {
pub fn new( pub fn new(
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>, defined_names: Vec<(String, Option<u32>)>,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
) -> Parser { ) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
@@ -244,7 +228,7 @@ impl Parser {
pub fn set_worksheets_and_names( pub fn set_worksheets_and_names(
&mut self, &mut self,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<DefinedNameS>, defined_names: Vec<(String, Option<u32>)>,
) { ) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names; self.defined_names = defined_names;
@@ -268,17 +252,17 @@ impl Parser {
// Returns: // Returns:
// * None: If there is no defined name by that name // * None: If there is no defined name by that name
// * Some((Some(index), formula)): If there is a defined name local to that sheet // * Some(Some(index)): If there is a defined name local to that sheet
// * Some(None): If there is a global defined name // * Some(None): If there is a global defined name
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> { fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> {
for (df_name, df_scope, df_formula) in &self.defined_names { for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) { if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some((*df_scope, df_formula.to_owned())); return Some(*df_scope);
} }
} }
for (df_name, df_scope, df_formula) in &self.defined_names { for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() { if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some((None, df_formula.to_owned())); return Some(None);
} }
} }
None None
@@ -427,7 +411,7 @@ impl Parser {
} }
fn parse_range(&mut self) -> Node { fn parse_range(&mut self) -> Node {
let t = self.parse_implicit(); let t = self.parse_primary();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
return t; return t;
} }
@@ -446,65 +430,6 @@ impl Parser {
t t
} }
fn parse_implicit(&mut self) -> Node {
let next_token = self.lexer.peek_token();
if next_token == TokenType::At {
self.lexer.advance_token();
let t = self.parse_primary();
if let Node::ParseErrorKind { .. } = t {
return t;
}
return Node::ImplicitIntersection {
automatic: false,
child: Box::new(t),
};
}
self.parse_primary()
}
fn parse_array_row(&mut self) -> Result<Vec<ArrayNode>, Node> {
let mut row = Vec::new();
// and array can only have numbers, string or booleans
// otherwise it is a syntax error
let first_element = match self.parse_expr() {
Node::BooleanKind(s) => ArrayNode::Boolean(s),
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
};
row.push(first_element);
let mut next_token = self.lexer.peek_token();
// FIXME: this is not respecting the locale
while next_token == TokenType::Comma {
self.lexer.advance_token();
let value = match self.parse_expr() {
Node::BooleanKind(s) => ArrayNode::Boolean(s),
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
};
row.push(value);
next_token = self.lexer.peek_token();
}
Ok(row)
}
fn parse_primary(&mut self) -> Node { fn parse_primary(&mut self) -> Node {
let next_token = self.lexer.next_token(); let next_token = self.lexer.next_token();
match next_token { match next_token {
@@ -526,35 +451,21 @@ impl Parser {
TokenType::Number(s) => Node::NumberKind(s), TokenType::Number(s) => Node::NumberKind(s),
TokenType::String(s) => Node::StringKind(s), TokenType::String(s) => Node::StringKind(s),
TokenType::LeftBrace => { TokenType::LeftBrace => {
// It's an array. It's a collection of rows all of the same dimension let t = self.parse_expr();
if let Node::ParseErrorKind { .. } = t {
let first_row = match self.parse_array_row() { return t;
Ok(s) => s, }
Err(error) => return error,
};
let length = first_row.len();
let mut matrix = Vec::new();
matrix.push(first_row);
// FIXME: this is not respecting the locale
let mut next_token = self.lexer.peek_token(); let mut next_token = self.lexer.peek_token();
let mut args: Vec<Node> = vec![t];
while next_token == TokenType::Semicolon { while next_token == TokenType::Semicolon {
self.lexer.advance_token(); self.lexer.advance_token();
let row = match self.parse_array_row() { let p = self.parse_expr();
Ok(s) => s, if let Node::ParseErrorKind { .. } = p {
Err(error) => return error, return p;
};
next_token = self.lexer.peek_token();
if row.len() != length {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "All rows in an array should be the same length".to_string(),
};
} }
matrix.push(row); next_token = self.lexer.peek_token();
args.push(p);
} }
if let Err(err) = self.lexer.expect(TokenType::RightBrace) { if let Err(err) = self.lexer.expect(TokenType::RightBrace) {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
@@ -562,7 +473,7 @@ impl Parser {
message: err.message, message: err.message,
}; };
} }
Node::ArrayKind(matrix) Node::ArrayKind(args)
} }
TokenType::Reference { TokenType::Reference {
sheet, sheet,
@@ -693,20 +604,6 @@ impl Parser {
args, args,
}; };
} }
if &name == "_xlfn.SINGLE" {
if args.len() != 1 {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Implicit Intersection requires just one argument"
.to_string(),
};
}
return Node::ImplicitIntersection {
automatic: false,
child: Box::new(args[0].clone()),
};
}
return Node::InvalidFunctionKind { name, args }; return Node::InvalidFunctionKind { name, args };
} }
let context = &self.context; let context = &self.context;
@@ -717,14 +614,14 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("sheet not found: {}", context.sheet), message: "sheet not found".to_string(),
}; };
} }
}; };
// Could be a defined name or a table // Could be a defined name or a table
if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) { if let Some(scope) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope, formula)); return Node::DefinedNameKind((name, scope));
} }
let name_lower = name.to_lowercase(); let name_lower = name.to_lowercase();
for table_name in self.tables.keys() { for table_name in self.tables.keys() {
@@ -745,38 +642,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 {
@@ -809,14 +675,6 @@ impl Parser {
message: "Unexpected token: 'POWER'".to_string(), message: "Unexpected token: 'POWER'".to_string(),
} }
} }
TokenType::At => {
// A primary Node cannot start with an operator
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Unexpected token: '@'".to_string(),
}
}
TokenType::RightParenthesis TokenType::RightParenthesis
| TokenType::RightBracket | TokenType::RightBracket
| TokenType::Colon | TokenType::Colon
@@ -828,7 +686,7 @@ impl Parser {
| TokenType::Percent => Node::ParseErrorKind { | TokenType::Percent => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("Unexpected token: '{next_token:?}'"), message: format!("Unexpected token: '{:?}'", next_token),
}, },
TokenType::LeftBracket => Node::ParseErrorKind { TokenType::LeftBracket => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
@@ -850,7 +708,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("sheet not found: {}", context.sheet), message: "sheet not found".to_string(),
}; };
} }
}; };
@@ -878,7 +736,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("table sheet not found: {}", table.sheet_name), message: "sheet not found".to_string(),
}; };
} }
}; };

View File

@@ -1,6 +1,6 @@
use super::{ use super::{
stringify::{stringify_reference, DisplaceData}, stringify::{stringify_reference, DisplaceData},
ArrayNode, Node, Reference, Node, Reference,
}; };
use crate::{ use crate::{
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW},
@@ -53,24 +53,15 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
arguments = to_string_moved(el, move_context); arguments = to_string_moved(el, move_context);
} }
} }
format!("{name}({arguments})") format!("{}({})", name, arguments)
}
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
match node {
ArrayNode::Boolean(value) => format!("{value}").to_ascii_uppercase(),
ArrayNode::Number(number) => to_excel_precision_str(*number),
ArrayNode::String(value) => format!("\"{value}\""),
ArrayNode::Error(kind) => format!("{kind}"),
}
} }
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{value}").to_ascii_uppercase(), BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{value}\""), StringKind(value) => format!("\"{}\"", value),
ReferenceKind { ReferenceKind {
sheet_name, sheet_name,
sheet_index, sheet_index,
@@ -241,7 +232,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
@@ -325,7 +316,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
@@ -358,7 +349,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
_ => to_string_moved(right, move_context), _ => to_string_moved(right, move_context),
}; };
format!("{x}{kind}{y}") format!("{}{}{}", x, kind, y)
} }
OpPowerKind { left, right } => format!( OpPowerKind { left, right } => format!(
"{}^{}", "{}^{}",
@@ -371,41 +362,20 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
move_function(name, args, move_context) move_function(name, args, move_context)
} }
ArrayKind(args) => { ArrayKind(args) => {
let mut first_row = true; // This code is a placeholder. Arrays are not yet implemented
let mut matrix_string = String::new(); let mut first = true;
let mut arguments = "".to_string();
// Each element in `args` is assumed to be one "row" (itself a `Vec<T>`). for el in args {
for row in args { if !first {
if !first_row { arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
matrix_string.push(',');
} else { } else {
first_row = false; first = false;
arguments = to_string_moved(el, move_context);
} }
// Build the string for the current row
let mut first_col = true;
let mut row_string = String::new();
for el in row {
if !first_col {
row_string.push(',');
} else {
first_col = false;
}
// Reuse your existing element-stringification function
row_string.push_str(&to_string_array_node(el));
}
// Enclose the row in braces
matrix_string.push('{');
matrix_string.push_str(&row_string);
matrix_string.push('}');
} }
format!("{{{}}}", arguments)
// Enclose the whole matrix in braces
format!("{{{matrix_string}}}")
} }
DefinedNameKind((name, ..)) => name.to_string(), DefinedNameKind((name, _)) => name.to_string(),
TableNameKind(name) => name.to_string(), TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
@@ -418,18 +388,12 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)), OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)), OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
}, },
ErrorKind(kind) => format!("{kind}"), ErrorKind(kind) => format!("{}", kind),
ParseErrorKind { ParseErrorKind {
formula, formula,
message: _, message: _,
position: _, position: _,
} => formula.to_string(), } => formula.to_string(),
EmptyArgKind => "".to_string(), EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
format!("@{}", to_string_moved(child, move_context))
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
use super::{super::utils::quote_name, Node, Reference}; use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::move_formula::to_string_array_node; use crate::expressions::token::OpUnary;
use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::{OpSum, OpUnary};
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str}; use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
pub enum DisplaceData { pub enum DisplaceData {
@@ -28,11 +26,6 @@ pub enum DisplaceData {
column: i32, column: i32,
delta: i32, delta: i32,
}, },
RowMove {
sheet: u32,
row: i32,
delta: i32,
},
ColumnMove { ColumnMove {
sheet: u32, sheet: u32,
column: i32, column: i32,
@@ -41,21 +34,10 @@ pub enum DisplaceData {
None, None,
} }
/// This is the internal mode in IronCalc
pub fn to_rc_format(node: &Node) -> String { pub fn to_rc_format(node: &Node) -> String {
stringify(node, None, &DisplaceData::None, false) stringify(node, None, &DisplaceData::None, false)
} }
/// This is the mode used to display the formula in the UI
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false)
}
/// This is the mode used to export the formula to Excel
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true)
}
pub fn to_string_displaced( pub fn to_string_displaced(
node: &Node, node: &Node,
context: &CellReferenceRC, context: &CellReferenceRC,
@@ -64,10 +46,18 @@ pub fn to_string_displaced(
stringify(node, Some(context), displace_data, false) stringify(node, Some(context), displace_data, false)
} }
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false)
}
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true)
}
/// Converts a local reference to a string applying some displacement if needed. /// Converts a local reference to a string applying some displacement if needed.
/// It uses A1 style if context is not None. If context is None it uses R1C1 style /// It uses A1 style if context is not None. If context is None it uses R1C1 style
/// If full_row is true then the row details will be omitted in the A1 case /// If full_row is true then the row details will be omitted in the A1 case
/// If full_column is true then column details will be omitted. /// If full_colum is true then column details will be omitted.
pub(crate) fn stringify_reference( pub(crate) fn stringify_reference(
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
@@ -164,29 +154,6 @@ pub(crate) fn stringify_reference(
} }
} }
} }
DisplaceData::RowMove {
sheet,
row: move_row,
delta,
} => {
if sheet_index == *sheet {
if row == *move_row {
row += *delta;
} else if *delta > 0 {
// Moving the row downwards
if row > *move_row && row <= *move_row + *delta {
// Intermediate rows move up by one position
row -= 1;
}
} else if *delta < 0 {
// Moving the row upwards
if row < *move_row && row >= *move_row + *delta {
// Intermediate rows move down by one position
row += 1;
}
}
}
}
DisplaceData::ColumnMove { DisplaceData::ColumnMove {
sheet, sheet,
column: move_column, column: move_column,
@@ -195,18 +162,14 @@ pub(crate) fn stringify_reference(
if sheet_index == *sheet { if sheet_index == *sheet {
if column == *move_column { if column == *move_column {
column += *delta; column += *delta;
} else if *delta > 0 { } else if (*delta > 0
// Moving the column to the right && column > *move_column
if column > *move_column && column <= *move_column + *delta { && column <= *move_column + *delta)
// Intermediate columns move left by one position || (*delta < 0
column -= 1; && column < *move_column
} && column >= *move_column + *delta)
} else if *delta < 0 { {
// Moving the column to the left column -= *delta;
if column < *move_column && column >= *move_column + *delta {
// Intermediate columns move right by one position
column += 1;
}
} }
} }
} }
@@ -216,16 +179,16 @@ pub(crate) fn stringify_reference(
return "#REF!".to_string(); return "#REF!".to_string();
} }
let mut row_abs = if absolute_row { let mut row_abs = if absolute_row {
format!("${row}") format!("${}", row)
} else { } else {
format!("{row}") format!("{}", row)
}; };
let column = match crate::expressions::utils::number_to_column(column) { let column = match crate::expressions::utils::number_to_column(column) {
Some(s) => s, Some(s) => s,
None => return "#REF!".to_string(), None => return "#REF!".to_string(),
}; };
let mut col_abs = if absolute_column { let mut col_abs = if absolute_column {
format!("${column}") format!("${}", column)
} else { } else {
column column
}; };
@@ -240,27 +203,27 @@ pub(crate) fn stringify_reference(
format!("{}!{}{}", quote_name(name), col_abs, row_abs) format!("{}!{}{}", quote_name(name), col_abs, row_abs)
} }
None => { None => {
format!("{col_abs}{row_abs}") format!("{}{}", col_abs, row_abs)
} }
} }
} }
None => { None => {
let row_abs = if absolute_row { let row_abs = if absolute_row {
format!("R{row}") format!("R{}", row)
} else { } else {
format!("R[{row}]") format!("R[{}]", row)
}; };
let col_abs = if absolute_column { let col_abs = if absolute_column {
format!("C{column}") format!("C{}", column)
} else { } else {
format!("C[{column}]") format!("C[{}]", column)
}; };
match &sheet_name { match &sheet_name {
Some(name) => { Some(name) => {
format!("{}!{}{}", quote_name(name), row_abs, col_abs) format!("{}!{}{}", quote_name(name), row_abs, col_abs)
} }
None => { None => {
format!("{row_abs}{col_abs}") format!("{}{}", row_abs, col_abs)
} }
} }
} }
@@ -272,7 +235,7 @@ fn format_function(
args: &Vec<Node>, args: &Vec<Node>,
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
export_to_excel: bool, use_original_name: bool,
) -> String { ) -> String {
let mut first = true; let mut first = true;
let mut arguments = "".to_string(); let mut arguments = "".to_string();
@@ -281,52 +244,27 @@ fn format_function(
arguments = format!( arguments = format!(
"{},{}", "{},{}",
arguments, arguments,
stringify(el, context, displace_data, export_to_excel) stringify(el, context, displace_data, use_original_name)
); );
} else { } else {
first = false; first = false;
arguments = stringify(el, context, displace_data, export_to_excel); arguments = stringify(el, context, displace_data, use_original_name);
} }
} }
format!("{name}({arguments})") format!("{}({})", name, arguments)
} }
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
// But three different ways to convert it to a string.
//
// To stringify a formula we need a "context", that is in which cell are we doing the "stringifying"
//
// But there are three ways to stringify a formula:
//
// * To show it to the IronCalc user
// * To store internally
// * To export to Excel
//
// There are, of course correspondingly three "modes" when parsing a formula.
//
// The internal representation is the more different as references are stored in the RC representation.
// The the AST of the formula is kept close to this representation we don't need a context
//
// In the export to Excel representation certain things are different:
// * We add a _xlfn. in front of some (more modern) functions
// * We remove the Implicit Intersection operator when it is automatic and add _xlfn.SINGLE when it is not
//
// Examples:
// * =A1+B2
// * =RC+R1C1
// * =A1+B1
fn stringify( fn stringify(
node: &Node, node: &Node,
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
export_to_excel: bool, use_original_name: bool,
) -> String { ) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{value}").to_ascii_uppercase(), BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{value}\""), StringKind(value) => format!("\"{}\"", value),
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
column, column,
@@ -416,7 +354,7 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
WrongRangeKind { WrongRangeKind {
sheet_name, sheet_name,
@@ -465,56 +403,58 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
stringify(left, context, displace_data, export_to_excel), stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
), ),
OpConcatenateKind { left, right } => format!( OpConcatenateKind { left, right } => format!(
"{}&{}", "{}&{}",
stringify(left, context, displace_data, export_to_excel), stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
), ),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
stringify(left, context, displace_data, export_to_excel), stringify(left, context, displace_data, use_original_name),
kind, kind,
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
),
OpSumKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, use_original_name),
kind,
stringify(right, context, displace_data, use_original_name)
), ),
OpSumKind { kind, left, right } => {
let left_str = stringify(left, context, displace_data, export_to_excel);
// if kind is minus then we need parentheses in the right side if they are OpSumKind or CompareKind
let right_str = if (matches!(kind, OpSum::Minus) && matches!(**right, OpSumKind { .. }))
| matches!(**right, CompareKind { .. })
{
format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
)
} else {
stringify(right, context, displace_data, export_to_excel)
};
format!("{left_str}{kind}{right_str}")
}
OpProductKind { kind, left, right } => { OpProductKind { kind, left, right } => {
let x = match **left { let x = match **left {
OpSumKind { .. } | CompareKind { .. } => format!( OpSumKind { .. } => format!(
"({})", "({})",
stringify(left, context, displace_data, export_to_excel) stringify(left, context, displace_data, use_original_name)
), ),
_ => stringify(left, context, displace_data, export_to_excel), CompareKind { .. } => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
),
_ => stringify(left, context, displace_data, use_original_name),
}; };
let y = match **right { let y = match **right {
OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!( OpSumKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
), ),
_ => stringify(right, context, displace_data, export_to_excel), CompareKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
),
OpProductKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
),
_ => stringify(right, context, displace_data, use_original_name),
}; };
format!("{x}{kind}{y}") format!("{}{}{}", x, kind, y)
} }
OpPowerKind { left, right } => { OpPowerKind { left, right } => {
let x = match **left { let x = match **left {
@@ -527,7 +467,9 @@ fn stringify(
| DefinedNameKind(_) | DefinedNameKind(_)
| TableNameKind(_) | TableNameKind(_)
| WrongVariableKind(_) | WrongVariableKind(_)
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel), | WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name)
}
OpRangeKind { .. } OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
| OpProductKind { .. } | OpProductKind { .. }
@@ -540,10 +482,9 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
| CompareKind { .. } | CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!( | EmptyArgKind => format!(
"({})", "({})",
stringify(left, context, displace_data, export_to_excel) stringify(left, context, displace_data, use_original_name)
), ),
}; };
let y = match **right { let y = match **right {
@@ -557,7 +498,7 @@ fn stringify(
| TableNameKind(_) | TableNameKind(_)
| WrongVariableKind(_) | WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
} }
OpRangeKind { .. } OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
@@ -571,129 +512,65 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
| CompareKind { .. } | CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!( | EmptyArgKind => format!(
"({})", "({})",
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
), ),
}; };
format!("{x}^{y}") format!("{}^{}", x, y)
} }
InvalidFunctionKind { name, args } => { InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, export_to_excel) format_function(name, args, context, displace_data, use_original_name)
} }
FunctionKind { kind, args } => { FunctionKind { kind, args } => {
let name = if export_to_excel { let name = if use_original_name {
kind.to_xlsx_string() kind.to_xlsx_string()
} else { } else {
kind.to_string() kind.to_string()
}; };
format_function(&name, args, context, displace_data, export_to_excel) format_function(&name, args, context, displace_data, use_original_name)
} }
ArrayKind(args) => { ArrayKind(args) => {
let mut first_row = true; let mut first = true;
let mut matrix_string = String::new(); let mut arguments = "".to_string();
for el in args {
for row in args { if !first {
if !first_row { arguments = format!(
matrix_string.push(';'); "{},{}",
arguments,
stringify(el, context, displace_data, use_original_name)
);
} else { } else {
first_row = false; first = false;
arguments = stringify(el, context, displace_data, use_original_name);
} }
let mut first_column = true;
let mut row_string = String::new();
for el in row {
if !first_column {
row_string.push(',');
} else {
first_column = false;
}
row_string.push_str(&to_string_array_node(el));
}
matrix_string.push_str(&row_string);
} }
format!("{{{matrix_string}}}") format!("{{{}}}", arguments)
} }
TableNameKind(value) => value.to_string(), TableNameKind(value) => value.to_string(),
DefinedNameKind((name, ..)) => name.to_string(), DefinedNameKind((name, _)) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
let needs_parentheses = match **right { format!(
BooleanKind(_) "-{}",
| NumberKind(_) stringify(right, context, displace_data, use_original_name)
| StringKind(_) )
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| WrongRangeKind { .. }
| OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| ImplicitIntersection { .. }
| CompareKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| EmptyArgKind => false,
OpSumKind { .. } | UnaryKind { .. } => true,
};
if needs_parentheses {
format!(
"-({})",
stringify(right, context, displace_data, export_to_excel)
)
} else {
format!(
"-{}",
stringify(right, context, displace_data, export_to_excel)
)
}
} }
OpUnary::Percentage => { OpUnary::Percentage => {
format!( format!(
"{}%", "{}%",
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, use_original_name)
) )
} }
}, },
ErrorKind(kind) => format!("{kind}"), ErrorKind(kind) => format!("{}", kind),
ParseErrorKind { ParseErrorKind {
formula, formula,
position: _, position: _,
message: _, message: _,
} => formula.to_string(), } => formula.to_string(),
EmptyArgKind => "".to_string(), EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
if export_to_excel {
// We need to check wether the II can be automatic or not
let mut new_node = child.as_ref().clone();
add_implicit_intersection(&mut new_node, true);
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
return stringify(child, context, displace_data, export_to_excel);
}
return format!(
"_xlfn.SINGLE({})",
stringify(child, context, displace_data, export_to_excel)
);
}
format!(
"@{}",
stringify(child, context, displace_data, export_to_excel)
)
}
} }
} }
@@ -781,12 +658,6 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::UnaryKind { kind: _, right } => { Node::UnaryKind { kind: _, right } => {
rename_sheet_in_node(right, sheet_index, new_name); rename_sheet_in_node(right, sheet_index, new_name);
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_sheet_in_node(child, sheet_index, new_name);
}
// Do nothing // Do nothing
Node::BooleanKind(_) => {} Node::BooleanKind(_) => {}
@@ -810,7 +681,7 @@ pub(crate) fn rename_defined_name_in_node(
) { ) {
match node { match node {
// Rename // Rename
Node::DefinedNameKind((n, s, _)) => { Node::DefinedNameKind((n, s)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope { if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string(); *n = new_name.to_string();
} }
@@ -865,12 +736,6 @@ pub(crate) fn rename_defined_name_in_node(
Node::UnaryKind { kind: _, right } => { Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name); rename_defined_name_in_node(right, name, scope, new_name);
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_defined_name_in_node(child, name, scope, new_name);
}
// Do nothing // Do nothing
Node::BooleanKind(_) => {} Node::BooleanKind(_) => {}

View File

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

View File

@@ -1,80 +0,0 @@
use std::collections::HashMap;
use crate::expressions::{
parser::{
stringify::{to_excel_string, to_string},
Parser,
},
types::CellReferenceRC,
};
use crate::expressions::parser::static_analysis::add_implicit_intersection;
#[test]
fn simple_test() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let cases = vec![
("A1:A10*SUM(A1:A10)", "@A1:A10*SUM(A1:A10)"),
("A1:A10", "@A1:A10"),
// Math and trigonometry functions
("SUM(A1:A10)", "SUM(A1:A10)"),
("SIN(A1:A10)", "SIN(@A1:A10)"),
("COS(A1:A10)", "COS(@A1:A10)"),
("TAN(A1:A10)", "TAN(@A1:A10)"),
("ASIN(A1:A10)", "ASIN(@A1:A10)"),
("ACOS(A1:A10)", "ACOS(@A1:A10)"),
("ATAN(A1:A10)", "ATAN(@A1:A10)"),
("SINH(A1:A10)", "SINH(@A1:A10)"),
("COSH(A1:A10)", "COSH(@A1:A10)"),
("TANH(A1:A10)", "TANH(@A1:A10)"),
("ASINH(A1:A10)", "ASINH(@A1:A10)"),
("ACOSH(A1:A10)", "ACOSH(@A1:A10)"),
("ATANH(A1:A10)", "ATANH(@A1:A10)"),
("ATAN2(A1:A10,B1:B10)", "ATAN2(@A1:A10,@B1:B10)"),
("ATAN2(A1:A10,A1)", "ATAN2(@A1:A10,A1)"),
("SQRT(A1:A10)", "SQRT(@A1:A10)"),
("SQRTPI(A1:A10)", "SQRTPI(@A1:A10)"),
("POWER(A1:A10,A1)", "POWER(@A1:A10,A1)"),
("POWER(A1:A10,B1:B10)", "POWER(@A1:A10,@B1:B10)"),
("MAX(A1:A10)", "MAX(A1:A10)"),
("MIN(A1:A10)", "MIN(A1:A10)"),
("ABS(A1:A10)", "ABS(@A1:A10)"),
("FALSE()", "FALSE()"),
("TRUE()", "TRUE()"),
// Defined names
("BADNMAE", "@BADNMAE"),
// Logical
("AND(A1:A10)", "AND(A1:A10)"),
("OR(A1:A10)", "OR(A1:A10)"),
("NOT(A1:A10)", "NOT(@A1:A10)"),
("IF(A1:A10,B1:B10,C1:C10)", "IF(@A1:A10,@B1:B10,@C1:C10)"),
// Information
// ("ISBLANK(A1:A10)", "ISBLANK(A1:A10)"),
// ("ISERR(A1:A10)", "ISERR(A1:A10)"),
// ("ISERROR(A1:A10)", "ISERROR(A1:A10)"),
// ("ISEVEN(A1:A10)", "ISEVEN(A1:A10)"),
// ("ISLOGICAL(A1:A10)", "ISLOGICAL(A1:A10)"),
// ("ISNA(A1:A10)", "ISNA(A1:A10)"),
// ("ISNONTEXT(A1:A10)", "ISNONTEXT(A1:A10)"),
// ("ISNUMBER(A1:A10)", "ISNUMBER(A1:A10)"),
// ("ISODD(A1:A10)", "ISODD(A1:A10)"),
// ("ISREF(A1:A10)", "ISREF(A1:A10)"),
// ("ISTEXT(A1:A10)", "ISTEXT(A1:A10)"),
];
for (formula, expected) in cases {
let mut t = parser.parse(formula, &cell_reference);
add_implicit_intersection(&mut t, true);
let r = to_string(&t, &cell_reference);
assert_eq!(r, expected);
let excel_formula = to_excel_string(&t, &cell_reference);
assert_eq!(excel_formula, formula);
}
}

View File

@@ -1,92 +0,0 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::{ArrayNode, Node, Parser};
use crate::expressions::types::CellReferenceRC;
#[test]
fn simple_horizontal() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let horizontal = parser.parse("{1, 2, 3}", &cell_reference);
assert_eq!(
horizontal,
Node::ArrayKind(vec![vec![
ArrayNode::Number(1.0),
ArrayNode::Number(2.0),
ArrayNode::Number(3.0)
]])
);
assert_eq!(to_rc_format(&horizontal), "{1,2,3}");
assert_eq!(to_string(&horizontal, &cell_reference), "{1,2,3}");
}
#[test]
fn simple_vertical() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let vertical = parser.parse("{1;2; 3}", &cell_reference);
assert_eq!(
vertical,
Node::ArrayKind(vec![
vec![ArrayNode::Number(1.0)],
vec![ArrayNode::Number(2.0)],
vec![ArrayNode::Number(3.0)]
])
);
assert_eq!(to_rc_format(&vertical), "{1;2;3}");
assert_eq!(to_string(&vertical, &cell_reference), "{1;2;3}");
}
#[test]
fn simple_matrix() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let matrix = parser.parse("{1,2,3; 4, 5, 6; 7,8,9}", &cell_reference);
assert_eq!(
matrix,
Node::ArrayKind(vec![
vec![
ArrayNode::Number(1.0),
ArrayNode::Number(2.0),
ArrayNode::Number(3.0)
],
vec![
ArrayNode::Number(4.0),
ArrayNode::Number(5.0),
ArrayNode::Number(6.0)
],
vec![
ArrayNode::Number(7.0),
ArrayNode::Number(8.0),
ArrayNode::Number(9.0)
]
])
);
assert_eq!(to_rc_format(&matrix), "{1,2,3;4,5,6;7,8,9}");
assert_eq!(to_string(&matrix, &cell_reference), "{1,2,3;4,5,6;7,8,9}");
}

View File

@@ -1,75 +0,0 @@
#![allow(clippy::panic)]
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
use std::collections::HashMap;
#[test]
fn simple() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 3,
column: 2,
};
let t = parser.parse("@A1:A10", &cell_reference);
let child = Node::RangeKind {
sheet_name: None,
sheet_index: 0,
absolute_row1: false,
absolute_column1: false,
row1: -2,
column1: -1,
absolute_row2: false,
absolute_column2: false,
row2: 7,
column2: -1,
};
assert_eq!(
t,
Node::ImplicitIntersection {
automatic: false,
child: Box::new(child)
}
)
}
#[test]
fn simple_add() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 3,
column: 2,
};
let t = parser.parse("@A1:A10+12", &cell_reference);
let child = Node::RangeKind {
sheet_name: None,
sheet_index: 0,
absolute_row1: false,
absolute_column1: false,
row1: -2,
column1: -1,
absolute_row2: false,
absolute_column2: false,
row2: 7,
column2: -1,
};
assert_eq!(
t,
Node::OpSumKind {
kind: crate::expressions::token::OpSum::Add,
left: Box::new(Node::ImplicitIntersection {
automatic: false,
child: Box::new(child)
}),
right: Box::new(Node::NumberKind(12.0))
}
)
}

View File

@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
width: 4, width: 4,
height: 5, height: 5,
}; };
let node = parser.parse("X9^C2-F4*H2+SUM(F2:H4)+SUM(C2:F6)", context); let node = parser.parse("X9^C2-F4*H2", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -400,7 +400,7 @@ fn test_move_formula_misc() {
column_delta: 10, column_delta: 10,
}, },
); );
assert_eq!(t, "X9^M12-P14*H2+SUM(F2:H4)+SUM(M12:P16)"); 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)", context);
let t = move_formula( let t = move_formula(
@@ -475,77 +475,3 @@ fn test_move_formula_another_sheet() {
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)" "Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
); );
} }
#[test]
fn move_formula_implicit_intersetion() {
// context is E4
let row = 4;
let column = 5;
let context = &CellReferenceRC {
sheet: "Sheet1".to_string(),
row,
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
sheet: 0,
row: 2,
column: 3,
width: 4,
height: 5,
};
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)", context);
let t = move_formula(
&node,
&MoveContext {
source_sheet_name: "Sheet1",
row,
column,
area,
target_sheet_name: "Sheet1",
row_delta: 10,
column_delta: 10,
},
);
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)");
}
#[test]
fn move_formula_implicit_intersetion_with_ranges() {
// context is E4
let row = 4;
let column = 5;
let context = &CellReferenceRC {
sheet: "Sheet1".to_string(),
row,
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
sheet: 0,
row: 2,
column: 3,
width: 4,
height: 5,
};
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)+SUM(@A1, @X9, @$D$5)", context);
let t = move_formula(
&node,
&MoveContext {
source_sheet_name: "Sheet1",
row,
column,
area,
target_sheet_name: "Sheet1",
row_delta: 10,
column_delta: 10,
},
);
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)+SUM(@A1,@X9,@$N$15)");
}

View File

@@ -32,39 +32,3 @@ fn exp_order() {
let t = parser.parse("(5)^(4)", &cell_reference); let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4"); assert_eq!(to_string(&t, &cell_reference), "5^4");
} }
#[test]
fn correct_parenthesis() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let t = parser.parse("-(1 + 1)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "-(1+1)");
let t = parser.parse("1 - (3 + 4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-(3+4)");
let t = parser.parse("-(1.05*(0.0284 + 0.0046) - 0.0284)", &cell_reference);
assert_eq!(
to_string(&t, &cell_reference),
"-(1.05*(0.0284+0.0046)-0.0284)"
);
let t = parser.parse("1 + (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1+3+5");
let t = parser.parse("1 - (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-(3+5)");
let t = parser.parse("(1 - 3) - (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-3-(3+5)");
let t = parser.parse("1 + (3<5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1+(3<5)");
}

View File

@@ -3,11 +3,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string; use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
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 crate::expressions::types::CellReferenceRC;
fn create_test_table( fn create_test_table(
table_name: &str, table_name: &str,
column_names: &[&str], column_names: &[&str],

View File

@@ -0,0 +1,278 @@
use super::{move_formula::ref_is_in_area, Node};
use crate::expressions::types::{Area, CellReferenceIndex};
pub(crate) fn forward_references(
node: &mut Node,
context: &CellReferenceIndex,
source_area: &Area,
target_sheet: u32,
target_sheet_name: &str,
target_row: i32,
target_column: i32,
) {
match node {
Node::ReferenceKind {
sheet_name,
sheet_index: reference_sheet,
absolute_row,
absolute_column,
row: reference_row,
column: reference_column,
} => {
let reference_row_absolute = if *absolute_row {
*reference_row
} else {
*reference_row + context.row
};
let reference_column_absolute = if *absolute_column {
*reference_column
} else {
*reference_column + context.column
};
if ref_is_in_area(
*reference_sheet,
reference_row_absolute,
reference_column_absolute,
source_area,
) {
if *reference_sheet != target_sheet {
*sheet_name = Some(target_sheet_name.to_string());
*reference_sheet = target_sheet;
}
*reference_row = target_row + *reference_row - source_area.row;
*reference_column = target_column + *reference_column - source_area.column;
}
}
Node::RangeKind {
sheet_name,
sheet_index,
absolute_row1,
absolute_column1,
row1,
column1,
absolute_row2,
absolute_column2,
row2,
column2,
} => {
let reference_row1 = if *absolute_row1 {
*row1
} else {
*row1 + context.row
};
let reference_column1 = if *absolute_column1 {
*column1
} else {
*column1 + context.column
};
let reference_row2 = if *absolute_row2 {
*row2
} else {
*row2 + context.row
};
let reference_column2 = if *absolute_column2 {
*column2
} else {
*column2 + context.column
};
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
{
if *sheet_index != target_sheet {
*sheet_index = target_sheet;
*sheet_name = Some(target_sheet_name.to_string());
}
*row1 = target_row + *row1 - source_area.row;
*column1 = target_column + *column1 - source_area.column;
*row2 = target_row + *row2 - source_area.row;
*column2 = target_column + *column2 - source_area.column;
}
}
// Recurse
Node::OpRangeKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpConcatenateKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpPowerKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
forward_references(
arg,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
forward_references(
arg,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::UnaryKind { kind: _, right } => {
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
// TODO: Not implemented
Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {}
Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {}
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
}
}

View File

@@ -197,7 +197,7 @@ pub fn is_english_error_string(name: &str) -> bool {
"#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!", "#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!",
"#CALC!", "#CIRC!", "#NULL!", "#CALC!", "#CIRC!", "#NULL!",
]; ];
names.contains(&name) names.iter().any(|e| *e == name)
} }
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -240,7 +240,6 @@ pub enum TokenType {
Bang, // ! Bang, // !
Percent, // % Percent, // %
And, // & And, // &
At, // @
Reference { Reference {
sheet: Option<String>, sheet: Option<String>,
row: i32, row: i32,

View File

@@ -211,6 +211,4 @@ fn test_names() {
assert!(!is_valid_identifier("test€")); assert!(!is_valid_identifier("test€"));
assert!(!is_valid_identifier("truñe")); assert!(!is_valid_identifier("truñe"));
assert!(!is_valid_identifier("tr&ue")); assert!(!is_valid_identifier("tr&ue"));
assert!(!is_valid_identifier("LOG10"));
} }

View File

@@ -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;
pub const DATE_OUT_OF_RANGE_MESSAGE: &str = "Out of range parameters for date"; pub fn from_excel_date(days: i64) -> NaiveDate {
#[inline]
fn convert_to_serial_number(date: NaiveDate) -> i32 {
date.num_days_from_ce() - EXCEL_DATE_BASE
}
fn is_date_within_range(date: NaiveDate) -> bool {
convert_to_serial_number(date) >= MINIMUM_DATE_SERIAL_NUMBER
&& convert_to_serial_number(date) <= MAXIMUM_DATE_SERIAL_NUMBER
}
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!(
"Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
));
};
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!(
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
));
};
#[allow(clippy::expect_used)] #[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(DATE_OUT_OF_RANGE_MESSAGE.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(DATE_OUT_OF_RANGE_MESSAGE.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(DATE_OUT_OF_RANGE_MESSAGE.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(DATE_OUT_OF_RANGE_MESSAGE.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(DATE_OUT_OF_RANGE_MESSAGE.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

@@ -120,7 +120,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
// We should have different codepaths for general formatting and errors // We should have different codepaths for general formatting and errors
let value_abs = value.abs(); let value_abs = value.abs();
if (1.0e-8..1.0e+11).contains(&value_abs) { if (1.0e-8..1.0e+11).contains(&value_abs) {
let mut text = format!("{value:.9}"); let mut text = format!("{:.9}", value);
text = text.trim_end_matches('0').trim_end_matches('.').to_string(); text = text.trim_end_matches('0').trim_end_matches('.').to_string();
Formatted { Formatted {
text, text,
@@ -138,7 +138,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let exponent = value_abs.log10().floor(); let exponent = value_abs.log10().floor();
value /= 10.0_f64.powf(exponent); value /= 10.0_f64.powf(exponent);
let sign = if exponent < 0.0 { '-' } else { '+' }; let sign = if exponent < 0.0 { '-' } else { '+' };
let s = format!("{value:.5}"); let s = format!("{:.5}", value);
Formatted { Formatted {
text: format!( text: format!(
"{}E{}{:02}", "{}E{}{:02}",
@@ -154,46 +154,45 @@ 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) => {
text = format!("{text}{c}"); text = format!("{}{}", text, c);
} }
TextToken::Text(t) => { TextToken::Text(t) => {
text = format!("{text}{t}"); text = format!("{}{}", text, t);
} }
TextToken::Ghost(_) => { TextToken::Ghost(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Spacer(_) => { TextToken::Spacer(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Raw => { TextToken::Raw => {
text = format!("{text}{value}"); text = format!("{}{}", text, value);
} }
TextToken::Digit(_) => {} TextToken::Digit(_) => {}
TextToken::Period => {} TextToken::Period => {}
TextToken::Day => { TextToken::Day => {
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{text}{day}"); text = format!("{}{}", text, day);
} }
TextToken::DayPadded => { TextToken::DayPadded => {
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{text}{day:02}"); text = format!("{}{:02}", text, day);
} }
TextToken::DayNameShort => { TextToken::DayNameShort => {
let mut day = date.weekday().number_from_monday() as usize; let mut day = date.weekday().number_from_monday() as usize;
@@ -211,11 +210,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} }
TextToken::Month => { TextToken::Month => {
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{text}{month}"); text = format!("{}{}", text, month);
} }
TextToken::MonthPadded => { TextToken::MonthPadded => {
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{text}{month:02}"); text = format!("{}{:02}", text, month);
} }
TextToken::MonthNameShort => { TextToken::MonthNameShort => {
let month = date.month() as usize; let month = date.month() as usize;
@@ -228,7 +227,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => { TextToken::MonthLetter => {
let month = date.month() as usize; let month = date.month() as usize;
let months_letter = &locale.dates.months_letter[month - 1]; let months_letter = &locale.dates.months_letter[month - 1];
text = format!("{text}{months_letter}"); text = format!("{}{}", text, months_letter);
} }
TextToken::YearShort => { TextToken::YearShort => {
text = format!("{}{}", text, date.format("%y")); text = format!("{}{}", text, date.format("%y"));
@@ -247,7 +246,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Number(p) => { ParsePart::Number(p) => {
let mut text = "".to_string(); let mut text = "".to_string();
if let Some(c) = p.currency { if let Some(c) = p.currency {
text = format!("{c}"); text = format!("{}", c);
} }
let tokens = &p.tokens; let tokens = &p.tokens;
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma)); value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
@@ -295,26 +294,26 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
text = format!("{text}{c}"); text = format!("{}{}", text, c);
} }
TextToken::Text(t) => { TextToken::Text(t) => {
text = format!("{text}{t}"); text = format!("{}{}", text, t);
} }
TextToken::Ghost(_) => { TextToken::Ghost(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Spacer(_) => { TextToken::Spacer(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Raw => { TextToken::Raw => {
text = format!("{text}{value}"); text = format!("{}{}", text, value);
} }
TextToken::Period => { TextToken::Period => {
text = format!("{text}{decimal_separator}"); text = format!("{}{}", text, decimal_separator);
} }
TextToken::Digit(digit) => { TextToken::Digit(digit) => {
if digit.number == 'i' { if digit.number == 'i' {
@@ -322,7 +321,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let index = digit.index; let index = digit.index;
let number_index = ln - digit_count + index; let number_index = ln - digit_count + index;
if index == 0 && is_negative { if index == 0 && is_negative {
text = format!("-{text}"); text = format!("-{}", text);
} }
if ln <= digit_count { if ln <= digit_count {
// The number of digits is less or equal than the number of digit tokens // The number of digits is less or equal than the number of digit tokens
@@ -347,7 +346,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} else { } else {
"" ""
}; };
text = format!("{text}{c}{sep}"); text = format!("{}{}{}", text, c, sep);
} }
digit_index += 1; digit_index += 1;
} else { } else {
@@ -373,18 +372,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if index < fract_part.len() { if index < fract_part.len() {
text = format!("{}{}", text, fract_part[index]); text = format!("{}{}", text, fract_part[index]);
} else if digit.kind == '0' { } else if digit.kind == '0' {
text = format!("{text}0"); text = format!("{}0", text);
} else if digit.kind == '?' { } else if digit.kind == '?' {
text = format!("{text} "); text = format!("{} ", text);
} }
} else if digit.number == 'e' { } else if digit.number == 'e' {
// 3. Exponent part // 3. Exponent part
let index = digit.index; let index = digit.index;
if index == 0 { if index == 0 {
if exponent_is_negative { if exponent_is_negative {
text = format!("{text}E-"); text = format!("{}E-", text);
} else { } else {
text = format!("{text}E+"); text = format!("{}E+", text);
} }
} }
let number_index = l_exp - (p.exponent_digit_count - index); let number_index = l_exp - (p.exponent_digit_count - index);
@@ -400,7 +399,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
exponent_part[number_index as usize] exponent_part[number_index as usize]
}; };
text = format!("{text}{c}"); text = format!("{}{}", text, c);
} }
} else { } else {
for i in 0..number_index + 1 { for i in 0..number_index + 1 {
@@ -614,7 +613,7 @@ pub(crate) fn parse_formatted_number(
// check if it is a currency in currencies // check if it is a currency in currencies
for currency in currencies { for currency in currencies {
if let Some(p) = value.strip_prefix(&format!("-{currency}")) { if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
let (f, options) = parse_number(p.trim())?; let (f, options) = parse_number(p.trim())?;
if options.is_scientific { if options.is_scientific {
return Ok((f, Some(scientific_format.to_string()))); return Ok((f, Some(scientific_format.to_string())));

View File

@@ -178,7 +178,10 @@ impl Lexer {
} }
} }
self.position = position; self.position = position;
chars.parse::<f64>().ok() match chars.parse::<f64>() {
Err(_) => None,
Ok(v) => Some(v),
}
} }
fn consume_condition(&mut self) -> Option<(Compare, f64)> { fn consume_condition(&mut self) -> Option<(Compare, f64)> {
@@ -333,7 +336,7 @@ impl Lexer {
} else if s == '-' { } else if s == '-' {
Token::ScientificMinus Token::ScientificMinus
} else { } else {
self.set_error(&format!("Unexpected char: {s}. Expected + or -")); self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
Token::ILLEGAL Token::ILLEGAL
} }
} else { } else {
@@ -385,14 +388,14 @@ impl Lexer {
for c in "eneral".chars() { for c in "eneral".chars() {
let cc = self.read_next_char(); let cc = self.read_next_char();
if Some(c) != cc { if Some(c) != cc {
self.set_error(&format!("Unexpected character: {x}")); self.set_error(&format!("Unexpected character: {}", x));
return Token::ILLEGAL; return Token::ILLEGAL;
} }
} }
Token::General Token::General
} }
_ => { _ => {
self.set_error(&format!("Unexpected character: {x}")); self.set_error(&format!("Unexpected character: {}", x));
Token::ILLEGAL Token::ILLEGAL
} }
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -46,18 +46,18 @@ impl fmt::Display for Complex {
// it is a bit weird what Excel does but it seems it uses general notation for // it is a bit weird what Excel does but it seems it uses general notation for
// numbers > 1e-20 and scientific notation for the rest // numbers > 1e-20 and scientific notation for the rest
let y_str = if y.abs() <= 9e-20 { let y_str = if y.abs() <= 9e-20 {
format!("{y:E}") format!("{:E}", y)
} else if y == 1.0 { } else if y == 1.0 {
"".to_string() "".to_string()
} else if y == -1.0 { } else if y == -1.0 {
"-".to_string() "-".to_string()
} else { } else {
format!("{y}") format!("{}", y)
}; };
let x_str = if x.abs() <= 9e-20 { let x_str = if x.abs() <= 9e-20 {
format!("{x:E}") format!("{:E}", x)
} else { } else {
format!("{x}") format!("{}", x)
}; };
if y == 0.0 && x == 0.0 { if y == 0.0 && x == 0.0 {
write!(f, "0") write!(f, "0")
@@ -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

@@ -76,7 +76,7 @@ impl Model {
if value < 0 { if value < 0 {
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9)) CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
} else { } else {
let result = format!("{value:X}"); let result = format!("{:X}", value);
if let Some(places) = places { if let Some(places) = places {
if places < result.len() as i32 { if places < result.len() as i32 {
return CalcResult::new_error( return CalcResult::new_error(
@@ -120,7 +120,7 @@ impl Model {
if value < 0 { if value < 0 {
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9)) CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
} else { } else {
let result = format!("{value:o}"); let result = format!("{:o}", value);
if let Some(places) = places { if let Some(places) = places {
if places < result.len() as i32 { if places < result.len() as i32 {
return CalcResult::new_error( return CalcResult::new_error(
@@ -163,7 +163,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{value:b}"); let result = format!("{:b}", value);
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -202,7 +202,7 @@ impl Model {
if value < 0 { if value < 0 {
value += HEX_MAX; value += HEX_MAX;
} }
let result = format!("{value:X}"); let result = format!("{:X}", value);
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -242,7 +242,7 @@ impl Model {
if value < 0 { if value < 0 {
value += OCT_MAX; value += OCT_MAX;
} }
let result = format!("{value:o}"); let result = format!("{:o}", value);
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -301,7 +301,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{value:b}"); let result = format!("{:b}", value);
if let Some(places) = places { if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) { if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -391,7 +391,7 @@ impl Model {
if value < 0 { if value < 0 {
value += OCT_MAX; value += OCT_MAX;
} }
let result = format!("{value:o}"); let result = format!("{:o}", value);
if let Some(places) = places { if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) { if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -446,7 +446,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{value:b}"); let result = format!("{:b}", value);
if let Some(places) = places { if let Some(places) = places {
if value < 512 && places < result.len() as i32 { if value < 512 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -532,7 +532,7 @@ impl Model {
if value < 0 { if value < 0 {
value += HEX_MAX; value += HEX_MAX;
} }
let result = format!("{value:X}"); let result = format!("{:X}", value);
if let Some(places) = places { if let Some(places) = places {
if value < HEX_MAX_HALF && places < result.len() as i32 { if value < HEX_MAX_HALF && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());

View File

@@ -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(
@@ -231,7 +236,7 @@ impl Model {
CalcResult::new_error( CalcResult::new_error(
Error::ERROR, Error::ERROR,
*cell, *cell,
format!("Invalid worksheet index: '{sheet}'"), format!("Invalid worksheet index: '{}'", sheet),
) )
})? })?
.dimension() .dimension()
@@ -245,7 +250,7 @@ impl Model {
CalcResult::new_error( CalcResult::new_error(
Error::ERROR, Error::ERROR,
*cell, *cell,
format!("Invalid worksheet index: '{sheet}'"), format!("Invalid worksheet index: '{}'", sheet),
) )
})? })?
.dimension() .dimension()
@@ -431,7 +436,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(),
}; };
@@ -918,9 +923,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 +989,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,
@@ -1372,10 +1373,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 +1383,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 +1437,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 +1447,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 +1487,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 +1497,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,

View File

@@ -235,11 +235,6 @@ impl Model {
// This cannot happen // This cannot happen
CalcResult::Number(1.0) CalcResult::Number(1.0)
} }
CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
} }
} }
pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -254,7 +249,7 @@ impl Model {
// The arg could be a defined name or a table // The arg could be a defined name or a table
// let = &args[0]; // let = &args[0];
match &args[0] { match &args[0] {
Node::DefinedNameKind((name, scope, _)) => { Node::DefinedNameKind((name, scope)) => {
// Let's see if it is a defined name // Let's see if it is a defined name
if let Some(defined_name) = self if let Some(defined_name) = self
.parsed_defined_names .parsed_defined_names

View File

@@ -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);
@@ -161,13 +145,6 @@ impl Model {
CalcResult::Range { .. } CalcResult::Range { .. }
| CalcResult::String { .. } | CalcResult::String { .. }
| CalcResult::EmptyCell => {} | CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
if let (Some(current_result), Some(short_circuit_value)) = if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value) (result, short_circuit_value)
@@ -192,13 +169,6 @@ impl Model {
} }
// References to empty cells are ignored. If all args are ignored the result is #VALUE! // References to empty cells are ignored. If all args are ignored the result is #VALUE!
CalcResult::EmptyCell => {} CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value) if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)
@@ -246,7 +216,7 @@ impl Model {
} }
// None of the cases matched so we return the default // None of the cases matched so we return the default
// If there is an even number of args is the last one otherwise is #N/A // If there is an even number of args is the last one otherwise is #N/A
if args_count.is_multiple_of(2) { if args_count % 2 == 0 {
return self.evaluate_node_in_context(&args[args_count - 1], cell); return self.evaluate_node_in_context(&args[args_count - 1], cell);
} }
CalcResult::Error { CalcResult::Error {
@@ -262,7 +232,7 @@ impl Model {
if args_count < 2 { if args_count < 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
if !args_count.is_multiple_of(2) { if args_count % 2 != 0 {
// Missing value for last condition // Missing value for last condition
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }

View File

@@ -855,7 +855,7 @@ impl Model {
if left.row != right.row || left.column != right.column { if left.row != right.row || left.column != right.column {
// FIXME: Implicit intersection or dynamic arrays // FIXME: Implicit intersection or dynamic arrays
return CalcResult::Error { return CalcResult::Error {
error: Error::NIMPL, error: Error::ERROR,
origin: cell, origin: cell,
message: "argument must be a reference to a single cell".to_string(), message: "argument must be a reference to a single cell".to_string(),
}; };

View File

@@ -1,100 +0,0 @@
#[macro_export]
macro_rules! single_number_fn {
// The macro takes:
// 1) A function name to define (e.g. fn_sin)
// 2) The operation to apply (e.g. f64::sin)
($fn_name:ident, $op:expr) => {
pub(crate) fn $fn_name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// 1) Check exactly one argument
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
// 2) Try to get a "NumberOrArray"
match self.get_number_or_array(&args[0], cell) {
// -----------------------------------------
// Case A: It's a single number
// -----------------------------------------
Ok(NumberOrArray::Number(f)) => match $op(f) {
Ok(x) => CalcResult::Number(x),
Err(Error::DIV) => CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Divide by 0".to_string(),
},
Err(Error::VALUE) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid number".to_string(),
},
Err(e) => CalcResult::Error {
error: e,
origin: cell,
message: "Unknown error".to_string(),
},
},
// -----------------------------------------
// Case B: It's an array, so apply $op
// element-by-element.
// -----------------------------------------
Ok(NumberOrArray::Array(a)) => {
let mut array = Vec::new();
for row in a {
let mut data_row = Vec::with_capacity(row.len());
for value in row {
match value {
// If Boolean, treat as 0.0 or 1.0
ArrayNode::Boolean(b) => {
let n = if b { 1.0 } else { 0.0 };
match $op(n) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => {
data_row.push(ArrayNode::Error(Error::DIV))
}
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
}
}
// If Number, apply directly
ArrayNode::Number(n) => match $op(n) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
},
// If String, parse to f64 then apply or #VALUE! error
ArrayNode::String(s) => {
let node = match s.parse::<f64>() {
Ok(f) => match $op(f) {
Ok(x) => ArrayNode::Number(x),
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
Err(e) => ArrayNode::Error(e),
},
Err(_) => ArrayNode::Error(Error::VALUE),
};
data_row.push(node);
}
// If Error, propagate the error
e @ ArrayNode::Error(_) => {
data_row.push(e);
}
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------
// Case C: It's an Error => just return it
// -----------------------------------------
Err(err_result) => err_result,
}
}
};
}

View File

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

View File

@@ -15,7 +15,6 @@ mod financial_util;
mod information; mod information;
mod logical; mod logical;
mod lookup_and_reference; mod lookup_and_reference;
mod macros;
mod mathematical; mod mathematical;
mod statistical; mod statistical;
mod subtotal; mod subtotal;
@@ -54,9 +53,6 @@ pub enum Function {
Columns, Columns,
Cos, Cos,
Cosh, Cosh,
Log,
Log10,
Ln,
Max, Max,
Min, Min,
Pi, Pi,
@@ -148,30 +144,13 @@ pub enum Function {
// Date and time // Date and time
Date, Date,
Datedif,
Datevalue,
Day, Day,
Edate, Edate,
Eomonth, Eomonth,
Month, Month,
Time,
Timevalue,
Hour,
Minute,
Second,
Now, Now,
Today, Today,
Year, Year,
Networkdays,
NetworkdaysIntl,
Days,
Days360,
Weekday,
Weeknum,
Workday,
WorkdayIntl,
Yearfrac,
Isoweeknum,
// Financial // Financial
Cumipmt, Cumipmt,
@@ -270,7 +249,7 @@ pub enum Function {
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 215> { pub fn into_iter() -> IntoIter<Function, 195> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -297,9 +276,6 @@ impl Function {
Function::Atanh, Function::Atanh,
Function::Abs, Function::Abs,
Function::Pi, Function::Pi,
Function::Ln,
Function::Log,
Function::Log10,
Function::Sqrt, Function::Sqrt,
Function::Sqrtpi, Function::Sqrtpi,
Function::Atan2, Function::Atan2,
@@ -379,26 +355,9 @@ impl Function {
Function::Month, Function::Month,
Function::Eomonth, Function::Eomonth,
Function::Date, Function::Date,
Function::Datedif,
Function::Datevalue,
Function::Edate, Function::Edate,
Function::Networkdays,
Function::NetworkdaysIntl,
Function::Time,
Function::Timevalue,
Function::Hour,
Function::Minute,
Function::Second,
Function::Today, Function::Today,
Function::Now, Function::Now,
Function::Days,
Function::Days360,
Function::Weekday,
Function::Weeknum,
Function::Workday,
Function::WorkdayIntl,
Function::Yearfrac,
Function::Isoweeknum,
Function::Pmt, Function::Pmt,
Function::Pv, Function::Pv,
Function::Rate, Function::Rate,
@@ -528,7 +487,6 @@ impl Function {
Function::Isformula => "_xlfn.ISFORMULA".to_string(), Function::Isformula => "_xlfn.ISFORMULA".to_string(),
Function::Sheet => "_xlfn.SHEET".to_string(), Function::Sheet => "_xlfn.SHEET".to_string(),
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(), Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
_ => self.to_string(), _ => self.to_string(),
} }
} }
@@ -575,10 +533,6 @@ impl Function {
"POWER" => Some(Function::Power), "POWER" => Some(Function::Power),
"ATAN2" => Some(Function::Atan2), "ATAN2" => Some(Function::Atan2),
"LN" => Some(Function::Ln),
"LOG" => Some(Function::Log),
"LOG10" => Some(Function::Log10),
"MAX" => Some(Function::Max), "MAX" => Some(Function::Max),
"MIN" => Some(Function::Min), "MIN" => Some(Function::Min),
"PRODUCT" => Some(Function::Product), "PRODUCT" => Some(Function::Product),
@@ -666,26 +620,9 @@ impl Function {
"EOMONTH" => Some(Function::Eomonth), "EOMONTH" => Some(Function::Eomonth),
"MONTH" => Some(Function::Month), "MONTH" => Some(Function::Month),
"DATE" => Some(Function::Date), "DATE" => Some(Function::Date),
"DATEDIF" => Some(Function::Datedif),
"DATEVALUE" => Some(Function::Datevalue),
"EDATE" => Some(Function::Edate), "EDATE" => Some(Function::Edate),
"NETWORKDAYS" => Some(Function::Networkdays),
"NETWORKDAYS.INTL" => Some(Function::NetworkdaysIntl),
"TIME" => Some(Function::Time),
"TIMEVALUE" => Some(Function::Timevalue),
"HOUR" => Some(Function::Hour),
"MINUTE" => Some(Function::Minute),
"SECOND" => Some(Function::Second),
"TODAY" => Some(Function::Today), "TODAY" => Some(Function::Today),
"NOW" => Some(Function::Now), "NOW" => Some(Function::Now),
"DAYS" | "_XLFN.DAYS" => Some(Function::Days),
"DAYS360" => Some(Function::Days360),
"WEEKDAY" => Some(Function::Weekday),
"WEEKNUM" => Some(Function::Weeknum),
"WORKDAY" => Some(Function::Workday),
"WORKDAY.INTL" => Some(Function::WorkdayIntl),
"YEARFRAC" => Some(Function::Yearfrac),
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
// Financial // Financial
"PMT" => Some(Function::Pmt), "PMT" => Some(Function::Pmt),
"PV" => Some(Function::Pv), "PV" => Some(Function::Pv),
@@ -796,9 +733,6 @@ impl fmt::Display for Function {
Function::Switch => write!(f, "SWITCH"), Function::Switch => write!(f, "SWITCH"),
Function::True => write!(f, "TRUE"), Function::True => write!(f, "TRUE"),
Function::Xor => write!(f, "XOR"), Function::Xor => write!(f, "XOR"),
Function::Log => write!(f, "LOG"),
Function::Log10 => write!(f, "LOG10"),
Function::Ln => write!(f, "LN"),
Function::Sin => write!(f, "SIN"), Function::Sin => write!(f, "SIN"),
Function::Cos => write!(f, "COS"), Function::Cos => write!(f, "COS"),
Function::Tan => write!(f, "TAN"), Function::Tan => write!(f, "TAN"),
@@ -893,26 +827,9 @@ impl fmt::Display for Function {
Function::Month => write!(f, "MONTH"), Function::Month => write!(f, "MONTH"),
Function::Eomonth => write!(f, "EOMONTH"), Function::Eomonth => write!(f, "EOMONTH"),
Function::Date => write!(f, "DATE"), Function::Date => write!(f, "DATE"),
Function::Datedif => write!(f, "DATEDIF"),
Function::Datevalue => write!(f, "DATEVALUE"),
Function::Edate => write!(f, "EDATE"), Function::Edate => write!(f, "EDATE"),
Function::Networkdays => write!(f, "NETWORKDAYS"),
Function::NetworkdaysIntl => write!(f, "NETWORKDAYS.INTL"),
Function::Time => write!(f, "TIME"),
Function::Timevalue => write!(f, "TIMEVALUE"),
Function::Hour => write!(f, "HOUR"),
Function::Minute => write!(f, "MINUTE"),
Function::Second => write!(f, "SECOND"),
Function::Today => write!(f, "TODAY"), Function::Today => write!(f, "TODAY"),
Function::Now => write!(f, "NOW"), Function::Now => write!(f, "NOW"),
Function::Days => write!(f, "DAYS"),
Function::Days360 => write!(f, "DAYS360"),
Function::Weekday => write!(f, "WEEKDAY"),
Function::Weeknum => write!(f, "WEEKNUM"),
Function::Workday => write!(f, "WORKDAY"),
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
Function::Yearfrac => write!(f, "YEARFRAC"),
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
Function::Pmt => write!(f, "PMT"), Function::Pmt => write!(f, "PMT"),
Function::Pv => write!(f, "PV"), Function::Pv => write!(f, "PV"),
Function::Rate => write!(f, "RATE"), Function::Rate => write!(f, "RATE"),
@@ -1032,7 +949,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),
@@ -1040,12 +957,9 @@ 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::Log => self.fn_log(args, cell),
Function::Log10 => self.fn_log10(args, cell),
Function::Ln => self.fn_ln(args, cell),
Function::Sin => self.fn_sin(args, cell), Function::Sin => self.fn_sin(args, cell),
Function::Cos => self.fn_cos(args, cell), Function::Cos => self.fn_cos(args, cell),
Function::Tan => self.fn_tan(args, cell), Function::Tan => self.fn_tan(args, cell),
@@ -1151,26 +1065,9 @@ impl Model {
Function::Eomonth => self.fn_eomonth(args, cell), Function::Eomonth => self.fn_eomonth(args, cell),
Function::Month => self.fn_month(args, cell), Function::Month => self.fn_month(args, cell),
Function::Date => self.fn_date(args, cell), Function::Date => self.fn_date(args, cell),
Function::Datedif => self.fn_datedif(args, cell),
Function::Datevalue => self.fn_datevalue(args, cell),
Function::Edate => self.fn_edate(args, cell), Function::Edate => self.fn_edate(args, cell),
Function::Networkdays => self.fn_networkdays(args, cell),
Function::NetworkdaysIntl => self.fn_networkdays_intl(args, cell),
Function::Time => self.fn_time(args, cell),
Function::Timevalue => self.fn_timevalue(args, cell),
Function::Hour => self.fn_hour(args, cell),
Function::Minute => self.fn_minute(args, cell),
Function::Second => self.fn_second(args, cell),
Function::Today => self.fn_today(args, cell), Function::Today => self.fn_today(args, cell),
Function::Now => self.fn_now(args, cell), Function::Now => self.fn_now(args, cell),
Function::Days => self.fn_days(args, cell),
Function::Days360 => self.fn_days360(args, cell),
Function::Weekday => self.fn_weekday(args, cell),
Function::Weeknum => self.fn_weeknum(args, cell),
Function::Workday => self.fn_workday(args, cell),
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
Function::Yearfrac => self.fn_yearfrac(args, cell),
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
// Financial // Financial
Function::Pmt => self.fn_pmt(args, cell), Function::Pmt => self.fn_pmt(args, cell),
Function::Pv => self.fn_pv(args, cell), Function::Pv => self.fn_pv(args, cell),
@@ -1316,7 +1213,7 @@ mod tests {
} }
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE // We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
let iter_list = Function::into_iter() let iter_list = Function::into_iter()
.map(|f| format!("{f}").replace('.', "")) .map(|f| format!("{}", f).replace('.', ""))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let len = iter_list.len(); let len = iter_list.len();

View File

@@ -134,13 +134,6 @@ impl Model {
); );
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -172,13 +165,6 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
if count == 0.0 { if count == 0.0 {
@@ -350,7 +336,7 @@ impl Model {
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them? // FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len(); let args_count = args.len();
if args_count < 2 || !args_count.is_multiple_of(2) { if args_count < 2 || args_count % 2 == 1 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
@@ -476,7 +462,7 @@ impl Model {
F: FnMut(f64), F: FnMut(f64),
{ {
let args_count = args.len(); let args_count = args.len();
if args_count < 3 || args_count.is_multiple_of(2) { if args_count < 3 || args_count % 2 == 0 {
return Err(CalcResult::new_args_number_error(cell)); return Err(CalcResult::new_args_number_error(cell));
} }
let arg_0 = self.evaluate_node_in_context(&args[0], cell); let arg_0 = self.evaluate_node_in_context(&args[0], cell);

View File

@@ -182,13 +182,6 @@ impl Model {
} }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0),
CalcResult::Array(_) => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
} }
} }
} }
@@ -433,13 +426,6 @@ impl Model {
| CalcResult::Number(_) | CalcResult::Number(_)
| CalcResult::Boolean(_) | CalcResult::Boolean(_)
| CalcResult::Error { .. } => counta += 1, | CalcResult::Error { .. } => counta += 1,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }

View File

@@ -55,14 +55,14 @@ impl Model {
let mut result = "".to_string(); let mut result = "".to_string();
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::String(value) => result = format!("{result}{value}"), CalcResult::String(value) => result = format!("{}{}", result, value),
CalcResult::Number(value) => result = format!("{result}{value}"), CalcResult::Number(value) => result = format!("{}{}", result, value),
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{result}TRUE"); result = format!("{}TRUE", result);
} else { } else {
result = format!("{result}FALSE"); result = format!("{}FALSE", result);
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
@@ -82,37 +82,25 @@ impl Model {
column, column,
}) { }) {
CalcResult::String(value) => { CalcResult::String(value) => {
result = format!("{result}{value}"); result = format!("{}{}", result, value);
}
CalcResult::Number(value) => {
result = format!("{}{}", result, value)
} }
CalcResult::Number(value) => result = format!("{result}{value}"),
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{result}TRUE"); result = format!("{}TRUE", result);
} else { } else {
result = format!("{result}FALSE"); result = format!("{}FALSE", result);
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Range { .. } => {} CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
} }
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
CalcResult::String(result) CalcResult::String(result)
@@ -137,13 +125,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let format_code = match self.get_string(&args[1], cell) { let format_code = match self.get_string(&args[1], cell) {
Ok(s) => s, Ok(s) => s,
@@ -280,7 +261,7 @@ impl Model {
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -299,13 +280,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::Number(s.chars().count() as f64); return CalcResult::Number(s.chars().count() as f64);
} }
@@ -315,7 +289,7 @@ impl Model {
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -334,13 +308,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.trim().to_owned()); return CalcResult::String(s.trim().to_owned());
} }
@@ -350,7 +317,7 @@ impl Model {
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -369,13 +336,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.to_lowercase()); return CalcResult::String(s.to_lowercase());
} }
@@ -385,7 +345,7 @@ impl Model {
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -410,13 +370,6 @@ impl Model {
message: "Empty cell".to_string(), message: "Empty cell".to_string(),
} }
} }
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
match s.chars().next() { match s.chars().next() {
@@ -439,7 +392,7 @@ impl Model {
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -458,13 +411,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.to_uppercase()); return CalcResult::String(s.to_uppercase());
} }
@@ -476,7 +422,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -495,13 +441,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let num_chars = if args.len() == 2 { let num_chars = if args.len() == 2 {
match self.evaluate_node_in_context(&args[1], cell) { match self.evaluate_node_in_context(&args[1], cell) {
@@ -532,13 +471,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} else { } else {
1 1
@@ -558,7 +490,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -577,13 +509,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let num_chars = if args.len() == 2 { let num_chars = if args.len() == 2 {
match self.evaluate_node_in_context(&args[1], cell) { match self.evaluate_node_in_context(&args[1], cell) {
@@ -614,13 +539,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} else { } else {
1 1
@@ -640,7 +558,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -659,13 +577,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let start_num = match self.evaluate_node_in_context(&args[1], cell) { let start_num = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Number(v) => { CalcResult::Number(v) => {
@@ -730,13 +641,6 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let mut result = "".to_string(); let mut result = "".to_string();
let mut count: usize = 0; let mut count: usize = 0;
@@ -1079,13 +983,6 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg | CalcResult::Range { .. } => {} CalcResult::EmptyArg | CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -1105,13 +1002,6 @@ impl Model {
} }
} }
CalcResult::EmptyArg => {} CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
let result = values.join(&delimiter); let result = values.join(&delimiter);
@@ -1235,11 +1125,6 @@ impl Model {
} }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0),
CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
} }
} }

View File

@@ -110,7 +110,7 @@ pub(crate) fn from_wildcard_to_regex(
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?") // And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
if exact { if exact {
return regex::Regex::new(&format!("^{reg}$")); return regex::Regex::new(&format!("^{}$", reg));
} }
regex::Regex::new(reg) regex::Regex::new(reg)
} }
@@ -393,8 +393,10 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResul
// An error will match an error (never a string that is an error) // An error will match an error (never a string that is an error)
Box::new(move |x| result_is_equal_to_error(x, &error.to_string())) Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
} }
CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false), CalcResult::Range { left: _, right: _ } => {
CalcResult::Array(_) => Box::new(move |_x| false), // TODO: Implicit Intersection
Box::new(move |_x| false)
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty), CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
} }
} }

View File

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

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, sync::OnceLock}; use std::collections::HashMap;
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Booleans { pub struct Booleans {
@@ -30,17 +31,14 @@ pub struct Language {
pub errors: Errors, pub errors: Errors,
} }
static LANGUAGES: OnceLock<HashMap<String, Language>> = OnceLock::new();
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fn get_languages() -> &'static HashMap<String, Language> { static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
LANGUAGES.get_or_init(|| { bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file") });
})
}
pub fn get_language(id: &str) -> Result<&Language, String> { pub fn get_language(id: &str) -> Result<&Language, String> {
get_languages() let language = LANGUAGES
.get(id) .get(id)
.ok_or_else(|| format!("Language is not supported: '{id}'")) .ok_or(format!("Language is not supported: '{}'", id))?;
Ok(language)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,15 +7,12 @@ mod test_column_width;
mod test_criteria; mod test_criteria;
mod test_currency; mod test_currency;
mod test_date_and_time; mod test_date_and_time;
mod test_datedif_leap_month_end;
mod test_days360_month_end;
mod test_error_propagation; mod test_error_propagation;
mod test_fn_average; mod test_fn_average;
mod test_fn_averageifs; 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;
@@ -29,8 +26,8 @@ mod test_fn_sum;
mod test_fn_sumifs; mod test_fn_sumifs;
mod test_fn_textbefore; mod test_fn_textbefore;
mod test_fn_textjoin; mod test_fn_textjoin;
mod test_fn_time;
mod test_fn_unicode; mod test_fn_unicode;
mod test_forward_references;
mod test_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
mod test_math; mod test_math;
@@ -39,39 +36,27 @@ 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_weekday_return_types;
mod test_weeknum_return_types;
mod test_workbook; mod test_workbook;
mod test_worksheet; mod test_worksheet;
mod test_yearfrac_basis;
pub(crate) mod util; pub(crate) mod util;
mod engineering; mod engineering;
mod test_fn_offset; mod test_fn_offset;
mod test_number_format; mod test_number_format;
mod test_arrays;
mod test_escape_quotes; mod test_escape_quotes;
mod test_extend; mod test_extend;
mod test_fn_fv; mod test_fn_fv;
mod test_fn_round;
mod test_fn_type; mod test_fn_type;
mod test_frozen_rows_and_columns; mod test_frozen_rows_and_columns;
mod test_geomean; mod test_geomean;
mod test_get_cell_content; mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_ln;
mod test_log;
mod test_log10;
mod test_networkdays;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;
mod test_today; mod test_today;

View File

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

View File

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

View File

@@ -6,11 +6,6 @@
/// We can also enter examples that illustrate/document a part of the function /// We can also enter examples that illustrate/document a part of the function
use crate::{cell::CellValue, test::util::new_empty_model}; use crate::{cell::CellValue, test::util::new_empty_model};
// Excel uses a serial date system where Jan 1, 1900 = 1 (though it treats 1900 as a leap year)
// Most test dates are documented inline, but we define boundary values here:
const EXCEL_MAX_DATE: f64 = 2958465.0; // Dec 31, 9999 - used in boundary tests
const EXCEL_INVALID_DATE: f64 = 2958466.0; // One day past max - used in error tests
#[test] #[test]
fn test_fn_date_arguments() { fn test_fn_date_arguments() {
let mut model = new_empty_model(); let mut model = new_empty_model();
@@ -42,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"),
@@ -69,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]
@@ -137,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");
@@ -157,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");
@@ -177,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");
@@ -209,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");
@@ -221,382 +213,3 @@ fn test_date_early_dates() {
Ok(CellValue::Number(61.0)) Ok(CellValue::Number(61.0))
); );
} }
#[test]
fn test_days_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=DAYS(44570,44561)");
model._set("A2", "=DAYS(44561,44570)"); // Reversed order
model._set("A3", "=DAYS(44561,44561)");
// Edge cases
model._set("A4", "=DAYS(1,2)"); // Early dates
model._set(
"A5",
&format!("=DAYS({},{})", EXCEL_MAX_DATE, EXCEL_MAX_DATE - 1.0),
); // Near max date
// Error cases - wrong argument count
model._set("A6", "=DAYS()");
model._set("A7", "=DAYS(44561)");
model._set("A8", "=DAYS(44561,44570,1)");
// Error cases - invalid dates
model._set("A9", "=DAYS(-1,44561)");
model._set("A10", &format!("=DAYS(44561,{EXCEL_INVALID_DATE})"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"9");
assert_eq!(model._get_text("A2"), *"-9");
assert_eq!(model._get_text("A3"), *"0");
assert_eq!(model._get_text("A4"), *"-1"); // DAYS(1,2) = 1-2 = -1
assert_eq!(model._get_text("A5"), *"1");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#ERROR!");
assert_eq!(model._get_text("A9"), *"#NUM!");
assert_eq!(model._get_text("A10"), *"#NUM!");
}
#[test]
fn test_days360_function() {
let mut model = new_empty_model();
// Basic functionality with different basis values
model._set("A1", "=DAYS360(44196,44560)"); // Default basis (US 30/360)
model._set("A2", "=DAYS360(44196,44560,FALSE)"); // US 30/360 explicitly
model._set("A3", "=DAYS360(44196,44560,TRUE)"); // European 30/360
// Same date
model._set("A4", "=DAYS360(44561,44561)");
model._set("A5", "=DAYS360(44561,44561,TRUE)");
// Reverse order (negative result)
model._set("A6", "=DAYS360(44560,44196)");
model._set("A7", "=DAYS360(44560,44196,TRUE)");
// Edge cases
model._set("A8", "=DAYS360(1,2)");
model._set("A9", "=DAYS360(1,2,FALSE)");
// Error cases - wrong argument count
model._set("A10", "=DAYS360()");
model._set("A11", "=DAYS360(44561)");
model._set("A12", "=DAYS360(44561,44570,TRUE,1)");
// Error cases - invalid dates
model._set("A13", "=DAYS360(-1,44561)");
model._set("A14", &format!("=DAYS360(44561,{EXCEL_INVALID_DATE})"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"360");
assert_eq!(model._get_text("A2"), *"360");
assert_eq!(model._get_text("A3"), *"360");
assert_eq!(model._get_text("A4"), *"0");
assert_eq!(model._get_text("A5"), *"0");
assert_eq!(model._get_text("A6"), *"-360");
assert_eq!(model._get_text("A7"), *"-360");
assert_eq!(model._get_text("A8"), *"1");
assert_eq!(model._get_text("A9"), *"1");
assert_eq!(model._get_text("A10"), *"#ERROR!");
assert_eq!(model._get_text("A11"), *"#ERROR!");
assert_eq!(model._get_text("A12"), *"#ERROR!");
assert_eq!(model._get_text("A13"), *"#NUM!");
assert_eq!(model._get_text("A14"), *"#NUM!");
}
#[test]
fn test_weekday_function() {
let mut model = new_empty_model();
// Test return_type parameter variations with one known date (Friday 44561)
model._set("A1", "=WEEKDAY(44561)"); // Default: Sun=1, Fri=6
model._set("A2", "=WEEKDAY(44561,2)"); // Mon=1, Fri=5
model._set("A3", "=WEEKDAY(44561,3)"); // Mon=0, Fri=4
// Test boundary days (Sun/Mon) to verify return_type logic
model._set("A4", "=WEEKDAY(44556,1)"); // Sunday: should be 1
model._set("A5", "=WEEKDAY(44556,2)"); // Sunday: should be 7
model._set("A6", "=WEEKDAY(44557,2)"); // Monday: should be 1
// Error cases
model._set("A7", "=WEEKDAY()"); // Wrong arg count
model._set("A8", "=WEEKDAY(44561,0)"); // Invalid return_type
model._set("A9", "=WEEKDAY(-1)"); // Invalid date
model.evaluate();
// Core functionality
assert_eq!(model._get_text("A1"), *"6"); // Friday default
assert_eq!(model._get_text("A2"), *"5"); // Friday Mon=1
assert_eq!(model._get_text("A3"), *"4"); // Friday Mon=0
// Boundary verification
assert_eq!(model._get_text("A4"), *"1"); // Sunday Sun=1
assert_eq!(model._get_text("A5"), *"7"); // Sunday Mon=1
assert_eq!(model._get_text("A6"), *"1"); // Monday Mon=1
// Error cases
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#VALUE!");
assert_eq!(model._get_text("A9"), *"#NUM!");
}
#[test]
fn test_weeknum_function() {
let mut model = new_empty_model();
// Test different return_type values (1=week starts Sunday, 2=week starts Monday)
model._set("A1", "=WEEKNUM(44561)"); // Default return_type=1
model._set("A2", "=WEEKNUM(44561,1)"); // Sunday start
model._set("A3", "=WEEKNUM(44561,2)"); // Monday start
// Test year boundaries
model._set("A4", "=WEEKNUM(43831,1)"); // Jan 1, 2020 (Wednesday)
model._set("A5", "=WEEKNUM(43831,2)"); // Jan 1, 2020 (Wednesday)
model._set("A6", "=WEEKNUM(44196,1)"); // Dec 31, 2020 (Thursday)
model._set("A7", "=WEEKNUM(44196,2)"); // Dec 31, 2020 (Thursday)
// Test first and last weeks of year
model._set("A8", "=WEEKNUM(44197,1)"); // Jan 1, 2021 (Friday)
model._set("A9", "=WEEKNUM(44197,2)"); // Jan 1, 2021 (Friday)
model._set("A10", "=WEEKNUM(44561,1)"); // Dec 31, 2021 (Friday)
model._set("A11", "=WEEKNUM(44561,2)"); // Dec 31, 2021 (Friday)
// Error cases - wrong argument count
model._set("A12", "=WEEKNUM()");
model._set("A13", "=WEEKNUM(44561,1,1)");
// Error cases - invalid return_type
model._set("A14", "=WEEKNUM(44561,0)");
model._set("A15", "=WEEKNUM(44561,3)");
model._set("A16", "=WEEKNUM(44561,-1)");
// Error cases - invalid dates
model._set("A17", "=WEEKNUM(-1)");
model._set("A18", &format!("=WEEKNUM({EXCEL_INVALID_DATE})"));
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"53"); // Week 53
assert_eq!(model._get_text("A2"), *"53"); // Week 53 (Sunday start)
assert_eq!(model._get_text("A3"), *"53"); // Week 53 (Monday start)
// Year boundary tests
assert_eq!(model._get_text("A4"), *"1"); // Jan 1, 2020 (Sunday start)
assert_eq!(model._get_text("A5"), *"1"); // Jan 1, 2020 (Monday start)
assert_eq!(model._get_text("A6"), *"53"); // Dec 31, 2020 (Sunday start)
assert_eq!(model._get_text("A7"), *"53"); // Dec 31, 2020 (Monday start)
// 2021 tests
assert_eq!(model._get_text("A8"), *"1"); // Jan 1, 2021 (Sunday start)
assert_eq!(model._get_text("A9"), *"1"); // Jan 1, 2021 (Monday start)
assert_eq!(model._get_text("A10"), *"53"); // Dec 31, 2021 (Sunday start)
assert_eq!(model._get_text("A11"), *"53"); // Dec 31, 2021 (Monday start)
// Error cases
assert_eq!(model._get_text("A12"), *"#ERROR!");
assert_eq!(model._get_text("A13"), *"#ERROR!");
assert_eq!(model._get_text("A14"), *"#VALUE!");
assert_eq!(model._get_text("A15"), *"#VALUE!");
assert_eq!(model._get_text("A16"), *"#VALUE!");
assert_eq!(model._get_text("A17"), *"#NUM!");
assert_eq!(model._get_text("A18"), *"#NUM!");
}
#[test]
fn test_workday_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=WORKDAY(44560,1)");
model._set("A2", "=WORKDAY(44561,-1)");
model._set("A3", "=WORKDAY(44561,0)");
model._set("A4", "=WORKDAY(44560,5)");
// Test with holidays
model._set("B1", "44561");
model._set("A5", "=WORKDAY(44560,1,B1)"); // Should skip the holiday
model._set("B2", "44562");
model._set("B3", "44563");
model._set("A6", "=WORKDAY(44560,3,B1:B3)"); // Multiple holidays
// Test starting on weekend
model._set("A7", "=WORKDAY(44562,1)"); // Saturday start
model._set("A8", "=WORKDAY(44563,1)"); // Sunday start
// Test negative workdays
model._set("A9", "=WORKDAY(44565,-3)"); // Go backwards 3 days
model._set("A10", "=WORKDAY(44565,-5,B1:B3)"); // Backwards with holidays
// Edge cases
model._set("A11", "=WORKDAY(1,1)"); // Early date
model._set("A12", "=WORKDAY(100000,10)"); // Large numbers
// Error cases - wrong argument count
model._set("A13", "=WORKDAY()");
model._set("A14", "=WORKDAY(44560)");
model._set("A15", "=WORKDAY(44560,1,B1,B2)");
// Error cases - invalid dates
model._set("A16", "=WORKDAY(-1,1)");
model._set("A17", &format!("=WORKDAY({EXCEL_INVALID_DATE},1)"));
// Error cases - invalid holiday dates
model._set("B4", "-1");
model._set("A18", "=WORKDAY(44560,1,B4)");
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"44561"); // 1 day forward
assert_eq!(model._get_text("A2"), *"44560"); // 1 day backward
assert_eq!(model._get_text("A3"), *"44561"); // 0 days
assert_eq!(model._get_text("A4"), *"44567"); // 5 days forward
// With holidays
assert_eq!(model._get_text("A5"), *"44564"); // Skip holiday, go to Monday
assert_eq!(model._get_text("A6"), *"44566"); // Skip multiple holidays
// Weekend starts
assert_eq!(model._get_text("A7"), *"44564"); // From Saturday
assert_eq!(model._get_text("A8"), *"44564"); // From Sunday
// Negative workdays
assert_eq!(model._get_text("A9"), *"44560"); // 3 days back
assert_eq!(model._get_text("A10"), *"44557"); // 5 days back with holidays
// Edge cases
assert_eq!(model._get_text("A11"), *"2"); // Early date
assert_eq!(model._get_text("A12"), *"100014"); // Large numbers
// Error cases
assert_eq!(model._get_text("A13"), *"#ERROR!");
assert_eq!(model._get_text("A14"), *"#ERROR!");
assert_eq!(model._get_text("A15"), *"#ERROR!");
assert_eq!(model._get_text("A16"), *"#NUM!");
assert_eq!(model._get_text("A17"), *"#NUM!");
assert_eq!(model._get_text("A18"), *"#NUM!"); // Invalid holiday
}
#[test]
fn test_workday_intl_function() {
let mut model = new_empty_model();
// Test key weekend mask types
model._set("A1", "=WORKDAY.INTL(44560,1,1)"); // Numeric: standard (Sat-Sun)
model._set("A2", "=WORKDAY.INTL(44560,1,2)"); // Numeric: Sun-Mon
model._set("A3", "=WORKDAY.INTL(44560,1,\"0000001\")"); // String: Sunday only
model._set("A4", "=WORKDAY.INTL(44560,1,\"1100000\")"); // String: Mon-Tue
// Test with holidays
model._set("B1", "44561");
model._set("A5", "=WORKDAY.INTL(44560,2,1,B1)"); // Standard + holiday
model._set("A6", "=WORKDAY.INTL(44560,2,7,B1)"); // Fri-Sat + holiday
// Basic edge cases
model._set("A7", "=WORKDAY.INTL(44561,0,1)"); // Zero days
model._set("A8", "=WORKDAY.INTL(44565,-1,1)"); // Negative days
// Error cases
model._set("A9", "=WORKDAY.INTL()"); // Wrong arg count
model._set("A10", "=WORKDAY.INTL(44560,1,0)"); // Invalid weekend mask
model._set("A11", "=WORKDAY.INTL(44560,1,\"123\")"); // Invalid string mask
model._set("A12", "=WORKDAY.INTL(-1,1,1)"); // Invalid date
model.evaluate();
// Weekend mask functionality
assert_eq!(model._get_text("A1"), *"44561"); // Standard weekend
assert_eq!(model._get_text("A2"), *"44561"); // Sun-Mon weekend
assert_eq!(model._get_text("A3"), *"44561"); // Sunday only
assert_eq!(model._get_text("A4"), *"44561"); // Mon-Tue weekend
// With holidays
assert_eq!(model._get_text("A5"), *"44565"); // Skip holiday + standard weekend
assert_eq!(model._get_text("A6"), *"44564"); // Skip holiday + Fri-Sat weekend
// Edge cases
assert_eq!(model._get_text("A7"), *"44561"); // Zero days
assert_eq!(model._get_text("A8"), *"44564"); // Negative days
// Error cases
assert_eq!(model._get_text("A9"), *"#ERROR!");
assert_eq!(model._get_text("A10"), *"#NUM!");
assert_eq!(model._get_text("A11"), *"#VALUE!");
assert_eq!(model._get_text("A12"), *"#NUM!");
}
#[test]
fn test_yearfrac_function() {
let mut model = new_empty_model();
// Test key basis values (not exhaustive - just verify parameter works)
model._set("A1", "=YEARFRAC(44561,44926)"); // Default (30/360)
model._set("A2", "=YEARFRAC(44561,44926,1)"); // Actual/actual
model._set("A3", "=YEARFRAC(44561,44926,4)"); // European 30/360
// Edge cases
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
model._set("A5", "=YEARFRAC(44926,44561,1)"); // Reverse = negative
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
// Error cases
model._set("A7", "=YEARFRAC()"); // Wrong arg count
model._set("A8", "=YEARFRAC(44561,44926,5)"); // Invalid basis
model._set("A9", "=YEARFRAC(-1,44926,1)"); // Invalid date
model.evaluate();
// Basic functionality (approximate values expected)
assert_eq!(model._get_text("A1"), *"1"); // About 1 year
assert_eq!(model._get_text("A2"), *"1"); // About 1 year
assert_eq!(model._get_text("A3"), *"1"); // About 1 year
// Edge cases
assert_eq!(model._get_text("A4"), *"0"); // Same date
assert_eq!(model._get_text("A5"), *"-1"); // Negative
assert_eq!(model._get_text("A6"), *"1"); // Exact year
// Error cases
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#NUM!"); // Invalid basis should return #NUM!
assert_eq!(model._get_text("A9"), *"#NUM!");
}
#[test]
fn test_isoweeknum_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=ISOWEEKNUM(44563)"); // Mid-week date
model._set("A2", "=ISOWEEKNUM(44561)"); // Year-end date
// Key ISO week boundaries (just critical cases)
model._set("A3", "=ISOWEEKNUM(44197)"); // Jan 1, 2021 (Fri) -> Week 53 of 2020
model._set("A4", "=ISOWEEKNUM(44200)"); // Jan 4, 2021 (Mon) -> Week 1 of 2021
model._set("A5", "=ISOWEEKNUM(44564)"); // Jan 3, 2022 (Mon) -> Week 1 of 2022
// Error cases
model._set("A6", "=ISOWEEKNUM()"); // Wrong arg count
model._set("A7", "=ISOWEEKNUM(-1)"); // Invalid date
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"52");
assert_eq!(model._get_text("A2"), *"52");
// ISO week boundaries
assert_eq!(model._get_text("A3"), *"53"); // Week 53 of previous year
assert_eq!(model._get_text("A4"), *"1"); // Week 1 of current year
assert_eq!(model._get_text("A5"), *"1"); // Week 1 of next year
// Error cases
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#NUM!");
}

View File

@@ -1,33 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_datedif_yd_leap_year_edge_cases() {
let mut model = new_empty_model();
// 29 Feb 2020 → 28 Feb 2021 (should be 0 days)
model._set("A1", "=DATEDIF(\"29/2/2020\", \"28/2/2021\", \"YD\")");
// 29 Feb 2020 → 1 Mar 2021 (should be 1 day)
model._set("A2", "=DATEDIF(\"29/2/2020\", \"2021-03-01\", \"YD\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0");
assert_eq!(model._get_text("A2"), *"1");
}
#[test]
fn test_datedif_md_month_end_edge_cases() {
let mut model = new_empty_model();
// 31 Jan 2021 → 28 Feb 2021 (non-leap) => 28
model._set("B1", "=DATEDIF(\"31/1/2021\", \"28/2/2021\", \"MD\")");
// 31 Jan 2020 → 29 Feb 2020 (leap) => 29
model._set("B2", "=DATEDIF(\"31/1/2020\", \"29/2/2020\", \"MD\")");
model.evaluate();
assert_eq!(model._get_text("B1"), *"28");
assert_eq!(model._get_text("B2"), *"29");
}

View File

@@ -1,43 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_days360_month_end_us() {
let mut model = new_empty_model();
// 31 Jan 2021 -> 28 Feb 2021 (non-leap)
model._set("A1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28))");
// 31 Jan 2020 -> 28 Feb 2020 (leap year not last day of Feb)
model._set("A2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,28))");
// 28 Feb 2020 -> 31 Mar 2020 (leap year span crossing month ends)
model._set("A3", "=DAYS360(DATE(2020,2,28),DATE(2020,3,31))");
// 30 Apr 2021 -> 31 May 2021 (end-of-month adjustment rule)
model._set("A4", "=DAYS360(DATE(2021,4,30),DATE(2021,5,31))");
model.evaluate();
assert_eq!(model._get_text("A1"), *"30");
assert_eq!(model._get_text("A2"), *"28");
assert_eq!(model._get_text("A3"), *"33");
assert_eq!(model._get_text("A4"), *"30");
}
#[test]
fn test_days360_month_end_european() {
let mut model = new_empty_model();
// European basis = TRUE (or 1)
model._set("B1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28),TRUE)");
model._set("B2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,29),TRUE)");
model._set("B3", "=DAYS360(DATE(2021,8,31),DATE(2021,9,30),TRUE)");
model.evaluate();
assert_eq!(model._get_text("B1"), *"28");
assert_eq!(model._get_text("B2"), *"29");
assert_eq!(model._get_text("B3"), *"30");
}

View File

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

View File

@@ -1,182 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::types::Cell;
// Helper to evaluate a formula and return the formatted text
fn eval_formula(formula: &str) -> String {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
model._get_text("A1")
}
// Helper that evaluates a formula and returns the raw value of A1 as a Result<f64, String>
fn eval_formula_raw_number(formula: &str) -> Result<f64, String> {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
match model._get_cell("A1") {
Cell::NumberCell { v, .. } => Ok(*v),
Cell::BooleanCell { v, .. } => Ok(if *v { 1.0 } else { 0.0 }),
Cell::ErrorCell { ei, .. } => Err(format!("{}", ei)),
_ => Err(model._get_text("A1")),
}
}
#[test]
fn test_datevalue_basic_numeric() {
// DATEVALUE should return the serial number representing the date, **not** a formatted date
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2/1/2023\")").unwrap(),
44958.0
);
}
#[test]
fn test_datevalue_mmdd_with_leading_zero() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"02/01/2023\")").unwrap(),
44958.0
); // 1-Feb-2023
}
#[test]
fn test_datevalue_iso() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023-01-02\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_month_name() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2-Jan-23\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_ambiguous_ddmm() {
// 01/02/2023 interpreted as MM/DD -> 2-Jan-2023
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"01/02/2023\")").unwrap(),
44929.0
);
}
#[test]
fn test_datevalue_ddmm_unambiguous() {
// 15/01/2023 should be 15-Jan-2023 since 15 cannot be month
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"15/01/2023\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_leap_day() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"29/02/2020\")").unwrap(),
43890.0
);
}
#[test]
fn test_datevalue_year_first_text_month() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023/Jan/15\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_mmdd_with_day_gt_12() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"6/15/2021\")").unwrap(),
44373.0
);
}
#[test]
fn test_datevalue_error_conditions() {
let cases = [
"=DATEVALUE(\"31/04/2023\")", // invalid day (Apr has 30 days)
"=DATEVALUE(\"13/13/2023\")", // invalid month
"=DATEVALUE(\"not a date\")", // non-date text
];
for formula in cases {
let result = eval_formula(formula);
assert_eq!(result, *"#VALUE!", "Expected #VALUE! for {}", formula);
}
}
// Helper to set and evaluate a single DATEDIF call
fn eval_datedif(unit: &str) -> String {
let mut model = new_empty_model();
let formula = format!("=DATEDIF(\"2020-01-01\", \"2021-06-15\", \"{}\")", unit);
model._set("A1", &formula);
model.evaluate();
model._get_text("A1")
}
#[test]
fn test_datedif_y() {
assert_eq!(eval_datedif("Y"), *"1");
}
#[test]
fn test_datedif_m() {
assert_eq!(eval_datedif("M"), *"17");
}
#[test]
fn test_datedif_d() {
assert_eq!(eval_datedif("D"), *"531");
}
#[test]
fn test_datedif_ym() {
assert_eq!(eval_datedif("YM"), *"5");
}
#[test]
fn test_datedif_yd() {
assert_eq!(eval_datedif("YD"), *"165");
}
#[test]
fn test_datedif_md() {
assert_eq!(eval_datedif("MD"), *"14");
}
#[test]
fn test_datedif_edge_and_error_cases() {
let mut model = new_empty_model();
// Leap-year spanning
model._set("A1", "=DATEDIF(\"28/2/2020\", \"1/3/2020\", \"D\")");
// End date before start date => #NUM!
model._set("A2", "=DATEDIF(\"1/2/2021\", \"1/1/2021\", \"D\")");
// Invalid unit => #VALUE!
model._set("A3", "=DATEDIF(\"1/1/2020\", \"1/1/2021\", \"Z\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"#VALUE!");
}
#[test]
fn test_datedif_mixed_case_unit() {
assert_eq!(eval_datedif("yD"), *"165"); // mixed-case should work
}
#[test]
fn test_datedif_error_propagation() {
// Invalid date in arguments should propagate #VALUE!
let mut model = new_empty_model();
model._set("A1", "=DATEDIF(\"bad\", \"bad\", \"Y\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
}

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

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

View File

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

View File

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

View File

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

View File

@@ -1,520 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
// Helper constants for common time values with detailed documentation
const MIDNIGHT: &str = "0"; // 00:00:00 = 0/24 = 0
const NOON: &str = "0.5"; // 12:00:00 = 12/24 = 0.5
const TIME_14_30: &str = "0.604166667"; // 14:30:00 = 14.5/24 ≈ 0.604166667
const TIME_14_30_45: &str = "0.6046875"; // 14:30:45 = 14.5125/24 = 0.6046875
const TIME_14_30_59: &str = "0.604849537"; // 14:30:59 (from floored fractional inputs)
const TIME_23_59_59: &str = "0.999988426"; // 23:59:59 = 23.99972.../24 ≈ 0.999988426
// Excel documentation test values with explanations
const TIME_2_24_AM: &str = "0.1"; // 2:24 AM = 2.4/24 = 0.1
const TIME_2_PM: &str = "0.583333333"; // 2:00 PM = 14/24 ≈ 0.583333333
const TIME_6_45_PM: &str = "0.78125"; // 6:45 PM = 18.75/24 = 0.78125
const TIME_6_35_AM: &str = "0.274305556"; // 6:35 AM = 6.583333.../24 ≈ 0.274305556
const TIME_2_30_AM: &str = "0.104166667"; // 2:30 AM = 2.5/24 ≈ 0.104166667
const TIME_1_AM: &str = "0.041666667"; // 1:00 AM = 1/24 ≈ 0.041666667
const TIME_9_PM: &str = "0.875"; // 9:00 PM = 21/24 = 0.875
const TIME_2_AM: &str = "0.083333333"; // 2:00 AM = 2/24 ≈ 0.083333333
// Additional helper: 1-second past midnight (00:00:01)
const TIME_00_00_01: &str = "0.000011574"; // 1 second = 1/86400 ≈ 0.000011574
/// Helper function to set up and evaluate a model with time expressions
fn test_time_expressions(expressions: &[(&str, &str)]) -> crate::model::Model {
let mut model = new_empty_model();
for (cell, formula) in expressions {
model._set(cell, formula);
}
model.evaluate();
model
}
/// Helper function to test component extraction for a given time value
/// Returns (hour, minute, second) as strings
fn test_component_extraction(time_value: &str) -> (String, String, String) {
let model = test_time_expressions(&[
("A1", &format!("=HOUR({time_value})")),
("B1", &format!("=MINUTE({time_value})")),
("C1", &format!("=SECOND({time_value})")),
]);
(
model._get_text("A1").to_string(),
model._get_text("B1").to_string(),
model._get_text("C1").to_string(),
)
}
#[test]
fn test_excel_timevalue_compatibility() {
// Test cases based on Excel's official documentation and examples
let model = test_time_expressions(&[
// Excel documentation examples
("A1", "=TIMEVALUE(\"2:24 AM\")"), // Should be 0.1
("A2", "=TIMEVALUE(\"2 PM\")"), // Should be 0.583333... (14/24)
("A3", "=TIMEVALUE(\"6:45 PM\")"), // Should be 0.78125 (18.75/24)
("A4", "=TIMEVALUE(\"18:45\")"), // Same as above, 24-hour format
// Date-time format (date should be ignored)
("B1", "=TIMEVALUE(\"22-Aug-2011 6:35 AM\")"), // Should be ~0.2743
("B2", "=TIMEVALUE(\"2023-01-01 14:30:00\")"), // Should be 0.604166667
// Edge cases that Excel should support
("C1", "=TIMEVALUE(\"12:00 AM\")"), // Midnight: 0
("C2", "=TIMEVALUE(\"12:00 PM\")"), // Noon: 0.5
("C3", "=TIMEVALUE(\"11:59:59 PM\")"), // Almost midnight: 0.999988426
// Single digit variations
("D1", "=TIMEVALUE(\"1 AM\")"), // 1:00 AM
("D2", "=TIMEVALUE(\"9 PM\")"), // 9:00 PM
("D3", "=TIMEVALUE(\"12 AM\")"), // Midnight
("D4", "=TIMEVALUE(\"12 PM\")"), // Noon
]);
// Excel documentation examples - verify exact values
assert_eq!(model._get_text("A1"), *TIME_2_24_AM); // 2:24 AM
assert_eq!(model._get_text("A2"), *TIME_2_PM); // 2 PM = 14:00
assert_eq!(model._get_text("A3"), *TIME_6_45_PM); // 6:45 PM = 18:45
assert_eq!(model._get_text("A4"), *TIME_6_45_PM); // 18:45 (24-hour)
// Date-time formats (date ignored, extract time only)
assert_eq!(model._get_text("B1"), *TIME_6_35_AM); // 6:35 AM ≈ 0.2743
assert_eq!(model._get_text("B2"), *TIME_14_30); // 14:30:00
// Edge cases
assert_eq!(model._get_text("C1"), *MIDNIGHT); // 12:00 AM = 00:00
assert_eq!(model._get_text("C2"), *NOON); // 12:00 PM = 12:00
assert_eq!(model._get_text("C3"), *TIME_23_59_59); // 11:59:59 PM
// Single digit hours
assert_eq!(model._get_text("D1"), *TIME_1_AM); // 1:00 AM
assert_eq!(model._get_text("D2"), *TIME_9_PM); // 9:00 PM = 21:00
assert_eq!(model._get_text("D3"), *MIDNIGHT); // 12 AM = 00:00
assert_eq!(model._get_text("D4"), *NOON); // 12 PM = 12:00
}
#[test]
fn test_time_function_basic_cases() {
let model = test_time_expressions(&[
("A1", "=TIME(0,0,0)"), // Midnight
("A2", "=TIME(12,0,0)"), // Noon
("A3", "=TIME(14,30,0)"), // 2:30 PM
("A4", "=TIME(23,59,59)"), // Max time
]);
assert_eq!(model._get_text("A1"), *MIDNIGHT);
assert_eq!(model._get_text("A2"), *NOON);
assert_eq!(model._get_text("A3"), *TIME_14_30);
assert_eq!(model._get_text("A4"), *TIME_23_59_59);
}
#[test]
fn test_time_function_normalization() {
let model = test_time_expressions(&[
("A1", "=TIME(25,0,0)"), // Hours > 24 wrap around
("A2", "=TIME(48,0,0)"), // 48 hours = 0 (2 full days)
("A3", "=TIME(0,90,0)"), // 90 minutes = 1.5 hours
("A4", "=TIME(0,0,90)"), // 90 seconds = 1.5 minutes
("A5", "=TIME(14.9,30.9,59.9)"), // Fractional inputs floored to 14:30:59
]);
assert_eq!(model._get_text("A1"), *TIME_1_AM); // 1:00:00
assert_eq!(model._get_text("A2"), *MIDNIGHT); // 0:00:00
assert_eq!(model._get_text("A3"), *"0.0625"); // 1:30:00
assert_eq!(model._get_text("A4"), *"0.001041667"); // 0:01:30
assert_eq!(model._get_text("A5"), *TIME_14_30_59); // 14:30:59 (floored)
}
#[test]
fn test_time_function_precision_edge_cases() {
let model = test_time_expressions(&[
// High precision fractional seconds
("A1", "=TIME(14,30,45.999)"), // Fractional seconds should be floored
("A2", "=SECOND(TIME(14,30,45.999))"), // Should extract 45, not 46
// Very large normalization values
("B1", "=TIME(999,999,999)"), // Extreme normalization test
("B2", "=HOUR(999.5)"), // Multiple days, extract hour from fractional part
("B3", "=MINUTE(999.75)"), // Multiple days, extract minute
// Boundary conditions at rollover points
("C1", "=TIME(24,60,60)"), // Should normalize to next day (00:01:00)
("C2", "=HOUR(0.999999999)"), // Almost 24 hours should be 23
("C3", "=MINUTE(0.999999999)"), // Almost 24 hours, extract minutes
("C4", "=SECOND(0.999999999)"), // Almost 24 hours, extract seconds
// Precision at boundaries
("D1", "=TIME(23,59,59.999)"), // Very close to midnight
("D2", "=TIME(0,0,0.001)"), // Just after midnight
]);
// Fractional seconds are floored
assert_eq!(model._get_text("A2"), *"45"); // 45.999 floored to 45
// Multiple days should work with rem_euclid
assert_eq!(model._get_text("B2"), *"12"); // 999.5 days, hour = 12 (noon)
// Boundary normalization
assert_eq!(model._get_text("C1"), *"0.042361111"); // 24:60:60 = 01:01:00 (normalized)
assert_eq!(model._get_text("C2"), *"23"); // Almost 24 hours = 23:xx:xx
// High precision should be handled correctly
let result_d1 = model._get_text("D1").parse::<f64>().unwrap();
assert!(result_d1 < 1.0 && result_d1 > 0.999); // Very close to but less than 1.0
}
#[test]
fn test_time_function_errors() {
let model = test_time_expressions(&[
("A1", "=TIME()"), // Wrong arg count
("A2", "=TIME(12)"), // Wrong arg count
("A3", "=TIME(12,30,0,0)"), // Wrong arg count
("B1", "=TIME(-1,0,0)"), // Negative hour
("B2", "=TIME(0,-1,0)"), // Negative minute
("B3", "=TIME(0,0,-1)"), // Negative second
]);
// Wrong argument count
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
// Negative values should return #NUM! error
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn test_timevalue_function_formats() {
let model = test_time_expressions(&[
// Basic formats
("A1", "=TIMEVALUE(\"14:30\")"),
("A2", "=TIMEVALUE(\"14:30:45\")"),
("A3", "=TIMEVALUE(\"00:00:00\")"),
// AM/PM formats
("B1", "=TIMEVALUE(\"2:30 PM\")"),
("B2", "=TIMEVALUE(\"2:30 AM\")"),
("B3", "=TIMEVALUE(\"12:00 PM\")"), // Noon
("B4", "=TIMEVALUE(\"12:00 AM\")"), // Midnight
// Single hour with AM/PM (now supported!)
("B5", "=TIMEVALUE(\"2 PM\")"),
("B6", "=TIMEVALUE(\"2 AM\")"),
// Date-time formats (extract time only)
("C1", "=TIMEVALUE(\"2023-01-01 14:30:00\")"),
("C2", "=TIMEVALUE(\"2023-01-01T14:30:00\")"),
// Whitespace handling
("D1", "=TIMEVALUE(\" 14:30 \")"),
]);
// Basic formats
assert_eq!(model._get_text("A1"), *TIME_14_30);
assert_eq!(model._get_text("A2"), *TIME_14_30_45);
assert_eq!(model._get_text("A3"), *MIDNIGHT);
// AM/PM formats
assert_eq!(model._get_text("B1"), *TIME_14_30); // 2:30 PM = 14:30
assert_eq!(model._get_text("B2"), *TIME_2_30_AM); // 2:30 AM
assert_eq!(model._get_text("B3"), *NOON); // 12:00 PM = noon
assert_eq!(model._get_text("B4"), *MIDNIGHT); // 12:00 AM = midnight
// Single hour AM/PM formats (now supported!)
assert_eq!(model._get_text("B5"), *TIME_2_PM); // 2 PM = 14:00
assert_eq!(model._get_text("B6"), *TIME_2_AM); // 2 AM = 02:00
// Date-time formats
assert_eq!(model._get_text("C1"), *TIME_14_30);
assert_eq!(model._get_text("C2"), *TIME_14_30);
// Whitespace
assert_eq!(model._get_text("D1"), *TIME_14_30);
}
#[test]
fn test_timevalue_function_errors() {
let model = test_time_expressions(&[
("A1", "=TIMEVALUE()"), // Wrong arg count
("A2", "=TIMEVALUE(\"14:30\", \"x\")"), // Wrong arg count
("B1", "=TIMEVALUE(\"invalid\")"), // Invalid format
("B2", "=TIMEVALUE(\"25:00\")"), // Invalid hour
("B3", "=TIMEVALUE(\"14:70\")"), // Invalid minute
("B4", "=TIMEVALUE(\"\")"), // Empty string
("B5", "=TIMEVALUE(\"2PM\")"), // Missing space (still unsupported)
]);
// Wrong argument count should return #ERROR!
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
// Invalid formats should return #VALUE!
assert_eq!(model._get_text("B1"), *"#VALUE!");
assert_eq!(model._get_text("B2"), *"#VALUE!");
assert_eq!(model._get_text("B3"), *"#VALUE!");
assert_eq!(model._get_text("B4"), *"#VALUE!");
assert_eq!(model._get_text("B5"), *"#VALUE!"); // "2PM" no space - not supported
}
#[test]
fn test_time_component_extraction_comprehensive() {
// Test component extraction using helper function for consistency
// Test basic time values
let test_cases = [
(MIDNIGHT, ("0", "0", "0")), // 00:00:00
(NOON, ("12", "0", "0")), // 12:00:00
(TIME_14_30, ("14", "30", "0")), // 14:30:00
(TIME_23_59_59, ("23", "59", "59")), // 23:59:59
];
for (time_value, expected) in test_cases {
let (hour, minute, second) = test_component_extraction(time_value);
assert_eq!(hour, expected.0, "Hour mismatch for {time_value}");
assert_eq!(minute, expected.1, "Minute mismatch for {time_value}");
assert_eq!(second, expected.2, "Second mismatch for {time_value}");
}
// Test multiple days (extract from fractional part)
let (hour, minute, second) = test_component_extraction("1.5"); // Day 2, 12:00
assert_eq!(
(hour, minute, second),
("12".to_string(), "0".to_string(), "0".to_string())
);
let (hour, minute, second) = test_component_extraction("100.604166667"); // Day 101, 14:30
assert_eq!(
(hour, minute, second),
("14".to_string(), "30".to_string(), "0".to_string())
);
// Test precision at boundaries
let (hour, _, _) = test_component_extraction("0.041666666"); // Just under 1:00 AM
assert_eq!(hour, "0");
let (hour, _, _) = test_component_extraction("0.041666667"); // Exactly 1:00 AM
assert_eq!(hour, "1");
let (hour, _, _) = test_component_extraction("0.041666668"); // Just over 1:00 AM
assert_eq!(hour, "1");
// Test very large day values
let (hour, minute, second) = test_component_extraction("1000000.25"); // Million days + 6 hours
assert_eq!(
(hour, minute, second),
("6".to_string(), "0".to_string(), "0".to_string())
);
}
#[test]
fn test_time_component_function_errors() {
let model = test_time_expressions(&[
// Wrong argument counts
("A1", "=HOUR()"), // No arguments
("A2", "=MINUTE()"), // No arguments
("A3", "=SECOND()"), // No arguments
("A4", "=HOUR(1, 2)"), // Too many arguments
("A5", "=MINUTE(1, 2)"), // Too many arguments
("A6", "=SECOND(1, 2)"), // Too many arguments
// Negative values should return #NUM!
("B1", "=HOUR(-0.5)"), // Negative value
("B2", "=MINUTE(-1)"), // Negative value
("B3", "=SECOND(-1)"), // Negative value
("B4", "=HOUR(-0.000001)"), // Slightly negative
("B5", "=MINUTE(-0.000001)"), // Slightly negative
("B6", "=SECOND(-0.000001)"), // Slightly negative
]);
// Wrong argument count should return #ERROR!
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
// Negative values should return #NUM!
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
assert_eq!(model._get_text("B4"), *"#NUM!");
assert_eq!(model._get_text("B5"), *"#NUM!");
assert_eq!(model._get_text("B6"), *"#NUM!");
}
#[test]
fn test_time_functions_integration() {
// Test how TIME, TIMEVALUE and component extraction functions work together
let model = test_time_expressions(&[
// Create times with both functions
("A1", "=TIME(14,30,45)"),
("A2", "=TIMEVALUE(\"14:30:45\")"),
// Extract components from TIME function results
("B1", "=HOUR(A1)"),
("B2", "=MINUTE(A1)"),
("B3", "=SECOND(A1)"),
// Extract components from TIMEVALUE function results
("C1", "=HOUR(A2)"),
("C2", "=MINUTE(A2)"),
("C3", "=SECOND(A2)"),
// Test additional TIME variations
("D1", "=TIME(14,0,0)"), // 14:00:00
("E1", "=HOUR(D1)"), // Extract hour from 14:00:00
("E2", "=MINUTE(D1)"), // Extract minute from 14:00:00
("E3", "=SECOND(D1)"), // Extract second from 14:00:00
]);
// TIME and TIMEVALUE should produce equivalent results
assert_eq!(model._get_text("A1"), model._get_text("A2"));
// Extracting components should work consistently
assert_eq!(model._get_text("B1"), *"14");
assert_eq!(model._get_text("B2"), *"30");
assert_eq!(model._get_text("B3"), *"45");
assert_eq!(model._get_text("C1"), *"14");
assert_eq!(model._get_text("C2"), *"30");
assert_eq!(model._get_text("C3"), *"45");
// Components from TIME(14,0,0)
assert_eq!(model._get_text("E1"), *"14");
assert_eq!(model._get_text("E2"), *"0");
assert_eq!(model._get_text("E3"), *"0");
}
#[test]
fn test_time_function_extreme_values() {
// Test missing edge cases: very large fractional inputs
let model = test_time_expressions(&[
// Extremely large fractional values to TIME function
("A1", "=TIME(999999.9, 999999.9, 999999.9)"), // Very large fractional inputs
("A2", "=TIME(1e6, 1e6, 1e6)"), // Scientific notation inputs
("A3", "=TIME(0.000001, 0.000001, 0.000001)"), // Very small fractional inputs
// Large day values for component extraction (stress test)
("B1", "=HOUR(999999.999)"), // Almost a million days
("B2", "=MINUTE(999999.999)"),
("B3", "=SECOND(999999.999)"),
// Edge case: exactly 1.0 (should be midnight of next day)
("C1", "=HOUR(1.0)"),
("C2", "=MINUTE(1.0)"),
("C3", "=SECOND(1.0)"),
// Very high precision values
("D1", "=HOUR(0.999999999999)"), // Almost exactly 24:00:00
("D2", "=MINUTE(0.999999999999)"),
("D3", "=SECOND(0.999999999999)"),
]);
// Large fractional inputs should be floored and normalized
let result_a1 = model._get_text("A1").parse::<f64>().unwrap();
assert!(
(0.0..1.0).contains(&result_a1),
"Result should be valid time fraction"
);
// Component extraction should work with very large values
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_b1), "Hour should be 0-23");
// Exactly 1.0 should be midnight (start of next day)
assert_eq!(model._get_text("C1"), *"0");
assert_eq!(model._get_text("C2"), *"0");
assert_eq!(model._get_text("C3"), *"0");
// Very high precision should still extract valid components
let hour_d1 = model._get_text("D1").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_d1), "Hour should be 0-23");
}
#[test]
fn test_timevalue_malformed_but_parseable() {
// Test missing edge case: malformed but potentially parseable strings
let model = test_time_expressions(&[
// Test various malformed but potentially parseable time strings
("A1", "=TIMEVALUE(\"14:30:00.123\")"), // Milliseconds (might be truncated)
("A2", "=TIMEVALUE(\"14:30:00.999\")"), // High precision milliseconds
("A3", "=TIMEVALUE(\"02:30:00\")"), // Leading zero hours
("A4", "=TIMEVALUE(\"2:05:00\")"), // Single digit hour, zero-padded minute
// Boundary cases for AM/PM parsing
("B1", "=TIMEVALUE(\"11:59:59 PM\")"), // Just before midnight
("B2", "=TIMEVALUE(\"12:00:01 AM\")"), // Just after midnight
("B3", "=TIMEVALUE(\"12:00:01 PM\")"), // Just after noon
("B4", "=TIMEVALUE(\"11:59:59 AM\")"), // Just before noon
// Test various date-time combinations
("C1", "=TIMEVALUE(\"2023-12-31T23:59:59\")"), // ISO format at year end
("C2", "=TIMEVALUE(\"2023-01-01 00:00:01\")"), // New year, just after midnight
// Test potential edge cases that might still be parseable
("D1", "=TIMEVALUE(\"24:00:00\")"), // Should error (invalid hour)
("D2", "=TIMEVALUE(\"23:60:00\")"), // Should error (invalid minute)
("D3", "=TIMEVALUE(\"23:59:60\")"), // Should error (invalid second)
]);
// Milliseconds are not supported, should return a #VALUE! error like Excel
assert_eq!(model._get_text("A1"), *"#VALUE!");
assert_eq!(model._get_text("A2"), *"#VALUE!");
// Leading zeros should work fine
assert_eq!(model._get_text("A3"), *TIME_2_30_AM); // 02:30:00 should parse as 2:30:00
// AM/PM boundary cases should work
let result_b1 = model._get_text("B1").parse::<f64>().unwrap();
assert!(
result_b1 > 0.99 && result_b1 < 1.0,
"11:59:59 PM should be very close to 1.0"
);
let result_b2 = model._get_text("B2").parse::<f64>().unwrap();
assert!(
result_b2 > 0.0 && result_b2 < 0.01,
"12:00:01 AM should be very close to 0.0"
);
// ISO 8601 format with "T" separator should be parsed correctly
assert_eq!(model._get_text("C1"), *TIME_23_59_59); // 23:59:59 → almost midnight
assert_eq!(model._get_text("C2"), *TIME_00_00_01); // 00:00:01 → one second past midnight
// Time parser normalizes edge cases to midnight (Excel compatibility)
assert_eq!(model._get_text("D1"), *"0"); // 24:00:00 = midnight of next day
assert_eq!(model._get_text("D2"), *"0"); // 23:60:00 normalizes to 24:00:00 = midnight
assert_eq!(model._get_text("D3"), *"0"); // 23:59:60 normalizes to 24:00:00 = midnight
}
#[test]
fn test_performance_stress_with_extreme_values() {
// Test performance/stress cases with extreme values
let model = test_time_expressions(&[
// Very large numbers that should still work
("A1", "=TIME(2147483647, 0, 0)"), // Max i32 hours
("A2", "=TIME(0, 2147483647, 0)"), // Max i32 minutes
("A3", "=TIME(0, 0, 2147483647)"), // Max i32 seconds
// Component extraction with extreme day values
("B1", "=HOUR(1e15)"), // Very large day number
("B2", "=MINUTE(1e15)"),
("B3", "=SECOND(1e15)"),
// Edge of floating point precision
("C1", "=HOUR(1.7976931348623157e+308)"), // Near max f64
("C2", "=HOUR(2.2250738585072014e-308)"), // Near min positive f64
// Multiple TIME function calls with large values
("D1", "=TIME(1000000, 1000000, 1000000)"), // Large normalized values
("D2", "=HOUR(D1)"), // Extract from large TIME result
("D3", "=MINUTE(D1)"),
("D4", "=SECOND(D1)"),
]);
// All results should be valid (not errors) even with extreme inputs
for cell in ["A1", "A2", "A3", "B1", "B2", "B3", "D1", "D2", "D3", "D4"] {
let result = model._get_text(cell);
assert!(
result != *"#ERROR!" && result != *"#NUM!" && result != *"#VALUE!",
"Cell {cell} should not error with extreme values: {result}",
);
}
// Results should be mathematically valid
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
let minute_b2 = model._get_text("B2").parse::<i32>().unwrap();
let second_b3 = model._get_text("B3").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_b1));
assert!((0..=59).contains(&minute_b2));
assert!((0..=59).contains(&second_b3));
// TIME function results should be valid time fractions
let time_d1 = model._get_text("D1").parse::<f64>().unwrap();
assert!(
(0.0..1.0).contains(&time_d1),
"TIME result should be valid fraction"
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,347 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
// Test data: Jan 1-10, 2023 week
const JAN_1_2023: i32 = 44927; // Sunday
const JAN_2_2023: i32 = 44928; // Monday
const JAN_6_2023: i32 = 44932; // Friday
const JAN_9_2023: i32 = 44935; // Monday
const JAN_10_2023: i32 = 44936; // Tuesday
#[test]
fn networkdays_calculates_weekdays_excluding_weekends() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Should count 7 weekdays in 10-day span"
);
}
#[test]
fn networkdays_handles_reverse_date_order() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_10_2023},{JAN_1_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"-7",
"Reversed dates should return negative count"
);
}
#[test]
fn networkdays_excludes_holidays_from_weekdays() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{JAN_9_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"6",
"Should exclude Monday holiday from 7 weekdays"
);
}
#[test]
fn networkdays_handles_same_start_end_date() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_9_2023},{JAN_9_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"1",
"Same weekday date should count as 1 workday"
);
}
#[test]
fn networkdays_accepts_holiday_ranges() {
let mut model = new_empty_model();
model._set("B1", &JAN_2_2023.to_string());
model._set("B2", &JAN_6_2023.to_string());
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B2)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"5",
"Should exclude 2 holidays from 7 weekdays"
);
}
#[test]
fn networkdays_intl_uses_standard_weekend_by_default() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Default should be Saturday-Sunday weekend"
);
}
#[test]
fn networkdays_intl_supports_numeric_weekend_patterns() {
let mut model = new_empty_model();
// Pattern 2 = Sunday-Monday weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},2)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"6",
"Sunday-Monday weekend should give 6 workdays"
);
}
#[test]
fn networkdays_intl_supports_single_day_weekends() {
let mut model = new_empty_model();
// Pattern 11 = Sunday only weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},11)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"8",
"Sunday-only weekend should give 8 workdays"
);
}
#[test]
fn networkdays_intl_supports_string_weekend_patterns() {
let mut model = new_empty_model();
// "0000110" = Friday-Saturday weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\")"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"8",
"Friday-Saturday weekend should give 8 workdays"
);
}
#[test]
fn networkdays_intl_no_weekends_counts_all_days() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000000\")"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"10",
"No weekends should count all 10 days"
);
}
#[test]
fn networkdays_intl_combines_custom_weekends_with_holidays() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\",{JAN_9_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Should exclude both weekend and holiday"
);
}
#[test]
fn networkdays_validates_argument_count() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS()");
model._set("A2", "=NETWORKDAYS(1,2,3,4)");
model._set("A3", "=NETWORKDAYS.INTL()");
model._set("A4", "=NETWORKDAYS.INTL(1,2,3,4,5)");
model.evaluate();
assert_eq!(model._get_text("A1"), "#ERROR!");
assert_eq!(model._get_text("A2"), "#ERROR!");
assert_eq!(model._get_text("A3"), "#ERROR!");
assert_eq!(model._get_text("A4"), "#ERROR!");
}
#[test]
fn networkdays_rejects_invalid_dates() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(-1,100)");
model._set("A2", "=NETWORKDAYS(1,3000000)");
model._set("A3", "=NETWORKDAYS(\"text\",100)");
model.evaluate();
assert_eq!(model._get_text("A1"), "#NUM!");
assert_eq!(model._get_text("A2"), "#NUM!");
assert_eq!(model._get_text("A3"), "#VALUE!");
}
#[test]
fn networkdays_intl_rejects_invalid_weekend_patterns() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS.INTL(1,10,99)");
model._set("A2", "=NETWORKDAYS.INTL(1,10,\"111110\")");
model._set("A3", "=NETWORKDAYS.INTL(1,10,\"11111000\")");
model._set("A4", "=NETWORKDAYS.INTL(1,10,\"1111102\")");
model.evaluate();
assert_eq!(model._get_text("A1"), "#NUM!");
assert_eq!(model._get_text("A2"), "#VALUE!");
assert_eq!(model._get_text("A3"), "#VALUE!");
assert_eq!(model._get_text("A4"), "#VALUE!");
}
#[test]
fn networkdays_rejects_invalid_holidays() {
let mut model = new_empty_model();
model._set("B1", "invalid");
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1)"),
);
model._set(
"A2",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},-1)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"#VALUE!",
"Should reject non-numeric holidays"
);
assert_eq!(
model._get_text("A2"),
"#NUM!",
"Should reject out-of-range holidays"
);
}
#[test]
fn networkdays_handles_weekend_only_periods() {
let mut model = new_empty_model();
let saturday = JAN_1_2023 - 1;
model._set("A1", &format!("=NETWORKDAYS({saturday},{JAN_1_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"0",
"Weekend-only period should count 0 workdays"
);
}
#[test]
fn networkdays_ignores_holidays_outside_date_range() {
let mut model = new_empty_model();
let future_holiday = JAN_10_2023 + 100;
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{future_holiday})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Out-of-range holidays should be ignored"
);
}
#[test]
fn networkdays_handles_empty_holiday_ranges() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B3)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Empty holiday range should be treated as no holidays"
);
}
#[test]
fn networkdays_handles_minimum_valid_dates() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(1,7)");
model.evaluate();
assert_eq!(
model._get_text("A1"),
"5",
"Should handle earliest Excel dates correctly"
);
}
#[test]
fn networkdays_handles_large_date_ranges_efficiently() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(1,365)");
model.evaluate();
assert!(
!model._get_text("A1").starts_with('#'),
"Large ranges should not error"
);
}

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

View File

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

@@ -1,26 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_weekday_return_types_11_to_17() {
let mut model = new_empty_model();
// Test date: 44561 corresponds to a Friday (2021-12-31). We verify the
// numeric result for each custom week start defined by return_type 11-17.
model._set("A1", "=WEEKDAY(44561,11)"); // Monday start
model._set("A2", "=WEEKDAY(44561,12)"); // Tuesday start
model._set("A3", "=WEEKDAY(44561,13)"); // Wednesday start
model._set("A4", "=WEEKDAY(44561,14)"); // Thursday start
model._set("A5", "=WEEKDAY(44561,15)"); // Friday start
model._set("A6", "=WEEKDAY(44561,16)"); // Saturday start
model._set("A7", "=WEEKDAY(44561,17)"); // Sunday start
model.evaluate();
assert_eq!(model._get_text("A1"), *"5"); // Mon=1 .. Sun=7 ⇒ Fri=5
assert_eq!(model._get_text("A2"), *"4"); // Tue start ⇒ Fri=4
assert_eq!(model._get_text("A3"), *"3"); // Wed start ⇒ Fri=3
assert_eq!(model._get_text("A4"), *"2"); // Thu start ⇒ Fri=2
assert_eq!(model._get_text("A5"), *"1"); // Fri start ⇒ Fri=1
assert_eq!(model._get_text("A6"), *"7"); // Sat start ⇒ Fri=7
assert_eq!(model._get_text("A7"), *"6"); // Sun start ⇒ Fri=6
}

View File

@@ -1,31 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_weeknum_return_types_11_to_17_and_21() {
let mut model = new_empty_model();
// Date 44561 -> 2021-12-31 (Friday). Previously verified as week 53 (Sunday/Monday start).
// We verify that custom week-start codes 11-17 all map to week 53 and ISO variant (21) maps to 52.
let formulas = [
("A1", "=WEEKNUM(44561,11)"),
("A2", "=WEEKNUM(44561,12)"),
("A3", "=WEEKNUM(44561,13)"),
("A4", "=WEEKNUM(44561,14)"),
("A5", "=WEEKNUM(44561,15)"),
("A6", "=WEEKNUM(44561,16)"),
("A7", "=WEEKNUM(44561,17)"),
("A8", "=WEEKNUM(44561,21)"), // ISO week numbering
];
for (cell, formula) in formulas {
model._set(cell, formula);
}
model.evaluate();
// All 11-17 variations should yield 53
for cell in ["A1", "A2", "A3", "A4", "A5", "A6", "A7"] {
assert_eq!(model._get_text(cell), *"53", "{cell} should be 53");
}
// ISO week (return_type 21)
assert_eq!(model._get_text("A8"), *"52");
}

View File

@@ -1,60 +0,0 @@
#![allow(clippy::panic)]
use crate::{cell::CellValue, test::util::new_empty_model};
#[test]
fn test_yearfrac_basis_2_actual_360() {
let mut model = new_empty_model();
// Non-leap span of exactly 360 days should result in 1.0
model._set("A1", "=YEARFRAC(44561,44921,2)");
// Leap-year span of 366 days: Jan 1 2020 → Jan 1 2021
model._set("A2", "=YEARFRAC(43831,44197,2)");
// Reverse order should yield negative value
model._set("A3", "=YEARFRAC(44921,44561,2)");
model.evaluate();
// 360/360
assert_eq!(model._get_text("A1"), *"1");
// 366/360 ≈ 1.0166666667 (tolerance 1e-10)
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!A2") {
assert!((v - 1.016_666_666_7).abs() < 1e-10);
} else {
panic!("Expected numeric value in A2");
}
// Negative symmetric of A1
assert_eq!(model._get_text("A3"), *"-1");
}
#[test]
fn test_yearfrac_basis_3_actual_365() {
let mut model = new_empty_model();
// Non-leap span of exactly 365 days should result in 1.0
model._set("B1", "=YEARFRAC(44561,44926,3)");
// Leap-year span of 366 days
model._set("B2", "=YEARFRAC(43831,44197,3)");
// Same date should be 0
model._set("B3", "=YEARFRAC(44561,44561,3)");
model.evaluate();
// 365/365
assert_eq!(model._get_text("B1"), *"1");
// 366/365 ≈ 1.002739726 (tolerance 1e-10)
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B2") {
assert!((v - 1.002_739_726).abs() < 1e-10);
} else {
panic!("Expected numeric value in B2");
}
// Same date
assert_eq!(model._get_text("B3"), *"0");
}

View File

@@ -1,28 +1,21 @@
mod test_add_delete_sheets; mod test_add_delete_sheets;
mod test_autofill_columns; mod test_autofill_columns;
mod test_autofill_rows; mod test_autofill_rows;
mod test_batch_row_column_diff;
mod test_border; mod test_border;
mod test_clear_cells; mod test_clear_cells;
mod test_column_style;
mod test_defined_names; 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;
mod test_grid_lines; mod test_grid_lines;
mod test_keyboard_navigation; mod test_keyboard_navigation;
mod test_last_empty_cell;
mod test_multi_row_column;
mod test_on_area_selection; mod test_on_area_selection;
mod test_on_expand_selected_range; mod test_on_expand_selected_range;
mod test_on_paste_styles; mod test_on_paste_styles;
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_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

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

View File

@@ -50,7 +50,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(top_border), Some(top_border),
top_cell_style.border.bottom, top_cell_style.border.bottom,
"(Top). Sheet: {sheet}, row: {row}, column: {column}" "(Top). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -62,7 +65,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(right_border), Some(right_border),
right_cell_style.border.left, right_cell_style.border.left,
"(Right). Sheet: {sheet}, row: {row}, column: {column}" "(Right). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -74,7 +80,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(bottom_border), Some(bottom_border),
bottom_cell_style.border.top, bottom_cell_style.border.top,
"(Bottom). Sheet: {sheet}, row: {row}, column: {column}" "(Bottom). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -85,7 +94,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(left_border), Some(left_border),
left_cell_style.border.right, left_cell_style.border.right,
"(Left). Sheet: {sheet}, row: {row}, column: {column}" "(Left). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -508,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())
@@ -530,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,
}; };
@@ -640,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())
@@ -698,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())
@@ -708,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);
@@ -744,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())
@@ -759,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);
} }
} }
@@ -1022,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

@@ -89,67 +89,3 @@ fn clear_all_empty_cell() {
model.undo().unwrap(); model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
} }
#[test]
fn issue_454() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_user_input(
0,
1,
1,
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
)
.unwrap();
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("TRUE".to_string())
);
model
.range_clear_contents(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("FALSE".to_string())
);
model.undo().unwrap();
}
#[test]
fn issue_454b() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_user_input(
0,
1,
1,
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
)
.unwrap();
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("TRUE".to_string())
);
model
.range_clear_all(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("FALSE".to_string())
);
model.undo().unwrap();
}

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ use crate::{
fn send_queue() { 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,16 +57,16 @@ 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 {
model1.set_user_input(0, row, 17, "=ROW()").unwrap(); model1.set_user_input(0, row, 17, "=ROW()").unwrap();
} }
model1.insert_rows(0, 3, 1).unwrap(); model1.insert_row(0, 3).unwrap();
model1.insert_rows(0, 3, 1).unwrap(); model1.insert_row(0, 3).unwrap();
// undo al of them // undo al of them
while model1.can_undo() { while model1.can_undo() {

View File

@@ -26,7 +26,7 @@ fn set_user_input_errors() {
#[test] #[test]
fn user_model_debug_message() { fn user_model_debug_message() {
let model = UserModel::new_empty("model", "en", "UTC").unwrap(); let model = UserModel::new_empty("model", "en", "UTC").unwrap();
let s = &format!("{model:?}"); let s = &format!("{:?}", model);
assert_eq!(s, "UserModel"); assert_eq!(s, "UserModel");
} }
@@ -59,10 +59,10 @@ 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_rows(0, 5, 1).is_ok()); assert!(model.delete_row(0, 5).is_ok());
// Row 5 has now the normal height // Row 5 has now the normal height
assert_eq!(model.get_row_height(0, 5), Ok(height)); assert_eq!(model.get_row_height(0, 5), Ok(height));
// There is no value in A5 // There is no value in A5
@@ -95,11 +95,11 @@ fn insert_remove_columns() {
// 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
assert!(model.delete_columns(0, 5, 1).is_ok()); assert!(model.delete_column(0, 5).is_ok());
// Column 5 has now the normal width // Column 5 has now the normal width
assert_eq!(model.get_column_width(0, 5), Ok(column_width)); assert_eq!(model.get_column_width(0, 5), Ok(column_width));
// There is no value in E5 // There is no value in E5

View File

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

View File

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

View File

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

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