Compare commits
96 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c48b860337 | ||
|
|
e4ba28f72d | ||
|
|
6b84c9de60 | ||
|
|
c78bdb32fd | ||
|
|
81e96f1401 | ||
|
|
aa4ecb2c89 | ||
|
|
e116d7d29f | ||
|
|
cd75380923 | ||
|
|
79af9c6cb5 | ||
|
|
96fb0aaa96 | ||
|
|
03dfc151e2 | ||
|
|
d122f0cbd1 | ||
|
|
1476e8f6da | ||
|
|
8ca73c6224 | ||
|
|
1017eef981 | ||
|
|
1981b0833a | ||
|
|
fb2f2a9fcf | ||
|
|
91276b0f60 | ||
|
|
ec841f2fd9 | ||
|
|
8ebcb5dcb9 | ||
|
|
f7a3b95db5 | ||
|
|
911175f0d2 | ||
|
|
4d75f6b5c0 | ||
|
|
f3f59dbda7 | ||
|
|
f581ce5570 | ||
|
|
429615ae85 | ||
|
|
f2cb05d7bf | ||
|
|
71d6a3d66c | ||
|
|
07854f1593 | ||
|
|
faa0ff9b69 | ||
|
|
b9b3cb1628 | ||
|
|
b157347e68 | ||
|
|
fb7886ca9e | ||
|
|
caf26194df | ||
|
|
e420f7e998 | ||
|
|
d73b5ff12d | ||
|
|
d45e8fd56d | ||
|
|
c2777c73ac | ||
|
|
7dc49d5dd7 | ||
|
|
183d04b923 | ||
|
|
037766c744 | ||
|
|
d5ccd9dbdd | ||
|
|
3f1f2bb896 | ||
|
|
a2181a5a48 | ||
|
|
b07603b728 | ||
|
|
fe87dc49b4 | ||
|
|
b4349ff5da | ||
|
|
51f2da8663 | ||
|
|
87cdfb2ba1 | ||
|
|
d7113622e7 | ||
|
|
2d23f5d4e4 | ||
|
|
56abac79ca | ||
|
|
7193c9bf1b | ||
|
|
266c14d5d2 | ||
|
|
9852ce2504 | ||
|
|
107fc99409 | ||
|
|
77bb7ebe0e | ||
|
|
f8af302413 | ||
|
|
c700101f35 | ||
|
|
5f659a2eb5 | ||
|
|
40baf16a73 | ||
|
|
61c71cd6f6 | ||
|
|
b99ddbaee2 | ||
|
|
2428c6c89b | ||
|
|
46b1ade34a | ||
|
|
1eea2a8c46 | ||
|
|
eb8b129431 | ||
|
|
4850b6518f | ||
|
|
77a784df86 | ||
|
|
57896f83ca | ||
|
|
cfaa373510 | ||
|
|
cc140b087d | ||
|
|
42c651da29 | ||
|
|
2a5f001361 | ||
|
|
df913e73d4 | ||
|
|
198f3108ef | ||
|
|
3a68145848 | ||
|
|
5d7f4a31d6 | ||
|
|
7c8cd22ad8 | ||
|
|
84c3cf01ce | ||
|
|
33e9894f9b | ||
|
|
483cd43041 | ||
|
|
b4aed93bbb | ||
|
|
689f55effa | ||
|
|
b1327d83d4 | ||
|
|
8f7798d088 | ||
|
|
df0012a1c4 | ||
|
|
8a9ae00cad | ||
|
|
97d3b04772 | ||
|
|
5744ae4d77 | ||
|
|
0be7d9b85a | ||
|
|
46ea92966f | ||
|
|
a19124cc16 | ||
|
|
b0a5e2553a | ||
|
|
5ca50f15d7 | ||
|
|
03e227fbb2 |
4
.github/workflows/pypi.yml
vendored
4
.github/workflows/pypi.yml
vendored
@@ -117,7 +117,7 @@ jobs:
|
|||||||
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
|
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
|
||||||
with:
|
with:
|
||||||
command: upload
|
command: upload
|
||||||
args: "--skip-existing **/*.whl"
|
args: "--skip-existing **/*.whl **/*.tar.gz"
|
||||||
working-directory: bindings/python
|
working-directory: bindings/python
|
||||||
|
|
||||||
publish-pypi:
|
publish-pypi:
|
||||||
@@ -137,5 +137,5 @@ jobs:
|
|||||||
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
|
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
|
||||||
with:
|
with:
|
||||||
command: upload
|
command: upload
|
||||||
args: "--skip-existing **/*.whl"
|
args: "--skip-existing **/*.whl **/*.tar.gz"
|
||||||
working-directory: bindings/python
|
working-directory: bindings/python
|
||||||
|
|||||||
61
Cargo.lock
generated
61
Cargo.lock
generated
@@ -437,7 +437,6 @@ dependencies = [
|
|||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"csv",
|
"csv",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
@@ -721,11 +720,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3"
|
name = "pyo3"
|
||||||
version = "0.23.4"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
|
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"indoc",
|
"indoc",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
@@ -739,9 +737,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-build-config"
|
name = "pyo3-build-config"
|
||||||
version = "0.23.4"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
|
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
@@ -749,9 +747,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-ffi"
|
name = "pyo3-ffi"
|
||||||
version = "0.23.4"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
|
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pyo3-build-config",
|
"pyo3-build-config",
|
||||||
@@ -759,9 +757,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros"
|
name = "pyo3-macros"
|
||||||
version = "0.23.4"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
|
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"pyo3-macros-backend",
|
"pyo3-macros-backend",
|
||||||
@@ -771,9 +769,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros-backend"
|
name = "pyo3-macros-backend"
|
||||||
version = "0.23.4"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
|
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -784,8 +782,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyroncalc"
|
name = "pyroncalc"
|
||||||
version = "0.5.0"
|
version = "0.5.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitcode",
|
||||||
"ironcalc",
|
"ironcalc",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -872,6 +871,12 @@ version = "0.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@@ -979,9 +984,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
@@ -1081,23 +1086,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.92"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.92"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
@@ -1118,9 +1124,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.92"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -1128,9 +1134,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.92"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1141,9 +1147,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.92"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test"
|
name = "wasm-bindgen-test"
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -31,7 +31,12 @@ clean: remove-artifacts
|
|||||||
rm -r -f base/target
|
rm -r -f base/target
|
||||||
rm -r -f xlsx/target
|
rm -r -f xlsx/target
|
||||||
rm -r -f bindings/python/target
|
rm -r -f bindings/python/target
|
||||||
rm -r -f bindings/wasm/targets
|
rm -r -f bindings/wasm/target
|
||||||
|
rm -r -f bindings/wasm/pkg
|
||||||
|
rm -r -f webapp/IronCalc/node_modules
|
||||||
|
rm -r -f webapp/IronCalc/dist
|
||||||
|
rm -r -f webapp/app.ironcalc.com/frontend/node_modules
|
||||||
|
rm -r -f webapp/app.ironcalc.com/frontend/dist
|
||||||
rm -f cargo-test-*
|
rm -f cargo-test-*
|
||||||
rm -f base/cargo-test-*
|
rm -f base/cargo-test-*
|
||||||
rm -f xlsx/cargo-test-*
|
rm -f xlsx/cargo-test-*
|
||||||
|
|||||||
61
base/CALC.md
Normal file
61
base/CALC.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Evaluation Strategy
|
||||||
|
|
||||||
|
|
||||||
|
We have a list of the spill cells:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Checks if the array starting at cell will cover cells whose values
|
||||||
|
// has been requested
|
||||||
|
def CheckSpill(cell, array):
|
||||||
|
for c in cell+array:
|
||||||
|
support CellHasBeenRequested(c):
|
||||||
|
if support is not empty:
|
||||||
|
return support
|
||||||
|
return []
|
||||||
|
|
||||||
|
// Fills cells with the result (an array)
|
||||||
|
def FillCells(cell, result):
|
||||||
|
|
||||||
|
|
||||||
|
def EvaluateNodeInContext(node, context):
|
||||||
|
match node:
|
||||||
|
case OP(left, right, op):
|
||||||
|
l = EvaluateNodeInContext(left, context)?
|
||||||
|
r = EvaluateNodeInContext(left, context)?
|
||||||
|
return op(l, r)
|
||||||
|
case FUNCTION(args, fn):
|
||||||
|
...
|
||||||
|
case CELL(cell):
|
||||||
|
EvaluateCell(cell)
|
||||||
|
case RANGE(start, end):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def EvaluateCell(cell):
|
||||||
|
if IsCellEvaluating(cell):
|
||||||
|
return CIRC
|
||||||
|
MarkEvaluating(cell)
|
||||||
|
result = EvaluateNodeInContext(cell.formula, cell)
|
||||||
|
if isSpill(result):
|
||||||
|
CheckSpill(cell, array)?
|
||||||
|
FillCells(result)
|
||||||
|
|
||||||
|
|
||||||
|
def EvaluateWorkbook():
|
||||||
|
spill_cells = [cell_1, ...., cell_n];
|
||||||
|
|
||||||
|
|
||||||
|
for cell in spill_cells:
|
||||||
|
result = evaluate(cell)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# When updating a cell value
|
||||||
|
|
||||||
|
If it was a spill cell we nee
|
||||||
@@ -17,7 +17,6 @@ chrono = "0.4"
|
|||||||
chrono-tz = "0.10"
|
chrono-tz = "0.10"
|
||||||
regex = { version = "1.0", optional = true}
|
regex = { version = "1.0", optional = true}
|
||||||
regex-lite = { version = "0.1.6", optional = true}
|
regex-lite = { version = "0.1.6", optional = true}
|
||||||
once_cell = "1.16.0"
|
|
||||||
bitcode = "0.6.3"
|
bitcode = "0.6.3"
|
||||||
csv = "1.3.0"
|
csv = "1.3.0"
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl Model {
|
|||||||
.cell(row, column)
|
.cell(row, column)
|
||||||
.and_then(|c| c.get_formula())
|
.and_then(|c| c.get_formula())
|
||||||
{
|
{
|
||||||
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
|
let node = &self.parsed_formulas[sheet as usize][f as usize].0.clone();
|
||||||
let cell_reference = CellReferenceRC {
|
let cell_reference = CellReferenceRC {
|
||||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||||
row,
|
row,
|
||||||
@@ -212,6 +212,12 @@ impl Model {
|
|||||||
if column_count <= 0 {
|
if column_count <= 0 {
|
||||||
return Err("Please use insert columns instead".to_string());
|
return Err("Please use insert columns instead".to_string());
|
||||||
}
|
}
|
||||||
|
if !(1..=LAST_COLUMN).contains(&column) {
|
||||||
|
return Err(format!("Column number '{column}' is not valid."));
|
||||||
|
}
|
||||||
|
if column + column_count - 1 > LAST_COLUMN {
|
||||||
|
return Err("Cannot delete columns beyond the last column of the sheet".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
// first column being deleted
|
// first column being deleted
|
||||||
let column_start = column;
|
let column_start = column;
|
||||||
@@ -384,6 +390,13 @@ impl Model {
|
|||||||
if row_count <= 0 {
|
if row_count <= 0 {
|
||||||
return Err("Please use insert rows instead".to_string());
|
return Err("Please use insert rows instead".to_string());
|
||||||
}
|
}
|
||||||
|
if !(1..=LAST_ROW).contains(&row) {
|
||||||
|
return Err(format!("Row number '{row}' is not valid."));
|
||||||
|
}
|
||||||
|
if row + row_count - 1 > LAST_ROW {
|
||||||
|
return Err("Cannot delete rows beyond the last row of the sheet".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
// Move cells
|
// Move cells
|
||||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||||
@@ -444,7 +457,7 @@ impl Model {
|
|||||||
/// * Column is one of the extremes of the range. The new extreme would be target_column.
|
/// * Column is one of the extremes of the range. The new extreme would be target_column.
|
||||||
/// Range is then normalized
|
/// Range is then normalized
|
||||||
/// * Any other case, range is left unchanged.
|
/// * Any other case, range is left unchanged.
|
||||||
/// NOTE: This does NOT move the data in the columns or move the colum styles
|
/// NOTE: This moves the data and column styles along with the formulas
|
||||||
pub fn move_column_action(
|
pub fn move_column_action(
|
||||||
&mut self,
|
&mut self,
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
@@ -460,7 +473,70 @@ impl Model {
|
|||||||
return Err("Initial column out of boundaries".to_string());
|
return Err("Initial column out of boundaries".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add the actual displacement of data and styles
|
if delta == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve cell contents, width and style of the column being moved
|
||||||
|
let original_refs = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.column_cell_references(column)?;
|
||||||
|
let mut original_cells = Vec::new();
|
||||||
|
for r in &original_refs {
|
||||||
|
let cell = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(r.row, column)
|
||||||
|
.ok_or("Expected Cell to exist")?;
|
||||||
|
let style_idx = cell.get_style();
|
||||||
|
let formula_or_value = self
|
||||||
|
.get_cell_formula(sheet, r.row, column)?
|
||||||
|
.unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||||
|
original_cells.push((r.row, formula_or_value, style_idx));
|
||||||
|
self.cell_clear_all(sheet, r.row, column)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = self.workbook.worksheet(sheet)?.get_column_width(column)?;
|
||||||
|
let style = self.workbook.worksheet(sheet)?.get_column_style(column)?;
|
||||||
|
|
||||||
|
if delta > 0 {
|
||||||
|
for c in column + 1..=target_column {
|
||||||
|
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
|
||||||
|
for r in refs {
|
||||||
|
self.move_cell(sheet, r.row, c, r.row, c - 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
|
||||||
|
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_column_width_and_style(c - 1, w, s)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for c in (target_column..=column - 1).rev() {
|
||||||
|
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
|
||||||
|
for r in refs {
|
||||||
|
self.move_cell(sheet, r.row, c, r.row, c + 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
|
||||||
|
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_column_width_and_style(c + 1, w, s)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (r, value, style_idx) in original_cells {
|
||||||
|
self.set_user_input(sheet, r, target_column, value)?;
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_cell_style(r, target_column, style_idx)?;
|
||||||
|
}
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_column_width_and_style(target_column, width, style)?;
|
||||||
|
|
||||||
// Update all formulas in the workbook
|
// Update all formulas in the workbook
|
||||||
self.displace_cells(
|
self.displace_cells(
|
||||||
@@ -473,4 +549,88 @@ impl Model {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Displaces cells due to a move row action
|
||||||
|
/// from initial_row to target_row = initial_row + row_delta
|
||||||
|
/// References will be updated following the same rules as move_column_action
|
||||||
|
/// NOTE: This moves the data and row styles along with the formulas
|
||||||
|
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
|
||||||
|
// Check boundaries
|
||||||
|
let target_row = row + delta;
|
||||||
|
if !(1..=LAST_ROW).contains(&target_row) {
|
||||||
|
return Err("Target row out of boundaries".to_string());
|
||||||
|
}
|
||||||
|
if !(1..=LAST_ROW).contains(&row) {
|
||||||
|
return Err("Initial row out of boundaries".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if delta == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let original_cols = self.get_columns_for_row(sheet, row, false)?;
|
||||||
|
let mut original_cells = Vec::new();
|
||||||
|
for c in &original_cols {
|
||||||
|
let cell = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(row, *c)
|
||||||
|
.ok_or("Expected Cell to exist")?;
|
||||||
|
let style_idx = cell.get_style();
|
||||||
|
let formula_or_value = self
|
||||||
|
.get_cell_formula(sheet, row, *c)?
|
||||||
|
.unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||||
|
original_cells.push((*c, formula_or_value, style_idx));
|
||||||
|
self.cell_clear_all(sheet, row, *c)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if delta > 0 {
|
||||||
|
for r in row + 1..=target_row {
|
||||||
|
let cols = self.get_columns_for_row(sheet, r, false)?;
|
||||||
|
for c in cols {
|
||||||
|
self.move_cell(sheet, r, c, r - 1, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for r in (target_row..=row - 1).rev() {
|
||||||
|
let cols = self.get_columns_for_row(sheet, r, false)?;
|
||||||
|
for c in cols {
|
||||||
|
self.move_cell(sheet, r, c, r + 1, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (c, value, style_idx) in original_cells {
|
||||||
|
self.set_user_input(sheet, target_row, c, value)?;
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_cell_style(target_row, c, style_idx)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||||
|
let mut new_rows = Vec::new();
|
||||||
|
for r in worksheet.rows.iter() {
|
||||||
|
if r.r == row {
|
||||||
|
let mut nr = r.clone();
|
||||||
|
nr.r = target_row;
|
||||||
|
new_rows.push(nr);
|
||||||
|
} else if delta > 0 && r.r > row && r.r <= target_row {
|
||||||
|
let mut nr = r.clone();
|
||||||
|
nr.r -= 1;
|
||||||
|
new_rows.push(nr);
|
||||||
|
} else if delta < 0 && r.r < row && r.r >= target_row {
|
||||||
|
let mut nr = r.clone();
|
||||||
|
nr.r += 1;
|
||||||
|
new_rows.push(nr);
|
||||||
|
} else {
|
||||||
|
new_rows.push(r.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worksheet.rows = new_rows;
|
||||||
|
|
||||||
|
// Update all formulas in the workbook
|
||||||
|
self.displace_cells(&(DisplaceData::RowMove { sheet, row, delta }))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ impl Model {
|
|||||||
match to_f64(&node) {
|
match to_f64(&node) {
|
||||||
Ok(f2) => match op(f1, f2) {
|
Ok(f2) => match op(f1, f2) {
|
||||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
|
||||||
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
|
|
||||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||||
},
|
},
|
||||||
Err(err) => data_row.push(ArrayNode::Error(err)),
|
Err(err) => data_row.push(ArrayNode::Error(err)),
|
||||||
@@ -100,8 +98,6 @@ impl Model {
|
|||||||
match to_f64(&node) {
|
match to_f64(&node) {
|
||||||
Ok(f1) => match op(f1, f2) {
|
Ok(f1) => match op(f1, f2) {
|
||||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
|
||||||
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
|
|
||||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||||
},
|
},
|
||||||
Err(err) => data_row.push(ArrayNode::Error(err)),
|
Err(err) => data_row.push(ArrayNode::Error(err)),
|
||||||
@@ -137,10 +133,6 @@ impl Model {
|
|||||||
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
|
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
|
||||||
(Ok(f1), Ok(f2)) => match op(f1, f2) {
|
(Ok(f1), Ok(f2)) => match op(f1, f2) {
|
||||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
|
||||||
Err(Error::VALUE) => {
|
|
||||||
data_row.push(ArrayNode::Error(Error::VALUE))
|
|
||||||
}
|
|
||||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||||
},
|
},
|
||||||
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),
|
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ impl Model {
|
|||||||
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
|
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
|
||||||
// See function Exact
|
// See function Exact
|
||||||
match result {
|
match result {
|
||||||
CalcResult::Number(f) => Ok(format!("{}", f)),
|
CalcResult::Number(f) => Ok(format!("{f}")),
|
||||||
CalcResult::String(s) => Ok(s),
|
CalcResult::String(s) => Ok(s),
|
||||||
CalcResult::Boolean(f) => {
|
CalcResult::Boolean(f) => {
|
||||||
if f {
|
if f {
|
||||||
|
|||||||
100
base/src/cell.rs
100
base/src/cell.rs
@@ -64,12 +64,50 @@ impl Cell {
|
|||||||
/// Returns the formula of a cell if any.
|
/// Returns the formula of a cell if any.
|
||||||
pub fn get_formula(&self) -> Option<i32> {
|
pub fn get_formula(&self) -> Option<i32> {
|
||||||
match self {
|
match self {
|
||||||
Cell::CellFormula { f, .. } => Some(*f),
|
Cell::CellFormula { f, .. }
|
||||||
Cell::CellFormulaBoolean { f, .. } => Some(*f),
|
| Cell::CellFormulaBoolean { f, .. }
|
||||||
Cell::CellFormulaNumber { f, .. } => Some(*f),
|
| Cell::CellFormulaNumber { f, .. }
|
||||||
Cell::CellFormulaString { f, .. } => Some(*f),
|
| Cell::CellFormulaString { f, .. }
|
||||||
Cell::CellFormulaError { f, .. } => Some(*f),
|
| Cell::CellFormulaError { f, .. }
|
||||||
_ => None,
|
| Cell::DynamicCellFormula { f, .. }
|
||||||
|
| Cell::DynamicCellFormulaBoolean { f, .. }
|
||||||
|
| Cell::DynamicCellFormulaNumber { f, .. }
|
||||||
|
| Cell::DynamicCellFormulaString { f, .. }
|
||||||
|
| Cell::DynamicCellFormulaError { f, .. } => Some(*f),
|
||||||
|
Cell::EmptyCell { .. }
|
||||||
|
| Cell::BooleanCell { .. }
|
||||||
|
| Cell::NumberCell { .. }
|
||||||
|
| Cell::ErrorCell { .. }
|
||||||
|
| Cell::SharedString { .. }
|
||||||
|
| Cell::SpillNumberCell { .. }
|
||||||
|
| Cell::SpillBooleanCell { .. }
|
||||||
|
| Cell::SpillErrorCell { .. }
|
||||||
|
| Cell::SpillStringCell { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the dynamic range of a cell if any.
|
||||||
|
pub fn get_dynamic_range(&self) -> Option<(i32, i32)> {
|
||||||
|
match self {
|
||||||
|
Cell::DynamicCellFormula { r, .. } => Some(*r),
|
||||||
|
Cell::DynamicCellFormulaBoolean { r, .. } => Some(*r),
|
||||||
|
Cell::DynamicCellFormulaNumber { r, .. } => Some(*r),
|
||||||
|
Cell::DynamicCellFormulaString { r, .. } => Some(*r),
|
||||||
|
Cell::DynamicCellFormulaError { r, .. } => Some(*r),
|
||||||
|
Cell::EmptyCell { .. }
|
||||||
|
| Cell::BooleanCell { .. }
|
||||||
|
| Cell::NumberCell { .. }
|
||||||
|
| Cell::ErrorCell { .. }
|
||||||
|
| Cell::SharedString { .. }
|
||||||
|
| Cell::CellFormula { .. }
|
||||||
|
| Cell::CellFormulaBoolean { .. }
|
||||||
|
| Cell::CellFormulaNumber { .. }
|
||||||
|
| Cell::CellFormulaString { .. }
|
||||||
|
| Cell::CellFormulaError { .. }
|
||||||
|
| Cell::SpillNumberCell { .. }
|
||||||
|
| Cell::SpillBooleanCell { .. }
|
||||||
|
| Cell::SpillErrorCell { .. }
|
||||||
|
| Cell::SpillStringCell { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +127,15 @@ impl Cell {
|
|||||||
Cell::CellFormulaNumber { s, .. } => *s = style,
|
Cell::CellFormulaNumber { s, .. } => *s = style,
|
||||||
Cell::CellFormulaString { s, .. } => *s = style,
|
Cell::CellFormulaString { s, .. } => *s = style,
|
||||||
Cell::CellFormulaError { s, .. } => *s = style,
|
Cell::CellFormulaError { s, .. } => *s = style,
|
||||||
|
Cell::SpillBooleanCell { s, .. } => *s = style,
|
||||||
|
Cell::SpillNumberCell { s, .. } => *s = style,
|
||||||
|
Cell::SpillStringCell { s, .. } => *s = style,
|
||||||
|
Cell::SpillErrorCell { s, .. } => *s = style,
|
||||||
|
Cell::DynamicCellFormula { s, .. } => *s = style,
|
||||||
|
Cell::DynamicCellFormulaBoolean { s, .. } => *s = style,
|
||||||
|
Cell::DynamicCellFormulaNumber { s, .. } => *s = style,
|
||||||
|
Cell::DynamicCellFormulaString { s, .. } => *s = style,
|
||||||
|
Cell::DynamicCellFormulaError { s, .. } => *s = style,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +151,15 @@ impl Cell {
|
|||||||
Cell::CellFormulaNumber { s, .. } => *s,
|
Cell::CellFormulaNumber { s, .. } => *s,
|
||||||
Cell::CellFormulaString { s, .. } => *s,
|
Cell::CellFormulaString { s, .. } => *s,
|
||||||
Cell::CellFormulaError { s, .. } => *s,
|
Cell::CellFormulaError { s, .. } => *s,
|
||||||
|
Cell::SpillBooleanCell { s, .. } => *s,
|
||||||
|
Cell::SpillNumberCell { s, .. } => *s,
|
||||||
|
Cell::SpillStringCell { s, .. } => *s,
|
||||||
|
Cell::SpillErrorCell { s, .. } => *s,
|
||||||
|
Cell::DynamicCellFormula { s, .. } => *s,
|
||||||
|
Cell::DynamicCellFormulaBoolean { s, .. } => *s,
|
||||||
|
Cell::DynamicCellFormulaNumber { s, .. } => *s,
|
||||||
|
Cell::DynamicCellFormulaString { s, .. } => *s,
|
||||||
|
Cell::DynamicCellFormulaError { s, .. } => *s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +175,15 @@ impl Cell {
|
|||||||
Cell::CellFormulaNumber { .. } => CellType::Number,
|
Cell::CellFormulaNumber { .. } => CellType::Number,
|
||||||
Cell::CellFormulaString { .. } => CellType::Text,
|
Cell::CellFormulaString { .. } => CellType::Text,
|
||||||
Cell::CellFormulaError { .. } => CellType::ErrorValue,
|
Cell::CellFormulaError { .. } => CellType::ErrorValue,
|
||||||
|
Cell::SpillBooleanCell { .. } => CellType::LogicalValue,
|
||||||
|
Cell::SpillNumberCell { .. } => CellType::Number,
|
||||||
|
Cell::SpillStringCell { .. } => CellType::Text,
|
||||||
|
Cell::SpillErrorCell { .. } => CellType::ErrorValue,
|
||||||
|
Cell::DynamicCellFormula { .. } => CellType::Number,
|
||||||
|
Cell::DynamicCellFormulaBoolean { .. } => CellType::LogicalValue,
|
||||||
|
Cell::DynamicCellFormulaNumber { .. } => CellType::Number,
|
||||||
|
Cell::DynamicCellFormulaString { .. } => CellType::Text,
|
||||||
|
Cell::DynamicCellFormulaError { .. } => CellType::ErrorValue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +201,7 @@ impl Cell {
|
|||||||
Cell::EmptyCell { .. } => CellValue::None,
|
Cell::EmptyCell { .. } => CellValue::None,
|
||||||
Cell::BooleanCell { v, s: _ } => CellValue::Boolean(*v),
|
Cell::BooleanCell { v, s: _ } => CellValue::Boolean(*v),
|
||||||
Cell::NumberCell { v, s: _ } => CellValue::Number(*v),
|
Cell::NumberCell { v, s: _ } => CellValue::Number(*v),
|
||||||
Cell::ErrorCell { ei, .. } => {
|
Cell::ErrorCell { ei, .. } | Cell::SpillErrorCell { ei, .. } => {
|
||||||
let v = ei.to_localized_error_string(language);
|
let v = ei.to_localized_error_string(language);
|
||||||
CellValue::String(v)
|
CellValue::String(v)
|
||||||
}
|
}
|
||||||
@@ -148,14 +213,25 @@ impl Cell {
|
|||||||
};
|
};
|
||||||
CellValue::String(v)
|
CellValue::String(v)
|
||||||
}
|
}
|
||||||
Cell::CellFormula { .. } => CellValue::String("#ERROR!".to_string()),
|
Cell::DynamicCellFormula { .. } | Cell::CellFormula { .. } => {
|
||||||
Cell::CellFormulaBoolean { v, .. } => CellValue::Boolean(*v),
|
CellValue::String("#ERROR!".to_string())
|
||||||
Cell::CellFormulaNumber { v, .. } => CellValue::Number(*v),
|
}
|
||||||
Cell::CellFormulaString { v, .. } => CellValue::String(v.clone()),
|
Cell::DynamicCellFormulaBoolean { v, .. } | Cell::CellFormulaBoolean { v, .. } => {
|
||||||
Cell::CellFormulaError { ei, .. } => {
|
CellValue::Boolean(*v)
|
||||||
|
}
|
||||||
|
Cell::DynamicCellFormulaNumber { v, .. } | Cell::CellFormulaNumber { v, .. } => {
|
||||||
|
CellValue::Number(*v)
|
||||||
|
}
|
||||||
|
Cell::DynamicCellFormulaString { v, .. } | Cell::CellFormulaString { v, .. } => {
|
||||||
|
CellValue::String(v.clone())
|
||||||
|
}
|
||||||
|
Cell::DynamicCellFormulaError { ei, .. } | Cell::CellFormulaError { ei, .. } => {
|
||||||
let v = ei.to_localized_error_string(language);
|
let v = ei.to_localized_error_string(language);
|
||||||
CellValue::String(v)
|
CellValue::String(v)
|
||||||
}
|
}
|
||||||
|
Cell::SpillBooleanCell { v, .. } => CellValue::Boolean(*v),
|
||||||
|
Cell::SpillNumberCell { v, .. } => CellValue::Number(*v),
|
||||||
|
Cell::SpillStringCell { v, .. } => CellValue::String(v.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ impl Lexer {
|
|||||||
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
|
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
|
||||||
let nt = self.next_token();
|
let nt = self.next_token();
|
||||||
if mem::discriminant(&nt) != mem::discriminant(&tk) {
|
if mem::discriminant(&nt) != mem::discriminant(&tk) {
|
||||||
return Err(self.set_error(&format!("Error, expected {:?}", tk), self.position));
|
return Err(self.set_error(&format!("Error, expected {tk:?}"), self.position));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -314,6 +314,9 @@ impl Lexer {
|
|||||||
} else if name_upper == self.language.booleans.r#false {
|
} else if name_upper == self.language.booleans.r#false {
|
||||||
return TokenType::Boolean(false);
|
return TokenType::Boolean(false);
|
||||||
}
|
}
|
||||||
|
if self.peek_char() == Some('(') {
|
||||||
|
return TokenType::Ident(name);
|
||||||
|
}
|
||||||
if self.mode == LexerMode::A1 {
|
if self.mode == LexerMode::A1 {
|
||||||
let parsed_reference = utils::parse_reference_a1(&name_upper);
|
let parsed_reference = utils::parse_reference_a1(&name_upper);
|
||||||
if parsed_reference.is_some()
|
if parsed_reference.is_some()
|
||||||
@@ -511,7 +514,7 @@ impl Lexer {
|
|||||||
self.position = position;
|
self.position = position;
|
||||||
chars.parse::<i32>().map_err(|_| LexerError {
|
chars.parse::<i32>().map_err(|_| LexerError {
|
||||||
position,
|
position,
|
||||||
message: format!("Failed to parse to int: {}", chars),
|
message: format!("Failed to parse to int: {chars}"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,9 +575,7 @@ impl Lexer {
|
|||||||
}
|
}
|
||||||
self.position = position;
|
self.position = position;
|
||||||
match chars.parse::<f64>() {
|
match chars.parse::<f64>() {
|
||||||
Err(_) => {
|
Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)),
|
||||||
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
|
|
||||||
}
|
|
||||||
Ok(v) => Ok(v),
|
Ok(v) => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,15 +148,16 @@ impl Lexer {
|
|||||||
let row_left = match row_left.parse::<i32>() {
|
let row_left = match row_left.parse::<i32>() {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(self
|
return Err(
|
||||||
.set_error(&format!("Failed parsing row {}", row_left), position))
|
self.set_error(&format!("Failed parsing row {row_left}"), position)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let row_right = match row_right.parse::<i32>() {
|
let row_right = match row_right.parse::<i32>() {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(self
|
return Err(self
|
||||||
.set_error(&format!("Failed parsing row {}", row_right), position))
|
.set_error(&format!("Failed parsing row {row_right}"), position))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if row_left > LAST_ROW {
|
if row_left > LAST_ROW {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::expressions::utils::column_to_number;
|
||||||
use crate::language::get_language;
|
use crate::language::get_language;
|
||||||
use crate::locale::get_locale;
|
use crate::locale::get_locale;
|
||||||
|
|
||||||
@@ -685,3 +686,29 @@ fn test_comparisons() {
|
|||||||
assert_eq!(lx.next_token(), Number(7.0));
|
assert_eq!(lx.next_token(), Number(7.0));
|
||||||
assert_eq!(lx.next_token(), EOF);
|
assert_eq!(lx.next_token(), EOF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_log10_is_cell_reference() {
|
||||||
|
let mut lx = new_lexer("LOG10", true);
|
||||||
|
assert_eq!(
|
||||||
|
lx.next_token(),
|
||||||
|
Reference {
|
||||||
|
sheet: None,
|
||||||
|
column: column_to_number("LOG").unwrap(),
|
||||||
|
row: 10,
|
||||||
|
absolute_column: false,
|
||||||
|
absolute_row: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(lx.next_token(), EOF);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_log10_is_function() {
|
||||||
|
let mut lx = new_lexer("LOG10(100)", true);
|
||||||
|
assert_eq!(lx.next_token(), Ident("LOG10".to_string()));
|
||||||
|
assert_eq!(lx.next_token(), LeftParenthesis);
|
||||||
|
assert_eq!(lx.next_token(), Number(100.0));
|
||||||
|
assert_eq!(lx.next_token(), RightParenthesis);
|
||||||
|
assert_eq!(lx.next_token(), EOF);
|
||||||
|
}
|
||||||
|
|||||||
@@ -828,7 +828,7 @@ impl Parser {
|
|||||||
| TokenType::Percent => Node::ParseErrorKind {
|
| TokenType::Percent => Node::ParseErrorKind {
|
||||||
formula: self.lexer.get_formula(),
|
formula: self.lexer.get_formula(),
|
||||||
position: 0,
|
position: 0,
|
||||||
message: format!("Unexpected token: '{:?}'", next_token),
|
message: format!("Unexpected token: '{next_token:?}'"),
|
||||||
},
|
},
|
||||||
TokenType::LeftBracket => Node::ParseErrorKind {
|
TokenType::LeftBracket => Node::ParseErrorKind {
|
||||||
formula: self.lexer.get_formula(),
|
formula: self.lexer.get_formula(),
|
||||||
|
|||||||
@@ -53,24 +53,24 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
|
|||||||
arguments = to_string_moved(el, move_context);
|
arguments = to_string_moved(el, move_context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("{}({})", name, arguments)
|
format!("{name}({arguments})")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
|
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
|
||||||
match node {
|
match node {
|
||||||
ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(),
|
ArrayNode::Boolean(value) => format!("{value}").to_ascii_uppercase(),
|
||||||
ArrayNode::Number(number) => to_excel_precision_str(*number),
|
ArrayNode::Number(number) => to_excel_precision_str(*number),
|
||||||
ArrayNode::String(value) => format!("\"{}\"", value),
|
ArrayNode::String(value) => format!("\"{value}\""),
|
||||||
ArrayNode::Error(kind) => format!("{}", kind),
|
ArrayNode::Error(kind) => format!("{kind}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||||
use self::Node::*;
|
use self::Node::*;
|
||||||
match node {
|
match node {
|
||||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
|
||||||
NumberKind(number) => to_excel_precision_str(*number),
|
NumberKind(number) => to_excel_precision_str(*number),
|
||||||
StringKind(value) => format!("\"{}\"", value),
|
StringKind(value) => format!("\"{value}\""),
|
||||||
ReferenceKind {
|
ReferenceKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
sheet_index,
|
sheet_index,
|
||||||
@@ -241,7 +241,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{}:{}", s1, s2)
|
format!("{s1}:{s2}")
|
||||||
}
|
}
|
||||||
WrongReferenceKind {
|
WrongReferenceKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
@@ -325,7 +325,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{}:{}", s1, s2)
|
format!("{s1}:{s2}")
|
||||||
}
|
}
|
||||||
OpRangeKind { left, right } => format!(
|
OpRangeKind { left, right } => format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
@@ -358,7 +358,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
}
|
}
|
||||||
_ => to_string_moved(right, move_context),
|
_ => to_string_moved(right, move_context),
|
||||||
};
|
};
|
||||||
format!("{}{}{}", x, kind, y)
|
format!("{x}{kind}{y}")
|
||||||
}
|
}
|
||||||
OpPowerKind { left, right } => format!(
|
OpPowerKind { left, right } => format!(
|
||||||
"{}^{}",
|
"{}^{}",
|
||||||
@@ -403,7 +403,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enclose the whole matrix in braces
|
// Enclose the whole matrix in braces
|
||||||
format!("{{{}}}", matrix_string)
|
format!("{{{matrix_string}}}")
|
||||||
}
|
}
|
||||||
DefinedNameKind((name, ..)) => name.to_string(),
|
DefinedNameKind((name, ..)) => name.to_string(),
|
||||||
TableNameKind(name) => name.to_string(),
|
TableNameKind(name) => name.to_string(),
|
||||||
@@ -418,7 +418,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
||||||
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
||||||
},
|
},
|
||||||
ErrorKind(kind) => format!("{}", kind),
|
ErrorKind(kind) => format!("{kind}"),
|
||||||
ParseErrorKind {
|
ParseErrorKind {
|
||||||
formula,
|
formula,
|
||||||
message: _,
|
message: _,
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ use crate::functions::Function;
|
|||||||
|
|
||||||
use super::Node;
|
use super::Node;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static RANGE_REFERENCE_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||||
|
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
static RE: Lazy<Regex> =
|
fn get_re() -> &'static Regex {
|
||||||
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
|
RANGE_REFERENCE_REGEX
|
||||||
|
.get_or_init(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"))
|
||||||
|
}
|
||||||
|
|
||||||
fn is_range_reference(s: &str) -> bool {
|
fn is_range_reference(s: &str) -> bool {
|
||||||
RE.is_match(s)
|
get_re().is_match(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -182,7 +186,8 @@ pub fn add_implicit_intersection(node: &mut Node, add: bool) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum StaticResult {
|
#[derive(Clone)]
|
||||||
|
pub enum StaticResult {
|
||||||
Scalar,
|
Scalar,
|
||||||
Array(i32, i32),
|
Array(i32, i32),
|
||||||
Range(i32, i32),
|
Range(i32, i32),
|
||||||
@@ -218,7 +223,7 @@ fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
|
|||||||
// * Array(a, b) if we know it will be an a x b array.
|
// * Array(a, b) if we know it will be an a x b array.
|
||||||
// * Range(a, b) if we know it will be a a x b range.
|
// * Range(a, b) if we know it will be a a x b range.
|
||||||
// * Unknown if we cannot guaranty either
|
// * Unknown if we cannot guaranty either
|
||||||
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
|
pub(crate) fn run_static_analysis_on_node(node: &Node) -> StaticResult {
|
||||||
match node {
|
match node {
|
||||||
Node::BooleanKind(_)
|
Node::BooleanKind(_)
|
||||||
| Node::NumberKind(_)
|
| Node::NumberKind(_)
|
||||||
@@ -605,6 +610,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Choose => vec![Signature::Scalar; arg_count],
|
Function::Choose => vec![Signature::Scalar; arg_count],
|
||||||
Function::Column => args_signature_row(arg_count),
|
Function::Column => args_signature_row(arg_count),
|
||||||
Function::Columns => args_signature_one_vector(arg_count),
|
Function::Columns => args_signature_one_vector(arg_count),
|
||||||
|
Function::Ln => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::Log => args_signature_scalars(arg_count, 1, 1),
|
||||||
|
Function::Log10 => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Cos => args_signature_scalars(arg_count, 1, 0),
|
Function::Cos => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
|
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Max => vec![Signature::Vector; arg_count],
|
Function::Max => vec![Signature::Vector; arg_count],
|
||||||
@@ -816,6 +824,9 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Round => scalar_arguments(args),
|
Function::Round => scalar_arguments(args),
|
||||||
Function::Rounddown => scalar_arguments(args),
|
Function::Rounddown => scalar_arguments(args),
|
||||||
Function::Roundup => scalar_arguments(args),
|
Function::Roundup => scalar_arguments(args),
|
||||||
|
Function::Ln => scalar_arguments(args),
|
||||||
|
Function::Log => scalar_arguments(args),
|
||||||
|
Function::Log10 => scalar_arguments(args),
|
||||||
Function::Sin => scalar_arguments(args),
|
Function::Sin => scalar_arguments(args),
|
||||||
Function::Sinh => scalar_arguments(args),
|
Function::Sinh => scalar_arguments(args),
|
||||||
Function::Sqrt => scalar_arguments(args),
|
Function::Sqrt => scalar_arguments(args),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::{super::utils::quote_name, Node, Reference};
|
|||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::move_formula::to_string_array_node;
|
use crate::expressions::parser::move_formula::to_string_array_node;
|
||||||
use crate::expressions::parser::static_analysis::add_implicit_intersection;
|
use crate::expressions::parser::static_analysis::add_implicit_intersection;
|
||||||
use crate::expressions::token::OpUnary;
|
use crate::expressions::token::{OpSum, OpUnary};
|
||||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||||
|
|
||||||
pub enum DisplaceData {
|
pub enum DisplaceData {
|
||||||
@@ -28,6 +28,11 @@ pub enum DisplaceData {
|
|||||||
column: i32,
|
column: i32,
|
||||||
delta: i32,
|
delta: i32,
|
||||||
},
|
},
|
||||||
|
RowMove {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
delta: i32,
|
||||||
|
},
|
||||||
ColumnMove {
|
ColumnMove {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
column: i32,
|
column: i32,
|
||||||
@@ -159,6 +164,29 @@ pub(crate) fn stringify_reference(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DisplaceData::RowMove {
|
||||||
|
sheet,
|
||||||
|
row: move_row,
|
||||||
|
delta,
|
||||||
|
} => {
|
||||||
|
if sheet_index == *sheet {
|
||||||
|
if row == *move_row {
|
||||||
|
row += *delta;
|
||||||
|
} else if *delta > 0 {
|
||||||
|
// Moving the row downwards
|
||||||
|
if row > *move_row && row <= *move_row + *delta {
|
||||||
|
// Intermediate rows move up by one position
|
||||||
|
row -= 1;
|
||||||
|
}
|
||||||
|
} else if *delta < 0 {
|
||||||
|
// Moving the row upwards
|
||||||
|
if row < *move_row && row >= *move_row + *delta {
|
||||||
|
// Intermediate rows move down by one position
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
DisplaceData::ColumnMove {
|
DisplaceData::ColumnMove {
|
||||||
sheet,
|
sheet,
|
||||||
column: move_column,
|
column: move_column,
|
||||||
@@ -167,14 +195,18 @@ pub(crate) fn stringify_reference(
|
|||||||
if sheet_index == *sheet {
|
if sheet_index == *sheet {
|
||||||
if column == *move_column {
|
if column == *move_column {
|
||||||
column += *delta;
|
column += *delta;
|
||||||
} else if (*delta > 0
|
} else if *delta > 0 {
|
||||||
&& column > *move_column
|
// Moving the column to the right
|
||||||
&& column <= *move_column + *delta)
|
if column > *move_column && column <= *move_column + *delta {
|
||||||
|| (*delta < 0
|
// Intermediate columns move left by one position
|
||||||
&& column < *move_column
|
column -= 1;
|
||||||
&& column >= *move_column + *delta)
|
}
|
||||||
{
|
} else if *delta < 0 {
|
||||||
column -= *delta;
|
// Moving the column to the left
|
||||||
|
if column < *move_column && column >= *move_column + *delta {
|
||||||
|
// Intermediate columns move right by one position
|
||||||
|
column += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,16 +216,16 @@ pub(crate) fn stringify_reference(
|
|||||||
return "#REF!".to_string();
|
return "#REF!".to_string();
|
||||||
}
|
}
|
||||||
let mut row_abs = if absolute_row {
|
let mut row_abs = if absolute_row {
|
||||||
format!("${}", row)
|
format!("${row}")
|
||||||
} else {
|
} else {
|
||||||
format!("{}", row)
|
format!("{row}")
|
||||||
};
|
};
|
||||||
let column = match crate::expressions::utils::number_to_column(column) {
|
let column = match crate::expressions::utils::number_to_column(column) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return "#REF!".to_string(),
|
None => return "#REF!".to_string(),
|
||||||
};
|
};
|
||||||
let mut col_abs = if absolute_column {
|
let mut col_abs = if absolute_column {
|
||||||
format!("${}", column)
|
format!("${column}")
|
||||||
} else {
|
} else {
|
||||||
column
|
column
|
||||||
};
|
};
|
||||||
@@ -208,27 +240,27 @@ pub(crate) fn stringify_reference(
|
|||||||
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
format!("{}{}", col_abs, row_abs)
|
format!("{col_abs}{row_abs}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let row_abs = if absolute_row {
|
let row_abs = if absolute_row {
|
||||||
format!("R{}", row)
|
format!("R{row}")
|
||||||
} else {
|
} else {
|
||||||
format!("R[{}]", row)
|
format!("R[{row}]")
|
||||||
};
|
};
|
||||||
let col_abs = if absolute_column {
|
let col_abs = if absolute_column {
|
||||||
format!("C{}", column)
|
format!("C{column}")
|
||||||
} else {
|
} else {
|
||||||
format!("C[{}]", column)
|
format!("C[{column}]")
|
||||||
};
|
};
|
||||||
match &sheet_name {
|
match &sheet_name {
|
||||||
Some(name) => {
|
Some(name) => {
|
||||||
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
format!("{}{}", row_abs, col_abs)
|
format!("{row_abs}{col_abs}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +288,7 @@ fn format_function(
|
|||||||
arguments = stringify(el, context, displace_data, export_to_excel);
|
arguments = stringify(el, context, displace_data, export_to_excel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("{}({})", name, arguments)
|
format!("{name}({arguments})")
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
|
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
|
||||||
@@ -292,9 +324,9 @@ fn stringify(
|
|||||||
) -> String {
|
) -> String {
|
||||||
use self::Node::*;
|
use self::Node::*;
|
||||||
match node {
|
match node {
|
||||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
|
||||||
NumberKind(number) => to_excel_precision_str(*number),
|
NumberKind(number) => to_excel_precision_str(*number),
|
||||||
StringKind(value) => format!("\"{}\"", value),
|
StringKind(value) => format!("\"{value}\""),
|
||||||
WrongReferenceKind {
|
WrongReferenceKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
column,
|
column,
|
||||||
@@ -384,7 +416,7 @@ fn stringify(
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{}:{}", s1, s2)
|
format!("{s1}:{s2}")
|
||||||
}
|
}
|
||||||
WrongRangeKind {
|
WrongRangeKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
@@ -433,7 +465,7 @@ fn stringify(
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{}:{}", s1, s2)
|
format!("{s1}:{s2}")
|
||||||
}
|
}
|
||||||
OpRangeKind { left, right } => format!(
|
OpRangeKind { left, right } => format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
@@ -451,40 +483,38 @@ fn stringify(
|
|||||||
kind,
|
kind,
|
||||||
stringify(right, context, displace_data, export_to_excel)
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
),
|
),
|
||||||
OpSumKind { kind, left, right } => format!(
|
OpSumKind { kind, left, right } => {
|
||||||
"{}{}{}",
|
let left_str = stringify(left, context, displace_data, export_to_excel);
|
||||||
stringify(left, context, displace_data, export_to_excel),
|
// if kind is minus then we need parentheses in the right side if they are OpSumKind or CompareKind
|
||||||
kind,
|
let right_str = if (matches!(kind, OpSum::Minus) && matches!(**right, OpSumKind { .. }))
|
||||||
stringify(right, context, displace_data, export_to_excel)
|
| matches!(**right, CompareKind { .. })
|
||||||
),
|
{
|
||||||
|
format!(
|
||||||
|
"({})",
|
||||||
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{left_str}{kind}{right_str}")
|
||||||
|
}
|
||||||
OpProductKind { kind, left, right } => {
|
OpProductKind { kind, left, right } => {
|
||||||
let x = match **left {
|
let x = match **left {
|
||||||
OpSumKind { .. } => format!(
|
OpSumKind { .. } | CompareKind { .. } => format!(
|
||||||
"({})",
|
|
||||||
stringify(left, context, displace_data, export_to_excel)
|
|
||||||
),
|
|
||||||
CompareKind { .. } => format!(
|
|
||||||
"({})",
|
"({})",
|
||||||
stringify(left, context, displace_data, export_to_excel)
|
stringify(left, context, displace_data, export_to_excel)
|
||||||
),
|
),
|
||||||
_ => stringify(left, context, displace_data, export_to_excel),
|
_ => stringify(left, context, displace_data, export_to_excel),
|
||||||
};
|
};
|
||||||
let y = match **right {
|
let y = match **right {
|
||||||
OpSumKind { .. } => format!(
|
OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!(
|
||||||
"({})",
|
|
||||||
stringify(right, context, displace_data, export_to_excel)
|
|
||||||
),
|
|
||||||
CompareKind { .. } => format!(
|
|
||||||
"({})",
|
|
||||||
stringify(right, context, displace_data, export_to_excel)
|
|
||||||
),
|
|
||||||
OpProductKind { .. } => format!(
|
|
||||||
"({})",
|
"({})",
|
||||||
stringify(right, context, displace_data, export_to_excel)
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
),
|
),
|
||||||
_ => stringify(right, context, displace_data, export_to_excel),
|
_ => stringify(right, context, displace_data, export_to_excel),
|
||||||
};
|
};
|
||||||
format!("{}{}{}", x, kind, y)
|
format!("{x}{kind}{y}")
|
||||||
}
|
}
|
||||||
OpPowerKind { left, right } => {
|
OpPowerKind { left, right } => {
|
||||||
let x = match **left {
|
let x = match **left {
|
||||||
@@ -547,7 +577,7 @@ fn stringify(
|
|||||||
stringify(right, context, displace_data, export_to_excel)
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
format!("{}^{}", x, y)
|
format!("{x}^{y}")
|
||||||
}
|
}
|
||||||
InvalidFunctionKind { name, args } => {
|
InvalidFunctionKind { name, args } => {
|
||||||
format_function(name, args, context, displace_data, export_to_excel)
|
format_function(name, args, context, displace_data, export_to_excel)
|
||||||
@@ -582,17 +612,50 @@ fn stringify(
|
|||||||
}
|
}
|
||||||
matrix_string.push_str(&row_string);
|
matrix_string.push_str(&row_string);
|
||||||
}
|
}
|
||||||
format!("{{{}}}", matrix_string)
|
format!("{{{matrix_string}}}")
|
||||||
}
|
}
|
||||||
TableNameKind(value) => value.to_string(),
|
TableNameKind(value) => value.to_string(),
|
||||||
DefinedNameKind((name, ..)) => name.to_string(),
|
DefinedNameKind((name, ..)) => name.to_string(),
|
||||||
WrongVariableKind(name) => name.to_string(),
|
WrongVariableKind(name) => name.to_string(),
|
||||||
UnaryKind { kind, right } => match kind {
|
UnaryKind { kind, right } => match kind {
|
||||||
OpUnary::Minus => {
|
OpUnary::Minus => {
|
||||||
format!(
|
let needs_parentheses = match **right {
|
||||||
"-{}",
|
BooleanKind(_)
|
||||||
stringify(right, context, displace_data, export_to_excel)
|
| NumberKind(_)
|
||||||
)
|
| StringKind(_)
|
||||||
|
| ReferenceKind { .. }
|
||||||
|
| RangeKind { .. }
|
||||||
|
| WrongReferenceKind { .. }
|
||||||
|
| WrongRangeKind { .. }
|
||||||
|
| OpRangeKind { .. }
|
||||||
|
| OpConcatenateKind { .. }
|
||||||
|
| OpProductKind { .. }
|
||||||
|
| OpPowerKind { .. }
|
||||||
|
| FunctionKind { .. }
|
||||||
|
| InvalidFunctionKind { .. }
|
||||||
|
| ArrayKind(_)
|
||||||
|
| DefinedNameKind(_)
|
||||||
|
| TableNameKind(_)
|
||||||
|
| WrongVariableKind(_)
|
||||||
|
| ImplicitIntersection { .. }
|
||||||
|
| CompareKind { .. }
|
||||||
|
| ErrorKind(_)
|
||||||
|
| ParseErrorKind { .. }
|
||||||
|
| EmptyArgKind => false,
|
||||||
|
|
||||||
|
OpSumKind { .. } | UnaryKind { .. } => true,
|
||||||
|
};
|
||||||
|
if needs_parentheses {
|
||||||
|
format!(
|
||||||
|
"-({})",
|
||||||
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"-{}",
|
||||||
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OpUnary::Percentage => {
|
OpUnary::Percentage => {
|
||||||
format!(
|
format!(
|
||||||
@@ -601,7 +664,7 @@ fn stringify(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ErrorKind(kind) => format!("{}", kind),
|
ErrorKind(kind) => format!("{kind}"),
|
||||||
ParseErrorKind {
|
ParseErrorKind {
|
||||||
formula,
|
formula,
|
||||||
position: _,
|
position: _,
|
||||||
|
|||||||
@@ -32,3 +32,39 @@ fn exp_order() {
|
|||||||
let t = parser.parse("(5)^(4)", &cell_reference);
|
let t = parser.parse("(5)^(4)", &cell_reference);
|
||||||
assert_eq!(to_string(&t, &cell_reference), "5^4");
|
assert_eq!(to_string(&t, &cell_reference), "5^4");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn correct_parenthesis() {
|
||||||
|
let worksheets = vec!["Sheet1".to_string()];
|
||||||
|
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||||
|
|
||||||
|
let cell_reference = CellReferenceRC {
|
||||||
|
sheet: "Sheet1".to_string(),
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let t = parser.parse("-(1 + 1)", &cell_reference);
|
||||||
|
assert_eq!(to_string(&t, &cell_reference), "-(1+1)");
|
||||||
|
|
||||||
|
let t = parser.parse("1 - (3 + 4)", &cell_reference);
|
||||||
|
assert_eq!(to_string(&t, &cell_reference), "1-(3+4)");
|
||||||
|
|
||||||
|
let t = parser.parse("-(1.05*(0.0284 + 0.0046) - 0.0284)", &cell_reference);
|
||||||
|
assert_eq!(
|
||||||
|
to_string(&t, &cell_reference),
|
||||||
|
"-(1.05*(0.0284+0.0046)-0.0284)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let t = parser.parse("1 + (3+5)", &cell_reference);
|
||||||
|
assert_eq!(to_string(&t, &cell_reference), "1+3+5");
|
||||||
|
|
||||||
|
let t = parser.parse("1 - (3+5)", &cell_reference);
|
||||||
|
assert_eq!(to_string(&t, &cell_reference), "1-(3+5)");
|
||||||
|
|
||||||
|
let t = parser.parse("(1 - 3) - (3+5)", &cell_reference);
|
||||||
|
assert_eq!(to_string(&t, &cell_reference), "1-3-(3+5)");
|
||||||
|
|
||||||
|
let t = parser.parse("1 + (3<5)", &cell_reference);
|
||||||
|
assert_eq!(to_string(&t, &cell_reference), "1+(3<5)");
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,4 +211,6 @@ fn test_names() {
|
|||||||
assert!(!is_valid_identifier("test€"));
|
assert!(!is_valid_identifier("test€"));
|
||||||
assert!(!is_valid_identifier("truñe"));
|
assert!(!is_valid_identifier("truñe"));
|
||||||
assert!(!is_valid_identifier("tr&ue"));
|
assert!(!is_valid_identifier("tr&ue"));
|
||||||
|
|
||||||
|
assert!(!is_valid_identifier("LOG10"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ fn is_date_within_range(date: NaiveDate) -> bool {
|
|||||||
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
||||||
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Excel date must be greater than {}",
|
"Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
|
||||||
MINIMUM_DATE_SERIAL_NUMBER
|
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Excel date must be less than {}",
|
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
|
||||||
MAXIMUM_DATE_SERIAL_NUMBER
|
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
// We should have different codepaths for general formatting and errors
|
// We should have different codepaths for general formatting and errors
|
||||||
let value_abs = value.abs();
|
let value_abs = value.abs();
|
||||||
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
||||||
let mut text = format!("{:.9}", value);
|
let mut text = format!("{value:.9}");
|
||||||
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||||
Formatted {
|
Formatted {
|
||||||
text,
|
text,
|
||||||
@@ -138,7 +138,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
let exponent = value_abs.log10().floor();
|
let exponent = value_abs.log10().floor();
|
||||||
value /= 10.0_f64.powf(exponent);
|
value /= 10.0_f64.powf(exponent);
|
||||||
let sign = if exponent < 0.0 { '-' } else { '+' };
|
let sign = if exponent < 0.0 { '-' } else { '+' };
|
||||||
let s = format!("{:.5}", value);
|
let s = format!("{value:.5}");
|
||||||
Formatted {
|
Formatted {
|
||||||
text: format!(
|
text: format!(
|
||||||
"{}E{}{:02}",
|
"{}E{}{:02}",
|
||||||
@@ -167,33 +167,33 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
for token in tokens {
|
for token in tokens {
|
||||||
match token {
|
match token {
|
||||||
TextToken::Literal(c) => {
|
TextToken::Literal(c) => {
|
||||||
text = format!("{}{}", text, c);
|
text = format!("{text}{c}");
|
||||||
}
|
}
|
||||||
TextToken::Text(t) => {
|
TextToken::Text(t) => {
|
||||||
text = format!("{}{}", text, t);
|
text = format!("{text}{t}");
|
||||||
}
|
}
|
||||||
TextToken::Ghost(_) => {
|
TextToken::Ghost(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{} ", text);
|
text = format!("{text} ");
|
||||||
}
|
}
|
||||||
TextToken::Spacer(_) => {
|
TextToken::Spacer(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{} ", text);
|
text = format!("{text} ");
|
||||||
}
|
}
|
||||||
TextToken::Raw => {
|
TextToken::Raw => {
|
||||||
text = format!("{}{}", text, value);
|
text = format!("{text}{value}");
|
||||||
}
|
}
|
||||||
TextToken::Digit(_) => {}
|
TextToken::Digit(_) => {}
|
||||||
TextToken::Period => {}
|
TextToken::Period => {}
|
||||||
TextToken::Day => {
|
TextToken::Day => {
|
||||||
let day = date.day() as usize;
|
let day = date.day() as usize;
|
||||||
text = format!("{}{}", text, day);
|
text = format!("{text}{day}");
|
||||||
}
|
}
|
||||||
TextToken::DayPadded => {
|
TextToken::DayPadded => {
|
||||||
let day = date.day() as usize;
|
let day = date.day() as usize;
|
||||||
text = format!("{}{:02}", text, day);
|
text = format!("{text}{day:02}");
|
||||||
}
|
}
|
||||||
TextToken::DayNameShort => {
|
TextToken::DayNameShort => {
|
||||||
let mut day = date.weekday().number_from_monday() as usize;
|
let mut day = date.weekday().number_from_monday() as usize;
|
||||||
@@ -211,11 +211,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
}
|
}
|
||||||
TextToken::Month => {
|
TextToken::Month => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
text = format!("{}{}", text, month);
|
text = format!("{text}{month}");
|
||||||
}
|
}
|
||||||
TextToken::MonthPadded => {
|
TextToken::MonthPadded => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
text = format!("{}{:02}", text, month);
|
text = format!("{text}{month:02}");
|
||||||
}
|
}
|
||||||
TextToken::MonthNameShort => {
|
TextToken::MonthNameShort => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
@@ -228,7 +228,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
TextToken::MonthLetter => {
|
TextToken::MonthLetter => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
let months_letter = &locale.dates.months_letter[month - 1];
|
let months_letter = &locale.dates.months_letter[month - 1];
|
||||||
text = format!("{}{}", text, months_letter);
|
text = format!("{text}{months_letter}");
|
||||||
}
|
}
|
||||||
TextToken::YearShort => {
|
TextToken::YearShort => {
|
||||||
text = format!("{}{}", text, date.format("%y"));
|
text = format!("{}{}", text, date.format("%y"));
|
||||||
@@ -247,7 +247,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
ParsePart::Number(p) => {
|
ParsePart::Number(p) => {
|
||||||
let mut text = "".to_string();
|
let mut text = "".to_string();
|
||||||
if let Some(c) = p.currency {
|
if let Some(c) = p.currency {
|
||||||
text = format!("{}", c);
|
text = format!("{c}");
|
||||||
}
|
}
|
||||||
let tokens = &p.tokens;
|
let tokens = &p.tokens;
|
||||||
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
||||||
@@ -295,26 +295,26 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
for token in tokens {
|
for token in tokens {
|
||||||
match token {
|
match token {
|
||||||
TextToken::Literal(c) => {
|
TextToken::Literal(c) => {
|
||||||
text = format!("{}{}", text, c);
|
text = format!("{text}{c}");
|
||||||
}
|
}
|
||||||
TextToken::Text(t) => {
|
TextToken::Text(t) => {
|
||||||
text = format!("{}{}", text, t);
|
text = format!("{text}{t}");
|
||||||
}
|
}
|
||||||
TextToken::Ghost(_) => {
|
TextToken::Ghost(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{} ", text);
|
text = format!("{text} ");
|
||||||
}
|
}
|
||||||
TextToken::Spacer(_) => {
|
TextToken::Spacer(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{} ", text);
|
text = format!("{text} ");
|
||||||
}
|
}
|
||||||
TextToken::Raw => {
|
TextToken::Raw => {
|
||||||
text = format!("{}{}", text, value);
|
text = format!("{text}{value}");
|
||||||
}
|
}
|
||||||
TextToken::Period => {
|
TextToken::Period => {
|
||||||
text = format!("{}{}", text, decimal_separator);
|
text = format!("{text}{decimal_separator}");
|
||||||
}
|
}
|
||||||
TextToken::Digit(digit) => {
|
TextToken::Digit(digit) => {
|
||||||
if digit.number == 'i' {
|
if digit.number == 'i' {
|
||||||
@@ -322,7 +322,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
let index = digit.index;
|
let index = digit.index;
|
||||||
let number_index = ln - digit_count + index;
|
let number_index = ln - digit_count + index;
|
||||||
if index == 0 && is_negative {
|
if index == 0 && is_negative {
|
||||||
text = format!("-{}", text);
|
text = format!("-{text}");
|
||||||
}
|
}
|
||||||
if ln <= digit_count {
|
if ln <= digit_count {
|
||||||
// The number of digits is less or equal than the number of digit tokens
|
// The number of digits is less or equal than the number of digit tokens
|
||||||
@@ -347,7 +347,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
text = format!("{}{}{}", text, c, sep);
|
text = format!("{text}{c}{sep}");
|
||||||
}
|
}
|
||||||
digit_index += 1;
|
digit_index += 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -373,18 +373,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
if index < fract_part.len() {
|
if index < fract_part.len() {
|
||||||
text = format!("{}{}", text, fract_part[index]);
|
text = format!("{}{}", text, fract_part[index]);
|
||||||
} else if digit.kind == '0' {
|
} else if digit.kind == '0' {
|
||||||
text = format!("{}0", text);
|
text = format!("{text}0");
|
||||||
} else if digit.kind == '?' {
|
} else if digit.kind == '?' {
|
||||||
text = format!("{} ", text);
|
text = format!("{text} ");
|
||||||
}
|
}
|
||||||
} else if digit.number == 'e' {
|
} else if digit.number == 'e' {
|
||||||
// 3. Exponent part
|
// 3. Exponent part
|
||||||
let index = digit.index;
|
let index = digit.index;
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
if exponent_is_negative {
|
if exponent_is_negative {
|
||||||
text = format!("{}E-", text);
|
text = format!("{text}E-");
|
||||||
} else {
|
} else {
|
||||||
text = format!("{}E+", text);
|
text = format!("{text}E+");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let number_index = l_exp - (p.exponent_digit_count - index);
|
let number_index = l_exp - (p.exponent_digit_count - index);
|
||||||
@@ -400,7 +400,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
exponent_part[number_index as usize]
|
exponent_part[number_index as usize]
|
||||||
};
|
};
|
||||||
|
|
||||||
text = format!("{}{}", text, c);
|
text = format!("{text}{c}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for i in 0..number_index + 1 {
|
for i in 0..number_index + 1 {
|
||||||
@@ -614,7 +614,7 @@ pub(crate) fn parse_formatted_number(
|
|||||||
|
|
||||||
// check if it is a currency in currencies
|
// check if it is a currency in currencies
|
||||||
for currency in currencies {
|
for currency in currencies {
|
||||||
if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
|
if let Some(p) = value.strip_prefix(&format!("-{currency}")) {
|
||||||
let (f, options) = parse_number(p.trim())?;
|
let (f, options) = parse_number(p.trim())?;
|
||||||
if options.is_scientific {
|
if options.is_scientific {
|
||||||
return Ok((f, Some(scientific_format.to_string())));
|
return Ok((f, Some(scientific_format.to_string())));
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ impl Lexer {
|
|||||||
} else if s == '-' {
|
} else if s == '-' {
|
||||||
Token::ScientificMinus
|
Token::ScientificMinus
|
||||||
} else {
|
} else {
|
||||||
self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
|
self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
|
||||||
Token::ILLEGAL
|
Token::ILLEGAL
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -385,14 +385,14 @@ impl Lexer {
|
|||||||
for c in "eneral".chars() {
|
for c in "eneral".chars() {
|
||||||
let cc = self.read_next_char();
|
let cc = self.read_next_char();
|
||||||
if Some(c) != cc {
|
if Some(c) != cc {
|
||||||
self.set_error(&format!("Unexpected character: {}", x));
|
self.set_error(&format!("Unexpected character: {x}"));
|
||||||
return Token::ILLEGAL;
|
return Token::ILLEGAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Token::General
|
Token::General
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.set_error(&format!("Unexpected character: {}", x));
|
self.set_error(&format!("Unexpected character: {x}"));
|
||||||
Token::ILLEGAL
|
Token::ILLEGAL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,18 +46,18 @@ impl fmt::Display for Complex {
|
|||||||
// it is a bit weird what Excel does but it seems it uses general notation for
|
// it is a bit weird what Excel does but it seems it uses general notation for
|
||||||
// numbers > 1e-20 and scientific notation for the rest
|
// numbers > 1e-20 and scientific notation for the rest
|
||||||
let y_str = if y.abs() <= 9e-20 {
|
let y_str = if y.abs() <= 9e-20 {
|
||||||
format!("{:E}", y)
|
format!("{y:E}")
|
||||||
} else if y == 1.0 {
|
} else if y == 1.0 {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else if y == -1.0 {
|
} else if y == -1.0 {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{}", y)
|
format!("{y}")
|
||||||
};
|
};
|
||||||
let x_str = if x.abs() <= 9e-20 {
|
let x_str = if x.abs() <= 9e-20 {
|
||||||
format!("{:E}", x)
|
format!("{x:E}")
|
||||||
} else {
|
} else {
|
||||||
format!("{}", x)
|
format!("{x}")
|
||||||
};
|
};
|
||||||
if y == 0.0 && x == 0.0 {
|
if y == 0.0 && x == 0.0 {
|
||||||
write!(f, "0")
|
write!(f, "0")
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
|
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
|
||||||
} else {
|
} else {
|
||||||
let result = format!("{:X}", value);
|
let result = format!("{value:X}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places < result.len() as i32 {
|
if places < result.len() as i32 {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
@@ -120,7 +120,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
|
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
|
||||||
} else {
|
} else {
|
||||||
let result = format!("{:o}", value);
|
let result = format!("{value:o}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places < result.len() as i32 {
|
if places < result.len() as i32 {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
@@ -163,7 +163,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += 1024;
|
value += 1024;
|
||||||
}
|
}
|
||||||
let result = format!("{:b}", value);
|
let result = format!("{value:b}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value_raw > 0.0 && places < result.len() as i32 {
|
if value_raw > 0.0 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -202,7 +202,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += HEX_MAX;
|
value += HEX_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{:X}", value);
|
let result = format!("{value:X}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value_raw > 0.0 && places < result.len() as i32 {
|
if value_raw > 0.0 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -242,7 +242,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += OCT_MAX;
|
value += OCT_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{:o}", value);
|
let result = format!("{value:o}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value_raw > 0.0 && places < result.len() as i32 {
|
if value_raw > 0.0 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -301,7 +301,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += 1024;
|
value += 1024;
|
||||||
}
|
}
|
||||||
let result = format!("{:b}", value);
|
let result = format!("{value:b}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -391,7 +391,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += OCT_MAX;
|
value += OCT_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{:o}", value);
|
let result = format!("{value:o}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -446,7 +446,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += 1024;
|
value += 1024;
|
||||||
}
|
}
|
||||||
let result = format!("{:b}", value);
|
let result = format!("{value:b}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value < 512 && places < result.len() as i32 {
|
if value < 512 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -532,7 +532,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += HEX_MAX;
|
value += HEX_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{:X}", value);
|
let result = format!("{value:X}");
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value < HEX_MAX_HALF && places < result.len() as i32 {
|
if value < HEX_MAX_HALF && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ impl Model {
|
|||||||
CalcResult::new_error(
|
CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
*cell,
|
*cell,
|
||||||
format!("Invalid worksheet index: '{}'", sheet),
|
format!("Invalid worksheet index: '{sheet}'"),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.dimension()
|
.dimension()
|
||||||
@@ -245,7 +245,7 @@ impl Model {
|
|||||||
CalcResult::new_error(
|
CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
*cell,
|
*cell,
|
||||||
format!("Invalid worksheet index: '{}'", sheet),
|
format!("Invalid worksheet index: '{sheet}'"),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.dimension()
|
.dimension()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::cast::NumberOrArray;
|
|||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::ArrayNode;
|
use crate::expressions::parser::ArrayNode;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::number_format::to_precision;
|
||||||
use crate::single_number_fn;
|
use crate::single_number_fn;
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
@@ -311,7 +312,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => to_precision(f, 15),
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||||
@@ -327,12 +328,13 @@ impl Model {
|
|||||||
let scale = 10.0_f64.powf(number_of_digits);
|
let scale = 10.0_f64.powf(number_of_digits);
|
||||||
CalcResult::Number((value * scale).round() / scale)
|
CalcResult::Number((value * scale).round() / scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => to_precision(f, 15),
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||||
@@ -352,12 +354,13 @@ impl Model {
|
|||||||
CalcResult::Number((value * scale).floor() / scale)
|
CalcResult::Number((value * scale).floor() / scale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => to_precision(f, 15),
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||||
@@ -378,6 +381,16 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single_number_fn!(fn_log10, |f| if f <= 0.0 {
|
||||||
|
Err(Error::NUM)
|
||||||
|
} else {
|
||||||
|
Ok(f64::log10(f))
|
||||||
|
});
|
||||||
|
single_number_fn!(fn_ln, |f| if f <= 0.0 {
|
||||||
|
Err(Error::NUM)
|
||||||
|
} else {
|
||||||
|
Ok(f64::ln(f))
|
||||||
|
});
|
||||||
single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
|
single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
|
||||||
single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
|
single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
|
||||||
single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
|
single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
|
||||||
@@ -431,6 +444,47 @@ impl Model {
|
|||||||
CalcResult::Number(f64::atan2(y, x))
|
CalcResult::Number(f64::atan2(y, x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_log(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let n_args = args.len();
|
||||||
|
if !(1..=2).contains(&n_args) {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let x = match self.get_number(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
let y = if n_args == 1 {
|
||||||
|
10.0
|
||||||
|
} else {
|
||||||
|
match self.get_number(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(s) => return s,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if x <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Number must be positive".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if y == 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Logarithm base cannot be 1".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if y <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Logarithm base must be positive".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Number(f64::log(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ pub enum Function {
|
|||||||
Columns,
|
Columns,
|
||||||
Cos,
|
Cos,
|
||||||
Cosh,
|
Cosh,
|
||||||
|
Log,
|
||||||
|
Log10,
|
||||||
|
Ln,
|
||||||
Max,
|
Max,
|
||||||
Min,
|
Min,
|
||||||
Pi,
|
Pi,
|
||||||
@@ -250,7 +253,7 @@ pub enum Function {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 195> {
|
pub fn into_iter() -> IntoIter<Function, 198> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -277,6 +280,9 @@ impl Function {
|
|||||||
Function::Atanh,
|
Function::Atanh,
|
||||||
Function::Abs,
|
Function::Abs,
|
||||||
Function::Pi,
|
Function::Pi,
|
||||||
|
Function::Ln,
|
||||||
|
Function::Log,
|
||||||
|
Function::Log10,
|
||||||
Function::Sqrt,
|
Function::Sqrt,
|
||||||
Function::Sqrtpi,
|
Function::Sqrtpi,
|
||||||
Function::Atan2,
|
Function::Atan2,
|
||||||
@@ -534,6 +540,10 @@ impl Function {
|
|||||||
"POWER" => Some(Function::Power),
|
"POWER" => Some(Function::Power),
|
||||||
"ATAN2" => Some(Function::Atan2),
|
"ATAN2" => Some(Function::Atan2),
|
||||||
|
|
||||||
|
"LN" => Some(Function::Ln),
|
||||||
|
"LOG" => Some(Function::Log),
|
||||||
|
"LOG10" => Some(Function::Log10),
|
||||||
|
|
||||||
"MAX" => Some(Function::Max),
|
"MAX" => Some(Function::Max),
|
||||||
"MIN" => Some(Function::Min),
|
"MIN" => Some(Function::Min),
|
||||||
"PRODUCT" => Some(Function::Product),
|
"PRODUCT" => Some(Function::Product),
|
||||||
@@ -734,6 +744,9 @@ impl fmt::Display for Function {
|
|||||||
Function::Switch => write!(f, "SWITCH"),
|
Function::Switch => write!(f, "SWITCH"),
|
||||||
Function::True => write!(f, "TRUE"),
|
Function::True => write!(f, "TRUE"),
|
||||||
Function::Xor => write!(f, "XOR"),
|
Function::Xor => write!(f, "XOR"),
|
||||||
|
Function::Log => write!(f, "LOG"),
|
||||||
|
Function::Log10 => write!(f, "LOG10"),
|
||||||
|
Function::Ln => write!(f, "LN"),
|
||||||
Function::Sin => write!(f, "SIN"),
|
Function::Sin => write!(f, "SIN"),
|
||||||
Function::Cos => write!(f, "COS"),
|
Function::Cos => write!(f, "COS"),
|
||||||
Function::Tan => write!(f, "TAN"),
|
Function::Tan => write!(f, "TAN"),
|
||||||
@@ -961,6 +974,9 @@ impl Model {
|
|||||||
Function::True => self.fn_true(args, cell),
|
Function::True => self.fn_true(args, cell),
|
||||||
Function::Xor => self.fn_xor(args, cell),
|
Function::Xor => self.fn_xor(args, cell),
|
||||||
// Math and trigonometry
|
// Math and trigonometry
|
||||||
|
Function::Log => self.fn_log(args, cell),
|
||||||
|
Function::Log10 => self.fn_log10(args, cell),
|
||||||
|
Function::Ln => self.fn_ln(args, cell),
|
||||||
Function::Sin => self.fn_sin(args, cell),
|
Function::Sin => self.fn_sin(args, cell),
|
||||||
Function::Cos => self.fn_cos(args, cell),
|
Function::Cos => self.fn_cos(args, cell),
|
||||||
Function::Tan => self.fn_tan(args, cell),
|
Function::Tan => self.fn_tan(args, cell),
|
||||||
@@ -1214,7 +1230,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
||||||
let iter_list = Function::into_iter()
|
let iter_list = Function::into_iter()
|
||||||
.map(|f| format!("{}", f).replace('.', ""))
|
.map(|f| format!("{f}").replace('.', ""))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let len = iter_list.len();
|
let len = iter_list.len();
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ impl Model {
|
|||||||
|
|
||||||
match cell.get_formula() {
|
match cell.get_formula() {
|
||||||
Some(f) => {
|
Some(f) => {
|
||||||
let node = &self.parsed_formulas[sheet_index as usize][f as usize];
|
let node = &self.parsed_formulas[sheet_index as usize][f as usize].0;
|
||||||
matches!(
|
matches!(
|
||||||
node,
|
node,
|
||||||
Node::FunctionKind {
|
Node::FunctionKind {
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ impl Model {
|
|||||||
let mut result = "".to_string();
|
let mut result = "".to_string();
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
CalcResult::String(value) => result = format!("{}{}", result, value),
|
CalcResult::String(value) => result = format!("{result}{value}"),
|
||||||
CalcResult::Number(value) => result = format!("{}{}", result, value),
|
CalcResult::Number(value) => result = format!("{result}{value}"),
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||||
CalcResult::Boolean(value) => {
|
CalcResult::Boolean(value) => {
|
||||||
if value {
|
if value {
|
||||||
result = format!("{}TRUE", result);
|
result = format!("{result}TRUE");
|
||||||
} else {
|
} else {
|
||||||
result = format!("{}FALSE", result);
|
result = format!("{result}FALSE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return error,
|
||||||
@@ -82,16 +82,14 @@ impl Model {
|
|||||||
column,
|
column,
|
||||||
}) {
|
}) {
|
||||||
CalcResult::String(value) => {
|
CalcResult::String(value) => {
|
||||||
result = format!("{}{}", result, value);
|
result = format!("{result}{value}");
|
||||||
}
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
result = format!("{}{}", result, value)
|
|
||||||
}
|
}
|
||||||
|
CalcResult::Number(value) => result = format!("{result}{value}"),
|
||||||
CalcResult::Boolean(value) => {
|
CalcResult::Boolean(value) => {
|
||||||
if value {
|
if value {
|
||||||
result = format!("{}TRUE", result);
|
result = format!("{result}TRUE");
|
||||||
} else {
|
} else {
|
||||||
result = format!("{}FALSE", result);
|
result = format!("{result}FALSE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return error,
|
||||||
@@ -282,7 +280,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -317,7 +315,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -352,7 +350,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -387,7 +385,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -441,7 +439,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -478,7 +476,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -560,7 +558,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -642,7 +640,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{}", v),
|
CalcResult::Number(v) => format!("{v}"),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ pub(crate) fn from_wildcard_to_regex(
|
|||||||
|
|
||||||
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
|
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
|
||||||
if exact {
|
if exact {
|
||||||
return regex::Regex::new(&format!("^{}$", reg));
|
return regex::Regex::new(&format!("^{reg}$"));
|
||||||
}
|
}
|
||||||
regex::Regex::new(reg)
|
regex::Regex::new(reg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::OnceLock};
|
||||||
|
|
||||||
use bitcode::{Decode, Encode};
|
use bitcode::{Decode, Encode};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
#[derive(Encode, Decode, Clone)]
|
#[derive(Encode, Decode, Clone)]
|
||||||
pub struct Booleans {
|
pub struct Booleans {
|
||||||
@@ -31,14 +30,17 @@ pub struct Language {
|
|||||||
pub errors: Errors,
|
pub errors: Errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static LANGUAGES: OnceLock<HashMap<String, Language>> = OnceLock::new();
|
||||||
|
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
fn get_languages() -> &'static HashMap<String, Language> {
|
||||||
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
|
LANGUAGES.get_or_init(|| {
|
||||||
});
|
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> {
|
||||||
let language = LANGUAGES
|
get_languages()
|
||||||
.get(id)
|
.get(id)
|
||||||
.ok_or(format!("Language is not supported: '{}'", id))?;
|
.ok_or_else(|| format!("Language is not supported: '{id}'"))
|
||||||
Ok(language)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use bitcode::{Decode, Encode};
|
use std::{collections::HashMap, sync::OnceLock};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use bitcode::{Decode, Encode};
|
||||||
|
|
||||||
#[derive(Encode, Decode, Clone)]
|
#[derive(Encode, Decode, Clone)]
|
||||||
pub struct Locale {
|
pub struct Locale {
|
||||||
@@ -65,12 +64,17 @@ pub struct DecimalFormats {
|
|||||||
pub standard: String,
|
pub standard: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static LOCALES: OnceLock<HashMap<String, Locale>> = OnceLock::new();
|
||||||
|
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
static LOCALES: Lazy<HashMap<String, Locale>> =
|
fn get_locales() -> &'static HashMap<String, Locale> {
|
||||||
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
|
LOCALES.get_or_init(|| {
|
||||||
|
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
|
get_locales()
|
||||||
let locale = LOCALES.get(id).ok_or("Invalid locale")?;
|
.get(id)
|
||||||
Ok(locale)
|
.ok_or_else(|| format!("Invalid locale: '{id}'"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ use crate::{
|
|||||||
lexer::LexerMode,
|
lexer::LexerMode,
|
||||||
parser::{
|
parser::{
|
||||||
move_formula::{move_formula, MoveContext},
|
move_formula::{move_formula, MoveContext},
|
||||||
|
static_analysis::{run_static_analysis_on_node, StaticResult},
|
||||||
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
|
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
|
||||||
Node, Parser,
|
ArrayNode, Node, Parser,
|
||||||
},
|
},
|
||||||
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
|
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
|
||||||
types::*,
|
types::*,
|
||||||
@@ -83,6 +84,24 @@ pub(crate) enum ParsedDefinedName {
|
|||||||
InvalidDefinedNameFormula,
|
InvalidDefinedNameFormula,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A support node is either a cell or a range of cells
|
||||||
|
pub(crate) enum SupportNode {
|
||||||
|
/// (sheet, row, column)
|
||||||
|
Cell((u32, i32, i32)),
|
||||||
|
/// (sheet, row, column, height, width)
|
||||||
|
Range((u32, i32, i32, u32, u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state of the computation
|
||||||
|
pub(crate) enum EvaluationState {
|
||||||
|
/// the model is ready for a new evaluation
|
||||||
|
Ready,
|
||||||
|
/// the model is evaluating cells that might spill
|
||||||
|
EvaluatingSpills,
|
||||||
|
/// the model is evaluating cells normally
|
||||||
|
Evaluating
|
||||||
|
}
|
||||||
|
|
||||||
/// A dynamical IronCalc model.
|
/// A dynamical IronCalc model.
|
||||||
///
|
///
|
||||||
/// Its is composed of a `Workbook`. Everything else are dynamical quantities:
|
/// Its is composed of a `Workbook`. Everything else are dynamical quantities:
|
||||||
@@ -99,23 +118,31 @@ pub struct Model {
|
|||||||
/// A Rust internal representation of an Excel workbook
|
/// A Rust internal representation of an Excel workbook
|
||||||
pub workbook: Workbook,
|
pub workbook: Workbook,
|
||||||
/// A list of parsed formulas
|
/// A list of parsed formulas
|
||||||
pub parsed_formulas: Vec<Vec<Node>>,
|
pub parsed_formulas: Vec<Vec<(Node, StaticResult)>>,
|
||||||
/// A list of parsed defined names
|
/// A list of parsed defined names
|
||||||
pub(crate) 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(crate) shared_strings: HashMap<String, usize>,
|
pub(crate) shared_strings: HashMap<String, usize>,
|
||||||
/// An instance of the parser
|
/// An instance of the parser
|
||||||
pub(crate) parser: Parser,
|
pub(crate) parser: Parser,
|
||||||
/// The list of cells with formulas that are evaluated of being evaluated
|
|
||||||
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
|
||||||
/// The locale of the model
|
/// The locale of the model
|
||||||
pub(crate) locale: Locale,
|
pub(crate) locale: Locale,
|
||||||
/// Tha language used
|
/// The language used
|
||||||
pub(crate) language: Language,
|
pub(crate) language: Language,
|
||||||
/// The timezone used to evaluate the model
|
/// The timezone used to evaluate the model
|
||||||
pub(crate) tz: Tz,
|
pub(crate) tz: Tz,
|
||||||
/// The view id. A view consist of a selected sheet and ranges.
|
/// The view id. A view consists of a selected sheet and ranges.
|
||||||
pub(crate) view_id: u32,
|
pub(crate) view_id: u32,
|
||||||
|
/// ** Runtime ***
|
||||||
|
/// The list of cells with formulas that are evaluated or being evaluated
|
||||||
|
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
||||||
|
/// The support graph. For a given cell (sheet, row, column) the list of cells and ranges that were requested
|
||||||
|
pub(crate) support_graph: HashMap<(u32, i32, i32), Vec<SupportNode>>,
|
||||||
|
/// If the model is in a switch state then spill cells in the indices should be switched and recalculation redone
|
||||||
|
pub(crate) switch_cells: Option<(i32, i32)>,
|
||||||
|
/// Stack of cells being evaluated
|
||||||
|
pub(crate) stack: Vec<(u32, i32, i32)>,
|
||||||
|
pub(crate) state: EvaluationState,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Maybe this should be the same as CellReference
|
// FIXME: Maybe this should be the same as CellReference
|
||||||
@@ -215,7 +242,7 @@ impl Model {
|
|||||||
_ => CalcResult::new_error(
|
_ => CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
cell,
|
cell,
|
||||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
format!("Error with Implicit Intersection in cell {cell:?}"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
_ => self.evaluate_node_in_context(node, cell),
|
_ => self.evaluate_node_in_context(node, cell),
|
||||||
@@ -355,7 +382,7 @@ impl Model {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let result = format!("{}{}", l, r);
|
let result = format!("{l}{r}");
|
||||||
CalcResult::String(result)
|
CalcResult::String(result)
|
||||||
}
|
}
|
||||||
OpProductKind { kind, left, right } => match kind {
|
OpProductKind { kind, left, right } => match kind {
|
||||||
@@ -375,7 +402,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
||||||
InvalidFunctionKind { name, args: _ } => {
|
InvalidFunctionKind { name, args: _ } => {
|
||||||
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
|
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {name}"))
|
||||||
}
|
}
|
||||||
ArrayKind(s) => CalcResult::Array(s.to_owned()),
|
ArrayKind(s) => CalcResult::Array(s.to_owned()),
|
||||||
DefinedNameKind((name, scope, _)) => {
|
DefinedNameKind((name, scope, _)) => {
|
||||||
@@ -391,26 +418,26 @@ impl Model {
|
|||||||
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
|
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("Defined name \"{}\" is not a reference.", name),
|
format!("Defined name \"{name}\" is not a reference."),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CalcResult::new_error(
|
CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("Defined name \"{}\" not found.", name),
|
format!("Defined name \"{name}\" not found."),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TableNameKind(s) => CalcResult::new_error(
|
TableNameKind(s) => CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("table name \"{}\" not supported.", s),
|
format!("table name \"{s}\" not supported."),
|
||||||
),
|
),
|
||||||
WrongVariableKind(s) => CalcResult::new_error(
|
WrongVariableKind(s) => CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("Variable name \"{}\" not found.", s),
|
format!("Variable name \"{s}\" not found."),
|
||||||
),
|
),
|
||||||
CompareKind { kind, left, right } => {
|
CompareKind { kind, left, right } => {
|
||||||
let l = self.evaluate_node_in_context(left, cell);
|
let l = self.evaluate_node_in_context(left, cell);
|
||||||
@@ -487,7 +514,7 @@ impl Model {
|
|||||||
} => CalcResult::new_error(
|
} => CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
cell,
|
cell,
|
||||||
format!("Error parsing {}: {}", formula, message),
|
format!("Error parsing {formula}: {message}"),
|
||||||
),
|
),
|
||||||
EmptyArgKind => CalcResult::EmptyArg,
|
EmptyArgKind => CalcResult::EmptyArg,
|
||||||
ImplicitIntersection {
|
ImplicitIntersection {
|
||||||
@@ -500,7 +527,7 @@ impl Model {
|
|||||||
None => CalcResult::new_error(
|
None => CalcResult::new_error(
|
||||||
Error::VALUE,
|
Error::VALUE,
|
||||||
cell,
|
cell,
|
||||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
format!("Error with Implicit Intersection in cell {cell:?}"),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,14 +549,203 @@ impl Model {
|
|||||||
}
|
}
|
||||||
Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row))
|
Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets sheet, target_row, target_column, (width, height), &v
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn set_spill_cell_with_formula_value(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
r: (i32, i32),
|
||||||
|
v: &CalcResult,
|
||||||
|
s: i32,
|
||||||
|
f: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let new_cell = match v {
|
||||||
|
CalcResult::EmptyCell => Cell::DynamicCellFormulaNumber {
|
||||||
|
f,
|
||||||
|
v: 0.0,
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
a: false,
|
||||||
|
},
|
||||||
|
CalcResult::String(v) => Cell::DynamicCellFormulaString {
|
||||||
|
f,
|
||||||
|
v: v.clone(),
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
a: false,
|
||||||
|
},
|
||||||
|
CalcResult::Number(v) => Cell::DynamicCellFormulaNumber {
|
||||||
|
v: *v,
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
f,
|
||||||
|
a: false,
|
||||||
|
},
|
||||||
|
CalcResult::Boolean(b) => Cell::DynamicCellFormulaBoolean {
|
||||||
|
v: *b,
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
f,
|
||||||
|
a: false,
|
||||||
|
},
|
||||||
|
CalcResult::Error { error, .. } => Cell::DynamicCellFormulaError {
|
||||||
|
ei: error.clone(),
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
f,
|
||||||
|
a: false,
|
||||||
|
o: "".to_string(),
|
||||||
|
m: "".to_string(),
|
||||||
|
},
|
||||||
|
|
||||||
|
// These cannot happen
|
||||||
|
// FIXME: Maybe the type of get_cell_value should be different
|
||||||
|
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
|
||||||
|
Cell::DynamicCellFormulaError {
|
||||||
|
ei: Error::ERROR,
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
f,
|
||||||
|
a: false,
|
||||||
|
o: "".to_string(),
|
||||||
|
m: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
|
||||||
|
|
||||||
|
match sheet_data.get_mut(&row) {
|
||||||
|
Some(column_data) => match column_data.get(&column) {
|
||||||
|
Some(_cell) => {
|
||||||
|
column_data.insert(column, new_cell);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
column_data.insert(column, new_cell);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mut column_data = HashMap::new();
|
||||||
|
column_data.insert(column, new_cell);
|
||||||
|
sheet_data.insert(row, column_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a cell with a "spill" value
|
||||||
|
fn set_spill_cell_with_value(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
m: (i32, i32),
|
||||||
|
v: &CalcResult,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||||
|
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
|
||||||
|
self.workbook
|
||||||
|
.styles
|
||||||
|
.get_style_without_quote_prefix(style_index)?
|
||||||
|
} else {
|
||||||
|
style_index
|
||||||
|
};
|
||||||
|
let new_cell = match v {
|
||||||
|
CalcResult::EmptyCell => Cell::SpillNumberCell {
|
||||||
|
v: 0.0,
|
||||||
|
s: style_index,
|
||||||
|
m,
|
||||||
|
},
|
||||||
|
CalcResult::String(s) => Cell::SpillStringCell {
|
||||||
|
v: s.clone(),
|
||||||
|
s: new_style_index,
|
||||||
|
m,
|
||||||
|
},
|
||||||
|
CalcResult::Number(f) => Cell::SpillNumberCell {
|
||||||
|
v: *f,
|
||||||
|
s: new_style_index,
|
||||||
|
m,
|
||||||
|
},
|
||||||
|
CalcResult::Boolean(b) => Cell::SpillBooleanCell {
|
||||||
|
v: *b,
|
||||||
|
s: new_style_index,
|
||||||
|
m,
|
||||||
|
},
|
||||||
|
CalcResult::Error { error, .. } => Cell::SpillErrorCell {
|
||||||
|
ei: error.clone(),
|
||||||
|
s: style_index,
|
||||||
|
m,
|
||||||
|
},
|
||||||
|
|
||||||
|
// These cannot happen
|
||||||
|
// FIXME: Maybe the type of get_cell_value should be different
|
||||||
|
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
|
||||||
|
Cell::SpillErrorCell {
|
||||||
|
ei: Error::ERROR,
|
||||||
|
s: style_index,
|
||||||
|
m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
|
||||||
|
|
||||||
|
match sheet_data.get_mut(&row) {
|
||||||
|
Some(column_data) => match column_data.get(&column) {
|
||||||
|
Some(_cell) => {
|
||||||
|
column_data.insert(column, new_cell);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
column_data.insert(column, new_cell);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mut column_data = HashMap::new();
|
||||||
|
column_data.insert(column, new_cell);
|
||||||
|
sheet_data.insert(row, column_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `None` if no cell has called this cell, otherwise returns the dependent cell
|
||||||
|
fn get_support_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Option<&Cell>, String> {
|
||||||
|
self.workbook.supporting_cells.get(&(sheet, row, column)).map(|c| Some(c)).ok_or_else(|| "Cell not found".into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets `result` in the cell given by `sheet` sheet index, row and column
|
/// Sets `result` in the cell given by `sheet` sheet index, row and column
|
||||||
/// Note that will panic if the cell does not exist
|
/// Note that will panic if the cell does not exist
|
||||||
/// It will do nothing if the cell does not have a formula
|
/// It will do nothing if the cell does not have a formula
|
||||||
|
/// If the cell is an array or a range it will check if it is possible to spill to other cells
|
||||||
|
/// if it is not it will return an error.
|
||||||
|
/// Then it will check if any of the cells has been requested before.
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
fn set_cell_value(&mut self, cell_reference: CellReferenceIndex, result: &CalcResult) {
|
fn set_cell_value(
|
||||||
|
&mut self,
|
||||||
|
cell_reference: CellReferenceIndex,
|
||||||
|
result: &CalcResult,
|
||||||
|
) -> Result<(), String> {
|
||||||
let CellReferenceIndex { sheet, column, row } = cell_reference;
|
let CellReferenceIndex { sheet, column, row } = cell_reference;
|
||||||
let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column];
|
let cell = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(row, column)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
let s = cell.get_style();
|
let s = cell.get_style();
|
||||||
|
// If the cell is a dynamic cell we need to delete all the cells in the range
|
||||||
|
if let Some((width, height)) = cell.get_dynamic_range() {
|
||||||
|
for r in row..row + height {
|
||||||
|
for c in column..column + width {
|
||||||
|
// skip the "mother" cell
|
||||||
|
if r == row && c == column {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.cell_clear_contents(sheet, r, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(f) = cell.get_formula() {
|
if let Some(f) = cell.get_formula() {
|
||||||
match result {
|
match result {
|
||||||
CalcResult::Number(value) => {
|
CalcResult::Number(value) => {
|
||||||
@@ -594,19 +810,145 @@ impl Model {
|
|||||||
ei: error.clone(),
|
ei: error.clone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
||||||
|
*self.workbook.worksheets[sheet as usize]
|
||||||
|
.sheet_data
|
||||||
|
.get_mut(&row)
|
||||||
|
.expect("expected a row")
|
||||||
|
.get_mut(&column)
|
||||||
|
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
|
||||||
|
}
|
||||||
CalcResult::Range { left, right } => {
|
CalcResult::Range { left, right } => {
|
||||||
if left.sheet == right.sheet
|
if left.sheet == right.sheet
|
||||||
&& left.row == right.row
|
&& left.row == right.row
|
||||||
&& left.column == right.column
|
&& left.column == right.column
|
||||||
{
|
{
|
||||||
let intersection_cell = CellReferenceIndex {
|
// There is only one cell
|
||||||
|
let single_cell = CellReferenceIndex {
|
||||||
sheet: left.sheet,
|
sheet: left.sheet,
|
||||||
column: left.column,
|
column: left.column,
|
||||||
row: left.row,
|
row: left.row,
|
||||||
};
|
};
|
||||||
let v = self.evaluate_cell(intersection_cell);
|
let v = self.evaluate_cell(single_cell);
|
||||||
self.set_cell_value(cell_reference, &v);
|
self.set_cell_value(cell_reference, &v)?;
|
||||||
} else {
|
} else {
|
||||||
|
// We need to check if all the cells are empty, otherwise we mark the cell as #SPILL!
|
||||||
|
let mut all_empty = true;
|
||||||
|
for r in row..=row + right.row - left.row {
|
||||||
|
for c in column..=column + right.column - left.column {
|
||||||
|
if r == row && c == column {
|
||||||
|
// skip the "mother" cell
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
|
||||||
|
all_empty = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(support) = self.get_support_cell(sheet, r, c) {
|
||||||
|
all_empty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !all_empty {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !all_empty {
|
||||||
|
let o = match self.cell_reference_to_string(&cell_reference) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => "".to_string(),
|
||||||
|
};
|
||||||
|
*self.workbook.worksheets[sheet as usize]
|
||||||
|
.sheet_data
|
||||||
|
.get_mut(&row)
|
||||||
|
.expect("expected a row")
|
||||||
|
.get_mut(&column)
|
||||||
|
.expect("expected a column") = Cell::DynamicCellFormulaError {
|
||||||
|
f,
|
||||||
|
s,
|
||||||
|
o,
|
||||||
|
m: "Result would spill to non empty cells".to_string(),
|
||||||
|
ei: Error::SPILL,
|
||||||
|
r: (1, 1),
|
||||||
|
a: false,
|
||||||
|
};
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// evaluate all the cells in that range
|
||||||
|
for r in left.row..=right.row {
|
||||||
|
for c in left.column..=right.column {
|
||||||
|
let cell_reference = CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row: r,
|
||||||
|
column: c,
|
||||||
|
};
|
||||||
|
// FIXME: We ned to return an error
|
||||||
|
self.evaluate_cell(cell_reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now write the result in the target
|
||||||
|
for r in left.row..=right.row {
|
||||||
|
let row_delta = r - left.row;
|
||||||
|
for c in left.column..=right.column {
|
||||||
|
let column_delta = c - left.column;
|
||||||
|
// We need to put whatever is in (left.sheet, r, c) in
|
||||||
|
// (sheet, row + row_delta, column + column_delta)
|
||||||
|
// But we need to preserve the style
|
||||||
|
let target_row = row + row_delta;
|
||||||
|
let target_column = column + column_delta;
|
||||||
|
let cell = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(left.sheet)?
|
||||||
|
.cell(r, c)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let cell_reference = CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row: r,
|
||||||
|
column: c,
|
||||||
|
};
|
||||||
|
let v = self.get_cell_value(&cell, cell_reference);
|
||||||
|
if row == target_row && column == target_column {
|
||||||
|
// let cell_reference = CellReferenceIndex { sheet, row, column };
|
||||||
|
// self.set_cell_value(cell_reference, &v);
|
||||||
|
self.set_spill_cell_with_formula_value(
|
||||||
|
sheet,
|
||||||
|
target_row,
|
||||||
|
target_column,
|
||||||
|
(right.column - left.column + 1, right.row - left.row + 1),
|
||||||
|
&v,
|
||||||
|
s,
|
||||||
|
f,
|
||||||
|
)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.set_spill_cell_with_value(
|
||||||
|
sheet,
|
||||||
|
target_row,
|
||||||
|
target_column,
|
||||||
|
(row, column),
|
||||||
|
&v,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
let width = array[0].len() as i32;
|
||||||
|
let height = array.len() as i32;
|
||||||
|
// First we check that we don't spill:
|
||||||
|
let mut all_empty = true;
|
||||||
|
for r in row..row + height {
|
||||||
|
for c in column..column + width {
|
||||||
|
if r == row && c == column {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
|
||||||
|
all_empty = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !all_empty {
|
||||||
let o = match self.cell_reference_to_string(&cell_reference) {
|
let o = match self.cell_reference_to_string(&cell_reference) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => "".to_string(),
|
Err(_) => "".to_string(),
|
||||||
@@ -620,57 +962,65 @@ impl Model {
|
|||||||
f,
|
f,
|
||||||
s,
|
s,
|
||||||
o,
|
o,
|
||||||
m: "Implicit Intersection not implemented".to_string(),
|
m: "Result would spill to non empty cells".to_string(),
|
||||||
ei: Error::NIMPL,
|
ei: Error::SPILL,
|
||||||
};
|
};
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut target_row = row;
|
||||||
|
for data_row in array {
|
||||||
|
let mut target_column = column;
|
||||||
|
for value in data_row {
|
||||||
|
if row == target_row && column == target_column {
|
||||||
|
// This is the root cell of the dynamic array
|
||||||
|
let cell_reference = CellReferenceIndex { sheet, row, column };
|
||||||
|
let v = match value {
|
||||||
|
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
|
||||||
|
ArrayNode::Number(f) => CalcResult::Number(*f),
|
||||||
|
ArrayNode::String(s) => CalcResult::String(s.clone()),
|
||||||
|
ArrayNode::Error(error) => CalcResult::new_error(
|
||||||
|
error.clone(),
|
||||||
|
cell_reference,
|
||||||
|
error.to_localized_error_string(&self.language),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
self.set_spill_cell_with_formula_value(
|
||||||
|
sheet,
|
||||||
|
target_row,
|
||||||
|
target_column,
|
||||||
|
(width, height),
|
||||||
|
&v,
|
||||||
|
s,
|
||||||
|
f,
|
||||||
|
)?;
|
||||||
|
target_column += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let v = match value {
|
||||||
|
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
|
||||||
|
ArrayNode::Number(f) => CalcResult::Number(*f),
|
||||||
|
ArrayNode::String(s) => CalcResult::String(s.clone()),
|
||||||
|
ArrayNode::Error(error) => CalcResult::new_error(
|
||||||
|
error.clone(),
|
||||||
|
cell_reference,
|
||||||
|
error.to_localized_error_string(&self.language),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
self.set_spill_cell_with_value(
|
||||||
|
sheet,
|
||||||
|
target_row,
|
||||||
|
target_column,
|
||||||
|
(row, column),
|
||||||
|
&v,
|
||||||
|
)?;
|
||||||
|
target_column += 1;
|
||||||
|
}
|
||||||
|
target_row += 1;
|
||||||
}
|
}
|
||||||
// if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
|
|
||||||
// {
|
|
||||||
// let v = self.evaluate_cell(intersection_cell);
|
|
||||||
// self.set_cell_value(cell_reference, &v);
|
|
||||||
// } else {
|
|
||||||
// let o = match self.cell_reference_to_string(&cell_reference) {
|
|
||||||
// Ok(s) => s,
|
|
||||||
// Err(_) => "".to_string(),
|
|
||||||
// };
|
|
||||||
// *self.workbook.worksheets[sheet as usize]
|
|
||||||
// .sheet_data
|
|
||||||
// .get_mut(&row)
|
|
||||||
// .expect("expected a row")
|
|
||||||
// .get_mut(&column)
|
|
||||||
// .expect("expected a column") = Cell::CellFormulaError {
|
|
||||||
// f,
|
|
||||||
// s,
|
|
||||||
// o,
|
|
||||||
// m: "Invalid reference".to_string(),
|
|
||||||
// ei: Error::VALUE,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
|
||||||
*self.workbook.worksheets[sheet as usize]
|
|
||||||
.sheet_data
|
|
||||||
.get_mut(&row)
|
|
||||||
.expect("expected a row")
|
|
||||||
.get_mut(&column)
|
|
||||||
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
|
|
||||||
}
|
|
||||||
CalcResult::Array(_) => {
|
|
||||||
*self.workbook.worksheets[sheet as usize]
|
|
||||||
.sheet_data
|
|
||||||
.get_mut(&row)
|
|
||||||
.expect("expected a row")
|
|
||||||
.get_mut(&column)
|
|
||||||
.expect("expected a column") = Cell::CellFormulaError {
|
|
||||||
f,
|
|
||||||
s,
|
|
||||||
o: "".to_string(),
|
|
||||||
m: "Arrays not supported yet".to_string(),
|
|
||||||
ei: Error::NIMPL,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the color of the sheet tab.
|
/// Sets the color of the sheet tab.
|
||||||
@@ -697,7 +1047,7 @@ impl Model {
|
|||||||
worksheet.color = Some(color.to_string());
|
worksheet.color = Some(color.to_string());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(format!("Invalid color: {}", color))
|
Err(format!("Invalid color: {color}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes the visibility of a sheet
|
/// Changes the visibility of a sheet
|
||||||
@@ -714,16 +1064,18 @@ impl Model {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmptyCell, Boolean, Number, String, Error
|
||||||
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||||
use Cell::*;
|
use Cell::*;
|
||||||
match cell {
|
match cell {
|
||||||
EmptyCell { .. } => CalcResult::EmptyCell,
|
EmptyCell { .. } => CalcResult::EmptyCell,
|
||||||
BooleanCell { v, .. } => CalcResult::Boolean(*v),
|
BooleanCell { v, .. } | SpillBooleanCell { v, .. } => CalcResult::Boolean(*v),
|
||||||
NumberCell { v, .. } => CalcResult::Number(*v),
|
NumberCell { v, .. } | SpillNumberCell { v, .. } => CalcResult::Number(*v),
|
||||||
ErrorCell { ei, .. } => {
|
ErrorCell { ei, .. } | SpillErrorCell { ei, .. } => {
|
||||||
let message = ei.to_localized_error_string(&self.language);
|
let message = ei.to_localized_error_string(&self.language);
|
||||||
CalcResult::new_error(ei.clone(), cell_reference, message)
|
CalcResult::new_error(ei.clone(), cell_reference, message)
|
||||||
}
|
}
|
||||||
|
SpillStringCell { v, .. } => CalcResult::String(v.clone()),
|
||||||
SharedString { si, .. } => {
|
SharedString { si, .. } => {
|
||||||
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
|
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
|
||||||
CalcResult::String(s.clone())
|
CalcResult::String(s.clone())
|
||||||
@@ -732,15 +1084,21 @@ impl Model {
|
|||||||
CalcResult::new_error(Error::ERROR, cell_reference, message)
|
CalcResult::new_error(Error::ERROR, cell_reference, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CellFormula { .. } => CalcResult::Error {
|
DynamicCellFormula { .. } | CellFormula { .. } => CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
origin: cell_reference,
|
origin: cell_reference,
|
||||||
message: "Unevaluated formula".to_string(),
|
message: "Unevaluated formula".to_string(),
|
||||||
},
|
},
|
||||||
CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v),
|
DynamicCellFormulaBoolean { v, .. } | CellFormulaBoolean { v, .. } => {
|
||||||
CellFormulaNumber { v, .. } => CalcResult::Number(*v),
|
CalcResult::Boolean(*v)
|
||||||
CellFormulaString { v, .. } => CalcResult::String(v.clone()),
|
}
|
||||||
CellFormulaError { ei, o, m, .. } => {
|
DynamicCellFormulaNumber { v, .. } | CellFormulaNumber { v, .. } => {
|
||||||
|
CalcResult::Number(*v)
|
||||||
|
}
|
||||||
|
DynamicCellFormulaString { v, .. } | CellFormulaString { v, .. } => {
|
||||||
|
CalcResult::String(v.clone())
|
||||||
|
}
|
||||||
|
DynamicCellFormulaError { ei, o, m, .. } | CellFormulaError { ei, o, m, .. } => {
|
||||||
if let Some(cell_reference) = self.parse_reference(o) {
|
if let Some(cell_reference) = self.parse_reference(o) {
|
||||||
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
|
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
|
||||||
} else {
|
} else {
|
||||||
@@ -772,6 +1130,8 @@ impl Model {
|
|||||||
self.workbook.worksheet(sheet)?.is_empty_cell(row, column)
|
self.workbook.worksheet(sheet)?.is_empty_cell(row, column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluates the cell. After the evaluation is done puts the value in the cell and other cells if it spills.
|
||||||
|
/// If when writing a spill cell encounter a cell whose value has been requested marks the model as "dirty"
|
||||||
pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||||
let row_data = match self.workbook.worksheets[cell_reference.sheet as usize]
|
let row_data = match self.workbook.worksheets[cell_reference.sheet as usize]
|
||||||
.sheet_data
|
.sheet_data
|
||||||
@@ -810,9 +1170,10 @@ impl Model {
|
|||||||
self.cells.insert(key, CellState::Evaluating);
|
self.cells.insert(key, CellState::Evaluating);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone();
|
let (node, _static_result) =
|
||||||
let result = self.evaluate_node_in_context(node, cell_reference);
|
&self.parsed_formulas[cell_reference.sheet as usize][f as usize];
|
||||||
self.set_cell_value(cell_reference, &result);
|
let result = self.evaluate_node_in_context(&node.clone(), cell_reference);
|
||||||
|
let _ = self.set_cell_value(cell_reference, &result);
|
||||||
// mark cell as evaluated
|
// mark cell as evaluated
|
||||||
self.cells.insert(key, CellState::Evaluated);
|
self.cells.insert(key, CellState::Evaluated);
|
||||||
result
|
result
|
||||||
@@ -922,6 +1283,10 @@ impl Model {
|
|||||||
locale,
|
locale,
|
||||||
tz,
|
tz,
|
||||||
view_id: 0,
|
view_id: 0,
|
||||||
|
support_graph: HashMap::new(),
|
||||||
|
switch_cells: None,
|
||||||
|
stack: Vec::new(),
|
||||||
|
state: EvaluationState::Ready,
|
||||||
};
|
};
|
||||||
|
|
||||||
model.parse_formulas();
|
model.parse_formulas();
|
||||||
@@ -1027,7 +1392,7 @@ impl Model {
|
|||||||
let source_sheet_name = self
|
let source_sheet_name = self
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(source.sheet)
|
.worksheet(source.sheet)
|
||||||
.map_err(|e| format!("Could not find source worksheet: {}", e))?
|
.map_err(|e| format!("Could not find source worksheet: {e}"))?
|
||||||
.get_name();
|
.get_name();
|
||||||
if source.sheet != area.sheet {
|
if source.sheet != area.sheet {
|
||||||
return Err("Source and area are in different sheets".to_string());
|
return Err("Source and area are in different sheets".to_string());
|
||||||
@@ -1041,7 +1406,7 @@ impl Model {
|
|||||||
let target_sheet_name = self
|
let target_sheet_name = self
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(target.sheet)
|
.worksheet(target.sheet)
|
||||||
.map_err(|e| format!("Could not find target worksheet: {}", e))?
|
.map_err(|e| format!("Could not find target worksheet: {e}"))?
|
||||||
.get_name();
|
.get_name();
|
||||||
if let Some(formula) = value.strip_prefix('=') {
|
if let Some(formula) = value.strip_prefix('=') {
|
||||||
let cell_reference = CellReferenceRC {
|
let cell_reference = CellReferenceRC {
|
||||||
@@ -1061,7 +1426,7 @@ impl Model {
|
|||||||
column_delta: target.column - source.column,
|
column_delta: target.column - source.column,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(format!("={}", formula_str))
|
Ok(format!("={formula_str}"))
|
||||||
} else {
|
} else {
|
||||||
Ok(value.to_string())
|
Ok(value.to_string())
|
||||||
}
|
}
|
||||||
@@ -1100,7 +1465,8 @@ impl Model {
|
|||||||
Some(cell) => match cell.get_formula() {
|
Some(cell) => match cell.get_formula() {
|
||||||
None => cell.get_text(&self.workbook.shared_strings, &self.language),
|
None => cell.get_text(&self.workbook.shared_strings, &self.language),
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
let formula = &self.parsed_formulas[sheet as usize][i as usize];
|
let (formula, static_result) =
|
||||||
|
&self.parsed_formulas[sheet as usize][i as usize];
|
||||||
let cell_ref = CellReferenceRC {
|
let cell_ref = CellReferenceRC {
|
||||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||||
row: target_row,
|
row: target_row,
|
||||||
@@ -1203,7 +1569,8 @@ impl Model {
|
|||||||
.get(sheet as usize)
|
.get(sheet as usize)
|
||||||
.ok_or("missing sheet")?
|
.ok_or("missing sheet")?
|
||||||
.get(formula_index as usize)
|
.get(formula_index as usize)
|
||||||
.ok_or("missing formula")?;
|
.ok_or("missing formula")?
|
||||||
|
.0;
|
||||||
let cell_ref = CellReferenceRC {
|
let cell_ref = CellReferenceRC {
|
||||||
sheet: worksheet.get_name(),
|
sheet: worksheet.get_name(),
|
||||||
row,
|
row,
|
||||||
@@ -1437,6 +1804,25 @@ impl Model {
|
|||||||
column: i32,
|
column: i32,
|
||||||
value: String,
|
value: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
// We need to check if the cell is part of a dynamic array
|
||||||
|
let cell = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(row, column)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
// If the cell is a dynamic cell we need to delete all the cells in the range
|
||||||
|
if let Some((width, height)) = cell.get_dynamic_range() {
|
||||||
|
for r in row..row + height {
|
||||||
|
for c in column..column + width {
|
||||||
|
// skip the "mother" cell
|
||||||
|
if r == row && c == column {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.cell_clear_contents(sheet, r, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// If value starts with "'" then we force the style to be quote_prefix
|
// If value starts with "'" then we force the style to be quote_prefix
|
||||||
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||||
if let Some(new_value) = value.strip_prefix('\'') {
|
if let Some(new_value) = value.strip_prefix('\'') {
|
||||||
@@ -1462,8 +1848,9 @@ impl Model {
|
|||||||
self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?;
|
self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?;
|
||||||
// Update the style if needed
|
// Update the style if needed
|
||||||
let cell = CellReferenceIndex { sheet, row, column };
|
let cell = CellReferenceIndex { sheet, row, column };
|
||||||
let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
let (parsed_formula, static_result) =
|
||||||
if let Some(units) = self.compute_node_units(parsed_formula, &cell) {
|
self.parsed_formulas[sheet as usize][formula_index as usize].clone();
|
||||||
|
if let Some(units) = self.compute_node_units(&parsed_formula, &cell) {
|
||||||
let new_style_index = self
|
let new_style_index = self
|
||||||
.workbook
|
.workbook
|
||||||
.styles
|
.styles
|
||||||
@@ -1471,6 +1858,14 @@ impl Model {
|
|||||||
let style = self.workbook.styles.get_style(new_style_index)?;
|
let style = self.workbook.styles.get_style(new_style_index)?;
|
||||||
self.set_cell_style(sheet, row, column, &style)?
|
self.set_cell_style(sheet, row, column, &style)?
|
||||||
}
|
}
|
||||||
|
match static_result {
|
||||||
|
StaticResult::Scalar => {}
|
||||||
|
StaticResult::Array(_, _)
|
||||||
|
| StaticResult::Range(_, _)
|
||||||
|
| StaticResult::Unknown => {
|
||||||
|
self.workbook.spill_cells.push((sheet, row, column));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// The list of currencies is '$', '€' and the local currency
|
// The list of currencies is '$', '€' and the local currency
|
||||||
let mut currencies = vec!["$", "€"];
|
let mut currencies = vec!["$", "€"];
|
||||||
@@ -1538,12 +1933,13 @@ impl Model {
|
|||||||
// If the formula fails to parse try adding a parenthesis
|
// If the formula fails to parse try adding a parenthesis
|
||||||
// SUM(A1:A3 => SUM(A1:A3)
|
// SUM(A1:A3 => SUM(A1:A3)
|
||||||
if let Node::ParseErrorKind { .. } = parsed_formula {
|
if let Node::ParseErrorKind { .. } = parsed_formula {
|
||||||
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference);
|
let new_parsed_formula = self.parser.parse(&format!("{formula})"), &cell_reference);
|
||||||
match new_parsed_formula {
|
match new_parsed_formula {
|
||||||
Node::ParseErrorKind { .. } => {}
|
Node::ParseErrorKind { .. } => {}
|
||||||
_ => parsed_formula = new_parsed_formula,
|
_ => parsed_formula = new_parsed_formula,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let static_result = run_static_analysis_on_node(&parsed_formula);
|
||||||
|
|
||||||
let s = to_rc_format(&parsed_formula);
|
let s = to_rc_format(&parsed_formula);
|
||||||
let mut formula_index: i32 = -1;
|
let mut formula_index: i32 = -1;
|
||||||
@@ -1552,7 +1948,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
if formula_index == -1 {
|
if formula_index == -1 {
|
||||||
shared_formulas.push(s);
|
shared_formulas.push(s);
|
||||||
self.parsed_formulas[sheet as usize].push(parsed_formula);
|
self.parsed_formulas[sheet as usize].push((parsed_formula, static_result));
|
||||||
formula_index = (shared_formulas.len() as i32) - 1;
|
formula_index = (shared_formulas.len() as i32) - 1;
|
||||||
}
|
}
|
||||||
worksheet.set_cell_with_formula(row, column, formula_index, style)?;
|
worksheet.set_cell_with_formula(row, column, formula_index, style)?;
|
||||||
@@ -1747,7 +2143,7 @@ impl Model {
|
|||||||
};
|
};
|
||||||
match cell.get_formula() {
|
match cell.get_formula() {
|
||||||
Some(formula_index) => {
|
Some(formula_index) => {
|
||||||
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize].0;
|
||||||
let cell_ref = CellReferenceRC {
|
let cell_ref = CellReferenceRC {
|
||||||
sheet: worksheet.get_name(),
|
sheet: worksheet.get_name(),
|
||||||
row,
|
row,
|
||||||
@@ -1783,9 +2179,34 @@ impl Model {
|
|||||||
|
|
||||||
/// Evaluates the model with a top-down recursive algorithm
|
/// Evaluates the model with a top-down recursive algorithm
|
||||||
pub fn evaluate(&mut self) {
|
pub fn evaluate(&mut self) {
|
||||||
// clear all computation artifacts
|
// We first evaluate all the cells that might spill to other cells
|
||||||
self.cells.clear();
|
let mut spills_computed = false;
|
||||||
|
self.state = EvaluationState::EvaluatingSpills;
|
||||||
|
while !spills_computed {
|
||||||
|
spills_computed = true;
|
||||||
|
// clear all computation artifacts
|
||||||
|
self.cells.clear();
|
||||||
|
// Evaluate all the cells that might spill
|
||||||
|
let spill_cells = self.workbook.spill_cells.clone();
|
||||||
|
for (sheet, row, column) in spill_cells {
|
||||||
|
self.evaluate_cell(CellReferenceIndex { sheet, row, column });
|
||||||
|
if self.switch_cells.is_some() {
|
||||||
|
spills_computed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((index1, index2)) = self.switch_cells {
|
||||||
|
spills_computed = false;
|
||||||
|
// switch the cells indices in the spill_cells
|
||||||
|
let cell1 = self.workbook.spill_cells[index1 as usize];
|
||||||
|
let cell2 = self.workbook.spill_cells[index2 as usize];
|
||||||
|
self.workbook.spill_cells[index1 as usize] = cell2;
|
||||||
|
self.workbook.spill_cells[index2 as usize] = cell1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state = EvaluationState::Evaluating;
|
||||||
|
|
||||||
|
// Now we compute all the rest
|
||||||
let cells = self.get_all_cells();
|
let cells = self.get_all_cells();
|
||||||
|
|
||||||
for cell in cells {
|
for cell in cells {
|
||||||
@@ -1795,6 +2216,7 @@ impl Model {
|
|||||||
column: cell.column,
|
column: cell.column,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
self.state = EvaluationState::Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the content of the cell but leaves the style.
|
/// Removes the content of the cell but leaves the style.
|
||||||
@@ -1818,9 +2240,22 @@ impl Model {
|
|||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||||
self.workbook
|
// If it has a spill formula we need to delete the contents of all the spilled cells
|
||||||
.worksheet_mut(sheet)?
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||||
.cell_clear_contents(row, column)?;
|
if let Some(cell) = worksheet.cell(row, column) {
|
||||||
|
if let Some((width, height)) = cell.get_dynamic_range() {
|
||||||
|
for r in row..row + height {
|
||||||
|
for c in column..column + width {
|
||||||
|
if row == r && column == c {
|
||||||
|
// we skip the root cell
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
worksheet.cell_clear_contents(r, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worksheet.cell_clear_contents(row, column)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,6 +2280,18 @@ impl Model {
|
|||||||
/// # }
|
/// # }
|
||||||
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
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)?;
|
||||||
|
// Delete the contents of spilled cells if any
|
||||||
|
if let Some(cell) = worksheet.cell(row, column) {
|
||||||
|
if let Some((width, height)) = cell.get_dynamic_range() {
|
||||||
|
for r in row..row + height {
|
||||||
|
for c in column..column + width {
|
||||||
|
if row == r && c == column {
|
||||||
|
worksheet.cell_clear_contents(r, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sheet_data = &mut worksheet.sheet_data;
|
let sheet_data = &mut worksheet.sheet_data;
|
||||||
if let Some(row_data) = sheet_data.get_mut(&row) {
|
if let Some(row_data) = sheet_data.get_mut(&row) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
expressions::{
|
expressions::{
|
||||||
lexer::LexerMode,
|
lexer::LexerMode,
|
||||||
parser::{
|
parser::{
|
||||||
|
static_analysis::run_static_analysis_on_node,
|
||||||
stringify::{rename_sheet_in_node, to_rc_format, to_string},
|
stringify::{rename_sheet_in_node, to_rc_format, to_string},
|
||||||
Parser,
|
Parser,
|
||||||
},
|
},
|
||||||
@@ -15,7 +16,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
language::get_language,
|
language::get_language,
|
||||||
locale::get_locale,
|
locale::get_locale,
|
||||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
model::{get_milliseconds_since_epoch, EvaluationState, Model, ParsedDefinedName},
|
||||||
types::{
|
types::{
|
||||||
DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
|
DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
|
||||||
WorksheetView,
|
WorksheetView,
|
||||||
@@ -94,7 +95,8 @@ impl Model {
|
|||||||
let mut parse_formula = Vec::new();
|
let mut parse_formula = Vec::new();
|
||||||
for formula in shared_formulas {
|
for formula in shared_formulas {
|
||||||
let t = self.parser.parse(formula, &cell_reference);
|
let t = self.parser.parse(formula, &cell_reference);
|
||||||
parse_formula.push(t);
|
let static_result = run_static_analysis_on_node(&t);
|
||||||
|
parse_formula.push((t, static_result));
|
||||||
}
|
}
|
||||||
self.parsed_formulas.push(parse_formula);
|
self.parsed_formulas.push(parse_formula);
|
||||||
}
|
}
|
||||||
@@ -168,11 +170,11 @@ impl Model {
|
|||||||
.get_worksheet_names()
|
.get_worksheet_names()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.to_uppercase())
|
.map(|s| s.to_uppercase())
|
||||||
.any(|x| x == format!("{}{}", base_name_uppercase, index))
|
.any(|x| x == format!("{base_name_uppercase}{index}"))
|
||||||
{
|
{
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
let sheet_name = format!("{}{}", base_name, index);
|
let sheet_name = format!("{base_name}{index}");
|
||||||
// Now we need a sheet_id
|
// Now we need a sheet_id
|
||||||
let sheet_id = self.get_new_sheet_id();
|
let sheet_id = self.get_new_sheet_id();
|
||||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||||
@@ -192,7 +194,7 @@ impl Model {
|
|||||||
sheet_id: Option<u32>,
|
sheet_id: Option<u32>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !is_valid_sheet_name(sheet_name) {
|
if !is_valid_sheet_name(sheet_name) {
|
||||||
return Err(format!("Invalid name for a sheet: '{}'", sheet_name));
|
return Err(format!("Invalid name for a sheet: '{sheet_name}'"));
|
||||||
}
|
}
|
||||||
if self
|
if self
|
||||||
.workbook
|
.workbook
|
||||||
@@ -234,7 +236,7 @@ impl Model {
|
|||||||
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
|
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
|
||||||
return self.rename_sheet_by_index(sheet_index, new_name);
|
return self.rename_sheet_by_index(sheet_index, new_name);
|
||||||
}
|
}
|
||||||
Err(format!("Could not find sheet {}", old_name))
|
Err(format!("Could not find sheet {old_name}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renames a sheet and updates all existing references to that sheet.
|
/// Renames a sheet and updates all existing references to that sheet.
|
||||||
@@ -248,10 +250,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}'."));
|
||||||
}
|
}
|
||||||
// Gets the new name and checks that a sheet with that index exists
|
// Gets the new name and checks that a sheet with that index exists
|
||||||
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
|
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
|
||||||
@@ -362,14 +364,14 @@ impl Model {
|
|||||||
};
|
};
|
||||||
let locale = match get_locale(locale_id) {
|
let locale = match get_locale(locale_id) {
|
||||||
Ok(l) => l.clone(),
|
Ok(l) => l.clone(),
|
||||||
Err(_) => return Err(format!("Invalid locale: {}", locale_id)),
|
Err(_) => return Err(format!("Invalid locale: {locale_id}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let milliseconds = get_milliseconds_since_epoch();
|
let milliseconds = get_milliseconds_since_epoch();
|
||||||
let seconds = milliseconds / 1000;
|
let seconds = milliseconds / 1000;
|
||||||
let dt = match DateTime::from_timestamp(seconds, 0) {
|
let dt = match DateTime::from_timestamp(seconds, 0) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
None => return Err(format!("Invalid timestamp: {milliseconds}")),
|
||||||
};
|
};
|
||||||
// "2020-08-06T21:20:53Z
|
// "2020-08-06T21:20:53Z
|
||||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
@@ -405,6 +407,7 @@ impl Model {
|
|||||||
},
|
},
|
||||||
tables: HashMap::new(),
|
tables: HashMap::new(),
|
||||||
views,
|
views,
|
||||||
|
spill_cells: Vec::new(),
|
||||||
};
|
};
|
||||||
let parsed_formulas = Vec::new();
|
let parsed_formulas = Vec::new();
|
||||||
let worksheets = &workbook.worksheets;
|
let worksheets = &workbook.worksheets;
|
||||||
@@ -427,6 +430,10 @@ impl Model {
|
|||||||
language,
|
language,
|
||||||
tz,
|
tz,
|
||||||
view_id: 0,
|
view_id: 0,
|
||||||
|
support_graph: HashMap::new(),
|
||||||
|
switch_cells: None,
|
||||||
|
stack: Vec::new(),
|
||||||
|
state: EvaluationState::Ready,
|
||||||
};
|
};
|
||||||
model.parse_formulas();
|
model.parse_formulas();
|
||||||
Ok(model)
|
Ok(model)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ pub fn to_precision_str(value: f64, precision: usize) -> String {
|
|||||||
let exponent = value.abs().log10().floor();
|
let exponent = value.abs().log10().floor();
|
||||||
let base = value / 10.0_f64.powf(exponent);
|
let base = value / 10.0_f64.powf(exponent);
|
||||||
let base = format!("{0:.1$}", base, precision - 1);
|
let base = format!("{0:.1$}", base, precision - 1);
|
||||||
let value = format!("{}e{}", base, exponent).parse::<f64>().unwrap_or({
|
let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
|
||||||
// TODO: do this in a way that does not require a possible error
|
// TODO: do this in a way that does not require a possible error
|
||||||
0.0
|
0.0
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ impl Styles {
|
|||||||
return Ok(cell_style.xf_id);
|
return Ok(cell_style.xf_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(format!("Style '{}' not found", style_name))
|
Err(format!("Style '{style_name}' not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {
|
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {
|
||||||
|
|||||||
@@ -52,15 +52,20 @@ mod test_fn_offset;
|
|||||||
mod test_number_format;
|
mod test_number_format;
|
||||||
|
|
||||||
mod test_arrays;
|
mod test_arrays;
|
||||||
|
mod test_dynamic_arrays;
|
||||||
mod test_escape_quotes;
|
mod test_escape_quotes;
|
||||||
mod test_extend;
|
mod test_extend;
|
||||||
mod test_fn_fv;
|
mod test_fn_fv;
|
||||||
|
mod test_fn_round;
|
||||||
mod test_fn_type;
|
mod test_fn_type;
|
||||||
mod test_frozen_rows_and_columns;
|
mod test_frozen_rows_and_columns;
|
||||||
mod test_geomean;
|
mod test_geomean;
|
||||||
mod test_get_cell_content;
|
mod test_get_cell_content;
|
||||||
mod test_implicit_intersection;
|
mod test_implicit_intersection;
|
||||||
mod test_issue_155;
|
mod test_issue_155;
|
||||||
|
mod test_ln;
|
||||||
|
mod test_log;
|
||||||
|
mod test_log10;
|
||||||
mod test_percentage;
|
mod test_percentage;
|
||||||
mod test_set_functions_error_handling;
|
mod test_set_functions_error_handling;
|
||||||
mod test_today;
|
mod test_today;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN};
|
use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
|
||||||
use crate::model::Model;
|
use crate::model::Model;
|
||||||
use crate::test::util::new_empty_model;
|
use crate::test::util::new_empty_model;
|
||||||
use crate::types::Col;
|
use crate::types::Col;
|
||||||
@@ -508,6 +508,10 @@ fn test_move_column_right() {
|
|||||||
assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)");
|
assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)");
|
||||||
assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)");
|
assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)");
|
||||||
assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)");
|
assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)");
|
||||||
|
|
||||||
|
// Data moved as well
|
||||||
|
assert_eq!(model._get_text("G1"), "1");
|
||||||
|
assert_eq!(model._get_text("H1"), "3");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -532,5 +536,249 @@ fn tets_move_column_error() {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_row_down() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
// Formulas referencing rows 3 and 4
|
||||||
|
model._set("E3", "=G3");
|
||||||
|
model._set("E4", "=G4");
|
||||||
|
model._set("E5", "=SUM(G3:J3)");
|
||||||
|
model._set("E6", "=SUM(G3:G3)");
|
||||||
|
model._set("E7", "=SUM(G4:G4)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Move row 3 down by one position
|
||||||
|
let result = model.move_row_action(0, 3, 1);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("E3"), "=G3");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=G4");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=SUM(G4:J4)");
|
||||||
|
assert_eq!(model._get_formula("E6"), "=SUM(G4:G4)");
|
||||||
|
assert_eq!(model._get_formula("E7"), "=SUM(G3:G3)");
|
||||||
|
|
||||||
|
// Data moved as well
|
||||||
|
assert_eq!(model._get_text("G4"), "-2");
|
||||||
|
assert_eq!(model._get_text("G3"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_row_up() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
// Formulas referencing rows 4 and 5
|
||||||
|
model._set("E4", "=G4");
|
||||||
|
model._set("E5", "=G5");
|
||||||
|
model._set("E6", "=SUM(G4:J4)");
|
||||||
|
model._set("E7", "=SUM(G4:G4)");
|
||||||
|
model._set("E8", "=SUM(G5:G5)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Move row 5 up by one position
|
||||||
|
let result = model.move_row_action(0, 5, -1);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("E4"), "=G4");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=G5");
|
||||||
|
assert_eq!(model._get_formula("E6"), "=SUM(G5:J5)");
|
||||||
|
assert_eq!(model._get_formula("E7"), "=SUM(G5:G5)");
|
||||||
|
assert_eq!(model._get_formula("E8"), "=SUM(G4:G4)");
|
||||||
|
|
||||||
|
// Data moved as well
|
||||||
|
assert_eq!(model._get_text("G4"), "");
|
||||||
|
assert_eq!(model._get_text("G5"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_row_error() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
let result = model.move_row_action(0, 7, -10);
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
let result = model.move_row_action(0, -7, 20);
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
let result = model.move_row_action(0, LAST_ROW, 1);
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
let result = model.move_row_action(0, LAST_ROW + 1, -10);
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// This works
|
||||||
|
let result = model.move_row_action(0, LAST_ROW, -1);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_row_down_absolute_refs() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
// Absolute references
|
||||||
|
model._set("E3", "=$G$3");
|
||||||
|
model._set("E4", "=$G$4");
|
||||||
|
model._set("E5", "=SUM($G$3:$J$3)");
|
||||||
|
model._set("E6", "=SUM($G$3:$G$3)");
|
||||||
|
model._set("E7", "=SUM($G$4:$G$4)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert!(model.move_row_action(0, 3, 1).is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("E3"), "=$G$3");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=$G$4");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=SUM($G$4:$J$4)");
|
||||||
|
assert_eq!(model._get_formula("E6"), "=SUM($G$4:$G$4)");
|
||||||
|
assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$3)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_column_right_absolute_refs() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
// Absolute references
|
||||||
|
model._set("E3", "=$G$3");
|
||||||
|
model._set("E4", "=$H$3");
|
||||||
|
model._set("E5", "=SUM($G$3:$J$7)");
|
||||||
|
model._set("E6", "=SUM($G$3:$G$7)");
|
||||||
|
model._set("E7", "=SUM($H$3:$H$7)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert!(model.move_column_action(0, 7, 1).is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("E3"), "=$H$3");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=$G$3");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=SUM($H$3:$J$7)");
|
||||||
|
assert_eq!(model._get_formula("E6"), "=SUM($H$3:$H$7)");
|
||||||
|
assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$7)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_row_down_mixed_refs() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
model._set("E3", "=$G3"); // absolute col, relative row
|
||||||
|
model._set("E4", "=$G4");
|
||||||
|
model._set("E5", "=SUM($G3:$J3)");
|
||||||
|
model._set("E6", "=SUM($G3:$G3)");
|
||||||
|
model._set("E7", "=SUM($G4:$G4)");
|
||||||
|
model._set("F3", "=H$3"); // relative col, absolute row
|
||||||
|
model._set("F4", "=G$3");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert!(model.move_row_action(0, 3, 1).is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("E3"), "=$G3");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=$G4");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=SUM($G4:$J4)");
|
||||||
|
assert_eq!(model._get_formula("E6"), "=SUM($G4:$G4)");
|
||||||
|
assert_eq!(model._get_formula("E7"), "=SUM($G3:$G3)");
|
||||||
|
assert_eq!(model._get_formula("F3"), "=G$4");
|
||||||
|
assert_eq!(model._get_formula("F4"), "=H$4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_column_right_mixed_refs() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
model._set("E3", "=$G3");
|
||||||
|
model._set("E4", "=$H3");
|
||||||
|
model._set("E5", "=SUM($G3:$J7)");
|
||||||
|
model._set("E6", "=SUM($G3:$G7)");
|
||||||
|
model._set("E7", "=SUM($H3:$H7)");
|
||||||
|
model._set("F3", "=H$3");
|
||||||
|
model._set("F4", "=H$3");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert!(model.move_column_action(0, 7, 1).is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("E3"), "=$H3");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=$G3");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=SUM($H3:$J7)");
|
||||||
|
assert_eq!(model._get_formula("E6"), "=SUM($H3:$H7)");
|
||||||
|
assert_eq!(model._get_formula("E7"), "=SUM($G3:$G7)");
|
||||||
|
assert_eq!(model._get_formula("F3"), "=G$3");
|
||||||
|
assert_eq!(model._get_formula("F4"), "=G$3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_row_height() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
let sheet = 0;
|
||||||
|
let custom_height = DEFAULT_ROW_HEIGHT * 2.0;
|
||||||
|
// Set a custom height for row 3
|
||||||
|
model
|
||||||
|
.workbook
|
||||||
|
.worksheet_mut(sheet)
|
||||||
|
.unwrap()
|
||||||
|
.set_row_height(3, custom_height)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Record the original height of row 4 (should be the default)
|
||||||
|
let original_row4_height = model.get_row_height(sheet, 4).unwrap();
|
||||||
|
|
||||||
|
// Move row 3 down by one position
|
||||||
|
assert!(model.move_row_action(sheet, 3, 1).is_ok());
|
||||||
|
|
||||||
|
// The custom height should now be on row 4
|
||||||
|
assert_eq!(model.get_row_height(sheet, 4), Ok(custom_height));
|
||||||
|
|
||||||
|
// Row 3 should now have the previous height of row 4
|
||||||
|
assert_eq!(model.get_row_height(sheet, 3), Ok(original_row4_height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moving a row down by two positions should shift formulas on intermediate
|
||||||
|
/// rows by only one (the row that gets skipped), not by the full delta ‒ this
|
||||||
|
/// guards against the regression fixed in the RowMove displacement logic.
|
||||||
|
#[test]
|
||||||
|
fn test_row_move_down_two_updates_intermediate_refs_by_one() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
// Set up formulas to verify intermediate rows shift by 1 (not full delta).
|
||||||
|
model._set("E3", "=G3"); // target row
|
||||||
|
model._set("E4", "=G4"); // intermediate row
|
||||||
|
model._set("E5", "=SUM(G3:J3)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Move row 3 down by two positions (row 3 -> row 5)
|
||||||
|
assert!(model.move_row_action(0, 3, 2).is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Assert that references for the moved row and intermediate row are correct.
|
||||||
|
assert_eq!(model._get_formula("E3"), "=G3");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=G5");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=SUM(G5:J5)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moving a column right by two positions should shift formulas on
|
||||||
|
/// intermediate columns by only one, ensuring the ColumnMove displacement
|
||||||
|
/// logic handles multi-position moves correctly.
|
||||||
|
#[test]
|
||||||
|
fn test_column_move_right_two_updates_intermediate_refs_by_one() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
populate_table(&mut model);
|
||||||
|
// Set up formulas to verify intermediate columns shift by 1 (not full delta).
|
||||||
|
model._set("E3", "=$G3"); // target column
|
||||||
|
model._set("E4", "=$H3"); // intermediate column
|
||||||
|
model._set("E5", "=SUM($G3:$J7)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Move column G (7) right by two positions (G -> I)
|
||||||
|
assert!(model.move_column_action(0, 7, 2).is_ok());
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Assert that references for moved and intermediate columns are correct.
|
||||||
|
assert_eq!(model._get_formula("E3"), "=$I3");
|
||||||
|
assert_eq!(model._get_formula("E4"), "=$G3");
|
||||||
|
assert_eq!(model._get_formula("E5"), "=SUM($I3:$J7)");
|
||||||
|
}
|
||||||
|
|
||||||
// A B C D E F G H I J K L M N O P Q R
|
// A B C D E F G H I J K L M N O P Q R
|
||||||
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
||||||
|
|||||||
50
base/src/test/test_dynamic_arrays.rs
Normal file
50
base/src/test/test_dynamic_arrays.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn they_spill() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "42");
|
||||||
|
model._set("A2", "5");
|
||||||
|
model._set("A3", "7");
|
||||||
|
|
||||||
|
model._set("B1", "=A1:A3");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B1"), *"42");
|
||||||
|
assert_eq!(model._get_text("B2"), *"5");
|
||||||
|
assert_eq!(model._get_text("B3"), *"7");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spill_error() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "42");
|
||||||
|
model._set("A2", "5");
|
||||||
|
model._set("A3", "7");
|
||||||
|
|
||||||
|
model._set("B1", "=A1:A3");
|
||||||
|
model._set("B2", "4");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B1"), *"#SPILL!");
|
||||||
|
assert_eq!(model._get_text("B2"), *"4");
|
||||||
|
assert_eq!(model._get_text("B3"), *"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn second_evaluation() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("C3", "={1,2,3}");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("D3"), "2");
|
||||||
|
|
||||||
|
model._set("D8", "23");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("D3"), "2");
|
||||||
|
}
|
||||||
@@ -91,12 +91,12 @@ fn fn_or_xor() {
|
|||||||
model._set("A10", &format!("={func}(X99:Z99"));
|
model._set("A10", &format!("={func}(X99:Z99"));
|
||||||
|
|
||||||
// Reference to cell with reference to empty range
|
// Reference to cell with reference to empty range
|
||||||
model._set("B11", "=X99:Z99");
|
model._set("B11", "=@X99:Z99");
|
||||||
model._set("A11", &format!("={func}(B11)"));
|
model._set("A11", &format!("={func}(B11)"));
|
||||||
|
|
||||||
// Reference to cell with non-empty range
|
// Reference to cell with non-empty range
|
||||||
model._set("X12", "1");
|
model._set("X12", "1");
|
||||||
model._set("B12", "=X12:Z12");
|
model._set("B12", "=@X12:Z12");
|
||||||
model._set("A12", &format!("={func}(B12)"));
|
model._set("A12", &format!("={func}(B12)"));
|
||||||
|
|
||||||
// Reference to text cell
|
// Reference to text cell
|
||||||
@@ -174,7 +174,7 @@ fn fn_or_xor_no_arguments() {
|
|||||||
println!("Testing function: {func}");
|
println!("Testing function: {func}");
|
||||||
|
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", &format!("={}()", func));
|
model._set("A1", &format!("={func}()"));
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
}
|
}
|
||||||
|
|||||||
15
base/src/test/test_fn_round.rs
Normal file
15
base/src/test/test_fn_round.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fn_round_approximation() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=ROUND(1.05*(0.0284+0.0046)-0.0284,4)");
|
||||||
|
model._set("A2", "=ROUNDDOWN(1.05*(0.0284+0.0046)-0.0284,5)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.0063");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.00625");
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
use crate::test::util::new_empty_model;
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_colum() {
|
fn simple_column() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
// We populate cells A1 to A3
|
// We populate cells A1 to A3
|
||||||
model._set("A1", "1");
|
model._set("A1", "1");
|
||||||
@@ -30,7 +30,7 @@ fn return_of_array_is_n_impl() {
|
|||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string());
|
assert_eq!(model._get_text("C2"), "1".to_string());
|
||||||
assert_eq!(model._get_text("D2"), "1.89188842".to_string());
|
assert_eq!(model._get_text("D2"), "1.89188842".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
base/src/test/test_ln.rs
Normal file
17
base/src/test/test_ln.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=LN(100)");
|
||||||
|
model._set("A2", "=LN()");
|
||||||
|
model._set("A3", "=LN(100, 10)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"4.605170186");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
}
|
||||||
19
base/src/test/test_log.rs
Normal file
19
base/src/test/test_log.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=LOG(100)");
|
||||||
|
model._set("A2", "=LOG()");
|
||||||
|
model._set("A3", "=LOG(10000, 10)");
|
||||||
|
model._set("A4", "=LOG(100, 10, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"4");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
}
|
||||||
35
base/src/test/test_log10.rs
Normal file
35
base/src/test/test_log10.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=LOG10(100)");
|
||||||
|
model._set("A2", "=LOG10()");
|
||||||
|
model._set("A3", "=LOG10(100, 10)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_and_function() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=LOG10");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// This is the cell LOG10
|
||||||
|
assert_eq!(model._get_text("A1"), *"0");
|
||||||
|
|
||||||
|
model._set("LOG10", "1000");
|
||||||
|
model._set("A2", "=LOG10(LOG10)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"1000");
|
||||||
|
assert_eq!(model._get_text("A2"), *"3");
|
||||||
|
}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
mod test_add_delete_sheets;
|
mod test_add_delete_sheets;
|
||||||
mod test_autofill_columns;
|
mod test_autofill_columns;
|
||||||
mod test_autofill_rows;
|
mod test_autofill_rows;
|
||||||
|
mod test_batch_row_column_diff;
|
||||||
mod test_border;
|
mod test_border;
|
||||||
mod test_clear_cells;
|
mod test_clear_cells;
|
||||||
mod test_column_style;
|
mod test_column_style;
|
||||||
mod test_defined_names;
|
mod test_defined_names;
|
||||||
|
mod test_delete_evaluates;
|
||||||
mod test_delete_row_column_formatting;
|
mod test_delete_row_column_formatting;
|
||||||
mod test_diff_queue;
|
mod test_diff_queue;
|
||||||
|
mod test_dynamic_array;
|
||||||
mod test_evaluation;
|
mod test_evaluation;
|
||||||
mod test_general;
|
mod test_general;
|
||||||
mod test_grid_lines;
|
mod test_grid_lines;
|
||||||
mod test_keyboard_navigation;
|
mod test_keyboard_navigation;
|
||||||
|
mod test_last_empty_cell;
|
||||||
|
mod test_multi_row_column;
|
||||||
mod test_on_area_selection;
|
mod test_on_area_selection;
|
||||||
mod test_on_expand_selected_range;
|
mod test_on_expand_selected_range;
|
||||||
mod test_on_paste_styles;
|
mod test_on_paste_styles;
|
||||||
|
|||||||
675
base/src/test/user_model/test_batch_row_column_diff.rs
Normal file
675
base/src/test/user_model/test_batch_row_column_diff.rs
Normal file
@@ -0,0 +1,675 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
#![allow(clippy::panic)]
|
||||||
|
|
||||||
|
use bitcode::decode;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{LAST_COLUMN, LAST_ROW},
|
||||||
|
test::util::new_empty_model,
|
||||||
|
user_model::history::{Diff, QueueDiffs},
|
||||||
|
UserModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn last_diff_list(model: &mut UserModel) -> Vec<Diff> {
|
||||||
|
let bytes = model.flush_send_queue();
|
||||||
|
let queue: Vec<QueueDiffs> = decode(&bytes).unwrap();
|
||||||
|
// Get the last operation's diff list
|
||||||
|
queue.last().unwrap().list.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diff_invariant_insert_rows() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
assert!(model.insert_rows(0, 5, 3).is_ok());
|
||||||
|
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
&list[0],
|
||||||
|
Diff::InsertRows {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
count: 3
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diff_invariant_insert_columns() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
assert!(model.insert_columns(0, 2, 4).is_ok());
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
&list[0],
|
||||||
|
Diff::InsertColumns {
|
||||||
|
sheet: 0,
|
||||||
|
column: 2,
|
||||||
|
count: 4
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undo_redo_after_batch_delete() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Place values that will shift.
|
||||||
|
model.set_user_input(0, 20, 1, "A").unwrap();
|
||||||
|
model.set_user_input(0, 1, 20, "B").unwrap();
|
||||||
|
|
||||||
|
// Fill some of the rows we are about to delete for testing
|
||||||
|
for r in 10..15 {
|
||||||
|
model.set_user_input(0, r, 1, "tmp").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete rows 10..14 and columns 5..8
|
||||||
|
assert!(model.delete_rows(0, 10, 5).is_ok());
|
||||||
|
assert!(model.delete_columns(0, 5, 4).is_ok());
|
||||||
|
|
||||||
|
// Verify shift
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
|
||||||
|
|
||||||
|
// Undo
|
||||||
|
model.undo().unwrap(); // columns
|
||||||
|
model.undo().unwrap(); // rows
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "A");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 20).unwrap(), "B");
|
||||||
|
|
||||||
|
// Redo
|
||||||
|
model.redo().unwrap(); // rows
|
||||||
|
model.redo().unwrap(); // columns
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diff_order_delete_rows() {
|
||||||
|
// Verifies that delete diffs are generated with all data preserved
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Populate rows to delete
|
||||||
|
for r in 5..10 {
|
||||||
|
model.set_user_input(0, r, 1, &r.to_string()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(model.delete_rows(0, 5, 5).is_ok());
|
||||||
|
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
|
||||||
|
// Should have one bulk diff with all the row data
|
||||||
|
match &list[0] {
|
||||||
|
Diff::DeleteRows {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
count,
|
||||||
|
old_data,
|
||||||
|
} => {
|
||||||
|
assert_eq!(*sheet, 0);
|
||||||
|
assert_eq!(*row, 5);
|
||||||
|
assert_eq!(*count, 5);
|
||||||
|
assert_eq!(old_data.len(), 5);
|
||||||
|
// Verify the data was collected for each row
|
||||||
|
for (i, row_data) in old_data.iter().enumerate() {
|
||||||
|
let _expected_value = (5 + i).to_string();
|
||||||
|
assert!(row_data.data.contains_key(&1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected diff variant"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_operations_with_formulas() {
|
||||||
|
// Verifies formulas update correctly after batch ops
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
model.set_user_input(0, 1, 1, "10").unwrap();
|
||||||
|
model.set_user_input(0, 5, 1, "=A1*2").unwrap(); // Will become A3 after insert
|
||||||
|
|
||||||
|
assert!(model.insert_rows(0, 2, 2).is_ok());
|
||||||
|
|
||||||
|
// Formula should now reference A1 (unchanged) but be in row 7
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "20");
|
||||||
|
assert_eq!(model.get_cell_content(0, 7, 1).unwrap(), "=A1*2");
|
||||||
|
|
||||||
|
// Undo and verify formula is back at original position
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "20");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edge_case_single_operation() {
|
||||||
|
// Single row/column operations should still work correctly
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
assert!(model.insert_rows(0, 1, 1).is_ok());
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
|
||||||
|
assert!(model.insert_columns(0, 1, 1).is_ok());
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_empty_rows() {
|
||||||
|
// Delete multiple empty rows and verify behavior
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set data in rows 1 and 10, leaving rows 5-8 empty
|
||||||
|
model.set_user_input(0, 1, 1, "Before").unwrap();
|
||||||
|
model.set_user_input(0, 10, 1, "After").unwrap();
|
||||||
|
|
||||||
|
// Delete empty rows 5-8
|
||||||
|
assert!(model.delete_rows(0, 5, 4).is_ok());
|
||||||
|
|
||||||
|
// Verify shift
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
|
||||||
|
|
||||||
|
// Verify diffs now use bulk operation
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
match &list[0] {
|
||||||
|
Diff::DeleteRows {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
count,
|
||||||
|
old_data,
|
||||||
|
} => {
|
||||||
|
assert_eq!(*sheet, 0);
|
||||||
|
assert_eq!(*row, 5);
|
||||||
|
assert_eq!(*count, 4);
|
||||||
|
assert_eq!(old_data.len(), 4);
|
||||||
|
// All rows should be empty
|
||||||
|
for row_data in old_data {
|
||||||
|
assert!(row_data.data.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected diff variant"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo/redo
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_mixed_empty_and_filled_rows() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Alternating filled and empty rows
|
||||||
|
model.set_user_input(0, 5, 1, "Row5").unwrap();
|
||||||
|
model.set_user_input(0, 7, 1, "Row7").unwrap();
|
||||||
|
model.set_user_input(0, 9, 1, "Row9").unwrap();
|
||||||
|
model.set_user_input(0, 10, 1, "After").unwrap();
|
||||||
|
|
||||||
|
assert!(model.delete_rows(0, 5, 5).is_ok());
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "After");
|
||||||
|
|
||||||
|
// Verify mix of empty and filled row diffs
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
match &list[0] {
|
||||||
|
Diff::DeleteRows {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
count,
|
||||||
|
old_data,
|
||||||
|
} => {
|
||||||
|
assert_eq!(*sheet, 0);
|
||||||
|
assert_eq!(*row, 5);
|
||||||
|
assert_eq!(*count, 5);
|
||||||
|
assert_eq!(old_data.len(), 5);
|
||||||
|
|
||||||
|
// Count filled rows (should be 3: rows 5, 7, 9)
|
||||||
|
let filled_count = old_data
|
||||||
|
.iter()
|
||||||
|
.filter(|row_data| !row_data.data.is_empty())
|
||||||
|
.count();
|
||||||
|
assert_eq!(filled_count, 3);
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected diff variant"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "Row7");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 9, 1).unwrap(), "Row9");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_insert_rows_undo_redo() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set up initial data
|
||||||
|
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||||
|
model.set_user_input(0, 2, 1, "A2").unwrap();
|
||||||
|
model.set_user_input(0, 5, 1, "A5").unwrap();
|
||||||
|
|
||||||
|
// Insert 3 rows at position 3
|
||||||
|
assert!(model.insert_rows(0, 3, 3).is_ok());
|
||||||
|
|
||||||
|
// Verify data has shifted
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "A2");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // A5 moved to A8
|
||||||
|
|
||||||
|
// Check diff structure
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
&list[0],
|
||||||
|
Diff::InsertRows {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
count: 3
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Undo
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "A5"); // Back to original position
|
||||||
|
|
||||||
|
// Redo
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // Shifted again
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_insert_columns_undo_redo() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set up initial data
|
||||||
|
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||||
|
model.set_user_input(0, 1, 2, "B1").unwrap();
|
||||||
|
model.set_user_input(0, 1, 5, "E1").unwrap();
|
||||||
|
|
||||||
|
// Insert 3 columns at position 3
|
||||||
|
assert!(model.insert_columns(0, 3, 3).is_ok());
|
||||||
|
|
||||||
|
// Verify data has shifted
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "B1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // E1 moved to H1
|
||||||
|
|
||||||
|
// Check diff structure
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
&list[0],
|
||||||
|
Diff::InsertColumns {
|
||||||
|
sheet: 0,
|
||||||
|
column: 3,
|
||||||
|
count: 3
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Undo
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1"); // Back to original position
|
||||||
|
|
||||||
|
// Redo
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // Shifted again
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_delete_rows_round_trip() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set up data with styles
|
||||||
|
model.set_user_input(0, 3, 1, "Row3").unwrap();
|
||||||
|
model.set_user_input(0, 4, 1, "Row4").unwrap();
|
||||||
|
model.set_user_input(0, 5, 1, "Row5").unwrap();
|
||||||
|
model.set_user_input(0, 6, 1, "Row6").unwrap();
|
||||||
|
model.set_user_input(0, 7, 1, "After").unwrap();
|
||||||
|
|
||||||
|
// Set some row heights to verify they're preserved
|
||||||
|
model.set_rows_height(0, 4, 4, 30.0).unwrap();
|
||||||
|
model.set_rows_height(0, 5, 5, 40.0).unwrap();
|
||||||
|
|
||||||
|
// Delete rows 3-6
|
||||||
|
assert!(model.delete_rows(0, 3, 4).is_ok());
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
|
||||||
|
|
||||||
|
// Check diff structure
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
match &list[0] {
|
||||||
|
Diff::DeleteRows {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
count,
|
||||||
|
old_data,
|
||||||
|
} => {
|
||||||
|
assert_eq!(*sheet, 0);
|
||||||
|
assert_eq!(*row, 3);
|
||||||
|
assert_eq!(*count, 4);
|
||||||
|
assert_eq!(old_data.len(), 4);
|
||||||
|
// Verify data was preserved
|
||||||
|
assert!(old_data[0].data.contains_key(&1)); // Row3
|
||||||
|
assert!(old_data[1].data.contains_key(&1)); // Row4
|
||||||
|
assert!(old_data[2].data.contains_key(&1)); // Row5
|
||||||
|
assert!(old_data[3].data.contains_key(&1)); // Row6
|
||||||
|
}
|
||||||
|
_ => panic!("Expected DeleteRows diff"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo - should restore all data and row heights
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "After");
|
||||||
|
assert_eq!(model.get_row_height(0, 4).unwrap(), 30.0);
|
||||||
|
assert_eq!(model.get_row_height(0, 5).unwrap(), 40.0);
|
||||||
|
|
||||||
|
// Redo - should delete again
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
|
||||||
|
|
||||||
|
// Final undo to verify round-trip
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_delete_columns_round_trip() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set up data with styles
|
||||||
|
model.set_user_input(0, 1, 3, "C1").unwrap();
|
||||||
|
model.set_user_input(0, 1, 4, "D1").unwrap();
|
||||||
|
model.set_user_input(0, 1, 5, "E1").unwrap();
|
||||||
|
model.set_user_input(0, 1, 6, "F1").unwrap();
|
||||||
|
model.set_user_input(0, 1, 7, "After").unwrap();
|
||||||
|
|
||||||
|
// Set some column widths to verify they're preserved
|
||||||
|
model.set_columns_width(0, 4, 4, 100.0).unwrap();
|
||||||
|
model.set_columns_width(0, 5, 5, 120.0).unwrap();
|
||||||
|
|
||||||
|
// Delete columns 3-6
|
||||||
|
assert!(model.delete_columns(0, 3, 4).is_ok());
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
|
||||||
|
|
||||||
|
// Check diff structure
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
match &list[0] {
|
||||||
|
Diff::DeleteColumns {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
count,
|
||||||
|
old_data,
|
||||||
|
} => {
|
||||||
|
assert_eq!(*sheet, 0);
|
||||||
|
assert_eq!(*column, 3);
|
||||||
|
assert_eq!(*count, 4);
|
||||||
|
assert_eq!(old_data.len(), 4);
|
||||||
|
// Verify data was preserved
|
||||||
|
assert!(old_data[0].data.contains_key(&1)); // C1
|
||||||
|
assert!(old_data[1].data.contains_key(&1)); // D1
|
||||||
|
assert!(old_data[2].data.contains_key(&1)); // E1
|
||||||
|
assert!(old_data[3].data.contains_key(&1)); // F1
|
||||||
|
}
|
||||||
|
_ => panic!("Expected DeleteColumns diff"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo - should restore all data and column widths
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 7).unwrap(), "After");
|
||||||
|
assert_eq!(model.get_column_width(0, 4).unwrap(), 100.0);
|
||||||
|
assert_eq!(model.get_column_width(0, 5).unwrap(), 120.0);
|
||||||
|
|
||||||
|
// Redo - should delete again
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
|
||||||
|
|
||||||
|
// Final undo to verify round-trip
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_bulk_operations_sequence() {
|
||||||
|
// Test a complex sequence of bulk operations
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||||
|
model.set_user_input(0, 2, 2, "B2").unwrap();
|
||||||
|
model.set_user_input(0, 3, 3, "C3").unwrap();
|
||||||
|
|
||||||
|
// Operation 1: Insert 2 rows at position 2
|
||||||
|
model.insert_rows(0, 2, 2).unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2"); // B2 moved down
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3"); // C3 moved down
|
||||||
|
|
||||||
|
// Operation 2: Insert 2 columns at position 2
|
||||||
|
model.insert_columns(0, 2, 2).unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2"); // B2 moved right
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3"); // C3 moved right
|
||||||
|
|
||||||
|
// Operation 3: Delete the inserted rows
|
||||||
|
model.delete_rows(0, 2, 2).unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
|
||||||
|
|
||||||
|
// Undo all operations
|
||||||
|
model.undo().unwrap(); // Undo delete rows
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3");
|
||||||
|
|
||||||
|
model.undo().unwrap(); // Undo insert columns
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3");
|
||||||
|
|
||||||
|
model.undo().unwrap(); // Undo insert rows
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "B2");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 3).unwrap(), "C3");
|
||||||
|
|
||||||
|
// Redo all operations
|
||||||
|
model.redo().unwrap(); // Redo insert rows
|
||||||
|
model.redo().unwrap(); // Redo insert columns
|
||||||
|
model.redo().unwrap(); // Redo delete rows
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_operations_with_formulas_update() {
|
||||||
|
// Test that formulas update correctly with bulk operations
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set up data and formulas
|
||||||
|
model.set_user_input(0, 1, 1, "10").unwrap();
|
||||||
|
model.set_user_input(0, 5, 1, "20").unwrap();
|
||||||
|
model.set_user_input(0, 10, 1, "=A1+A5").unwrap(); // Formula referencing A1 and A5
|
||||||
|
|
||||||
|
// Insert 3 rows at position 3
|
||||||
|
model.insert_rows(0, 3, 3).unwrap();
|
||||||
|
|
||||||
|
// Formula should update to reference the shifted cells
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "30"); // Formula moved down
|
||||||
|
assert_eq!(model.get_cell_content(0, 13, 1).unwrap(), "=A1+A8"); // A5 became A8
|
||||||
|
|
||||||
|
// Undo
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "30");
|
||||||
|
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1+A5");
|
||||||
|
|
||||||
|
// Now test column insertion
|
||||||
|
model.set_user_input(0, 1, 5, "20").unwrap(); // Add value in E1
|
||||||
|
model.set_user_input(0, 1, 10, "=A1+E1").unwrap();
|
||||||
|
model.insert_columns(0, 3, 2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 12).unwrap(), "30"); // Formula moved right
|
||||||
|
assert_eq!(model.get_cell_content(0, 1, 12).unwrap(), "=A1+G1"); // E1 became G1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_delete_with_styles() {
|
||||||
|
// Test that cell and row/column styles are preserved
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Set up data with various styles
|
||||||
|
for r in 5..10 {
|
||||||
|
model.set_user_input(0, r, 1, &format!("Row{r}")).unwrap();
|
||||||
|
model.set_rows_height(0, r, r, (r * 10) as f64).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete and verify style preservation
|
||||||
|
model.delete_rows(0, 5, 5).unwrap();
|
||||||
|
|
||||||
|
// Undo should restore all styles
|
||||||
|
model.undo().unwrap();
|
||||||
|
for r in 5..10 {
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, r, 1).unwrap(),
|
||||||
|
format!("Row{r}")
|
||||||
|
);
|
||||||
|
assert_eq!(model.get_row_height(0, r).unwrap(), (r * 10) as f64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_operations_large_count() {
|
||||||
|
// Test operations with large counts
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Insert a large number of rows
|
||||||
|
model.set_user_input(0, 1, 1, "Before").unwrap();
|
||||||
|
model.set_user_input(0, 100, 1, "After").unwrap();
|
||||||
|
|
||||||
|
assert!(model.insert_rows(0, 50, 100).is_ok());
|
||||||
|
|
||||||
|
// Verify shift
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After"); // Moved by 100
|
||||||
|
|
||||||
|
// Check diff
|
||||||
|
let list = last_diff_list(&mut model);
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
&list[0],
|
||||||
|
Diff::InsertRows {
|
||||||
|
sheet: 0,
|
||||||
|
row: 50,
|
||||||
|
count: 100
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Undo and redo
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 100, 1).unwrap(), "After");
|
||||||
|
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_operations_error_cases() {
|
||||||
|
// Test error conditions
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Negative count should fail
|
||||||
|
assert!(model.insert_rows(0, 1, -5).is_err());
|
||||||
|
assert!(model.insert_columns(0, 1, -5).is_err());
|
||||||
|
assert!(model.delete_rows(0, 1, -5).is_err());
|
||||||
|
assert!(model.delete_columns(0, 1, -5).is_err());
|
||||||
|
|
||||||
|
// Zero count should fail
|
||||||
|
assert!(model.insert_rows(0, 1, 0).is_err());
|
||||||
|
assert!(model.insert_columns(0, 1, 0).is_err());
|
||||||
|
assert!(model.delete_rows(0, 1, 0).is_err());
|
||||||
|
assert!(model.delete_columns(0, 1, 0).is_err());
|
||||||
|
|
||||||
|
// Out of bounds operations should fail
|
||||||
|
assert!(model.delete_rows(0, LAST_ROW - 5, 10).is_err());
|
||||||
|
assert!(model.delete_columns(0, LAST_COLUMN - 5, 10).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_diff_serialization() {
|
||||||
|
// Test that bulk diffs can be serialized/deserialized correctly
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Create some data
|
||||||
|
model.set_user_input(0, 1, 1, "Test").unwrap();
|
||||||
|
model.insert_rows(0, 2, 3).unwrap();
|
||||||
|
|
||||||
|
// Flush and get the serialized diffs
|
||||||
|
let bytes = model.flush_send_queue();
|
||||||
|
|
||||||
|
// Create a new model and apply the diffs
|
||||||
|
let base2 = new_empty_model();
|
||||||
|
let mut model2 = UserModel::from_model(base2);
|
||||||
|
|
||||||
|
assert!(model2.apply_external_diffs(&bytes).is_ok());
|
||||||
|
|
||||||
|
// Verify the state matches
|
||||||
|
assert_eq!(model2.get_formatted_cell_value(0, 1, 1).unwrap(), "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boundary_validation() {
|
||||||
|
let base = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(base);
|
||||||
|
|
||||||
|
// Test deleting rows beyond valid range
|
||||||
|
assert!(model.delete_rows(0, LAST_ROW, 2).is_err());
|
||||||
|
assert!(model.delete_rows(0, LAST_ROW + 1, 1).is_err());
|
||||||
|
|
||||||
|
// Test deleting columns beyond valid range
|
||||||
|
assert!(model.delete_columns(0, LAST_COLUMN, 2).is_err());
|
||||||
|
assert!(model.delete_columns(0, LAST_COLUMN + 1, 1).is_err());
|
||||||
|
|
||||||
|
// Test valid boundary deletions (should work with our empty row fix)
|
||||||
|
assert!(model.delete_rows(0, LAST_ROW, 1).is_ok());
|
||||||
|
assert!(model.delete_columns(0, LAST_COLUMN, 1).is_ok());
|
||||||
|
}
|
||||||
@@ -50,10 +50,7 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(top_border),
|
Some(top_border),
|
||||||
top_cell_style.border.bottom,
|
top_cell_style.border.bottom,
|
||||||
"(Top). Sheet: {}, row: {}, column: {}",
|
"(Top). Sheet: {sheet}, row: {row}, column: {column}"
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,10 +62,7 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(right_border),
|
Some(right_border),
|
||||||
right_cell_style.border.left,
|
right_cell_style.border.left,
|
||||||
"(Right). Sheet: {}, row: {}, column: {}",
|
"(Right). Sheet: {sheet}, row: {row}, column: {column}"
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,10 +74,7 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(bottom_border),
|
Some(bottom_border),
|
||||||
bottom_cell_style.border.top,
|
bottom_cell_style.border.top,
|
||||||
"(Bottom). Sheet: {}, row: {}, column: {}",
|
"(Bottom). Sheet: {sheet}, row: {row}, column: {column}"
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,10 +85,7 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(left_border),
|
Some(left_border),
|
||||||
left_cell_style.border.right,
|
left_cell_style.border.right,
|
||||||
"(Left). Sheet: {}, row: {}, column: {}",
|
"(Left). Sheet: {sheet}, row: {row}, column: {column}"
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
base/src/test/user_model/test_delete_evaluates.rs
Normal file
47
base/src/test/user_model/test_delete_evaluates.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{expressions::types::Area, UserModel};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_cell_contents_evaluates() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||||
|
model.set_user_input(0, 1, 2, "=A1").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("42".to_string())
|
||||||
|
);
|
||||||
|
model
|
||||||
|
.range_clear_contents(&Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("0".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_cell_all_evaluates() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||||
|
model.set_user_input(0, 1, 2, "=A1").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("42".to_string())
|
||||||
|
);
|
||||||
|
model
|
||||||
|
.range_clear_all(&Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("0".to_string()));
|
||||||
|
}
|
||||||
@@ -65,8 +65,8 @@ fn queue_undo_redo_multiple() {
|
|||||||
model1.set_user_input(0, row, 17, "=ROW()").unwrap();
|
model1.set_user_input(0, row, 17, "=ROW()").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
model1.insert_row(0, 3).unwrap();
|
model1.insert_rows(0, 3, 1).unwrap();
|
||||||
model1.insert_row(0, 3).unwrap();
|
model1.insert_rows(0, 3, 1).unwrap();
|
||||||
|
|
||||||
// undo al of them
|
// undo al of them
|
||||||
while model1.can_undo() {
|
while model1.can_undo() {
|
||||||
|
|||||||
130
base/src/test/user_model/test_dynamic_array.rs
Normal file
130
base/src/test/user_model/test_dynamic_array.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{expressions::types::Area, UserModel};
|
||||||
|
|
||||||
|
// Tests basic behavour.
|
||||||
|
#[test]
|
||||||
|
fn basic() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
// We put a value by the dynamic array to check the border conditions
|
||||||
|
model.set_user_input(0, 2, 1, "22").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "={34,35,3}").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 1),
|
||||||
|
Ok("34".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that overwriting a dynamic array with a single value dissolves the array
|
||||||
|
#[test]
|
||||||
|
fn sett_user_input_mother() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "={34,35,3}").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("35".to_string())
|
||||||
|
);
|
||||||
|
model.set_user_input(0, 1, 1, "123").unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_user_input_sibling() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "={43,55,34}").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("55".to_string())
|
||||||
|
);
|
||||||
|
// This does nothing
|
||||||
|
model.set_user_input(0, 1, 2, "123").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("55".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_undo_redo() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "={34,35,3}").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("35".to_string())
|
||||||
|
);
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("".to_string()));
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 1, 2),
|
||||||
|
Ok("35".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_spills() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
// D9 => ={1,2,3}
|
||||||
|
model.set_user_input(0, 9, 4, "={34,35,3}").unwrap();
|
||||||
|
// F6 => ={1;2;3;4}
|
||||||
|
model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap();
|
||||||
|
|
||||||
|
// F6 should be #SPILL!
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 6, 6),
|
||||||
|
Ok("#SPILL!".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// We delete D9
|
||||||
|
model
|
||||||
|
.range_clear_contents(&Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 9,
|
||||||
|
column: 4,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// F6 should be 1
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 6, 6), Ok("1".to_string()));
|
||||||
|
|
||||||
|
// Now we undo that
|
||||||
|
model.undo().unwrap();
|
||||||
|
// F6 should be #SPILL!
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 6, 6),
|
||||||
|
Ok("#SPILL!".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spill_order_d9_f6() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
// D9 => ={1,2,3}
|
||||||
|
model.set_user_input(0, 9, 4, "={34,35,3}").unwrap();
|
||||||
|
// F6 => ={1;2;3;4}
|
||||||
|
model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap();
|
||||||
|
|
||||||
|
// F6 should be #SPILL!
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 6, 6),
|
||||||
|
Ok("#SPILL!".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spill_order_f6_d9() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
// F6 => ={1;2;3;4}
|
||||||
|
model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap();
|
||||||
|
// D9 => ={1,2,3}
|
||||||
|
model.set_user_input(0, 9, 4, "={34,35,3}").unwrap();
|
||||||
|
|
||||||
|
// D9 should be #SPILL!
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(0, 9, 4),
|
||||||
|
Ok("#SPILL!".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ fn set_user_input_errors() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn user_model_debug_message() {
|
fn user_model_debug_message() {
|
||||||
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
let s = &format!("{:?}", model);
|
let s = &format!("{model:?}");
|
||||||
assert_eq!(s, "UserModel");
|
assert_eq!(s, "UserModel");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ fn insert_remove_rows() {
|
|||||||
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
|
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
|
||||||
|
|
||||||
// remove the row
|
// remove the row
|
||||||
assert!(model.delete_row(0, 5).is_ok());
|
assert!(model.delete_rows(0, 5, 1).is_ok());
|
||||||
// Row 5 has now the normal height
|
// Row 5 has now the normal height
|
||||||
assert_eq!(model.get_row_height(0, 5), Ok(height));
|
assert_eq!(model.get_row_height(0, 5), Ok(height));
|
||||||
// There is no value in A5
|
// There is no value in A5
|
||||||
@@ -99,7 +99,7 @@ fn insert_remove_columns() {
|
|||||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
||||||
|
|
||||||
// remove the column
|
// remove the column
|
||||||
assert!(model.delete_column(0, 5).is_ok());
|
assert!(model.delete_columns(0, 5, 1).is_ok());
|
||||||
// Column 5 has now the normal width
|
// Column 5 has now the normal width
|
||||||
assert_eq!(model.get_column_width(0, 5), Ok(column_width));
|
assert_eq!(model.get_column_width(0, 5), Ok(column_width));
|
||||||
// There is no value in E5
|
// There is no value in E5
|
||||||
|
|||||||
55
base/src/test/user_model/test_last_empty_cell.rs
Normal file
55
base/src/test/user_model/test_last_empty_cell.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::constants::LAST_ROW;
|
||||||
|
use crate::expressions::types::Area;
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
use crate::UserModel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_tests() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
// This is the first row, column 5
|
||||||
|
model.set_user_input(0, 3, 5, "todo").unwrap();
|
||||||
|
|
||||||
|
// Row 3 before column 5 should be empty
|
||||||
|
assert_eq!(
|
||||||
|
model
|
||||||
|
.get_last_non_empty_in_row_before_column(0, 3, 4)
|
||||||
|
.unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
// Row 3 before column 5 should be 5
|
||||||
|
assert_eq!(
|
||||||
|
model
|
||||||
|
.get_last_non_empty_in_row_before_column(0, 3, 7)
|
||||||
|
.unwrap(),
|
||||||
|
Some(5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_last_empty_cell() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Column 7 has a style but it is empty
|
||||||
|
assert_eq!(
|
||||||
|
model
|
||||||
|
.get_last_non_empty_in_row_before_column(0, 3, 14)
|
||||||
|
.unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
173
base/src/test/user_model/test_multi_row_column.rs
Normal file
173
base/src/test/user_model/test_multi_row_column.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{LAST_COLUMN, LAST_ROW},
|
||||||
|
test::util::new_empty_model,
|
||||||
|
UserModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_multiple_rows_shifts_cells() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
// Place a value below the insertion point.
|
||||||
|
model.set_user_input(0, 10, 1, "42").unwrap();
|
||||||
|
|
||||||
|
// Insert 3 rows starting at row 5.
|
||||||
|
assert!(model.insert_rows(0, 5, 3).is_ok());
|
||||||
|
|
||||||
|
// The original value should have moved down by 3 rows.
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
|
||||||
|
|
||||||
|
// Undo / redo cycle should restore the same behaviour.
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "42");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_rows_errors() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Negative or zero counts are rejected.
|
||||||
|
assert_eq!(
|
||||||
|
model.insert_rows(0, 1, -2),
|
||||||
|
Err("Cannot add a negative number of cells :)".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.insert_rows(0, 1, 0),
|
||||||
|
Err("Cannot add a negative number of cells :)".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inserting too many rows so that the sheet would overflow.
|
||||||
|
let too_many = LAST_ROW; // This guarantees max_row + too_many > LAST_ROW.
|
||||||
|
assert_eq!(
|
||||||
|
model.insert_rows(0, 1, too_many),
|
||||||
|
Err(
|
||||||
|
"Cannot shift cells because that would delete cells at the end of a column".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_multiple_columns_shifts_cells() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
// Place a value to the right of the insertion point.
|
||||||
|
model.set_user_input(0, 1, 10, "99").unwrap();
|
||||||
|
|
||||||
|
// Insert 3 columns starting at column 5.
|
||||||
|
assert!(model.insert_columns(0, 5, 3).is_ok());
|
||||||
|
|
||||||
|
// The original value should have moved right by 3 columns.
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
|
||||||
|
|
||||||
|
// Undo / redo cycle.
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 10).unwrap(), "99");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_columns_errors() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Negative or zero counts are rejected.
|
||||||
|
assert_eq!(
|
||||||
|
model.insert_columns(0, 1, -2),
|
||||||
|
Err("Cannot add a negative number of cells :)".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.insert_columns(0, 1, 0),
|
||||||
|
Err("Cannot add a negative number of cells :)".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Overflow to the right.
|
||||||
|
let too_many = LAST_COLUMN; // Ensures max_column + too_many > LAST_COLUMN
|
||||||
|
assert_eq!(
|
||||||
|
model.insert_columns(0, 1, too_many),
|
||||||
|
Err("Cannot shift cells because that would delete cells at the end of a row".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_multiple_rows_shifts_cells_upwards() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Populate rows 10..14 (to be deleted) so that the diff builder does not fail.
|
||||||
|
for r in 10..15 {
|
||||||
|
model.set_user_input(0, r, 1, "del").unwrap();
|
||||||
|
}
|
||||||
|
// Place a value below the deletion range.
|
||||||
|
model.set_user_input(0, 20, 1, "keep").unwrap();
|
||||||
|
|
||||||
|
// Delete 5 rows starting at row 10.
|
||||||
|
assert!(model.delete_rows(0, 10, 5).is_ok());
|
||||||
|
|
||||||
|
// The value originally at row 20 should now be at row 15.
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
|
||||||
|
|
||||||
|
// Undo / redo cycle.
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "keep");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_rows_errors() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Negative or zero counts are rejected.
|
||||||
|
assert_eq!(
|
||||||
|
model.delete_rows(0, 1, -3),
|
||||||
|
Err("Please use insert rows instead".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.delete_rows(0, 1, 0),
|
||||||
|
Err("Please use insert rows instead".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_multiple_columns_shifts_cells_left() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Place a value to the right of the deletion range.
|
||||||
|
model.set_user_input(0, 1, 15, "88").unwrap();
|
||||||
|
|
||||||
|
// Delete 4 columns starting at column 5.
|
||||||
|
assert!(model.delete_columns(0, 5, 4).is_ok());
|
||||||
|
|
||||||
|
// The value originally at column 15 should now be at column 11.
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
|
||||||
|
|
||||||
|
// Undo / redo cycle.
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 15).unwrap(), "88");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_columns_errors() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Negative or zero counts are rejected.
|
||||||
|
assert_eq!(
|
||||||
|
model.delete_columns(0, 1, -4),
|
||||||
|
Err("Please use insert columns instead".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.delete_columns(0, 1, 0),
|
||||||
|
Err("Please use insert columns instead".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ fn csv_paste() {
|
|||||||
model.get_formatted_cell_value(0, 7, 7),
|
model.get_formatted_cell_value(0, 7, 7),
|
||||||
Ok("21".to_string())
|
Ok("21".to_string())
|
||||||
);
|
);
|
||||||
|
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -45,6 +46,7 @@ fn csv_paste_formula() {
|
|||||||
model.get_formatted_cell_value(0, 1, 1),
|
model.get_formatted_cell_value(0, 1, 1),
|
||||||
Ok("2022".to_string())
|
Ok("2022".to_string())
|
||||||
);
|
);
|
||||||
|
assert_eq!([1, 1, 1, 1], model.get_selected_view().range);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -69,6 +71,7 @@ fn tsv_crlf_paste() {
|
|||||||
model.get_formatted_cell_value(0, 7, 7),
|
model.get_formatted_cell_value(0, 7, 7),
|
||||||
Ok("21".to_string())
|
Ok("21".to_string())
|
||||||
);
|
);
|
||||||
|
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -164,7 +167,7 @@ fn copy_paste_internal() {
|
|||||||
let copy = model.copy_to_clipboard().unwrap();
|
let copy = model.copy_to_clipboard().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
copy.csv,
|
copy.csv,
|
||||||
"42\t127\n\"A season of faith\t \"\"perfection\"\"\"\t\n"
|
"42\t127\n\"A season of faith\t \"\"perfection\"\"\""
|
||||||
);
|
);
|
||||||
assert_eq!(copy.range, (1, 1, 2, 2));
|
assert_eq!(copy.range, (1, 1, 2, 2));
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ fn simple_insert_row() {
|
|||||||
for row in 1..5 {
|
for row in 1..5 {
|
||||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
||||||
}
|
}
|
||||||
assert!(model.insert_row(sheet, 3).is_ok());
|
assert!(model.insert_rows(sheet, 3, 1).is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
||||||
""
|
""
|
||||||
@@ -40,7 +40,7 @@ fn simple_insert_column() {
|
|||||||
for column in 1..5 {
|
for column in 1..5 {
|
||||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
||||||
}
|
}
|
||||||
assert!(model.insert_column(sheet, 3).is_ok());
|
assert!(model.insert_columns(sheet, 3, 1).is_ok());
|
||||||
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
||||||
|
|
||||||
assert!(model.undo().is_ok());
|
assert!(model.undo().is_ok());
|
||||||
@@ -62,7 +62,7 @@ fn simple_delete_column() {
|
|||||||
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
model.delete_column(0, 5).unwrap();
|
model.delete_columns(0, 5, 1).unwrap();
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
|
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
|
||||||
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
|
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
|
||||||
@@ -92,20 +92,20 @@ fn delete_column_errors() {
|
|||||||
let model = new_empty_model();
|
let model = new_empty_model();
|
||||||
let mut model = UserModel::from_model(model);
|
let mut model = UserModel::from_model(model);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.delete_column(1, 1),
|
model.delete_columns(1, 1, 1),
|
||||||
Err("Invalid sheet index".to_string())
|
Err("Invalid sheet index".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.delete_column(0, 0),
|
model.delete_columns(0, 0, 1),
|
||||||
Err("Column number '0' is not valid.".to_string())
|
Err("Column number '0' is not valid.".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.delete_column(0, LAST_COLUMN + 1),
|
model.delete_columns(0, LAST_COLUMN + 1, 1),
|
||||||
Err("Column number '16385' is not valid.".to_string())
|
Err(format!("Column number '{}' is not valid.", LAST_COLUMN + 1))
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(()));
|
assert_eq!(model.delete_columns(0, LAST_COLUMN, 1), Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -119,7 +119,7 @@ fn simple_delete_row() {
|
|||||||
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
model.delete_row(0, 15).unwrap();
|
model.delete_rows(0, 15, 1).unwrap();
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
||||||
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
|
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
|
||||||
@@ -150,7 +150,7 @@ fn simple_delete_row_no_style() {
|
|||||||
let mut model = UserModel::from_model(model);
|
let mut model = UserModel::from_model(model);
|
||||||
model.set_user_input(0, 15, 4, "3").unwrap();
|
model.set_user_input(0, 15, 4, "3").unwrap();
|
||||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||||
model.delete_row(0, 15).unwrap();
|
model.delete_rows(0, 15, 1).unwrap();
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
||||||
}
|
}
|
||||||
@@ -180,14 +180,14 @@ fn insert_row_evaluates() {
|
|||||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||||
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
|
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
|
||||||
|
|
||||||
assert!(model.insert_row(0, 1).is_ok());
|
assert!(model.insert_rows(0, 1, 1).is_ok());
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||||
model.undo().unwrap();
|
model.undo().unwrap();
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||||
model.redo().unwrap();
|
model.redo().unwrap();
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||||
|
|
||||||
model.delete_row(0, 1).unwrap();
|
model.delete_rows(0, 1, 1).unwrap();
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||||
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
|
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ fn insert_column_evaluates() {
|
|||||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||||
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
|
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
|
||||||
|
|
||||||
assert!(model.insert_column(0, 1).is_ok());
|
assert!(model.insert_columns(0, 1, 1).is_ok());
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||||
|
|
||||||
model.undo().unwrap();
|
model.undo().unwrap();
|
||||||
@@ -207,7 +207,7 @@ fn insert_column_evaluates() {
|
|||||||
model.redo().unwrap();
|
model.redo().unwrap();
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||||
|
|
||||||
model.delete_column(0, 1).unwrap();
|
model.delete_columns(0, 1, 1).unwrap();
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||||
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
|
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ fn set_the_range_does_not_set_the_cell() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.set_selected_range(5, 4, 10, 6),
|
model.set_selected_range(5, 4, 10, 6),
|
||||||
Err(
|
Err(
|
||||||
"The selected cells is not in one of the corners. Row: '1' and row range '(5, 10)'"
|
"The selected cell is not in one of the corners. Row: '1' and row range '(5, 10)'"
|
||||||
.to_string()
|
.to_string()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ impl UserModel {
|
|||||||
r##"{{
|
r##"{{
|
||||||
"item": {{
|
"item": {{
|
||||||
"style": "thin",
|
"style": "thin",
|
||||||
"color": "{}"
|
"color": "{color}"
|
||||||
}},
|
}},
|
||||||
"type": "All"
|
"type": "All"
|
||||||
}}"##,
|
}}"##
|
||||||
color
|
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let range = &Area {
|
let range = &Area {
|
||||||
@@ -40,11 +39,10 @@ impl UserModel {
|
|||||||
r##"{{
|
r##"{{
|
||||||
"item": {{
|
"item": {{
|
||||||
"style": "thin",
|
"style": "thin",
|
||||||
"color": "{}"
|
"color": "{color}"
|
||||||
}},
|
}},
|
||||||
"type": "{}"
|
"type": "{kind}"
|
||||||
}}"##,
|
}}"##
|
||||||
color, kind
|
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let range = &Area {
|
let range = &Area {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl Model {
|
|||||||
if cell.contains('!') {
|
if cell.contains('!') {
|
||||||
self.parse_reference(cell).unwrap()
|
self.parse_reference(cell).unwrap()
|
||||||
} else {
|
} else {
|
||||||
self.parse_reference(&format!("Sheet1!{}", cell)).unwrap()
|
self.parse_reference(&format!("Sheet1!{cell}")).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn _set(&mut self, cell: &str, value: &str) {
|
pub fn _set(&mut self, cell: &str, value: &str) {
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ pub struct Workbook {
|
|||||||
pub metadata: Metadata,
|
pub metadata: Metadata,
|
||||||
pub tables: HashMap<String, Table>,
|
pub tables: HashMap<String, Table>,
|
||||||
pub views: HashMap<u32, WorkbookView>,
|
pub views: HashMap<u32, WorkbookView>,
|
||||||
|
/// The list of cells that spill in the order of evaluation
|
||||||
|
pub spill_cells: Vec<(u32, i32, i32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -159,17 +161,17 @@ pub enum CellType {
|
|||||||
CompoundData = 128,
|
CompoundData = 128,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cell types
|
||||||
|
/// s is always the style index of the cell
|
||||||
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
|
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
|
||||||
pub enum Cell {
|
pub enum Cell {
|
||||||
EmptyCell {
|
EmptyCell {
|
||||||
s: i32,
|
s: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
BooleanCell {
|
BooleanCell {
|
||||||
v: bool,
|
v: bool,
|
||||||
s: i32,
|
s: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
NumberCell {
|
NumberCell {
|
||||||
v: f64,
|
v: f64,
|
||||||
s: i32,
|
s: i32,
|
||||||
@@ -181,6 +183,7 @@ pub enum Cell {
|
|||||||
},
|
},
|
||||||
// Always a shared string
|
// Always a shared string
|
||||||
SharedString {
|
SharedString {
|
||||||
|
// string index
|
||||||
si: i32,
|
si: i32,
|
||||||
s: i32,
|
s: i32,
|
||||||
},
|
},
|
||||||
@@ -189,13 +192,11 @@ pub enum Cell {
|
|||||||
f: i32,
|
f: i32,
|
||||||
s: i32,
|
s: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
CellFormulaBoolean {
|
CellFormulaBoolean {
|
||||||
f: i32,
|
f: i32,
|
||||||
v: bool,
|
v: bool,
|
||||||
s: i32,
|
s: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
CellFormulaNumber {
|
CellFormulaNumber {
|
||||||
f: i32,
|
f: i32,
|
||||||
v: f64,
|
v: f64,
|
||||||
@@ -207,9 +208,9 @@ pub enum Cell {
|
|||||||
v: String,
|
v: String,
|
||||||
s: i32,
|
s: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
CellFormulaError {
|
CellFormulaError {
|
||||||
f: i32,
|
f: i32,
|
||||||
|
// error index
|
||||||
ei: Error,
|
ei: Error,
|
||||||
s: i32,
|
s: i32,
|
||||||
// Origin: Sheet3!C4
|
// Origin: Sheet3!C4
|
||||||
@@ -217,7 +218,81 @@ pub enum Cell {
|
|||||||
// Error Message: "Not implemented function"
|
// Error Message: "Not implemented function"
|
||||||
m: String,
|
m: String,
|
||||||
},
|
},
|
||||||
// TODO: Array formulas
|
// All Spill/dynamic cells have a boolean, a for array, if true it is an array formula
|
||||||
|
// Spill cells point to a mother cell (row, column)
|
||||||
|
SpillNumberCell {
|
||||||
|
v: f64,
|
||||||
|
s: i32,
|
||||||
|
// mother cell (row, column)
|
||||||
|
m: (i32, i32),
|
||||||
|
},
|
||||||
|
SpillBooleanCell {
|
||||||
|
v: bool,
|
||||||
|
s: i32,
|
||||||
|
// mother cell (row, column)
|
||||||
|
m: (i32, i32),
|
||||||
|
},
|
||||||
|
SpillErrorCell {
|
||||||
|
ei: Error,
|
||||||
|
s: i32,
|
||||||
|
// mother cell (row, column)
|
||||||
|
m: (i32, i32),
|
||||||
|
},
|
||||||
|
SpillStringCell {
|
||||||
|
v: String,
|
||||||
|
s: i32,
|
||||||
|
// mother cell (row, column)
|
||||||
|
m: (i32, i32),
|
||||||
|
},
|
||||||
|
// Dynamic cell formulas have a range (width, height)
|
||||||
|
DynamicCellFormula {
|
||||||
|
f: i32,
|
||||||
|
s: i32,
|
||||||
|
// range of the formula (width, height)
|
||||||
|
r: (i32, i32),
|
||||||
|
// true if the formula is a CSE formula
|
||||||
|
a: bool,
|
||||||
|
},
|
||||||
|
DynamicCellFormulaBoolean {
|
||||||
|
f: i32,
|
||||||
|
v: bool,
|
||||||
|
s: i32,
|
||||||
|
// range of the formula (width, height)
|
||||||
|
r: (i32, i32),
|
||||||
|
// true if the formula is a CSE formula
|
||||||
|
a: bool,
|
||||||
|
},
|
||||||
|
DynamicCellFormulaNumber {
|
||||||
|
f: i32,
|
||||||
|
v: f64,
|
||||||
|
s: i32,
|
||||||
|
// range of the formula (width, height)
|
||||||
|
r: (i32, i32),
|
||||||
|
// true if the formula is a CSE formula
|
||||||
|
a: bool,
|
||||||
|
},
|
||||||
|
DynamicCellFormulaString {
|
||||||
|
f: i32,
|
||||||
|
v: String,
|
||||||
|
s: i32,
|
||||||
|
// range of the formula (width, height)
|
||||||
|
r: (i32, i32),
|
||||||
|
// true if the formula is a CSE formula
|
||||||
|
a: bool,
|
||||||
|
},
|
||||||
|
DynamicCellFormulaError {
|
||||||
|
f: i32,
|
||||||
|
ei: Error,
|
||||||
|
s: i32,
|
||||||
|
// Cell origin of the error
|
||||||
|
o: String,
|
||||||
|
// Error message in text
|
||||||
|
m: String,
|
||||||
|
// range of the formula (width, height)
|
||||||
|
r: (i32, i32),
|
||||||
|
// true if the formula is a CSE formula
|
||||||
|
a: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Cell {
|
impl Default for Cell {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
model::Model,
|
model::Model,
|
||||||
types::{
|
types::{
|
||||||
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
|
Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties,
|
||||||
Style, VerticalAlignment,
|
SheetState, Style, VerticalAlignment,
|
||||||
},
|
},
|
||||||
utils::is_valid_hex_color,
|
utils::is_valid_hex_color,
|
||||||
};
|
};
|
||||||
@@ -24,6 +24,18 @@ use crate::user_model::history::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::border_utils::is_max_border;
|
use super::border_utils::is_max_border;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum CellArrayStructure {
|
||||||
|
// It s just a single cell
|
||||||
|
SingleCell,
|
||||||
|
// It is part o a dynamic array
|
||||||
|
// (mother_row, mother_column, width, height)
|
||||||
|
DynamicChild(i32, i32, i32, i32),
|
||||||
|
// Mother of a dynamic array (width, height)
|
||||||
|
DynamicMother(i32, i32),
|
||||||
|
}
|
||||||
|
|
||||||
/// Data for the clipboard
|
/// Data for the clipboard
|
||||||
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
|
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
|
||||||
|
|
||||||
@@ -627,6 +639,7 @@ impl UserModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
|
self.evaluate_if_not_paused();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,11 +669,16 @@ impl UserModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
|
self.evaluate_if_not_paused();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_column_formatting(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
fn clear_column_formatting(
|
||||||
let mut diff_list = Vec::new();
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
diff_list: &mut Vec<Diff>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let old_value = self.model.get_column_style(sheet, column)?;
|
let old_value = self.model.get_column_style(sheet, column)?;
|
||||||
self.model.delete_column_style(sheet, column)?;
|
self.model.delete_column_style(sheet, column)?;
|
||||||
diff_list.push(Diff::DeleteColumnStyle {
|
diff_list.push(Diff::DeleteColumnStyle {
|
||||||
@@ -739,12 +757,15 @@ impl UserModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_row_formatting(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
fn clear_row_formatting(
|
||||||
let mut diff_list = Vec::new();
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
diff_list: &mut Vec<Diff>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let old_value = self.model.get_row_style(sheet, row)?;
|
let old_value = self.model.get_row_style(sheet, row)?;
|
||||||
self.model.delete_row_style(sheet, row)?;
|
self.model.delete_row_style(sheet, row)?;
|
||||||
diff_list.push(Diff::DeleteRowStyle {
|
diff_list.push(Diff::DeleteRowStyle {
|
||||||
@@ -791,8 +812,6 @@ impl UserModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,19 +822,21 @@ impl UserModel {
|
|||||||
/// * [UserModel::range_clear_contents]
|
/// * [UserModel::range_clear_contents]
|
||||||
pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
|
pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
|
||||||
let sheet = range.sheet;
|
let sheet = range.sheet;
|
||||||
|
let mut diff_list = Vec::new();
|
||||||
if range.row == 1 && range.height == LAST_ROW {
|
if range.row == 1 && range.height == LAST_ROW {
|
||||||
for column in range.column..range.column + range.width {
|
for column in range.column..range.column + range.width {
|
||||||
self.clear_column_formatting(sheet, column)?;
|
self.clear_column_formatting(sheet, column, &mut diff_list)?;
|
||||||
}
|
}
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if range.column == 1 && range.width == LAST_COLUMN {
|
if range.column == 1 && range.width == LAST_COLUMN {
|
||||||
for row in range.row..range.row + range.height {
|
for row in range.row..range.row + range.height {
|
||||||
self.clear_row_formatting(sheet, row)?;
|
self.clear_row_formatting(sheet, row, &mut diff_list)?;
|
||||||
}
|
}
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut diff_list = Vec::new();
|
|
||||||
for row in range.row..range.row + range.height {
|
for row in range.row..range.row + range.height {
|
||||||
for column in range.column..range.column + range.width {
|
for column in range.column..range.column + range.width {
|
||||||
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
|
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
|
||||||
@@ -851,105 +872,184 @@ impl UserModel {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a row
|
/// Inserts `row_count` blank rows starting at `row` (both 0-based).
|
||||||
///
|
///
|
||||||
/// See also:
|
/// Parameters
|
||||||
/// * [Model::insert_rows]
|
/// * `sheet` – worksheet index.
|
||||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
/// * `row` – first row to insert.
|
||||||
let diff_list = vec![Diff::InsertRow { sheet, row }];
|
/// * `row_count` – number of rows (> 0).
|
||||||
|
///
|
||||||
|
/// History: the method pushes `row_count` [`crate::user_model::history::Diff::InsertRow`]
|
||||||
|
/// items **all using the same `row` index**. Replaying those diffs (undo / redo)
|
||||||
|
/// is therefore immune to the row-shifts that happen after each individual
|
||||||
|
/// insertion.
|
||||||
|
///
|
||||||
|
/// See also [`Model::insert_rows`].
|
||||||
|
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||||||
|
self.model.insert_rows(sheet, row, row_count)?;
|
||||||
|
|
||||||
|
let diff_list = vec![Diff::InsertRows {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
count: row_count,
|
||||||
|
}];
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
self.model.insert_rows(sheet, row, 1)?;
|
|
||||||
self.evaluate_if_not_paused();
|
self.evaluate_if_not_paused();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a row
|
/// Inserts `column_count` blank columns starting at `column` (0-based).
|
||||||
///
|
///
|
||||||
/// See also:
|
/// Parameters
|
||||||
/// * [Model::delete_rows]
|
/// * `sheet` – worksheet index.
|
||||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
/// * `column` – first column to insert.
|
||||||
let mut row_data = None;
|
/// * `column_count` – number of columns (> 0).
|
||||||
|
///
|
||||||
|
/// History: pushes one [`crate::user_model::history::Diff::InsertColumn`]
|
||||||
|
/// per inserted column, all with the same `column` value, preventing index
|
||||||
|
/// drift when the diffs are reapplied.
|
||||||
|
///
|
||||||
|
/// See also [`Model::insert_columns`].
|
||||||
|
pub fn insert_columns(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
column_count: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
self.model.insert_columns(sheet, column, column_count)?;
|
||||||
|
|
||||||
|
let diff_list = vec![Diff::InsertColumns {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
count: column_count,
|
||||||
|
}];
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
|
self.evaluate_if_not_paused();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes `row_count` rows starting at `row`.
|
||||||
|
///
|
||||||
|
/// History: a [`crate::user_model::history::Diff::DeleteRow`] is created for
|
||||||
|
/// each row, ordered **bottom → top**. Undo therefore recreates rows from
|
||||||
|
/// top → bottom and redo removes them bottom → top, avoiding index drift.
|
||||||
|
///
|
||||||
|
/// See also [`Model::delete_rows`].
|
||||||
|
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||||
for rd in &worksheet.rows {
|
let mut old_data = Vec::new();
|
||||||
if rd.r == row {
|
// Collect data for all rows to be deleted
|
||||||
row_data = Some(rd.clone());
|
for r in row..row + row_count {
|
||||||
break;
|
let mut row_data = None;
|
||||||
|
for rd in &worksheet.rows {
|
||||||
|
if rd.r == r {
|
||||||
|
row_data = Some(rd.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let data = match worksheet.sheet_data.get(&r) {
|
||||||
|
Some(s) => s.clone(),
|
||||||
|
None => HashMap::new(),
|
||||||
|
};
|
||||||
|
old_data.push(RowData {
|
||||||
|
row: row_data,
|
||||||
|
data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let data = match worksheet.sheet_data.get(&row) {
|
|
||||||
Some(s) => s.clone(),
|
self.model.delete_rows(sheet, row, row_count)?;
|
||||||
None => return Err(format!("Row number '{row}' is not valid.")),
|
|
||||||
};
|
let diff_list = vec![Diff::DeleteRows {
|
||||||
let old_data = Box::new(RowData {
|
|
||||||
row: row_data,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
let diff_list = vec![Diff::DeleteRow {
|
|
||||||
sheet,
|
sheet,
|
||||||
row,
|
row,
|
||||||
|
count: row_count,
|
||||||
old_data,
|
old_data,
|
||||||
}];
|
}];
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
self.model.delete_rows(sheet, row, 1)?;
|
|
||||||
self.evaluate_if_not_paused();
|
self.evaluate_if_not_paused();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a column
|
/// Deletes `column_count` columns starting at `column`.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// History: pushes one [`crate::user_model::history::Diff::DeleteColumn`]
|
||||||
/// * [Model::insert_columns]
|
/// per column, **right → left**, so replaying the list is always safe with
|
||||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
/// respect to index shifts.
|
||||||
let diff_list = vec![Diff::InsertColumn { sheet, column }];
|
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
self.model.insert_columns(sheet, column, 1)?;
|
|
||||||
self.evaluate_if_not_paused();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes a column
|
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also [`Model::delete_columns`].
|
||||||
/// * [Model::delete_columns]
|
pub fn delete_columns(
|
||||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
column_count: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||||
if !is_valid_column_number(column) {
|
let mut old_data = Vec::new();
|
||||||
return Err(format!("Column number '{column}' is not valid."));
|
// Collect data for all columns to be deleted
|
||||||
}
|
for c in column..column + column_count {
|
||||||
|
let mut column_data = None;
|
||||||
let mut column_data = None;
|
for col in &worksheet.cols {
|
||||||
for col in &worksheet.cols {
|
if c >= col.min && c <= col.max {
|
||||||
let min = col.min;
|
column_data = Some(Col {
|
||||||
let max = col.max;
|
min: c,
|
||||||
if column >= min && column <= max {
|
max: c,
|
||||||
column_data = Some(Col {
|
width: col.width,
|
||||||
min: column,
|
custom_width: col.custom_width,
|
||||||
max: column,
|
style: col.style,
|
||||||
width: col.width,
|
});
|
||||||
custom_width: col.custom_width,
|
break;
|
||||||
style: col.style,
|
}
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut data = HashMap::new();
|
let mut data = HashMap::new();
|
||||||
for (row, row_data) in &worksheet.sheet_data {
|
for (row_idx, row_data) in &worksheet.sheet_data {
|
||||||
if let Some(cell) = row_data.get(&column) {
|
if let Some(cell) = row_data.get(&c) {
|
||||||
data.insert(*row, cell.clone());
|
data.insert(*row_idx, cell.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let diff_list = vec![Diff::DeleteColumn {
|
old_data.push(ColumnData {
|
||||||
sheet,
|
|
||||||
column,
|
|
||||||
old_data: Box::new(ColumnData {
|
|
||||||
column: column_data,
|
column: column_data,
|
||||||
data,
|
data,
|
||||||
}),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model.delete_columns(sheet, column, column_count)?;
|
||||||
|
|
||||||
|
let diff_list = vec![Diff::DeleteColumns {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
count: column_count,
|
||||||
|
old_data,
|
||||||
}];
|
}];
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
self.model.delete_columns(sheet, column, 1)?;
|
self.evaluate_if_not_paused();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves a column horizontally and adjusts formulas
|
||||||
|
pub fn move_column_action(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
delta: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let diff_list = vec![Diff::MoveColumn {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
delta,
|
||||||
|
}];
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
|
self.model.move_column_action(sheet, column, delta)?;
|
||||||
|
self.evaluate_if_not_paused();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves a row vertically and adjusts formulas
|
||||||
|
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
|
||||||
|
let diff_list = vec![Diff::MoveRow { sheet, row, delta }];
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
|
self.model.move_row_action(sheet, row, delta)?;
|
||||||
self.evaluate_if_not_paused();
|
self.evaluate_if_not_paused();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1487,10 +1587,10 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{first_row}'"));
|
return Err(format!("Invalid row: '{first_row}'"));
|
||||||
}
|
}
|
||||||
if !is_valid_column_number(last_column) {
|
if !is_valid_column_number(last_column) {
|
||||||
return Err(format!("Invalid column: '{}'", last_column));
|
return Err(format!("Invalid column: '{last_column}'"));
|
||||||
}
|
}
|
||||||
if !is_valid_row(last_row) {
|
if !is_valid_row(last_row) {
|
||||||
return Err(format!("Invalid row: '{}'", last_row));
|
return Err(format!("Invalid row: '{last_row}'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_valid_row(to_column) {
|
if !is_valid_row(to_column) {
|
||||||
@@ -1595,6 +1695,125 @@ impl UserModel {
|
|||||||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the largest column in the row less than a column whose cell has a non empty value.
|
||||||
|
/// If there are none it returns `None`.
|
||||||
|
/// This is useful when rendering a part of a worksheet to know which cells spill over
|
||||||
|
pub fn get_last_non_empty_in_row_before_column(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<Option<i32>, String> {
|
||||||
|
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||||
|
let data = worksheet.sheet_data.get(&row);
|
||||||
|
if let Some(row_data) = data {
|
||||||
|
let mut last_column = None;
|
||||||
|
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||||||
|
columns.sort_unstable();
|
||||||
|
for col in columns {
|
||||||
|
if col < column {
|
||||||
|
if let Some(cell) = worksheet.cell(row, col) {
|
||||||
|
if matches!(cell, Cell::EmptyCell { .. }) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_column = Some(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(last_column)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the smallest column in the row larger than "column" whose cell has a non empty value.
|
||||||
|
/// If there are none it returns `None`.
|
||||||
|
/// This is useful when rendering a part of a worksheet to know which cells spill over
|
||||||
|
pub fn get_first_non_empty_in_row_after_column(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<Option<i32>, String> {
|
||||||
|
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||||
|
let data = worksheet.sheet_data.get(&row);
|
||||||
|
if let Some(row_data) = data {
|
||||||
|
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||||||
|
// We sort the keys to ensure we are going from left to right
|
||||||
|
columns.sort_unstable();
|
||||||
|
for col in columns {
|
||||||
|
if col > column {
|
||||||
|
if let Some(cell) = worksheet.cell(row, col) {
|
||||||
|
if matches!(cell, Cell::EmptyCell { .. }) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(Some(col));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the geometric structure of a cell
|
||||||
|
pub fn get_cell_array_structure(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<CellArrayStructure, String> {
|
||||||
|
let cell = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(row, column)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
match cell {
|
||||||
|
Cell::EmptyCell { .. }
|
||||||
|
| Cell::BooleanCell { .. }
|
||||||
|
| Cell::NumberCell { .. }
|
||||||
|
| Cell::ErrorCell { .. }
|
||||||
|
| Cell::SharedString { .. }
|
||||||
|
| Cell::CellFormula { .. }
|
||||||
|
| Cell::CellFormulaBoolean { .. }
|
||||||
|
| Cell::CellFormulaNumber { .. }
|
||||||
|
| Cell::CellFormulaString { .. }
|
||||||
|
| Cell::CellFormulaError { .. } => Ok(CellArrayStructure::SingleCell),
|
||||||
|
Cell::SpillNumberCell { m, .. }
|
||||||
|
| Cell::SpillBooleanCell { m, .. }
|
||||||
|
| Cell::SpillErrorCell { m, .. }
|
||||||
|
| Cell::SpillStringCell { m, .. } => {
|
||||||
|
let (m_row, m_column) = m;
|
||||||
|
let m_cell = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(m_row, m_column)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (width, height) = match m_cell {
|
||||||
|
Cell::DynamicCellFormula { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaBoolean { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaNumber { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaString { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaError { r, .. } => (r.0, r.1),
|
||||||
|
_ => return Err("Invalid structure".to_string()),
|
||||||
|
};
|
||||||
|
Ok(CellArrayStructure::DynamicChild(
|
||||||
|
m_row, m_column, width, height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Cell::DynamicCellFormula { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaBoolean { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaNumber { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaString { r, .. }
|
||||||
|
| Cell::DynamicCellFormulaError { r, .. } => {
|
||||||
|
Ok(CellArrayStructure::DynamicMother(r.0, r.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a copy of the selected area
|
/// Returns a copy of the selected area
|
||||||
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
|
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
|
||||||
let selected_area = self.get_selected_view();
|
let selected_area = self.get_selected_view();
|
||||||
@@ -1623,18 +1842,18 @@ impl UserModel {
|
|||||||
text_row.push(text);
|
text_row.push(text);
|
||||||
}
|
}
|
||||||
wtr.write_record(text_row)
|
wtr.write_record(text_row)
|
||||||
.map_err(|e| format!("Error while processing csv: {}", e))?;
|
.map_err(|e| format!("Error while processing csv: {e}"))?;
|
||||||
data.insert(row, data_row);
|
data.insert(row, data_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
let csv = String::from_utf8(
|
let csv = String::from_utf8(
|
||||||
wtr.into_inner()
|
wtr.into_inner()
|
||||||
.map_err(|e| format!("Processing error: '{}'", e))?,
|
.map_err(|e| format!("Processing error: '{e}'"))?,
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Error converting from utf8: '{}'", e))?;
|
.map_err(|e| format!("Error converting from utf8: '{e}'"))?;
|
||||||
|
|
||||||
Ok(Clipboard {
|
Ok(Clipboard {
|
||||||
csv,
|
csv: csv.trim().to_string(),
|
||||||
data,
|
data,
|
||||||
sheet,
|
sheet,
|
||||||
range: (row_start, column_start, row_end, column_end),
|
range: (row_start, column_start, row_end, column_end),
|
||||||
@@ -1802,7 +2021,7 @@ impl UserModel {
|
|||||||
}
|
}
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
// select the pasted area
|
// select the pasted area
|
||||||
self.set_selected_range(area.row, area.column, row, column)?;
|
self.set_selected_range(area.row, area.column, row - 1, column - 1)?;
|
||||||
self.evaluate_if_not_paused();
|
self.evaluate_if_not_paused();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1897,6 +2116,24 @@ impl UserModel {
|
|||||||
old_value,
|
old_value,
|
||||||
} => {
|
} => {
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
|
let cell = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.worksheet(*sheet)?
|
||||||
|
.cell(*row, *column)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some((width, height)) = cell.get_dynamic_range() {
|
||||||
|
for r in *row..*row + height {
|
||||||
|
for c in *column..*column + width {
|
||||||
|
// skip the "mother" cell
|
||||||
|
if r == *row && c == *column {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.model.cell_clear_contents(*sheet, r, c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
match *old_value.clone() {
|
match *old_value.clone() {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
self.model
|
self.model
|
||||||
@@ -1967,45 +2204,56 @@ impl UserModel {
|
|||||||
self.model.cell_clear_all(*sheet, *row, *column)?;
|
self.model.cell_clear_all(*sheet, *row, *column)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Diff::InsertRow { sheet, row } => {
|
Diff::InsertRows { sheet, row, count } => {
|
||||||
self.model.delete_rows(*sheet, *row, 1)?;
|
self.model.delete_rows(*sheet, *row, *count)?;
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
}
|
}
|
||||||
Diff::DeleteRow {
|
Diff::DeleteRows {
|
||||||
sheet,
|
sheet,
|
||||||
row,
|
row,
|
||||||
|
count: _,
|
||||||
old_data,
|
old_data,
|
||||||
} => {
|
} => {
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
self.model.insert_rows(*sheet, *row, 1)?;
|
self.model
|
||||||
|
.insert_rows(*sheet, *row, old_data.len() as i32)?;
|
||||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||||
if let Some(row_data) = old_data.row.clone() {
|
for (i, row_data) in old_data.iter().enumerate() {
|
||||||
worksheet.rows.push(row_data);
|
let r = *row + i as i32;
|
||||||
|
if let Some(row_style) = row_data.row.clone() {
|
||||||
|
worksheet.rows.push(row_style);
|
||||||
|
}
|
||||||
|
worksheet.sheet_data.insert(r, row_data.data.clone());
|
||||||
}
|
}
|
||||||
worksheet.sheet_data.insert(*row, old_data.data.clone());
|
|
||||||
}
|
}
|
||||||
Diff::InsertColumn { sheet, column } => {
|
Diff::InsertColumns {
|
||||||
self.model.delete_columns(*sheet, *column, 1)?;
|
|
||||||
needs_evaluation = true;
|
|
||||||
}
|
|
||||||
Diff::DeleteColumn {
|
|
||||||
sheet,
|
sheet,
|
||||||
column,
|
column,
|
||||||
|
count,
|
||||||
|
} => {
|
||||||
|
self.model.delete_columns(*sheet, *column, *count)?;
|
||||||
|
needs_evaluation = true;
|
||||||
|
}
|
||||||
|
Diff::DeleteColumns {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
count: _,
|
||||||
old_data,
|
old_data,
|
||||||
} => {
|
} => {
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
// inserts an empty column
|
self.model
|
||||||
self.model.insert_columns(*sheet, *column, 1)?;
|
.insert_columns(*sheet, *column, old_data.len() as i32)?;
|
||||||
// puts all the data back
|
|
||||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||||
for (row, cell) in &old_data.data {
|
for (i, col_data) in old_data.iter().enumerate() {
|
||||||
worksheet.update_cell(*row, *column, cell.clone())?;
|
let c = *column + i as i32;
|
||||||
}
|
for (row, cell) in &col_data.data {
|
||||||
// makes sure that the width and style is correct
|
worksheet.update_cell(*row, c, cell.clone())?;
|
||||||
if let Some(col) = &old_data.column {
|
}
|
||||||
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
|
if let Some(col) = &col_data.column {
|
||||||
let style = col.style;
|
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
|
||||||
worksheet.set_column_width_and_style(*column, width, style)?;
|
let style = col.style;
|
||||||
|
worksheet.set_column_width_and_style(c, width, style)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Diff::SetFrozenRowsCount {
|
Diff::SetFrozenRowsCount {
|
||||||
@@ -2163,6 +2411,21 @@ impl UserModel {
|
|||||||
self.model.delete_row_style(*sheet, *row)?;
|
self.model.delete_row_style(*sheet, *row)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Diff::MoveColumn {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
delta,
|
||||||
|
} => {
|
||||||
|
// For undo, we apply the opposite move
|
||||||
|
self.model
|
||||||
|
.move_column_action(*sheet, *column + *delta, -*delta)?;
|
||||||
|
needs_evaluation = true;
|
||||||
|
}
|
||||||
|
Diff::MoveRow { sheet, row, delta } => {
|
||||||
|
// For undo, we apply the opposite move
|
||||||
|
self.model.move_row_action(*sheet, *row + *delta, -*delta)?;
|
||||||
|
needs_evaluation = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if needs_evaluation {
|
if needs_evaluation {
|
||||||
@@ -2231,28 +2494,34 @@ impl UserModel {
|
|||||||
} => self
|
} => self
|
||||||
.model
|
.model
|
||||||
.set_cell_style(*sheet, *row, *column, new_value)?,
|
.set_cell_style(*sheet, *row, *column, new_value)?,
|
||||||
Diff::InsertRow { sheet, row } => {
|
Diff::InsertRows { sheet, row, count } => {
|
||||||
self.model.insert_rows(*sheet, *row, 1)?;
|
self.model.insert_rows(*sheet, *row, *count)?;
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
}
|
}
|
||||||
Diff::DeleteRow {
|
Diff::DeleteRows {
|
||||||
sheet,
|
sheet,
|
||||||
row,
|
row,
|
||||||
|
count,
|
||||||
old_data: _,
|
old_data: _,
|
||||||
} => {
|
} => {
|
||||||
self.model.delete_rows(*sheet, *row, 1)?;
|
self.model.delete_rows(*sheet, *row, *count)?;
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
}
|
}
|
||||||
Diff::InsertColumn { sheet, column } => {
|
Diff::InsertColumns {
|
||||||
needs_evaluation = true;
|
|
||||||
self.model.insert_columns(*sheet, *column, 1)?;
|
|
||||||
}
|
|
||||||
Diff::DeleteColumn {
|
|
||||||
sheet,
|
sheet,
|
||||||
column,
|
column,
|
||||||
|
count,
|
||||||
|
} => {
|
||||||
|
self.model.insert_columns(*sheet, *column, *count)?;
|
||||||
|
needs_evaluation = true;
|
||||||
|
}
|
||||||
|
Diff::DeleteColumns {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
count,
|
||||||
old_data: _,
|
old_data: _,
|
||||||
} => {
|
} => {
|
||||||
self.model.delete_columns(*sheet, *column, 1)?;
|
self.model.delete_columns(*sheet, *column, *count)?;
|
||||||
needs_evaluation = true;
|
needs_evaluation = true;
|
||||||
}
|
}
|
||||||
Diff::SetFrozenRowsCount {
|
Diff::SetFrozenRowsCount {
|
||||||
@@ -2364,6 +2633,18 @@ impl UserModel {
|
|||||||
} => {
|
} => {
|
||||||
self.model.delete_row_style(*sheet, *row)?;
|
self.model.delete_row_style(*sheet, *row)?;
|
||||||
}
|
}
|
||||||
|
Diff::MoveColumn {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
delta,
|
||||||
|
} => {
|
||||||
|
self.model.move_column_action(*sheet, *column, *delta)?;
|
||||||
|
needs_evaluation = true;
|
||||||
|
}
|
||||||
|
Diff::MoveRow { sheet, row, delta } => {
|
||||||
|
self.model.move_row_action(*sheet, *row, *delta)?;
|
||||||
|
needs_evaluation = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2391,7 +2672,7 @@ mod tests {
|
|||||||
VerticalAlignment::Top,
|
VerticalAlignment::Top,
|
||||||
];
|
];
|
||||||
for a in all {
|
for a in all {
|
||||||
assert_eq!(vertical(&format!("{}", a)), Ok(a));
|
assert_eq!(vertical(&format!("{a}")), Ok(a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2408,7 +2689,7 @@ mod tests {
|
|||||||
HorizontalAlignment::Right,
|
HorizontalAlignment::Right,
|
||||||
];
|
];
|
||||||
for a in all {
|
for a in all {
|
||||||
assert_eq!(horizontal(&format!("{}", a)), Ok(a));
|
assert_eq!(horizontal(&format!("{a}")), Ok(a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ use bitcode::{Decode, Encode};
|
|||||||
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
|
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
pub(crate) struct RowData {
|
pub(crate) struct RowData {
|
||||||
pub(crate) row: Option<Row>,
|
pub(crate) row: Option<Row>,
|
||||||
pub(crate) data: HashMap<i32, Cell>,
|
pub(crate) data: HashMap<i32, Cell>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
pub(crate) struct ColumnData {
|
pub(crate) struct ColumnData {
|
||||||
pub(crate) column: Option<Col>,
|
pub(crate) column: Option<Col>,
|
||||||
pub(crate) data: HashMap<i32, Cell>,
|
pub(crate) data: HashMap<i32, Cell>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
pub(crate) enum Diff {
|
pub(crate) enum Diff {
|
||||||
// Cell diffs
|
// Cell diffs
|
||||||
SetCellValue {
|
SetCellValue {
|
||||||
@@ -87,23 +90,27 @@ pub(crate) enum Diff {
|
|||||||
row: i32,
|
row: i32,
|
||||||
old_value: Box<Option<Style>>,
|
old_value: Box<Option<Style>>,
|
||||||
},
|
},
|
||||||
InsertRow {
|
InsertRows {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
row: i32,
|
row: i32,
|
||||||
|
count: i32,
|
||||||
},
|
},
|
||||||
DeleteRow {
|
DeleteRows {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
row: i32,
|
row: i32,
|
||||||
old_data: Box<RowData>,
|
count: i32,
|
||||||
|
old_data: Vec<RowData>,
|
||||||
},
|
},
|
||||||
InsertColumn {
|
InsertColumns {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
column: i32,
|
column: i32,
|
||||||
|
count: i32,
|
||||||
},
|
},
|
||||||
DeleteColumn {
|
DeleteColumns {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
column: i32,
|
column: i32,
|
||||||
old_data: Box<ColumnData>,
|
count: i32,
|
||||||
|
old_data: Vec<ColumnData>,
|
||||||
},
|
},
|
||||||
DeleteSheet {
|
DeleteSheet {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
@@ -161,6 +168,16 @@ pub(crate) enum Diff {
|
|||||||
new_scope: Option<u32>,
|
new_scope: Option<u32>,
|
||||||
new_formula: String,
|
new_formula: String,
|
||||||
},
|
},
|
||||||
|
MoveColumn {
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
delta: i32,
|
||||||
|
},
|
||||||
|
MoveRow {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
delta: i32,
|
||||||
|
},
|
||||||
// FIXME: we are missing SetViewDiffs
|
// FIXME: we are missing SetViewDiffs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
mod border;
|
mod border;
|
||||||
mod border_utils;
|
mod border_utils;
|
||||||
mod common;
|
mod common;
|
||||||
mod history;
|
pub(crate) mod history;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
pub use common::UserModel;
|
pub use common::UserModel;
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::expressions::utils::{is_valid_column_number, is_valid_row};
|
use crate::{
|
||||||
|
constants::{LAST_COLUMN, LAST_ROW},
|
||||||
|
expressions::utils::{is_valid_column_number, is_valid_row},
|
||||||
|
worksheet::NavigationDirection,
|
||||||
|
};
|
||||||
|
|
||||||
use super::common::UserModel;
|
use super::common::UserModel;
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ impl UserModel {
|
|||||||
/// Sets the the selected sheet
|
/// Sets the the selected sheet
|
||||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
return Err(format!("Invalid worksheet index {sheet}"));
|
||||||
}
|
}
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
||||||
view.sheet = sheet;
|
view.sheet = sheet;
|
||||||
@@ -98,7 +102,7 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{row}'"));
|
return Err(format!("Invalid row: '{row}'"));
|
||||||
}
|
}
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
return Err(format!("Invalid worksheet index {sheet}"));
|
||||||
}
|
}
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
@@ -110,7 +114,7 @@ impl UserModel {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the selected range. Note that the selected cell must be in one of the corners.
|
/// Sets the selected range. Note that the selected cell must be in the selected range.
|
||||||
pub fn set_selected_range(
|
pub fn set_selected_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
start_row: i32,
|
start_row: i32,
|
||||||
@@ -138,24 +142,38 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{end_row}'"));
|
return Err(format!("Invalid row: '{end_row}'"));
|
||||||
}
|
}
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
return Err(format!("Invalid worksheet index {sheet}"));
|
||||||
}
|
}
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
let selected_row = view.row;
|
let selected_row = view.row;
|
||||||
let selected_column = view.column;
|
let selected_column = view.column;
|
||||||
// The selected cells must be on one of the corners of the selected range:
|
if start_row == 1 && end_row == LAST_ROW {
|
||||||
if selected_row != start_row && selected_row != end_row {
|
// full row selected. The cell must be at the top or the bottom of the range
|
||||||
return Err(format!(
|
if selected_column != start_column && selected_column != end_column {
|
||||||
"The selected cells is not in one of the corners. Row: '{}' and row range '({}, {})'",
|
return Err(format!(
|
||||||
selected_row, start_row, end_row
|
"The selected cell is not the column edge. Column '{selected_column}' and column range '({start_column}, {end_column})'"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if start_column == 1 && end_column == LAST_COLUMN {
|
||||||
|
// full column selected. The cell must be at the left or the right of the range
|
||||||
|
if selected_row != start_row && selected_row != end_row {
|
||||||
|
return Err(format!(
|
||||||
|
"The selected cell is not in the row edge. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The selected cell must be on one of the corners of the selected range:
|
||||||
|
if selected_row != start_row && selected_row != end_row {
|
||||||
|
return Err(format!(
|
||||||
|
"The selected cell is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if selected_column != start_column && selected_column != end_column {
|
if selected_column != start_column && selected_column != end_column {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'",
|
"The selected cell is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
|
||||||
selected_column, start_column, end_column
|
|
||||||
));
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
view.range = [start_row, start_column, end_row, end_column];
|
view.range = [start_row, start_column, end_row, end_column];
|
||||||
}
|
}
|
||||||
@@ -192,6 +210,17 @@ impl UserModel {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let [row_start, column_start, row_end, column_end] = range;
|
let [row_start, column_start, row_end, column_end] = range;
|
||||||
|
if ["ArrowUp", "ArrowDown"].contains(&key) && row_start == 1 && row_end == LAST_ROW {
|
||||||
|
// full column selected, nothing to do
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if ["ArrowRight", "ArrowLeft"].contains(&key)
|
||||||
|
&& column_start == 1
|
||||||
|
&& column_end == LAST_COLUMN
|
||||||
|
{
|
||||||
|
// full row selected, nothing to do
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"ArrowRight" => {
|
"ArrowRight" => {
|
||||||
@@ -307,7 +336,7 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{top_row}'"));
|
return Err(format!("Invalid row: '{top_row}'"));
|
||||||
}
|
}
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
return Err(format!("Invalid worksheet index {sheet}"));
|
||||||
}
|
}
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
@@ -474,7 +503,7 @@ impl UserModel {
|
|||||||
// if the row is not fully visible we 'scroll' down until it is
|
// if the row is not fully visible we 'scroll' down until it is
|
||||||
let mut height = 0.0;
|
let mut height = 0.0;
|
||||||
let mut row = view.top_row;
|
let mut row = view.top_row;
|
||||||
while row <= new_row + 1 {
|
while row <= new_row + 1 && row <= LAST_ROW {
|
||||||
height += self.model.get_row_height(sheet, row)?;
|
height += self.model.get_row_height(sheet, row)?;
|
||||||
row += 1;
|
row += 1;
|
||||||
}
|
}
|
||||||
@@ -684,4 +713,94 @@ impl UserModel {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// User navigates to the edge in the given direction
|
||||||
|
pub fn on_navigate_to_edge_in_direction(
|
||||||
|
&mut self,
|
||||||
|
direction: NavigationDirection,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (sheet, window_height, window_width) =
|
||||||
|
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
|
(view.sheet, view.window_height, view.window_width)
|
||||||
|
} else {
|
||||||
|
return Err("View not found".to_string());
|
||||||
|
};
|
||||||
|
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return Err("Worksheet not found".to_string()),
|
||||||
|
};
|
||||||
|
let view = match worksheet.views.get(&self.model.view_id) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return Err("View not found".to_string()),
|
||||||
|
};
|
||||||
|
let row = view.row;
|
||||||
|
let column = view.column;
|
||||||
|
if !is_valid_row(row) || !is_valid_column_number(column) {
|
||||||
|
return Err("Invalid row or column".to_string());
|
||||||
|
}
|
||||||
|
let (new_row, new_column) =
|
||||||
|
worksheet.navigate_to_edge_in_direction(row, column, direction)?;
|
||||||
|
if !is_valid_row(new_row) || !is_valid_column_number(new_column) {
|
||||||
|
return Err("Invalid row or column after navigation".to_string());
|
||||||
|
}
|
||||||
|
if new_row == row && new_column == column {
|
||||||
|
return Ok(()); // No change in selection
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut top_row = view.top_row;
|
||||||
|
let mut left_column = view.left_column;
|
||||||
|
|
||||||
|
match direction {
|
||||||
|
NavigationDirection::Left | NavigationDirection::Right => {
|
||||||
|
// If the new column is not fully visible we 'scroll' until it is
|
||||||
|
// We need to check two conditions:
|
||||||
|
// 1. new_column > view.left_column
|
||||||
|
// 2. right_column < new_column
|
||||||
|
if new_column < view.left_column {
|
||||||
|
left_column = new_column;
|
||||||
|
} else {
|
||||||
|
let mut c = new_column;
|
||||||
|
let mut width = self.model.get_column_width(sheet, c)?;
|
||||||
|
while c > 1 && width <= window_width as f64 {
|
||||||
|
c -= 1;
|
||||||
|
width += self.model.get_column_width(sheet, c)?;
|
||||||
|
}
|
||||||
|
if c > view.left_column {
|
||||||
|
left_column = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavigationDirection::Up | NavigationDirection::Down => {
|
||||||
|
// If the new row is not fully visible we 'scroll' until it is
|
||||||
|
// We need to check two conditions:
|
||||||
|
// 1. new_row > view.top_row
|
||||||
|
// 2. bottom_row < new_row
|
||||||
|
if new_row < view.top_row {
|
||||||
|
top_row = new_row;
|
||||||
|
} else {
|
||||||
|
let mut r = new_row;
|
||||||
|
let mut height = self.model.get_row_height(sheet, r)?;
|
||||||
|
while r > 1 && height <= window_height as f64 {
|
||||||
|
r -= 1;
|
||||||
|
height += self.model.get_row_height(sheet, r)?;
|
||||||
|
}
|
||||||
|
if r > view.top_row {
|
||||||
|
top_row = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
|
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||||
|
view.row = new_row;
|
||||||
|
view.column = new_column;
|
||||||
|
view.range = [new_row, new_column, new_row, new_column];
|
||||||
|
|
||||||
|
view.top_row = top_row;
|
||||||
|
view.left_column = left_column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
bindings/nodejs/index.d.ts
vendored
8
bindings/nodejs/index.d.ts
vendored
@@ -59,10 +59,10 @@ export declare class UserModel {
|
|||||||
setSheetColor(sheet: number, color: string): void
|
setSheetColor(sheet: number, color: string): void
|
||||||
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
|
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
|
||||||
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
|
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
|
||||||
insertRow(sheet: number, row: number): void
|
insertRows(sheet: number, row: number, rowCount: number): void
|
||||||
insertColumn(sheet: number, column: number): void
|
insertColumns(sheet: number, column: number, columnCount: number): void
|
||||||
deleteRow(sheet: number, row: number): void
|
deleteRows(sheet: number, row: number, rowCount: number): void
|
||||||
deleteColumn(sheet: number, column: number): void
|
deleteColumns(sheet: number, column: number, columnCount: number): void
|
||||||
setRowHeight(sheet: number, row: number, height: number): void
|
setRowHeight(sheet: number, row: number, height: number): void
|
||||||
setColumnWidth(sheet: number, column: number, width: number): void
|
setColumnWidth(sheet: number, column: number, width: number): void
|
||||||
getRowHeight(sheet: number, row: number): number
|
getRowHeight(sheet: number, row: number): number
|
||||||
|
|||||||
@@ -340,4 +340,20 @@ impl Model {
|
|||||||
.delete_defined_name(&name, scope)
|
.delete_defined_name(&name, scope)
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "moveColumn")]
|
||||||
|
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.move_column_action(sheet, column, delta)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "moveRow")]
|
||||||
|
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.move_row_action(sheet, row, delta)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,24 +183,36 @@ impl UserModel {
|
|||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi(js_name = "insertRow")]
|
#[napi(js_name = "insertRows")]
|
||||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
|
||||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
self
|
||||||
|
.model
|
||||||
|
.insert_rows(sheet, row, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi(js_name = "insertColumn")]
|
#[napi(js_name = "insertColumns")]
|
||||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
|
||||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
self
|
||||||
|
.model
|
||||||
|
.insert_columns(sheet, column, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi(js_name = "deleteRow")]
|
#[napi(js_name = "deleteRows")]
|
||||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
|
||||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
self
|
||||||
|
.model
|
||||||
|
.delete_rows(sheet, row, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi(js_name = "deleteColumn")]
|
#[napi(js_name = "deleteColumns")]
|
||||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
|
||||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
self
|
||||||
|
.model
|
||||||
|
.delete_columns(sheet, column, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi(js_name = "setRowsHeight")]
|
#[napi(js_name = "setRowsHeight")]
|
||||||
@@ -651,4 +663,20 @@ impl UserModel {
|
|||||||
.delete_defined_name(&name, scope)
|
.delete_defined_name(&name, scope)
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "moveColumn")]
|
||||||
|
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.move_column_action(sheet, column, delta)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "moveRow")]
|
||||||
|
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.move_row_action(sheet, row, delta)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pyroncalc"
|
name = "pyroncalc"
|
||||||
version = "0.5.0"
|
version = "0.5.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +13,8 @@ crate-type = ["cdylib"]
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
|
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
|
||||||
pyo3 = { version = "0.23", features = ["extension-module"] }
|
pyo3 = { version = "0.25", features = ["extension-module"] }
|
||||||
|
bitcode = "0.6.3"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ You can add cell values, retrieve them and most importantly you can evaluate spr
|
|||||||
pip install ironcalc
|
pip install ironcalc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Compile and test
|
## Compile and test
|
||||||
|
|
||||||
To compile this and test it:
|
To compile this and test it:
|
||||||
@@ -29,3 +26,17 @@ examples $ python example.py
|
|||||||
From there if you use `python` you can `import ironcalc`. You can either create a new file, read it from a JSON string or import from Excel.
|
From there if you use `python` you can `import ironcalc`. You can either create a new file, read it from a JSON string or import from Excel.
|
||||||
|
|
||||||
Hopefully the API is straightforward.
|
Hopefully the API is straightforward.
|
||||||
|
|
||||||
|
## Creating documentation
|
||||||
|
|
||||||
|
We use sphinx
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install maturin
|
||||||
|
pip install sphinx
|
||||||
|
maturin develop
|
||||||
|
sphinx-build -M html docs html
|
||||||
|
python -m http.server --directory html/html/
|
||||||
|
```
|
||||||
|
|||||||
9
bindings/python/build_docs.sh
Executable file
9
bindings/python/build_docs.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install patchelf
|
||||||
|
pip install maturin
|
||||||
|
pip install sphinx
|
||||||
|
maturin develop
|
||||||
|
sphinx-build -M html docs html
|
||||||
|
python -m http.server --directory html/html/
|
||||||
@@ -8,7 +8,8 @@ IronCalc
|
|||||||
installation
|
installation
|
||||||
usage_examples
|
usage_examples
|
||||||
top_level_methods
|
top_level_methods
|
||||||
api_reference
|
raw_api_reference
|
||||||
|
user_api_reference
|
||||||
objects
|
objects
|
||||||
|
|
||||||
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.
|
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
API Reference
|
Raw API Reference
|
||||||
-------------
|
-----------------
|
||||||
|
|
||||||
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.
|
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.
|
||||||
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
Top Level Methods
|
Top Level Methods
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
This module provides a set of top-level methods for creating and loading IronCalc models.
|
||||||
|
|
||||||
.. autofunction:: ironcalc.create
|
.. autofunction:: ironcalc.create
|
||||||
.. autofunction:: ironcalc.load_from_xlsx
|
.. autofunction:: ironcalc.load_from_xlsx
|
||||||
.. autofunction:: ironcalc.load_from_icalc
|
.. autofunction:: ironcalc.load_from_icalc
|
||||||
|
.. autofunction:: ironcalc.load_from_bytes
|
||||||
|
.. autofunction:: ironcalc.create_user_model
|
||||||
|
.. autofunction:: ironcalc.create_user_model_from_bytes
|
||||||
|
.. autofunction:: ironcalc.create_user_model_from_xlsx
|
||||||
|
.. autofunction:: ironcalc.create_user_model_from_icalc
|
||||||
41
bindings/python/docs/user_api_reference.rst
Normal file
41
bindings/python/docs/user_api_reference.rst
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
User API Reference
|
||||||
|
------------------
|
||||||
|
|
||||||
|
This is the "user api". Models here have history, they evaluate automatically with each change and have a "diff" history.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: save_to_xlsx(file: str)
|
||||||
|
|
||||||
|
Saves the user model to file in the XLSX format.
|
||||||
|
|
||||||
|
::param file: The file path to save the model to.
|
||||||
|
|
||||||
|
.. method:: save_to_icalc(file: str)
|
||||||
|
|
||||||
|
Saves the user model to file in the internal binary ic format.
|
||||||
|
|
||||||
|
::param file: The file path to save the model to.
|
||||||
|
|
||||||
|
.. method:: apply_external_diffs(external_diffs: bytes)
|
||||||
|
|
||||||
|
Applies external diffs to the model. This is used to apply changes from other instances of the model.
|
||||||
|
|
||||||
|
::param external_diffs: The external diffs to apply, as a byte array.
|
||||||
|
|
||||||
|
.. method:: flush_send_queue() -> bytes
|
||||||
|
|
||||||
|
Flushes the send queue and returns the bytes to be sent to the client. This is used to send changes to the client.
|
||||||
|
|
||||||
|
.. method:: set_user_input(sheet: int, row: int, column: int, value: str)
|
||||||
|
|
||||||
|
Sets an input in a cell, as would be done by a user typing into a spreadsheet cell.
|
||||||
|
|
||||||
|
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
|
||||||
|
|
||||||
|
Returns the cell’s value as a formatted string, taking into account any number/currency/date formatting.
|
||||||
|
|
||||||
|
.. method:: to_bytes() -> bytes
|
||||||
|
|
||||||
|
Returns the model as a byte array. This is useful for sending the model over a network or saving it to a file.
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ironcalc"
|
name = "ironcalc"
|
||||||
version = "0.5.0"
|
version = "0.5.7"
|
||||||
description = "Create, edit and evaluate Excel spreadsheets"
|
description = "Create, edit and evaluate Excel spreadsheets"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
keywords = [
|
keywords = [
|
||||||
|
|||||||
@@ -1,19 +1,89 @@
|
|||||||
use pyo3::exceptions::PyException;
|
use pyo3::exceptions::PyException;
|
||||||
use pyo3::{create_exception, prelude::*, wrap_pyfunction};
|
use pyo3::{create_exception, prelude::*, wrap_pyfunction};
|
||||||
|
|
||||||
use types::{PySheetProperty, PyStyle};
|
use types::{PyCellType, PySheetProperty, PyStyle};
|
||||||
use xlsx::base::types::Style;
|
use xlsx::base::types::{Style, Workbook};
|
||||||
use xlsx::base::Model;
|
use xlsx::base::{Model, UserModel};
|
||||||
|
|
||||||
use xlsx::export::{save_to_icalc, save_to_xlsx};
|
use xlsx::export::{save_to_icalc, save_to_xlsx};
|
||||||
use xlsx::import;
|
use xlsx::import;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
use crate::types::PyCellType;
|
|
||||||
|
|
||||||
create_exception!(_ironcalc, WorkbookError, PyException);
|
create_exception!(_ironcalc, WorkbookError, PyException);
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
pub struct PyUserModel {
|
||||||
|
/// The user model, which is a wrapper around the Model
|
||||||
|
pub model: UserModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyUserModel {
|
||||||
|
/// Saves the user model to an xlsx file
|
||||||
|
pub fn save_to_xlsx(&self, file: &str) -> PyResult<()> {
|
||||||
|
let model = self.model.get_model();
|
||||||
|
save_to_xlsx(model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the user model to file in the internal binary ic format
|
||||||
|
pub fn save_to_icalc(&self, file: &str) -> PyResult<()> {
|
||||||
|
let model = self.model.get_model();
|
||||||
|
save_to_icalc(model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_external_diffs(&mut self, external_diffs: &[u8]) -> PyResult<()> {
|
||||||
|
self.model
|
||||||
|
.apply_external_diffs(external_diffs)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush_send_queue(&mut self) -> Vec<u8> {
|
||||||
|
self.model.flush_send_queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_user_input(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
value: &str,
|
||||||
|
) -> PyResult<()> {
|
||||||
|
self.model
|
||||||
|
.set_user_input(sheet, row, column, value)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> PyResult<String> {
|
||||||
|
self.model
|
||||||
|
.get_formatted_cell_value(sheet, row, column)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the dimensions of a worksheet, returning the bounds of all non-empty cells.
|
||||||
|
/// Returns a tuple of (min_row, max_row, min_column, max_column).
|
||||||
|
/// For an empty sheet, returns (1, 1, 1, 1).
|
||||||
|
pub fn get_sheet_dimensions(&self, sheet: u32) -> PyResult<(i32, i32, i32, i32)> {
|
||||||
|
let model = self.model.get_model();
|
||||||
|
let worksheet = model
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let dimension = worksheet.dimension();
|
||||||
|
Ok((
|
||||||
|
dimension.min_row,
|
||||||
|
dimension.max_row,
|
||||||
|
dimension.min_column,
|
||||||
|
dimension.max_column,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes(&self) -> PyResult<Vec<u8>> {
|
||||||
|
let bytes = self.model.to_bytes();
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This is a model implementing the 'raw' API
|
/// This is a model implementing the 'raw' API
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
pub struct PyModel {
|
pub struct PyModel {
|
||||||
@@ -32,6 +102,12 @@ impl PyModel {
|
|||||||
save_to_icalc(&self.model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
|
save_to_icalc(&self.model, file).map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// To bytes
|
||||||
|
pub fn to_bytes(&self) -> PyResult<Vec<u8>> {
|
||||||
|
let bytes = self.model.to_bytes();
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluates the workbook
|
/// Evaluates the workbook
|
||||||
pub fn evaluate(&mut self) {
|
pub fn evaluate(&mut self) {
|
||||||
self.model.evaluate()
|
self.model.evaluate()
|
||||||
@@ -225,6 +301,24 @@ impl PyModel {
|
|||||||
.map_err(|e| WorkbookError::new_err(e.to_string()))
|
.map_err(|e| WorkbookError::new_err(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the dimensions of a worksheet, returning the bounds of all non-empty cells.
|
||||||
|
/// Returns a tuple of (min_row, max_row, min_column, max_column).
|
||||||
|
/// For an empty sheet, returns (1, 1, 1, 1).
|
||||||
|
pub fn get_sheet_dimensions(&self, sheet: u32) -> PyResult<(i32, i32, i32, i32)> {
|
||||||
|
let worksheet = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let dimension = worksheet.dimension();
|
||||||
|
Ok((
|
||||||
|
dimension.min_row,
|
||||||
|
dimension.max_row,
|
||||||
|
dimension.min_column,
|
||||||
|
dimension.max_column,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::panic)]
|
#[allow(clippy::panic)]
|
||||||
pub fn test_panic(&self) -> PyResult<()> {
|
pub fn test_panic(&self) -> PyResult<()> {
|
||||||
panic!("This function panics for testing panic handling");
|
panic!("This function panics for testing panic handling");
|
||||||
@@ -249,7 +343,19 @@ pub fn load_from_icalc(file_name: &str) -> PyResult<PyModel> {
|
|||||||
Ok(PyModel { model })
|
Ok(PyModel { model })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an empty model
|
/// Loads a model from bytes
|
||||||
|
/// This function expects the bytes to be in the internal binary ic format
|
||||||
|
/// which is the same format used by the `save_to_icalc` function.
|
||||||
|
#[pyfunction]
|
||||||
|
pub fn load_from_bytes(bytes: &[u8]) -> PyResult<PyModel> {
|
||||||
|
let workbook: Workbook =
|
||||||
|
bitcode::decode(bytes).map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let model =
|
||||||
|
Model::from_workbook(workbook).map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
Ok(PyModel { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an empty model in the raw API
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
|
pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
|
||||||
let model =
|
let model =
|
||||||
@@ -257,6 +363,49 @@ pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
|
|||||||
Ok(PyModel { model })
|
Ok(PyModel { model })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a model with the user model API
|
||||||
|
#[pyfunction]
|
||||||
|
pub fn create_user_model(name: &str, locale: &str, tz: &str) -> PyResult<PyUserModel> {
|
||||||
|
let model = UserModel::new_empty(name, locale, tz)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
Ok(PyUserModel { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a user model from an Excel file
|
||||||
|
#[pyfunction]
|
||||||
|
pub fn create_user_model_from_xlsx(
|
||||||
|
file_path: &str,
|
||||||
|
locale: &str,
|
||||||
|
tz: &str,
|
||||||
|
) -> PyResult<PyUserModel> {
|
||||||
|
let model = import::load_from_xlsx(file_path, locale, tz)
|
||||||
|
.map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let model = UserModel::from_model(model);
|
||||||
|
Ok(PyUserModel { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a user model from an icalc file
|
||||||
|
#[pyfunction]
|
||||||
|
pub fn create_user_model_from_icalc(file_name: &str) -> PyResult<PyUserModel> {
|
||||||
|
let model =
|
||||||
|
import::load_from_icalc(file_name).map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let model = UserModel::from_model(model);
|
||||||
|
Ok(PyUserModel { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a user model from bytes
|
||||||
|
/// This function expects the bytes to be in the internal binary ic format
|
||||||
|
/// which is the same format used by the `save_to_icalc` function.
|
||||||
|
#[pyfunction]
|
||||||
|
pub fn create_user_model_from_bytes(bytes: &[u8]) -> PyResult<PyUserModel> {
|
||||||
|
let workbook: Workbook =
|
||||||
|
bitcode::decode(bytes).map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let model =
|
||||||
|
Model::from_workbook(workbook).map_err(|e| WorkbookError::new_err(e.to_string()))?;
|
||||||
|
let user_model = UserModel::from_model(model);
|
||||||
|
Ok(PyUserModel { model: user_model })
|
||||||
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
#[allow(clippy::panic)]
|
#[allow(clippy::panic)]
|
||||||
pub fn test_panic() {
|
pub fn test_panic() {
|
||||||
@@ -272,7 +421,14 @@ fn ironcalc(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_function(wrap_pyfunction!(create, m)?)?;
|
m.add_function(wrap_pyfunction!(create, m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(load_from_xlsx, m)?)?;
|
m.add_function(wrap_pyfunction!(load_from_xlsx, m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(load_from_icalc, m)?)?;
|
m.add_function(wrap_pyfunction!(load_from_icalc, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(load_from_bytes, m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(test_panic, m)?)?;
|
m.add_function(wrap_pyfunction!(test_panic, m)?)?;
|
||||||
|
|
||||||
|
// User model functions
|
||||||
|
m.add_function(wrap_pyfunction!(create_user_model, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(create_user_model_from_bytes, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(create_user_model_from_xlsx, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(create_user_model_from_icalc, m)?)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,52 @@ def test_simple():
|
|||||||
model.evaluate()
|
model.evaluate()
|
||||||
|
|
||||||
assert model.get_formatted_cell_value(0, 1, 1) == "3"
|
assert model.get_formatted_cell_value(0, 1, 1) == "3"
|
||||||
|
|
||||||
|
bytes = model.to_bytes()
|
||||||
|
|
||||||
|
model2 = ic.load_from_bytes(bytes)
|
||||||
|
assert model2.get_formatted_cell_value(0, 1, 1) == "3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_user():
|
||||||
|
model = ic.create_user_model("model", "en", "UTC")
|
||||||
|
model.set_user_input(0, 1, 1, "=1+2")
|
||||||
|
model.set_user_input(0, 1, 2, "=A1+3")
|
||||||
|
|
||||||
|
assert model.get_formatted_cell_value(0, 1, 1) == "3"
|
||||||
|
assert model.get_formatted_cell_value(0, 1, 2) == "6"
|
||||||
|
|
||||||
|
diffs = model.flush_send_queue()
|
||||||
|
|
||||||
|
model2 = ic.create_user_model("model", "en", "UTC")
|
||||||
|
model2.apply_external_diffs(diffs)
|
||||||
|
assert model2.get_formatted_cell_value(0, 1, 1) == "3"
|
||||||
|
assert model2.get_formatted_cell_value(0, 1, 2) == "6"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sheet_dimensions():
|
||||||
|
# Test with empty sheet
|
||||||
|
model = ic.create("model", "en", "UTC")
|
||||||
|
min_row, max_row, min_col, max_col = model.get_sheet_dimensions(0)
|
||||||
|
assert (min_row, max_row, min_col, max_col) == (1, 1, 1, 1)
|
||||||
|
|
||||||
|
# Add some cells
|
||||||
|
model.set_user_input(0, 3, 5, "Hello")
|
||||||
|
model.set_user_input(0, 10, 8, "World")
|
||||||
|
model.evaluate()
|
||||||
|
|
||||||
|
# Check dimensions - should span from (3,5) to (10,8)
|
||||||
|
min_row, max_row, min_col, max_col = model.get_sheet_dimensions(0)
|
||||||
|
assert (min_row, max_row, min_col, max_col) == (3, 10, 5, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sheet_dimensions_user_model():
|
||||||
|
# Test with user model API as well
|
||||||
|
model = ic.create_user_model("model", "en", "UTC")
|
||||||
|
|
||||||
|
# Add a single cell
|
||||||
|
model.set_user_input(0, 2, 3, "Test")
|
||||||
|
|
||||||
|
# Check dimensions
|
||||||
|
min_row, max_row, min_col, max_col = model.get_sheet_dimensions(0)
|
||||||
|
assert (min_row, max_row, min_col, max_col) == (2, 2, 3, 3)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ crate-type = ["cdylib"]
|
|||||||
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
||||||
ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
|
ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
wasm-bindgen = "0.2.92"
|
wasm-bindgen = "0.2.100"
|
||||||
serde-wasm-bindgen = "0.4"
|
serde-wasm-bindgen = "0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ clean:
|
|||||||
rm -rf pkg
|
rm -rf pkg
|
||||||
rm -f types.js
|
rm -f types.js
|
||||||
|
|
||||||
.PHONY: all lint clean
|
.PHONY: all lint clean tests
|
||||||
|
|||||||
@@ -1,231 +1,25 @@
|
|||||||
# 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"""
|
header = r"""
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
get_tokens_str = r"""
|
def fix_types(text: str):
|
||||||
* @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()
|
|
||||||
|
|
||||||
view = r"""
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
getSelectedView(): any;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
view_types = r"""
|
|
||||||
* @returns {CellStyle}
|
|
||||||
*/
|
|
||||||
getSelectedView(): SelectedView;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
autofill_rows = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} source_area
|
|
||||||
* @param {number} to_row
|
|
||||||
*/
|
|
||||||
autoFillRows(source_area: any, to_row: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
autofill_rows_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} source_area
|
|
||||||
* @param {number} to_row
|
|
||||||
*/
|
|
||||||
autoFillRows(source_area: Area, to_row: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
autofill_columns = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} source_area
|
|
||||||
* @param {number} to_column
|
|
||||||
*/
|
|
||||||
autoFillColumns(source_area: any, to_column: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
autofill_columns_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} source_area
|
|
||||||
* @param {number} to_column
|
|
||||||
*/
|
|
||||||
autoFillColumns(source_area: Area, to_column: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_cell_style = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} styles
|
|
||||||
*/
|
|
||||||
onPasteStyles(styles: any): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_cell_style_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {CellStyle[][]} styles
|
|
||||||
*/
|
|
||||||
onPasteStyles(styles: CellStyle[][]): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_area_border = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} area
|
|
||||||
* @param {any} border_area
|
|
||||||
*/
|
|
||||||
setAreaWithBorder(area: any, border_area: any): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_area_border_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} area
|
|
||||||
* @param {BorderArea} border_area
|
|
||||||
*/
|
|
||||||
setAreaWithBorder(area: Area, border_area: BorderArea): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
paste_csv_string = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} area
|
|
||||||
* @param {string} csv
|
|
||||||
*/
|
|
||||||
pasteCsvText(area: any, csv: string): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
paste_csv_string_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} area
|
|
||||||
* @param {string} csv
|
|
||||||
*/
|
|
||||||
pasteCsvText(area: Area, csv: string): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
clipboard = r"""
|
|
||||||
/**
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
copyToClipboard(): any;
|
|
||||||
"""
|
|
||||||
|
|
||||||
clipboard_types = r"""
|
|
||||||
/**
|
|
||||||
* @returns {Clipboard}
|
|
||||||
*/
|
|
||||||
copyToClipboard(): Clipboard;
|
|
||||||
"""
|
|
||||||
|
|
||||||
paste_from_clipboard = r"""
|
|
||||||
/**
|
|
||||||
* @param {number} source_sheet
|
|
||||||
* @param {any} source_range
|
|
||||||
* @param {any} clipboard
|
|
||||||
* @param {boolean} is_cut
|
|
||||||
*/
|
|
||||||
pasteFromClipboard(source_sheet: number, source_range: any, clipboard: any, is_cut: boolean): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
paste_from_clipboard_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {number} source_sheet
|
|
||||||
* @param {[number, number, number, number]} source_range
|
|
||||||
* @param {ClipboardData} clipboard
|
|
||||||
* @param {boolean} is_cut
|
|
||||||
*/
|
|
||||||
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
defined_name_list = r"""
|
|
||||||
/**
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
getDefinedNameList(): any;
|
|
||||||
"""
|
|
||||||
|
|
||||||
defined_name_list_types = r"""
|
|
||||||
/**
|
|
||||||
* @returns {DefinedName[]}
|
|
||||||
*/
|
|
||||||
getDefinedNameList(): DefinedName[];
|
|
||||||
"""
|
|
||||||
|
|
||||||
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)
|
|
||||||
text = text.replace(view, view_types)
|
|
||||||
text = text.replace(autofill_rows, autofill_rows_types)
|
|
||||||
text = text.replace(autofill_columns, autofill_columns_types)
|
|
||||||
text = text.replace(set_cell_style, set_cell_style_types)
|
|
||||||
text = text.replace(set_area_border, set_area_border_types)
|
|
||||||
text = text.replace(paste_csv_string, paste_csv_string_types)
|
|
||||||
text = text.replace(clipboard, clipboard_types)
|
|
||||||
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
|
|
||||||
text = text.replace(defined_name_list, defined_name_list_types)
|
|
||||||
with open("types.ts") as f:
|
with open("types.ts") as f:
|
||||||
types_str = f.read()
|
types_str = f.read()
|
||||||
header_types = "{}\n\n{}".format(header, types_str)
|
header_types = "{}\n\n{}".format(header, types_str)
|
||||||
|
|
||||||
text = text.replace(header, header_types)
|
text = text.replace(header, header_types)
|
||||||
if text.find("any") != -1:
|
for line in text.splitlines():
|
||||||
print("There are 'unfixed' types. Please check.")
|
line = line.lstrip()
|
||||||
exit(1)
|
# Skip internal methods
|
||||||
|
if line.startswith("readonly model_"):
|
||||||
|
continue
|
||||||
|
if line.find("any") != -1:
|
||||||
|
print("There are 'unfixed' public types. Please check.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
types_file = "pkg/wasm.d.ts"
|
types_file = "pkg/wasm.d.ts"
|
||||||
with open(types_file) as f:
|
with open(types_file) as f:
|
||||||
@@ -243,5 +37,3 @@ if __name__ == "__main__":
|
|||||||
with open(js_file, "wb") as f:
|
with open(js_file, "wb") as f:
|
||||||
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use wasm_bindgen::{
|
|||||||
use ironcalc_base::{
|
use ironcalc_base::{
|
||||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
|
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
|
||||||
types::{CellType, Style},
|
types::{CellType, Style},
|
||||||
|
worksheet::NavigationDirection,
|
||||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ fn to_js_error(error: String) -> JsError {
|
|||||||
|
|
||||||
/// Return an array with a list of all the tokens from a formula
|
/// 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.
|
/// This is used by the UI to color them according to a theme.
|
||||||
#[wasm_bindgen(js_name = "getTokens")]
|
#[wasm_bindgen(js_name = "getTokens", unchecked_return_type = "MarkedToken[]")]
|
||||||
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
||||||
let tokens = tokenizer(formula);
|
let tokens = tokenizer(formula);
|
||||||
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
||||||
@@ -195,24 +196,61 @@ impl Model {
|
|||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "insertRow")]
|
#[wasm_bindgen(js_name = "insertRows")]
|
||||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> {
|
||||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
self.model
|
||||||
|
.insert_rows(sheet, row, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "insertColumn")]
|
#[wasm_bindgen(js_name = "insertColumns")]
|
||||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
pub fn insert_columns(
|
||||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
column_count: i32,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
|
self.model
|
||||||
|
.insert_columns(sheet, column, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "deleteRow")]
|
#[wasm_bindgen(js_name = "deleteRows")]
|
||||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> {
|
||||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
self.model
|
||||||
|
.delete_rows(sheet, row, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "deleteColumn")]
|
#[wasm_bindgen(js_name = "deleteColumns")]
|
||||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
pub fn delete_columns(
|
||||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
column_count: i32,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
|
self.model
|
||||||
|
.delete_columns(sheet, column, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "moveColumn")]
|
||||||
|
pub fn move_column_action(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
delta: i32,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
|
self.model
|
||||||
|
.move_column_action(sheet, column, delta)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "moveRow")]
|
||||||
|
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), JsError> {
|
||||||
|
self.model
|
||||||
|
.move_row_action(sheet, row, delta)
|
||||||
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setRowsHeight")]
|
#[wasm_bindgen(js_name = "setRowsHeight")]
|
||||||
@@ -338,7 +376,7 @@ impl Model {
|
|||||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
||||||
pub fn update_range_style(
|
pub fn update_range_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
range: JsValue,
|
#[wasm_bindgen(unchecked_param_type = "Area")] range: JsValue,
|
||||||
style_path: &str,
|
style_path: &str,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
@@ -349,7 +387,7 @@ impl Model {
|
|||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getCellStyle")]
|
#[wasm_bindgen(js_name = "getCellStyle", unchecked_return_type = "CellStyle")]
|
||||||
pub fn get_cell_style(
|
pub fn get_cell_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
@@ -365,7 +403,10 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onPasteStyles")]
|
#[wasm_bindgen(js_name = "onPasteStyles")]
|
||||||
pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> {
|
pub fn on_paste_styles(
|
||||||
|
&mut self,
|
||||||
|
#[wasm_bindgen(unchecked_param_type = "CellStyle[][]")] styles: JsValue,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
let styles: &Vec<Vec<Style>> =
|
let styles: &Vec<Vec<Style>> =
|
||||||
&serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?;
|
&serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
self.model.on_paste_styles(styles).map_err(to_js_error)
|
self.model.on_paste_styles(styles).map_err(to_js_error)
|
||||||
@@ -391,7 +432,10 @@ impl Model {
|
|||||||
|
|
||||||
// I don't _think_ serializing to JsValue can't fail
|
// I don't _think_ serializing to JsValue can't fail
|
||||||
// FIXME: Remove this clippy directive
|
// FIXME: Remove this clippy directive
|
||||||
#[wasm_bindgen(js_name = "getWorksheetsProperties")]
|
#[wasm_bindgen(
|
||||||
|
js_name = "getWorksheetsProperties",
|
||||||
|
unchecked_return_type = "WorksheetProperties[]"
|
||||||
|
)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
pub fn get_worksheets_properties(&self) -> JsValue {
|
||||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
||||||
@@ -410,7 +454,7 @@ impl Model {
|
|||||||
|
|
||||||
// I don't _think_ serializing to JsValue can't fail
|
// I don't _think_ serializing to JsValue can't fail
|
||||||
// FIXME: Remove this clippy directive
|
// FIXME: Remove this clippy directive
|
||||||
#[wasm_bindgen(js_name = "getSelectedView")]
|
#[wasm_bindgen(js_name = "getSelectedView", unchecked_return_type = "SelectedView")]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
pub fn get_selected_view(&self) -> JsValue {
|
pub fn get_selected_view(&self) -> JsValue {
|
||||||
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
||||||
@@ -469,7 +513,11 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
#[wasm_bindgen(js_name = "autoFillRows")]
|
||||||
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
pub fn auto_fill_rows(
|
||||||
|
&mut self,
|
||||||
|
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
|
||||||
|
to_row: i32,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
let area: Area =
|
let area: Area =
|
||||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
self.model
|
self.model
|
||||||
@@ -480,7 +528,7 @@ impl Model {
|
|||||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
#[wasm_bindgen(js_name = "autoFillColumns")]
|
||||||
pub fn auto_fill_columns(
|
pub fn auto_fill_columns(
|
||||||
&mut self,
|
&mut self,
|
||||||
source_area: JsValue,
|
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
|
||||||
to_column: i32,
|
to_column: i32,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
let area: Area =
|
let area: Area =
|
||||||
@@ -520,6 +568,20 @@ impl Model {
|
|||||||
self.model.on_page_up().map_err(to_js_error)
|
self.model.on_page_up().map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "onNavigateToEdgeInDirection")]
|
||||||
|
pub fn on_navigate_to_edge_in_direction(&mut self, direction: &str) -> Result<(), JsError> {
|
||||||
|
let direction = match direction {
|
||||||
|
"ArrowLeft" => NavigationDirection::Left,
|
||||||
|
"ArrowRight" => NavigationDirection::Right,
|
||||||
|
"ArrowUp" => NavigationDirection::Up,
|
||||||
|
"ArrowDown" => NavigationDirection::Down,
|
||||||
|
_ => return Err(JsError::new(&format!("Invalid direction: {direction}"))),
|
||||||
|
};
|
||||||
|
self.model
|
||||||
|
.on_navigate_to_edge_in_direction(direction)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setWindowWidth")]
|
#[wasm_bindgen(js_name = "setWindowWidth")]
|
||||||
pub fn set_window_width(&mut self, window_width: f64) {
|
pub fn set_window_width(&mut self, window_width: f64) {
|
||||||
self.model.set_window_width(window_width);
|
self.model.set_window_width(window_width);
|
||||||
@@ -561,8 +623,8 @@ impl Model {
|
|||||||
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
||||||
pub fn set_area_with_border(
|
pub fn set_area_with_border(
|
||||||
&mut self,
|
&mut self,
|
||||||
area: JsValue,
|
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
|
||||||
border_area: JsValue,
|
#[wasm_bindgen(unchecked_param_type = "BorderArea")] border_area: JsValue,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
let range: Area =
|
let range: Area =
|
||||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
@@ -589,7 +651,7 @@ impl Model {
|
|||||||
self.model.set_name(name);
|
self.model.set_name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "copyToClipboard")]
|
#[wasm_bindgen(js_name = "copyToClipboard", unchecked_return_type = "Clipboard")]
|
||||||
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
|
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
|
||||||
let data = self
|
let data = self
|
||||||
.model
|
.model
|
||||||
@@ -603,8 +665,9 @@ impl Model {
|
|||||||
pub fn paste_from_clipboard(
|
pub fn paste_from_clipboard(
|
||||||
&mut self,
|
&mut self,
|
||||||
source_sheet: u32,
|
source_sheet: u32,
|
||||||
|
#[wasm_bindgen(unchecked_param_type = "[number, number, number, number]")]
|
||||||
source_range: JsValue,
|
source_range: JsValue,
|
||||||
clipboard: JsValue,
|
#[wasm_bindgen(unchecked_param_type = "ClipboardData")] clipboard: JsValue,
|
||||||
is_cut: bool,
|
is_cut: bool,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
let source_range: (i32, i32, i32, i32) =
|
let source_range: (i32, i32, i32, i32) =
|
||||||
@@ -617,7 +680,11 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "pasteCsvText")]
|
#[wasm_bindgen(js_name = "pasteCsvText")]
|
||||||
pub fn paste_csv_string(&mut self, area: JsValue, csv: &str) -> Result<(), JsError> {
|
pub fn paste_csv_string(
|
||||||
|
&mut self,
|
||||||
|
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
|
||||||
|
csv: &str,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
let range: Area =
|
let range: Area =
|
||||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
self.model
|
self.model
|
||||||
@@ -625,7 +692,10 @@ impl Model {
|
|||||||
.map_err(|e| to_js_error(e.to_string()))
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getDefinedNameList")]
|
#[wasm_bindgen(
|
||||||
|
js_name = "getDefinedNameList",
|
||||||
|
unchecked_return_type = "DefinedName[]"
|
||||||
|
)]
|
||||||
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
|
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
|
||||||
let data: Vec<DefinedName> = self
|
let data: Vec<DefinedName> = self
|
||||||
.model
|
.model
|
||||||
@@ -672,4 +742,45 @@ impl Model {
|
|||||||
.delete_defined_name(name, scope)
|
.delete_defined_name(name, scope)
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "getLastNonEmptyInRowBeforeColumn")]
|
||||||
|
pub fn get_last_non_empty_in_row_before_column(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<Option<i32>, JsError> {
|
||||||
|
self.model
|
||||||
|
.get_last_non_empty_in_row_before_column(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "getFirstNonEmptyInRowAfterColumn")]
|
||||||
|
pub fn get_first_non_empty_in_row_after_column(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<Option<i32>, JsError> {
|
||||||
|
self.model
|
||||||
|
.get_first_non_empty_in_row_after_column(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(
|
||||||
|
js_name = "getCellArrayStructure",
|
||||||
|
unchecked_return_type = "CellArrayStructure"
|
||||||
|
)]
|
||||||
|
pub fn get_cell_array_structure(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<JsValue, JsError> {
|
||||||
|
let cell_structure = self
|
||||||
|
.model
|
||||||
|
.get_cell_array_structure(sheet, row, column)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
serde_wasm_bindgen::to_value(&cell_structure).map_err(JsError::from)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,5 +130,82 @@ test("autofill", () => {
|
|||||||
assert.strictEqual(result, "23");
|
assert.strictEqual(result, "23");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('insertRows shifts cells', () => {
|
||||||
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
|
model.setUserInput(0, 1, 1, '42');
|
||||||
|
model.insertRows(0, 1, 1);
|
||||||
|
|
||||||
|
assert.strictEqual(model.getCellContent(0, 1, 1), '');
|
||||||
|
assert.strictEqual(model.getCellContent(0, 2, 1), '42');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('insertColumns shifts cells', () => {
|
||||||
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
|
model.setUserInput(0, 1, 1, 'A');
|
||||||
|
model.setUserInput(0, 1, 2, 'B');
|
||||||
|
|
||||||
|
model.insertColumns(0, 2, 1);
|
||||||
|
|
||||||
|
assert.strictEqual(model.getCellContent(0, 1, 2), '');
|
||||||
|
assert.strictEqual(model.getCellContent(0, 1, 3), 'B');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteRows removes cells', () => {
|
||||||
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
|
model.setUserInput(0, 1, 1, '1');
|
||||||
|
model.setUserInput(0, 2, 1, '2');
|
||||||
|
|
||||||
|
model.deleteRows(0, 1, 1);
|
||||||
|
|
||||||
|
assert.strictEqual(model.getCellContent(0, 1, 1), '2');
|
||||||
|
assert.strictEqual(model.getCellContent(0, 2, 1), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteColumns removes cells', () => {
|
||||||
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
|
model.setUserInput(0, 1, 1, 'A');
|
||||||
|
model.setUserInput(0, 1, 2, 'B');
|
||||||
|
|
||||||
|
model.deleteColumns(0, 1, 1);
|
||||||
|
|
||||||
|
assert.strictEqual(model.getCellContent(0, 1, 1), 'B');
|
||||||
|
assert.strictEqual(model.getCellContent(0, 1, 2), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("move row", () => {
|
||||||
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
|
model.setUserInput(0, 3, 5, "=G3");
|
||||||
|
model.setUserInput(0, 4, 5, "=G4");
|
||||||
|
model.setUserInput(0, 5, 5, "=SUM(G3:J3)");
|
||||||
|
model.setUserInput(0, 6, 5, "=SUM(G3:G3)");
|
||||||
|
model.setUserInput(0, 7, 5, "=SUM(G4:G4)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
model.moveRow(0, 3, 1);
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert.strictEqual(model.getCellContent(0, 3, 5), "=G3");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 4, 5), "=G4");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 5, 5), "=SUM(G4:J4)");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 6, 5), "=SUM(G4:G4)");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 7, 5), "=SUM(G3:G3)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("move column", () => {
|
||||||
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
|
model.setUserInput(0, 3, 5, "=G3");
|
||||||
|
model.setUserInput(0, 4, 5, "=H3");
|
||||||
|
model.setUserInput(0, 5, 5, "=SUM(G3:J7)");
|
||||||
|
model.setUserInput(0, 6, 5, "=SUM(G3:G7)");
|
||||||
|
model.setUserInput(0, 7, 5, "=SUM(H3:H7)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
model.moveColumn(0, 7, 1);
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert.strictEqual(model.getCellContent(0, 3, 5), "=H3");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 4, 5), "=G3");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 5, 5), "=SUM(H3:J7)");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 6, 5), "=SUM(H3:H7)");
|
||||||
|
assert.strictEqual(model.getCellContent(0, 7, 5), "=SUM(G3:G7)");
|
||||||
|
});
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ export interface MarkedToken {
|
|||||||
end: number;
|
end: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CellArrayStructure =
|
||||||
|
| "SingleCell"
|
||||||
|
| { DynamicChild: [number, number, number, number] }
|
||||||
|
| { DynamicMother: [number, number] };
|
||||||
|
|
||||||
export interface WorksheetProperties {
|
export interface WorksheetProperties {
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
@@ -216,7 +221,7 @@ export interface SelectedView {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
// type ClipboardData = Record<string, Record <string, ClipboardCell>>;
|
// type ClipboardData = Record<string, Record <string, ClipboardCell>>;
|
||||||
type ClipboardData = Map<number, Map <number, ClipboardCell>>;
|
type ClipboardData = Map<number, Map<number, ClipboardCell>>;
|
||||||
|
|
||||||
export interface ClipboardCell {
|
export interface ClipboardCell {
|
||||||
text: string;
|
text: string;
|
||||||
|
|||||||
1808
docs/package-lock.json
generated
1808
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"vitepress": "^1.5.0",
|
"vitepress": "^v2.0.0-alpha.8",
|
||||||
"vue": "^3.5.12"
|
"vue": "^3.5.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,57 @@ lang: en-US
|
|||||||
|
|
||||||
# Keyboard Shortcuts
|
# Keyboard Shortcuts
|
||||||
|
|
||||||
From within your keyboard you can navigate and interact with the spreadsheet. This is a fairly interesting feature for power users.
|
From within your keyboard you can navigate and interact with the spreadsheet.
|
||||||
|
|
||||||
## Common Actions
|
## Navigation Shortcuts
|
||||||
|
|
||||||
| Action | Windows | Mac |
|
| Action | Linux/Windows | Mac |
|
||||||
| ------ | -------- | ----- |
|
| ----------------------- | ----------------- | -------------- |
|
||||||
| Copy | ctrl + c | ⌘ + c |
|
| Move one cell | Arrow Keys | Arrow Keys |
|
||||||
| Paste | ctrl + v | ⌘ + v |
|
| Move down (Excel-style) | Enter | Enter |
|
||||||
| Cut | ctrl + x | ⌘ + x |
|
| Move right | Tab | Tab |
|
||||||
| Undo | ctrl + z | ⌘ + z |
|
| Move left | Shift + Tab | Shift + Tab |
|
||||||
| Redo | ctrl + y | ⌘ + y |
|
| Jump to first column | Home | Fn + ← |
|
||||||
|
| Jump to last column | End | Fn + → |
|
||||||
|
| Scroll one screen down | PageDown | Fn + ↓ |
|
||||||
|
| Scroll one screen up | PageUp | Fn + ↑ |
|
||||||
|
| Jump to cell edge | Ctrl + Arrow Keys | ⌘ + Arrow Keys |
|
||||||
|
| Jump to start of sheet | Ctrl + Home | ⌘ + Fn + ← |
|
||||||
|
| Jump to end of sheet | Ctrl + End | ⌘ + Fn + → |
|
||||||
|
|
||||||
## Navigation
|
## Selection & Editing
|
||||||
|
|
||||||
| <div style="width:200px">Action</div> | <div style="width:80px">Windows</div> | <div style="width:80px">Mac</div> |
|
| Action | Linux/Windows | Mac |
|
||||||
| ------------------------------------- | ------------------------------------- | --------------------------------- |
|
| -------------------- | ------------------ | ------------------ |
|
||||||
| Move to beginning of row | ??? | Fn + Left Arrow |
|
| Expand selection | Shift + Arrow Keys | Shift + Arrow Keys |
|
||||||
| Move to end of row | ??? | Fn + Right Arrow |
|
| Start editing a cell | F2 | F2 |
|
||||||
| Move to previous sheet | Option + Arrow Up | Option + Arrow Up |
|
| Edit directly | Any key | Any key |
|
||||||
| Move to next sheet | Option + Arrow Down | Option + Arrow Down |
|
|
||||||
|
## Text Styling
|
||||||
|
|
||||||
|
| Action | Linux/Windows | Mac |
|
||||||
|
| --------- | -------------- | ----- |
|
||||||
|
| Bold | Ctrl + B | ⌘ + B |
|
||||||
|
| Italic | Ctrl + I | ⌘ + I |
|
||||||
|
| Underline | Ctrl + U | ⌘ + U |
|
||||||
|
|
||||||
|
## Undo / Redo
|
||||||
|
|
||||||
|
| Action | Linux/Windows | Mac |
|
||||||
|
| ------ | ---------------------------- | -----------------------|
|
||||||
|
| Undo | Ctrl + Z | ⌘ + Z |
|
||||||
|
| Redo | Ctrl + Y or Ctrl + Shift + Z | ⌘ + Shift + Z or ⌘ + Y |
|
||||||
|
|
||||||
|
## Sheet Navigation
|
||||||
|
|
||||||
|
| Action | Linux/Windows | Mac |
|
||||||
|
| -------------- | ---------------- | -------------- |
|
||||||
|
| Next sheet | Alt + Arrow Down | ⌥ + Arrow Down |
|
||||||
|
| Previous sheet | Alt + Arrow Up | ⌥ + Arrow Up |
|
||||||
|
|
||||||
|
## Miscellaneous
|
||||||
|
|
||||||
|
| Action | Linux/Windows/Mac |
|
||||||
|
| ------------- | -------------------- |
|
||||||
|
| Cancel action | Escape |
|
||||||
|
| Delete cells | Delete |
|
||||||
|
|||||||
BIN
docs/src/functions/images/arccosine-curve.png
Normal file
BIN
docs/src/functions/images/arccosine-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/src/functions/images/arcsine-curve.png
Normal file
BIN
docs/src/functions/images/arcsine-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/src/functions/images/arctangent-curve.png
Normal file
BIN
docs/src/functions/images/arctangent-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/src/functions/images/hyperboliccosine-curve.png
Normal file
BIN
docs/src/functions/images/hyperboliccosine-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/src/functions/images/hyperbolicsine-curve.png
Normal file
BIN
docs/src/functions/images/hyperbolicsine-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/src/functions/images/hyperbolictangent-curve.png
Normal file
BIN
docs/src/functions/images/hyperbolictangent-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -12,15 +12,15 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| Function | Status | Documentation |
|
| Function | Status | Documentation |
|
||||||
| --------------- | ---------------------------------------------- | ------------- |
|
| --------------- | ---------------------------------------------- | ------------- |
|
||||||
| ABS | <Badge type="tip" text="Available" /> | – |
|
| ABS | <Badge type="tip" text="Available" /> | – |
|
||||||
| ACOS | <Badge type="tip" text="Available" /> | – |
|
| ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) |
|
||||||
| ACOSH | <Badge type="tip" text="Available" /> | – |
|
| ACOSH | <Badge type="tip" text="Available" /> | – |
|
||||||
| ACOT | <Badge type="info" text="Not implemented yet" /> | – |
|
| ACOT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ACOTH | <Badge type="info" text="Not implemented yet" /> | – |
|
| ACOTH | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | – |
|
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ARABIC | <Badge type="info" text="Not implemented yet" /> | – |
|
| ARABIC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ASIN | <Badge type="tip" text="Available" /> | – |
|
| ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) |
|
||||||
| ASINH | <Badge type="tip" text="Available" /> | – |
|
| ASINH | <Badge type="tip" text="Available" /> | – |
|
||||||
| ATAN | <Badge type="tip" text="Available" /> | – |
|
| ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) |
|
||||||
| ATAN2 | <Badge type="tip" text="Available" /> | – |
|
| ATAN2 | <Badge type="tip" text="Available" /> | – |
|
||||||
| ATANH | <Badge type="tip" text="Available" /> | – |
|
| ATANH | <Badge type="tip" text="Available" /> | – |
|
||||||
| BASE | <Badge type="info" text="Not implemented yet" /> | – |
|
| BASE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
@@ -30,7 +30,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| COMBIN | <Badge type="info" text="Not implemented yet" /> | – |
|
| COMBIN | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COMBINA | <Badge type="info" text="Not implemented yet" /> | – |
|
| COMBINA | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
|
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
|
||||||
| COSH | <Badge type="tip" text="Available" /> | – |
|
| COSH | <Badge type="tip" text="Available" /> | [COSH](math_and_trigonometry/cosh) |
|
||||||
| COT | <Badge type="info" text="Not implemented yet" /> | – |
|
| COT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COTH | <Badge type="info" text="Not implemented yet" /> | – |
|
| COTH | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| CSC | <Badge type="info" text="Not implemented yet" /> | – |
|
| CSC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
@@ -78,7 +78,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | – |
|
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SIGN | <Badge type="info" text="Not implemented yet" /> | – |
|
| SIGN | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
|
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
|
||||||
| SINH | <Badge type="tip" text="Available" /> | – |
|
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
|
||||||
| SQRT | <Badge type="tip" text="Available" /> | – |
|
| SQRT | <Badge type="tip" text="Available" /> | – |
|
||||||
| SQRTPI | <Badge type="info" text="Not implemented yet" /> | – |
|
| SQRTPI | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
@@ -91,5 +91,5 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) |
|
| TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) |
|
||||||
| TANH | <Badge type="tip" text="Available" /> | – |
|
| TANH | <Badge type="tip" text="Available" /> | [TANH](math_and_trigonometry/tanh) |
|
||||||
| TRUNC | <Badge type="info" text="Not implemented yet" /> | – |
|
| TRUNC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
|
|||||||
@@ -4,8 +4,35 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# ACOS
|
# ACOS function
|
||||||
|
## Overview
|
||||||
|
ACOS is a function of the Math and Trigonometry category that calculates the inverse cosine (arccosine) of a number in the range [-1 to 1], returning an angle in the range [0 to $\pi$], expressed in radians.
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**ACOS(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">acos</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The number whose arccosine is to be calculated, in the range [-1 to 1].
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
### Returned value
|
||||||
|
ACOS returns a number in radians in the range [0 to $\pi$] that is the angle whose cosine is the specified number.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, ACOS propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then ACOS returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ACOS returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the value of the *number* argument lies outside the range [-1 to 1], then ACOS returns the [`#NUM!`](/features/error-types.md#num) error.
|
||||||
|
* For some argument values, ACOS may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The ACOS function utilizes the *acos()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the output of the ACOS function for angles $x$ in the range -1 to +1.
|
||||||
|
<center><img src="/functions/images/arccosine-curve.png" width="350" alt="Graph showing acos(x) for x between -1 and +1."></center>
|
||||||
|
|
||||||
::: warning
|
## Examples
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=acos).
|
||||||
:::
|
|
||||||
|
## Links
|
||||||
|
* For more information about inverse trigonometric functions, visit Wikipedia's [Inverse trigonometric functions](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) page.
|
||||||
|
* See also IronCalc's [SIN](/functions/math_and_trigonometry/sin), [COS](/functions/math_and_trigonometry/cos) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [ACOS function](https://support.microsoft.com/en-us/office/acos-function-cb73173f-d089-4582-afa1-76e5524b5d5b) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093461) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ACOS) provide versions of the ACOS function.
|
||||||
|
|||||||
@@ -4,8 +4,37 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# ASIN
|
# ASIN function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
ASIN is a function of the Math and Trigonometry category that calculates the inverse sine (arcsine) of a number in the range [-1 to +1], returning an angle in the range [-$\pi$/2 to +$\pi$/2], expressed in radians.
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**ASIN(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">asin</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The number whose arcsine is to be calculated, in the range [-1 to +1].
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
### Returned value
|
||||||
|
ASIN returns a number in radians in the range [-$\pi$/2 to +$\pi$/2] that is the angle whose sine is the specified number.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, ASIN propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then ASIN returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ASIN returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the value of the *number* argument lies outside the range [-1 to +1], then ASIN returns the [`#NUM!`](/features/error-types.md#num) error.
|
||||||
|
* For some argument values, ASIN may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The ASIN function utilizes the *asin()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the output of the ASIN function for angles $x$ in the range -1 to +1 radians.
|
||||||
|
<center><img src="/functions/images/arcsine-curve.png" width="350" alt="Graph showing sin(x) for x between -2π and +2π."></center>
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=asin).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about inverse trigonometric functions, visit Wikipedia's [Inverse trigonometric functions](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) page.
|
||||||
|
* See also IronCalc's [SIN](/functions/math_and_trigonometry/sin), [COS](/functions/math_and_trigonometry/cos) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [ASIN function](https://support.microsoft.com/en-us/office/asin-function-81fb95e5-6d6f-48c4-bc45-58f955c6d347) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093464) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ASIN) provide versions of the ASIN function.
|
||||||
|
|||||||
@@ -4,8 +4,34 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# ATAN
|
# ATAN function
|
||||||
|
## Overview
|
||||||
|
ATAN is a function of the Math and Trigonometry category that calculates the inverse tangent (arctangent) of a number, returning an angle in the range [-$\pi$/2 to +$\pi$/2], expressed in radians.
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**ATAN (<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">atan</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The number whose arctangent is to be calculated, in the range [-$\infty$, +$\infty$].
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
### Returned value
|
||||||
|
ATAN returns a number in radians in the range [-$\pi$/2 to +$\pi$/2] that is the angle whose tangent is the specified number.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, ATAN propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then ATAN returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ATAN returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, ATAN may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The ATAN function utilizes the *atan()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the output of the ATAN function for angles $x$ in the range [-$\infty$, +$\infty$].
|
||||||
|
<center><img src="/functions/images/arctangent-curve.png" width="350" alt="Graph showing atan(x) for x between [-$\infty$, +$\infty$]."></center>
|
||||||
|
|
||||||
::: warning
|
## Examples
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=atan).
|
||||||
:::
|
|
||||||
|
## Links
|
||||||
|
* For more information about inverse trigonometric functions, visit Wikipedia's [Inverse trigonometric functions](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) page.
|
||||||
|
* See also IronCalc's [SIN](/functions/math_and_trigonometry/sin), [COS](/functions/math_and_trigonometry/cos) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [ATAN function](https://support.microsoft.com/en-us/office/atan-function-50746fa8-630a-406b-81d0-4a2aed395543) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093395) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ATAN) provide versions of the ATAN function.
|
||||||
|
|||||||
@@ -3,9 +3,37 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# COSH function
|
||||||
|
## Overview
|
||||||
|
COSH is a function of the Math and Trigonometry category that calculates the hyperbolic cosine of a number.
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**COSH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">cosh</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The hyperbolic angle whose hyperbolic cosine is to be calculated, expressed in radians.
|
||||||
|
### Additional guidance
|
||||||
|
The formula for the hyperbolic cosine is:
|
||||||
|
$$
|
||||||
|
\text{cosh(x)} = \dfrac{e^x+e^{-x}}{2}
|
||||||
|
$$
|
||||||
|
### Returned value
|
||||||
|
COSH returns a real [number](/features/value-types#numbers) that is the hyperbolic cosine of the specified hyperbolic angle.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, COSH propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then COSH returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then COSH returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, COSH may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The COSH function utilizes the *cosh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the COSH function.
|
||||||
|
<center><img src="/functions/images/hyperboliccosine-curve.png" width="350" alt="Graph showing cosh(x)."></center>
|
||||||
|
|
||||||
# COSH
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=cosh).
|
||||||
|
|
||||||
::: warning
|
## Links
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
* For more information about hyperbolic functions, visit Wikipedia's [Hyperbolic functions](https://en.wikipedia.org/wiki/Hyperbolic_functions) page.
|
||||||
:::
|
* See also IronCalc's [SINH](/functions/math_and_trigonometry/sinh), [COS](/functions/math_and_trigonometry/cos) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [COSH function](https://support.microsoft.com/en-us/office/cosh-function-e460d426-c471-43e8-9540-a57ff3b70555) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093477) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/COSH) provide versions of the COSH function.
|
||||||
@@ -3,9 +3,37 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# SINH function
|
||||||
|
## Overview
|
||||||
|
SINH is a function of the Math and Trigonometry category that calculates the hyperbolic sine of a number.
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**SINH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">sinh</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The hyperbolic angle whose hyperbolic sine is to be calculated, expressed in radians.
|
||||||
|
### Additional guidance
|
||||||
|
The formula for the hyperbolic sine is:
|
||||||
|
$$
|
||||||
|
\text{sinh(x)} = \dfrac{e^x-e^{-x}}{2}
|
||||||
|
$$
|
||||||
|
### Returned value
|
||||||
|
SINH returns a real [number](/features/value-types#numbers) that is the hyperbolic sine of the specified hyperbolic angle.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, SINH propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then SINH returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then SINH returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, SINH may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The SINH function utilizes the *sinh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the SINH function.
|
||||||
|
<center><img src="/functions/images/hyperbolicsine-curve.png" width="350" alt="Graph showing sinh(x)."></center>
|
||||||
|
|
||||||
# SINH
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=sinh).
|
||||||
|
|
||||||
::: warning
|
## Links
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
* For more information about hyperbolic functions, visit Wikipedia's [Hyperbolic functions](https://en.wikipedia.org/wiki/Hyperbolic_functions) page.
|
||||||
:::
|
* See also IronCalc's [SIN](/functions/math_and_trigonometry/sin), [COS](/functions/math_and_trigonometry/cos) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [SINH function](https://support.microsoft.com/en-us/office/sinh-function-4958f7e2-0d2b-4846-8ef5-8475f3aea5fb) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093517) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/SINH) provide versions of the SINH function.
|
||||||
@@ -33,7 +33,7 @@ TAN returns a unitless [number](/features/value-types#numbers) that is the trigo
|
|||||||
* The figure below illustrates the output of the TAN function for angles $x$ in the range -2$π$ to +2$π$.
|
* The figure below illustrates the output of the TAN function for angles $x$ in the range -2$π$ to +2$π$.
|
||||||
<center><img src="/functions/images/tangent-curve.png" width="350" alt="Graph showing tan(x) for x between -2π and +2π."></center>
|
<center><img src="/functions/images/tangent-curve.png" width="350" alt="Graph showing tan(x) for x between -2π and +2π."></center>
|
||||||
|
|
||||||
* Theoretically, $\text{tan}(x)$ is undefined for any critical $x$ that satisfies $x = \frac{\pi}{2} + k\pi$ (where $k$ is any integer). However, an exact representation of the mathmatical constant $\pi$ requires infinite precision, which cannot be achieved with the floating-point representation available. Hence, TAN will return very large or very small values close to critical $x$ values.
|
* Theoretically, $\text{tan}(x)$ is undefined for any critical $x$ that satisfies $x = \frac{\pi}{2} + k\pi$ (where $k$ is any integer). However, an exact representation of the mathematical constant $\pi$ requires infinite precision, which cannot be achieved with the floating-point representation available. Hence, TAN will return very large or very small values close to critical $x$ values.
|
||||||
## Examples
|
## Examples
|
||||||
[See some examples in IronCalc](https://app.ironcalc.com/?example=tan).
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=tan).
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,37 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# TANH function
|
||||||
|
## Overview
|
||||||
|
TANH is a function of the Math and Trigonometry category that calculates the hyperbolic tangent of a number.
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**TANH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">tanh</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The hyperbolic angle whose hyperbolic tangent is to be calculated, expressed in radians.
|
||||||
|
### Additional guidance
|
||||||
|
The formula for the hyperbolic tangent is:
|
||||||
|
$$
|
||||||
|
\text{tanh(x)} = \dfrac{sinh(x)}{cosh(x)} = \dfrac{e^x-e^{-x}}{e^x+e^{-x}}
|
||||||
|
$$
|
||||||
|
### Returned value
|
||||||
|
TANH returns a real [number](/features/value-types#numbers) in the range (-1,+1) that is the hyperbolic tangent of the specified hyperbolic angle.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, TANH propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then TANH returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then TANH returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, TANH may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The TANH function utilizes the *tanh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the TANH function.
|
||||||
|
<center><img src="/functions/images/hyperbolictangent-curve.png" width="350" alt="Graph showing tanh(x)."></center>
|
||||||
|
|
||||||
# TANH
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=tanh).
|
||||||
|
|
||||||
::: warning
|
## Links
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
* For more information about hyperbolic functions, visit Wikipedia's [Hyperbolic functions](https://en.wikipedia.org/wiki/Hyperbolic_functions) page.
|
||||||
:::
|
* See also IronCalc's [SINH](/functions/math_and_trigonometry/sinh), [COSH](/functions/math_and_trigonometry/cosh) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [TANH function](https://support.microsoft.com/en-us/office/tanh-function-017222f0-a0c3-4f69-9787-b3202295dc6c) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093755) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/TANH) provide versions of the TANH function.
|
||||||
1162
webapp/IronCalc/package-lock.json
generated
1162
webapp/IronCalc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user