Compare commits

..

23 Commits

Author SHA1 Message Date
Nicolás Hatcher
379c84f64a WIP: Failed attempt to do a solid-js app.
For now...
2024-05-22 20:55:54 +02:00
Nicolás Hatcher Andrés
49c3b14bf0 UPDATE: Adds get/set views to the user model API (#69) 2024-05-20 17:51:09 +02:00
Nicolás Hatcher Andrés
d2cba48f8e FIX: Fixes incorrect result in M1 Apple silicom (#68) 2024-05-19 10:34:57 +02:00
Nicolás Hatcher Andrés
f752c90058 UPDATE: Parses selected row/column/range and selected sheet (#67)
* FIX: Update to Rust 1.78.0

* UPDATE: Parses selected row/column/range and selected sheet
2024-05-09 11:46:26 +02:00
Daniel González-Albo
a78d5593f2 Merge pull request #60 from ironcalc/feature/dani-logo
UPDATE: adds missing favicons
2024-04-27 18:11:29 +02:00
Daniel
079208a1bd UPDATE: adds missing favicons 2024-04-27 18:02:04 +02:00
Daniel González-Albo
4721582dfe Merge pull request #42 from ironcalc/feature/dani-logo
UPDATE: adds logo
2024-04-25 19:49:43 +02:00
Daniel
1746eec5da UPDATE: adds logo 2024-04-25 19:42:10 +02:00
Nicolás Hatcher Andrés
f9cf86a17c Bugfix/nicolas more fixes (#36)
* FIX: Remove the serde_json depndendency

* UPDATE: Use binary representation also for languages and locales
2024-04-15 19:25:38 +02:00
Nicolás Hatcher Andrés
49ef846ebd FIX: small diverse fixes (#35) 2024-04-14 21:50:14 +02:00
Nicolás Hatcher Andrés
b3b7dea930 FIX: Fixes types for TypeScript manually (#32) 2024-04-12 21:08:03 +02:00
Nicolás Hatcher Andrés
196e074ef5 UPDATE: Serialize/Deserialize with bitcode (#31)
* UPDATE: Serialize/Deserialize with bitcode

Fixes #12
2024-04-11 23:45:00 +02:00
Nicolás Hatcher Andrés
489027991c UPDATE: Adds Web browser wasm bindings (#30)
* UPDATE: Adds Web browser wasm bindings

* FIX: install wasm-pack in the GitHub actions
2024-04-07 12:41:33 +02:00
Nicolás Hatcher Andrés
d445553d85 UPDATE: Adds 'user model' API (#27)
* bump version for documentation
* Fixes wrong doc comment
* renames old APIs to be consistent
2024-04-03 22:41:15 +02:00
Nicolás Hatcher
e9fc41541b FIX: adds test 2024-03-21 23:07:07 +01:00
Nicolás Hatcher
c474bd2fc0 FIX: Adds small test 2024-03-21 22:51:38 +01:00
Nicolás Hatcher
d6a1f9c28e FIX: Rust update to 1.77.0 2024-03-21 22:51:19 +01:00
Nicolás Hatcher
5be13d1602 FIX: Adds test case for escape_xml function 2024-03-21 22:32:48 +01:00
Nicolás Hatcher
053217d3e4 TEST: check yml file for codecov cfg 2024-03-16 18:07:03 +01:00
Nicolás Hatcher
493dc56892 FIX: Properly format title in function list 2024-03-16 17:38:57 +01:00
Nicolás Hatcher
a38ba93724 FIX: Clearer list for the wiki 2024-03-16 17:35:40 +01:00
Nicolás Hatcher Andrés
0029901ca3 UPDATE: Adds minimal generated documented functions (#22)
See #21
2024-03-16 12:57:25 +01:00
Nicolás Hatcher Andrés
1381533b9c Update README.md 2024-03-12 20:42:06 +01:00
168 changed files with 14180 additions and 3368 deletions

18
.github/workflows/publish-wiki.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Publish wiki
on:
push:
branches: [main]
paths:
- wiki/**
- .github/workflows/publish-wiki.yml
concurrency:
group: publish-wiki
cancel-in-progress: true
permissions:
contents: write
jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Andrew-Chen-Wang/github-wiki-action@v4

View File

@@ -16,6 +16,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build - name: Build
run: cargo build --release --verbose run: cargo build --release --verbose

3
.gitignore vendored
View File

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

604
Cargo.lock generated
View File

@@ -9,10 +9,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "aho-corasick" name = "aes"
version = "1.1.2" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -34,15 +45,57 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitcode"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48bc1c27654127a24c476d40198746860ef56475f41a601bfa5c4d0f832968f0"
dependencies = [
"bitcode_derive",
"bytemuck",
]
[[package]]
name = "bitcode_derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2966755a19aad59ee2aae91e2d48842c667a99d818ec72168efdab07200701cc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.14.0" version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]]
name = "bytemuck"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -73,10 +126,11 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.83" version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
dependencies = [ dependencies = [
"jobserver",
"libc", "libc",
] ]
@@ -88,9 +142,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.31" version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
@@ -102,9 +156,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono-tz" name = "chrono-tz"
version = "0.7.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc529705a6e0028189c83f0a5dd9fb214105116f7e3c0eeab7ff0369766b0d1" checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz-build", "chrono-tz-build",
@@ -113,9 +167,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono-tz-build" name = "chrono-tz-build"
version = "0.1.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751" checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
dependencies = [ dependencies = [
"parse-zoneinfo", "parse-zoneinfo",
"phf", "phf",
@@ -123,25 +177,96 @@ dependencies = [
] ]
[[package]] [[package]]
name = "core-foundation-sys" name = "cipher"
version = "0.8.4" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.3.2" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]] [[package]]
name = "either" name = "crossbeam-utils"
version = "1.9.0" version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]] [[package]]
name = "flate2" name = "flate2"
@@ -154,21 +279,40 @@ dependencies = [
] ]
[[package]] [[package]]
name = "getrandom" name = "generic-array"
version = "0.2.11" version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
] ]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.58" version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@@ -187,10 +331,20 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.1.3" version = "0.1.3"
dependencies = [ dependencies = [
"bitcode",
"chrono", "chrono",
"ironcalc_base", "ironcalc_base",
"itertools", "itertools",
@@ -204,8 +358,9 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.1.2" version = "0.1.3"
dependencies = [ dependencies = [
"bitcode",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"js-sys", "js-sys",
@@ -215,74 +370,88 @@ dependencies = [
"ryu", "ryu",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr",
] ]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [ dependencies = [
"either", "either",
] ]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jobserver"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.65" version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.150" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.20" version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.6.4" version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [ dependencies = [
"adler", "adler",
] ]
[[package]] [[package]]
name = "num-traits" name = "num-conv"
version = "0.2.17" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [ dependencies = [
"autocfg", "autocfg",
] ]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.18.0" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "parse-zoneinfo" name = "parse-zoneinfo"
@@ -293,6 +462,29 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.11.2" version = "0.11.2"
@@ -333,9 +525,15 @@ dependencies = [
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.27" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
@@ -345,18 +543,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.69" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -393,9 +591,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.2" version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -405,9 +603,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.3" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -416,39 +614,53 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.13.1" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
dependencies = [
"xmlparser",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.15" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.192" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde-wasm-bindgen"
version = "1.0.192" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -457,9 +669,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.108" version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@@ -467,14 +679,25 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_repr" name = "sha1"
version = "0.1.17" version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [ dependencies = [
"proc-macro2", "cfg-if",
"quote", "cpufeatures",
"syn", "digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
] ]
[[package]] [[package]]
@@ -484,10 +707,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]] [[package]]
name = "syn" name = "subtle"
version = "2.0.39" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -496,18 +725,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.50" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.50" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -516,15 +745,29 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.45" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [ dependencies = [
"libc", "deranged",
"wasi 0.10.0+wasi-snapshot-preview1", "num-conv",
"winapi", "powerfmt",
"serde",
"time-core",
] ]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"
@@ -533,19 +776,19 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.5.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde", "serde",
] ]
[[package]] [[package]]
name = "wasi" name = "version_check"
version = "0.10.0+wasi-snapshot-preview1" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "wasi" name = "wasi"
@@ -553,11 +796,22 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.1.3"
dependencies = [
"ironcalc_base",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.88" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@@ -565,9 +819,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.88" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@@ -579,10 +833,22 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-futures"
version = "0.2.88" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -590,9 +856,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.88" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -603,46 +869,59 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.88" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]] [[package]]
name = "winapi" name = "wasm-bindgen-test"
version = "0.3.9" version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b"
dependencies = [ dependencies = [
"winapi-i686-pc-windows-gnu", "console_error_panic_hook",
"winapi-x86_64-pc-windows-gnu", "js-sys",
"scoped-tls",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
] ]
[[package]] [[package]]
name = "winapi-i686-pc-windows-gnu" name = "wasm-bindgen-test-macro"
version = "0.4.0" version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "web-sys"
version = "0.4.0" version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.51.1" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm",
"windows_aarch64_msvc", "windows_aarch64_msvc",
@@ -655,62 +934,91 @@ dependencies = [
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]] [[package]]
name = "zip" name = "zip"
version = "0.5.13" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [ dependencies = [
"aes",
"byteorder", "byteorder",
"bzip2", "bzip2",
"constant_time_eq",
"crc32fast", "crc32fast",
"crossbeam-utils",
"flate2", "flate2",
"thiserror", "hmac",
"pbkdf2",
"sha1",
"time", "time",
"zstd",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.10+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
dependencies = [
"cc",
"pkg-config",
] ]

View File

@@ -4,6 +4,7 @@ resolver = "2"
members = [ members = [
"base", "base",
"xlsx", "xlsx",
"bindings/wasm",
] ]
exclude = [ exclude = [

View File

@@ -7,14 +7,18 @@ format:
tests: lint tests: lint
cargo test cargo test
make remove-xlsx ./target/debug/documentation
cmp functions.md wiki/functions.md || exit 1
make remove-artifacts
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs
remove-xlsx: remove-artifacts:
rm -f xlsx/hello-calc.xlsx rm -f xlsx/hello-calc.xlsx
rm -f xlsx/hello-styles.xlsx rm -f xlsx/hello-styles.xlsx
rm -f xlsx/widths-and-heights.xlsx rm -f xlsx/widths-and-heights.xlsx
rm -f functions.md
clean: remove-xlsx clean: remove-artifacts
cargo clean cargo clean
rm -r -f base/target rm -r -f base/target
rm -r -f xlsx/target rm -r -f xlsx/target
@@ -27,6 +31,10 @@ coverage:
CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
update-docs:
cargo build
./target/debug/documentation -o wiki/functions.md
docs: docs:
cargo doc --no-deps cargo doc --no-deps

View File

@@ -117,55 +117,7 @@ See more examples in the `examples` folder of the xlsx crate.
# ROADMAP # ROADMAP
> [!WARNING] See https://github.com/ironcalc
> This is work-in-progress. IronCalc in developed in the open. Expect things to be broken and change quickly until version 0.5
Major milestones:
* MVP, version 0.5.0: We intend to have a working version by mid March 2024 (version 0.5, MVP)
* Stable, version 1.0.0 will come later in December 2024
MVP stands for _Minimum Viable Product_
## Version 0.5 or MVP (early 2024)
Version 0.5 includes the engine, javascript and nodejs bindings and a web application
Features of the engine include:
* Read and write xlsx files
* API to set and read values from cells
* Implemented 192 Excel functions
* Time functions with timezones
* Prepared for i18n but will only support English
* Wide test coverage
UI features of the web application (backed by the engine):
* Enter values and formulas. Browse mode
* Italics, bold, underline, horizontal alignment
* Number formatting
* Add/remove/rename sheets
* Copy/Paste extend values
* Keyboard navigation
* Delete/Add rows and columns
* Resize rows and columns
* Correct scrolling and navigation
## Version 1.0 or Stable (December 2024)
Minor milestones in the ROADMAD for version 1.0.0 (engine and UI):
* Implementation of arrays and array formulas
* Formula documentation and context help
* Merge cells
* Pivot tables
* Define name manager (mostly UI)
* Update main evaluation algorithm with a support graph
* Dynamic arrays (SORT, UNIQUE, ..)
* Full i18n support with different locales and languages
* Python bindings
* Full test coverage
# Early testing # Early testing
@@ -192,4 +144,4 @@ Licensed under either of
* [MIT license](LICENSE-MIT) * [MIT license](LICENSE-MIT)
* [Apache license, version 2.0](LICENSE-Apache-2.0) * [Apache license, version 2.0](LICENSE-Apache-2.0)
at your option. at your option.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

BIN
assets/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/logo/png/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/logo/png/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.1.2" version = "0.1.3"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021" edition = "2021"
homepage = "https://www.ironcalc.com" homepage = "https://www.ironcalc.com"
@@ -12,18 +12,20 @@ readme = "README.md"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
ryu = "1.0" ryu = "1.0"
chrono = "0.4" chrono = "0.4"
chrono-tz = "0.7.0" chrono-tz = "0.9"
regex = "1.0" regex = "1.0"
once_cell = "1.16.0" once_cell = "1.16.0"
bitcode = "0.6.0"
[dev-dependencies]
serde_json = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.60" } js-sys = { version = "0.3.69" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rand = "0.8.4" rand = "0.8.5"

View File

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

View File

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

View File

@@ -77,13 +77,13 @@ impl Model {
let style = source_cell.get_style(); let style = source_cell.get_style();
// FIXME: we need some user_input getter instead of get_text // FIXME: we need some user_input getter instead of get_text
let formula_or_value = self let formula_or_value = self
.cell_formula(sheet, source_row, source_column)? .get_cell_formula(sheet, source_row, source_column)?
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language)); .unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
self.set_user_input(sheet, target_row, target_column, formula_or_value); self.set_user_input(sheet, target_row, target_column, formula_or_value);
self.workbook self.workbook
.worksheet_mut(sheet)? .worksheet_mut(sheet)?
.set_cell_style(target_row, target_column, style); .set_cell_style(target_row, target_column, style);
self.delete_cell(sheet, source_row, source_column)?; self.cell_clear_all(sheet, source_row, source_column)?;
Ok(()) Ok(())
} }
@@ -157,6 +157,11 @@ impl Model {
return Err("Please use insert columns instead".to_string()); return Err("Please use insert columns instead".to_string());
} }
// first column being deleted
let column_start = column;
// last column being deleted
let column_end = column + column_count - 1;
// Move cells // Move cells
let worksheet = &self.workbook.worksheet(sheet)?; let worksheet = &self.workbook.worksheet(sheet)?;
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect(); let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
@@ -166,11 +171,11 @@ impl Model {
for r in all_rows { for r in all_rows {
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?; let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
for col in columns { for col in columns {
if col >= column { if col >= column_start {
if col >= column + column_count { if col > column_end {
self.move_cell(sheet, r, col, r, col - column_count)?; self.move_cell(sheet, r, col, r, col - column_count)?;
} else { } else {
self.delete_cell(sheet, r, col)?; self.cell_clear_all(sheet, r, col)?;
} }
} }
} }
@@ -184,6 +189,64 @@ impl Model {
delta: -column_count, delta: -column_count,
}), }),
); );
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
// deletes all the column styles
let mut new_columns = Vec::new();
for col in worksheet.cols.iter_mut() {
// range under study
let min = col.min;
let max = col.max;
// In the diagram:
// |xxxxx| range we are studying [min, max]
// |*****| range we are deleting [column_start, column_end]
// we are going to split it in three big cases:
// ----------------|xxxxxxxx|-----------------
// -----|*****|------------------------------- Case A
// -------|**********|------------------------ Case B
// -------------|**************|-------------- Case C
// ------------------|****|------------------- Case D
// ---------------------|**********|---------- Case E
// -----------------------------|*****|------- Case F
if column_start < min {
if column_end < min {
// Case A
// We displace all columns
let mut new_column = col.clone();
new_column.min = min - column_count;
new_column.max = max - column_count;
new_columns.push(new_column);
} else if column_end < max {
// Case B
// We displace the end
let mut new_column = col.clone();
new_column.min = column_start;
new_column.max = max - column_count;
new_columns.push(new_column);
} else {
// Case C
// skip this, we are deleting the whole range
}
} else if column_start <= max {
if column_end <= max {
// Case D
// We displace the end
let mut new_column = col.clone();
new_column.max = max - column_count;
new_columns.push(new_column);
} else {
// Case E
let mut new_column = col.clone();
new_column.max = column_start - 1;
new_columns.push(new_column);
}
} else {
// Case F
// No action required
new_columns.push(col.clone());
}
}
worksheet.cols = new_columns;
Ok(()) Ok(())
} }
@@ -283,7 +346,7 @@ impl Model {
// remove all cells in row // remove all cells in row
// FIXME: We could just remove the entire row in one go // FIXME: We could just remove the entire row in one go
for column in columns { for column in columns {
self.delete_cell(sheet, r, column)?; self.cell_clear_all(sheet, r, column)?;
} }
} }
} }

View File

@@ -1,12 +1,9 @@
use crate::{ use crate::{
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*, expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
}; };
use serde::{Deserialize, Serialize};
use serde_json::json;
/// A CellValue is the representation of the cell content. /// A CellValue is the representation of the cell content.
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Debug, PartialEq)]
#[serde(untagged)]
pub enum CellValue { pub enum CellValue {
None, None,
String(String), String(String),
@@ -14,17 +11,6 @@ pub enum CellValue {
Boolean(bool), Boolean(bool),
} }
impl CellValue {
pub fn to_json_str(&self) -> String {
match &self {
CellValue::None => "null".to_string(),
CellValue::String(s) => json!(s).to_string(),
CellValue::Number(f) => json!(f).to_string(),
CellValue::Boolean(b) => json!(b).to_string(),
}
}
}
impl From<f64> for CellValue { impl From<f64> for CellValue {
fn from(value: f64) -> Self { fn from(value: f64) -> Self {
Self::Number(value) Self::Number(value)

View File

@@ -308,9 +308,9 @@ impl Lexer {
return self.consume_range(None); return self.consume_range(None);
} }
let name_upper = name.to_ascii_uppercase(); let name_upper = name.to_ascii_uppercase();
if name_upper == self.language.booleans.true_value { if name_upper == self.language.booleans.r#true {
return TokenType::Boolean(true); return TokenType::Boolean(true);
} else if name_upper == self.language.booleans.false_value { } else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false); return TokenType::Boolean(false);
} }
if self.mode == LexerMode::A1 { if self.mode == LexerMode::A1 {
@@ -660,8 +660,8 @@ impl Lexer {
fn consume_error(&mut self) -> TokenType { fn consume_error(&mut self) -> TokenType {
let errors = &self.language.errors; let errors = &self.language.errors;
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect(); let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
if rest_of_formula.starts_with(&errors.ref_value) { if rest_of_formula.starts_with(&errors.r#ref) {
self.position += errors.ref_value.chars().count() - 1; self.position += errors.r#ref.chars().count() - 1;
return TokenType::Error(Error::REF); return TokenType::Error(Error::REF);
} else if rest_of_formula.starts_with(&errors.name) { } else if rest_of_formula.starts_with(&errors.name) {
self.position += errors.name.chars().count() - 1; self.position += errors.name.chars().count() - 1;

View File

@@ -6,11 +6,11 @@ use crate::{
token::TokenType, token::TokenType,
}, },
language::get_language, language::get_language,
locale::get_locale_fix, locale::get_locale,
}; };
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer { fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
let locale = get_locale_fix(locale).unwrap(); let locale = get_locale(locale).unwrap();
let language = get_language(language).unwrap(); let language = get_language(language).unwrap();
Lexer::new(formula, LexerMode::A1, locale, language) Lexer::new(formula, LexerMode::A1, locale, language)
} }

View File

@@ -222,7 +222,7 @@ impl Parser {
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node { pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
self.lexer.set_formula(formula); self.lexer.set_formula(formula);
self.context = context.clone(); self.context.clone_from(context);
self.parse_expr() self.parse_expr()
} }

View File

@@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::language::Language; use crate::language::Language;
@@ -80,8 +80,7 @@ impl fmt::Display for OpProduct {
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+") /// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc /// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
/// Note that they are serialized/deserialized by index /// Note that they are serialized/deserialized by index
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
pub enum Error { pub enum Error {
REF, REF,
NAME, NAME,
@@ -119,7 +118,7 @@ impl Error {
pub fn to_localized_error_string(&self, language: &Language) -> String { pub fn to_localized_error_string(&self, language: &Language) -> String {
match self { match self {
Error::NULL => language.errors.null.to_string(), Error::NULL => language.errors.null.to_string(),
Error::REF => language.errors.ref_value.to_string(), Error::REF => language.errors.r#ref.to_string(),
Error::NAME => language.errors.name.to_string(), Error::NAME => language.errors.name.to_string(),
Error::VALUE => language.errors.value.to_string(), Error::VALUE => language.errors.value.to_string(),
Error::DIV => language.errors.div.to_string(), Error::DIV => language.errors.div.to_string(),
@@ -136,7 +135,7 @@ impl Error {
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> { pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
let errors = &language.errors; let errors = &language.errors;
if name == errors.ref_value { if name == errors.r#ref {
return Some(Error::REF); return Some(Error::REF);
} else if name == errors.name { } else if name == errors.name {
return Some(Error::NAME); return Some(Error::NAME);

View File

@@ -1,7 +1,6 @@
use chrono::DateTime;
use chrono::Datelike; use chrono::Datelike;
use chrono::Months; use chrono::Months;
use chrono::NaiveDateTime;
use chrono::TimeZone;
use chrono::Timelike; use chrono::Timelike;
use crate::expressions::types::CellReferenceIndex; use crate::expressions::types::CellReferenceIndex;
@@ -258,8 +257,8 @@ impl Model {
// milliseconds since January 1, 1970 00:00:00 UTC. // milliseconds since January 1, 1970 00:00:00 UTC.
let milliseconds = get_milliseconds_since_epoch(); let milliseconds = get_milliseconds_since_epoch();
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) { let local_time = match DateTime::from_timestamp(seconds, 0) {
Some(dt) => dt, Some(dt) => dt.with_timezone(&self.tz),
None => { None => {
return CalcResult::Error { return CalcResult::Error {
error: Error::ERROR, error: Error::ERROR,
@@ -268,7 +267,6 @@ impl Model {
} }
} }
}; };
let local_time = self.tz.from_utc_datetime(&dt);
// 693_594 is computed as: // 693_594 is computed as:
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2 // NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug // The 2 days offset is because of Excel 1900 bug
@@ -289,8 +287,8 @@ impl Model {
// milliseconds since January 1, 1970 00:00:00 UTC. // milliseconds since January 1, 1970 00:00:00 UTC.
let milliseconds = get_milliseconds_since_epoch(); let milliseconds = get_milliseconds_since_epoch();
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) { let local_time = match DateTime::from_timestamp(seconds, 0) {
Some(dt) => dt, Some(dt) => dt.with_timezone(&self.tz),
None => { None => {
return CalcResult::Error { return CalcResult::Error {
error: Error::ERROR, error: Error::ERROR,
@@ -299,7 +297,6 @@ impl Model {
} }
} }
}; };
let local_time = self.tz.from_utc_datetime(&dt);
// 693_594 is computed as: // 693_594 is computed as:
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2 // NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug // The 2 days offset is because of Excel 1900 bug

View File

@@ -165,7 +165,8 @@ impl Model {
message: "argument must be a reference to a single cell".to_string(), message: "argument must be a reference to a single cell".to_string(),
}; };
} }
let is_formula = if let Ok(f) = self.cell_formula(left.sheet, left.row, left.column) { let is_formula = if let Ok(f) = self.get_cell_formula(left.sheet, left.row, left.column)
{
f.is_some() f.is_some()
} else { } else {
false false

View File

@@ -1,4 +1,5 @@
use core::fmt; use core::fmt;
use std::array::IntoIter;
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
@@ -244,6 +245,206 @@ pub enum Function {
Subtotal, Subtotal,
} }
impl Function {
pub fn into_iter() -> IntoIter<Function, 192> {
[
Function::And,
Function::False,
Function::If,
Function::Iferror,
Function::Ifna,
Function::Ifs,
Function::Not,
Function::Or,
Function::Switch,
Function::True,
Function::Xor,
Function::Sin,
Function::Cos,
Function::Tan,
Function::Asin,
Function::Acos,
Function::Atan,
Function::Sinh,
Function::Cosh,
Function::Tanh,
Function::Asinh,
Function::Acosh,
Function::Atanh,
Function::Abs,
Function::Pi,
Function::Sqrt,
Function::Sqrtpi,
Function::Atan2,
Function::Power,
Function::Max,
Function::Min,
Function::Product,
Function::Rand,
Function::Randbetween,
Function::Round,
Function::Rounddown,
Function::Roundup,
Function::Sum,
Function::Sumif,
Function::Sumifs,
Function::Choose,
Function::Column,
Function::Columns,
Function::Index,
Function::Indirect,
Function::Hlookup,
Function::Lookup,
Function::Match,
Function::Offset,
Function::Row,
Function::Rows,
Function::Vlookup,
Function::Xlookup,
Function::Concatenate,
Function::Exact,
Function::Value,
Function::T,
Function::Valuetotext,
Function::Concat,
Function::Find,
Function::Left,
Function::Len,
Function::Lower,
Function::Mid,
Function::Right,
Function::Search,
Function::Text,
Function::Trim,
Function::Upper,
Function::Isnumber,
Function::Isnontext,
Function::Istext,
Function::Islogical,
Function::Isblank,
Function::Iserr,
Function::Iserror,
Function::Isna,
Function::Na,
Function::Isref,
Function::Isodd,
Function::Iseven,
Function::ErrorType,
Function::Isformula,
Function::Type,
Function::Sheet,
Function::Average,
Function::Averagea,
Function::Averageif,
Function::Averageifs,
Function::Count,
Function::Counta,
Function::Countblank,
Function::Countif,
Function::Countifs,
Function::Maxifs,
Function::Minifs,
Function::Year,
Function::Day,
Function::Month,
Function::Eomonth,
Function::Date,
Function::Edate,
Function::Today,
Function::Now,
Function::Pmt,
Function::Pv,
Function::Rate,
Function::Nper,
Function::Fv,
Function::Ppmt,
Function::Ipmt,
Function::Npv,
Function::Mirr,
Function::Irr,
Function::Xirr,
Function::Xnpv,
Function::Rept,
Function::Textafter,
Function::Textbefore,
Function::Textjoin,
Function::Substitute,
Function::Ispmt,
Function::Rri,
Function::Sln,
Function::Syd,
Function::Nominal,
Function::Effect,
Function::Pduration,
Function::Tbillyield,
Function::Tbillprice,
Function::Tbilleq,
Function::Dollarde,
Function::Dollarfr,
Function::Ddb,
Function::Db,
Function::Cumprinc,
Function::Cumipmt,
Function::Besseli,
Function::Besselj,
Function::Besselk,
Function::Bessely,
Function::Erf,
Function::ErfPrecise,
Function::Erfc,
Function::ErfcPrecise,
Function::Bin2dec,
Function::Bin2hex,
Function::Bin2oct,
Function::Dec2Bin,
Function::Dec2hex,
Function::Dec2oct,
Function::Hex2bin,
Function::Hex2dec,
Function::Hex2oct,
Function::Oct2bin,
Function::Oct2dec,
Function::Oct2hex,
Function::Bitand,
Function::Bitlshift,
Function::Bitor,
Function::Bitrshift,
Function::Bitxor,
Function::Complex,
Function::Imabs,
Function::Imaginary,
Function::Imargument,
Function::Imconjugate,
Function::Imcos,
Function::Imcosh,
Function::Imcot,
Function::Imcsc,
Function::Imcsch,
Function::Imdiv,
Function::Imexp,
Function::Imln,
Function::Imlog10,
Function::Imlog2,
Function::Impower,
Function::Improduct,
Function::Imreal,
Function::Imsec,
Function::Imsech,
Function::Imsin,
Function::Imsinh,
Function::Imsqrt,
Function::Imsub,
Function::Imsum,
Function::Imtan,
Function::Convert,
Function::Delta,
Function::Gestep,
Function::Subtotal,
]
.into_iter()
}
}
impl Function { impl Function {
/// Some functions in Excel like CONCAT are stringified as `_xlfn.CONCAT`. /// Some functions in Excel like CONCAT are stringified as `_xlfn.CONCAT`.
pub fn to_xlsx_string(&self) -> String { pub fn to_xlsx_string(&self) -> String {
@@ -708,7 +909,23 @@ impl fmt::Display for Function {
} }
} }
/// Documentation for one function
pub struct Documentation {
pub name: String,
}
impl Model { impl Model {
/// Produces documentation for all implemented functions
pub fn documentation() -> Vec<Documentation> {
let mut doc = Vec::new();
for function in Function::into_iter() {
doc.push(Documentation {
name: function.to_string(),
});
}
doc
}
pub(crate) fn evaluate_function( pub(crate) fn evaluate_function(
&mut self, &mut self,
kind: &Function, kind: &Function,
@@ -928,3 +1145,66 @@ impl Model {
} }
} }
} }
#[cfg(test)]
mod tests {
use std::{
fs::File,
io::{BufRead, BufReader},
};
use crate::functions::Function;
#[test]
fn function_iterator() {
// This checks that the number of functions in the enum is the same
// as the number of functions in the Iterator.
// This is tricky. In Rust we cannot loop over all the members of an enum.
// There are alternatives like using an external crate like strum.
// But I am not in the mood for that.
// What we do here is read this file , extract the functions in the enum
// and check they are the same as in the iterator
let file = File::open("src/functions/mod.rs").unwrap();
let reader = BufReader::new(file);
let mut start = false;
let mut list = Vec::new();
for line in reader.lines() {
let text = line.unwrap();
let text = text.trim().trim_end_matches(',');
if text == "pub enum Function {" {
start = true;
continue;
}
if start {
if text == "}" {
break;
}
if text.starts_with("//") {
// skip comments
continue;
}
if text.is_empty() {
// skip empty lines
continue;
}
list.push(text.to_owned());
}
}
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
let iter_list = Function::into_iter()
.map(|f| format!("{}", f).replace('.', ""))
.collect::<Vec<_>>();
let len = iter_list.len();
assert_eq!(list.len(), len);
// We still need to check there are no duplicates. This will fail if a function in iter_list
// is included twice and one is missing
for function in list {
assert!(iter_list.contains(&function.to_uppercase()));
}
}
}

View File

@@ -1,12 +0,0 @@
use crate::model::Model;
// The gc (Garbage Collector) cleans up leftover elements in the workbook:
// * Strings that are no longe reachable
// * Styles that are no longer reachable
// * ...
impl Model {
pub(crate) fn gc(&mut self) -> Result<(), String> {
todo!()
}
}

View File

@@ -0,0 +1 @@
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!

View File

@@ -1,20 +1,17 @@
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)] use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)]
pub struct Booleans { pub struct Booleans {
#[serde(rename = "true")] pub r#true: String,
pub true_value: String, pub r#false: String,
#[serde(rename = "false")]
pub false_value: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Errors { pub struct Errors {
#[serde(rename = "ref")] pub r#ref: String,
pub ref_value: String,
pub name: String, pub name: String,
pub value: String, pub value: String,
pub div: String, pub div: String,
@@ -28,14 +25,14 @@ pub struct Errors {
pub null: String, pub null: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Language { pub struct Language {
pub booleans: Booleans, pub booleans: Booleans,
pub errors: Errors, pub errors: Errors,
} }
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| { static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file") bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
}); });
pub fn get_language(id: &str) -> Result<&Language, String> { pub fn get_language(id: &str) -> Result<&Language, String> {

View File

@@ -8,7 +8,7 @@
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [dependencies]
//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"} //! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc" }
//! ``` //! ```
//! //!
//! <small> until version 0.5.0 you should use the git dependencies as stated </small> //! <small> until version 0.5.0 you should use the git dependencies as stated </small>
@@ -31,29 +31,30 @@ pub mod expressions;
pub mod formatter; pub mod formatter;
pub mod language; pub mod language;
pub mod locale; pub mod locale;
pub mod model;
pub mod new_empty; pub mod new_empty;
pub mod number_format; pub mod number_format;
pub mod types; pub mod types;
pub mod worksheet; pub mod worksheet;
mod functions;
mod actions; mod actions;
mod cast; mod cast;
mod constants; mod constants;
mod styles;
mod diffs; mod diffs;
mod functions;
mod implicit_intersection; mod implicit_intersection;
mod model;
mod styles;
mod units; mod units;
mod user_model;
mod utils; mod utils;
mod workbook; mod workbook;
mod garbage_collector;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
#[cfg(test)] #[cfg(test)]
pub mod mock_time; pub mod mock_time;
pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use user_model::UserModel;

BIN
base/src/locale/locales.bin Normal file

Binary file not shown.

View File

@@ -1,32 +1,29 @@
use bitcode::{Decode, Encode};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Locale { pub struct Locale {
pub dates: Dates, pub dates: Dates,
pub numbers: NumbersProperties, pub numbers: NumbersProperties,
pub currency: Currency, pub currency: Currency,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Currency { pub struct Currency {
pub iso: String, pub iso: String,
pub symbol: String, pub symbol: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct NumbersProperties { pub struct NumbersProperties {
#[serde(rename = "symbols-numberSystem-latn")]
pub symbols: NumbersSymbols, pub symbols: NumbersSymbols,
#[serde(rename = "decimalFormats-numberSystem-latn")]
pub decimal_formats: DecimalFormats, pub decimal_formats: DecimalFormats,
#[serde(rename = "currencyFormats-numberSystem-latn")]
pub currency_formats: CurrencyFormats, pub currency_formats: CurrencyFormats,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Dates { pub struct Dates {
pub day_names: Vec<String>, pub day_names: Vec<String>,
pub day_names_short: Vec<String>, pub day_names_short: Vec<String>,
@@ -35,8 +32,7 @@ pub struct Dates {
pub months_letter: Vec<String>, pub months_letter: Vec<String>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
#[serde(rename_all = "camelCase")]
pub struct NumbersSymbols { pub struct NumbersSymbols {
pub decimal: String, pub decimal: String,
pub group: String, pub group: String,
@@ -54,40 +50,26 @@ pub struct NumbersSymbols {
} }
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns // See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct CurrencyFormats { pub struct CurrencyFormats {
pub standard: String, pub standard: String,
#[serde(rename = "standard-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub standard_alpha_next_to_number: Option<String>, pub standard_alpha_next_to_number: Option<String>,
#[serde(rename = "standard-noCurrency")]
pub standard_no_currency: String, pub standard_no_currency: String,
pub accounting: String, pub accounting: String,
#[serde(rename = "accounting-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub accounting_alpha_next_to_number: Option<String>, pub accounting_alpha_next_to_number: Option<String>,
#[serde(rename = "accounting-noCurrency")]
pub accounting_no_currency: String, pub accounting_no_currency: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DecimalFormats { pub struct DecimalFormats {
pub standard: String, pub standard: String,
} }
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| { static LOCALES: Lazy<HashMap<String, Locale>> =
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale") Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
});
pub fn get_locale(_id: &str) -> Result<&Locale, String> { pub fn get_locale(id: &str) -> Result<&Locale, String> {
// TODO: pass the locale once we implement locales in Rust // TODO: pass the locale once we implement locales in Rust
let locale = LOCALES.get("en").ok_or("Invalid locale")?;
Ok(locale)
}
// TODO: Remove this function one we implement locales properly
pub fn get_locale_fix(id: &str) -> Result<&Locale, String> {
let locale = LOCALES.get(id).ok_or("Invalid locale")?; let locale = LOCALES.get(id).ok_or("Invalid locale")?;
Ok(locale) Ok(locale)
} }

View File

@@ -10,7 +10,7 @@ use std::cell::RefCell;
// 8 November 2022 12:13 Berlin time // 8 November 2022 12:13 Berlin time
thread_local! { thread_local! {
static MOCK_TIME: RefCell<i64> = RefCell::new(1667906008578); static MOCK_TIME: RefCell<i64> = const { RefCell::new(1667906008578) };
} }
pub fn get_milliseconds_since_epoch() -> i64 { pub fn get_milliseconds_since_epoch() -> i64 {
@@ -20,3 +20,18 @@ pub fn get_milliseconds_since_epoch() -> i64 {
pub fn set_mock_time(time: i64) { pub fn set_mock_time(time: i64) {
MOCK_TIME.with(|cell| *cell.borrow_mut() = time); MOCK_TIME.with(|cell| *cell.borrow_mut() = time);
} }
#[cfg(test)]
mod tests {
use crate::mock_time::MOCK_TIME;
use super::get_milliseconds_since_epoch;
#[test]
fn mock_time() {
let t = get_milliseconds_since_epoch();
assert_eq!(t, 1667906008578);
MOCK_TIME.with_borrow(|v| assert_eq!(*v, 1667906008578));
}
}

View File

@@ -1,14 +1,5 @@
#![deny(missing_docs)] #![deny(missing_docs)]
//! # Model
//!
//! Note that sheets are 0-indexed and rows and columns are 1-indexed.
//!
//! IronCalc is row first. A cell is referenced by (`sheet`, `row`, `column`)
//!
use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use std::vec::Vec; use std::vec::Vec;
@@ -47,7 +38,11 @@ use chrono_tz::Tz;
#[cfg(test)] #[cfg(test)]
pub use crate::mock_time::get_milliseconds_since_epoch; pub use crate::mock_time::get_milliseconds_since_epoch;
/// wasm implementation for time /// Number of milliseconds since January 1, 1970
/// Used by time and date functions. It takes the value from the environment:
/// * The Operative System
/// * The JavaScript environment
/// * Or mocked for tests
#[cfg(not(test))] #[cfg(not(test))]
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn get_milliseconds_since_epoch() -> i64 { pub fn get_milliseconds_since_epoch() -> i64 {
@@ -58,6 +53,11 @@ pub fn get_milliseconds_since_epoch() -> i64 {
.as_millis() as i64 .as_millis() as i64
} }
/// Number of milliseconds since January 1, 1970
/// Used by time and date functions. It takes the value from the environment:
/// * The Operative System
/// * The JavaScript environment
/// * Or mocked for tests
#[cfg(not(test))] #[cfg(not(test))]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn get_milliseconds_since_epoch() -> i64 { pub fn get_milliseconds_since_epoch() -> i64 {
@@ -67,7 +67,7 @@ pub fn get_milliseconds_since_epoch() -> i64 {
/// A cell might be evaluated or being evaluated /// A cell might be evaluated or being evaluated
#[derive(Clone)] #[derive(Clone)]
pub enum CellState { pub(crate) enum CellState {
/// The cell has already been evaluated /// The cell has already been evaluated
Evaluated, Evaluated,
/// The cell is being evaluated /// The cell is being evaluated
@@ -75,7 +75,7 @@ pub enum CellState {
} }
/// A parsed formula for a defined name /// A parsed formula for a defined name
pub enum ParsedDefinedName { pub(crate) enum ParsedDefinedName {
/// CellReference (`=C4`) /// CellReference (`=C4`)
CellReference(CellReferenceIndex), CellReference(CellReferenceIndex),
/// A Range (`=C4:D6`) /// A Range (`=C4:D6`)
@@ -105,19 +105,21 @@ pub struct Model {
/// A list of parsed formulas /// A list of parsed formulas
pub parsed_formulas: Vec<Vec<Node>>, pub parsed_formulas: Vec<Vec<Node>>,
/// A list of parsed defined names /// A list of parsed defined names
pub parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>, pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
/// An optimization to lookup strings faster /// An optimization to lookup strings faster
pub shared_strings: HashMap<String, usize>, pub(crate) shared_strings: HashMap<String, usize>,
/// An instance of the parser /// An instance of the parser
pub parser: Parser, pub(crate) parser: Parser,
/// The list of cells with formulas that are evaluated of being evaluated /// The list of cells with formulas that are evaluated of being evaluated
pub cells: HashMap<(u32, i32, i32), CellState>, pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The locale of the model /// The locale of the model
pub locale: Locale, pub(crate) locale: Locale,
/// Tha language used /// Tha language used
pub language: Language, pub(crate) language: Language,
/// The timezone used to evaluate the model /// The timezone used to evaluate the model
pub tz: Tz, pub(crate) tz: Tz,
/// The view id. A view consist of a selected sheet and ranges.
pub(crate) view_id: u32,
} }
// FIXME: Maybe this should be the same as CellReference // FIXME: Maybe this should be the same as CellReference
@@ -659,7 +661,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// assert_eq!(model.workbook.worksheet(0)?.color, None); /// assert_eq!(model.workbook.worksheet(0)?.color, None);
@@ -726,7 +728,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// assert_eq!(model.is_empty_cell(0, 1, 1)?, true); /// assert_eq!(model.is_empty_cell(0, 1, 1)?, true);
@@ -798,17 +800,17 @@ impl Model {
None None
} }
/// Returns a model from a String representation of a workbook /// Returns a model from an internal binary representation of a workbook
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # use ironcalc_base::cell::CellValue; /// # use ironcalc_base::cell::CellValue;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// model.set_user_input(0, 1, 1, "Stella!".to_string()); /// model.set_user_input(0, 1, 1, "Stella!".to_string());
/// let model2 = Model::from_json(&model.to_json_str())?; /// let model2 = Model::from_bytes(&model.to_bytes())?;
/// assert_eq!( /// assert_eq!(
/// model2.get_cell_value_by_index(0, 1, 1), /// model2.get_cell_value_by_index(0, 1, 1),
/// Ok(CellValue::String("Stella!".to_string())) /// Ok(CellValue::String("Stella!".to_string()))
@@ -816,9 +818,12 @@ impl Model {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn from_json(s: &str) -> Result<Model, String> { ///
/// See also:
/// * [Model::to_bytes]
pub fn from_bytes(s: &[u8]) -> Result<Model, String> {
let workbook: Workbook = let workbook: Workbook =
serde_json::from_str(s).map_err(|_| "Error parsing workbook".to_string())?; bitcode::decode(s).map_err(|e| format!("Error parsing workbook: {e}"))?;
Model::from_workbook(workbook) Model::from_workbook(workbook)
} }
@@ -827,7 +832,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # use ironcalc_base::cell::CellValue; /// # use ironcalc_base::cell::CellValue;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
@@ -883,6 +888,7 @@ impl Model {
language, language,
locale, locale,
tz, tz,
view_id: 0,
}; };
model.parse_formulas(); model.parse_formulas();
@@ -896,7 +902,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # use ironcalc_base::expressions::types::CellReferenceIndex; /// # use ironcalc_base::expressions::types::CellReferenceIndex;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
@@ -965,7 +971,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex}; /// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
@@ -1036,7 +1042,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
@@ -1083,7 +1089,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # use ironcalc_base::expressions::types::CellReferenceIndex; /// # use ironcalc_base::expressions::types::CellReferenceIndex;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
@@ -1138,13 +1144,13 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
/// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string()); /// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string());
/// model.evaluate(); /// model.evaluate();
/// let result = model.cell_formula(sheet, row, column)?; /// let result = model.get_cell_formula(sheet, row, column)?;
/// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string())); /// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string()));
/// # Ok(()) /// # Ok(())
/// # } /// # }
@@ -1152,24 +1158,33 @@ impl Model {
/// ///
/// See also: /// See also:
/// * [Model::get_cell_content()] /// * [Model::get_cell_content()]
pub fn cell_formula( pub fn get_cell_formula(
&self, &self,
sheet: u32, sheet: u32,
row: i32, row: i32,
column: i32, column: i32,
) -> Result<Option<String>, String> { ) -> Result<Option<String>, String> {
let worksheet = self.workbook.worksheet(sheet)?; let worksheet = self.workbook.worksheet(sheet)?;
Ok(worksheet.cell(row, column).and_then(|cell| { match worksheet.cell(row, column) {
cell.get_formula().map(|formula_index| { Some(cell) => match cell.get_formula() {
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize]; Some(formula_index) => {
let cell_ref = CellReferenceRC { let formula = &self
sheet: worksheet.get_name(), .parsed_formulas
row, .get(sheet as usize)
column, .ok_or("missing sheet")?
}; .get(formula_index as usize)
format!("={}", to_string(formula, &cell_ref)) .ok_or("missing formula")?;
}) let cell_ref = CellReferenceRC {
})) sheet: worksheet.get_name(),
row,
column,
};
Ok(Some(format!("={}", to_string(formula, &cell_ref))))
}
None => Ok(None),
},
None => Ok(None),
}
} }
/// Updates the value of a cell with some text /// Updates the value of a cell with some text
@@ -1178,7 +1193,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
@@ -1221,7 +1236,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
@@ -1258,7 +1273,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
@@ -1296,7 +1311,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
@@ -1348,7 +1363,7 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # use ironcalc_base::cell::CellValue; /// # use ironcalc_base::cell::CellValue;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
@@ -1358,7 +1373,7 @@ impl Model {
/// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string()); /// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string());
/// model.evaluate(); /// model.evaluate();
/// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0))); /// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0)));
/// assert_eq!(model.formatted_cell_value(0, 1, 2), Ok("215$".to_string())); /// assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("215$".to_string()));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -1529,7 +1544,7 @@ impl Model {
/// Returns the cell value for (`sheet`, `row`, `column`) /// Returns the cell value for (`sheet`, `row`, `column`)
/// ///
/// See also: /// See also:
/// * [Model::formatted_cell_value()] /// * [Model::get_formatted_cell_value()]
pub fn get_cell_value_by_index( pub fn get_cell_value_by_index(
&self, &self,
sheet_index: u32, sheet_index: u32,
@@ -1555,35 +1570,42 @@ impl Model {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ironcalc_base::model::Model; /// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?; /// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1); /// let (sheet, row, column) = (0, 1, 1);
/// model.set_user_input(sheet, row, column, "=1/3".to_string()); /// model.set_user_input(sheet, row, column, "=1/3".to_string());
/// model.evaluate(); /// model.evaluate();
/// let result = model.formatted_cell_value(sheet, row, column)?; /// let result = model.get_formatted_cell_value(sheet, row, column)?;
/// assert_eq!(result, "0.333333333".to_string()); /// assert_eq!(result, "0.333333333".to_string());
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn formatted_cell_value( pub fn get_formatted_cell_value(
&self, &self,
sheet_index: u32, sheet_index: u32,
row: i32, row: i32,
column: i32, column: i32,
) -> Result<String, String> { ) -> Result<String, String> {
let format = self.get_style_for_cell(sheet_index, row, column).num_fmt; match self.workbook.worksheet(sheet_index)?.cell(row, column) {
let cell = self Some(cell) => {
.workbook let format = self.get_style_for_cell(sheet_index, row, column).num_fmt;
.worksheet(sheet_index)? let formatted_value =
.cell(row, column) cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| {
.cloned() format_number(value, &format, &self.locale).text
.unwrap_or_default(); });
let formatted_value = Ok(formatted_value)
cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| { }
format_number(value, &format, &self.locale).text None => Ok("".to_string()),
}); }
Ok(formatted_value) }
/// Return the typeof a cell
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<CellType, String> {
Ok(match self.workbook.worksheet(sheet)?.cell(row, column) {
Some(c) => c.get_type(),
None => CellType::Number,
})
} }
/// Returns a string with the cell content. If there is a formula returns the formula /// Returns a string with the cell content. If there is a formula returns the formula
@@ -1647,15 +1669,53 @@ impl Model {
} }
} }
/// Sets cell to empty. Can be used to delete value without affecting style. /// Removes the content of the cell but leaves the style.
pub fn set_cell_empty(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { ///
let worksheet = self.workbook.worksheet_mut(sheet)?; /// See also:
worksheet.set_cell_empty(row, column); /// * [Model::cell_clear_all()]
///
/// # Examples
///
/// ```rust
/// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1);
/// model.set_user_input(sheet, row, column, "100$".to_string());
/// model.cell_clear_contents(sheet, row, column);
/// model.set_user_input(sheet, row, column, "10".to_string());
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
/// assert_eq!(result, "10$".to_string());
/// # Ok(())
/// # }
/// ```
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
self.workbook
.worksheet_mut(sheet)?
.cell_clear_contents(row, column);
Ok(()) Ok(())
} }
/// Deletes a cell by removing it from worksheet data. /// Deletes a cell by removing it from worksheet data. All content and style is removed.
pub fn delete_cell(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { ///
/// See also:
/// * [Model::cell_clear_contents()]
///
/// # Examples
///
/// ```rust
/// # use ironcalc_base::Model;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = Model::new_empty("model", "en", "UTC")?;
/// let (sheet, row, column) = (0, 1, 1);
/// model.set_user_input(sheet, row, column, "100$".to_string());
/// model.cell_clear_all(sheet, row, column);
/// model.set_user_input(sheet, row, column, "10".to_string());
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
/// assert_eq!(result, "10".to_string());
/// # Ok(())
/// # }
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?; let worksheet = self.workbook.worksheet_mut(sheet)?;
let sheet_data = &mut worksheet.sheet_data; let sheet_data = &mut worksheet.sheet_data;
@@ -1682,9 +1742,8 @@ impl Model {
if r.r == row { if r.r == row {
if r.custom_format { if r.custom_format {
return r.s; return r.s;
} else {
break;
} }
break;
} }
} }
let cols = &self.workbook.worksheets[sheet as usize].cols; let cols = &self.workbook.worksheets[sheet as usize].cols;
@@ -1707,19 +1766,30 @@ impl Model {
.get_style(self.get_cell_style_index(sheet, row, column)) .get_style(self.get_cell_style_index(sheet, row, column))
} }
/// Returns a JSON string of the workbook /// Returns an internal binary representation of the workbook
pub fn to_json_str(&self) -> String { ///
match serde_json::to_string(&self.workbook) { /// See also:
Ok(s) => s, /// * [Model::from_bytes]
Err(_) => { pub fn to_bytes(&self) -> Vec<u8> {
// TODO, is this branch possible at all? bitcode::encode(&self.workbook)
json!({"error": "Error stringifying workbook"}).to_string() }
}
} /// Returns data about the worksheets
pub fn get_worksheets_properties(&self) -> Vec<SheetProperties> {
self.workbook
.worksheets
.iter()
.map(|worksheet| SheetProperties {
name: worksheet.get_name(),
state: worksheet.state.to_string(),
color: worksheet.color.clone(),
sheet_id: worksheet.sheet_id,
})
.collect()
} }
/// Returns markup representation of the given `sheet`. /// Returns markup representation of the given `sheet`.
pub fn sheet_markup(&self, sheet: u32) -> Result<String, String> { pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
let worksheet = self.workbook.worksheet(sheet)?; let worksheet = self.workbook.worksheet(sheet)?;
let dimension = worksheet.dimension(); let dimension = worksheet.dimension();
@@ -1729,9 +1799,9 @@ impl Model {
let mut row_markup: Vec<String> = Vec::new(); let mut row_markup: Vec<String> = Vec::new();
for column in 1..(dimension.max_column + 1) { for column in 1..(dimension.max_column + 1) {
let mut cell_markup = match self.cell_formula(sheet, row, column)? { let mut cell_markup = match self.get_cell_formula(sheet, row, column)? {
Some(formula) => formula, Some(formula) => formula,
None => self.formatted_cell_value(sheet, row, column)?, None => self.get_formatted_cell_value(sheet, row, column)?,
}; };
let style = self.get_style_for_cell(sheet, row, column); let style = self.get_style_for_cell(sheet, row, column);
if style.font.b { if style.font.b {
@@ -1770,7 +1840,7 @@ impl Model {
} }
/// Returns the number of frozen rows in `sheet` /// Returns the number of frozen rows in `sheet`
pub fn get_frozen_rows(&self, sheet: u32) -> Result<i32, String> { pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, String> {
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) { if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
Ok(worksheet.frozen_rows) Ok(worksheet.frozen_rows)
} else { } else {
@@ -1779,7 +1849,7 @@ impl Model {
} }
/// Return the number of frozen columns in `sheet` /// Return the number of frozen columns in `sheet`
pub fn get_frozen_columns(&self, sheet: u32) -> Result<i32, String> { pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, String> {
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) { if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
Ok(worksheet.frozen_columns) Ok(worksheet.frozen_columns)
} else { } else {
@@ -1820,6 +1890,34 @@ impl Model {
Err("Invalid sheet".to_string()) Err("Invalid sheet".to_string())
} }
} }
/// Returns the width of a column
#[inline]
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
self.workbook.worksheet(sheet)?.get_column_width(column)
}
/// Sets the width of a column
#[inline]
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), String> {
self.workbook
.worksheet_mut(sheet)?
.set_column_width(column, width)
}
/// Returns the height of a row
#[inline]
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
self.workbook.worksheet(sheet)?.row_height(row)
}
/// Sets the height of a row
#[inline]
pub fn set_row_height(&mut self, sheet: u32, column: i32, height: f64) -> Result<(), String> {
self.workbook
.worksheet_mut(sheet)?
.set_row_height(column, height)
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,4 +1,4 @@
use chrono::NaiveDateTime; use chrono::DateTime;
use std::collections::HashMap; use std::collections::HashMap;
@@ -6,14 +6,18 @@ use crate::{
calc_result::Range, calc_result::Range,
expressions::{ expressions::{
lexer::LexerMode, lexer::LexerMode,
parser::stringify::{rename_sheet_in_node, to_rc_format}, parser::{
parser::Parser, stringify::{rename_sheet_in_node, to_rc_format},
Parser,
},
types::CellReferenceRC, types::CellReferenceRC,
}, },
language::get_language, language::get_language,
locale::get_locale, locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName}, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet}, types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
},
utils::ParsedReference, utils::ParsedReference,
}; };
@@ -33,7 +37,20 @@ fn is_valid_sheet_name(name: &str) -> bool {
impl Model { impl Model {
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists /// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet { fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
let mut views = HashMap::new();
for id in view_ids {
views.insert(
**id,
WorksheetView {
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1,
},
);
}
Worksheet { Worksheet {
cols: vec![], cols: vec![],
rows: vec![], rows: vec![],
@@ -48,6 +65,7 @@ impl Model {
color: Default::default(), color: Default::default(),
frozen_columns: 0, frozen_columns: 0,
frozen_rows: 0, frozen_rows: 0,
views,
} }
} }
@@ -122,8 +140,8 @@ impl Model {
self.parsed_defined_names = parsed_defined_names; self.parsed_defined_names = parsed_defined_names;
} }
// Reparses all formulas and defined names /// Reparses all formulas and defined names
fn reset_parsed_structures(&mut self) { pub(crate) fn reset_parsed_structures(&mut self) {
self.parser self.parser
.set_worksheets(self.workbook.get_worksheet_names()); .set_worksheets(self.workbook.get_worksheet_names());
self.parsed_formulas = vec![]; self.parsed_formulas = vec![];
@@ -134,10 +152,10 @@ impl Model {
} }
/// Adds a sheet with a automatically generated name /// Adds a sheet with a automatically generated name
pub fn new_sheet(&mut self) { pub fn new_sheet(&mut self) -> (String, u32) {
// First we find a name // First we find a name
// TODO: When/if we support i18n the name could depend on the locale // TODO: The name should depend on the locale
let base_name = "Sheet"; let base_name = "Sheet";
let base_name_uppercase = base_name.to_uppercase(); let base_name_uppercase = base_name.to_uppercase();
let mut index = 1; let mut index = 1;
@@ -153,9 +171,11 @@ impl Model {
let sheet_name = format!("{}{}", base_name, index); let sheet_name = format!("{}{}", base_name, index);
// Now we need a sheet_id // Now we need a sheet_id
let sheet_id = self.get_new_sheet_id(); let sheet_id = self.get_new_sheet_id();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id); let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
self.workbook.worksheets.push(worksheet); self.workbook.worksheets.push(worksheet);
self.reset_parsed_structures(); self.reset_parsed_structures();
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
} }
/// Inserts a sheet with a particular index /// Inserts a sheet with a particular index
@@ -183,7 +203,8 @@ impl Model {
Some(id) => id, Some(id) => id,
None => self.get_new_sheet_id(), None => self.get_new_sheet_id(),
}; };
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id); let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
if sheet_index as usize > self.workbook.worksheets.len() { if sheet_index as usize > self.workbook.worksheets.len() {
return Err("Sheet index out of range".to_string()); return Err("Sheet index out of range".to_string());
} }
@@ -223,10 +244,10 @@ impl Model {
new_name: &str, new_name: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(new_name) { if !is_valid_sheet_name(new_name) {
return Err(format!("Invalid name for a sheet: '{}'", new_name)); return Err(format!("Invalid name for a sheet: '{}'.", new_name));
} }
if self.get_sheet_index_by_name(new_name).is_some() { if self.get_sheet_index_by_name(new_name).is_some() {
return Err(format!("Sheet already exists: '{}'", new_name)); return Err(format!("Sheet already exists: '{}'.", new_name));
} }
let worksheets = &self.workbook.worksheets; let worksheets = &self.workbook.worksheets;
let sheet_count = worksheets.len() as u32; let sheet_count = worksheets.len() as u32;
@@ -270,7 +291,7 @@ impl Model {
if sheet_count == 1 { if sheet_count == 1 {
return Err("Cannot delete only sheet".to_string()); return Err("Cannot delete only sheet".to_string());
}; };
if sheet_index > sheet_count { if sheet_index >= sheet_count {
return Err("Sheet index too large".to_string()); return Err("Sheet index too large".to_string());
} }
self.workbook.worksheets.remove(sheet_index as usize); self.workbook.worksheets.remove(sheet_index as usize);
@@ -323,18 +344,21 @@ impl Model {
let milliseconds = get_milliseconds_since_epoch(); let milliseconds = get_milliseconds_since_epoch();
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) { let dt = match DateTime::from_timestamp(seconds, 0) {
Some(s) => s, Some(s) => s,
None => return Err(format!("Invalid timestamp: {}", milliseconds)), None => return Err(format!("Invalid timestamp: {}", milliseconds)),
}; };
// "2020-08-06T21:20:53Z // "2020-08-06T21:20:53Z
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
let mut views = HashMap::new();
views.insert(0, WorkbookView { sheet: 0 });
// String versions of the locale are added here to simplify the serialize/deserialize logic // String versions of the locale are added here to simplify the serialize/deserialize logic
let workbook = Workbook { let workbook = Workbook {
shared_strings: vec![], shared_strings: vec![],
defined_names: vec![], defined_names: vec![],
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)], worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
styles: Default::default(), styles: Default::default(),
name: name.to_string(), name: name.to_string(),
settings: WorkbookSettings { settings: WorkbookSettings {
@@ -350,6 +374,7 @@ impl Model {
last_modified: now, last_modified: now,
}, },
tables: HashMap::new(), tables: HashMap::new(),
views,
}; };
let parsed_formulas = Vec::new(); let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets; let worksheets = &workbook.worksheets;
@@ -370,6 +395,7 @@ impl Model {
locale, locale,
language, language,
tz, tz,
view_id: 0,
}; };
model.parse_formulas(); model.parse_formulas();
Ok(model) Ok(model)

View File

@@ -76,10 +76,16 @@ fn fn_imconjugate() {
fn fn_imcos() { fn fn_imcos() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", r#"=IMCOS("4+3i")"#); model._set("A1", r#"=IMCOS("4+3i")"#);
// In macos non intel this is "-6.58066304055116+7.58155274274655i"
model._set("A2", r#"=COMPLEX(-6.58066304055116, 7.58155274274654)"#);
model._set("A3", r#"=IMABS(IMSUB(A1, A2)) < G1"#);
// small number
model._set("G1", "0.0000001");
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("A1"), "-6.58066304055116+7.58155274274654i"); assert_eq!(model._get_text("A3"), "TRUE");
} }
#[test] #[test]

View File

@@ -1,6 +1,7 @@
mod test_actions; mod test_actions;
mod test_binary_search; mod test_binary_search;
mod test_cell; mod test_cell;
mod test_cell_clear_contents;
mod test_circular_references; mod test_circular_references;
mod test_column_width; mod test_column_width;
mod test_criteria; mod test_criteria;
@@ -28,9 +29,8 @@ mod test_frozen_rows_columns;
mod test_general; mod test_general;
mod test_math; mod test_math;
mod test_metadata; mod test_metadata;
mod test_model_delete_cell; mod test_model_cell_clear_all;
mod test_model_is_empty_cell; mod test_model_is_empty_cell;
mod test_model_set_cell_empty;
mod test_move_formula; mod test_move_formula;
mod test_quote_prefix; mod test_quote_prefix;
mod test_set_user_input; mod test_set_user_input;
@@ -53,4 +53,5 @@ mod test_frozen_rows_and_columns;
mod test_get_cell_content; mod test_get_cell_content;
mod test_percentage; mod test_percentage;
mod test_today; mod test_today;
mod test_gc; mod test_types;
mod user_model;

View File

@@ -3,6 +3,7 @@
use crate::constants::LAST_COLUMN; use crate::constants::LAST_COLUMN;
use crate::model::Model; use crate::model::Model;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::types::Col;
#[test] #[test]
fn test_insert_columns() { fn test_insert_columns() {
@@ -195,6 +196,250 @@ fn test_delete_columns() {
assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)"); assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)");
} }
#[test]
fn test_delete_column_width() {
let mut model = new_empty_model();
let (sheet, column) = (0, 5);
let normal_width = model.get_column_width(sheet, column).unwrap();
// Set the width of one column to 5 times the normal width
assert!(model
.set_column_width(sheet, column, normal_width * 5.0)
.is_ok());
// delete it
assert!(model.delete_columns(sheet, column, 1).is_ok());
// all the columns around have the expected width
assert_eq!(
model.get_column_width(sheet, column - 1).unwrap(),
normal_width
);
assert_eq!(model.get_column_width(sheet, column).unwrap(), normal_width);
assert_eq!(
model.get_column_width(sheet, column + 1).unwrap(),
normal_width
);
}
#[test]
// We set the style of columns 4 to 7 and delete column 4
// We check that columns 4 to 6 have the new style
fn test_delete_first_column_width() {
let mut model = new_empty_model();
model.workbook.worksheets[0].cols = vec![Col {
min: 4,
max: 7,
width: 300.0,
custom_width: true,
style: None,
}];
let (sheet, column) = (0, 4);
assert!(model.delete_columns(sheet, column, 1).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 4,
max: 6,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
// Delete the last column in the range
fn test_delete_last_column_width() {
let mut model = new_empty_model();
model.workbook.worksheets[0].cols = vec![Col {
min: 4,
max: 7,
width: 300.0,
custom_width: true,
style: None,
}];
let (sheet, column) = (0, 7);
assert!(model.delete_columns(sheet, column, 1).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 4,
max: 6,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
// Deletes columns at the end
fn test_delete_last_few_columns_width() {
let mut model = new_empty_model();
model.workbook.worksheets[0].cols = vec![Col {
min: 4,
max: 17,
width: 300.0,
custom_width: true,
style: None,
}];
let (sheet, column) = (0, 13);
assert!(model.delete_columns(sheet, column, 10).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 4,
max: 12,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
fn test_delete_columns_non_overlapping_left() {
let mut model = new_empty_model();
model.workbook.worksheets[0].cols = vec![Col {
min: 10,
max: 17,
width: 300.0,
custom_width: true,
style: None,
}];
let (sheet, column) = (0, 3);
assert!(model.delete_columns(sheet, column, 4).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 6,
max: 13,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
fn test_delete_columns_overlapping_left() {
let mut model = new_empty_model();
model.workbook.worksheets[0].cols = vec![Col {
min: 10,
max: 20,
width: 300.0,
custom_width: true,
style: None,
}];
let (sheet, column) = (0, 8);
assert!(model.delete_columns(sheet, column, 4).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 8,
max: 16,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
fn test_delete_columns_non_overlapping_right() {
let mut model = new_empty_model();
model.workbook.worksheets[0].cols = vec![Col {
min: 10,
max: 17,
width: 300.0,
custom_width: true,
style: None,
}];
let (sheet, column) = (0, 23);
assert!(model.delete_columns(sheet, column, 4).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 10,
max: 17,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
// deletes some columns in the middle of the range
fn test_delete_middle_column_width() {
let mut model = new_empty_model();
// styled columns [4, 17]
model.workbook.worksheets[0].cols = vec![Col {
min: 4,
max: 17,
width: 300.0,
custom_width: true,
style: None,
}];
// deletes columns 10, 11, 12
let (sheet, column) = (0, 10);
assert!(model.delete_columns(sheet, column, 3).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0],
Col {
min: 4,
max: 14,
width: 300.0,
custom_width: true,
style: None
}
);
}
#[test]
// the range is inside the deleted columns
fn delete_range_in_columns() {
let mut model = new_empty_model();
// styled columns [6, 10]
model.workbook.worksheets[0].cols = vec![Col {
min: 6,
max: 10,
width: 300.0,
custom_width: true,
style: None,
}];
// deletes columns [4, 17]
let (sheet, column) = (0, 4);
assert!(model.delete_columns(sheet, column, 8).is_ok());
let cols = &model.workbook.worksheets[0].cols;
assert_eq!(cols.len(), 0);
}
#[test]
fn test_delete_columns_error() {
let mut model = new_empty_model();
let (sheet, column) = (0, 5);
assert!(model.delete_columns(sheet, column, -1).is_err());
assert!(model.delete_columns(sheet, column, 0).is_err());
assert!(model.delete_columns(sheet, column, 1).is_ok());
}
#[test] #[test]
fn test_delete_rows() { fn test_delete_rows() {
let mut model = new_empty_model(); let mut model = new_empty_model();

View File

@@ -2,25 +2,25 @@
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
#[test] #[test]
fn test_set_cell_empty_non_existing_sheet() { fn test_cell_clear_contents_non_existing_sheet() {
let mut model = new_empty_model(); let mut model = new_empty_model();
assert_eq!( assert_eq!(
model.set_cell_empty(13, 1, 1), model.cell_clear_contents(13, 1, 1),
Err("Invalid sheet index".to_string()) Err("Invalid sheet index".to_string())
); );
} }
#[test] #[test]
fn test_set_cell_empty_unset_cell() { fn test_cell_clear_contents_unset_cell() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model.set_cell_empty(0, 1, 1).unwrap(); model.cell_clear_contents(0, 1, 1).unwrap();
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true));
model.evaluate(); model.evaluate();
assert_eq!(model._get_text_at(0, 1, 1), ""); assert_eq!(model._get_text_at(0, 1, 1), "");
} }
#[test] #[test]
fn test_set_cell_empty_with_value() { fn test_cell_clear_contents_with_value() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", "hello"); model._set("A1", "hello");
model.evaluate(); model.evaluate();
@@ -28,7 +28,7 @@ fn test_set_cell_empty_with_value() {
assert_eq!(model._get_text_at(0, 1, 1), "hello"); assert_eq!(model._get_text_at(0, 1, 1), "hello");
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
model.set_cell_empty(0, 1, 1).unwrap(); model.cell_clear_contents(0, 1, 1).unwrap();
model.evaluate(); model.evaluate();
assert_eq!(model._get_text_at(0, 1, 1), ""); assert_eq!(model._get_text_at(0, 1, 1), "");
@@ -36,7 +36,7 @@ fn test_set_cell_empty_with_value() {
} }
#[test] #[test]
fn test_set_cell_empty_referenced_elsewhere() { fn test_cell_clear_contents_referenced_elsewhere() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", "35"); model._set("A1", "35");
model._set("A2", "=2*A1"); model._set("A2", "=2*A1");
@@ -47,7 +47,7 @@ fn test_set_cell_empty_referenced_elsewhere() {
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
model.set_cell_empty(0, 1, 1).unwrap(); model.cell_clear_contents(0, 1, 1).unwrap();
model.evaluate(); model.evaluate();
assert_eq!(model._get_text_at(0, 1, 1), ""); assert_eq!(model._get_text_at(0, 1, 1), "");

View File

@@ -23,9 +23,9 @@ fn test_column_width() {
.unwrap(); .unwrap();
assert_eq!(model.workbook.worksheets[0].cols.len(), 3); assert_eq!(model.workbook.worksheets[0].cols.len(), 3);
let worksheet = model.workbook.worksheet(0).unwrap(); let worksheet = model.workbook.worksheet(0).unwrap();
assert!((worksheet.column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); assert!((worksheet.get_column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
assert!((worksheet.column_width(2).unwrap() - 30.0).abs() < f64::EPSILON); assert!((worksheet.get_column_width(2).unwrap() - 30.0).abs() < f64::EPSILON);
assert!((worksheet.column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); assert!((worksheet.get_column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
assert_eq!(model.get_cell_style_index(0, 23, 2), 6); assert_eq!(model.get_cell_style_index(0, 23, 2), 6);
} }
@@ -48,9 +48,11 @@ fn test_column_width_lower_edge() {
.unwrap(); .unwrap();
assert_eq!(model.workbook.worksheets[0].cols.len(), 2); assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
let worksheet = model.workbook.worksheet(0).unwrap(); let worksheet = model.workbook.worksheet(0).unwrap();
assert!((worksheet.column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); assert!((worksheet.get_column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
assert!((worksheet.column_width(5).unwrap() - 30.0).abs() < f64::EPSILON); assert!((worksheet.get_column_width(5).unwrap() - 30.0).abs() < f64::EPSILON);
assert!((worksheet.column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON); assert!(
(worksheet.get_column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
);
assert_eq!(model.get_cell_style_index(0, 23, 5), 1); assert_eq!(model.get_cell_style_index(0, 23, 5), 1);
} }
@@ -74,9 +76,9 @@ fn test_column_width_higher_edge() {
assert_eq!(model.workbook.worksheets[0].cols.len(), 2); assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
let worksheet = model.workbook.worksheet(0).unwrap(); let worksheet = model.workbook.worksheet(0).unwrap();
assert!( assert!(
(worksheet.column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON (worksheet.get_column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
); );
assert!((worksheet.column_width(16).unwrap() - 30.0).abs() < f64::EPSILON); assert!((worksheet.get_column_width(16).unwrap() - 30.0).abs() < f64::EPSILON);
assert!((worksheet.column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
assert_eq!(model.get_cell_style_index(0, 23, 16), 1); assert_eq!(model.get_cell_style_index(0, 23, 16), 1);
} }

View File

@@ -8,34 +8,37 @@ use crate::{
#[test] #[test]
fn test_empty_model() { fn test_empty_model() {
let mut model = new_empty_model(); let mut model = new_empty_model();
assert_eq!(model.get_frozen_rows(0), Ok(0)); assert_eq!(model.get_frozen_rows_count(0), Ok(0));
assert_eq!(model.get_frozen_columns(0), Ok(0)); assert_eq!(model.get_frozen_columns_count(0), Ok(0));
let e = model.set_frozen_rows(0, 3); let e = model.set_frozen_rows(0, 3);
assert!(e.is_ok()); assert!(e.is_ok());
assert_eq!(model.get_frozen_rows(0), Ok(3)); assert_eq!(model.get_frozen_rows_count(0), Ok(3));
assert_eq!(model.get_frozen_columns(0), Ok(0)); assert_eq!(model.get_frozen_columns_count(0), Ok(0));
let e = model.set_frozen_columns(0, 53); let e = model.set_frozen_columns(0, 53);
assert!(e.is_ok()); assert!(e.is_ok());
assert_eq!(model.get_frozen_rows(0), Ok(3)); assert_eq!(model.get_frozen_rows_count(0), Ok(3));
assert_eq!(model.get_frozen_columns(0), Ok(53)); assert_eq!(model.get_frozen_columns_count(0), Ok(53));
// Set them back to zero // Set them back to zero
let e = model.set_frozen_rows(0, 0); let e = model.set_frozen_rows(0, 0);
assert!(e.is_ok()); assert!(e.is_ok());
let e = model.set_frozen_columns(0, 0); let e = model.set_frozen_columns(0, 0);
assert!(e.is_ok()); assert!(e.is_ok());
assert_eq!(model.get_frozen_rows(0), Ok(0)); assert_eq!(model.get_frozen_rows_count(0), Ok(0));
assert_eq!(model.get_frozen_columns(0), Ok(0)); assert_eq!(model.get_frozen_columns_count(0), Ok(0));
} }
#[test] #[test]
fn test_invalid_sheet() { fn test_invalid_sheet() {
let mut model = new_empty_model(); let mut model = new_empty_model();
assert_eq!(model.get_frozen_rows(1), Err("Invalid sheet".to_string()));
assert_eq!( assert_eq!(
model.get_frozen_columns(3), model.get_frozen_rows_count(1),
Err("Invalid sheet".to_string())
);
assert_eq!(
model.get_frozen_columns_count(3),
Err("Invalid sheet".to_string()) Err("Invalid sheet".to_string())
); );

View File

@@ -1,22 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn test_empty_model() {
let mut model = new_empty_model();
// set a string
model._set("A1", "Hello");
assert_eq!(model.shared_strings.len(), 1);
// calling the gc doesn't do anything
model.gc().unwrap();
assert_eq!(model.shared_strings.len(), 1);
// If we delete the cell the string is still in the list
model.delete_cell(0, 1, 1).unwrap();
assert_eq!(model.shared_strings.len(), 1);
// after the gc the string is no longer present
model.gc().unwrap();
assert_eq!(model.shared_strings.len(), 0);
}

View File

@@ -411,11 +411,11 @@ fn test_get_formatted_cell_value() {
model.evaluate(); model.evaluate();
assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "foobar"); assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "foobar");
assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "TRUE"); assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "TRUE");
assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), ""); assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "");
assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "123.456"); assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "123.456");
assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "$123.46"); assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "$123.46");
} }
#[test] #[test]
@@ -426,20 +426,20 @@ fn test_cell_formula() {
model.evaluate(); model.evaluate();
assert_eq!( assert_eq!(
model.cell_formula(0, 1, 1), // A1 model.get_cell_formula(0, 1, 1), // A1
Ok(Some("=1+2+3".to_string())), Ok(Some("=1+2+3".to_string())),
); );
assert_eq!( assert_eq!(
model.cell_formula(0, 2, 1), // A2 model.get_cell_formula(0, 2, 1), // A2
Ok(None), Ok(None),
); );
assert_eq!( assert_eq!(
model.cell_formula(0, 3, 1), // A3 - empty cell model.get_cell_formula(0, 3, 1), // A3 - empty cell
Ok(None), Ok(None),
); );
assert_eq!( assert_eq!(
model.cell_formula(42, 1, 1), model.get_cell_formula(42, 1, 1),
Err("Invalid sheet index".to_string()), Err("Invalid sheet index".to_string()),
); );
} }
@@ -453,16 +453,16 @@ fn test_xlfn() {
model.evaluate(); model.evaluate();
// Only modern formulas strip the '_xlfn.' // Only modern formulas strip the '_xlfn.'
assert_eq!( assert_eq!(
model.cell_formula(0, 1, 1).unwrap(), model.get_cell_formula(0, 1, 1).unwrap(),
Some("=_xlfn.SIN(1)".to_string()) Some("=_xlfn.SIN(1)".to_string())
); );
// unknown formulas keep the '_xlfn.' prefix // unknown formulas keep the '_xlfn.' prefix
assert_eq!( assert_eq!(
model.cell_formula(0, 2, 1).unwrap(), model.get_cell_formula(0, 2, 1).unwrap(),
Some("=_xlfn.SINY(1)".to_string()) Some("=_xlfn.SINY(1)".to_string())
); );
assert_eq!( assert_eq!(
model.cell_formula(0, 3, 1).unwrap(), model.get_cell_formula(0, 3, 1).unwrap(),
Some("=CONCAT(3,4)".to_string()) Some("=CONCAT(3,4)".to_string())
); );
} }
@@ -474,11 +474,11 @@ fn test_letter_case() {
model._set("A2", "=sIn(2)"); model._set("A2", "=sIn(2)");
model.evaluate(); model.evaluate();
assert_eq!( assert_eq!(
model.cell_formula(0, 1, 1).unwrap(), model.get_cell_formula(0, 1, 1).unwrap(),
Some("=SIN(1)".to_string()) Some("=SIN(1)".to_string())
); );
assert_eq!( assert_eq!(
model.cell_formula(0, 2, 1).unwrap(), model.get_cell_formula(0, 2, 1).unwrap(),
Some("=SIN(2)".to_string()) Some("=SIN(2)".to_string())
); );
} }

View File

@@ -2,22 +2,22 @@
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
#[test] #[test]
fn test_delete_cell_non_existing_sheet() { fn test_cell_clear_all_non_existing_sheet() {
let mut model = new_empty_model(); let mut model = new_empty_model();
assert_eq!( assert_eq!(
model.delete_cell(13, 1, 1), model.cell_clear_all(13, 1, 1),
Err("Invalid sheet index".to_string()) Err("Invalid sheet index".to_string())
); );
} }
#[test] #[test]
fn test_delete_cell_unset_cell() { fn test_cell_clear_all_unset_cell() {
let mut model = new_empty_model(); let mut model = new_empty_model();
assert!(model.delete_cell(0, 1, 1).is_ok()); assert!(model.cell_clear_all(0, 1, 1).is_ok());
} }
#[test] #[test]
fn test_delete_cell_with_value() { fn test_cell_clear_all_with_value() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", "hello"); model._set("A1", "hello");
model.evaluate(); model.evaluate();
@@ -25,7 +25,7 @@ fn test_delete_cell_with_value() {
assert_eq!(model._get_text_at(0, 1, 1), "hello"); assert_eq!(model._get_text_at(0, 1, 1), "hello");
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
model.delete_cell(0, 1, 1).unwrap(); model.cell_clear_all(0, 1, 1).unwrap();
model.evaluate(); model.evaluate();
assert_eq!(model._get_text_at(0, 1, 1), ""); assert_eq!(model._get_text_at(0, 1, 1), "");
@@ -33,7 +33,7 @@ fn test_delete_cell_with_value() {
} }
#[test] #[test]
fn test_delete_cell_referenced_elsewhere() { fn test_cell_clear_all_referenced_elsewhere() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", "35"); model._set("A1", "35");
model._set("A2", "=2*A1"); model._set("A2", "=2*A1");
@@ -44,7 +44,7 @@ fn test_delete_cell_referenced_elsewhere() {
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
model.delete_cell(0, 1, 1).unwrap(); model.cell_clear_all(0, 1, 1).unwrap();
model.evaluate(); model.evaluate();
assert_eq!(model._get_text_at(0, 1, 1), ""); assert_eq!(model._get_text_at(0, 1, 1), "");

View File

@@ -16,7 +16,7 @@ fn test_is_empty_cell() {
assert!(model.is_empty_cell(0, 3, 1).unwrap()); assert!(model.is_empty_cell(0, 3, 1).unwrap());
model.set_user_input(0, 3, 1, "Hello World".to_string()); model.set_user_input(0, 3, 1, "Hello World".to_string());
assert!(!model.is_empty_cell(0, 3, 1).unwrap()); assert!(!model.is_empty_cell(0, 3, 1).unwrap());
model.set_cell_empty(0, 3, 1).unwrap(); model.cell_clear_contents(0, 3, 1).unwrap();
assert!(model.is_empty_cell(0, 3, 1).unwrap()); assert!(model.is_empty_cell(0, 3, 1).unwrap());
} }

View File

@@ -21,7 +21,7 @@ fn test_sheet_markup() {
model.set_cell_style(0, 4, 1, &style).unwrap(); model.set_cell_style(0, 4, 1, &style).unwrap();
assert_eq!( assert_eq!(
model.sheet_markup(0), model.get_sheet_markup(0),
Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()), Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()),
) )
} }

View File

@@ -236,3 +236,11 @@ fn test_delete_sheet_by_index() {
assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]); assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]);
assert_eq!(model._get_text("Sheet2!A1"), "#REF!"); assert_eq!(model._get_text("Sheet2!A1"), "#REF!");
} }
#[test]
fn delete_sheet_error() {
let mut model = new_empty_model();
model.new_sheet();
assert!(model.delete_sheet(2).is_err());
assert!(model.delete_sheet(1).is_ok());
}

View File

@@ -0,0 +1,24 @@
#![allow(clippy::unwrap_used)]
use crate::types::{Alignment, HorizontalAlignment, VerticalAlignment};
#[test]
fn alignment_default() {
let alignment = Alignment::default();
assert_eq!(
alignment,
Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Bottom,
wrap_text: false
}
);
let s = serde_json::to_string(&alignment).unwrap();
// defaults stringifies as an empty object
assert_eq!(s, "{}");
let a: Alignment = serde_json::from_str("{}").unwrap();
assert_eq!(a, alignment)
}

View File

@@ -1,14 +1,14 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{test::util::new_empty_model, types::SheetInfo}; use crate::{test::util::new_empty_model, types::SheetProperties};
#[test] #[test]
fn workbook_worksheets_info() { fn workbook_worksheets_info() {
let model = new_empty_model(); let model = new_empty_model();
let sheets_info = model.workbook.get_worksheets_info(); let sheets_info = model.get_worksheets_properties();
assert_eq!( assert_eq!(
sheets_info[0], sheets_info[0],
SheetInfo { SheetProperties {
name: "Sheet1".to_string(), name: "Sheet1".to_string(),
state: "visible".to_string(), state: "visible".to_string(),
sheet_id: 1, sheet_id: 1,

View File

@@ -39,7 +39,7 @@ fn test_worksheet_dimension_single_cell() {
fn test_worksheet_dimension_single_cell_set_empty() { fn test_worksheet_dimension_single_cell_set_empty() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("W11", "1"); model._set("W11", "1");
model.set_cell_empty(0, 11, 23).unwrap(); model.cell_clear_contents(0, 11, 23).unwrap();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().dimension(),
WorksheetDimension { WorksheetDimension {
@@ -55,7 +55,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
fn test_worksheet_dimension_single_cell_deleted() { fn test_worksheet_dimension_single_cell_deleted() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("W11", "1"); model._set("W11", "1");
model.delete_cell(0, 11, 23).unwrap(); model.cell_clear_all(0, 11, 23).unwrap();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().dimension(),
WorksheetDimension { WorksheetDimension {
@@ -75,7 +75,7 @@ fn test_worksheet_dimension_multiple_cells() {
model._set("AA17", "1"); model._set("AA17", "1");
model._set("G17", "1"); model._set("G17", "1");
model._set("B19", "1"); model._set("B19", "1");
model.delete_cell(0, 11, 23).unwrap(); model.cell_clear_all(0, 11, 23).unwrap();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().dimension(),
WorksheetDimension { WorksheetDimension {

View File

@@ -0,0 +1,11 @@
mod test_add_delete_sheets;
mod test_clear_cells;
mod test_diff_queue;
mod test_evaluation;
mod test_general;
mod test_rename_sheet;
mod test_row_column;
mod test_styles;
mod test_to_from_bytes;
mod test_undo_redo;
mod test_view;

View File

@@ -0,0 +1,84 @@
#![allow(clippy::unwrap_used)]
use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel};
#[test]
fn add_undo_redo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.new_sheet();
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
model
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
.unwrap();
model.new_sheet();
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
model.undo().unwrap();
model.undo().unwrap();
assert!(model.get_formatted_cell_value(2, 1, 1).is_err());
model.redo().unwrap();
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
model.delete_sheet(1).unwrap();
assert!(!model.can_undo());
assert!(!model.can_redo());
}
#[test]
fn set_sheet_color() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_sheet_color(0, "#343434").unwrap();
let worksheets_properties = model.get_worksheets_properties();
assert_eq!(worksheets_properties.len(), 1);
assert_eq!(worksheets_properties[0].color, Some("#343434".to_owned()));
model.undo().unwrap();
assert_eq!(model.get_worksheets_properties()[0].color, None);
model.redo().unwrap();
assert_eq!(
model.get_worksheets_properties()[0].color,
Some("#343434".to_owned())
);
// changes the color if there is one
model.set_sheet_color(0, "#2534FF").unwrap();
assert_eq!(
model.get_worksheets_properties()[0].color,
Some("#2534FF".to_owned())
);
// Setting it back to none
model.set_sheet_color(0, "").unwrap();
assert_eq!(model.get_worksheets_properties()[0].color, None);
}
#[test]
fn new_sheet_propagates() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.new_sheet();
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
let worksheets_properties = model2.get_worksheets_properties();
assert_eq!(worksheets_properties.len(), 2);
}
#[test]
fn delete_sheet_propagates() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.new_sheet();
model.delete_sheet(0).unwrap();
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
let sheets_info = model2.get_worksheets_properties();
assert_eq!(sheets_info.len(), 1);
}

View File

@@ -0,0 +1,91 @@
#![allow(clippy::unwrap_used)]
use crate::{expressions::types::Area, UserModel};
#[test]
fn basic() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "100$").unwrap();
model
.range_clear_contents(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
model.undo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("100$".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
model.set_user_input(0, 1, 1, "300").unwrap();
// clear contents keeps the formatting
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("300$".to_string())
);
model
.range_clear_all(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
model.undo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("300$".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
model.set_user_input(0, 1, 1, "400").unwrap();
// clear contents keeps the formatting
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("400".to_string())
);
}
#[test]
fn clear_empty_cell() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.range_clear_contents(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
}
#[test]
fn clear_all_empty_cell() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.range_clear_all(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
}

View File

@@ -0,0 +1,161 @@
use crate::{
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT},
test::util::new_empty_model,
UserModel,
};
#[test]
fn send_queue() {
let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_column_width(0, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
let send_queue = model1.flush_send_queue();
let mut model2 = UserModel::from_model(new_empty_model());
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(model2.get_column_width(0, 3), Ok(width));
assert_eq!(
model2.get_formatted_cell_value(0, 1, 2),
Ok("Hello IronCalc!".to_string())
);
}
#[test]
fn apply_external_diffs_wrong_str() {
let mut model1 = UserModel::from_model(new_empty_model());
assert!(model1.apply_external_diffs("invalid".as_bytes()).is_err());
}
#[test]
fn queue_undo_redo() {
let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_column_width(0, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
assert!(model1.undo().is_ok());
assert!(model1.redo().is_ok());
let send_queue = model1.flush_send_queue();
let mut model2 = UserModel::from_model(new_empty_model());
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(model2.get_column_width(0, 3), Ok(width));
assert_eq!(
model2.get_formatted_cell_value(0, 1, 2),
Ok("Hello IronCalc!".to_string())
);
}
#[test]
fn queue_undo_redo_multiple() {
let mut model1 = UserModel::from_model(new_empty_model());
// do a bunch of things
model1.set_frozen_columns_count(0, 5).unwrap();
model1.set_frozen_rows_count(0, 6).unwrap();
model1.set_column_width(0, 7, 300.0).unwrap();
model1.set_row_height(0, 23, 123.0).unwrap();
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
for row in 1..5 {
model1.set_user_input(0, row, 17, "=ROW()").unwrap();
}
model1.insert_row(0, 3).unwrap();
model1.insert_row(0, 3).unwrap();
// undo al of them
while model1.can_undo() {
model1.undo().unwrap();
}
// check it is an empty model
assert_eq!(model1.get_frozen_columns_count(0), Ok(0));
assert_eq!(model1.get_frozen_rows_count(0), Ok(0));
assert_eq!(model1.get_column_width(0, 7), Ok(DEFAULT_COLUMN_WIDTH));
assert_eq!(
model1.get_formatted_cell_value(0, 55, 55),
Ok("".to_string())
);
assert_eq!(model1.get_row_height(0, 23), Ok(DEFAULT_ROW_HEIGHT));
assert_eq!(
model1.get_formatted_cell_value(0, 57, 55),
Ok("".to_string())
);
assert_eq!(model1.get_row_height(0, 25), Ok(DEFAULT_ROW_HEIGHT));
// redo all of them
while model1.can_redo() {
model1.redo().unwrap();
}
// now send all this to a new model
let send_queue = model1.flush_send_queue();
let mut model2 = UserModel::from_model(new_empty_model());
model2.apply_external_diffs(&send_queue).unwrap();
// Check everything is as expected
assert_eq!(model2.get_frozen_columns_count(0), Ok(5));
assert_eq!(model2.get_frozen_rows_count(0), Ok(6));
assert_eq!(model2.get_column_width(0, 7), Ok(300.0));
// I inserted two rows
assert_eq!(
model2.get_formatted_cell_value(0, 57, 55),
Ok("50".to_string())
);
assert_eq!(model2.get_row_height(0, 25), Ok(123.0));
assert_eq!(
model2.get_formatted_cell_value(0, 1, 17),
Ok("1".to_string())
);
assert_eq!(
model2.get_formatted_cell_value(0, 2, 17),
Ok("2".to_string())
);
assert_eq!(
model2.get_formatted_cell_value(0, 3, 17),
Ok("".to_string())
);
assert_eq!(
model2.get_formatted_cell_value(0, 4, 17),
Ok("".to_string())
);
assert_eq!(
model2.get_formatted_cell_value(0, 5, 17),
Ok("5".to_string())
);
assert_eq!(
model2.get_formatted_cell_value(0, 6, 17),
Ok("6".to_string())
);
}
#[test]
fn new_sheet() {
let mut model1 = UserModel::from_model(new_empty_model());
model1.new_sheet();
model1.set_user_input(0, 1, 1, "42").unwrap();
model1.set_user_input(1, 1, 1, "=Sheet1!A1*2").unwrap();
let send_queue = model1.flush_send_queue();
let mut model2 = UserModel::from_model(new_empty_model());
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(
model2.get_formatted_cell_value(1, 1, 1),
Ok("84".to_string())
);
}
#[test]
fn wrong_diffs_handled() {
let mut model = UserModel::from_model(new_empty_model());
assert!(model
.apply_external_diffs("Hello world".as_bytes())
.is_err());
}

View File

@@ -0,0 +1,31 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
#[test]
fn model_evaluates_automatically() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "=1 + 1").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string()));
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string()));
}
#[test]
fn pause_resume_evaluation() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.pause_evaluation();
model.set_user_input(0, 1, 1, "=1+1").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("#ERROR!".to_string())
);
model.evaluate();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string()));
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string()));
model.resume_evaluation();
model.set_user_input(0, 2, 1, "=1+4").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 1), Ok("5".to_string()));
}

View File

@@ -0,0 +1,131 @@
#![allow(clippy::unwrap_used)]
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::test::util::new_empty_model;
use crate::types::CellType;
use crate::UserModel;
#[test]
fn set_user_input_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Wrong sheet
assert!(model.set_user_input(1, 1, 1, "1").is_err());
// Wrong row
assert!(model.set_user_input(0, 0, 1, "1").is_err());
// Wrong column
assert!(model.set_user_input(0, 1, 0, "1").is_err());
// row too large
assert!(model.set_user_input(0, LAST_ROW, 1, "1").is_ok());
assert!(model.set_user_input(0, LAST_ROW + 1, 1, "1").is_err());
// column too large
assert!(model.set_user_input(0, 1, LAST_COLUMN, "1").is_ok());
assert!(model.set_user_input(0, 1, LAST_COLUMN + 1, "1").is_err());
}
#[test]
fn user_model_debug_message() {
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
let s = &format!("{:?}", model);
assert_eq!(s, "UserModel");
}
#[test]
fn cell_type() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "1").unwrap();
model.set_user_input(0, 1, 2, "Wish you were here").unwrap();
model.set_user_input(0, 1, 3, "true").unwrap();
model.set_user_input(0, 1, 4, "=1/0").unwrap();
assert_eq!(model.get_cell_type(0, 1, 1).unwrap(), CellType::Number);
assert_eq!(model.get_cell_type(0, 1, 2).unwrap(), CellType::Text);
assert_eq!(
model.get_cell_type(0, 1, 3).unwrap(),
CellType::LogicalValue
);
assert_eq!(model.get_cell_type(0, 1, 4).unwrap(), CellType::ErrorValue);
// empty cells are number type
assert_eq!(model.get_cell_type(0, 40, 40).unwrap(), CellType::Number);
}
#[test]
fn insert_remove_rows() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let height = model.get_row_height(0, 5).unwrap();
// Insert some data in row 5 (and change the style)
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
// Change the height of the column
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
// remove the row
assert!(model.delete_row(0, 5).is_ok());
// Row 5 has now the normal height
assert_eq!(model.get_row_height(0, 5), Ok(height));
// There is no value in A5
assert_eq!(model.get_formatted_cell_value(0, 5, 1), Ok("".to_string()));
// Setting a value will not format it
assert!(model.set_user_input(0, 5, 1, "125").is_ok());
assert_eq!(
model.get_formatted_cell_value(0, 5, 1),
Ok("125".to_string())
);
// undo twice
assert!(model.undo().is_ok());
assert!(model.undo().is_ok());
assert_eq!(model.get_row_height(0, 5), Ok(3.0 * height));
assert_eq!(
model.get_formatted_cell_value(0, 5, 1),
Ok("100$".to_string())
);
}
#[test]
fn insert_remove_columns() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// column E
let column_width = model.get_column_width(0, 5).unwrap();
println!("{column_width}");
// Insert some data in row 5 (and change the style) in E1
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
// Change the width of the column
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
// remove the column
assert!(model.delete_column(0, 5).is_ok());
// Column 5 has now the normal width
assert_eq!(model.get_column_width(0, 5), Ok(column_width));
// There is no value in E5
assert_eq!(model.get_formatted_cell_value(0, 1, 5), Ok("".to_string()));
// Setting a value will not format it
assert!(model.set_user_input(0, 1, 5, "125").is_ok());
assert_eq!(
model.get_formatted_cell_value(0, 1, 5),
Ok("125".to_string())
);
// undo twice (set_user_input and delete_column)
assert!(model.undo().is_ok());
assert!(model.undo().is_ok());
assert_eq!(model.get_column_width(0, 5), Ok(3.0 * column_width));
assert_eq!(
model.get_formatted_cell_value(0, 1, 5),
Ok("100$".to_string())
);
}
#[test]
fn delete_remove_cell() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let (sheet, row, column) = (0, 1, 1);
model.set_user_input(sheet, row, column, "100$").unwrap();
}

View File

@@ -0,0 +1,39 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
#[test]
fn basic_rename() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.rename_sheet(0, "NewSheet").unwrap();
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
}
#[test]
fn undo_redo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.rename_sheet(0, "NewSheet").unwrap();
model.undo().unwrap();
assert_eq!(model.get_worksheets_properties()[0].name, "Sheet1");
model.redo().unwrap();
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
}
#[test]
fn errors() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
assert_eq!(
model.rename_sheet(0, ""),
Err("Invalid name for a sheet: ''.".to_string())
);
assert_eq!(
model.rename_sheet(1, "Hello"),
Err("Invalid sheet index".to_string())
);
}

View File

@@ -0,0 +1,156 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
test::util::new_empty_model,
UserModel,
};
#[test]
fn simple_insert_row() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let (sheet, column) = (0, 5);
for row in 1..5 {
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
}
assert!(model.insert_row(sheet, 3).is_ok());
assert_eq!(
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
""
);
assert!(model.undo().is_ok());
assert_eq!(
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
"123"
);
assert!(model.redo().is_ok());
assert_eq!(
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
""
);
}
#[test]
fn simple_insert_column() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let (sheet, row) = (0, 5);
for column in 1..5 {
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
}
assert!(model.insert_column(sheet, 3).is_ok());
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
assert!(model.undo().is_ok());
assert_eq!(
model.get_formatted_cell_value(sheet, row, 3).unwrap(),
"123"
);
assert!(model.redo().is_ok());
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
}
#[test]
fn simple_delete_column() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 1, 5, "3").unwrap();
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
model
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
.unwrap();
model.delete_column(0, 5).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("6".to_string()));
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH * 3.0));
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(
model2.get_formatted_cell_value(0, 2, 5),
Ok("6".to_string())
);
assert_eq!(
model2.get_column_width(0, 5),
Ok(DEFAULT_COLUMN_WIDTH * 3.0)
);
}
#[test]
fn delete_column_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.delete_column(1, 1),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.delete_column(0, 0),
Err("Column number '0' is not valid.".to_string())
);
assert_eq!(
model.delete_column(0, LAST_COLUMN + 1),
Err("Column number '16385' is not valid.".to_string())
);
assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(()));
}
#[test]
fn simple_delete_row() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 15, 4, "3").unwrap();
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
model
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
.unwrap();
model.delete_row(0, 15).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
model.undo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 15, 6),
Ok("6".to_string())
);
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0));
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(
model2.get_formatted_cell_value(0, 15, 6),
Ok("6".to_string())
);
assert_eq!(model2.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0));
}
#[test]
fn simple_delete_row_no_style() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_user_input(0, 15, 4, "3").unwrap();
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
model.delete_row(0, 15).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
}

View File

@@ -0,0 +1,723 @@
#![allow(clippy::unwrap_used)]
use crate::{
expressions::types::Area,
types::{Alignment, BorderItem, BorderStyle, HorizontalAlignment, VerticalAlignment},
UserModel,
};
#[test]
fn basic_fonts() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.i);
assert!(!style.font.b);
assert!(!style.font.u);
assert!(!style.font.strike);
assert_eq!(style.font.color, Some("#000000".to_owned()));
// bold
model.update_range_style(&range, "font.b", "true").unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.b);
// italics
model.update_range_style(&range, "font.i", "true").unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.i);
// underline
model.update_range_style(&range, "font.u", "true").unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.u);
// strike
model
.update_range_style(&range, "font.strike", "true")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.strike);
// color
model
.update_range_style(&range, "font.color", "#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
while model.can_undo() {
model.undo().unwrap();
}
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.i);
assert!(!style.font.b);
assert!(!style.font.u);
assert!(!style.font.strike);
assert_eq!(style.font.color, Some("#000000".to_owned()));
while model.can_redo() {
model.redo().unwrap();
}
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.i);
assert!(style.font.b);
assert!(style.font.u);
assert!(style.font.strike);
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
let style = model2.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.i);
assert!(style.font.b);
assert!(style.font.u);
assert!(style.font.strike);
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
}
#[test]
fn font_errors() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
assert_eq!(
model.update_range_style(&range, "font.b", "True"),
Err("Invalid value for boolean: 'True'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "font.i", "FALSE"),
Err("Invalid value for boolean: 'FALSE'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "font.bold", "true"),
Err("Invalid style path: 'font.bold'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "font.strike", ""),
Err("Invalid value for boolean: ''.".to_string())
);
// There is no cast for booleans
assert_eq!(
model.update_range_style(&range, "font.b", "1"),
Err("Invalid value for boolean: '1'.".to_string())
);
// colors don't work by name
assert_eq!(
model.update_range_style(&range, "font.color", "blue"),
Err("Invalid color: 'blue'.".to_string())
);
// No short form
assert_eq!(
model.update_range_style(&range, "font.color", "#FFF"),
Err("Invalid color: '#FFF'.".to_string())
);
}
#[test]
fn basic_fill() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, None);
assert_eq!(style.fill.fg_color, None);
// bg_color
model
.update_range_style(&range, "fill.bg_color", "#F2F2F2")
.unwrap();
model
.update_range_style(&range, "fill.fg_color", "#F3F4F5")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
let style = model2.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
}
#[test]
fn fill_errors() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
assert_eq!(
model.update_range_style(&range, "fill.bg_color", "#FFF"),
Err("Invalid color: '#FFF'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "fill.fg_color", "#FFF"),
Err("Invalid color: '#FFF'.".to_string())
);
}
#[test]
fn basic_format() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.num_fmt, "general");
model
.update_range_style(&range, "num_fmt", "$#,##0.0000")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.num_fmt, "$#,##0.0000");
model.undo().unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.num_fmt, "general");
model.redo().unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.num_fmt, "$#,##0.0000");
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
let style = model2.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.num_fmt, "$#,##0.0000");
}
#[test]
fn basic_borders() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
model
.update_range_style(&range, "border.left", "thin,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::Thin,
color: Some("#F1F1F1".to_owned()),
})
);
model
.update_range_style(&range, "border.left", "thin,")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::Thin,
color: None,
})
);
model
.update_range_style(&range, "border.right", "dotted,#F1F1F2")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.right,
Some(BorderItem {
style: BorderStyle::Dotted,
color: Some("#F1F1F2".to_owned()),
})
);
model
.update_range_style(&range, "border.top", "double,#F1F1F3")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.top,
Some(BorderItem {
style: BorderStyle::Double,
color: Some("#F1F1F3".to_owned()),
})
);
model
.update_range_style(&range, "border.bottom", "medium,#F1F1F4")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.bottom,
Some(BorderItem {
style: BorderStyle::Medium,
color: Some("#F1F1F4".to_owned()),
})
);
while model.can_undo() {
model.undo().unwrap();
}
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.border.left, None);
assert_eq!(style.border.top, None);
assert_eq!(style.border.right, None);
assert_eq!(style.border.bottom, None);
while model.can_redo() {
model.redo().unwrap();
}
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::Thin,
color: None,
})
);
assert_eq!(
style.border.right,
Some(BorderItem {
style: BorderStyle::Dotted,
color: Some("#F1F1F2".to_owned()),
})
);
assert_eq!(
style.border.top,
Some(BorderItem {
style: BorderStyle::Double,
color: Some("#F1F1F3".to_owned()),
})
);
assert_eq!(
style.border.bottom,
Some(BorderItem {
style: BorderStyle::Medium,
color: Some("#F1F1F4".to_owned()),
})
);
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
let style = model2.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::Thin,
color: None,
})
);
assert_eq!(
style.border.right,
Some(BorderItem {
style: BorderStyle::Dotted,
color: Some("#F1F1F2".to_owned()),
})
);
assert_eq!(
style.border.top,
Some(BorderItem {
style: BorderStyle::Double,
color: Some("#F1F1F3".to_owned()),
})
);
assert_eq!(
style.border.bottom,
Some(BorderItem {
style: BorderStyle::Medium,
color: Some("#F1F1F4".to_owned()),
})
);
}
#[test]
fn basic_alignment() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
assert_eq!(alignment, None);
model
.update_range_style(&range, "alignment.horizontal", "center")
.unwrap();
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::Center,
vertical: VerticalAlignment::Bottom,
wrap_text: false
})
);
model
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
.unwrap();
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::CenterContinuous,
vertical: VerticalAlignment::Bottom,
wrap_text: false
})
);
let range = Area {
sheet: 0,
row: 2,
column: 2,
width: 1,
height: 1,
};
model
.update_range_style(&range, "alignment.vertical", "distributed")
.unwrap();
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Distributed,
wrap_text: false
})
);
model
.update_range_style(&range, "alignment.vertical", "justify")
.unwrap();
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Justify,
wrap_text: false
})
);
model.update_range_style(&range, "alignment", "").unwrap();
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
assert_eq!(alignment, None);
model.undo().unwrap();
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Justify,
wrap_text: false
})
);
}
#[test]
fn alignment_errors() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
assert_eq!(
model.update_range_style(&range, "alignment", "some"),
Err("Alignment must be empty, but found: 'some'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "alignment.vertical", "justified"),
Err("Invalid value for vertical alignment: 'justified'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "alignment.horizontal", "unjustified"),
Err("Invalid value for horizontal alignment: 'unjustified'.".to_string())
);
model
.update_range_style(&range, "alignment.vertical", "justify")
.unwrap();
// Also fail if there is an alignment
assert_eq!(
model.update_range_style(&range, "alignment", "some"),
Err("Alignment must be empty, but found: 'some'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "alignment.vertical", "justified"),
Err("Invalid value for vertical alignment: 'justified'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "alignment.horizontal", "unjustified"),
Err("Invalid value for horizontal alignment: 'unjustified'.".to_string())
);
}
#[test]
fn basic_wrap_text() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
assert_eq!(
model.update_range_style(&range, "alignment.wrap_text", "T"),
Err("Invalid value for boolean: 'T'.".to_string())
);
model
.update_range_style(&range, "alignment.wrap_text", "true")
.unwrap();
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Bottom,
wrap_text: true
})
);
model.undo().unwrap();
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
assert_eq!(alignment, None);
model.redo().unwrap();
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
assert_eq!(
alignment,
Some(Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Bottom,
wrap_text: true
})
);
assert_eq!(
model.update_range_style(&range, "alignment.wrap_text", "True"),
Err("Invalid value for boolean: 'True'.".to_string())
);
}
#[test]
fn more_basic_borders() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
model
.update_range_style(&range, "border.left", "thick,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::Thick,
color: Some("#F1F1F1".to_owned()),
})
);
model
.update_range_style(&range, "border.left", "slantDashDot,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::SlantDashDot,
color: Some("#F1F1F1".to_owned()),
})
);
model
.update_range_style(&range, "border.left", "mediumDashDot,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::MediumDashDot,
color: Some("#F1F1F1".to_owned()),
})
);
model
.update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::MediumDashDotDot,
color: Some("#F1F1F1".to_owned()),
})
);
model
.update_range_style(&range, "border.left", "mediumDashed,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::MediumDashed,
color: Some("#F1F1F1".to_owned()),
})
);
}
#[test]
fn border_errors() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
assert_eq!(
model.update_range_style(&range, "border.lef", "thick,#F1F1F1"),
Err("Invalid style path: 'border.lef'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "border.left", "thic,#F1F1F1"),
Err("Invalid border style: 'thic'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "border.left", "thick,#F1F1F"),
Err("Invalid color: '#F1F1F'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "border.left", " "),
Err("Invalid border value: ' '.".to_string())
);
assert_eq!(
model.update_range_style(&range, "border.left", "thick,#F1F1F1,thin"),
Err("Invalid border value: 'thick,#F1F1F1,thin'.".to_string())
);
}
#[test]
fn empty_removes_border() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
model
.update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(
style.border.left,
Some(BorderItem {
style: BorderStyle::MediumDashDotDot,
color: Some("#F1F1F1".to_owned()),
})
);
model.update_range_style(&range, "border.left", "").unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.border.left, None);
}
#[test]
fn false_removes_value() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
// bold
model.update_range_style(&range, "font.b", "true").unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(style.font.b);
model.update_range_style(&range, "font.b", "false").unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert!(!style.font.b);
}

View File

@@ -0,0 +1,30 @@
#![allow(clippy::unwrap_used)]
use crate::{test::util::new_empty_model, UserModel};
#[test]
fn basic() {
let mut model1 = UserModel::from_model(new_empty_model());
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
model1.set_column_width(0, 3, width).unwrap();
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
let model_bytes = model1.to_bytes();
let model2 = UserModel::from_bytes(&model_bytes).unwrap();
assert_eq!(model2.get_column_width(0, 3), Ok(width));
assert_eq!(
model2.get_formatted_cell_value(0, 1, 2),
Ok("Hello IronCalc!".to_string())
);
}
#[test]
fn errors() {
let model_bytes = "Early in the morning, late in the century, Cricklewood Broadway.".as_bytes();
assert_eq!(
&UserModel::from_bytes(model_bytes).unwrap_err(),
"Error parsing workbook: invalid packing"
);
}

View File

@@ -0,0 +1,66 @@
#![allow(clippy::unwrap_used)]
use crate::{test::util::new_empty_model, UserModel};
#[test]
fn simple_undo_redo() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// at the beginning I cannot undo or redo
assert!(!model.can_undo());
assert!(!model.can_redo());
assert!(model.set_user_input(0, 1, 1, "=1+2").is_ok());
// Once I enter a value I can undo but not redo
assert!(model.can_undo());
assert!(!model.can_redo());
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string()));
// If I undo, I can't undo anymore, but I can redo
assert!(model.undo().is_ok());
assert!(!model.can_undo());
assert!(model.can_redo());
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
// If I redo, I have the old value and formula
assert!(model.redo().is_ok());
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string()));
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+2".to_string()));
assert!(model.can_undo());
assert!(!model.can_redo());
}
#[test]
fn undo_redo_respect_styles() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert!(model.set_user_input(0, 1, 1, "100").is_ok());
assert!(model.set_user_input(0, 1, 1, "125$").is_ok());
// The content of the cell is just the number 125
assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string()));
assert!(model.undo().is_ok());
// The cell has no currency number formatting
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("100".to_string())
);
assert_eq!(model.get_cell_content(0, 1, 1), Ok("100".to_string()));
assert!(model.redo().is_ok());
// The cell has the number 125 formatted as '125$'
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("125$".to_string())
);
assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string()));
}
#[test]
fn can_undo_can_redo() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert!(!model.can_undo());
assert!(!model.can_redo());
assert!(model.undo().is_ok());
assert!(model.redo().is_ok());
}

View File

@@ -0,0 +1,216 @@
#![allow(clippy::unwrap_used)]
use std::collections::HashMap;
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
user_model::SelectedView,
UserModel,
};
#[test]
fn initial_view() {
let model = new_empty_model();
let model = UserModel::from_model(model);
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn set_the_cell_sets_the_range() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_selected_cell(5, 4).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 5, 4));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 5,
column: 4,
range: [5, 4, 5, 4],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn set_the_range_does_not_set_the_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_selected_range(5, 4, 10, 6).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [5, 4, 10, 6],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn add_new_sheet_and_back() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.new_sheet();
assert_eq!(model.get_selected_sheet(), 0);
model.set_selected_cell(5, 4).unwrap();
model.set_selected_sheet(1).unwrap();
assert_eq!(model.get_selected_cell(), (1, 1, 1));
model.set_selected_sheet(0).unwrap();
assert_eq!(model.get_selected_cell(), (0, 5, 4));
}
#[test]
fn set_selected_cell_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.set_selected_cell(-5, 4),
Err("Invalid row: '-5'".to_string())
);
assert_eq!(
model.set_selected_cell(5, -4),
Err("Invalid column: '-4'".to_string())
);
assert_eq!(
model.set_selected_range(-1, 1, 1, 1),
Err("Invalid row: '-1'".to_string())
);
assert_eq!(
model.set_selected_range(1, 0, 1, 1),
Err("Invalid column: '0'".to_string())
);
assert_eq!(
model.set_selected_range(1, 1, LAST_ROW + 1, 1),
Err("Invalid row: '1048577'".to_string())
);
assert_eq!(
model.set_selected_range(1, 1, 1, LAST_COLUMN + 1),
Err("Invalid column: '16385'".to_string())
);
}
#[test]
fn set_selected_cell_errors_wrong_sheet() {
let mut model = new_empty_model();
// forcefully set a wrong index
model.workbook.views.get_mut(&0).unwrap().sheet = 2;
let mut model = UserModel::from_model(model);
// It's returning the wrong number
assert_eq!(model.get_selected_sheet(), 2);
// But we can't set the selected cell anymore
assert_eq!(
model.set_selected_cell(3, 4),
Err("Invalid worksheet index 2".to_string())
);
assert_eq!(
model.set_selected_range(3, 4, 5, 6),
Err("Invalid worksheet index 2".to_string())
);
assert_eq!(
model.set_top_left_visible_cell(3, 4),
Err("Invalid worksheet index 2".to_string())
);
// we can fix it by setting the right cell
model.set_selected_sheet(0).unwrap();
model.set_selected_cell(3, 4).unwrap();
}
#[test]
fn set_visible_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_top_left_visible_cell(100, 12).unwrap();
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 100,
left_column: 12
}
);
let s = serde_json::to_string(&model.get_selected_view()).unwrap();
assert_eq!(
serde_json::from_str::<SelectedView>(&s).unwrap(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 100,
left_column: 12
}
);
}
#[test]
fn set_visible_cell_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.set_top_left_visible_cell(-100, 12),
Err("Invalid row: '-100'".to_string())
);
assert_eq!(
model.set_top_left_visible_cell(100, -12),
Err("Invalid column: '-12'".to_string())
);
}
#[test]
fn errors_no_views() {
let mut model = new_empty_model();
// forcefully remove the view
model.workbook.views = HashMap::new();
// also in the sheet
model.workbook.worksheets[0].views = HashMap::new();
let mut model = UserModel::from_model(model);
// get methods will return defaults
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1
}
);
// set methods won't complain. but won't work either
model.set_selected_sheet(0).unwrap();
model.set_selected_cell(5, 6).unwrap();
assert_eq!(model.get_selected_cell(), (0, 1, 1));
}

View File

@@ -32,11 +32,11 @@ impl Model {
let cell_reference = self._parse_reference(cell); let cell_reference = self._parse_reference(cell);
let column = cell_reference.column; let column = cell_reference.column;
let row = cell_reference.row; let row = cell_reference.row;
self.cell_formula(cell_reference.sheet, row, column) self.get_cell_formula(cell_reference.sheet, row, column)
.unwrap() .unwrap()
} }
pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String { pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String {
self.formatted_cell_value(sheet, row, column).unwrap() self.get_formatted_cell_value(sheet, row, column).unwrap()
} }
pub fn _get_text(&self, cell: &str) -> String { pub fn _get_text(&self, cell: &str) -> String {
let CellReferenceIndex { sheet, row, column } = self._parse_reference(cell); let CellReferenceIndex { sheet, row, column } = self._parse_reference(cell);

View File

@@ -1,39 +1,18 @@
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display}; use std::{collections::HashMap, fmt::Display};
use crate::expressions::token::Error; use crate::expressions::token::Error;
// Useful for `#[serde(default = "default_as_true")]`
fn default_as_true() -> bool {
true
}
fn default_as_false() -> bool { fn default_as_false() -> bool {
false false
} }
// Useful for `#[serde(skip_serializing_if = "is_true")]`
fn is_true(b: &bool) -> bool {
*b
}
fn is_false(b: &bool) -> bool { fn is_false(b: &bool) -> bool {
!*b !*b
} }
fn is_zero(num: &i32) -> bool { #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
*num == 0
}
fn is_default_alignment(o: &Option<Alignment>) -> bool {
o.is_none() || *o == Some(Alignment::default())
}
fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
h.values().len() == 0
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Metadata { pub struct Metadata {
pub application: String, pub application: String,
pub app_version: String, pub app_version: String,
@@ -43,14 +22,21 @@ pub struct Metadata {
pub last_modified: String, //"2020-11-20T16:24:35" pub last_modified: String, //"2020-11-20T16:24:35"
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct WorkbookSettings { pub struct WorkbookSettings {
pub tz: String, pub tz: String,
pub locale: String, pub locale: String,
} }
/// A Workbook View tracks of the selected sheet for each view
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct WorkbookView {
/// The index of the currently selected sheet.
pub sheet: u32,
}
/// An internal representation of an IronCalc Workbook /// An internal representation of an IronCalc Workbook
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
#[serde(deny_unknown_fields)]
pub struct Workbook { pub struct Workbook {
pub shared_strings: Vec<String>, pub shared_strings: Vec<String>,
pub defined_names: Vec<DefinedName>, pub defined_names: Vec<DefinedName>,
@@ -59,28 +45,22 @@ pub struct Workbook {
pub name: String, pub name: String,
pub settings: WorkbookSettings, pub settings: WorkbookSettings,
pub metadata: Metadata, pub metadata: Metadata,
#[serde(default)]
#[serde(skip_serializing_if = "hashmap_is_empty")]
pub tables: HashMap<String, Table>, pub tables: HashMap<String, Table>,
pub views: HashMap<u32, WorkbookView>,
} }
/// A defined name. The `sheet_id` is the sheet index in case the name is local /// A defined name. The `sheet_id` is the sheet index in case the name is local
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct DefinedName { pub struct DefinedName {
pub name: String, pub name: String,
pub formula: String, pub formula: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sheet_id: Option<u32>, pub sheet_id: Option<u32>,
} }
// TODO: Move to worksheet.rs make frozen_rows/columns private and u32
/// Internal representation of a worksheet Excel object
/// * state: /// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types) /// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible /// hidden, veryHidden, visible
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum SheetState { pub enum SheetState {
Visible, Visible,
Hidden, Hidden,
@@ -97,8 +77,25 @@ impl Display for SheetState {
} }
} }
/// Represents the state of the worksheet as seen by the user. This includes
/// details such as the currently selected cell, the visible range, and the
/// position of the viewport.
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct WorksheetView {
/// The row index of the currently selected cell.
pub row: i32,
/// The column index of the currently selected cell.
pub column: i32,
/// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
pub range: [i32; 4],
/// The row index of the topmost visible cell in the worksheet view.
pub top_row: i32,
/// The column index of the leftmost visible cell in the worksheet view.
pub left_column: i32,
}
/// Internal representation of a worksheet Excel object /// Internal representation of a worksheet Excel object
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Worksheet { pub struct Worksheet {
pub dimension: String, pub dimension: String,
pub cols: Vec<Col>, pub cols: Vec<Col>,
@@ -108,16 +105,12 @@ pub struct Worksheet {
pub shared_formulas: Vec<String>, pub shared_formulas: Vec<String>,
pub sheet_id: u32, pub sheet_id: u32,
pub state: SheetState, pub state: SheetState,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>, pub color: Option<String>,
pub merge_cells: Vec<String>, pub merge_cells: Vec<String>,
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_rows: i32, pub frozen_rows: i32,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_columns: i32, pub frozen_columns: i32,
pub views: HashMap<u32, WorksheetView>,
} }
/// Internal representation of Excel's sheet_data /// Internal representation of Excel's sheet_data
@@ -125,7 +118,7 @@ pub struct Worksheet {
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>; pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
// ECMA-376-1:2016 section 18.3.1.73 // ECMA-376-1:2016 section 18.3.1.73
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Row { pub struct Row {
/// Row index /// Row index
pub r: i32, pub r: i32,
@@ -133,23 +126,19 @@ pub struct Row {
pub custom_format: bool, pub custom_format: bool,
pub custom_height: bool, pub custom_height: bool,
pub s: i32, pub s: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub hidden: bool, pub hidden: bool,
} }
// ECMA-376-1:2016 section 18.3.1.13 // ECMA-376-1:2016 section 18.3.1.13
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Col { pub struct Col {
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries. // Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
/// First column affected by this record. Settings apply to column in \[min, max\] range. /// First column affected by this record. Settings apply to column in \[min, max\] range.
pub min: i32, pub min: i32,
/// Last column affected by this record. Settings apply to column in \[min, max\] range. /// Last column affected by this record. Settings apply to column in \[min, max\] range.
pub max: i32, pub max: i32,
pub width: f64, pub width: f64,
pub custom_width: bool, pub custom_width: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<i32>, pub style: Option<i32>,
} }
@@ -164,32 +153,55 @@ pub enum CellType {
CompoundData = 128, CompoundData = 128,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Encode, Decode, Debug, Clone, PartialEq)]
#[serde(tag = "t", deny_unknown_fields)]
pub enum Cell { pub enum Cell {
#[serde(rename = "empty")] EmptyCell {
EmptyCell { s: i32 }, s: i32,
#[serde(rename = "b")] },
BooleanCell { v: bool, s: i32 },
#[serde(rename = "n")] BooleanCell {
NumberCell { v: f64, s: i32 }, v: bool,
s: i32,
},
NumberCell {
v: f64,
s: i32,
},
// Maybe we should not have this type. In Excel this is just a string // Maybe we should not have this type. In Excel this is just a string
#[serde(rename = "e")] ErrorCell {
ErrorCell { ei: Error, s: i32 }, ei: Error,
s: i32,
},
// Always a shared string // Always a shared string
#[serde(rename = "s")] SharedString {
SharedString { si: i32, s: i32 }, si: i32,
s: i32,
},
// Non evaluated Formula // Non evaluated Formula
#[serde(rename = "u")] CellFormula {
CellFormula { f: i32, s: i32 }, f: i32,
#[serde(rename = "fb")] s: i32,
CellFormulaBoolean { f: i32, v: bool, s: i32 }, },
#[serde(rename = "fn")]
CellFormulaNumber { f: i32, v: f64, s: i32 }, CellFormulaBoolean {
f: i32,
v: bool,
s: i32,
},
CellFormulaNumber {
f: i32,
v: f64,
s: i32,
},
// always inline string // always inline string
#[serde(rename = "str")] CellFormulaString {
CellFormulaString { f: i32, v: String, s: i32 }, f: i32,
#[serde(rename = "fe")] v: String,
s: i32,
},
CellFormulaError { CellFormulaError {
f: i32, f: i32,
ei: Error, ei: Error,
@@ -208,17 +220,16 @@ impl Default for Cell {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Comment { pub struct Comment {
pub text: String, pub text: String,
pub author_name: String, pub author_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_id: Option<String>, pub author_id: Option<String>,
pub cell_ref: String, pub cell_ref: String,
} }
// ECMA-376-1:2016 section 18.5.1.2 // ECMA-376-1:2016 section 18.5.1.2
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Table { pub struct Table {
pub name: String, pub name: String,
pub display_name: String, pub display_name: String,
@@ -226,34 +237,24 @@ pub struct Table {
pub reference: String, pub reference: String,
pub totals_row_count: u32, pub totals_row_count: u32,
pub header_row_count: u32, pub header_row_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>, pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>, pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>, pub totals_row_dxf_id: Option<u32>,
pub columns: Vec<TableColumn>, pub columns: Vec<TableColumn>,
pub style_info: TableStyleInfo, pub style_info: TableStyleInfo,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub has_filters: bool, pub has_filters: bool,
} }
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum? // totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
// the totals_row_function is an enum not String methinks // the totals_row_function is an enum not String methinks
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct TableColumn { pub struct TableColumn {
pub id: u32, pub id: u32,
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_label: Option<String>, pub totals_row_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>, pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>, pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>, pub totals_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_function: Option<String>, pub totals_row_function: Option<String>,
} }
@@ -271,25 +272,16 @@ impl Default for TableColumn {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct TableStyleInfo { pub struct TableStyleInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_first_column: bool, pub show_first_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_last_column: bool, pub show_last_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_row_stripes: bool, pub show_row_stripes: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_column_stripes: bool, pub show_column_stripes: bool,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Styles { pub struct Styles {
pub num_fmts: Vec<NumFmt>, pub num_fmts: Vec<NumFmt>,
pub fonts: Vec<Font>, pub fonts: Vec<Font>,
@@ -314,8 +306,9 @@ impl Default for Styles {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Style { pub struct Style {
#[serde(skip_serializing_if = "Option::is_none")]
pub alignment: Option<Alignment>, pub alignment: Option<Alignment>,
pub num_fmt: String, pub num_fmt: String,
pub fill: Fill, pub fill: Fill,
@@ -324,7 +317,7 @@ pub struct Style {
pub quote_prefix: bool, pub quote_prefix: bool,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct NumFmt { pub struct NumFmt {
pub num_fmt_id: i32, pub num_fmt_id: i32,
pub format_code: String, pub format_code: String,
@@ -342,7 +335,7 @@ impl Default for NumFmt {
// ST_FontScheme simple type (§18.18.33). // ST_FontScheme simple type (§18.18.33).
// Usually major fonts are used for styles like headings, // Usually major fonts are used for styles like headings,
// and minor fonts are used for body and paragraph text. // and minor fonts are used for body and paragraph text.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)] #[derive(Default)]
pub enum FontScheme { pub enum FontScheme {
@@ -362,7 +355,7 @@ impl Display for FontScheme {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Font { pub struct Font {
#[serde(default = "default_as_false")] #[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")] #[serde(skip_serializing_if = "is_false")]
@@ -405,7 +398,7 @@ impl Default for Font {
} }
// TODO: Maybe use an enum for the pattern_type values here? // TODO: Maybe use an enum for the pattern_type values here?
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Fill { pub struct Fill {
pub pattern_type: String, pub pattern_type: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -424,7 +417,7 @@ impl Default for Fill {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum HorizontalAlignment { pub enum HorizontalAlignment {
Center, Center,
@@ -466,7 +459,7 @@ impl Display for HorizontalAlignment {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum VerticalAlignment { pub enum VerticalAlignment {
Bottom, Bottom,
@@ -501,7 +494,7 @@ impl Display for VerticalAlignment {
} }
// 1762 // 1762
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct Alignment { pub struct Alignment {
#[serde(default)] #[serde(default)]
#[serde(skip_serializing_if = "HorizontalAlignment::is_default")] #[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
@@ -514,29 +507,17 @@ pub struct Alignment {
pub wrap_text: bool, pub wrap_text: bool,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyleXfs { pub struct CellStyleXfs {
pub num_fmt_id: i32, pub num_fmt_id: i32,
pub font_id: i32, pub font_id: i32,
pub fill_id: i32, pub fill_id: i32,
pub border_id: i32, pub border_id: i32,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_number_format: bool, pub apply_number_format: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_border: bool, pub apply_border: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_alignment: bool, pub apply_alignment: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_protection: bool, pub apply_protection: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_font: bool, pub apply_font: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_fill: bool, pub apply_fill: bool,
} }
@@ -557,39 +538,24 @@ impl Default for CellStyleXfs {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct CellXfs { pub struct CellXfs {
pub xf_id: i32, pub xf_id: i32,
pub num_fmt_id: i32, pub num_fmt_id: i32,
pub font_id: i32, pub font_id: i32,
pub fill_id: i32, pub fill_id: i32,
pub border_id: i32, pub border_id: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_number_format: bool, pub apply_number_format: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_border: bool, pub apply_border: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_alignment: bool, pub apply_alignment: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_protection: bool, pub apply_protection: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_font: bool, pub apply_font: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_fill: bool, pub apply_fill: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub quote_prefix: bool, pub quote_prefix: bool,
#[serde(skip_serializing_if = "is_default_alignment")]
pub alignment: Option<Alignment>, pub alignment: Option<Alignment>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyles { pub struct CellStyles {
pub name: String, pub name: String,
pub xf_id: i32, pub xf_id: i32,
@@ -606,7 +572,7 @@ impl Default for CellStyles {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum BorderStyle { pub enum BorderStyle {
Thin, Thin,
@@ -636,13 +602,13 @@ impl Display for BorderStyle {
} }
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct BorderItem { pub struct BorderItem {
pub style: BorderStyle, pub style: BorderStyle,
pub color: Option<String>, pub color: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct Border { pub struct Border {
#[serde(default = "default_as_false")] #[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")] #[serde(skip_serializing_if = "is_false")]
@@ -665,9 +631,10 @@ pub struct Border {
/// Information need to show a sheet tab in the UI /// Information need to show a sheet tab in the UI
/// The color is serialized only if it is not Color::None /// The color is serialized only if it is not Color::None
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SheetInfo { pub struct SheetProperties {
pub name: String, pub name: String,
pub state: String, pub state: String,
pub sheet_id: u32, pub sheet_id: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>, pub color: Option<String>,
} }

1440
base/src/user_model.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -27,16 +27,4 @@ impl Workbook {
.get_mut(worksheet_index as usize) .get_mut(worksheet_index as usize)
.ok_or_else(|| "Invalid sheet index".to_string()) .ok_or_else(|| "Invalid sheet index".to_string())
} }
pub fn get_worksheets_info(&self) -> Vec<SheetInfo> {
self.worksheets
.iter()
.map(|worksheet| SheetInfo {
name: worksheet.get_name(),
state: worksheet.state.to_string(),
color: worksheet.color.clone(),
sheet_id: worksheet.sheet_id,
})
.collect()
}
} }

View File

@@ -42,7 +42,7 @@ impl Worksheet {
self.sheet_data.get_mut(&row)?.get_mut(&column) self.sheet_data.get_mut(&row)?.get_mut(&column)
} }
fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) { pub(crate) fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) {
match self.sheet_data.get_mut(&row) { match self.sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) { Some(column_data) => match column_data.get(&column) {
Some(_cell) => { Some(_cell) => {
@@ -68,9 +68,8 @@ impl Worksheet {
if row.r == row_index { if row.r == row_index {
if row.custom_format { if row.custom_format {
return row.s; return row.s;
} else {
break;
} }
break;
} }
} }
let cols = &self.cols; let cols = &self.cols;
@@ -106,64 +105,8 @@ impl Worksheet {
} }
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> { pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
let cols = &mut self.cols; let width = constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR;
let col = Col { self.set_column_width_and_style(column, width, Some(style_index))
min: column,
max: column,
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
custom_width: true,
style: Some(style_index),
};
let mut index = 0;
let mut split = false;
for c in cols.iter_mut() {
let min = c.min;
let max = c.max;
if min <= column && column <= max {
if min == column && max == column {
c.style = Some(style_index);
return Ok(());
} else {
// We need to split the result
split = true;
break;
}
}
if column < min {
// We passed, we should insert at index
break;
}
index += 1;
}
if split {
let min = cols[index].min;
let max = cols[index].max;
let pre = Col {
min,
max: column - 1,
width: cols[index].width,
custom_width: cols[index].custom_width,
style: cols[index].style,
};
let post = Col {
min: column + 1,
max,
width: cols[index].width,
custom_width: cols[index].custom_width,
style: cols[index].style,
};
cols.remove(index);
if column != max {
cols.insert(index, post);
}
cols.insert(index, col);
if column != min {
cols.insert(index, pre);
}
} else {
cols.insert(index, col);
}
Ok(())
} }
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> { pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
@@ -191,7 +134,7 @@ impl Worksheet {
cell.set_style(style_index); cell.set_style(style_index);
} }
None => { None => {
self.set_cell_empty_with_style(row, column, style_index); self.cell_clear_contents_with_style(row, column, style_index);
} }
} }
@@ -223,13 +166,13 @@ impl Worksheet {
self.update_cell(row, column, cell); self.update_cell(row, column, cell);
} }
pub fn set_cell_empty(&mut self, row: i32, column: i32) { pub fn cell_clear_contents(&mut self, row: i32, column: i32) {
let s = self.get_style(row, column); let s = self.get_style(row, column);
let cell = Cell::EmptyCell { s }; let cell = Cell::EmptyCell { s };
self.update_cell(row, column, cell); self.update_cell(row, column, cell);
} }
pub fn set_cell_empty_with_style(&mut self, row: i32, column: i32, style: i32) { pub fn cell_clear_contents_with_style(&mut self, row: i32, column: i32, style: i32) {
let cell = Cell::EmptyCell { s: style }; let cell = Cell::EmptyCell { s: style };
self.update_cell(row, column, cell); self.update_cell(row, column, cell);
} }
@@ -237,7 +180,8 @@ impl Worksheet {
pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> { pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> {
if frozen_rows < 0 { if frozen_rows < 0 {
return Err("Frozen rows cannot be negative".to_string()); return Err("Frozen rows cannot be negative".to_string());
} else if frozen_rows >= constants::LAST_ROW { }
if frozen_rows >= constants::LAST_ROW {
return Err("Too many rows".to_string()); return Err("Too many rows".to_string());
} }
self.frozen_rows = frozen_rows; self.frozen_rows = frozen_rows;
@@ -247,7 +191,8 @@ impl Worksheet {
pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> { pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> {
if frozen_columns < 0 { if frozen_columns < 0 {
return Err("Frozen columns cannot be negative".to_string()); return Err("Frozen columns cannot be negative".to_string());
} else if frozen_columns >= constants::LAST_COLUMN { }
if frozen_columns >= constants::LAST_COLUMN {
return Err("Too many columns".to_string()); return Err("Too many columns".to_string());
} }
self.frozen_columns = frozen_columns; self.frozen_columns = frozen_columns;
@@ -281,11 +226,21 @@ impl Worksheet {
}); });
Ok(()) Ok(())
} }
/// Changes the width of a column. /// Changes the width of a column.
/// * If the column does not a have a width we simply add it /// * If the column does not a have a width we simply add it
/// * If it has, it might be part of a range and we ned to split the range. /// * If it has, it might be part of a range and we ned to split the range.
/// Fails if column index is outside allowed range. /// Fails if column index is outside allowed range.
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> { pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
self.set_column_width_and_style(column, width, None)
}
pub(crate) fn set_column_width_and_style(
&mut self,
column: i32,
width: f64,
style: Option<i32>,
) -> Result<(), String> {
if !is_valid_column_number(column) { if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid.")); return Err(format!("Column number '{column}' is not valid."));
} }
@@ -295,7 +250,7 @@ impl Worksheet {
max: column, max: column,
width: width / constants::COLUMN_WIDTH_FACTOR, width: width / constants::COLUMN_WIDTH_FACTOR,
custom_width: true, custom_width: true,
style: None, style,
}; };
let mut index = 0; let mut index = 0;
let mut split = false; let mut split = false;
@@ -306,11 +261,9 @@ impl Worksheet {
if min == column && max == column { if min == column && max == column {
c.width = width / constants::COLUMN_WIDTH_FACTOR; c.width = width / constants::COLUMN_WIDTH_FACTOR;
return Ok(()); return Ok(());
} else {
// We need to split the result
split = true;
break;
} }
split = true;
break;
} }
if column < min { if column < min {
// We passed, we should insert at index // We passed, we should insert at index
@@ -351,7 +304,7 @@ impl Worksheet {
} }
/// Return the width of a column in pixels /// Return the width of a column in pixels
pub fn column_width(&self, column: i32) -> Result<f64, String> { pub fn get_column_width(&self, column: i32) -> Result<f64, String> {
if !is_valid_column_number(column) { if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid.")); return Err(format!("Column number '{column}' is not valid."));
} }
@@ -363,9 +316,8 @@ impl Worksheet {
if column >= min && column <= max { if column >= min && column <= max {
if col.custom_width { if col.custom_width {
return Ok(col.width * constants::COLUMN_WIDTH_FACTOR); return Ok(col.width * constants::COLUMN_WIDTH_FACTOR);
} else {
break;
} }
break;
} }
} }
Ok(constants::DEFAULT_COLUMN_WIDTH) Ok(constants::DEFAULT_COLUMN_WIDTH)

1
bindings/wasm/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/*

23
bindings/wasm/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "wasm"
version = "0.1.3"
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
description = "IronCalc Web bindings"
license = "MIT/Apache-2.0"
repository = "https://github.com/ironcalc/web-bindings"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
# Uses `../ironcalc/base` when used locally, and uses
# the inicated version from crates.io when published.
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../../base", version = "0.1" }
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2.92"
serde-wasm-bindgen = "0.4"
[dev-dependencies]
wasm-bindgen-test = "0.3.38"

20
bindings/wasm/Makefile Normal file
View File

@@ -0,0 +1,20 @@
all:
wasm-pack build --target web --scope ironcalc --release
cp README.pkg.md pkg/README.md
tsc types.ts --target esnext --module esnext
python fix_types.py
tests:
wasm-pack build --target nodejs && node tests/test.mjs
lint:
cargo check
cargo fmt -- --check
cargo clippy --all-targets --all-features -- -D warnings
clean:
cargo clean
rm -rf pkg
rm -f types.js
.PHONY: all lint clean

31
bindings/wasm/README.md Normal file
View File

@@ -0,0 +1,31 @@
# IronCalc Web bindings
This crate is used to build the web bindings for IronCalc.
Note that it does not contain the xlsx writer and reader, only the engine.
https://www.npmjs.com/package/@ironcalc/wasm?activeTab=readme
## Building
```bash
make
```
## Testing
Right now this is a manual process and only carries out a smoke test:
1. Build the package
2. Run `python -m http.server`
3. In your browser open <http://0.0.0.0:8000/test.html>
## Publishing
Follow the commands:
```bash
wasm-pack login
make
cd pkg
npm publish --access=public
```

View File

@@ -0,0 +1,33 @@
# IronCalc Web bindings
This package contains web bindings for IronCalc. Note that it does not contain the xlsx writer and reader, only the engine.
## Usage
In your project
```
npm install @ironcalc/wasm
```
And then in your TypeScript
```TypeScript
import init, { Model } from "@ironcalc/wasm";
await init();
function compute() {
const model = new Model('en', 'UTC');
model.setUserInput(0, 1, 1, "23");
model.setUserInput(0, 1, 2, "=A1*3+1");
const result = model.getFormattedCellValue(0, 1, 2);
console.log("Result: ", result);
}
compute();
```

View File

@@ -0,0 +1,97 @@
# Regrettably at the time of writing there is not a perfect way to
# generate the TypeScript types from Rust so we basically fix them manually
# Hopefully this will suffice for our needs and one day will be automatic
header = r"""
/* tslint:disable */
/* eslint-disable */
""".strip()
get_tokens_str = r"""
* @returns {any}
*/
export function getTokens(formula: string): any;
""".strip()
get_tokens_str_types = r"""
* @returns {MarkedToken[]}
*/
export function getTokens(formula: string): MarkedToken[];
""".strip()
update_style_str = r"""
/**
* @param {any} range
* @param {string} style_path
* @param {string} value
*/
updateRangeStyle(range: any, style_path: string, value: string): void;
""".strip()
update_style_str_types = r"""
/**
* @param {Area} range
* @param {string} style_path
* @param {string} value
*/
updateRangeStyle(range: Area, style_path: string, value: string): void;
""".strip()
properties = r"""
/**
* @returns {any}
*/
getWorksheetsProperties(): any;
""".strip()
properties_types = r"""
/**
* @returns {WorksheetProperties[]}
*/
getWorksheetsProperties(): WorksheetProperties[];
""".strip()
style = r"""
* @returns {any}
*/
getCellStyle(sheet: number, row: number, column: number): any;
""".strip()
style_types = r"""
* @returns {CellStyle}
*/
getCellStyle(sheet: number, row: number, column: number): CellStyle;
""".strip()
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
text = text.replace(properties, properties_types)
text = text.replace(style, style_types)
with open("types.ts") as f:
types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str)
text = text.replace(header, header_types)
return text
if __name__ == "__main__":
types_file = "pkg/wasm.d.ts"
with open(types_file) as f:
text = f.read()
text = fix_types(text)
with open(types_file, "wb") as f:
f.write(bytes(text, "utf8"))
js_file = "pkg/wasm.js"
with open("types.js") as f:
text_js = f.read()
with open(js_file) as f:
text = f.read()
with open(js_file, "wb") as f:
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))

341
bindings/wasm/src/lib.rs Normal file
View File

@@ -0,0 +1,341 @@
use wasm_bindgen::{
prelude::{wasm_bindgen, JsError},
JsValue,
};
use ironcalc_base::{
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
types::CellType,
UserModel as BaseModel,
};
fn to_js_error(error: String) -> JsError {
JsError::new(&error.to_string())
}
/// Return an array with a list of all the tokens from a formula
/// This is used by the UI to color them according to a theme.
#[wasm_bindgen(js_name = "getTokens")]
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
let tokens = tokenizer(formula);
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
}
#[wasm_bindgen]
pub struct Model {
model: BaseModel,
}
#[wasm_bindgen]
impl Model {
#[wasm_bindgen(constructor)]
pub fn new(locale: &str, timezone: &str) -> Result<Model, JsError> {
let model = BaseModel::new_empty("workbook", locale, timezone).map_err(to_js_error)?;
Ok(Model { model })
}
pub fn from_bytes(bytes: &[u8]) -> Result<Model, JsError> {
let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?;
Ok(Model { model })
}
pub fn undo(&mut self) -> Result<(), JsError> {
self.model.undo().map_err(to_js_error)
}
pub fn redo(&mut self) -> Result<(), JsError> {
self.model.redo().map_err(to_js_error)
}
#[wasm_bindgen(js_name = "canUndo")]
pub fn can_undo(&self) -> bool {
self.model.can_undo()
}
#[wasm_bindgen(js_name = "canRedo")]
pub fn can_redo(&self) -> bool {
self.model.can_redo()
}
#[wasm_bindgen(js_name = "pauseEvaluation")]
pub fn pause_evaluation(&mut self) {
self.model.pause_evaluation()
}
#[wasm_bindgen(js_name = "resumeEvaluation")]
pub fn resume_evaluation(&mut self) {
self.model.resume_evaluation()
}
pub fn evaluate(&mut self) {
self.model.evaluate();
}
#[wasm_bindgen(js_name = "flushSendQueue")]
pub fn flush_send_queue(&mut self) -> Vec<u8> {
self.model.flush_send_queue()
}
#[wasm_bindgen(js_name = "applyExternalDiffs")]
pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<(), JsError> {
self.model.apply_external_diffs(diffs).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getCellContent")]
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, JsError> {
self.model
.get_cell_content(sheet, row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "newSheet")]
pub fn new_sheet(&mut self) {
self.model.new_sheet()
}
#[wasm_bindgen(js_name = "deleteSheet")]
pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.delete_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "renameSheet")]
pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> {
self.model.rename_sheet(sheet, name).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "rangeClearAll")]
pub fn range_clear_all(
&mut self,
sheet: u32,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<(), JsError> {
let range = Area {
sheet,
row: start_row,
column: start_column,
width: end_column - start_column + 1,
height: end_row - start_row + 1,
};
self.model.range_clear_all(&range).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "rangeClearContents")]
pub fn range_clear_contents(
&mut self,
sheet: u32,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<(), JsError> {
let range = Area {
sheet,
row: start_row,
column: start_column,
width: end_column - start_column + 1,
height: end_row - start_row + 1,
};
self.model.range_clear_contents(&range).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "insertRow")]
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
self.model.insert_row(sheet, row).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "insertColumn")]
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
self.model.insert_column(sheet, column).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "deleteRow")]
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
self.model.delete_row(sheet, row).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "deleteColumn")]
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
self.model.delete_column(sheet, column).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setRowHeight")]
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), JsError> {
self.model
.set_row_height(sheet, row, height)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setColumnWidth")]
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), JsError> {
self.model
.set_column_width(sheet, column, width)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getRowHeight")]
pub fn get_row_height(&mut self, sheet: u32, row: i32) -> Result<f64, JsError> {
self.model.get_row_height(sheet, row).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getColumnWidth")]
pub fn get_column_width(&mut self, sheet: u32, column: i32) -> Result<f64, JsError> {
self.model
.get_column_width(sheet, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setUserInput")]
pub fn set_user_input(
&mut self,
sheet: u32,
row: i32,
column: i32,
input: &str,
) -> Result<(), JsError> {
self.model
.set_user_input(sheet, row, column, input)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getFormattedCellValue")]
pub fn get_formatted_cell_value(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<String, JsError> {
self.model
.get_formatted_cell_value(sheet, row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getFrozenRowsCount")]
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, JsError> {
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getFrozenColumnsCount")]
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, JsError> {
self.model
.get_frozen_columns_count(sheet)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setFrozenRowsCount")]
pub fn set_frozen_rows_count(&mut self, sheet: u32, count: i32) -> Result<(), JsError> {
self.model
.set_frozen_rows_count(sheet, count)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setFrozenColumnsCount")]
pub fn set_frozen_columns_count(&mut self, sheet: u32, count: i32) -> Result<(), JsError> {
self.model
.set_frozen_columns_count(sheet, count)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "updateRangeStyle")]
pub fn update_range_style(
&mut self,
range: JsValue,
style_path: &str,
value: &str,
) -> Result<(), JsError> {
let range: Area =
serde_wasm_bindgen::from_value(range).map_err(|e| to_js_error(e.to_string()))?;
self.model
.update_range_style(&range, style_path, value)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getCellStyle")]
pub fn get_cell_style(
&mut self,
sheet: u32,
row: i32,
column: i32,
) -> Result<JsValue, JsError> {
self.model
.get_cell_style(sheet, row, column)
.map_err(to_js_error)
.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
}
#[wasm_bindgen(js_name = "getCellType")]
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32, JsError> {
Ok(
match self
.model
.get_cell_type(sheet, row, column)
.map_err(to_js_error)?
{
CellType::Number => 1,
CellType::Text => 2,
CellType::LogicalValue => 4,
CellType::ErrorValue => 16,
CellType::Array => 64,
CellType::CompoundData => 128,
},
)
}
#[wasm_bindgen(js_name = "getWorksheetsProperties")]
pub fn get_worksheets_properties(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
}
#[wasm_bindgen(js_name = "getSelectedSheet")]
pub fn get_selected_sheet(&self) -> u32 {
self.model.get_selected_sheet()
}
#[wasm_bindgen(js_name = "getSelectedCell")]
pub fn get_selected_cell(&self) -> Vec<i32> {
let (sheet, row, column) = self.model.get_selected_cell();
vec![sheet as i32, row, column]
}
#[wasm_bindgen(js_name = "getSelectedView")]
pub fn get_selected_view(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
}
#[wasm_bindgen(js_name = "setSelectedSheet")]
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.set_selected_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setSelectedCell")]
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), JsError> {
self.model
.set_selected_cell(row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setSelectedRange")]
pub fn set_selected_range(
&mut self,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<(), JsError> {
self.model
.set_selected_range(start_row, start_column, end_row, end_column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setTopLeftVisibleCell")]
pub fn set_top_left_visible_cell(
&mut self,
top_row: i32,
top_column: i32,
) -> Result<(), JsError> {
self.model
.set_top_left_visible_cell(top_row, top_column)
.map_err(to_js_error)
}
}

28
bindings/wasm/test.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test IronCalc Web Bindings</title>
<script type="module">
import init, { Model } from "./pkg/wasm.js";
await init();
function test() {
const model = new Model('en', 'UTC');
model.setUserInput(0, 1, 1, "23");
model.setUserInput(0, 1, 2, "=A1*3+1");
const result = model.getFormattedCellValue(0, 1, 2);
console.assert(result === "70");
console.log("Hoooray! Tests passed");
}
test();
</script>
</head>
<body>
<div>Please have a look at the console</div>
</body>
</html>

View File

@@ -0,0 +1,123 @@
import test from 'node:test';
import assert from 'node:assert'
import { Model } from "../pkg/wasm.js";
test('Frozen rows and columns', () => {
let model = new Model('en', 'UTC');
assert.strictEqual(model.getFrozenRowsCount(0), 0);
assert.strictEqual(model.getFrozenColumnsCount(0), 0);
model.setFrozenColumnsCount(0, 4);
model.setFrozenRowsCount(0, 3)
assert.strictEqual(model.getFrozenRowsCount(0), 3);
assert.strictEqual(model.getFrozenColumnsCount(0), 4);
});
test('Row height', () => {
let model = new Model('en', 'UTC');
assert.strictEqual(model.getRowHeight(0, 3), 21);
model.setRowHeight(0, 3, 32);
assert.strictEqual(model.getRowHeight(0, 3), 32);
model.undo();
assert.strictEqual(model.getRowHeight(0, 3), 21);
model.redo();
assert.strictEqual(model.getRowHeight(0, 3), 32);
model.setRowHeight(0, 3, 320);
assert.strictEqual(model.getRowHeight(0, 3), 320);
});
test('Evaluates correctly', (t) => {
const model = new Model('en', 'UTC');
model.setUserInput(0, 1, 1, "23");
model.setUserInput(0, 1, 2, "=A1*3+1");
const result = model.getFormattedCellValue(0, 1, 2);
assert.strictEqual(result, "70");
});
test('Styles work', () => {
const model = new Model('en', 'UTC');
let style = model.getCellStyle(0, 1, 1);
assert.deepEqual(style, {
num_fmt: 'general',
fill: { pattern_type: 'none' },
font: {
sz: 11,
color: '#000000',
name: 'Calibri',
family: 2,
scheme: 'minor'
},
border: {},
quote_prefix: false
});
model.setUserInput(0, 1, 1, "'=1+1");
style = model.getCellStyle(0, 1, 1);
assert.deepEqual(style, {
num_fmt: 'general',
fill: { pattern_type: 'none' },
font: {
sz: 11,
color: '#000000',
name: 'Calibri',
family: 2,
scheme: 'minor'
},
border: {},
quote_prefix: true
});
});
test("Add sheets", (t) => {
const model = new Model('en', 'UTC');
model.newSheet();
model.renameSheet(1, "NewName");
let props = model.getWorksheetsProperties();
assert.deepEqual(props, [{
name: 'Sheet1',
sheet_id: 1,
state: 'visible'
},
{
name: 'NewName',
sheet_id: 2,
state: 'visible'
}
]);
});
test("invalid sheet index throws an exception", () => {
const model = new Model('en', 'UTC');
assert.throws(() => {
model.setRowHeight(1, 1, 100);
}, {
name: 'Error',
message: 'Invalid sheet index',
});
});
test("invalid column throws an exception", () => {
const model = new Model('en', 'UTC');
assert.throws(() => {
model.setRowHeight(0, -1, 100);
}, {
name: 'Error',
message: "Row number '-1' is not valid.",
});
});
test("floating column numbers get truncated", () => {
const model = new Model('en', 'UTC');
model.setRowHeight(0.8, 5.2, 100.5);
assert.strictEqual(model.getRowHeight(0.11, 5.99), 100.5);
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
});

194
bindings/wasm/types.ts Normal file
View File

@@ -0,0 +1,194 @@
export interface Area {
sheet: number;
row: number;
column: number;
width: number;
height: number;
}
type ErrorType =
| "REF"
| "NAME"
| "VALUE"
| "DIV"
| "NA"
| "NUM"
| "ERROR"
| "NIMPL"
| "SPILL"
| "CALC"
| "CIRC";
type OpCompareType =
| "LessThan"
| "GreaterThan"
| "Equal"
| "LessOrEqualThan"
| "GreaterOrEqualThan"
| "NonEqual";
type OpSumType = "Add" | "Minus";
type OpProductType = "Times" | "Divide";
interface ReferenceType {
sheet: string | null;
row: number;
column: number;
absolute_column: boolean;
absolute_row: boolean;
}
interface ParsedReferenceType {
column: number;
row: number;
absolute_column: boolean;
absolute_row: boolean;
}
interface Reference {
Reference: ReferenceType;
}
interface Range {
Range: {
sheet: string | null;
left: ParsedReferenceType;
right: ParsedReferenceType;
};
}
export type TokenType =
| "Illegal"
| "Eof"
| { Ident: string }
| { String: string }
| { Boolean: boolean }
| { Number: number }
| { ERROR: ErrorType }
| { COMPARE: OpCompareType }
| { SUM: OpSumType }
| { PRODUCT: OpProductType }
| "POWER"
| "LPAREN"
| "RPAREN"
| "COLON"
| "SEMICOLON"
| "LBRACKET"
| "RBRACKET"
| "LBRACE"
| "RBRACE"
| "COMMA"
| "BANG"
| "PERCENT"
| "AND"
| Reference
| Range;
export interface MarkedToken {
token: TokenType;
start: number;
end: number;
}
export interface WorksheetProperties {
name: string;
color: string;
sheet_id: number;
}
interface CellStyleFill {
pattern_type: string;
fg_color?: string;
bg_color?: string;
}
interface CellStyleFont {
u: boolean;
b: boolean;
i: boolean;
strike: boolean;
sz: number;
color: string;
name: string;
family: number;
scheme: string;
}
export enum BorderType {
BorderAll,
BorderInner,
BorderCenterH,
BorderCenterV,
BorderOuter,
BorderNone,
BorderTop,
BorderRight,
BorderBottom,
BorderLeft,
None,
}
export interface BorderOptions {
color: string;
style: BorderStyle;
border: BorderType;
}
export enum BorderStyle {
Thin = "thin",
Medium = "medium",
Thick = "thick",
Dashed = "dashed",
Dotted = "dotted",
Double = "double",
None = "none",
}
interface BorderItem {
style: string;
color: string;
}
interface CellStyleBorder {
diagonal_up?: boolean;
diagonal_down?: boolean;
left: BorderItem;
right: BorderItem;
top: BorderItem;
bottom: BorderItem;
diagonal: BorderItem;
}
export type VerticalAlignment =
| "bottom"
| "center"
| "distributed"
| "justify"
| "top";
export type HorizontalAlignment =
| "left"
| "center"
| "right"
| "general"
| "centerContinuous"
| "distributed"
| "fill"
| "justify";
interface Alignment {
horizontal: HorizontalAlignment;
vertical: VerticalAlignment;
wrap_text: boolean;
}
export interface CellStyle {
read_only: boolean;
quote_prefix: boolean;
fill: CellStyleFill;
font: CellStyleFont;
border: CellStyleBorder;
num_fmt: string;
alignment?: Alignment;
}

3
codecov.yml Normal file
View File

@@ -0,0 +1,3 @@
ignore:
- "xlsx/src/bin"
- "bindings/wasm"

2
solidjs_app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/*
dist/*

View File

@@ -0,0 +1,19 @@
import type { StorybookConfig } from "storybook-solidjs-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "storybook-solidjs-vite",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;

View File

@@ -0,0 +1,12 @@
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

8
solidjs_app/Makefile Normal file
View File

@@ -0,0 +1,8 @@
lint:
pnpm biome lint *
format:
pnpm biome format *
build:
pnpm run build

70
solidjs_app/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Web IronCalc
## Widgets
Toolbar
NavigationBar
FormulaBar
ColorPicker
Number Formatter
Border Picker
## Stack
Vite
TypeScript
SolidJs
Lucide Icons
BiomeJs
Storybook
pnpm
## Recreate
Install nodejs
Activate pnpm
corepack enable pnpm
Create app
pnpm create vite
pnpm install
add biomejs
pnpm add --save-dev --save-exact @biomejs/biome
pnpm biome init
add solidjs
add storybook
pnpm dlx storybook@latest init
add i18n
pnpm add @solid-primitives/i18n
(https://github.com/jfgodoy/vite-plugin-solid-svg)
add vite-plugin-solid-svg
add script: "restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
## Usage
```bash
$ pnpm install # or npm install or yarn install
```
## Available Scripts
In the project directory, you can run:
### `pnpm run dev`
Runs the app in the development mode.<br>
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
### `pnpm run build`
Builds the app for production to the `dist` folder.<br>
It correctly bundles Solid in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
## Deployment
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)

15
solidjs_app/biome.json Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"indentStyle": "space"
}
}

13
solidjs_app/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ironcalc_icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Solid + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

35
solidjs_app/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
"@solid-primitives/i18n": "^2.1.1",
"lucide-solid": "^0.379.0",
"solid-js": "^1.8.15"
},
"devDependencies": {
"@biomejs/biome": "1.7.0",
"@chromatic-com/storybook": "^1.3.3",
"@storybook/addon-essentials": "^8.0.8",
"@storybook/addon-interactions": "^8.0.8",
"@storybook/addon-links": "^8.0.8",
"@storybook/blocks": "^8.0.8",
"storybook": "^8.0.8",
"storybook-solidjs": "^1.0.0-beta.2",
"storybook-solidjs-vite": "^1.0.0-beta.2",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-solid": "^2.10.2",
"vite-plugin-solid-svg": "^0.8.1"
}
}

6864
solidjs_app/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

7
solidjs_app/src/App.css Normal file
View File

@@ -0,0 +1,7 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

29
solidjs_app/src/App.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { Show, createResource } from "solid-js";
// import "./App.css";
// import solidLogo from "./assets/solid.svg";
import init, { Model } from "@ironcalc/wasm";
import Workbook from "./components/Workbook";
const fetchModel = async () => {
await init();
// const model_bytes = new Uint8Array(
// await (await fetch("./example.ic")).arrayBuffer(),
// );
// const _model = Model.from_bytes(model_bytes);*/
const model = new Model("en", "UTC");
model.setUserInput(0, 1, 1, "=1+1");
return model;
};
function App() {
const [model] = createResource(fetchModel);
return (
<Show when={model()} fallback={<div>Loading...</div>}>
{(model) => <Workbook model={model()} />}
</Show>
);
}
export default App;

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