Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel
3edb068a01 update: add leftbar to app 2025-06-29 13:30:05 +02:00
197 changed files with 9444 additions and 12617 deletions

View File

@@ -31,82 +31,38 @@ jobs:
- host: ubuntu-latest - host: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: | build: yarn build --target x86_64-unknown-linux-gnu
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target x86_64-unknown-linux-gnu
- host: ubuntu-latest - host: ubuntu-latest
target: x86_64-unknown-linux-musl target: x86_64-unknown-linux-musl
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
build: | build: yarn build --target x86_64-unknown-linux-musl
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target x86_64-unknown-linux-musl
- host: macos-latest - host: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
build: | build: yarn build --target aarch64-apple-darwin
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target aarch64-apple-darwin
- host: ubuntu-latest - host: ubuntu-latest
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
build: | build: yarn build --target aarch64-unknown-linux-gnu
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 - host: ubuntu-latest
target: armv7-unknown-linux-gnueabihf target: armv7-unknown-linux-gnueabihf
setup: | setup: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y gcc-arm-linux-gnueabihf libc6-dev-armhf-cross sudo apt-get install gcc-arm-linux-gnueabihf -y
build: | build: yarn build --target armv7-unknown-linux-gnueabihf
set -e - host: ubuntu-latest
rustup toolchain install 1.90.0 target: armv7-unknown-linux-musleabihf
rustup default 1.90.0 build: yarn build --target armv7-unknown-linux-musleabihf
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 - host: ubuntu-latest
target: aarch64-linux-android target: aarch64-linux-android
build: | build: yarn build --target aarch64-linux-android
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target aarch64-linux-android
- host: ubuntu-latest - host: ubuntu-latest
target: armv7-linux-androideabi target: armv7-linux-androideabi
build: | build: yarn build --target armv7-linux-androideabi
set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
yarn build --target armv7-linux-androideabi
- host: ubuntu-latest - host: ubuntu-latest
target: aarch64-unknown-linux-musl target: aarch64-unknown-linux-musl
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
build: |- build: |-
set -e && set -e &&
rustup toolchain install 1.90.0 &&
rustup default 1.90.0 &&
rustup target add aarch64-unknown-linux-musl && rustup target add aarch64-unknown-linux-musl &&
yarn build --target aarch64-unknown-linux-musl yarn build --target aarch64-unknown-linux-musl
- host: windows-latest - host: windows-latest
@@ -136,7 +92,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
if: ${{ !matrix.settings.docker }} if: ${{ !matrix.settings.docker }}
with: with:
toolchain: 1.90.0 toolchain: stable
targets: ${{ matrix.settings.target }} targets: ${{ matrix.settings.target }}
- name: Cache cargo - name: Cache cargo
uses: actions/cache@v4 uses: actions/cache@v4
@@ -188,6 +144,7 @@ jobs:
- host: windows-latest - host: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
node: node:
- '18'
- '20' - '20'
runs-on: ${{ matrix.settings.host }} runs-on: ${{ matrix.settings.host }}
defaults: defaults:
@@ -222,6 +179,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node: node:
- '18'
- '20' - '20'
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
@@ -255,6 +213,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node: node:
- '18'
- '20' - '20'
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
@@ -290,6 +249,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node: node:
- '18'
- '20' - '20'
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
@@ -360,6 +320,48 @@ jobs:
run: | run: |
set -e set -e
yarn test yarn test
test-linux-arm-gnueabihf-binding:
name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }}
needs:
- build
strategy:
fail-fast: false
matrix:
node:
- '18'
- '20'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./bindings/nodejs
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-armv7-unknown-linux-gnueabihf
path: bindings/nodejs/
- name: List packages
run: ls -R .
shell: bash
- name: Install dependencies
run: |
yarn config set supportedArchitectures.cpu "arm"
yarn install
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- name: Setup and run tests
uses: addnab/docker-run-action@v3
with:
image: node:${{ matrix.node }}-bullseye-slim
options: '--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build/bindings/nodejs'
run: |
set -e
yarn test
ls -la
universal-macOS: universal-macOS:
name: Build universal macOS binary name: Build universal macOS binary
needs: needs:
@@ -408,6 +410,7 @@ jobs:
- test-linux-x64-musl-binding - test-linux-x64-musl-binding
- test-linux-aarch64-gnu-binding - test-linux-aarch64-gnu-binding
- test-linux-aarch64-musl-binding - test-linux-aarch64-musl-binding
- test-linux-arm-gnueabihf-binding
- universal-macOS - universal-macOS
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -117,7 +117,7 @@ jobs:
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/" MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
with: with:
command: upload command: upload
args: "--skip-existing **/*.whl **/*.tar.gz" args: "--skip-existing **/*.whl"
working-directory: bindings/python working-directory: bindings/python
publish-pypi: publish-pypi:
@@ -137,5 +137,5 @@ jobs:
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/" MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
with: with:
command: upload command: upload
args: "--skip-existing **/*.whl **/*.tar.gz" args: "--skip-existing **/*.whl"
working-directory: bindings/python working-directory: bindings/python

111
Cargo.lock generated
View File

@@ -218,9 +218,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.8.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
@@ -288,20 +288,14 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.5.0" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [ dependencies = [
"ctor-proc-macro", "quote",
"dtor", "syn",
] ]
[[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 +316,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 +414,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -451,13 +430,14 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.5.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",
@@ -468,7 +448,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"napi", "napi",
@@ -558,34 +538,33 @@ dependencies = [
[[package]] [[package]]
name = "napi" name = "napi"
version = "3.3.0" version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7" checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"ctor", "ctor",
"napi-build", "napi-derive",
"napi-sys", "napi-sys",
"nohash-hasher", "once_cell",
"rustc-hash",
"serde", "serde",
"serde_json", "serde_json",
] ]
[[package]] [[package]]
name = "napi-build" name = "napi-build"
version = "2.2.3" version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14" checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]] [[package]]
name = "napi-derive" name = "napi-derive"
version = "3.2.5" version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0" checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [ dependencies = [
"cfg-if",
"convert_case", "convert_case",
"ctor",
"napi-derive-backend", "napi-derive-backend",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -594,32 +573,28 @@ dependencies = [
[[package]] [[package]]
name = "napi-derive-backend" name = "napi-derive-backend"
version = "2.2.0" version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a" checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex",
"semver", "semver",
"syn", "syn",
] ]
[[package]] [[package]]
name = "napi-sys" name = "napi-sys"
version = "3.0.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
dependencies = [ dependencies = [
"libloading", "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 +721,11 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4" checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
dependencies = [ dependencies = [
"cfg-if",
"indoc", "indoc",
"libc", "libc",
"memoffset", "memoffset",
@@ -763,9 +739,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d" checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@@ -773,9 +749,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e" checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@@ -783,9 +759,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214" checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -795,9 +771,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e" checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -808,9 +784,8 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode",
"ironcalc", "ironcalc",
"pyo3", "pyo3",
"serde", "serde",
@@ -897,12 +872,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]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.21" version = "1.0.21"
@@ -1016,9 +985,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"
@@ -1107,7 +1076,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",

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

@@ -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
@@ -94,7 +84,7 @@ 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,6 +1,6 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.5.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"
@@ -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

@@ -212,12 +212,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;
@@ -390,13 +384,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();
@@ -457,7 +444,7 @@ 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,
@@ -473,70 +460,7 @@ impl Model {
return Err("Initial column out of boundaries".to_string()); return Err("Initial column out of boundaries".to_string());
} }
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(
@@ -549,88 +473,4 @@ impl Model {
Ok(()) 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(())
}
} }

View File

@@ -314,9 +314,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()

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

@@ -717,7 +717,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(),
}; };
} }
}; };
@@ -850,7 +850,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 +878,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

