Compare commits

..

24 Commits

Author SHA1 Message Date
Nicolás Hatcher
ff5be2f544 UPDATE: Adds ACCRINT and ACCRINTM 2025-11-08 07:49:31 +01:00
Elsa Minsut
a2d11a42cc update: adds docs, unit tests and xlsx tests for EVEN and ODD functions (#517)
* update: adds unit test for EVEN and ODD functions

* update: adds xlsx test for EVEN and ODD functions

* update: adds EVEN and ODD doc pages

* update: Math and Trigonometry main page links to new functions

* update: changes to functions badge type in main Math and Trigonometry page
2025-11-07 04:26:01 +01:00
Elsa Minsut
480a2d1769 update: adds docs, unit tests and xlsx tests for DATEVALUE and TIMEVALUE functions (#506)
* update: adds documentation for DATEVALUE and TIMEVALUE functions

* update: adds DATEVALUE and TIMEVALUE unit tests

* update: adds DATEVALUE and TIMEVALUE xlsx tests

* update: Date and Time main page links

* update: adds testing for multiple arguments

* update: removes links to example files

* update: removes DATEVALUE and TIMEVALUE xlsx tests
2025-11-06 22:56:14 +01:00
Elsa Minsut
f30f6864e2 update: adds docs and xlsx tests for DEGREES and RADIANS functions (#507)
* update: adds DEGREES and RADIANS documentation pages

* update: adds DEGREES and RADIANS xlsx tests

* update: Math and Trigonometry main page links

* update: removes links to missing example file
2025-11-06 22:55:28 +01:00
Nicolás Hatcher Andrés
d4f69f2ec2 UPDATE: Adds missing information functions (#514)
* UPDATE: Adds missing information functions

Implements N, CELL, INFO and SHEETS

Note that INFO is implemented as N/IMPL! and CELL is not implemented
for those values that is not implemented in Excel for the web

* FIX: Copilot fixes

* FIX: Make clippy happy
2025-11-06 18:58:39 +01:00
Daniel González-Albo
3d265bba27 update: in the app, add missing favicons and use dynamic title (#508)
* update: adds multiple favicon options to the app

* update: uses the current workbook name in as page title

* update: replace favicons in assets
2025-11-05 20:54:39 +01:00
Nicolás Hatcher Andrés
68a33a5f87 UPDATE: Adds COMBIN, COMBINA and SUMSQ (#511) 2025-11-04 22:16:16 +01:00
Nicolás Hatcher Andrés
e5854ab3d7 UPDATE: Adds ARABIC and ROMAN (#509) 2025-11-03 23:44:22 +01:00
Nicolás Hatcher Andrés
7f57826371 UPDATE: Implements BASE and DECIMAL (#504) 2025-11-02 23:30:43 +01:00
Daniel González-Albo
8b7fdce278 style: widget footer improvements (#503)
* fix: add menu items to translation file

* style: tooltips, icons and paddings in footer

* style: beautify link to main site
2025-11-02 19:59:13 +01:00
Nicolás Hatcher Andrés
3e2b177ffe UPDATE: Adds GCD and LCM functions (#502)
* UPDATE: Adds GCD and LCM functions

They follow SUM and accept arrays

* FIX: Implement copilot suggestions
2025-11-02 19:50:58 +01:00
Nicolás Hatcher Andrés
efb3b66777 UPDATE: Adds time formats (#501)
* UPDATE: Adds time formats

This is the initial implementation of time formats. Simple things like:

"hh:mm:ss AM/PM"

works

* FIX: Correct padded vs unppadded time formats

Thank you copilot!
2025-11-02 13:18:26 +01:00
Nicolás Hatcher Andrés
b2d848ae2a UPDATE: Adds a bunch of mathematical functions (#496) 2025-11-01 19:32:49 +01:00
Nicolás Hatcher Andrés
c8ae835bbe UPDATE: Adds unit tests for DEGREES and RADIANS (#495) 2025-11-01 11:23:29 +01:00
Nicolás Hatcher Andrés
6ce4756d55 UPDATE: Adds DEGREES and RADIANS (#493) 2025-10-30 23:45:29 +01:00
Nicolás Hatcher Andrés
a768bc5974 Bugfix/nicolas bufixes (#491)
* UPDATE: package lock

* FIX: Add function definitions

* FIX: Small fix to get FACT working

* FIX: We only need integer FACT and FACTDOUBLE

* FIX: Make clippy happy
2025-10-30 23:24:47 +01:00
Nicolás Hatcher Andrés
7e379e24e7 UPDATE: Adds simple functions (#489)
Exp, Fact, Factdouble and sign
2025-10-30 18:28:07 +01:00
Nicolás Hatcher Andrés
f2f4992230 UPDATE: Add some missing trigonometric functions (#487)
Acot, Acoth, Cot, Coth, Csc, Csch, Sec, Sech,
2025-10-30 17:38:02 +01:00
Nicolás Hatcher Andrés
a890865eaf FIX: Quote sheet names properly (#486)
Fixes #485
2025-10-29 23:26:18 +01:00
Nicolás Hatcher Andrés
1edfb2df1c FIX: Correct order when stringify -(A1^1.22) and (-A1)^1.22 (#484)
Fixes #483
2025-10-27 19:09:31 +01:00
Nicolás Hatcher Andrés
c88bcb94ae FIX: Uses a dump randomUUID in non secure environmentes (#482)
Fixes #480
2025-10-25 17:25:29 +02:00
Nicolás Hatcher Andrés
371bec2805 FIX: Add image info (#479) 2025-10-24 22:11:39 +02:00
Elsa Minsut
92527b5e92 update: fixes to Date and Time main page (#477) 2025-10-22 12:22:24 +02:00
Nicolás Hatcher Andrés
f6b7af3555 FIX: Updates docs and minor fixes (#474) 2025-10-22 02:30:28 +02:00
88 changed files with 8718 additions and 1655 deletions

View File

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

74
Cargo.lock generated
View File

@@ -218,9 +218,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.8.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
@@ -288,20 +288,14 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.5.0"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"ctor-proc-macro",
"dtor",
"quote",
"syn",
]
[[package]]
name = "ctor-proc-macro"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
[[package]]
name = "deranged"
version = "0.3.11"
@@ -322,21 +316,6 @@ dependencies = [
"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]]
name = "either"
version = "1.10.0"
@@ -558,34 +537,33 @@ dependencies = [
[[package]]
name = "napi"
version = "3.3.0"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
"napi-build",
"napi-derive",
"napi-sys",
"nohash-hasher",
"rustc-hash",
"once_cell",
"serde",
"serde_json",
]
[[package]]
name = "napi-build"
version = "2.2.3"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
version = "3.2.5"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [
"cfg-if",
"convert_case",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
@@ -594,32 +572,28 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "2.2.0"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "3.0.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
dependencies = [
"libloading",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -897,12 +871,6 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustversion"
version = "1.0.21"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -834,6 +834,50 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Geomean => vec![Signature::Vector; arg_count],
Function::Networkdays => args_signature_networkdays(arg_count),
Function::NetworkdaysIntl => args_signature_networkdays_intl(arg_count),
Function::Acot => args_signature_scalars(arg_count, 1, 0),
Function::Acoth => args_signature_scalars(arg_count, 1, 0),
Function::Cot => args_signature_scalars(arg_count, 1, 0),
Function::Coth => args_signature_scalars(arg_count, 1, 0),
Function::Csc => args_signature_scalars(arg_count, 1, 0),
Function::Csch => args_signature_scalars(arg_count, 1, 0),
Function::Sec => args_signature_scalars(arg_count, 1, 0),
Function::Sech => args_signature_scalars(arg_count, 1, 0),
Function::Exp => args_signature_scalars(arg_count, 1, 0),
Function::Fact => args_signature_scalars(arg_count, 1, 0),
Function::Factdouble => args_signature_scalars(arg_count, 1, 0),
Function::Sign => args_signature_scalars(arg_count, 1, 0),
Function::Radians => args_signature_scalars(arg_count, 1, 0),
Function::Degrees => args_signature_scalars(arg_count, 1, 0),
Function::Int => args_signature_scalars(arg_count, 1, 0),
Function::Even => args_signature_scalars(arg_count, 1, 0),
Function::Odd => args_signature_scalars(arg_count, 1, 0),
Function::Ceiling => args_signature_scalars(arg_count, 2, 0),
Function::CeilingMath => args_signature_scalars(arg_count, 1, 2),
Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1),
Function::Floor => args_signature_scalars(arg_count, 2, 0),
Function::FloorMath => args_signature_scalars(arg_count, 1, 2),
Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1),
Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1),
Function::Mod => args_signature_scalars(arg_count, 2, 0),
Function::Quotient => args_signature_scalars(arg_count, 2, 0),
Function::Mround => args_signature_scalars(arg_count, 2, 0),
Function::Trunc => args_signature_scalars(arg_count, 1, 1),
Function::Gcd => vec![Signature::Vector; arg_count],
Function::Lcm => vec![Signature::Vector; arg_count],
Function::Base => args_signature_scalars(arg_count, 2, 1),
Function::Decimal => args_signature_scalars(arg_count, 2, 0),
Function::Roman => args_signature_scalars(arg_count, 1, 1),
Function::Arabic => args_signature_scalars(arg_count, 1, 0),
Function::Combin => args_signature_scalars(arg_count, 2, 0),
Function::Combina => args_signature_scalars(arg_count, 2, 0),
Function::Sumsq => vec![Signature::Vector; arg_count],
Function::N => args_signature_scalars(arg_count, 1, 0),
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
Function::Cell => args_signature_scalars(arg_count, 1, 1),
Function::Info => args_signature_scalars(arg_count, 1, 1),
Function::Accrint => args_signature_scalars(arg_count, 6, 2),
Function::Accrintm => args_signature_scalars(arg_count, 4, 1),
}
}
@@ -1056,5 +1100,48 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Geomean => not_implemented(args),
Function::Networkdays => not_implemented(args),
Function::NetworkdaysIntl => not_implemented(args),
Function::Acot => scalar_arguments(args),
Function::Acoth => scalar_arguments(args),
Function::Cot => scalar_arguments(args),
Function::Coth => scalar_arguments(args),
Function::Csc => scalar_arguments(args),
Function::Csch => scalar_arguments(args),
Function::Sec => scalar_arguments(args),
Function::Sech => scalar_arguments(args),
Function::Exp => scalar_arguments(args),
Function::Fact => scalar_arguments(args),
Function::Factdouble => scalar_arguments(args),
Function::Sign => scalar_arguments(args),
Function::Radians => scalar_arguments(args),
Function::Degrees => scalar_arguments(args),
Function::Int => scalar_arguments(args),
Function::Even => scalar_arguments(args),
Function::Odd => scalar_arguments(args),
Function::Ceiling => scalar_arguments(args),
Function::CeilingMath => scalar_arguments(args),
Function::CeilingPrecise => scalar_arguments(args),
Function::Floor => scalar_arguments(args),
Function::FloorMath => scalar_arguments(args),
Function::FloorPrecise => scalar_arguments(args),
Function::IsoCeiling => scalar_arguments(args),
Function::Mod => scalar_arguments(args),
Function::Quotient => scalar_arguments(args),
Function::Mround => scalar_arguments(args),
Function::Trunc => scalar_arguments(args),
Function::Gcd => not_implemented(args),
Function::Lcm => not_implemented(args),
Function::Base => scalar_arguments(args),
Function::Decimal => scalar_arguments(args),
Function::Roman => scalar_arguments(args),
Function::Arabic => scalar_arguments(args),
Function::Combin => scalar_arguments(args),
Function::Combina => scalar_arguments(args),
Function::Sumsq => StaticResult::Scalar,
Function::N => scalar_arguments(args),
Function::Sheets => scalar_arguments(args),
Function::Cell => scalar_arguments(args),
Function::Info => scalar_arguments(args),
Function::Accrint => scalar_arguments(args),
Function::Accrintm => scalar_arguments(args),
}
}

View File

@@ -520,6 +520,7 @@ fn stringify(
let x = match **left {
BooleanKind(_)
| NumberKind(_)
| UnaryKind { .. }
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
@@ -535,7 +536,6 @@ fn stringify(
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| UnaryKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| OpSumKind { .. }
@@ -630,7 +630,6 @@ fn stringify(
| OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
@@ -643,7 +642,7 @@ fn stringify(
| ParseErrorKind { .. }
| EmptyArgKind => false,
OpSumKind { .. } | UnaryKind { .. } => true,
OpPowerKind { .. } | OpSumKind { .. } | UnaryKind { .. } => true,
};
if needs_parentheses {
format!(

View File

@@ -3,6 +3,7 @@ mod test_arrays;
mod test_general;
mod test_implicit_intersection;
mod test_issue_155;
mod test_issue_483;
mod test_move_formula;
mod test_ranges;
mod test_stringify;

View File

@@ -0,0 +1,27 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_483_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 2,
column: 2,
};
let t = parser.parse("-(A1^1.22)", &cell_reference);
assert!(matches!(t, Node::UnaryKind { .. }));
assert_eq!(to_string(&t, &cell_reference), "-(A1^1.22)");
let t = parser.parse("-A1^1.22", &cell_reference);
assert!(matches!(t, Node::OpPowerKind { .. }));
assert_eq!(to_string(&t, &cell_reference), "-A1^1.22");
}

View File

@@ -259,15 +259,23 @@ pub fn is_valid_identifier(name: &str) -> bool {
fn name_needs_quoting(name: &str) -> bool {
let chars = name.chars();
// it contains any of these characters: ()'$,;-+{} or space
for char in chars {
for (i, char) in chars.enumerate() {
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
return true;
}
// if it starts with a number
if i == 0 && char.is_ascii_digit() {
return true;
}
}
if parse_reference_a1(name).is_some() {
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
return true;
}
if parse_reference_r1c1(name).is_some() {
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
return true;
}
// TODO:
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
// integers
false
}
@@ -279,3 +287,32 @@ pub fn quote_name(name: &str) -> String {
};
name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_name() {
assert_eq!(quote_name("Sheet1"), "Sheet1");
assert_eq!(quote_name("Sheet 1"), "'Sheet 1'");
// escape and quote
assert_eq!(quote_name("Sheet1'"), "'Sheet1'''");
assert_eq!(quote_name("Data(2024)"), "'Data(2024)'");
assert_eq!(quote_name("Data$2024"), "'Data$2024'");
assert_eq!(quote_name("Data-2024"), "'Data-2024'");
assert_eq!(quote_name("Data+2024"), "'Data+2024'");
assert_eq!(quote_name("Data,2024"), "'Data,2024'");
assert_eq!(quote_name("Data;2024"), "'Data;2024'");
assert_eq!(quote_name("Data{2024}"), "'Data{2024}'");
assert_eq!(quote_name("2024"), "'2024'");
assert_eq!(quote_name("1Data"), "'1Data'");
assert_eq!(quote_name("A1"), "'A1'");
assert_eq!(quote_name("R1C1"), "'R1C1'");
assert_eq!(quote_name("MySheet"), "MySheet");
assert_eq!(quote_name("B1048576"), "'B1048576'");
assert_eq!(quote_name("B1048577"), "B1048577");
}
}

View File

@@ -154,16 +154,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Date(p) => {
let tokens = &p.tokens;
let mut text = "".to_string();
let date = match from_excel_date(value as i64) {
Ok(d) => d,
Err(e) => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(e),
}
}
};
let time_fract = value.fract();
let hours = (time_fract * 24.0).floor();
let minutes = ((time_fract * 24.0 - hours) * 60.0).floor();
let seconds = ((((time_fract * 24.0 - hours) * 60.0) - minutes) * 60.0).round();
let date = from_excel_date(value as i64).ok();
for token in tokens {
match token {
TextToken::Literal(c) => {
@@ -187,15 +182,44 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
}
TextToken::Digit(_) => {}
TextToken::Period => {}
TextToken::Day => {
let day = date.day() as usize;
text = format!("{text}{day}");
}
TextToken::Day => match date {
Some(date) => {
let day = date.day() as usize;
text = format!("{text}{day}");
}
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
},
TextToken::DayPadded => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let day = date.day() as usize;
text = format!("{text}{day:02}");
}
TextToken::DayNameShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let mut day = date.weekday().number_from_monday() as usize;
if day == 7 {
day = 0;
@@ -203,6 +227,16 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names_short[day]);
}
TextToken::DayName => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let mut day = date.weekday().number_from_monday() as usize;
if day == 7 {
day = 0;
@@ -210,32 +244,144 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names[day]);
}
TextToken::Month => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{text}{month}");
}
TextToken::MonthPadded => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{text}{month:02}");
}
TextToken::MonthNameShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months_short[month - 1]);
}
TextToken::MonthName => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months[month - 1]);
}
TextToken::MonthLetter => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize;
let months_letter = &locale.dates.months_letter[month - 1];
text = format!("{text}{months_letter}");
}
TextToken::YearShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
text = format!("{}{}", text, date.format("%y"));
}
TextToken::Year => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
text = format!("{}{}", text, date.year());
}
TextToken::Hour => {
let mut hour = hours as i32;
if p.use_ampm {
if hour == 0 {
hour = 12;
} else if hour > 12 {
hour -= 12;
}
}
text = format!("{text}{hour}");
}
TextToken::HourPadded => {
let mut hour = hours as i32;
if p.use_ampm {
if hour == 0 {
hour = 12;
} else if hour > 12 {
hour -= 12;
}
}
text = format!("{text}{hour:02}");
}
TextToken::Second => {
let second = seconds as i32;
text = format!("{text}{second}");
}
TextToken::SecondPadded => {
let second = seconds as i32;
text = format!("{text}{second:02}");
}
TextToken::AMPM => {
let ampm = if hours < 12.0 { "AM" } else { "PM" };
text = format!("{text}{ampm}");
}
TextToken::Minute => {
let minute = minutes as i32;
text = format!("{text}{minute}");
}
TextToken::MinutePadded => {
let minute = minutes as i32;
text = format!("{text}{minute:02}");
}
}
}
Formatted {
@@ -422,6 +568,13 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => {}
TextToken::YearShort => {}
TextToken::Year => {}
TextToken::Hour => {}
TextToken::HourPadded => {}
TextToken::Minute => {}
TextToken::MinutePadded => {}
TextToken::Second => {}
TextToken::SecondPadded => {}
TextToken::AMPM => {}
}
}
Formatted {

View File

@@ -26,19 +26,23 @@ pub enum Token {
Scientific, // E+
ScientificMinus, // E-
General, // General
// Dates
// Dates and time
Day, // d
DayPadded, // dd
DayNameShort, // ddd
DayName, // dddd+
Month, // m
MonthPadded, // mm
Month, // m (or minute)
MonthPadded, // mm (or minute padded)
MonthNameShort, // mmm
MonthName, // mmmm or mmmmmm+
MonthLetter, // mmmmm
YearShort, // y or yy
Year, // yyy+
// TODO: Hours Minutes and Seconds
Hour, // h
HourPadded, // hh
Second, // s
SecondPadded, // ss
AMPM, // AM/PM (or A/P)
ILLEGAL,
EOF,
}
@@ -361,8 +365,8 @@ impl Lexer {
self.read_next_char();
}
match m {
1 => Token::Month,
2 => Token::MonthPadded,
1 => Token::Month, // (or minute)
2 => Token::MonthPadded, // (or minute padded)
3 => Token::MonthNameShort,
4 => Token::MonthName,
5 => Token::MonthLetter,
@@ -381,6 +385,63 @@ impl Lexer {
Token::Year
}
}
'h' => {
let mut h = 1;
while let Some('h') = self.peek_char() {
h += 1;
self.read_next_char();
}
if h == 1 {
Token::Hour
} else if h == 2 {
Token::HourPadded
} else {
self.set_error("Unexpected character after 'h'");
Token::ILLEGAL
}
}
's' => {
let mut s = 1;
while let Some('s') = self.peek_char() {
s += 1;
self.read_next_char();
}
if s == 1 {
Token::Second
} else if s == 2 {
Token::SecondPadded
} else {
self.set_error("Unexpected character after 's'");
Token::ILLEGAL
}
}
'A' | 'a' => {
if let Some('M') | Some('m') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'A'");
return Token::ILLEGAL;
}
if let Some('/') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AM'");
return Token::ILLEGAL;
}
if let Some('P') | Some('p') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AM'");
return Token::ILLEGAL;
}
if let Some('M') | Some('m') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AMP'");
return Token::ILLEGAL;
}
Token::AMPM
}
'g' | 'G' => {
for c in "eneral".chars() {
let cc = self.read_next_char();

View File

@@ -27,6 +27,13 @@ pub enum TextToken {
MonthLetter,
YearShort,
Year,
Hour,
HourPadded,
Minute,
MinutePadded,
Second,
SecondPadded,
AMPM,
}
pub struct NumberPart {
pub color: Option<i32>,
@@ -45,6 +52,7 @@ pub struct NumberPart {
pub struct DatePart {
pub color: Option<i32>,
pub use_ampm: bool,
pub tokens: Vec<TextToken>,
}
@@ -101,6 +109,7 @@ impl Parser {
let mut digit_count = 0;
let mut precision = 0;
let mut is_date = false;
let mut use_ampm = false;
let mut is_number = false;
let mut found_decimal_dot = false;
let mut use_thousands = false;
@@ -116,6 +125,7 @@ impl Parser {
let mut number = 'i';
let mut index = 0;
let mut currency = None;
let mut is_time = false;
while token != Token::EOF && token != Token::Separator {
let next_token = self.lexer.next_token();
@@ -200,6 +210,9 @@ impl Parser {
index += 1;
}
Token::Literal(value) => {
if value == ':' {
is_time = true;
}
tokens.push(TextToken::Literal(value));
}
Token::Text(value) => {
@@ -236,12 +249,22 @@ impl Parser {
tokens.push(TextToken::MonthName);
}
Token::Month => {
is_date = true;
tokens.push(TextToken::Month);
if is_time {
// minute
tokens.push(TextToken::Minute);
} else {
is_date = true;
tokens.push(TextToken::Month);
}
}
Token::MonthPadded => {
is_date = true;
tokens.push(TextToken::MonthPadded);
if is_time {
// minute padded
tokens.push(TextToken::MinutePadded);
} else {
is_date = true;
tokens.push(TextToken::MonthPadded);
}
}
Token::MonthLetter => {
is_date = true;
@@ -255,6 +278,32 @@ impl Parser {
is_date = true;
tokens.push(TextToken::Year);
}
Token::Hour => {
is_date = true;
is_time = true;
tokens.push(TextToken::Hour);
}
Token::HourPadded => {
is_date = true;
is_time = true;
tokens.push(TextToken::HourPadded);
}
Token::Second => {
is_date = true;
is_time = true;
tokens.push(TextToken::Second);
}
Token::SecondPadded => {
is_date = true;
is_time = true;
tokens.push(TextToken::SecondPadded);
}
Token::AMPM => {
is_date = true;
use_ampm = true;
tokens.push(TextToken::AMPM);
}
Token::Scientific => {
if !is_scientific {
index = 0;
@@ -282,7 +331,11 @@ impl Parser {
if is_number {
return ParsePart::Error(ErrorPart {});
}
ParsePart::Date(DatePart { color, tokens })
ParsePart::Date(DatePart {
color,
use_ampm,
tokens,
})
} else {
ParsePart::Number(NumberPart {
color,

View File

@@ -1,2 +1,3 @@
mod test_general;
mod test_parse_formatted_number;
mod test_time;

View File

@@ -0,0 +1,32 @@
#![allow(clippy::unwrap_used)]
use crate::{
formatter::format::format_number,
locale::{get_locale, Locale},
};
fn get_default_locale() -> &'static Locale {
get_locale("en").unwrap()
}
#[test]
fn simple_test() {
let locale = get_default_locale();
let format = "h:mm AM/PM";
let value = 16.001_423_611_111_11; // =1/86400 => 12:02 AM
let formatted = format_number(value, format, locale);
assert_eq!(formatted.text, "12:02 AM");
}
#[test]
fn padded_vs_unpadded() {
let locale = get_default_locale();
let padded_format = "hh:mm:ss AM/PM";
let unpadded_format = "h:m:s AM/PM";
let value = 0.25351851851851853; // => 6:05:04 AM (21904/(24*60*60)) where 21904 = 6 * 3600 + 5*60 + 4
let formatted = format_number(value, padded_format, locale);
assert_eq!(formatted.text, "06:05:04 AM");
let formatted = format_number(value, unpadded_format, locale);
assert_eq!(formatted.text, "6:5:4 AM");
}

View File

@@ -1830,4 +1830,14 @@ impl Model {
CalcResult::Number(rate * (cost - result))
}
// ACCRINT(issue, first_interest, settlement, rate, par, frequency, [basis], [calc_method])
pub(crate) fn fn_accrint(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
todo!()
}
// ACCRINTM(issue, settlement, rate, par, [basis])
pub(crate) fn fn_accrintm(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
todo!()
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
expressions::{parser::Node, token::Error, types::CellReferenceIndex, utils::number_to_column},
model::{Model, ParsedDefinedName},
};
@@ -320,4 +320,150 @@ impl Model {
message: "Invalid name".to_string(),
}
}
pub(crate) fn fn_n(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(n) => n,
CalcResult::String(_) => 0.0,
CalcResult::Boolean(f) => {
if f {
1.0
} else {
0.0
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
CalcResult::Number(value)
}
pub(crate) fn fn_sheets(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count > 1 {
return CalcResult::new_args_number_error(cell);
}
if arg_count == 1 {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Sheets function with an argument is not implemented".to_string(),
};
}
let sheet_count = self.workbook.worksheets.len() as f64;
CalcResult::Number(sheet_count)
}
pub(crate) fn fn_cell(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count == 0 || arg_count > 2 {
return CalcResult::new_args_number_error(cell);
}
let reference = if arg_count == 2 {
match self.evaluate_node_with_reference(&args[1], cell) {
CalcResult::Range { left, right: _ } => {
// we just take the left cell of the range
left
}
_ => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument must be a reference".to_string(),
}
}
}
} else {
CellReferenceIndex {
sheet: cell.sheet,
row: cell.row,
column: cell.column,
}
};
let info_type = match self.get_string(&args[0], cell) {
Ok(s) => s.to_uppercase(),
Err(e) => return e,
};
match info_type.as_str() {
"ADDRESS" => {
if reference.sheet != cell.sheet {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "References to other sheets not implemented".to_string(),
};
}
let column = match number_to_column(reference.column) {
Some(c) => c,
None => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid column".to_string(),
}
}
};
let address = format!("${}${}", column, reference.row);
CalcResult::String(address)
}
"COL" => CalcResult::Number(reference.column as f64),
"COLOR" | "FILENAME" | "FORMAT" | "PARENTHESES" | "PREFIX" | "PROTECT" | "WIDTH" => {
CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "info_type not implemented".to_string(),
}
}
"CONTENTS" => self.evaluate_cell(reference),
"ROW" => CalcResult::Number(reference.row as f64),
"TYPE" => {
let cell_type = match self.evaluate_cell(reference) {
CalcResult::EmptyCell => "b",
CalcResult::String(_) => "l",
CalcResult::Number(_) => "v",
CalcResult::Boolean(_) => "v",
CalcResult::Error { .. } => "v",
CalcResult::Range { .. } => "v",
CalcResult::EmptyArg => "v",
CalcResult::Array(_) => "v",
};
CalcResult::String(cell_type.to_string())
}
_ => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid info_type".to_string(),
},
}
}
pub(crate) fn fn_info(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() || args.len() > 2 {
return CalcResult::new_args_number_error(cell);
}
CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Info function not implemented".to_string(),
}
}
}

View File

@@ -0,0 +1,200 @@
/// Parse Roman (classic or Excel variants) → number
pub fn from_roman(s: &str) -> Result<u32, String> {
if s.is_empty() {
return Err("empty numeral".into());
}
fn val(c: char) -> Option<u32> {
Some(match c {
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
_ => return None,
})
}
// Accept the union of subtractive pairs used by the tables above (Excel-compatible).
fn allowed_subtractive(a: char, b: char) -> bool {
matches!(
(a, b),
// classic:
('I','V')|('I','X')|('X','L')|('X','C')|('C','D')|('C','M')
// Excel forms:
|('V','L')|('L','D')|('L','M') // VL, LD, LM
|('X','D')|('X','M') // XD, XM
|('V','M') // VM
|('I','L')|('I','C')|('I','D')|('I','M') // IL, IC, ID, IM
|('V','D')|('V','C') // VD, VC
)
}
let chars: Vec<char> = s.chars().map(|c| c.to_ascii_uppercase()).collect();
let mut total = 0u32;
let mut i = 0usize;
// Repetition rules similar to classic Romans:
// V, L, D cannot repeat; I, X, C, M max 3 in a row.
let mut last_char: Option<char> = None;
let mut run_len = 0usize;
while i < chars.len() {
let c = chars[i];
let v = val(c).ok_or_else(|| format!("invalid character '{c}'"))?;
if Some(c) == last_char {
run_len += 1;
match c {
'V' | 'L' | 'D' => return Err(format!("invalid repetition of '{c}'")),
_ if run_len >= 3 => return Err(format!("invalid repetition of '{c}'")),
_ => {}
}
} else {
last_char = Some(c);
run_len = 0;
}
if i + 1 < chars.len() {
let c2 = chars[i + 1];
let v2 = val(c2).ok_or_else(|| format!("invalid character '{c2}'"))?;
if v < v2 {
if !allowed_subtractive(c, c2) {
return Err(format!("invalid subtractive pair '{c}{c2}'"));
}
// Disallow stacked subtractives like IIV, XXL:
if run_len > 0 {
return Err(format!("malformed numeral near position {i}"));
}
total += v2 - v;
i += 2;
last_char = None;
run_len = 0;
continue;
}
}
total += v;
i += 1;
}
Ok(total)
}
/// Classic Roman (strict) encoder used as a base for all forms.
fn to_roman(mut n: u32) -> Result<String, String> {
if !(1..=3999).contains(&n) {
return Err("value out of range (must be 1..=3999)".into());
}
const MAP: &[(u32, &str)] = &[
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut out = String::with_capacity(15);
for &(val, sym) in MAP {
while n >= val {
out.push_str(sym);
n -= val;
}
if n == 0 {
break;
}
}
Ok(out)
}
/// Excel/Google Sheets compatible ROMAN(number, [form]) encoder.
/// `form`: 0..=4 (0=Classic, 4=Simplified).
pub fn to_roman_with_form(n: u32, form: i32) -> Result<String, String> {
let mut s = to_roman(n)?;
if form == 0 {
return Ok(s);
}
if !(0..=4).contains(&form) {
return Err("form must be between 0 and 4".into());
}
// Base rules (apply for all f >= 1)
let base_rules: &[(&str, &str)] = &[
// C(D|M)XC -> L$1XL
("CDXC", "LDXL"),
("CMXC", "LMXL"),
// C(D|M)L -> L$1
("CDL", "LD"),
("CML", "LM"),
// X(L|C)IX -> V$1IV
("XLIX", "VLIV"),
("XCIX", "VCIV"),
// X(L|C)V -> V$1
("XLV", "VL"),
("XCV", "VC"),
];
// Level 2 extra rules
let lvl2_rules: &[(&str, &str)] = &[
// V(L|C)IV -> I$1
("VLIV", "IL"),
("VCIV", "IC"),
// L(D|M)XL -> X$1
("LDXL", "XD"),
("LMXL", "XM"),
// L(D|M)VL -> X$1V
("LDVL", "XDV"),
("LMVL", "XMV"),
// L(D|M)IL -> X$1IX
("LDIL", "XDIX"),
("LMIL", "XMIX"),
];
// Level 3 extra rules
let lvl3_rules: &[(&str, &str)] = &[
// X(D|M)V -> V$1
("XDV", "VD"),
("XMV", "VM"),
// X(D|M)IX -> V$1IV
("XDIX", "VDIV"),
("XMIX", "VMIV"),
];
// Level 4 extra rules
let lvl4_rules: &[(&str, &str)] = &[
// V(D|M)IV -> I$1
("VDIV", "ID"),
("VMIV", "IM"),
];
// Helper to apply a batch of (from -> to) globally, in order.
fn apply_rules(mut t: String, rules: &[(&str, &str)]) -> String {
for (from, to) in rules {
if t.contains(from) {
t = t.replace(from, to);
}
}
t
}
s = apply_rules(s, base_rules);
if form >= 2 {
s = apply_rules(s, lvl2_rules);
}
if form >= 3 {
s = apply_rules(s, lvl3_rules);
}
if form >= 4 {
s = apply_rules(s, lvl4_rules);
}
Ok(s)
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ mod information;
mod logical;
mod lookup_and_reference;
mod macros;
mod math_util;
mod mathematical;
mod statistical;
mod subtotal;
@@ -76,6 +77,43 @@ pub enum Function {
Sumifs,
Tan,
Tanh,
Acot,
Acoth,
Cot,
Coth,
Csc,
Csch,
Sec,
Sech,
Exp,
Fact,
Factdouble,
Sign,
Radians,
Degrees,
Int,
Even,
Odd,
Ceiling,
CeilingMath,
CeilingPrecise,
Floor,
FloorMath,
FloorPrecise,
IsoCeiling,
Mod,
Quotient,
Mround,
Trunc,
Gcd,
Lcm,
Base,
Decimal,
Roman,
Arabic,
Combin,
Combina,
Sumsq,
// Information
ErrorType,
@@ -96,6 +134,11 @@ pub enum Function {
Sheet,
Type,
Sheets,
N,
Cell,
Info,
// Lookup and reference
Hlookup,
Index,
@@ -174,6 +217,8 @@ pub enum Function {
Isoweeknum,
// Financial
Accrint,
Accrintm,
Cumipmt,
Cumprinc,
Db,
@@ -270,7 +315,7 @@ pub enum Function {
}
impl Function {
pub fn into_iter() -> IntoIter<Function, 215> {
pub fn into_iter() -> IntoIter<Function, 258> {
[
Function::And,
Function::False,
@@ -303,12 +348,44 @@ impl Function {
Function::Sqrt,
Function::Sqrtpi,
Function::Atan2,
Function::Acot,
Function::Acoth,
Function::Cot,
Function::Coth,
Function::Csc,
Function::Csch,
Function::Sec,
Function::Sech,
Function::Power,
Function::Exp,
Function::Fact,
Function::Factdouble,
Function::Sign,
Function::Int,
Function::Even,
Function::Odd,
Function::Ceiling,
Function::CeilingMath,
Function::CeilingPrecise,
Function::Floor,
Function::FloorMath,
Function::FloorPrecise,
Function::IsoCeiling,
Function::Mod,
Function::Quotient,
Function::Mround,
Function::Trunc,
Function::Gcd,
Function::Lcm,
Function::Base,
Function::Decimal,
Function::Max,
Function::Min,
Function::Product,
Function::Rand,
Function::Randbetween,
Function::Radians,
Function::Degrees,
Function::Round,
Function::Rounddown,
Function::Roundup,
@@ -487,6 +564,17 @@ impl Function {
Function::Delta,
Function::Gestep,
Function::Subtotal,
Function::Roman,
Function::Arabic,
Function::Combin,
Function::Combina,
Function::Sumsq,
Function::N,
Function::Cell,
Function::Info,
Function::Sheets,
Function::Accrint,
Function::Accrintm,
]
.into_iter()
}
@@ -529,6 +617,18 @@ impl Function {
Function::Sheet => "_xlfn.SHEET".to_string(),
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
Function::Ceiling => "_xlfn.CEILING".to_string(),
Function::CeilingMath => "_xlfn.CEILING.MATH".to_string(),
Function::CeilingPrecise => "_xlfn.CEILING.PRECISE".to_string(),
Function::FloorMath => "_xlfn.FLOOR.MATH".to_string(),
Function::FloorPrecise => "_xlfn.FLOOR.PRECISE".to_string(),
Function::IsoCeiling => "_xlfn.ISO.CEILING".to_string(),
Function::Base => "_xlfn.BASE".to_string(),
Function::Decimal => "_xlfn.DECIMAL".to_string(),
Function::Arabic => "_xlfn.ARABIC".to_string(),
Function::Combina => "_xlfn.COMBINA".to_string(),
Function::Sheets => "_xlfn.SHEETS".to_string(),
_ => self.to_string(),
}
}
@@ -551,34 +651,61 @@ impl Function {
"SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch),
"TRUE" => Some(Function::True),
"XOR" | "_XLFN.XOR" => Some(Function::Xor),
"SIN" => Some(Function::Sin),
"COS" => Some(Function::Cos),
"TAN" => Some(Function::Tan),
"ASIN" => Some(Function::Asin),
"ACOS" => Some(Function::Acos),
"ATAN" => Some(Function::Atan),
"SINH" => Some(Function::Sinh),
"COSH" => Some(Function::Cosh),
"TANH" => Some(Function::Tanh),
"ASINH" => Some(Function::Asinh),
"ACOSH" => Some(Function::Acosh),
"ATANH" => Some(Function::Atanh),
"ACOT" => Some(Function::Acot),
"COTH" => Some(Function::Coth),
"COT" => Some(Function::Cot),
"CSC" => Some(Function::Csc),
"CSCH" => Some(Function::Csch),
"SEC" => Some(Function::Sec),
"SECH" => Some(Function::Sech),
"ACOTH" => Some(Function::Acoth),
"FACT" => Some(Function::Fact),
"FACTDOUBLE" => Some(Function::Factdouble),
"EXP" => Some(Function::Exp),
"SIGN" => Some(Function::Sign),
"RADIANS" => Some(Function::Radians),
"DEGREES" => Some(Function::Degrees),
"INT" => Some(Function::Int),
"EVEN" => Some(Function::Even),
"ODD" => Some(Function::Odd),
"CEILING" | "_XLFN.CEILING" => Some(Function::Ceiling),
"CEILING.MATH" | "_XLFN.CEILING.MATH" => Some(Function::CeilingMath),
"CEILING.PRECISE" | "_XLFN.CEILING.PRECISE" => Some(Function::CeilingPrecise),
"FLOOR" => Some(Function::Floor),
"FLOOR.MATH" | "_XLFN.FLOOR.MATH" => Some(Function::FloorMath),
"FLOOR.PRECISE" | "_XLFN.FLOOR.PRECISE" => Some(Function::FloorPrecise),
"ISO.CEILING" | "_XLFN.ISO.CEILING" => Some(Function::IsoCeiling),
"MOD" => Some(Function::Mod),
"QUOTIENT" => Some(Function::Quotient),
"MROUND" => Some(Function::Mround),
"TRUNC" => Some(Function::Trunc),
"GCD" => Some(Function::Gcd),
"LCM" => Some(Function::Lcm),
"BASE" | "_XLFN.BASE" => Some(Function::Base),
"DECIMAL" | "_XLFN.DECIMAL" => Some(Function::Decimal),
"ROMAN" => Some(Function::Roman),
"ARABIC" | "_XLFN.ARABIC" => Some(Function::Arabic),
"PI" => Some(Function::Pi),
"ABS" => Some(Function::Abs),
"SQRT" => Some(Function::Sqrt),
"SQRTPI" => Some(Function::Sqrtpi),
"POWER" => Some(Function::Power),
"ATAN2" => Some(Function::Atan2),
"LN" => Some(Function::Ln),
"LOG" => Some(Function::Log),
"LOG10" => Some(Function::Log10),
"MAX" => Some(Function::Max),
"MIN" => Some(Function::Min),
"PRODUCT" => Some(Function::Product),
@@ -590,6 +717,9 @@ impl Function {
"SUM" => Some(Function::Sum),
"SUMIF" => Some(Function::Sumif),
"SUMIFS" => Some(Function::Sumifs),
"COMBIN" => Some(Function::Combin),
"COMBINA" | "_XLFN.COMBINA" => Some(Function::Combina),
"SUMSQ" => Some(Function::Sumsq),
// Lookup and Reference
"CHOOSE" => Some(Function::Choose),
@@ -777,6 +907,14 @@ impl Function {
"GESTEP" => Some(Function::Gestep),
"SUBTOTAL" => Some(Function::Subtotal),
"N" => Some(Function::N),
"CELL" => Some(Function::Cell),
"INFO" => Some(Function::Info),
"SHEETS" | "_XLFN.SHEETS" => Some(Function::Sheets),
"ACCRINT" => Some(Function::Accrint),
"ACCRINTM" => Some(Function::Accrintm),
_ => None,
}
}
@@ -811,6 +949,14 @@ impl fmt::Display for Function {
Function::Asinh => write!(f, "ASINH"),
Function::Acosh => write!(f, "ACOSH"),
Function::Atanh => write!(f, "ATANH"),
Function::Acot => write!(f, "ACOT"),
Function::Acoth => write!(f, "ACOTH"),
Function::Cot => write!(f, "COT"),
Function::Coth => write!(f, "COTH"),
Function::Csc => write!(f, "CSC"),
Function::Csch => write!(f, "CSCH"),
Function::Sec => write!(f, "SEC"),
Function::Sech => write!(f, "SECH"),
Function::Abs => write!(f, "ABS"),
Function::Pi => write!(f, "PI"),
Function::Sqrt => write!(f, "SQRT"),
@@ -875,7 +1021,6 @@ impl fmt::Display for Function {
Function::Isformula => write!(f, "ISFORMULA"),
Function::Type => write!(f, "TYPE"),
Function::Sheet => write!(f, "SHEET"),
Function::Average => write!(f, "AVERAGE"),
Function::Averagea => write!(f, "AVERAGEA"),
Function::Averageif => write!(f, "AVERAGEIF"),
@@ -1000,8 +1145,44 @@ impl fmt::Display for Function {
Function::Convert => write!(f, "CONVERT"),
Function::Delta => write!(f, "DELTA"),
Function::Gestep => write!(f, "GESTEP"),
Function::Subtotal => write!(f, "SUBTOTAL"),
Function::Exp => write!(f, "EXP"),
Function::Fact => write!(f, "FACT"),
Function::Factdouble => write!(f, "FACTDOUBLE"),
Function::Sign => write!(f, "SIGN"),
Function::Radians => write!(f, "RADIANS"),
Function::Degrees => write!(f, "DEGREES"),
Function::Int => write!(f, "INT"),
Function::Even => write!(f, "EVEN"),
Function::Odd => write!(f, "ODD"),
Function::Ceiling => write!(f, "CEILING"),
Function::CeilingMath => write!(f, "CEILING.MATH"),
Function::CeilingPrecise => write!(f, "CEILING.PRECISE"),
Function::Floor => write!(f, "FLOOR"),
Function::FloorMath => write!(f, "FLOOR.MATH"),
Function::FloorPrecise => write!(f, "FLOOR.PRECISE"),
Function::IsoCeiling => write!(f, "ISO.CEILING"),
Function::Mod => write!(f, "MOD"),
Function::Quotient => write!(f, "QUOTIENT"),
Function::Mround => write!(f, "MROUND"),
Function::Trunc => write!(f, "TRUNC"),
Function::Gcd => write!(f, "GCD"),
Function::Lcm => write!(f, "LCM"),
Function::Base => write!(f, "BASE"),
Function::Decimal => write!(f, "DECIMAL"),
Function::Roman => write!(f, "ROMAN"),
Function::Arabic => write!(f, "ARABIC"),
Function::Combin => write!(f, "COMBIN"),
Function::Combina => write!(f, "COMBINA"),
Function::Sumsq => write!(f, "SUMSQ"),
Function::N => write!(f, "N"),
Function::Cell => write!(f, "CELL"),
Function::Info => write!(f, "INFO"),
Function::Sheets => write!(f, "SHEETS"),
Function::Accrint => write!(f, "ACCRINT"),
Function::Accrintm => write!(f, "ACCRINTM"),
}
}
}
@@ -1030,7 +1211,6 @@ impl Model {
cell: CellReferenceIndex,
) -> CalcResult {
match kind {
// Logical
Function::And => self.fn_and(args, cell),
Function::False => self.fn_false(args, cell),
Function::If => self.fn_if(args, cell),
@@ -1042,34 +1222,27 @@ impl Model {
Function::Switch => self.fn_switch(args, cell),
Function::True => self.fn_true(args, cell),
Function::Xor => self.fn_xor(args, cell),
// 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::Cos => self.fn_cos(args, cell),
Function::Tan => self.fn_tan(args, cell),
Function::Asin => self.fn_asin(args, cell),
Function::Acos => self.fn_acos(args, cell),
Function::Atan => self.fn_atan(args, cell),
Function::Sinh => self.fn_sinh(args, cell),
Function::Cosh => self.fn_cosh(args, cell),
Function::Tanh => self.fn_tanh(args, cell),
Function::Asinh => self.fn_asinh(args, cell),
Function::Acosh => self.fn_acosh(args, cell),
Function::Atanh => self.fn_atanh(args, cell),
Function::Pi => self.fn_pi(args, cell),
Function::Abs => self.fn_abs(args, cell),
Function::Sqrt => self.fn_sqrt(args, cell),
Function::Sqrtpi => self.fn_sqrtpi(args, cell),
Function::Atan2 => self.fn_atan2(args, cell),
Function::Power => self.fn_power(args, cell),
Function::Max => self.fn_max(args, cell),
Function::Min => self.fn_min(args, cell),
Function::Product => self.fn_product(args, cell),
@@ -1081,8 +1254,6 @@ impl Model {
Function::Sum => self.fn_sum(args, cell),
Function::Sumif => self.fn_sumif(args, cell),
Function::Sumifs => self.fn_sumifs(args, cell),
// Lookup and Reference
Function::Choose => self.fn_choose(args, cell),
Function::Column => self.fn_column(args, cell),
Function::Columns => self.fn_columns(args, cell),
@@ -1096,7 +1267,6 @@ impl Model {
Function::Rows => self.fn_rows(args, cell),
Function::Vlookup => self.fn_vlookup(args, cell),
Function::Xlookup => self.fn_xlookup(args, cell),
// Text
Function::Concatenate => self.fn_concatenate(args, cell),
Function::Exact => self.fn_exact(args, cell),
Function::Value => self.fn_value(args, cell),
@@ -1114,7 +1284,6 @@ impl Model {
Function::Trim => self.fn_trim(args, cell),
Function::Unicode => self.fn_unicode(args, cell),
Function::Upper => self.fn_upper(args, cell),
// Information
Function::Isnumber => self.fn_isnumber(args, cell),
Function::Isnontext => self.fn_isnontext(args, cell),
Function::Istext => self.fn_istext(args, cell),
@@ -1132,7 +1301,6 @@ impl Model {
Function::Isformula => self.fn_isformula(args, cell),
Function::Type => self.fn_type(args, cell),
Function::Sheet => self.fn_sheet(args, cell),
// Statistical
Function::Average => self.fn_average(args, cell),
Function::Averagea => self.fn_averagea(args, cell),
Function::Averageif => self.fn_averageif(args, cell),
@@ -1145,7 +1313,6 @@ impl Model {
Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell),
Function::Geomean => self.fn_geomean(args, cell),
// Date and Time
Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell),
Function::Eomonth => self.fn_eomonth(args, cell),
@@ -1171,7 +1338,6 @@ impl Model {
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
Function::Yearfrac => self.fn_yearfrac(args, cell),
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
// Financial
Function::Pmt => self.fn_pmt(args, cell),
Function::Pv => self.fn_pv(args, cell),
Function::Rate => self.fn_rate(args, cell),
@@ -1205,7 +1371,6 @@ impl Model {
Function::Db => self.fn_db(args, cell),
Function::Cumprinc => self.fn_cumprinc(args, cell),
Function::Cumipmt => self.fn_cumipmt(args, cell),
// Engineering
Function::Besseli => self.fn_besseli(args, cell),
Function::Besselj => self.fn_besselj(args, cell),
Function::Besselk => self.fn_besselk(args, cell),
@@ -1260,8 +1425,50 @@ impl Model {
Function::Convert => self.fn_convert(args, cell),
Function::Delta => self.fn_delta(args, cell),
Function::Gestep => self.fn_gestep(args, cell),
Function::Subtotal => self.fn_subtotal(args, cell),
Function::Acot => self.fn_acot(args, cell),
Function::Acoth => self.fn_acoth(args, cell),
Function::Cot => self.fn_cot(args, cell),
Function::Coth => self.fn_coth(args, cell),
Function::Csc => self.fn_csc(args, cell),
Function::Csch => self.fn_csch(args, cell),
Function::Sec => self.fn_sec(args, cell),
Function::Sech => self.fn_sech(args, cell),
Function::Exp => self.fn_exp(args, cell),
Function::Fact => self.fn_fact(args, cell),
Function::Factdouble => self.fn_factdouble(args, cell),
Function::Sign => self.fn_sign(args, cell),
Function::Radians => self.fn_radians(args, cell),
Function::Degrees => self.fn_degrees(args, cell),
Function::Int => self.fn_int(args, cell),
Function::Even => self.fn_even(args, cell),
Function::Odd => self.fn_odd(args, cell),
Function::Ceiling => self.fn_ceiling(args, cell),
Function::CeilingMath => self.fn_ceiling_math(args, cell),
Function::CeilingPrecise => self.fn_ceiling_precise(args, cell),
Function::Floor => self.fn_floor(args, cell),
Function::FloorMath => self.fn_floor_math(args, cell),
Function::FloorPrecise => self.fn_floor_precise(args, cell),
Function::IsoCeiling => self.fn_iso_ceiling(args, cell),
Function::Mod => self.fn_mod(args, cell),
Function::Quotient => self.fn_quotient(args, cell),
Function::Mround => self.fn_mround(args, cell),
Function::Trunc => self.fn_trunc(args, cell),
Function::Gcd => self.fn_gcd(args, cell),
Function::Lcm => self.fn_lcm(args, cell),
Function::Base => self.fn_base(args, cell),
Function::Decimal => self.fn_decimal(args, cell),
Function::Roman => self.fn_roman(args, cell),
Function::Arabic => self.fn_arabic(args, cell),
Function::Combin => self.fn_combin(args, cell),
Function::Combina => self.fn_combina(args, cell),
Function::Sumsq => self.fn_sumsq(args, cell),
Function::N => self.fn_n(args, cell),
Function::Cell => self.fn_cell(args, cell),
Function::Info => self.fn_info(args, cell),
Function::Sheets => self.fn_sheets(args, cell),
Function::Accrint => self.fn_accrint(args, cell),
Function::Accrintm => self.fn_accrintm(args, cell),
}
}
}

View File

@@ -9,6 +9,7 @@ mod test_currency;
mod test_date_and_time;
mod test_datedif_leap_month_end;
mod test_days360_month_end;
mod test_degrees_radians;
mod test_error_propagation;
mod test_fn_average;
mod test_fn_averageifs;
@@ -60,6 +61,7 @@ mod test_number_format;
mod test_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_accrint;
mod test_fn_fv;
mod test_fn_round;
mod test_fn_type;
@@ -68,12 +70,14 @@ mod test_geomean;
mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155;
mod test_issue_483;
mod test_ln;
mod test_log;
mod test_log10;
mod test_networkdays;
mod test_percentage;
mod test_set_functions_error_handling;
mod test_sheet_names;
mod test_today;
mod test_types;
mod user_model;

View File

@@ -0,0 +1,22 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_degrees_radians_arguments() {
let mut model = new_empty_model();
model._set("A1", "=DEGREES()");
model._set("A2", "=RADIANS()");
model._set("A3", "=RADIANS(180)");
model._set("A4", "=RADIANS(180, 2)");
model._set("A5", "=DEGREES(RADIANS(180))");
model._set("A6", "=DEGREES(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"3.141592654");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"180");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -0,0 +1,23 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=EVEN(2)");
model._set("A2", "=ODD(2)");
model._set("A3", "=EVEN()");
model._set("A4", "=ODD()");
model._set("A5", "=EVEN(1, 2)");
model._set("A6", "=ODD(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"3");
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!");
}

View File

@@ -0,0 +1,23 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_average_accrint_simple_cases() {
let mut model = new_empty_model();
// ACCRINT(issue, first_interest, settlement, rate, par, frequency, [basis], [calc_method])
model._set("A1", "=ACCRINT(39508, 39691, 39569, 0.1, 1000, 2, 0)");
model._set(
"A2",
"=ACCRINT(DATE(2008, 3, 5), 39691, 39569, 0.1, 1000, 2, 0, FALSE)",
);
model._set(
"A3",
"=ACCRINT(DATE(2008, 4, 5), 39691, 39569, 0.1, 1000, 2, 0, TRUE)",
);
model.evaluate();
assert_eq!(model._get_text("A1"), *"16.666666667");
assert_eq!(model._get_text("A2"), *"15.555555556");
assert_eq!(model._get_text("A3"), *"7.222222222");
}

View File

@@ -0,0 +1,24 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn datevalue_timevalue_arguments() {
let mut model = new_empty_model();
model._set("A1", "=DATEVALUE()");
model._set("A2", "=TIMEVALUE()");
model._set("A3", "=DATEVALUE("2000-01-01")")
model._set("A4", "=TIMEVALUE("12:00:00")")
model._set("A5", "=DATEVALUE(1,2)");
model._set("A6", "=TIMEVALUE(1,2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"36526");
assert_eq!(model._get_text("A4"), *"0.5");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -0,0 +1,13 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn issue_155() {
let mut model = new_empty_model();
model._set("A1", "123");
model._set("D2", "=-(A1^1.22)");
model.evaluate();
assert_eq!(model._get_formula("D2"), "=-(A1^1.22)".to_string());
}

View File

@@ -0,0 +1,16 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn sheet_number_name() {
let mut model = new_empty_model();
model.new_sheet();
model._set("A1", "7");
model._set("A2", "=Sheet2!C3");
model.evaluate();
model.rename_sheet("Sheet2", "2024").unwrap();
model.evaluate();
assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1", "2024"]);
assert_eq!(model._get_text("A2"), "0");
}

View File

@@ -8,8 +8,8 @@ crate-type = ["cdylib"]
[dependencies]
# 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-derive = "3.2"
napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] }
napi-derive = "2.12.2"
ironcalc = { path = "../../xlsx", version = "0.6.0" }
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,5 +1,8 @@
/* auto-generated by NAPI-RS */
/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export declare class Model {
constructor(name: string, locale: string, timezone: string)
static fromXlsx(filePath: string, locale: string, tz: string): Model
@@ -36,10 +39,8 @@ export declare class Model {
newDefinedName(name: string, scope: number | undefined | null, formula: string): void
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
deleteDefinedName(name: string, scope?: number | undefined | null): void
moveColumn(sheet: number, column: number, delta: number): void
moveRow(sheet: number, row: number, delta: number): void
testPanic(): void
}
export declare class UserModel {
constructor(name: string, locale: string, timezone: string)
static fromBytes(bytes: Uint8Array): UserModel
@@ -58,13 +59,12 @@ export declare class UserModel {
setSheetColor(sheet: number, color: string): void
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
rangeClearFormatting(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
insertRows(sheet: number, row: number, rowCount: number): void
insertColumns(sheet: number, column: number, columnCount: number): void
deleteRows(sheet: number, row: number, rowCount: number): void
deleteColumns(sheet: number, column: number, columnCount: number): void
setRowsHeight(sheet: number, rowStart: number, rowEnd: number, height: number): void
setColumnsWidth(sheet: number, columnStart: number, columnEnd: number, width: number): void
setRowHeight(sheet: number, row: number, height: number): void
setColumnWidth(sheet: number, column: number, width: number): void
getRowHeight(sheet: number, row: number): number
getColumnWidth(sheet: number, column: number): number
setUserInput(sheet: number, row: number, column: number, input: string): void
@@ -112,6 +112,4 @@ export declare class UserModel {
newDefinedName(name: string, scope: number | undefined | null, formula: string): void
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
deleteDefinedName(name: string, scope?: number | undefined | null): void
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 */
// @ts-nocheck
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const { createRequire } = require('node:module')
require = createRequire(__filename)
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
const { readFileSync } = require('node:fs')
let nativeBinding = null
const loadErrors = []
let localFileExisted = false
let loadError = null
const isMusl = () => {
let musl = false
if (process.platform === 'linux') {
musl = isMuslFromFilesystem()
if (musl === null) {
musl = isMuslFromReport()
}
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)) {
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
}
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 {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
nativeBinding = requireNative()
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
let wasiBinding = null
let wasiBindingError = null
try {
wasiBinding = require('./nodejs.wasi.cjs')
nativeBinding = wasiBinding
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
wasiBindingError = err
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'nodejs.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.android-arm64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'nodejs.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.android-arm-eabi.node')
} else {
nativeBinding = require('@ironcalc/nodejs-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
}
if (!nativeBinding) {
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-x64-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-ia32-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.win32-arm64-msvc.node')
} else {
nativeBinding = require('@ironcalc/nodejs-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'nodejs.darwin-universal.node'))
try {
wasiBinding = require('@ironcalc/nodejs-wasm32-wasi')
nativeBinding = wasiBinding
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
wasiBindingError.cause = err
loadErrors.push(err)
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-universal.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'nodejs.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-x64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nodejs.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.darwin-arm64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
}
if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) {
const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error')
error.cause = wasiBindingError
throw error
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, 'nodejs.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.freebsd-x64.node')
} else {
nativeBinding = require('@ironcalc/nodejs-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-x64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-x64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm-musleabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm-musleabihf.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm-musleabihf')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-riscv64-musl.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-riscv64-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'nodejs.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nodejs.linux-s390x-gnu.node')
} else {
nativeBinding = require('@ironcalc/nodejs-linux-s390x-gnu')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadErrors.length > 0) {
throw new Error(
`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
}),
},
)
if (loadError) {
throw loadError
}
throw new Error(`Failed to load native binding`)
}
module.exports = nativeBinding
module.exports.Model = nativeBinding.Model
module.exports.UserModel = nativeBinding.UserModel
const { Model, UserModel } = nativeBinding
module.exports.Model = Model
module.exports.UserModel = UserModel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
#![deny(clippy::all)]
use napi::{self, bindgen_prelude::*, Result, Unknown};
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use serde::Serialize;
use ironcalc::{
@@ -127,7 +127,7 @@ impl Model {
sheet: u32,
row: i32,
column: i32,
style: Unknown,
style: JsUnknown,
) -> Result<()> {
let style: Style = env
.from_js_value(style)
@@ -140,12 +140,12 @@ impl Model {
#[napi(js_name = "getCellStyle")]
pub fn get_cell_style(
&'_ self,
&mut self,
env: Env,
sheet: u32,
row: i32,
column: i32,
) -> Result<Unknown<'_>> {
) -> Result<JsUnknown> {
let style = self
.model
.get_style_for_cell(sheet, row, column)
@@ -246,11 +246,11 @@ impl Model {
.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
#[napi(js_name = "getWorksheetsProperties")]
#[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&'_ self, env: Env) -> Unknown<'_> {
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
env
.to_js_value(&self.model.get_worksheets_properties())
.unwrap()
@@ -288,7 +288,7 @@ impl Model {
}
#[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
.model
.workbook

View File

@@ -2,7 +2,7 @@
use serde::Serialize;
use napi::{self, bindgen_prelude::*, Result, Unknown};
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use ironcalc::base::{
expressions::types::Area,
@@ -305,7 +305,7 @@ impl UserModel {
pub fn update_range_style(
&mut self,
env: Env,
range: Unknown,
range: JsUnknown,
style_path: String,
value: String,
) -> Result<()> {
@@ -320,12 +320,12 @@ impl UserModel {
#[napi(js_name = "getCellStyle")]
pub fn get_cell_style(
&'_ mut self,
&mut self,
env: Env,
sheet: u32,
row: i32,
column: i32,
) -> Result<Unknown<'_>> {
) -> Result<JsUnknown> {
let style = self
.model
.get_cell_style(sheet, row, column)
@@ -337,7 +337,7 @@ impl UserModel {
}
#[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
.from_js_value(styles)
.map_err(|e| to_js_error(e.to_string()))?;
@@ -362,11 +362,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
#[napi(js_name = "getWorksheetsProperties")]
#[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&'_ self, env: Env) -> Unknown<'_> {
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
env
.to_js_value(&self.model.get_worksheets_properties())
.unwrap()
@@ -383,11 +383,11 @@ impl UserModel {
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
#[napi(js_name = "getSelectedView")]
#[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()
}
@@ -440,7 +440,7 @@ impl UserModel {
}
#[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
.from_js_value(source_area)
.map_err(|e| to_js_error(e.to_string()))?;
@@ -454,7 +454,7 @@ impl UserModel {
pub fn auto_fill_columns(
&mut self,
env: Env,
source_area: Unknown,
source_area: JsUnknown,
to_column: i32,
) -> Result<()> {
let area: Area = env
@@ -536,8 +536,8 @@ impl UserModel {
pub fn set_area_with_border(
&mut self,
env: Env,
area: Unknown,
border_area: Unknown,
area: JsUnknown,
border_area: JsUnknown,
) -> Result<()> {
let range: Area = env
.from_js_value(area)
@@ -568,7 +568,7 @@ impl UserModel {
}
#[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
.model
.copy_to_clipboard()
@@ -584,8 +584,8 @@ impl UserModel {
&mut self,
env: Env,
source_sheet: u32,
source_range: Unknown,
clipboard: Unknown,
source_range: JsUnknown,
clipboard: JsUnknown,
is_cut: bool,
) -> Result<()> {
let source_range: (i32, i32, i32, i32) = env
@@ -601,7 +601,7 @@ impl UserModel {
}
#[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
.from_js_value(area)
.map_err(|e| to_js_error(e.to_string()))?;
@@ -612,7 +612,7 @@ impl UserModel {
}
#[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
.model
.get_defined_name_list()

1575
bindings/nodejs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,11 @@ use wasm_bindgen::{
};
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, quote_name as quote_name_ic},
},
types::{CellType, Style},
worksheet::NavigationDirection,
BorderArea, ClipboardData, UserModel as BaseModel,
@@ -31,6 +35,11 @@ pub fn column_name_from_number(column: i32) -> Result<String, JsError> {
}
}
#[wasm_bindgen(js_name = "quoteName")]
pub fn quote_name(name: &str) -> String {
quote_name_ic(name)
}
#[derive(Serialize)]
struct DefinedName {
name: String,

View File

@@ -1,10 +1,12 @@
services:
server:
image: ghcr.io/ironcalc/ironcalc-server:0.6.0
build:
context: .
target: server-runtime
caddy:
image: ghcr.io/ironcalc/ironcalc-caddy:0.6.0
build:
context: .
target: caddy-runtime

1182
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -68,6 +68,8 @@ Using IronCalc, a complex number is a string of the form "1+j3".
## Arrays
## Ranges
## 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

@@ -6,28 +6,27 @@ lang: en-US
# Date and Time functions
At the moment IronCalc only supports a few function in this section.
You can track the progress in this [GitHub issue](https://github.com/ironcalc/IronCalc/issues/48).
All Date and Time functions are already supported in IronCalc.
| Function | Status | Documentation |
| ---------------- | ---------------------------------------------- | ------------- |
| DATE | <Badge type="tip" text="Available" /> | |
| DATEDIF | <Badge type="tip" text="Available" /> | [DATEDIF](date_and_time/datedif) |
| DATEDIF | <Badge type="tip" text="Available" /> | |
| DATEVALUE | <Badge type="tip" text="Available" /> | [DATEVALUE](date_and_time/datevalue) |
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
| DAYS | <Badge type="tip" text="Available" /> | |
| DAYS360 | <Badge type="tip" text="Available" /> | |
| EDATE | <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="tip" text="Available" /> | |
| ISOWEEKNUM | <Badge type="tip" text="Available" /> | |
| MINUTE | <Badge type="tip" text="Available" /> | [MINUTE](date_and_time/minute) |
| MINUTE | <Badge type="tip" text="Available" /> | |
| MONTH | <Badge type="tip" text="Available" /> | [MONTH](date_and_time/month) |
| NETWORKDAYS | <Badge type="tip" text="Available" /> | [NETWORKDAYS](date_and_time/networkdays) |
| NETWORKDAYS.INTL | <Badge type="tip" text="Available" /> | [NETWORKDAYS.INTL](date_and_time/networkdays.intl) |
| NOW | <Badge type="tip" text="Available" /> | |
| SECOND | <Badge type="tip" text="Available" /> | [SECOND](date_and_time/second) |
| TIME | <Badge type="tip" text="Available" /> | [TIME](date_and_time/time) |
| SECOND | <Badge type="tip" text="Available" /> | |
| TIME | <Badge type="tip" text="Available" /> | |
| TIMEVALUE | <Badge type="tip" text="Available" /> | [TIMEVALUE](date_and_time/timevalue) |
| TODAY | <Badge type="tip" text="Available" /> | |
| WEEKDAY | <Badge type="tip" text="Available" /> | |

View File

@@ -4,8 +4,41 @@ outline: deep
lang: en-US
---
# DATEVALUE
# DATEVALUE function
::: 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).
:::
## Overview
DATEVALUE is a function of the Date and Time category that converts a date stored as text to a [serial number](/features/serial-numbers.md) corresponding to a date value.
## Usage
### Syntax
**DATEVALUE(<span title="Text" style="color:#1E88E5">date_text</span>) => <span title="Number" style="color:#1E88E5">datevalue</span>**
### Argument descriptions
* *date_text* ([text](/features/value-types#strings), required). A text string that represents a date in a known format. The text must represent a date between December 31, 1899 and December 31, 9999.
### Additional guidance
* If the year portion of the *date_text* argument is omitted, DATEVALUE uses the current year from the system clock.
* Time information in the *date_text* argument is ignored. DATEVALUE processes only the date portion.
### Returned value
DATEVALUE returns a [number](/features/value-types#numbers) that represents the date as a [serial number](/features/serial-numbers.md). The serial number corresponds to the number of days since December 31, 1899.
### Error conditions
* In common with many other IronCalc functions, DATEVALUE propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then DATEVALUE returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *date_text* argument is not (or cannot be converted to) a [text](/features/value-types#strings) value, then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *date_text* argument represents a date outside the valid range (before December 31, 1899 or after December 31, 9999), then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *date_text* argument cannot be recognized as a valid date format, then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!-- ## Details
For more information on how IronCalc processes Date and Time functions and values, visit [Date and Time](/features/serial-numbers.md)
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=datevalue).
-->
## Links
* See also IronCalc's [TIMEVALUE](/functions/date_and_time/timevalue.md) function for converting time text to serial numbers.
* Visit Microsoft Excel's [DATEVALUE function](https://support.microsoft.com/en-us/office/datevalue-function-df8b07d4-7761-4a93-bc33-b7471bbff252) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093039) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/DATEVALUE) provide versions of the DATEVALUE function.

View File

@@ -22,7 +22,7 @@ NETWORKDAYS.INTL is a function of the Date and Time category that calculates the
* *start_date* ([number](/features/value-types#numbers), required). The start date expressed as a [serial number](/features/serial-numbers.md).
* *end_date* ([number](/features/value-types#numbers), required). The end date expressed as a [serial number](/features/serial-numbers.md).
* *weekend* ([number](/features/value-types#numbers) or [string](/features/value-types#strings), optional). Defines which days are considered weekends. Default is 1 (Saturday-Sunday).
* *holidays* ([array](/features/value-types#arrays) or [range](/features/ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
* *holidays* ([array](/features/value-types#arrays) or [range](/features/value-types#ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
### Weekend parameter options
The _weekend_ parameter can be specified in two ways:

View File

@@ -21,7 +21,7 @@ NETWORKDAYS is a function of the Date and Time category that calculates the numb
### Argument descriptions
* *start_date* ([number](/features/value-types#numbers), required). The start date expressed as a [serial number](/features/serial-numbers.md).
* *end_date* ([number](/features/value-types#numbers), required). The end date expressed as a [serial number](/features/serial-numbers.md).
* *holidays* ([array](/features/value-types#arrays) or [range](/features/ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
* *holidays* ([array](/features/value-types#arrays) or [range](/features/value-types#ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
### Additional guidance
- If the supplied _start_date_ and _end_date_ arguments have fractional parts, NETWORKDAYS uses their [floor values](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions).

View File

@@ -4,9 +4,42 @@ outline: deep
lang: en-US
---
# TIMEVALUE
# TIMEVALUE function
::: warning
**Note:** This draft page is under construction 🚧
The TIMEVALUE function is implemented and available in IronCalc.
:::
## Overview
TIMEVALUE is a function of the Date and Time category that converts a time stored as text to a [serial number](/features/serial-numbers.md) corresponding to a time value. The serial number represents time as a decimal fraction of a 24-hour day (e.g., 0.5 represents 12:00:00 noon).
## Usage
### Syntax
**TIMEVALUE(<span title="Text" style="color:#1E88E5">time_text</span>) => <span title="Number" style="color:#1E88E5">timevalue</span>**
### Argument descriptions
* *time_text* ([text](/features/value-types#strings), required). A text string that represents a time in a known format. The text must represent a time between 00:00:00 and 23:59:59.
### Additional guidance
* Date information in the *time_text* argument is ignored. TIMEVALUE processes only the time portion.
* The function can handle various time formats, including both 12-hour and 24-hour formats, as well as text that includes both date and time information.
### Returned value
TIMEVALUE returns a [number](/features/value-types#numbers) that represents the time as a [serial number](/features/serial-numbers.md). The serial number is a decimal fraction of a 24-hour day, where:
* 0.0 represents 00:00:00 (midnight)
* 0.5 represents 12:00:00 (midday)
* 0.99999... represents 23:59:59 (just before midnight)
### Error conditions
* In common with many other IronCalc functions, TIMEVALUE propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then TIMEVALUE returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *time_text* argument is not (or cannot be converted to) a [text](/features/value-types#strings) value, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *time_text* argument represents a time outside the valid range, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *time_text* argument cannot be recognized as a valid time format, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!--
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=timevalue).
-->
## Links
* See also IronCalc's [DATEVALUE](/functions/date_and_time/datevalue.md) function for converting date text to serial numbers.
* Visit Microsoft Excel's [TIMEVALUE function](https://support.microsoft.com/en-us/office/timevalue-function-0b615c12-33d8-4431-bf3d-f3eb6d186645) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3267350) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/TIMEVALUE) provide versions of the TIMEVALUE function.

View File

@@ -36,8 +36,8 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| CSC | <Badge type="info" text="Not implemented yet" /> | |
| CSCH | <Badge type="info" text="Not implemented yet" /> | |
| DECIMAL | <Badge type="info" text="Not implemented yet" /> | |
| DEGREES | <Badge type="info" text="Not implemented yet" /> | |
| EVEN | <Badge type="info" text="Not implemented yet" /> | |
| DEGREES | <Badge type="info" text="Not implemented yet" /> | [DEGREES](math_and_trigonometry/degrees) |
| EVEN | <Badge type="info" text="Not implemented yet" /> | [EVEN](math_and_trigonometry/even) |
| EXP | <Badge type="info" text="Not implemented yet" /> | |
| FACT | <Badge type="info" text="Not implemented yet" /> | |
| FACTDOUBLE | <Badge type="info" text="Not implemented yet" /> | |
@@ -49,9 +49,9 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | |
| LCM | <Badge type="info" text="Not implemented yet" /> | |
| LET | <Badge type="info" text="Not implemented yet" /> | |
| LN | <Badge type="info" text="Available" /> | |
| LOG | <Badge type="info" text="Available" /> | |
| LOG10 | <Badge type="info" text="Available" /> | |
| LN | <Badge type="tip" text="Available" /> | |
| LOG | <Badge type="tip" text="Available" /> | |
| LOG10 | <Badge type="tip" text="Available" /> | |
| MDETERM | <Badge type="info" text="Not implemented yet" /> | |
| MINVERSE | <Badge type="info" text="Not implemented yet" /> | |
| MMULT | <Badge type="info" text="Not implemented yet" /> | |
@@ -59,12 +59,12 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| MROUND | <Badge type="info" text="Not implemented yet" /> | |
| MULTINOMIAL | <Badge type="info" text="Not implemented yet" /> | |
| MUNIT | <Badge type="info" text="Not implemented yet" /> | |
| ODD | <Badge type="info" text="Not implemented yet" /> | |
| ODD | <Badge type="info" text="Not implemented yet" /> | [ODD](math_and_trigonometry/odd) |
| PI | <Badge type="info" text="Not implemented yet" /> | |
| POWER | <Badge type="tip" text="Available" /> | |
| PRODUCT | <Badge type="tip" text="Available" /> | |
| QUOTIENT | <Badge type="info" text="Not implemented yet" /> | |
| RADIANS | <Badge type="info" text="Not implemented yet" /> | |
| RADIANS | <Badge type="info" text="Not implemented yet" /> | [RADIANS](math_and_trigonometry/radians) |
| RAND | <Badge type="tip" text="Available" /> | |
| RANDARRAY | <Badge type="info" text="Not implemented yet" /> | |
| RANDBETWEEN | <Badge type="tip" text="Available" /> | |
@@ -80,7 +80,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
| SQRT | <Badge type="tip" text="Available" /> | |
| SQRTPI | <Badge type="info" text="Available" /> | |
| SQRTPI | <Badge type="tip" text="Available" /> | |
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | |
| SUM | <Badge type="tip" text="Available" /> | |
| SUMIF | <Badge type="tip" text="Available" /> | |

View File

@@ -4,9 +4,40 @@ outline: deep
lang: en-US
---
# DEGREES
# DEGREES function
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
## Overview
DEGREES is a function of the Math and Trigonometry category that converts an angle measured in radians to an equivalent angle measured in degrees.
## Usage
### Syntax
**DEGREES(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">degrees</span>**
### Argument descriptions
* *angle* ([number](/features/value-types#numbers), required). The angle in radians that is to be converted to degrees.
### Additional guidance
The conversion from radians to degrees is based on the relationship:
$$
1~\:~\text{radian} = \dfrac{180}{\pi}~\text{degrees} \approx 57.29577951~\text{degrees}
$$
### Returned value
DEGREES returns a [number](/features/value-types#numbers) that represents the value of the given angle expressed in degrees.
### Error conditions
* In common with many other IronCalc functions, DEGREES propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then DEGREES returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then DEGREES returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!--
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=degrees).
-->
## Links
* For more information about angle conversions, visit Wikipedia's [Degree (angle)](https://en.wikipedia.org/wiki/Degree_(angle)) page.
* See also IronCalc's [RADIANS](/functions/math_and_trigonometry/radians) function for converting degrees to radians.
* Visit Microsoft Excel's [DEGREES function](https://support.microsoft.com/en-us/office/degrees-function-4d6ec4db-e694-4b94-ace0-1cc3f61f9ba1) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093481) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/DEGREES) provide versions of the DEGREES function.

View File

@@ -4,9 +4,41 @@ outline: deep
lang: en-US
---
# EVEN
# EVEN function
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
## Overview
EVEN is a function of the Math and Trigonometry category that rounds a number up (away from zero) to the nearest even integer.
## Usage
### Syntax
**EVEN(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">even</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The number that is to be rounded to the nearest even integer.
### Additional guidance
* EVEN rounds away from zero, meaning:
* Positive numbers are rounded up to the next even integer.
* Negative numbers are rounded down (toward negative infinity) to the next even integer.
* If the *number* argument is already an even integer, EVEN returns it unchanged.
* Since zero is considered an even number, the EVEN function returns 0 when *number* is 0.
### Returned value
EVEN returns a [number](/features/value-types#numbers) that is the nearest even integer, rounded away from zero.
### Error conditions
* In common with many other IronCalc functions, EVEN propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then EVEN returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then EVEN returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!--
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=even).
-->
## Links
* For more information about even and odd numbers, visit Wikipedia's [Parity](https://en.wikipedia.org/wiki/Parity_(mathematics)) page.
* See also IronCalc's [ODD](/functions/math_and_trigonometry/odd) function.
* Visit Microsoft Excel's [EVEN function](https://support.microsoft.com/en-us/office/even-function-197b5f06-c795-4c1e-8696-3c3b8a646cf9) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093409) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/EVEN) provide versions of the EVEN function.

View File

@@ -4,9 +4,41 @@ outline: deep
lang: en-US
---
# ODD
# ODD function
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
## Overview
ODD is a function of the Math and Trigonometry category that rounds a number up (away from zero) to the nearest odd integer.
## Usage
### Syntax
**ODD(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">odd</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The number that is to be rounded to the nearest odd integer.
### Additional guidance
* ODD rounds away from zero, meaning:
* Positive numbers are rounded up to the next odd integer.
* Negative numbers are rounded down (toward negative infinity) to the next odd integer.
* If the *number* argument is already an odd integer, ODD returns it unchanged.
* Since zero is considered an even number, the ODD function returns 1 when *number* is 0.
### Returned value
ODD returns a [number](/features/value-types#numbers) that is the nearest odd integer, rounded away from zero.
### Error conditions
* In common with many other IronCalc functions, ODD propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ODD returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ODD returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!--
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=odd).
-->
## Links
* For more information about even and odd numbers, visit Wikipedia's [Parity](https://en.wikipedia.org/wiki/Parity_(mathematics)) page.
* See also IronCalc's [EVEN](/functions/math_and_trigonometry/even) function.
* Visit Microsoft Excel's [ODD function](https://support.microsoft.com/en-us/office/odd-function-deae64eb-e08a-4c88-8b40-6d0b42575c98) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093499) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ODD) provide versions of the ODD function.

View File

@@ -4,9 +4,38 @@ outline: deep
lang: en-US
---
# RADIANS
# RADIANS function
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
## Overview
RADIANS is a function of the Math and Trigonometry category that converts an angle measured in degrees to an equivalent angle measured in radians.
## Usage
### Syntax
**RADIANS(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">radians</span>**
### Argument descriptions
* *angle* ([number](/features/value-types#numbers), required). The angle in degrees that is to be converted to radians.
### Additional guidance
The conversion from degrees to radians is based on the relationship:
$$
1~\:~\text{degree} = \dfrac{\pi}{180}~\text{radians} \approx 0.01745329252~\text{radians}
$$
### Returned value
RADIANS returns a [number](/features/value-types#numbers) that represents the value of the given angle expressed in radians.
### Error conditions
* In common with many other IronCalc functions, RADIANS propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then RADIANS returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then RADIANS returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!--
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=radians).
-->
## Links
* For more information about angle conversions, visit Wikipedia's [Radian](https://en.wikipedia.org/wiki/Radian) page.
* See also IronCalc's [DEGREES](/functions/math_and_trigonometry/degrees) function for converting radians to degrees.
* Visit Microsoft Excel's [RADIANS function](https://support.microsoft.com/en-us/office/radians-function-907f0ede-ef2e-4f7b-911a-015e2f8ab878) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093481) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/RADIANS) provide versions of the RADIANS function.

View File

@@ -40,7 +40,7 @@
},
"../../bindings/wasm/pkg": {
"name": "@ironcalc/wasm",
"version": "0.5.0",
"version": "0.6.0",
"license": "MIT/Apache-2.0"
},
"node_modules/@adobe/css-tools": {

View File

@@ -1,6 +1,12 @@
import { Button, Menu, MenuItem, styled } from "@mui/material";
import type { MenuItemProps } from "@mui/material";
import { ChevronDown } from "lucide-react";
import {
ChevronDown,
EyeOff,
PaintBucket,
TextCursorInput,
Trash2,
} from "lucide-react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
@@ -98,7 +104,8 @@ function SheetTab(props: SheetTabProps) {
handleClose();
}}
>
Rename
<TextCursorInput />
{t("sheet_tab.rename")}
</StyledMenuItem>
<StyledMenuItem
onClick={() => {
@@ -106,16 +113,8 @@ function SheetTab(props: SheetTabProps) {
handleClose();
}}
>
Change Color
</StyledMenuItem>
<StyledMenuItem
disabled={!props.canDelete}
onClick={() => {
handleOpenDeleteDialog();
handleClose();
}}
>
Delete
<PaintBucket />
{t("sheet_tab.change_color")}
</StyledMenuItem>
<StyledMenuItem
disabled={!props.canDelete}
@@ -124,8 +123,20 @@ function SheetTab(props: SheetTabProps) {
handleClose();
}}
>
Hide sheet
<EyeOff />
{t("sheet_tab.hide_sheet")}
</StyledMenuItem>
<MenuDivider />
<DeleteButton
disabled={!props.canDelete}
onClick={() => {
handleOpenDeleteDialog();
handleClose();
}}
>
<Trash2 />
{t("sheet_tab.delete")}
</DeleteButton>
</StyledMenu>
<SheetRenameDialog
open={renameDialogOpen}
@@ -178,7 +189,9 @@ const StyledMenu = styled(Menu)`
const StyledMenuItem = styled(MenuItem)<MenuItemProps>(() => ({
display: "flex",
justifyContent: "space-between",
justifyContent: "flex-start",
alignItems: "center",
gap: "8px",
fontSize: "12px",
width: "calc(100% - 8px)",
margin: "0px 4px",
@@ -188,6 +201,11 @@ const StyledMenuItem = styled(MenuItem)<MenuItemProps>(() => ({
"&:disabled": {
color: "#BDBDBD",
},
"& svg": {
width: "16px",
height: "16px",
color: `${theme.palette.grey[600]}`,
},
}));
const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
@@ -233,4 +251,25 @@ const Name = styled("div")`
user-select: none;
`;
const MenuDivider = styled("div")`
width: 100%;
margin: auto;
margin-top: 4px;
margin-bottom: 4px;
border-top: 1px solid ${theme.palette.grey[200]};
`;
const DeleteButton = styled(StyledMenuItem)`
color: ${theme.palette.error.main};
svg {
color: ${theme.palette.error.main};
}
&:hover {
background-color: ${theme.palette.error.main}1A;
}
&:active {
background-color: ${theme.palette.error.main}1A;
}
`;
export default SheetTab;

View File

@@ -1,7 +1,9 @@
import { styled } from "@mui/material";
import { Tooltip } from "@mui/material";
import { Menu, Plus } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { IronCalcLogo } from "../../icons";
import { theme } from "../../theme";
import { StyledButton } from "../Toolbar/Toolbar";
import { NAVIGATION_HEIGHT } from "../constants";
@@ -49,20 +51,16 @@ function SheetTabBar(props: SheetTabBarProps) {
return (
<Container>
<LeftButtonsContainer>
<StyledButton
title={t("navigation.add_sheet")}
$pressed={false}
onClick={props.onAddBlankSheet}
>
<Plus />
</StyledButton>
<StyledButton
onClick={handleClick}
title={t("navigation.sheet_list")}
$pressed={false}
>
<Menu />
</StyledButton>
<Tooltip title={t("navigation.add_sheet")}>
<StyledButton $pressed={false} onClick={props.onAddBlankSheet}>
<Plus />
</StyledButton>
</Tooltip>
<Tooltip title={t("navigation.sheet_list")}>
<StyledButton onClick={handleClick} $pressed={false}>
<Menu />
</StyledButton>
</Tooltip>
</LeftButtonsContainer>
<VerticalDivider />
<Sheets>
@@ -90,9 +88,15 @@ function SheetTabBar(props: SheetTabBarProps) {
))}
</SheetInner>
</Sheets>
<Advert href="https://www.ironcalc.com" target="_blank">
ironcalc.com
</Advert>
<RightContainer>
<Tooltip title={t("navigation.link")}>
<LogoLink
onClick={() => window.open("https://www.ironcalc.com", "_blank")}
>
<IronCalcLogo />
</LogoLink>
</Tooltip>
</RightContainer>
<SheetListMenu
anchorEl={anchorEl}
open={open}
@@ -119,14 +123,11 @@ const Container = styled("div")`
display: flex;
height: ${NAVIGATION_HEIGHT}px;
align-items: center;
padding: 0px 12px;
padding: 0px;
font-family: Inter;
overflow: hidden;
background-color: ${theme.palette.common.white};
border-top: 1px solid ${theme.palette.grey["300"]};
@media (max-width: 769px) {
padding-right: 0px;
padding-left: 8px;
}
`;
const Sheets = styled("div")`
@@ -143,30 +144,15 @@ const SheetInner = styled("div")`
display: flex;
`;
const Advert = styled("a")`
display: flex;
align-items: center;
color: ${theme.palette.primary.main};
padding: 0px 0px 0px 12px;
font-size: 12px;
text-decoration: none;
border-left: 1px solid ${theme.palette.grey["300"]};
transition: color 0.2s ease-in-out;
&:hover {
text-decoration: underline;
}
@media (max-width: 769px) {
display: none;
}
`;
const LeftButtonsContainer = styled("div")`
display: flex;
flex-direction: row;
align-items: center;
height: 100%;
gap: 4px;
padding-right: 12px;
padding: 0px 12px;
@media (max-width: 769px) {
padding-right: 8px;
padding: 0px 8px;
}
`;
@@ -178,4 +164,29 @@ const VerticalDivider = styled("div")`
}
`;
const RightContainer = styled("a")`
display: flex;
align-items: center;
color: ${theme.palette.primary.main};
height: 100%;
padding: 0px 8px;
@media (max-width: 769px) {
display: none;
}
`;
const LogoLink = styled("div")`
display: flex;
align-items: center;
padding: 8px;
border-radius: 4px;
svg {
height: 14px;
width: auto;
}
&:hover {
background-color: ${theme.palette.grey["100"]};
}
`;
export default SheetTabBar;

View File

@@ -1,36 +1,12 @@
import type { Area, Cell } from "./types";
import { type SelectedView, columnNameFromNumber } from "@ironcalc/wasm";
import {
type SelectedView,
columnNameFromNumber,
quoteName,
} from "@ironcalc/wasm";
import { LAST_COLUMN, LAST_ROW } from "./WorksheetCanvas/constants";
// FIXME: Use the `quoteName` function from the wasm module
function nameNeedsQuoting(name: string): boolean {
// it contains any of these characters: ()'$,;-+{} or space
for (const char of name) {
if (" ()'$,;-+{}".includes(char)) {
return true;
}
}
// TODO:
// - cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
// - cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
// - integers
return false;
}
/**
* Quotes a string sheet name if it needs to
* NOTE: Invalid characters in a sheet name: \, /, *, [, ], :, ?
*/
export function quoteName(name: string): string {
if (nameNeedsQuoting(name)) {
return `'${name.replace(/'/g, "''")}'`;
}
return name;
}
/**
* Returns true if the keypress should start editing
*/

View File

@@ -81,6 +81,12 @@
"confirm": "Yes, delete sheet",
"cancel": "Cancel"
},
"sheet_tab": {
"rename": "Rename",
"change_color": "Change Color",
"delete": "Delete",
"hide_sheet": "Hide sheet"
},
"formula_input": {
"update": "Update",
"label": "Formula",
@@ -88,7 +94,8 @@
},
"navigation": {
"add_sheet": "Add sheet",
"sheet_list": "Sheet list"
"sheet_list": "Sheet list",
"link": "Open ironcalc.com"
},
"name_manager_dialog": {
"title": "Named Ranges",

View File

@@ -2,7 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ironcalc.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=1" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=1" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=1" />
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png?v=1" />
<link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png?v=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <meta name="theme-color" content="#1bb566"> -->
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F2994A" />

View File

@@ -75,6 +75,16 @@ function App() {
start();
}, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: localStorageId needed to detect name changes (model mutates internally)
useEffect(() => {
if (model) {
const workbookName = model.getName();
document.title = workbookName ? `${workbookName} - IronCalc` : "IronCalc";
} else {
document.title = "IronCalc";
}
}, [model, localStorageId]);
if (!model) {
return (
<Loading>

View File

@@ -8,6 +8,19 @@ type ModelsMetadata = Record<
{ name: string; createdAt: number; pinned?: boolean }
>;
function randomUUID(): string {
try {
return crypto.randomUUID();
} catch {
// Fallback for environments without crypto.randomUUID()
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
}
export function updateNameSelectedWorkbook(model: Model, newName: string) {
const uuid = localStorage.getItem("selected");
if (uuid) {
@@ -77,7 +90,7 @@ export function createNewModel(): Model {
const name = getNewName(Object.values(models).map((m) => m.name));
const model = new Model(name, "en", "UTC");
const uuid = crypto.randomUUID();
const uuid = randomUUID();
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
@@ -121,7 +134,7 @@ export function saveSelectedModelInStorage(model: Model) {
}
export function saveModelToStorage(model: Model) {
const uuid = crypto.randomUUID();
const uuid = randomUUID();
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
let modelsJson = localStorage.getItem("models");
@@ -228,7 +241,7 @@ export function duplicateModel(uuid: string): Model | null {
duplicatedModel.setName(newName);
const newUuid = crypto.randomUUID();
const newUuid = randomUUID();
localStorage.setItem("selected", newUuid);
localStorage.setItem(newUuid, bytesToBase64(duplicatedModel.toBytes()));

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.