@@ -2,19 +2,15 @@ use crate::functions::Function;
use super::Node; use super::Node;
use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::sync::OnceLock;
static RANGE_REFERENCE_REGEX: OnceLock<Regex> = OnceLock::new();
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fn get_re() -> &'static Regex { static RE: Lazy<Regex> =
RANGE_REFERENCE_REGEX Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
.get_or_init(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"))
}
fn is_range_reference(s: &str) -> bool { fn is_range_reference(s: &str) -> bool {
get_re().is_match(s) RE.is_match(s)
} }
/* /*
@@ -341,8 +337,7 @@ fn static_analysis_offset(args: &[Node]) -> StaticResult {
} }
_ => return StaticResult::Unknown, _ => return StaticResult::Unknown,
}; };
// Both height and width are explicitly 1, so OFFSET will return a single cell StaticResult::Unknown
StaticResult::Scalar
} }
// fn static_analysis_choose(_args: &[Node]) -> StaticResult { // fn static_analysis_choose(_args: &[Node]) -> StaticResult {
@@ -576,37 +571,6 @@ fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
} }
} }
// NETWORKDAYS(start_date, end_date, [holidays])
// Parameters: start_date (scalar), end_date (scalar), holidays (optional vector)
fn args_signature_networkdays(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
// NETWORKDAYS.INTL(start_date, end_date, [weekend], [holidays])
// Parameters: start_date (scalar), end_date (scalar), weekend (optional scalar), holidays (optional vector)
fn args_signature_networkdays_intl(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
Signature::Vector,
]
} else {
vec![Signature::Error; arg_count]
}
}
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places: // FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
// 1. When computing the function // 1. When computing the function
// 2. Checking the arguments to see if we need to insert the implicit intersection operator // 2. Checking the arguments to see if we need to insert the implicit intersection operator
@@ -641,9 +605,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Choose => vec![Signature::Scalar; arg_count], Function::Choose => vec![Signature::Scalar; arg_count],
Function::Column => args_signature_row(arg_count), Function::Column => args_signature_row(arg_count),
Function::Columns => args_signature_one_vector(arg_count), Function::Columns => args_signature_one_vector(arg_count),
Function::Ln => args_signature_scalars(arg_count, 1, 0),
Function::Log => args_signature_scalars(arg_count, 1, 1),
Function::Log10 => args_signature_scalars(arg_count, 1, 0),
Function::Cos => args_signature_scalars(arg_count, 1, 0), Function::Cos => args_signature_scalars(arg_count, 1, 0),
Function::Cosh => args_signature_scalars(arg_count, 1, 0), Function::Cosh => args_signature_scalars(arg_count, 1, 0),
Function::Max => vec![Signature::Vector; arg_count], Function::Max => vec![Signature::Vector; arg_count],
@@ -722,28 +683,13 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Maxifs => vec![Signature::Vector; arg_count], Function::Maxifs => vec![Signature::Vector; arg_count],
Function::Minifs => vec![Signature::Vector; arg_count], Function::Minifs => vec![Signature::Vector; arg_count],
Function::Date => args_signature_scalars(arg_count, 3, 0), Function::Date => args_signature_scalars(arg_count, 3, 0),
Function::Datedif => args_signature_scalars(arg_count, 3, 0),
Function::Datevalue => args_signature_scalars(arg_count, 1, 0),
Function::Day => args_signature_scalars(arg_count, 1, 0), Function::Day => args_signature_scalars(arg_count, 1, 0),
Function::Edate => args_signature_scalars(arg_count, 2, 0), Function::Edate => args_signature_scalars(arg_count, 2, 0),
Function::Eomonth => args_signature_scalars(arg_count, 2, 0), Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
Function::Month => args_signature_scalars(arg_count, 1, 0), Function::Month => args_signature_scalars(arg_count, 1, 0),
Function::Time => args_signature_scalars(arg_count, 3, 0),
Function::Timevalue => args_signature_scalars(arg_count, 1, 0),
Function::Hour => args_signature_scalars(arg_count, 1, 0),
Function::Minute => args_signature_scalars(arg_count, 1, 0),
Function::Second => args_signature_scalars(arg_count, 1, 0),
Function::Now => args_signature_no_args(arg_count), Function::Now => args_signature_no_args(arg_count),
Function::Today => args_signature_no_args(arg_count), Function::Today => args_signature_no_args(arg_count),
Function::Year => args_signature_scalars(arg_count, 1, 0), Function::Year => args_signature_scalars(arg_count, 1, 0),
Function::Days => args_signature_scalars(arg_count, 2, 0),
Function::Days360 => args_signature_scalars(arg_count, 2, 1),
Function::Weekday => args_signature_scalars(arg_count, 1, 1),
Function::Weeknum => args_signature_scalars(arg_count, 1, 1),
Function::Workday => args_signature_scalars(arg_count, 2, 1),
Function::WorkdayIntl => args_signature_scalars(arg_count, 2, 2),
Function::Yearfrac => args_signature_scalars(arg_count, 2, 1),
Function::Isoweeknum => args_signature_scalars(arg_count, 1, 0),
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0), Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0), Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
Function::Db => args_signature_scalars(arg_count, 4, 1), Function::Db => args_signature_scalars(arg_count, 4, 1),
@@ -832,8 +778,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Formulatext => args_signature_scalars(arg_count, 1, 0), Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
Function::Unicode => args_signature_scalars(arg_count, 1, 0), Function::Unicode => args_signature_scalars(arg_count, 1, 0),
Function::Geomean => vec![Signature::Vector; arg_count], Function::Geomean => vec![Signature::Vector; arg_count],
Function::Networkdays => args_signature_networkdays(arg_count),
Function::NetworkdaysIntl => args_signature_networkdays_intl(arg_count),
} }
} }
@@ -872,9 +816,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Round => scalar_arguments(args), Function::Round => scalar_arguments(args),
Function::Rounddown => scalar_arguments(args), Function::Rounddown => scalar_arguments(args),
Function::Roundup => scalar_arguments(args), Function::Roundup => scalar_arguments(args),
Function::Ln => scalar_arguments(args),
Function::Log => scalar_arguments(args),
Function::Log10 => scalar_arguments(args),
Function::Sin => scalar_arguments(args), Function::Sin => scalar_arguments(args),
Function::Sinh => scalar_arguments(args), Function::Sinh => scalar_arguments(args),
Function::Sqrt => scalar_arguments(args), Function::Sqrt => scalar_arguments(args),
@@ -945,27 +886,12 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Maxifs => not_implemented(args), Function::Maxifs => not_implemented(args),
Function::Minifs => not_implemented(args), Function::Minifs => not_implemented(args),
Function::Date => not_implemented(args), Function::Date => not_implemented(args),
Function::Datedif => not_implemented(args),
Function::Datevalue => not_implemented(args),
Function::Day => not_implemented(args), Function::Day => not_implemented(args),
Function::Edate => not_implemented(args), Function::Edate => not_implemented(args),
Function::Month => not_implemented(args), Function::Month => not_implemented(args),
Function::Time => not_implemented(args),
Function::Timevalue => not_implemented(args),
Function::Hour => not_implemented(args),
Function::Minute => not_implemented(args),
Function::Second => not_implemented(args),
Function::Now => not_implemented(args), Function::Now => not_implemented(args),
Function::Today => not_implemented(args), Function::Today => not_implemented(args),
Function::Year => not_implemented(args), Function::Year => not_implemented(args),
Function::Days => not_implemented(args),
Function::Days360 => not_implemented(args),
Function::Weekday => not_implemented(args),
Function::Weeknum => not_implemented(args),
Function::Workday => not_implemented(args),
Function::WorkdayIntl => not_implemented(args),
Function::Yearfrac => not_implemented(args),
Function::Isoweeknum => not_implemented(args),
Function::Cumipmt => not_implemented(args), Function::Cumipmt => not_implemented(args),
Function::Cumprinc => not_implemented(args), Function::Cumprinc => not_implemented(args),
Function::Db => not_implemented(args), Function::Db => not_implemented(args),
@@ -1054,7 +980,5 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Eomonth => scalar_arguments(args), Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args), Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args), Function::Geomean => not_implemented(args),
Function::Networkdays => not_implemented(args),
Function::NetworkdaysIntl => not_implemented(args),
} }
} }

View File

@@ -2,7 +2,7 @@ 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::parser::move_formula::to_string_array_node;
use crate::expressions::parser::static_analysis::add_implicit_intersection; use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::{OpSum, OpUnary}; use crate::expressions::token::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 +28,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,
@@ -164,29 +159,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 +167,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;
}
} }
} }
} }
@@ -483,32 +451,34 @@ fn stringify(
kind, kind,
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, export_to_excel)
), ),
OpSumKind { kind, left, right } => { OpSumKind { kind, left, right } => format!(
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 stringify(left, context, displace_data, export_to_excel),
let right_str = if (matches!(kind, OpSum::Minus) && matches!(**right, OpSumKind { .. })) kind,
| matches!(**right, CompareKind { .. }) stringify(right, context, displace_data, export_to_excel)
{ ),
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)
),
CompareKind { .. } => format!(
"({})", "({})",
stringify(left, context, displace_data, export_to_excel) stringify(left, context, displace_data, export_to_excel)
), ),
_ => stringify(left, context, displace_data, export_to_excel), _ => stringify(left, context, displace_data, export_to_excel),
}; };
let y = match **right { let y = match **right {
OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!( OpSumKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
),
CompareKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
),
OpProductKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, export_to_excel)
), ),
@@ -619,43 +589,10 @@ fn stringify(
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, export_to_excel)
| 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!(

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

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

@@ -8,8 +8,6 @@ use crate::constants::EXCEL_DATE_BASE;
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER; use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER; use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
pub const DATE_OUT_OF_RANGE_MESSAGE: &str = "Out of range parameters for date";
#[inline] #[inline]
fn convert_to_serial_number(date: NaiveDate) -> i32 { fn convert_to_serial_number(date: NaiveDate) -> i32 {
date.num_days_from_ce() - EXCEL_DATE_BASE date.num_days_from_ce() - EXCEL_DATE_BASE
@@ -39,7 +37,7 @@ pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
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(convert_to_serial_number(native_date)),
None => Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()), None => Err("Out of range parameters for date".to_string()),
} }
} }
@@ -57,7 +55,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
return Ok(MINIMUM_DATE_SERIAL_NUMBER); return Ok(MINIMUM_DATE_SERIAL_NUMBER);
} }
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else { let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
}; };
// One thing to note for example is that even if you started with a year out of range // One thing to note for example is that even if you started with a year out of range
@@ -70,7 +68,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
// As a result, we have to run range checks as we parse the date from the biggest unit to the // As a result, we have to run range checks as we parse the date from the biggest unit to the
// smallest unit. // smallest unit.
if !is_date_within_range(date) { if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
} }
date = { date = {
@@ -82,7 +80,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
date = date + Months::new(abs_month); date = date + Months::new(abs_month);
} }
if !is_date_within_range(date) { if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
} }
date date
}; };
@@ -96,7 +94,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
date = date + Days::new(abs_day); date = date + Days::new(abs_day);
} }
if !is_date_within_range(date) { if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
} }
date date
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -246,7 +246,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 +262,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

@@ -2,7 +2,6 @@ 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::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::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,
@@ -312,7 +311,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 +327,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 +352,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,16 +378,6 @@ impl Model {
} }
} }
single_number_fn!(fn_log10, |f| if f <= 0.0 {
Err(Error::NUM)
} else {
Ok(f64::log10(f))
});
single_number_fn!(fn_ln, |f| if f <= 0.0 {
Err(Error::NUM)
} else {
Ok(f64::ln(f))
});
single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
single_number_fn!(fn_cos, |f| Ok(f64::cos(f))); single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
single_number_fn!(fn_tan, |f| Ok(f64::tan(f))); single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
@@ -444,47 +431,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

@@ -54,9 +54,6 @@ pub enum Function {
Columns, Columns,
Cos, Cos,
Cosh, Cosh,
Log,
Log10,
Ln,
Max, Max,
Min, Min,
Pi, Pi,
@@ -148,30 +145,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 +250,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 +277,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 +356,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 +488,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 +534,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 +621,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 +734,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 +828,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"),
@@ -1043,9 +961,6 @@ impl Model {
Function::True => self.fn_true(args, cell), Function::True => self.fn_true(args, cell),
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 +1066,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),

View File

@@ -350,7 +350,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 +476,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

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

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

@@ -7,8 +7,6 @@ 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;
@@ -29,7 +27,6 @@ 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_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
@@ -46,11 +43,8 @@ mod test_sheets;
mod test_styles; mod test_styles;
mod test_trigonometric; mod test_trigonometric;
mod test_true_false; 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;
@@ -61,17 +55,12 @@ 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_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

@@ -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();
@@ -221,382 +216,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

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

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

@@ -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,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,7 +1,6 @@
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_column_style;
@@ -12,8 +11,6 @@ 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;

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

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

@@ -65,8 +65,8 @@ fn queue_undo_redo_multiple() {
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

@@ -62,7 +62,7 @@ fn insert_remove_rows() {
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok()); assert!(model.set_rows_height(0, 5, 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
@@ -99,7 +99,7 @@ fn insert_remove_columns() {
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));

View File

@@ -14,7 +14,7 @@ fn simple_insert_row() {
for row in 1..5 { for row in 1..5 {
assert!(model.set_user_input(sheet, row, column, "123").is_ok()); assert!(model.set_user_input(sheet, row, column, "123").is_ok());
} }
assert!(model.insert_rows(sheet, 3, 1).is_ok()); assert!(model.insert_row(sheet, 3).is_ok());
assert_eq!( assert_eq!(
model.get_formatted_cell_value(sheet, 3, column).unwrap(), model.get_formatted_cell_value(sheet, 3, column).unwrap(),
"" ""
@@ -40,7 +40,7 @@ fn simple_insert_column() {
for column in 1..5 { for column in 1..5 {
assert!(model.set_user_input(sheet, row, column, "123").is_ok()); assert!(model.set_user_input(sheet, row, column, "123").is_ok());
} }
assert!(model.insert_columns(sheet, 3, 1).is_ok()); assert!(model.insert_column(sheet, 3).is_ok());
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), ""); assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
assert!(model.undo().is_ok()); assert!(model.undo().is_ok());
@@ -62,7 +62,7 @@ fn simple_delete_column() {
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0) .set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
.unwrap(); .unwrap();
model.delete_columns(0, 5, 1).unwrap(); model.delete_column(0, 5).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH)); assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
@@ -92,20 +92,20 @@ fn delete_column_errors() {
let model = new_empty_model(); let model = new_empty_model();
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
assert_eq!( assert_eq!(
model.delete_columns(1, 1, 1), model.delete_column(1, 1),
Err("Invalid sheet index".to_string()) Err("Invalid sheet index".to_string())
); );
assert_eq!( assert_eq!(
model.delete_columns(0, 0, 1), model.delete_column(0, 0),
Err("Column number '0' is not valid.".to_string()) Err("Column number '0' is not valid.".to_string())
); );
assert_eq!( assert_eq!(
model.delete_columns(0, LAST_COLUMN + 1, 1), model.delete_column(0, LAST_COLUMN + 1),
Err(format!("Column number '{}' is not valid.", LAST_COLUMN + 1)) Err("Column number '16385' is not valid.".to_string())
); );
assert_eq!(model.delete_columns(0, LAST_COLUMN, 1), Ok(())); assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(()));
} }
#[test] #[test]
@@ -119,7 +119,7 @@ fn simple_delete_row() {
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0) .set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
.unwrap(); .unwrap();
model.delete_rows(0, 15, 1).unwrap(); model.delete_row(0, 15).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT)); assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
@@ -150,7 +150,7 @@ fn simple_delete_row_no_style() {
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
model.set_user_input(0, 15, 4, "3").unwrap(); model.set_user_input(0, 15, 4, "3").unwrap();
model.set_user_input(0, 15, 6, "=D15*2").unwrap(); model.set_user_input(0, 15, 6, "=D15*2").unwrap();
model.delete_rows(0, 15, 1).unwrap(); model.delete_row(0, 15).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
} }
@@ -180,14 +180,14 @@ fn insert_row_evaluates() {
model.set_user_input(0, 1, 1, "42").unwrap(); model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*2").unwrap(); model.set_user_input(0, 1, 2, "=A1*2").unwrap();
assert!(model.insert_rows(0, 1, 1).is_ok()); assert!(model.insert_row(0, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.undo().unwrap(); model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
model.redo().unwrap(); model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
model.delete_rows(0, 1, 1).unwrap(); model.delete_row(0, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2"); assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
} }
@@ -199,7 +199,7 @@ fn insert_column_evaluates() {
model.set_user_input(0, 1, 1, "42").unwrap(); model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 10, 1, "=A1*2").unwrap(); model.set_user_input(0, 10, 1, "=A1*2").unwrap();
assert!(model.insert_columns(0, 1, 1).is_ok()); assert!(model.insert_column(0, 1).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.undo().unwrap(); model.undo().unwrap();
@@ -207,7 +207,7 @@ fn insert_column_evaluates() {
model.redo().unwrap(); model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
model.delete_columns(0, 1, 1).unwrap(); model.delete_column(0, 1).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84"); assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2"); assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
} }

View File

@@ -55,7 +55,7 @@ fn set_the_range_does_not_set_the_cell() {
assert_eq!( assert_eq!(
model.set_selected_range(5, 4, 10, 6), model.set_selected_range(5, 4, 10, 6),
Err( Err(
"The selected cell is not in one of the corners. Row: '1' and row range '(5, 10)'" "The selected cells is not in one of the corners. Row: '1' and row range '(5, 10)'"
.to_string() .to_string()
) )
); );

View File

@@ -13,8 +13,8 @@ use crate::{
}, },
model::Model, model::Model,
types::{ types::{
Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties, Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
SheetState, Style, VerticalAlignment, Style, VerticalAlignment,
}, },
utils::is_valid_hex_color, utils::is_valid_hex_color,
}; };
@@ -627,7 +627,6 @@ impl UserModel {
} }
} }
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -657,16 +656,11 @@ impl UserModel {
} }
} }
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
fn clear_column_formatting( fn clear_column_formatting(&mut self, sheet: u32, column: i32) -> Result<(), String> {
&mut self, let mut diff_list = Vec::new();
sheet: u32,
column: i32,
diff_list: &mut Vec<Diff>,
) -> Result<(), String> {
let old_value = self.model.get_column_style(sheet, column)?; let old_value = self.model.get_column_style(sheet, column)?;
self.model.delete_column_style(sheet, column)?; self.model.delete_column_style(sheet, column)?;
diff_list.push(Diff::DeleteColumnStyle { diff_list.push(Diff::DeleteColumnStyle {
@@ -745,15 +739,12 @@ impl UserModel {
} }
} }
} }
self.push_diff_list(diff_list);
Ok(()) Ok(())
} }
fn clear_row_formatting( fn clear_row_formatting(&mut self, sheet: u32, row: i32) -> Result<(), String> {
&mut self, let mut diff_list = Vec::new();
sheet: u32,
row: i32,
diff_list: &mut Vec<Diff>,
) -> Result<(), String> {
let old_value = self.model.get_row_style(sheet, row)?; let old_value = self.model.get_row_style(sheet, row)?;
self.model.delete_row_style(sheet, row)?; self.model.delete_row_style(sheet, row)?;
diff_list.push(Diff::DeleteRowStyle { diff_list.push(Diff::DeleteRowStyle {
@@ -800,6 +791,8 @@ impl UserModel {
} }
} }
} }
self.push_diff_list(diff_list);
Ok(()) Ok(())
} }
@@ -810,21 +803,19 @@ impl UserModel {
/// * [UserModel::range_clear_contents] /// * [UserModel::range_clear_contents]
pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> { pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
let sheet = range.sheet; let sheet = range.sheet;
let mut diff_list = Vec::new();
if range.row == 1 && range.height == LAST_ROW { if range.row == 1 && range.height == LAST_ROW {
for column in range.column..range.column + range.width { for column in range.column..range.column + range.width {
self.clear_column_formatting(sheet, column, &mut diff_list)?; self.clear_column_formatting(sheet, column)?;
} }
self.push_diff_list(diff_list);
return Ok(()); return Ok(());
} }
if range.column == 1 && range.width == LAST_COLUMN { if range.column == 1 && range.width == LAST_COLUMN {
for row in range.row..range.row + range.height { for row in range.row..range.row + range.height {
self.clear_row_formatting(sheet, row, &mut diff_list)?; self.clear_row_formatting(sheet, row)?;
} }
self.push_diff_list(diff_list);
return Ok(()); return Ok(());
} }
let mut diff_list = Vec::new();
for row in range.row..range.row + range.height { for row in range.row..range.row + range.height {
for column in range.column..range.column + range.width { for column in range.column..range.column + range.width {
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? { if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
@@ -860,184 +851,105 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Inserts `row_count` blank rows starting at `row` (both 0-based). /// Inserts a row
/// ///
/// Parameters /// See also:
/// * `sheet` worksheet index. /// * [Model::insert_rows]
/// * `row` first row to insert. pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
/// * `row_count` number of rows (> 0). let diff_list = vec![Diff::InsertRow { sheet, row }];
///
/// History: the method pushes `row_count` [`crate::user_model::history::Diff::InsertRow`]
/// items **all using the same `row` index**. Replaying those diffs (undo / redo)
/// is therefore immune to the row-shifts that happen after each individual
/// insertion.
///
/// See also [`Model::insert_rows`].
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
self.model.insert_rows(sheet, row, row_count)?;
let diff_list = vec![Diff::InsertRows {
sheet,
row,
count: row_count,
}];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.model.insert_rows(sheet, row, 1)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
/// Inserts `column_count` blank columns starting at `column` (0-based). /// Deletes a row
/// ///
/// Parameters /// See also:
/// * `sheet` worksheet index. /// * [Model::delete_rows]
/// * `column` first column to insert. pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
/// * `column_count` number of columns (> 0). let mut row_data = None;
///
/// History: pushes one [`crate::user_model::history::Diff::InsertColumn`]
/// per inserted column, all with the same `column` value, preventing index
/// drift when the diffs are reapplied.
///
/// See also [`Model::insert_columns`].
pub fn insert_columns(
&mut self,
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), String> {
self.model.insert_columns(sheet, column, column_count)?;
let diff_list = vec![Diff::InsertColumns {
sheet,
column,
count: column_count,
}];
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Deletes `row_count` rows starting at `row`.
///
/// History: a [`crate::user_model::history::Diff::DeleteRow`] is created for
/// each row, ordered **bottom → top**. Undo therefore recreates rows from
/// top → bottom and redo removes them bottom → top, avoiding index drift.
///
/// See also [`Model::delete_rows`].
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
let worksheet = self.model.workbook.worksheet(sheet)?; let worksheet = self.model.workbook.worksheet(sheet)?;
let mut old_data = Vec::new(); for rd in &worksheet.rows {
// Collect data for all rows to be deleted if rd.r == row {
for r in row..row + row_count { row_data = Some(rd.clone());
let mut row_data = None; break;
for rd in &worksheet.rows {
if rd.r == r {
row_data = Some(rd.clone());
break;
}
} }
let data = match worksheet.sheet_data.get(&r) {
Some(s) => s.clone(),
None => HashMap::new(),
};
old_data.push(RowData {
row: row_data,
data,
});
} }
let data = match worksheet.sheet_data.get(&row) {
self.model.delete_rows(sheet, row, row_count)?; Some(s) => s.clone(),
None => return Err(format!("Row number '{row}' is not valid.")),
let diff_list = vec![Diff::DeleteRows { };
let old_data = Box::new(RowData {
row: row_data,
data,
});
let diff_list = vec![Diff::DeleteRow {
sheet, sheet,
row, row,
count: row_count,
old_data, old_data,
}]; }];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.model.delete_rows(sheet, row, 1)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
/// Deletes `column_count` columns starting at `column`. /// Inserts a column
/// ///
/// History: pushes one [`crate::user_model::history::Diff::DeleteColumn`] /// See also:
/// per column, **right → left**, so replaying the list is always safe with /// * [Model::insert_columns]
/// respect to index shifts. pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
let diff_list = vec![Diff::InsertColumn { sheet, column }];
self.push_diff_list(diff_list);
self.model.insert_columns(sheet, column, 1)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Deletes a column
/// ///
/// See also [`Model::delete_columns`]. /// See also:
pub fn delete_columns( /// * [Model::delete_columns]
&mut self, pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), String> {
let worksheet = self.model.workbook.worksheet(sheet)?; let worksheet = self.model.workbook.worksheet(sheet)?;
let mut old_data = Vec::new(); if !is_valid_column_number(column) {
// Collect data for all columns to be deleted return Err(format!("Column number '{column}' is not valid."));
for c in column..column + column_count { }
let mut column_data = None;
for col in &worksheet.cols {
if c >= col.min && c <= col.max {
column_data = Some(Col {
min: c,
max: c,
width: col.width,
custom_width: col.custom_width,
style: col.style,
});
break;
}
}
let mut data = HashMap::new(); let mut column_data = None;
for (row_idx, row_data) in &worksheet.sheet_data { for col in &worksheet.cols {
if let Some(cell) = row_data.get(&c) { let min = col.min;
data.insert(*row_idx, cell.clone()); let max = col.max;
} if column >= min && column <= max {
column_data = Some(Col {
min: column,
max: column,
width: col.width,
custom_width: col.custom_width,
style: col.style,
});
break;
} }
}
old_data.push(ColumnData { let mut data = HashMap::new();
for (row, row_data) in &worksheet.sheet_data {
if let Some(cell) = row_data.get(&column) {
data.insert(*row, cell.clone());
}
}
let diff_list = vec![Diff::DeleteColumn {
sheet,
column,
old_data: Box::new(ColumnData {
column: column_data, column: column_data,
data, data,
}); }),
}
self.model.delete_columns(sheet, column, column_count)?;
let diff_list = vec![Diff::DeleteColumns {
sheet,
column,
count: column_count,
old_data,
}]; }];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.evaluate_if_not_paused(); self.model.delete_columns(sheet, column, 1)?;
Ok(())
}
/// Moves a column horizontally and adjusts formulas
pub fn move_column_action(
&mut self,
sheet: u32,
column: i32,
delta: i32,
) -> Result<(), String> {
let diff_list = vec![Diff::MoveColumn {
sheet,
column,
delta,
}];
self.push_diff_list(diff_list);
self.model.move_column_action(sheet, column, delta)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Moves a row vertically and adjusts formulas
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
let diff_list = vec![Diff::MoveRow { sheet, row, delta }];
self.push_diff_list(diff_list);
self.model.move_row_action(sheet, row, delta)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -1683,66 +1595,6 @@ impl UserModel {
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines) Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
} }
/// Returns the largest column in the row less than a column whose cell has a non empty value.
/// If there are none it returns `None`.
/// This is useful when rendering a part of a worksheet to know which cells spill over
pub fn get_last_non_empty_in_row_before_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
let data = worksheet.sheet_data.get(&row);
if let Some(row_data) = data {
let mut last_column = None;
let mut columns: Vec<i32> = row_data.keys().copied().collect();
columns.sort_unstable();
for col in columns {
if col < column {
if let Some(cell) = worksheet.cell(row, col) {
if matches!(cell, Cell::EmptyCell { .. }) {
continue;
}
}
last_column = Some(col);
}
}
Ok(last_column)
} else {
Ok(None)
}
}
/// Returns the smallest column in the row larger than "column" whose cell has a non empty value.
/// If there are none it returns `None`.
/// This is useful when rendering a part of a worksheet to know which cells spill over
pub fn get_first_non_empty_in_row_after_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
let data = worksheet.sheet_data.get(&row);
if let Some(row_data) = data {
let mut columns: Vec<i32> = row_data.keys().copied().collect();
// We sort the keys to ensure we are going from left to right
columns.sort_unstable();
for col in columns {
if col > column {
if let Some(cell) = worksheet.cell(row, col) {
if matches!(cell, Cell::EmptyCell { .. }) {
continue;
}
}
return Ok(Some(col));
}
}
}
Ok(None)
}
/// Returns a copy of the selected area /// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> { pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view(); let selected_area = self.get_selected_view();
@@ -1782,7 +1634,7 @@ impl UserModel {
.map_err(|e| format!("Error converting from utf8: '{e}'"))?; .map_err(|e| format!("Error converting from utf8: '{e}'"))?;
Ok(Clipboard { Ok(Clipboard {
csv: csv.trim().to_string(), csv,
data, data,
sheet, sheet,
range: (row_start, column_start, row_end, column_end), range: (row_start, column_start, row_end, column_end),
@@ -1950,7 +1802,7 @@ impl UserModel {
} }
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
// select the pasted area // select the pasted area
self.set_selected_range(area.row, area.column, row - 1, column - 1)?; self.set_selected_range(area.row, area.column, row, column)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -2115,56 +1967,45 @@ impl UserModel {
self.model.cell_clear_all(*sheet, *row, *column)?; self.model.cell_clear_all(*sheet, *row, *column)?;
} }
} }
Diff::InsertRows { sheet, row, count } => { Diff::InsertRow { sheet, row } => {
self.model.delete_rows(*sheet, *row, *count)?; self.model.delete_rows(*sheet, *row, 1)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::DeleteRows { Diff::DeleteRow {
sheet, sheet,
row, row,
count: _,
old_data, old_data,
} => { } => {
needs_evaluation = true; needs_evaluation = true;
self.model self.model.insert_rows(*sheet, *row, 1)?;
.insert_rows(*sheet, *row, old_data.len() as i32)?;
let worksheet = self.model.workbook.worksheet_mut(*sheet)?; let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
for (i, row_data) in old_data.iter().enumerate() { if let Some(row_data) = old_data.row.clone() {
let r = *row + i as i32; worksheet.rows.push(row_data);
if let Some(row_style) = row_data.row.clone() {
worksheet.rows.push(row_style);
}
worksheet.sheet_data.insert(r, row_data.data.clone());
} }
worksheet.sheet_data.insert(*row, old_data.data.clone());
} }
Diff::InsertColumns { Diff::InsertColumn { sheet, column } => {
sheet, self.model.delete_columns(*sheet, *column, 1)?;
column,
count,
} => {
self.model.delete_columns(*sheet, *column, *count)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::DeleteColumns { Diff::DeleteColumn {
sheet, sheet,
column, column,
count: _,
old_data, old_data,
} => { } => {
needs_evaluation = true; needs_evaluation = true;
self.model // inserts an empty column
.insert_columns(*sheet, *column, old_data.len() as i32)?; self.model.insert_columns(*sheet, *column, 1)?;
// puts all the data back
let worksheet = self.model.workbook.worksheet_mut(*sheet)?; let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
for (i, col_data) in old_data.iter().enumerate() { for (row, cell) in &old_data.data {
let c = *column + i as i32; worksheet.update_cell(*row, *column, cell.clone())?;
for (row, cell) in &col_data.data { }
worksheet.update_cell(*row, c, cell.clone())?; // makes sure that the width and style is correct
} if let Some(col) = &old_data.column {
if let Some(col) = &col_data.column { let width = col.width * constants::COLUMN_WIDTH_FACTOR;
let width = col.width * constants::COLUMN_WIDTH_FACTOR; let style = col.style;
let style = col.style; worksheet.set_column_width_and_style(*column, width, style)?;
worksheet.set_column_width_and_style(c, width, style)?;
}
} }
} }
Diff::SetFrozenRowsCount { Diff::SetFrozenRowsCount {
@@ -2322,21 +2163,6 @@ impl UserModel {
self.model.delete_row_style(*sheet, *row)?; self.model.delete_row_style(*sheet, *row)?;
} }
} }
Diff::MoveColumn {
sheet,
column,
delta,
} => {
// For undo, we apply the opposite move
self.model
.move_column_action(*sheet, *column + *delta, -*delta)?;
needs_evaluation = true;
}
Diff::MoveRow { sheet, row, delta } => {
// For undo, we apply the opposite move
self.model.move_row_action(*sheet, *row + *delta, -*delta)?;
needs_evaluation = true;
}
} }
} }
if needs_evaluation { if needs_evaluation {
@@ -2405,34 +2231,28 @@ impl UserModel {
} => self } => self
.model .model
.set_cell_style(*sheet, *row, *column, new_value)?, .set_cell_style(*sheet, *row, *column, new_value)?,
Diff::InsertRows { sheet, row, count } => { Diff::InsertRow { sheet, row } => {
self.model.insert_rows(*sheet, *row, *count)?; self.model.insert_rows(*sheet, *row, 1)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::DeleteRows { Diff::DeleteRow {
sheet, sheet,
row, row,
count,
old_data: _, old_data: _,
} => { } => {
self.model.delete_rows(*sheet, *row, *count)?; self.model.delete_rows(*sheet, *row, 1)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::InsertColumns { Diff::InsertColumn { sheet, column } => {
sheet,
column,
count,
} => {
self.model.insert_columns(*sheet, *column, *count)?;
needs_evaluation = true; needs_evaluation = true;
self.model.insert_columns(*sheet, *column, 1)?;
} }
Diff::DeleteColumns { Diff::DeleteColumn {
sheet, sheet,
column, column,
count,
old_data: _, old_data: _,
} => { } => {
self.model.delete_columns(*sheet, *column, *count)?; self.model.delete_columns(*sheet, *column, 1)?;
needs_evaluation = true; needs_evaluation = true;
} }
Diff::SetFrozenRowsCount { Diff::SetFrozenRowsCount {
@@ -2544,18 +2364,6 @@ impl UserModel {
} => { } => {
self.model.delete_row_style(*sheet, *row)?; self.model.delete_row_style(*sheet, *row)?;
} }
Diff::MoveColumn {
sheet,
column,
delta,
} => {
self.model.move_column_action(*sheet, *column, *delta)?;
needs_evaluation = true;
}
Diff::MoveRow { sheet, row, delta } => {
self.model.move_row_action(*sheet, *row, *delta)?;
needs_evaluation = true;
}
} }
} }

View File

@@ -87,27 +87,23 @@ pub(crate) enum Diff {
row: i32, row: i32,
old_value: Box<Option<Style>>, old_value: Box<Option<Style>>,
}, },
InsertRows { InsertRow {
sheet: u32, sheet: u32,
row: i32, row: i32,
count: i32,
}, },
DeleteRows { DeleteRow {
sheet: u32, sheet: u32,
row: i32, row: i32,
count: i32, old_data: Box<RowData>,
old_data: Vec<RowData>,
}, },
InsertColumns { InsertColumn {
sheet: u32, sheet: u32,
column: i32, column: i32,
count: i32,
}, },
DeleteColumns { DeleteColumn {
sheet: u32, sheet: u32,
column: i32, column: i32,
count: i32, old_data: Box<ColumnData>,
old_data: Vec<ColumnData>,
}, },
DeleteSheet { DeleteSheet {
sheet: u32, sheet: u32,
@@ -165,16 +161,6 @@ pub(crate) enum Diff {
new_scope: Option<u32>, new_scope: Option<u32>,
new_formula: String, new_formula: String,
}, },
MoveColumn {
sheet: u32,
column: i32,
delta: i32,
},
MoveRow {
sheet: u32,
row: i32,
delta: i32,
},
// FIXME: we are missing SetViewDiffs // FIXME: we are missing SetViewDiffs
} }

View File

@@ -3,7 +3,7 @@
mod border; mod border;
mod border_utils; mod border_utils;
mod common; mod common;
pub(crate) mod history; mod history;
mod ui; mod ui;
pub use common::UserModel; pub use common::UserModel;

View File

@@ -2,11 +2,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::expressions::utils::{is_valid_column_number, is_valid_row};
constants::{LAST_COLUMN, LAST_ROW},
expressions::utils::{is_valid_column_number, is_valid_row},
worksheet::NavigationDirection,
};
use super::common::UserModel; use super::common::UserModel;
@@ -114,7 +110,7 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Sets the selected range. Note that the selected cell must be in the selected range. /// Sets the selected range. Note that the selected cell must be in one of the corners.
pub fn set_selected_range( pub fn set_selected_range(
&mut self, &mut self,
start_row: i32, start_row: i32,
@@ -148,32 +144,16 @@ impl UserModel {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {
let selected_row = view.row; let selected_row = view.row;
let selected_column = view.column; let selected_column = view.column;
if start_row == 1 && end_row == LAST_ROW { // The selected cells must be on one of the corners of the selected range:
// full row selected. The cell must be at the top or the bottom of the range if selected_row != start_row && selected_row != end_row {
if selected_column != start_column && selected_column != end_column { return Err(format!(
return Err(format!( "The selected cells is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
"The selected cell is not the column edge. Column '{selected_column}' and column range '({start_column}, {end_column})'"
));
}
} else if start_column == 1 && end_column == LAST_COLUMN {
// full column selected. The cell must be at the left or the right of the range
if selected_row != start_row && selected_row != end_row {
return Err(format!(
"The selected cell is not in the row edge. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
));
}
} else {
// The selected cell must be on one of the corners of the selected range:
if selected_row != start_row && selected_row != end_row {
return Err(format!(
"The selected cell is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
)); ));
} }
if selected_column != start_column && selected_column != end_column { if selected_column != start_column && selected_column != end_column {
return Err(format!( return Err(format!(
"The selected cell is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'" "The selected cells is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
)); ));
}
} }
view.range = [start_row, start_column, end_row, end_column]; view.range = [start_row, start_column, end_row, end_column];
} }
@@ -210,17 +190,6 @@ impl UserModel {
return Ok(()); return Ok(());
}; };
let [row_start, column_start, row_end, column_end] = range; let [row_start, column_start, row_end, column_end] = range;
if ["ArrowUp", "ArrowDown"].contains(&key) && row_start == 1 && row_end == LAST_ROW {
// full column selected, nothing to do
return Ok(());
}
if ["ArrowRight", "ArrowLeft"].contains(&key)
&& column_start == 1
&& column_end == LAST_COLUMN
{
// full row selected, nothing to do
return Ok(());
}
match key { match key {
"ArrowRight" => { "ArrowRight" => {
@@ -503,7 +472,7 @@ impl UserModel {
// if the row is not fully visible we 'scroll' down until it is // if the row is not fully visible we 'scroll' down until it is
let mut height = 0.0; let mut height = 0.0;
let mut row = view.top_row; let mut row = view.top_row;
while row <= new_row + 1 && row <= LAST_ROW { while row <= new_row + 1 {
height += self.model.get_row_height(sheet, row)?; height += self.model.get_row_height(sheet, row)?;
row += 1; row += 1;
} }
@@ -713,94 +682,4 @@ impl UserModel {
Ok(()) Ok(())
} }
/// User navigates to the edge in the given direction
pub fn on_navigate_to_edge_in_direction(
&mut self,
direction: NavigationDirection,
) -> Result<(), String> {
let (sheet, window_height, window_width) =
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
(view.sheet, view.window_height, view.window_width)
} else {
return Err("View not found".to_string());
};
let worksheet = match self.model.workbook.worksheet(sheet) {
Ok(s) => s,
Err(_) => return Err("Worksheet not found".to_string()),
};
let view = match worksheet.views.get(&self.model.view_id) {
Some(s) => s,
None => return Err("View not found".to_string()),
};
let row = view.row;
let column = view.column;
if !is_valid_row(row) || !is_valid_column_number(column) {
return Err("Invalid row or column".to_string());
}
let (new_row, new_column) =
worksheet.navigate_to_edge_in_direction(row, column, direction)?;
if !is_valid_row(new_row) || !is_valid_column_number(new_column) {
return Err("Invalid row or column after navigation".to_string());
}
if new_row == row && new_column == column {
return Ok(()); // No change in selection
}
let mut top_row = view.top_row;
let mut left_column = view.left_column;
match direction {
NavigationDirection::Left | NavigationDirection::Right => {
// If the new column is not fully visible we 'scroll' until it is
// We need to check two conditions:
// 1. new_column > view.left_column
// 2. right_column < new_column
if new_column < view.left_column {
left_column = new_column;
} else {
let mut c = new_column;
let mut width = self.model.get_column_width(sheet, c)?;
while c > 1 && width <= window_width as f64 {
c -= 1;
width += self.model.get_column_width(sheet, c)?;
}
if c > view.left_column {
left_column = c;
}
}
}
NavigationDirection::Up | NavigationDirection::Down => {
// If the new row is not fully visible we 'scroll' until it is
// We need to check two conditions:
// 1. new_row > view.top_row
// 2. bottom_row < new_row
if new_row < view.top_row {
top_row = new_row;
} else {
let mut r = new_row;
let mut height = self.model.get_row_height(sheet, r)?;
while r > 1 && height <= window_height as f64 {
r -= 1;
height += self.model.get_row_height(sheet, r)?;
}
if r > view.top_row {
top_row = r;
}
}
}
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
view.row = new_row;
view.column = new_column;
view.range = [new_row, new_column, new_row, new_column];
view.top_row = top_row;
view.left_column = left_column;
}
}
Ok(())
}
} }

View File

@@ -1,16 +1,16 @@
[package] [package]
edition = "2021" edition = "2021"
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.6.0" version = "0.5.0"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "3.3", default-features = false, features = ["napi4", "serde-json"] } napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] }
napi-derive = "3.2" napi-derive = "2.12.2"
ironcalc = { path = "../../xlsx", version = "0.6.0" } ironcalc = { path = "../../xlsx", version = "0.5.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[build-dependencies] [build-dependencies]

View File

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

View File

@@ -1,5 +1,8 @@
/* auto-generated by NAPI-RS */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
/* auto-generated by NAPI-RS */
export declare class Model { export declare class Model {
constructor(name: string, locale: string, timezone: string) constructor(name: string, locale: string, timezone: string)
static fromXlsx(filePath: string, locale: string, tz: string): Model static fromXlsx(filePath: string, locale: string, tz: string): Model
@@ -36,10 +39,8 @@ export declare class Model {
newDefinedName(name: string, scope: number | undefined | null, formula: string): void newDefinedName(name: string, scope: number | undefined | null, formula: string): void
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
deleteDefinedName(name: string, scope?: number | undefined | null): void deleteDefinedName(name: string, scope?: number | undefined | null): void
moveColumn(sheet: number, column: number, delta: number): void testPanic(): void
moveRow(sheet: number, row: number, delta: number): void
} }
export declare class UserModel { export declare class UserModel {
constructor(name: string, locale: string, timezone: string) constructor(name: string, locale: string, timezone: string)
static fromBytes(bytes: Uint8Array): UserModel static fromBytes(bytes: Uint8Array): UserModel
@@ -58,13 +59,12 @@ export declare class UserModel {
setSheetColor(sheet: number, color: string): void setSheetColor(sheet: number, color: string): void
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
rangeClearFormatting(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void insertRow(sheet: number, row: number): void
insertRows(sheet: number, row: number, rowCount: number): void insertColumn(sheet: number, column: number): void
insertColumns(sheet: number, column: number, columnCount: number): void deleteRow(sheet: number, row: number): void
deleteRows(sheet: number, row: number, rowCount: number): void deleteColumn(sheet: number, column: number): void
deleteColumns(sheet: number, column: number, columnCount: number): void setRowHeight(sheet: number, row: number, height: number): void
setRowsHeight(sheet: number, rowStart: number, rowEnd: number, height: number): void setColumnWidth(sheet: number, column: number, width: number): void
setColumnsWidth(sheet: number, columnStart: number, columnEnd: number, width: number): void
getRowHeight(sheet: number, row: number): number getRowHeight(sheet: number, row: number): number
getColumnWidth(sheet: number, column: number): number getColumnWidth(sheet: number, column: number): number
setUserInput(sheet: number, row: number, column: number, input: string): void setUserInput(sheet: number, row: number, column: number, input: string): void
@@ -112,6 +112,4 @@ export declare class UserModel {
newDefinedName(name: string, scope: number | undefined | null, formula: string): void newDefinedName(name: string, scope: number | undefined | null, formula: string): void
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
deleteDefinedName(name: string, scope?: number | undefined | null): void deleteDefinedName(name: string, scope?: number | undefined | null): void
moveColumn(sheet: number, column: number, delta: number): void
moveRow(sheet: number, row: number, delta: number): void
} }

View File

@@ -1,579 +1,316 @@
// prettier-ignore /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
// @ts-nocheck /* prettier-ignore */
/* auto-generated by NAPI-RS */ /* auto-generated by NAPI-RS */
const { createRequire } = require('node:module') const { existsSync, readFileSync } = require('fs')
require = createRequire(__filename) const { join } = require('path')
const { platform, arch } = process
const { readFileSync } = require('node:fs')
let nativeBinding = null let nativeBinding = null
const loadErrors = [] let localFileExisted = false
let loadError = null
const isMusl = () => { function isMusl() {
let musl = false // For Node 10
if (process.platform === 'linux') { if (!process.report || typeof process.report.getReport !== 'function') {
musl = isMuslFromFilesystem() try {
if (musl === null) { const lddPath = require('child_process').execSync('which ldd').toString().trim()
musl = isMuslFromReport() return readFileSync(lddPath, 'utf8').includes('musl')
} } catch (e) {
if (musl === null) {
musl = isMuslFromChildProcess()
}
}
return musl
}
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
const isMuslFromFilesystem = () => {
try {
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
} catch {
return null
}
}
const isMuslFromReport = () => {
let report = null
if (typeof process.report?.getReport === 'function') {
process.report.excludeNetwork = true
report = process.report.getReport()
}
if (!report) {
return null
}
if (report.header && report.header.glibcVersionRuntime) {
return false
}
if (Array.isArray(report.sharedObjects)) {
if (report.sharedObjects.some(isFileMusl)) {
return true return true
} }
}
return false
}
const isMuslFromChildProcess = () => {
try {
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
} catch (e) {
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
return false
}
}
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
} catch (err) {
loadErrors.push(err)
}
} else if (process.platform === 'android') {
if (process.arch === 'arm64') {
try {
return require('./nodejs.android-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-android-arm64')
const bindingPackageVersion = require('@ironcalc/nodejs-android-arm64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./nodejs.android-arm-eabi.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-android-arm-eabi')
const bindingPackageVersion = require('@ironcalc/nodejs-android-arm-eabi/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
}
} else if (process.platform === 'win32') {
if (process.arch === 'x64') {
if (process.report?.getReport?.()?.header?.osName?.startsWith?.('MINGW')) {
try {
return require('./nodejs.win32-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-win32-x64-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-win32-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nodejs.win32-x64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-win32-x64-msvc')
const bindingPackageVersion = require('@ironcalc/nodejs-win32-x64-msvc/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ia32') {
try {
return require('./nodejs.win32-ia32-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-win32-ia32-msvc')
const bindingPackageVersion = require('@ironcalc/nodejs-win32-ia32-msvc/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./nodejs.win32-arm64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-win32-arm64-msvc')
const bindingPackageVersion = require('@ironcalc/nodejs-win32-arm64-msvc/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
}
} else if (process.platform === 'darwin') {
try {
return require('./nodejs.darwin-universal.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-darwin-universal')
const bindingPackageVersion = require('@ironcalc/nodejs-darwin-universal/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
if (process.arch === 'x64') {
try {
return require('./nodejs.darwin-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-darwin-x64')
const bindingPackageVersion = require('@ironcalc/nodejs-darwin-x64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./nodejs.darwin-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-darwin-arm64')
const bindingPackageVersion = require('@ironcalc/nodejs-darwin-arm64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
}
} else if (process.platform === 'freebsd') {
if (process.arch === 'x64') {
try {
return require('./nodejs.freebsd-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-freebsd-x64')
const bindingPackageVersion = require('@ironcalc/nodejs-freebsd-x64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./nodejs.freebsd-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-freebsd-arm64')
const bindingPackageVersion = require('@ironcalc/nodejs-freebsd-arm64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
}
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
if (isMusl()) {
try {
return require('./nodejs.linux-x64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-x64-musl')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-x64-musl/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nodejs.linux-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-x64-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm64') {
if (isMusl()) {
try {
return require('./nodejs.linux-arm64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-arm64-musl')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-arm64-musl/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nodejs.linux-arm64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-arm64-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-arm64-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm') {
if (isMusl()) {
try {
return require('./nodejs.linux-arm-musleabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-arm-musleabihf')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-arm-musleabihf/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nodejs.linux-arm-gnueabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-arm-gnueabihf')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-arm-gnueabihf/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'loong64') {
if (isMusl()) {
try {
return require('./nodejs.linux-loong64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-loong64-musl')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-loong64-musl/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nodejs.linux-loong64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-loong64-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-loong64-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'riscv64') {
if (isMusl()) {
try {
return require('./nodejs.linux-riscv64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-riscv64-musl')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-riscv64-musl/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nodejs.linux-riscv64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-riscv64-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-riscv64-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ppc64') {
try {
return require('./nodejs.linux-ppc64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-ppc64-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-ppc64-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 's390x') {
try {
return require('./nodejs.linux-s390x-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-linux-s390x-gnu')
const bindingPackageVersion = require('@ironcalc/nodejs-linux-s390x-gnu/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
}
} else if (process.platform === 'openharmony') {
if (process.arch === 'arm64') {
try {
return require('./nodejs.openharmony-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-openharmony-arm64')
const bindingPackageVersion = require('@ironcalc/nodejs-openharmony-arm64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'x64') {
try {
return require('./nodejs.openharmony-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-openharmony-x64')
const bindingPackageVersion = require('@ironcalc/nodejs-openharmony-x64/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./nodejs.openharmony-arm.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@ironcalc/nodejs-openharmony-arm')
const bindingPackageVersion = require('@ironcalc/nodejs-openharmony-arm/package.json').version
if (bindingPackageVersion !== '0.6.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
}
} else { } else {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
} }
} }
nativeBinding = requireNative() switch (platform) {
case 'android':
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { switch (arch) {
let wasiBinding = null case 'arm64':
let wasiBindingError = null localFileExisted = existsSync(join(__dirname, 'nodejs.android-arm64.node'))
try { try {
wasiBinding = require('./nodejs.wasi.cjs') if (localFileExisted) {
nativeBinding = wasiBinding nativeBinding = require('./nodejs.android-arm64.node')
} catch (err) { } else {
if (process.env.NAPI_RS_FORCE_WASI) { nativeBinding = require('@ironcalc/nodejs-android-arm64')
wasiBindingError = err }
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'nodejs.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.android-arm-eabi.node')
} else {
nativeBinding = require('@ironcalc/nodejs-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
} }
} break
if (!nativeBinding) { case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-x64-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-ia32-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-arm64-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'nodejs.darwin-universal.node'))
try { try {
wasiBinding = require('@ironcalc/nodejs-wasm32-wasi') if (localFileExisted) {
nativeBinding = wasiBinding nativeBinding = require('./nodejs.darwin-universal.node')
} catch (err) { } else {
if (process.env.NAPI_RS_FORCE_WASI) { nativeBinding = require('@ironcalc/nodejs-darwin-universal')
wasiBindingError.cause = err
loadErrors.push(err)
} }
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'nodejs.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-x64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-arm64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
} }
} break
if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { case 'freebsd':
const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') if (arch !== 'x64') {
error.cause = wasiBindingError throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
throw error }
} localFileExisted = existsSync(join(__dirname, 'nodejs.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.freebsd-x64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-x64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-x64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm-musleabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm-musleabihf.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm-musleabihf')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-riscv64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-riscv64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-s390x-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-s390x-gnu')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
} }
if (!nativeBinding) { if (!nativeBinding) {
if (loadErrors.length > 0) { if (loadError) {
throw new Error( throw loadError
`Cannot find native binding. ` +
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
{
cause: loadErrors.reduce((err, cur) => {
cur.cause = err
return cur
}),
},
)
} }
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
module.exports = nativeBinding const { Model, UserModel } = nativeBinding
module.exports.Model = nativeBinding.Model
module.exports.UserModel = nativeBinding.UserModel module.exports.Model = Model
module.exports.UserModel = UserModel

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-android-arm-eabi", "name": "@ironcalc/nodejs-android-arm-eabi",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"android" "android"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-android-arm64", "name": "@ironcalc/nodejs-android-arm64",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"android" "android"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-darwin-arm64", "name": "@ironcalc/nodejs-darwin-arm64",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"darwin" "darwin"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-darwin-universal", "name": "@ironcalc/nodejs-darwin-universal",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"darwin" "darwin"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-darwin-x64", "name": "@ironcalc/nodejs-darwin-x64",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"darwin" "darwin"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-arm-gnueabihf", "name": "@ironcalc/nodejs-linux-arm-gnueabihf",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-arm-musleabihf", "name": "@ironcalc/nodejs-linux-arm-musleabihf",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-arm64-gnu", "name": "@ironcalc/nodejs-linux-arm64-gnu",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-arm64-musl", "name": "@ironcalc/nodejs-linux-arm64-musl",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-riscv64-gnu", "name": "@ironcalc/nodejs-linux-riscv64-gnu",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-x64-gnu", "name": "@ironcalc/nodejs-linux-x64-gnu",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-linux-x64-musl", "name": "@ironcalc/nodejs-linux-x64-musl",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"linux" "linux"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-win32-arm64-msvc", "name": "@ironcalc/nodejs-win32-arm64-msvc",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"win32" "win32"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs-win32-x64-msvc", "name": "@ironcalc/nodejs-win32-x64-msvc",
"version": "0.6.0", "version": "0.2.0",
"os": [ "os": [
"win32" "win32"
], ],

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,29 @@
{ {
"name": "@ironcalc/nodejs", "name": "@ironcalc/nodejs",
"version": "0.6.0", "version": "0.5.1",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"napi": { "napi": {
"binaryName": "nodejs", "name": "nodejs",
"targets": [ "triples": {
"aarch64-apple-darwin", "additional": [
"aarch64-linux-android", "aarch64-apple-darwin",
"aarch64-unknown-linux-gnu", "aarch64-linux-android",
"aarch64-unknown-linux-musl", "aarch64-unknown-linux-gnu",
"aarch64-pc-windows-msvc", "aarch64-unknown-linux-musl",
"armv7-unknown-linux-gnueabihf", "aarch64-pc-windows-msvc",
"armv7-unknown-linux-musleabihf", "armv7-unknown-linux-gnueabihf",
"x86_64-unknown-linux-musl", "armv7-unknown-linux-musleabihf",
"armv7-linux-androideabi", "x86_64-unknown-linux-musl",
"universal-apple-darwin", "armv7-linux-androideabi",
"riscv64gc-unknown-linux-gnu" "universal-apple-darwin",
] "riscv64gc-unknown-linux-gnu"
]
}
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@napi-rs/cli": "^3.3", "@napi-rs/cli": "^2.18.4",
"ava": "^6.0.1" "ava": "^6.0.1"
}, },
"ava": { "ava": {
@@ -36,7 +38,7 @@
"build:debug": "napi build --platform", "build:debug": "napi build --platform",
"prepublishOnly": "napi prepublish -t npm", "prepublishOnly": "napi prepublish -t npm",
"test": "ava", "test": "ava",
"universal": "napi universalize", "universal": "napi universal",
"version": "napi version" "version": "napi version"
} }
} }

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
#![deny(clippy::all)] #![deny(clippy::all)]
use napi::{self, bindgen_prelude::*, Result, Unknown}; use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use serde::Serialize; use serde::Serialize;
use ironcalc::{ use ironcalc::{
@@ -127,7 +127,7 @@ impl Model {
sheet: u32, sheet: u32,
row: i32, row: i32,
column: i32, column: i32,
style: Unknown, style: JsUnknown,
) -> Result<()> { ) -> Result<()> {
let style: Style = env let style: Style = env
.from_js_value(style) .from_js_value(style)
@@ -140,12 +140,12 @@ impl Model {
#[napi(js_name = "getCellStyle")] #[napi(js_name = "getCellStyle")]
pub fn get_cell_style( pub fn get_cell_style(
&'_ self, &mut self,
env: Env, env: Env,
sheet: u32, sheet: u32,
row: i32, row: i32,
column: i32, column: i32,
) -> Result<Unknown<'_>> { ) -> Result<JsUnknown> {
let style = self let style = self
.model .model
.get_style_for_cell(sheet, row, column) .get_style_for_cell(sheet, row, column)
@@ -246,11 +246,11 @@ impl Model {
.map_err(to_js_error) .map_err(to_js_error)
} }
// I don't _think_ serializing to Unknown can't fail // I don't _think_ serializing to JsUnknown can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[napi(js_name = "getWorksheetsProperties")] #[napi(js_name = "getWorksheetsProperties")]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&'_ self, env: Env) -> Unknown<'_> { pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
env env
.to_js_value(&self.model.get_worksheets_properties()) .to_js_value(&self.model.get_worksheets_properties())
.unwrap() .unwrap()
@@ -288,7 +288,7 @@ impl Model {
} }
#[napi(js_name = "getDefinedNameList")] #[napi(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&'_ self, env: Env) -> Result<Unknown<'_>> { pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
let data: Vec<DefinedName> = self let data: Vec<DefinedName> = self
.model .model
.workbook .workbook
@@ -340,20 +340,4 @@ impl Model {
.delete_defined_name(&name, scope) .delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[napi(js_name = "moveColumn")]
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
self
.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[napi(js_name = "moveRow")]
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
self
.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
}
} }

View File

@@ -2,7 +2,7 @@
use serde::Serialize; use serde::Serialize;
use napi::{self, bindgen_prelude::*, Result, Unknown}; use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use ironcalc::base::{ use ironcalc::base::{
expressions::types::Area, expressions::types::Area,
@@ -183,36 +183,24 @@ impl UserModel {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[napi(js_name = "insertRows")] #[napi(js_name = "insertRow")]
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> { pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
self self.model.insert_row(sheet, row).map_err(to_js_error)
.model
.insert_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[napi(js_name = "insertColumns")] #[napi(js_name = "insertColumn")]
pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> { pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
self self.model.insert_column(sheet, column).map_err(to_js_error)
.model
.insert_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[napi(js_name = "deleteRows")] #[napi(js_name = "deleteRow")]
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> { pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
self self.model.delete_row(sheet, row).map_err(to_js_error)
.model
.delete_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[napi(js_name = "deleteColumns")] #[napi(js_name = "deleteColumn")]
pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> { pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
self self.model.delete_column(sheet, column).map_err(to_js_error)
.model
.delete_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[napi(js_name = "setRowsHeight")] #[napi(js_name = "setRowsHeight")]
@@ -305,7 +293,7 @@ impl UserModel {
pub fn update_range_style( pub fn update_range_style(
&mut self, &mut self,
env: Env, env: Env,
range: Unknown, range: JsUnknown,
style_path: String, style_path: String,
value: String, value: String,
) -> Result<()> { ) -> Result<()> {
@@ -320,12 +308,12 @@ impl UserModel {
#[napi(js_name = "getCellStyle")] #[napi(js_name = "getCellStyle")]
pub fn get_cell_style( pub fn get_cell_style(
&'_ mut self, &mut self,
env: Env, env: Env,
sheet: u32, sheet: u32,
row: i32, row: i32,
column: i32, column: i32,
) -> Result<Unknown<'_>> { ) -> Result<JsUnknown> {
let style = self let style = self
.model .model
.get_cell_style(sheet, row, column) .get_cell_style(sheet, row, column)
@@ -337,7 +325,7 @@ impl UserModel {
} }
#[napi(js_name = "onPasteStyles")] #[napi(js_name = "onPasteStyles")]
pub fn on_paste_styles(&mut self, env: Env, styles: Unknown) -> Result<()> { pub fn on_paste_styles(&mut self, env: Env, styles: JsUnknown) -> Result<()> {
let styles: &Vec<Vec<Style>> = &env let styles: &Vec<Vec<Style>> = &env
.from_js_value(styles) .from_js_value(styles)
.map_err(|e| to_js_error(e.to_string()))?; .map_err(|e| to_js_error(e.to_string()))?;
@@ -362,11 +350,11 @@ impl UserModel {
) )
} }
// I don't _think_ serializing to Unknown can't fail // I don't _think_ serializing to JsUnknown can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[napi(js_name = "getWorksheetsProperties")] #[napi(js_name = "getWorksheetsProperties")]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&'_ self, env: Env) -> Unknown<'_> { pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
env env
.to_js_value(&self.model.get_worksheets_properties()) .to_js_value(&self.model.get_worksheets_properties())
.unwrap() .unwrap()
@@ -383,11 +371,11 @@ impl UserModel {
vec![sheet as i32, row, column] vec![sheet as i32, row, column]
} }
// I don't _think_ serializing to Unknown can't fail // I don't _think_ serializing to JsUnknown can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[napi(js_name = "getSelectedView")] #[napi(js_name = "getSelectedView")]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_selected_view(&'_ self, env: Env) -> Unknown<'_> { pub fn get_selected_view(&self, env: Env) -> JsUnknown {
env.to_js_value(&self.model.get_selected_view()).unwrap() env.to_js_value(&self.model.get_selected_view()).unwrap()
} }
@@ -440,7 +428,7 @@ impl UserModel {
} }
#[napi(js_name = "autoFillRows")] #[napi(js_name = "autoFillRows")]
pub fn auto_fill_rows(&mut self, env: Env, source_area: Unknown, to_row: i32) -> Result<()> { pub fn auto_fill_rows(&mut self, env: Env, source_area: JsUnknown, to_row: i32) -> Result<()> {
let area: Area = env let area: Area = env
.from_js_value(source_area) .from_js_value(source_area)
.map_err(|e| to_js_error(e.to_string()))?; .map_err(|e| to_js_error(e.to_string()))?;
@@ -454,7 +442,7 @@ impl UserModel {
pub fn auto_fill_columns( pub fn auto_fill_columns(
&mut self, &mut self,
env: Env, env: Env,
source_area: Unknown, source_area: JsUnknown,
to_column: i32, to_column: i32,
) -> Result<()> { ) -> Result<()> {
let area: Area = env let area: Area = env
@@ -536,8 +524,8 @@ impl UserModel {
pub fn set_area_with_border( pub fn set_area_with_border(
&mut self, &mut self,
env: Env, env: Env,
area: Unknown, area: JsUnknown,
border_area: Unknown, border_area: JsUnknown,
) -> Result<()> { ) -> Result<()> {
let range: Area = env let range: Area = env
.from_js_value(area) .from_js_value(area)
@@ -568,7 +556,7 @@ impl UserModel {
} }
#[napi(js_name = "copyToClipboard")] #[napi(js_name = "copyToClipboard")]
pub fn copy_to_clipboard(&'_ self, env: Env) -> Result<Unknown<'_>> { pub fn copy_to_clipboard(&self, env: Env) -> Result<JsUnknown> {
let data = self let data = self
.model .model
.copy_to_clipboard() .copy_to_clipboard()
@@ -584,8 +572,8 @@ impl UserModel {
&mut self, &mut self,
env: Env, env: Env,
source_sheet: u32, source_sheet: u32,
source_range: Unknown, source_range: JsUnknown,
clipboard: Unknown, clipboard: JsUnknown,
is_cut: bool, is_cut: bool,
) -> Result<()> { ) -> Result<()> {
let source_range: (i32, i32, i32, i32) = env let source_range: (i32, i32, i32, i32) = env
@@ -601,7 +589,7 @@ impl UserModel {
} }
#[napi(js_name = "pasteCsvText")] #[napi(js_name = "pasteCsvText")]
pub fn paste_csv_string(&mut self, env: Env, area: Unknown, csv: String) -> Result<()> { pub fn paste_csv_string(&mut self, env: Env, area: JsUnknown, csv: String) -> Result<()> {
let range: Area = env let range: Area = env
.from_js_value(area) .from_js_value(area)
.map_err(|e| to_js_error(e.to_string()))?; .map_err(|e| to_js_error(e.to_string()))?;
@@ -612,7 +600,7 @@ impl UserModel {
} }
#[napi(js_name = "getDefinedNameList")] #[napi(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&'_ self, env: Env) -> Result<Unknown<'_>> { pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
let data: Vec<DefinedName> = self let data: Vec<DefinedName> = self
.model .model
.get_defined_name_list() .get_defined_name_list()
@@ -663,20 +651,4 @@ impl UserModel {
.delete_defined_name(&name, scope) .delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[napi(js_name = "moveColumn")]
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
self
.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[napi(js_name = "moveRow")]
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
self
.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
}
} }

1575
bindings/nodejs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pyroncalc" name = "pyroncalc"
version = "0.6.0" version = "0.5.0"
edition = "2021" edition = "2021"
@@ -12,9 +12,8 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.6.0" } xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
pyo3 = { version = "0.25", features = ["extension-module"] } pyo3 = { version = "0.23", features = ["extension-module"] }
bitcode = "0.6.3"
[features] [features]

View File

@@ -10,6 +10,9 @@ You can add cell values, retrieve them and most importantly you can evaluate spr
pip install ironcalc pip install ironcalc
``` ```
## Compile and test ## Compile and test
To compile this and test it: To compile this and test it:
@@ -26,17 +29,3 @@ examples $ python example.py
From there if you use `python` you can `import ironcalc`. You can either create a new file, read it from a JSON string or import from Excel. From there if you use `python` you can `import ironcalc`. You can either create a new file, read it from a JSON string or import from Excel.
Hopefully the API is straightforward. Hopefully the API is straightforward.
## Creating documentation
We use sphinx
```
python -m venv venv
source venv/bin/activate
pip install maturin
pip install sphinx
maturin develop
sphinx-build -M html docs html
python -m http.server --directory html/html/
```

View File

@@ -1,9 +0,0 @@
#!/bin/bash
python -m venv venv
source venv/bin/activate
pip install patchelf
pip install maturin
pip install sphinx
maturin develop
sphinx-build -M html docs html
python -m http.server --directory html/html/

View File

@@ -1,6 +1,6 @@
Raw API Reference API Reference
----------------- -------------
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes. In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.

View File

@@ -8,8 +8,7 @@ IronCalc
installation installation
usage_examples usage_examples
top_level_methods top_level_methods
raw_api_reference api_reference
user_api_reference
objects objects
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets. IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.

View File

@@ -1,13 +1,6 @@
Top Level Methods Top Level Methods
----------------- -----------------
This module provides a set of top-level methods for creating and loading IronCalc models.
.. autofunction:: ironcalc.create .. autofunction:: ironcalc.create
.. autofunction:: ironcalc.load_from_xlsx .. autofunction:: ironcalc.load_from_xlsx
.. autofunction:: ironcalc.load_from_icalc .. autofunction:: ironcalc.load_from_icalc
.. autofunction:: ironcalc.load_from_bytes
.. autofunction:: ironcalc.create_user_model
.. autofunction:: ironcalc.create_user_model_from_bytes
.. autofunction:: ironcalc.create_user_model_from_xlsx
.. autofunction:: ironcalc.create_user_model_from_icalc

View File

@@ -1,41 +0,0 @@
User API Reference
------------------
This is the "user api". Models here have history, they evaluate automatically with each change and have a "diff" history.
.. method:: save_to_xlsx(file: str)
Saves the user model to file in the XLSX format.
::param file: The file path to save the model to.
.. method:: save_to_icalc(file: str)
Saves the user model to file in the internal binary ic format.
::param file: The file path to save the model to.
.. method:: apply_external_diffs(external_diffs: bytes)
Applies external diffs to the model. This is used to apply changes from other instances of the model.
::param external_diffs: The external diffs to apply, as a byte array.
.. method:: flush_send_queue() -> bytes
Flushes the send queue and returns the bytes to be sent to the client. This is used to send changes to the client.
.. method:: set_user_input(sheet: int, row: int, column: int, value: str)
Sets an input in a cell, as would be done by a user typing into a spreadsheet cell.
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
Returns the cells value as a formatted string, taking into account any number/currency/date formatting.
.. method:: to_bytes() -> bytes
Returns the model as a byte array. This is useful for sending the model over a network or saving it to a file.

View File

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

View File

@@ -1,89 +1,19 @@
use pyo3::exceptions::PyException; use pyo3::exceptions::PyException;
use pyo3::{create_exception, prelude::*, wrap_pyfunction}; use pyo3::{create_exception, prelude::*, wrap_pyfunction};
use types::{PyCellType, PySheetProperty, PyStyle}; use types::{PySheetProperty, PyStyle};
use xlsx::base::types::{Style, Workbook}; use xlsx::base::types::Style;
use xlsx::base::{Model, UserModel}; use xlsx::base::Model;
use xlsx::export::{save_to_icalc, save_to_xlsx}; use xlsx::export::{save_to_icalc, save_to_xlsx};
use xlsx::import; use xlsx::import;
mod types; mod types;
use crate::types::PyCellType;
create_exception!(_ironcalc, WorkbookError, PyException); create_exception!(_ironcalc, WorkbookError, PyException);
#[pyclass]
pub struct PyUserModel {
/// The user model, which is a wrapper around the Model
pub model: UserModel,
}
#[pymethods]
impl PyUserModel {
/// Saves the user model to an xlsx file
pub fn save_to_xlsx(&self, file: &str) -> PyResult<()> {
let model = self.model.get_model();
save_to_xlsx(model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
}
/// Saves the user model to file in the internal binary ic format
pub fn save_to_icalc(&self, file: &str) -> PyResult<()> {
let model = self.model.get_model();
save_to_icalc(model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
}
pub fn apply_external_diffs(&mut self, external_diffs: &[u8]) -> PyResult<()> {
self.model
.apply_external_diffs(external_diffs)
.map_err(|e| WorkbookError::new_err(e.to_string()))
}
pub fn flush_send_queue(&mut self) -> Vec<u8> {
self.model.flush_send_queue()
}
pub fn set_user_input(
&mut self,
sheet: u32,
row: i32,
column: i32,
value: &str,
) -> PyResult<()> {
self.model
.set_user_input(sheet, row, column, value)
.map_err(|e| WorkbookError::new_err(e.to_string()))
}
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> PyResult<String> {
self.model
.get_formatted_cell_value(sheet, row, column)
.map_err(|e| WorkbookError::new_err(e.to_string()))
}
/// Gets the dimensions of a worksheet, returning the bounds of all non-empty cells.
/// Returns a tuple of (min_row, max_row, min_column, max_column).
/// For an empty sheet, returns (1, 1, 1, 1).
pub fn get_sheet_dimensions(&self, sheet: u32) -> PyResult<(i32, i32, i32, i32)> {
let model = self.model.get_model();
let worksheet = model
.workbook
.worksheet(sheet)
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
let dimension = worksheet.dimension();
Ok((
dimension.min_row,
dimension.max_row,
dimension.min_column,
dimension.max_column,
))
}
pub fn to_bytes(&self) -> PyResult<Vec<u8>> {
let bytes = self.model.to_bytes();
Ok(bytes)
}
}
/// This is a model implementing the 'raw' API /// This is a model implementing the 'raw' API
#[pyclass] #[pyclass]
pub struct PyModel { pub struct PyModel {
@@ -102,12 +32,6 @@ impl PyModel {
save_to_icalc(&self.model, file).map_err(|e| WorkbookError::new_err(e.to_string())) save_to_icalc(&self.model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
} }
/// To bytes
pub fn to_bytes(&self) -> PyResult<Vec<u8>> {
let bytes = self.model.to_bytes();
Ok(bytes)
}
/// Evaluates the workbook /// Evaluates the workbook
pub fn evaluate(&mut self) { pub fn evaluate(&mut self) {
self.model.evaluate() self.model.evaluate()
@@ -301,24 +225,6 @@ impl PyModel {
.map_err(|e| WorkbookError::new_err(e.to_string())) .map_err(|e| WorkbookError::new_err(e.to_string()))
} }
/// Gets the dimensions of a worksheet, returning the bounds of all non-empty cells.
/// Returns a tuple of (min_row, max_row, min_column, max_column).
/// For an empty sheet, returns (1, 1, 1, 1).
pub fn get_sheet_dimensions(&self, sheet: u32) -> PyResult<(i32, i32, i32, i32)> {
let worksheet = self
.model
.workbook
.worksheet(sheet)
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
let dimension = worksheet.dimension();
Ok((
dimension.min_row,
dimension.max_row,
dimension.min_column,
dimension.max_column,
))
}
#[allow(clippy::panic)] #[allow(clippy::panic)]
pub fn test_panic(&self) -> PyResult<()> { pub fn test_panic(&self) -> PyResult<()> {
panic!("This function panics for testing panic handling"); panic!("This function panics for testing panic handling");
@@ -343,19 +249,7 @@ pub fn load_from_icalc(file_name: &str) -> PyResult<PyModel> {
Ok(PyModel { model }) Ok(PyModel { model })
} }
/// Loads a model from bytes /// Creates an empty model
/// This function expects the bytes to be in the internal binary ic format
/// which is the same format used by the `save_to_icalc` function.
#[pyfunction]
pub fn load_from_bytes(bytes: &[u8]) -> PyResult<PyModel> {
let workbook: Workbook =
bitcode::decode(bytes).map_err(|e| WorkbookError::new_err(e.to_string()))?;
let model =
Model::from_workbook(workbook).map_err(|e| WorkbookError::new_err(e.to_string()))?;
Ok(PyModel { model })
}
/// Creates an empty model in the raw API
#[pyfunction] #[pyfunction]
pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> { pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
let model = let model =
@@ -363,49 +257,6 @@ pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
Ok(PyModel { model }) Ok(PyModel { model })
} }
/// Creates a model with the user model API
#[pyfunction]
pub fn create_user_model(name: &str, locale: &str, tz: &str) -> PyResult<PyUserModel> {
let model = UserModel::new_empty(name, locale, tz)
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
Ok(PyUserModel { model })
}
/// Creates a user model from an Excel file
#[pyfunction]
pub fn create_user_model_from_xlsx(
file_path: &str,
locale: &str,
tz: &str,
) -> PyResult<PyUserModel> {
let model = import::load_from_xlsx(file_path, locale, tz)
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
let model = UserModel::from_model(model);
Ok(PyUserModel { model })
}
/// Creates a user model from an icalc file
#[pyfunction]
pub fn create_user_model_from_icalc(file_name: &str) -> PyResult<PyUserModel> {
let model =
import::load_from_icalc(file_name).map_err(|e| WorkbookError::new_err(e.to_string()))?;
let model = UserModel::from_model(model);
Ok(PyUserModel { model })
}
/// Creates a user model from bytes
/// This function expects the bytes to be in the internal binary ic format
/// which is the same format used by the `save_to_icalc` function.
#[pyfunction]
pub fn create_user_model_from_bytes(bytes: &[u8]) -> PyResult<PyUserModel> {
let workbook: Workbook =
bitcode::decode(bytes).map_err(|e| WorkbookError::new_err(e.to_string()))?;
let model =
Model::from_workbook(workbook).map_err(|e| WorkbookError::new_err(e.to_string()))?;
let user_model = UserModel::from_model(model);
Ok(PyUserModel { model: user_model })
}
#[pyfunction] #[pyfunction]
#[allow(clippy::panic)] #[allow(clippy::panic)]
pub fn test_panic() { pub fn test_panic() {
@@ -421,14 +272,7 @@ fn ironcalc(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(create, m)?)?; m.add_function(wrap_pyfunction!(create, m)?)?;
m.add_function(wrap_pyfunction!(load_from_xlsx, m)?)?; m.add_function(wrap_pyfunction!(load_from_xlsx, m)?)?;
m.add_function(wrap_pyfunction!(load_from_icalc, m)?)?; m.add_function(wrap_pyfunction!(load_from_icalc, m)?)?;
m.add_function(wrap_pyfunction!(load_from_bytes, m)?)?;
m.add_function(wrap_pyfunction!(test_panic, m)?)?; m.add_function(wrap_pyfunction!(test_panic, m)?)?;
// User model functions
m.add_function(wrap_pyfunction!(create_user_model, m)?)?;
m.add_function(wrap_pyfunction!(create_user_model_from_bytes, m)?)?;
m.add_function(wrap_pyfunction!(create_user_model_from_xlsx, m)?)?;
m.add_function(wrap_pyfunction!(create_user_model_from_icalc, m)?)?;
Ok(()) Ok(())
} }

View File

@@ -6,52 +6,3 @@ def test_simple():
model.evaluate() model.evaluate()
assert model.get_formatted_cell_value(0, 1, 1) == "3" assert model.get_formatted_cell_value(0, 1, 1) == "3"
bytes = model.to_bytes()
model2 = ic.load_from_bytes(bytes)
assert model2.get_formatted_cell_value(0, 1, 1) == "3"
def test_simple_user():
model = ic.create_user_model("model", "en", "UTC")
model.set_user_input(0, 1, 1, "=1+2")
model.set_user_input(0, 1, 2, "=A1+3")
assert model.get_formatted_cell_value(0, 1, 1) == "3"
assert model.get_formatted_cell_value(0, 1, 2) == "6"
diffs = model.flush_send_queue()
model2 = ic.create_user_model("model", "en", "UTC")
model2.apply_external_diffs(diffs)
assert model2.get_formatted_cell_value(0, 1, 1) == "3"
assert model2.get_formatted_cell_value(0, 1, 2) == "6"
def test_sheet_dimensions():
# Test with empty sheet
model = ic.create("model", "en", "UTC")
min_row, max_row, min_col, max_col = model.get_sheet_dimensions(0)
assert (min_row, max_row, min_col, max_col) == (1, 1, 1, 1)
# Add some cells
model.set_user_input(0, 3, 5, "Hello")
model.set_user_input(0, 10, 8, "World")
model.evaluate()
# Check dimensions - should span from (3,5) to (10,8)
min_row, max_row, min_col, max_col = model.get_sheet_dimensions(0)
assert (min_row, max_row, min_col, max_col) == (3, 10, 5, 8)
def test_sheet_dimensions_user_model():
# Test with user model API as well
model = ic.create_user_model("model", "en", "UTC")
# Add a single cell
model.set_user_input(0, 2, 3, "Test")
# Check dimensions
min_row, max_row, min_col, max_col = model.get_sheet_dimensions(0)
assert (min_row, max_row, min_col, max_col) == (2, 2, 3, 3)

View File

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

View File

@@ -7,7 +7,6 @@ use wasm_bindgen::{
use ironcalc_base::{ use ironcalc_base::{
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column}, expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
types::{CellType, Style}, types::{CellType, Style},
worksheet::NavigationDirection,
BorderArea, ClipboardData, UserModel as BaseModel, BorderArea, ClipboardData, UserModel as BaseModel,
}; };
@@ -196,61 +195,24 @@ impl Model {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "insertRows")] #[wasm_bindgen(js_name = "insertRow")]
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> { pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
self.model self.model.insert_row(sheet, row).map_err(to_js_error)
.insert_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "insertColumns")] #[wasm_bindgen(js_name = "insertColumn")]
pub fn insert_columns( pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
&mut self, self.model.insert_column(sheet, column).map_err(to_js_error)
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), JsError> {
self.model
.insert_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "deleteRows")] #[wasm_bindgen(js_name = "deleteRow")]
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> { pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
self.model self.model.delete_row(sheet, row).map_err(to_js_error)
.delete_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "deleteColumns")] #[wasm_bindgen(js_name = "deleteColumn")]
pub fn delete_columns( pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
&mut self, self.model.delete_column(sheet, column).map_err(to_js_error)
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), JsError> {
self.model
.delete_columns(sheet, column, column_count)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "moveColumn")]
pub fn move_column_action(
&mut self,
sheet: u32,
column: i32,
delta: i32,
) -> Result<(), JsError> {
self.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "moveRow")]
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), JsError> {
self.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "setRowsHeight")] #[wasm_bindgen(js_name = "setRowsHeight")]
@@ -568,20 +530,6 @@ impl Model {
self.model.on_page_up().map_err(to_js_error) self.model.on_page_up().map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "onNavigateToEdgeInDirection")]
pub fn on_navigate_to_edge_in_direction(&mut self, direction: &str) -> Result<(), JsError> {
let direction = match direction {
"ArrowLeft" => NavigationDirection::Left,
"ArrowRight" => NavigationDirection::Right,
"ArrowUp" => NavigationDirection::Up,
"ArrowDown" => NavigationDirection::Down,
_ => return Err(JsError::new(&format!("Invalid direction: {direction}"))),
};
self.model
.on_navigate_to_edge_in_direction(direction)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setWindowWidth")] #[wasm_bindgen(js_name = "setWindowWidth")]
pub fn set_window_width(&mut self, window_width: f64) { pub fn set_window_width(&mut self, window_width: f64) {
self.model.set_window_width(window_width); self.model.set_window_width(window_width);
@@ -742,28 +690,4 @@ impl Model {
.delete_defined_name(name, scope) .delete_defined_name(name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[wasm_bindgen(js_name = "getLastNonEmptyInRowBeforeColumn")]
pub fn get_last_non_empty_in_row_before_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, JsError> {
self.model
.get_last_non_empty_in_row_before_column(sheet, row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getFirstNonEmptyInRowAfterColumn")]
pub fn get_first_non_empty_in_row_after_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, JsError> {
self.model
.get_first_non_empty_in_row_after_column(sheet, row, column)
.map_err(to_js_error)
}
} }

View File

@@ -130,82 +130,5 @@ test("autofill", () => {
assert.strictEqual(result, "23"); assert.strictEqual(result, "23");
}); });
test('insertRows shifts cells', () => {
const model = new Model('Workbook1', 'en', 'UTC');
model.setUserInput(0, 1, 1, '42');
model.insertRows(0, 1, 1);
assert.strictEqual(model.getCellContent(0, 1, 1), '');
assert.strictEqual(model.getCellContent(0, 2, 1), '42');
});
test('insertColumns shifts cells', () => {
const model = new Model('Workbook1', 'en', 'UTC');
model.setUserInput(0, 1, 1, 'A');
model.setUserInput(0, 1, 2, 'B');
model.insertColumns(0, 2, 1);
assert.strictEqual(model.getCellContent(0, 1, 2), '');
assert.strictEqual(model.getCellContent(0, 1, 3), 'B');
});
test('deleteRows removes cells', () => {
const model = new Model('Workbook1', 'en', 'UTC');
model.setUserInput(0, 1, 1, '1');
model.setUserInput(0, 2, 1, '2');
model.deleteRows(0, 1, 1);
assert.strictEqual(model.getCellContent(0, 1, 1), '2');
assert.strictEqual(model.getCellContent(0, 2, 1), '');
});
test('deleteColumns removes cells', () => {
const model = new Model('Workbook1', 'en', 'UTC');
model.setUserInput(0, 1, 1, 'A');
model.setUserInput(0, 1, 2, 'B');
model.deleteColumns(0, 1, 1);
assert.strictEqual(model.getCellContent(0, 1, 1), 'B');
assert.strictEqual(model.getCellContent(0, 1, 2), '');
});
test("move row", () => {
const model = new Model('Workbook1', 'en', 'UTC');
model.setUserInput(0, 3, 5, "=G3");
model.setUserInput(0, 4, 5, "=G4");
model.setUserInput(0, 5, 5, "=SUM(G3:J3)");
model.setUserInput(0, 6, 5, "=SUM(G3:G3)");
model.setUserInput(0, 7, 5, "=SUM(G4:G4)");
model.evaluate();
model.moveRow(0, 3, 1);
model.evaluate();
assert.strictEqual(model.getCellContent(0, 3, 5), "=G3");
assert.strictEqual(model.getCellContent(0, 4, 5), "=G4");
assert.strictEqual(model.getCellContent(0, 5, 5), "=SUM(G4:J4)");
assert.strictEqual(model.getCellContent(0, 6, 5), "=SUM(G4:G4)");
assert.strictEqual(model.getCellContent(0, 7, 5), "=SUM(G3:G3)");
});
test("move column", () => {
const model = new Model('Workbook1', 'en', 'UTC');
model.setUserInput(0, 3, 5, "=G3");
model.setUserInput(0, 4, 5, "=H3");
model.setUserInput(0, 5, 5, "=SUM(G3:J7)");
model.setUserInput(0, 6, 5, "=SUM(G3:G7)");
model.setUserInput(0, 7, 5, "=SUM(H3:H7)");
model.evaluate();
model.moveColumn(0, 7, 1);
model.evaluate();
assert.strictEqual(model.getCellContent(0, 3, 5), "=H3");
assert.strictEqual(model.getCellContent(0, 4, 5), "=G3");
assert.strictEqual(model.getCellContent(0, 5, 5), "=SUM(H3:J7)");
assert.strictEqual(model.getCellContent(0, 6, 5), "=SUM(H3:H7)");
assert.strictEqual(model.getCellContent(0, 7, 5), "=SUM(G3:G7)");
});

View File

@@ -1,12 +0,0 @@
services:
server:
build:
context: .
target: server-runtime
caddy:
build:
context: .
target: caddy-runtime
ports:
- "2080:2080"

1802
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
}, },
"devDependencies": { "devDependencies": {
"markdown-it-mathjax3": "^4.3.2", "markdown-it-mathjax3": "^4.3.2",
"vitepress": "^v2.0.0-alpha.8", "vitepress": "^1.5.0",
"vue": "^3.5.17" "vue": "^3.5.12"
} }
} }

View File

@@ -6,57 +6,23 @@ lang: en-US
# Keyboard Shortcuts # Keyboard Shortcuts
From within your keyboard you can navigate and interact with the spreadsheet. From within your keyboard you can navigate and interact with the spreadsheet. This is a fairly interesting feature for power users.
## Navigation Shortcuts ## Common Actions
| Action | Linux/Windows | Mac | | Action | Windows | Mac |
| ----------------------- | ----------------- | -------------- | | ------ | -------- | ----- |
| Move one cell | Arrow Keys | Arrow Keys | | Copy | ctrl + c | ⌘ + c |
| Move down (Excel-style) | Enter | Enter | | Paste | ctrl + v | ⌘ + v |
| Move right | Tab | Tab | | Cut | ctrl + x | ⌘ + x |
| Move left | Shift + Tab | Shift + Tab | | Undo | ctrl + z | ⌘ + z |
| Jump to first column | Home | Fn + | | Redo | ctrl + y | + y |
| Jump to last column | End | Fn + → |
| Scroll one screen down | PageDown | Fn + ↓ |
| Scroll one screen up | PageUp | Fn + ↑ |
| Jump to cell edge | Ctrl + Arrow Keys | ⌘ + Arrow Keys |
| Jump to start of sheet | Ctrl + Home | ⌘ + Fn + ← |
| Jump to end of sheet | Ctrl + End | ⌘ + Fn + → |
## Selection & Editing ## Navigation
| Action | Linux/Windows | Mac | | <div style="width:200px">Action</div> | <div style="width:80px">Windows</div> | <div style="width:80px">Mac</div> |
| -------------------- | ------------------ | ------------------ | | ------------------------------------- | ------------------------------------- | --------------------------------- |
| Expand selection | Shift + Arrow Keys | Shift + Arrow Keys | | Move to beginning of row | ??? | Fn + Left Arrow |
| Start editing a cell | F2 | F2 | | Move to end of row | ??? | Fn + Right Arrow |
| Edit directly | Any key | Any key | | Move to previous sheet | Option + Arrow Up | Option + Arrow Up |
| Move to next sheet | Option + Arrow Down | Option + Arrow Down |
## Text Styling
| Action | Linux/Windows | Mac |
| --------- | -------------- | ----- |
| Bold | Ctrl + B | ⌘ + B |
| Italic | Ctrl + I | ⌘ + I |
| Underline | Ctrl + U | ⌘ + U |
## Undo / Redo
| Action | Linux/Windows | Mac |
| ------ | ---------------------------- | -----------------------|
| Undo | Ctrl + Z | ⌘ + Z |
| Redo | Ctrl + Y or Ctrl + Shift + Z | ⌘ + Shift + Z or ⌘ + Y |
## Sheet Navigation
| Action | Linux/Windows | Mac |
| -------------- | ---------------- | -------------- |
| Next sheet | Alt + Arrow Down | ⌥ + Arrow Down |
| Previous sheet | Alt + Arrow Up | ⌥ + Arrow Up |
## Miscellaneous
| Action | Linux/Windows/Mac |
| ------------- | -------------------- |
| Cancel action | Escape |
| Delete cells | Delete |

View File

@@ -67,7 +67,3 @@ Using IronCalc, a complex number is a string of the form "1+j3".
"#N/A" => #N/A "#N/A" => #N/A
## Arrays ## Arrays
## References
A reference is a pointer to a single cell or a range of cells. The reference can either be entered manually, for example "A4", or as the result of a calculation, such as the OFFSET Function or the INDIRECT Function. A reference can also be built, for example with the Colon (\:) Operator.

View File

@@ -12,27 +12,27 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| Function | Status | Documentation | | Function | Status | Documentation |
| ---------------- | ---------------------------------------------- | ------------- | | ---------------- | ---------------------------------------------- | ------------- |
| DATE | <Badge type="tip" text="Available" /> | | | DATE | <Badge type="tip" text="Available" /> | |
| DATEDIF | <Badge type="tip" text="Available" /> | [DATEDIF](date_and_time/datedif) | | DATEDIF | <Badge type="info" text="Not implemented yet" /> | |
| DATEVALUE | <Badge type="tip" text="Available" /> | [DATEVALUE](date_and_time/datevalue) | | DATEVALUE | <Badge type="info" text="Not implemented yet" /> | |
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) | | DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
| DAYS | <Badge type="tip" text="Available" /> | | | DAYS | <Badge type="info" text="Not implemented yet" /> | |
| DAYS360 | <Badge type="tip" text="Available" /> | | | DAYS360 | <Badge type="info" text="Not implemented yet" /> | |
| EDATE | <Badge type="tip" text="Available" /> | | | EDATE | <Badge type="tip" text="Available" /> | |
| EOMONTH | <Badge type="tip" text="Available" /> | | | EOMONTH | <Badge type="tip" text="Available" /> | |
| HOUR | <Badge type="tip" text="Available" /> | [HOUR](date_and_time/hour) | | HOUR | <Badge type="info" text="Not implemented yet" /> | |
| ISOWEEKNUM | <Badge type="tip" text="Available" /> | | | ISOWEEKNUM | <Badge type="info" text="Not implemented yet" /> | |
| MINUTE | <Badge type="tip" text="Available" /> | [MINUTE](date_and_time/minute) | | MINUTE | <Badge type="info" text="Not implemented yet" /> | |
| MONTH | <Badge type="tip" text="Available" /> | [MONTH](date_and_time/month) | | MONTH | <Badge type="tip" text="Available" /> | [MONTH](date_and_time/month) |
| NETWORKDAYS | <Badge type="tip" text="Available" /> | [NETWORKDAYS](date_and_time/networkdays) | | NETWORKDAYS | <Badge type="info" text="Not implemented yet" /> | |
| NETWORKDAYS.INTL | <Badge type="tip" text="Available" /> | [NETWORKDAYS.INTL](date_and_time/networkdays.intl) | | NETWORKDAYS.INTL | <Badge type="info" text="Not implemented yet" /> | |
| NOW | <Badge type="tip" text="Available" /> | | | NOW | <Badge type="tip" text="Available" /> | |
| SECOND | <Badge type="tip" text="Available" /> | [SECOND](date_and_time/second) | | SECOND | <Badge type="info" text="Not implemented yet" /> | |
| TIME | <Badge type="tip" text="Available" /> | [TIME](date_and_time/time) | | TIME | <Badge type="info" text="Not implemented yet" /> | |
| TIMEVALUE | <Badge type="tip" text="Available" /> | [TIMEVALUE](date_and_time/timevalue) | | TIMEVALUE | <Badge type="info" text="Not implemented yet" /> | |
| TODAY | <Badge type="tip" text="Available" /> | | | TODAY | <Badge type="tip" text="Available" /> | |
| WEEKDAY | <Badge type="tip" text="Available" /> | | | WEEKDAY | <Badge type="info" text="Not implemented yet" /> | |
| WEEKNUM | <Badge type="tip" text="Available" /> | | | WEEKNUM | <Badge type="info" text="Not implemented yet" /> | |
| WORKDAY | <Badge type="tip" text="Available" /> | | | WORKDAY | <Badge type="info" text="Not implemented yet" /> | |
| WORKDAY.INTL | <Badge type="tip" text="Available" /> | | | WORKDAY.INTL | <Badge type="info" text="Not implemented yet" /> | |
| YEAR | <Badge type="tip" text="Available" /> | [YEAR](date_and_time/year) | | YEAR | <Badge type="tip" text="Available" /> | [YEAR](date_and_time/year) |
| YEARFRAC | <Badge type="tip" text="Available" /> | | | YEARFRAC | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -7,5 +7,6 @@ lang: en-US
# DATEDIF # DATEDIF
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# DATEVALUE # DATEVALUE
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# DAYS # DAYS
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# DAYS360 # DAYS360
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,6 +7,6 @@ lang: en-US
# HOUR # HOUR
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
The HOUR function is implemented and available in IronCalc. [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# ISOWEEKNUM # ISOWEEKNUM
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

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