Compare commits
2 Commits
v0.6.0
...
feature/dy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945897a455 | ||
|
|
6f577575c7 |
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/"
|
||||
with:
|
||||
command: upload
|
||||
args: "--skip-existing **/*.whl **/*.tar.gz"
|
||||
args: "--skip-existing **/*.whl"
|
||||
working-directory: bindings/python
|
||||
|
||||
publish-pypi:
|
||||
@@ -137,5 +137,5 @@ jobs:
|
||||
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
|
||||
with:
|
||||
command: upload
|
||||
args: "--skip-existing **/*.whl **/*.tar.gz"
|
||||
args: "--skip-existing **/*.whl"
|
||||
working-directory: bindings/python
|
||||
|
||||
69
Cargo.lock
generated
69
Cargo.lock
generated
@@ -414,7 +414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bitcode",
|
||||
"chrono",
|
||||
@@ -430,13 +430,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc_base"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bitcode",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"csv",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
@@ -447,7 +448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc_nodejs"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"ironcalc",
|
||||
"napi",
|
||||
@@ -720,10 +721,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.25.0"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
|
||||
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
"libc",
|
||||
"memoffset",
|
||||
@@ -737,9 +739,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.25.0"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
|
||||
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
@@ -747,9 +749,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.25.0"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
|
||||
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@@ -757,9 +759,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.25.0"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
|
||||
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@@ -769,9 +771,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.25.0"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
|
||||
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -782,9 +784,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyroncalc"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bitcode",
|
||||
"ironcalc",
|
||||
"pyo3",
|
||||
"serde",
|
||||
@@ -871,12 +872,6 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
@@ -984,9 +979,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.2"
|
||||
version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
@@ -1075,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"ironcalc_base",
|
||||
"serde",
|
||||
@@ -1086,24 +1081,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -1124,9 +1118,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1134,9 +1128,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1147,12 +1141,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
|
||||
93
Dockerfile
93
Dockerfile
@@ -1,93 +0,0 @@
|
||||
FROM rust:latest AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Tools + wasm toolchain + Node via nvm
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
bash curl ca-certificates make \
|
||||
&& rustup target add wasm32-unknown-unknown \
|
||||
&& cargo install wasm-pack \
|
||||
&& bash -lc "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash -" \
|
||||
&& bash -lc '\
|
||||
export NVM_DIR="$HOME/.nvm" && \
|
||||
source "$NVM_DIR/nvm.sh" && \
|
||||
nvm install 22 && nvm alias default 22 && \
|
||||
nroot="$NVM_DIR/versions/node/$(nvm version default)/bin" && \
|
||||
ln -sf "$nroot/node" /usr/local/bin/node && \
|
||||
ln -sf "$nroot/npm" /usr/local/bin/npm && \
|
||||
ln -sf "$nroot/npx" /usr/local/bin/npx \
|
||||
' \
|
||||
&& npm install typescript \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# build the server
|
||||
RUN cargo build --release --manifest-path webapp/app.ironcalc.com/server/Cargo.toml
|
||||
|
||||
# build the wasm
|
||||
RUN make -C bindings/wasm
|
||||
|
||||
# build the widget
|
||||
WORKDIR /app/webapp/IronCalc
|
||||
RUN npm install && npm run build
|
||||
|
||||
# build the frontend app
|
||||
WORKDIR /app/webapp/app.ironcalc.com/frontend
|
||||
RUN npm install && npm run build
|
||||
|
||||
# build the xlsx_2_icalc binary (we don't need the release version here)
|
||||
WORKDIR /app/xlsx
|
||||
RUN cargo build
|
||||
|
||||
WORKDIR /app
|
||||
# copy the artifacts to a dist/ directory
|
||||
RUN mkdir dist
|
||||
RUN mkdir dist/frontend
|
||||
RUN cp -r webapp/app.ironcalc.com/frontend/dist/* dist/frontend/
|
||||
RUN mkdir dist/server
|
||||
RUN cp webapp/app.ironcalc.com/server/target/release/ironcalc_server dist/server/
|
||||
RUN cp webapp/app.ironcalc.com/server/Rocket.toml dist/server/
|
||||
RUN cp webapp/app.ironcalc.com/server/ironcalc.sqlite dist/server/
|
||||
|
||||
# Create ic files in docs
|
||||
RUN mkdir -p dist/frontend/models
|
||||
|
||||
# Loop over all xlsx files in xlsx/tests/docs & templates and convert them to .ic
|
||||
RUN bash -lc 'set -euo pipefail; \
|
||||
mkdir -p dist/frontend/models; \
|
||||
shopt -s nullglob; \
|
||||
for xlsx_file in xlsx/tests/docs/*.xlsx; do \
|
||||
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
|
||||
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
|
||||
done; \
|
||||
for xlsx_file in xlsx/tests/templates/*.xlsx; do \
|
||||
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
|
||||
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
|
||||
done'
|
||||
|
||||
# ---------- server runtime ----------
|
||||
FROM debian:bookworm-slim AS server-runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
# Copy EVERYTHING you put in dist/server (binary + Rocket.toml + DB)
|
||||
COPY --from=builder /app/dist/server/ ./
|
||||
# Make sure Rocket binds to the container IP; explicitly point to the config file
|
||||
ENV ROCKET_ADDRESS=0.0.0.0 \
|
||||
ROCKET_PORT=8000 \
|
||||
ROCKET_CONFIG=/app/Rocket.toml
|
||||
EXPOSE 8000
|
||||
# Run from /app so relative paths in Rocket.toml/DB work
|
||||
CMD ["./ironcalc_server"]
|
||||
|
||||
# ---------- caddy runtime (serves frontend + reverse-proxy /api) ----------
|
||||
FROM caddy:latest AS caddy-runtime
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
# Copy the frontend build output to /srv
|
||||
COPY --from=builder /app/dist/frontend/ /srv/
|
||||
|
||||
# Copy the Caddyfile
|
||||
COPY --from=builder /app/webapp/app.ironcalc.com/Caddyfile.compose /etc/caddy/Caddyfile
|
||||
|
||||
14
README.md
14
README.md
@@ -31,17 +31,7 @@ This repository contains the main engine and the xlsx reader and writer.
|
||||
|
||||
Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go.
|
||||
|
||||
We will build different _skins_: in the terminal, as a desktop application or use it in your own web application.
|
||||
|
||||
# Docker
|
||||
|
||||
If you have docker installed just run:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
head over to <http://localhost:2080> to test the application.
|
||||
We will build different _skins_: in the terminal, as a desktop application or use it in you own web application.
|
||||
|
||||
# Building
|
||||
|
||||
@@ -94,7 +84,7 @@ And then use this code in `main.rs`:
|
||||
|
||||
```rust
|
||||
use ironcalc::{
|
||||
base::{expressions::utils::number_to_column, Model},
|
||||
base::{expressions::utils::number_to_column, model::Model},
|
||||
export::save_to_xlsx,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ironcalc_base"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
homepage = "https://www.ironcalc.com"
|
||||
@@ -17,6 +17,7 @@ chrono = "0.4"
|
||||
chrono-tz = "0.10"
|
||||
regex = { version = "1.0", optional = true}
|
||||
regex-lite = { version = "0.1.6", optional = true}
|
||||
once_cell = "1.16.0"
|
||||
bitcode = "0.6.3"
|
||||
csv = "1.3.0"
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Model {
|
||||
.cell(row, column)
|
||||
.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 {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row,
|
||||
@@ -212,12 +212,6 @@ impl Model {
|
||||
if column_count <= 0 {
|
||||
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
|
||||
let column_start = column;
|
||||
@@ -390,13 +384,6 @@ impl Model {
|
||||
if row_count <= 0 {
|
||||
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
|
||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||
@@ -457,7 +444,7 @@ impl Model {
|
||||
/// * Column is one of the extremes of the range. The new extreme would be target_column.
|
||||
/// Range is then normalized
|
||||
/// * Any other case, range is left unchanged.
|
||||
/// NOTE: This moves the data and column styles along with the formulas
|
||||
/// NOTE: This does NOT move the data in the columns or move the colum styles
|
||||
pub fn move_column_action(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
@@ -473,70 +460,7 @@ impl Model {
|
||||
return Err("Initial column out of boundaries".to_string());
|
||||
}
|
||||
|
||||
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)?;
|
||||
// TODO: Add the actual displacement of data and styles
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(
|
||||
@@ -549,88 +473,4 @@ impl Model {
|
||||
|
||||
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) {
|
||||
Ok(f2) => match op(f1, f2) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
Err(err) => data_row.push(ArrayNode::Error(err)),
|
||||
@@ -100,8 +98,6 @@ impl Model {
|
||||
match to_f64(&node) {
|
||||
Ok(f1) => match op(f1, f2) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
Err(err) => data_row.push(ArrayNode::Error(err)),
|
||||
@@ -137,10 +133,6 @@ impl Model {
|
||||
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
|
||||
(Ok(f1), Ok(f2)) => match op(f1, f2) {
|
||||
Ok(x) => data_row.push(ArrayNode::Number(x)),
|
||||
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
|
||||
Err(Error::VALUE) => {
|
||||
data_row.push(ArrayNode::Error(Error::VALUE))
|
||||
}
|
||||
Err(e) => data_row.push(ArrayNode::Error(e)),
|
||||
},
|
||||
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),
|
||||
|
||||
@@ -159,7 +159,7 @@ impl Model {
|
||||
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
|
||||
// See function Exact
|
||||
match result {
|
||||
CalcResult::Number(f) => Ok(format!("{f}")),
|
||||
CalcResult::Number(f) => Ok(format!("{}", f)),
|
||||
CalcResult::String(s) => Ok(s),
|
||||
CalcResult::Boolean(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.
|
||||
pub fn get_formula(&self) -> Option<i32> {
|
||||
match self {
|
||||
Cell::CellFormula { f, .. } => Some(*f),
|
||||
Cell::CellFormulaBoolean { f, .. } => Some(*f),
|
||||
Cell::CellFormulaNumber { f, .. } => Some(*f),
|
||||
Cell::CellFormulaString { f, .. } => Some(*f),
|
||||
Cell::CellFormulaError { f, .. } => Some(*f),
|
||||
_ => None,
|
||||
Cell::CellFormula { f, .. }
|
||||
| Cell::CellFormulaBoolean { f, .. }
|
||||
| Cell::CellFormulaNumber { f, .. }
|
||||
| Cell::CellFormulaString { f, .. }
|
||||
| Cell::CellFormulaError { f, .. }
|
||||
| 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::CellFormulaString { 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::CellFormulaString { 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::CellFormulaString { .. } => CellType::Text,
|
||||
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::BooleanCell { v, s: _ } => CellValue::Boolean(*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);
|
||||
CellValue::String(v)
|
||||
}
|
||||
@@ -148,14 +213,25 @@ impl Cell {
|
||||
};
|
||||
CellValue::String(v)
|
||||
}
|
||||
Cell::CellFormula { .. } => CellValue::String("#ERROR!".to_string()),
|
||||
Cell::CellFormulaBoolean { v, .. } => CellValue::Boolean(*v),
|
||||
Cell::CellFormulaNumber { v, .. } => CellValue::Number(*v),
|
||||
Cell::CellFormulaString { v, .. } => CellValue::String(v.clone()),
|
||||
Cell::CellFormulaError { ei, .. } => {
|
||||
Cell::DynamicCellFormula { .. } | Cell::CellFormula { .. } => {
|
||||
CellValue::String("#ERROR!".to_string())
|
||||
}
|
||||
Cell::DynamicCellFormulaBoolean { v, .. } | Cell::CellFormulaBoolean { v, .. } => {
|
||||
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);
|
||||
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<()> {
|
||||
let nt = self.next_token();
|
||||
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(())
|
||||
}
|
||||
@@ -314,9 +314,6 @@ impl Lexer {
|
||||
} else if name_upper == self.language.booleans.r#false {
|
||||
return TokenType::Boolean(false);
|
||||
}
|
||||
if self.peek_char() == Some('(') {
|
||||
return TokenType::Ident(name);
|
||||
}
|
||||
if self.mode == LexerMode::A1 {
|
||||
let parsed_reference = utils::parse_reference_a1(&name_upper);
|
||||
if parsed_reference.is_some()
|
||||
@@ -514,7 +511,7 @@ impl Lexer {
|
||||
self.position = position;
|
||||
chars.parse::<i32>().map_err(|_| LexerError {
|
||||
position,
|
||||
message: format!("Failed to parse to int: {chars}"),
|
||||
message: format!("Failed to parse to int: {}", chars),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -575,7 +572,9 @@ impl Lexer {
|
||||
}
|
||||
self.position = position;
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)),
|
||||
Err(_) => {
|
||||
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
|
||||
}
|
||||
Ok(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,16 +148,15 @@ impl Lexer {
|
||||
let row_left = match row_left.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(
|
||||
self.set_error(&format!("Failed parsing row {row_left}"), position)
|
||||
)
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_left), position))
|
||||
}
|
||||
};
|
||||
let row_right = match row_right.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::utils::column_to_number;
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
@@ -686,29 +685,3 @@ fn test_comparisons() {
|
||||
assert_eq!(lx.next_token(), Number(7.0));
|
||||
assert_eq!(lx.next_token(), 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);
|
||||
}
|
||||
|
||||
@@ -717,7 +717,7 @@ impl Parser {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: format!("sheet not found: {}", context.sheet),
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -828,7 +828,7 @@ impl Parser {
|
||||
| TokenType::Percent => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: format!("Unexpected token: '{next_token:?}'"),
|
||||
message: format!("Unexpected token: '{:?}'", next_token),
|
||||
},
|
||||
TokenType::LeftBracket => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
@@ -850,7 +850,7 @@ impl Parser {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: format!("sheet not found: {}", context.sheet),
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -878,7 +878,7 @@ impl Parser {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: format!("table sheet not found: {}", table.sheet_name),
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,24 +53,24 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
|
||||
arguments = to_string_moved(el, move_context);
|
||||
}
|
||||
}
|
||||
format!("{name}({arguments})")
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
|
||||
match node {
|
||||
ArrayNode::Boolean(value) => format!("{value}").to_ascii_uppercase(),
|
||||
ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
ArrayNode::Number(number) => to_excel_precision_str(*number),
|
||||
ArrayNode::String(value) => format!("\"{value}\""),
|
||||
ArrayNode::Error(kind) => format!("{kind}"),
|
||||
ArrayNode::String(value) => format!("\"{}\"", value),
|
||||
ArrayNode::Error(kind) => format!("{}", kind),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
|
||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
NumberKind(number) => to_excel_precision_str(*number),
|
||||
StringKind(value) => format!("\"{value}\""),
|
||||
StringKind(value) => format!("\"{}\"", value),
|
||||
ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
@@ -241,7 +241,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{s1}:{s2}")
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
WrongReferenceKind {
|
||||
sheet_name,
|
||||
@@ -325,7 +325,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{s1}:{s2}")
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
@@ -358,7 +358,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
}
|
||||
_ => to_string_moved(right, move_context),
|
||||
};
|
||||
format!("{x}{kind}{y}")
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => format!(
|
||||
"{}^{}",
|
||||
@@ -403,7 +403,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
}
|
||||
|
||||
// Enclose the whole matrix in braces
|
||||
format!("{{{matrix_string}}}")
|
||||
format!("{{{}}}", matrix_string)
|
||||
}
|
||||
DefinedNameKind((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::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
||||
},
|
||||
ErrorKind(kind) => format!("{kind}"),
|
||||
ErrorKind(kind) => format!("{}", kind),
|
||||
ParseErrorKind {
|
||||
formula,
|
||||
message: _,
|
||||
|
||||
@@ -2,19 +2,15 @@ use crate::functions::Function;
|
||||
|
||||
use super::Node;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static RANGE_REFERENCE_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
fn get_re() -> &'static Regex {
|
||||
RANGE_REFERENCE_REGEX
|
||||
.get_or_init(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"))
|
||||
}
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
|
||||
|
||||
fn is_range_reference(s: &str) -> bool {
|
||||
get_re().is_match(s)
|
||||
RE.is_match(s)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -186,7 +182,7 @@ pub fn add_implicit_intersection(node: &mut Node, add: bool) {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) enum StaticResult {
|
||||
pub enum StaticResult {
|
||||
Scalar,
|
||||
Array(i32, i32),
|
||||
Range(i32, i32),
|
||||
@@ -222,7 +218,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.
|
||||
// * Range(a, b) if we know it will be a a x b range.
|
||||
// * 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 {
|
||||
Node::BooleanKind(_)
|
||||
| Node::NumberKind(_)
|
||||
@@ -341,8 +337,7 @@ fn static_analysis_offset(args: &[Node]) -> StaticResult {
|
||||
}
|
||||
_ => return StaticResult::Unknown,
|
||||
};
|
||||
// Both height and width are explicitly 1, so OFFSET will return a single cell
|
||||
StaticResult::Scalar
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
|
||||
@@ -576,37 +571,6 @@ fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
|
||||
}
|
||||
}
|
||||
|
||||
// NETWORKDAYS(start_date, end_date, [holidays])
|
||||
// Parameters: start_date (scalar), end_date (scalar), holidays (optional vector)
|
||||
fn args_signature_networkdays(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Scalar, Signature::Scalar, Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
// NETWORKDAYS.INTL(start_date, end_date, [weekend], [holidays])
|
||||
// Parameters: start_date (scalar), end_date (scalar), weekend (optional scalar), holidays (optional vector)
|
||||
fn args_signature_networkdays_intl(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Scalar, Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Vector,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
|
||||
// 1. When computing the function
|
||||
// 2. Checking the arguments to see if we need to insert the implicit intersection operator
|
||||
@@ -641,9 +605,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Choose => vec![Signature::Scalar; arg_count],
|
||||
Function::Column => args_signature_row(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::Cosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Max => vec![Signature::Vector; arg_count],
|
||||
@@ -722,28 +683,13 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Maxifs => vec![Signature::Vector; arg_count],
|
||||
Function::Minifs => vec![Signature::Vector; arg_count],
|
||||
Function::Date => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Datedif => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Datevalue => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Day => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Edate => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Month => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Time => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Timevalue => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hour => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Minute => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Second => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Now => args_signature_no_args(arg_count),
|
||||
Function::Today => args_signature_no_args(arg_count),
|
||||
Function::Year => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Days => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Days360 => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Weekday => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Weeknum => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Workday => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::WorkdayIntl => args_signature_scalars(arg_count, 2, 2),
|
||||
Function::Yearfrac => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Isoweeknum => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
|
||||
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
|
||||
Function::Db => args_signature_scalars(arg_count, 4, 1),
|
||||
@@ -832,8 +778,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Unicode => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Geomean => vec![Signature::Vector; arg_count],
|
||||
Function::Networkdays => args_signature_networkdays(arg_count),
|
||||
Function::NetworkdaysIntl => args_signature_networkdays_intl(arg_count),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,9 +816,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Round => scalar_arguments(args),
|
||||
Function::Rounddown => 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::Sinh => scalar_arguments(args),
|
||||
Function::Sqrt => scalar_arguments(args),
|
||||
@@ -945,27 +886,12 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Maxifs => not_implemented(args),
|
||||
Function::Minifs => not_implemented(args),
|
||||
Function::Date => not_implemented(args),
|
||||
Function::Datedif => not_implemented(args),
|
||||
Function::Datevalue => not_implemented(args),
|
||||
Function::Day => not_implemented(args),
|
||||
Function::Edate => not_implemented(args),
|
||||
Function::Month => not_implemented(args),
|
||||
Function::Time => not_implemented(args),
|
||||
Function::Timevalue => not_implemented(args),
|
||||
Function::Hour => not_implemented(args),
|
||||
Function::Minute => not_implemented(args),
|
||||
Function::Second => not_implemented(args),
|
||||
Function::Now => not_implemented(args),
|
||||
Function::Today => not_implemented(args),
|
||||
Function::Year => not_implemented(args),
|
||||
Function::Days => not_implemented(args),
|
||||
Function::Days360 => not_implemented(args),
|
||||
Function::Weekday => not_implemented(args),
|
||||
Function::Weeknum => not_implemented(args),
|
||||
Function::Workday => not_implemented(args),
|
||||
Function::WorkdayIntl => not_implemented(args),
|
||||
Function::Yearfrac => not_implemented(args),
|
||||
Function::Isoweeknum => not_implemented(args),
|
||||
Function::Cumipmt => not_implemented(args),
|
||||
Function::Cumprinc => not_implemented(args),
|
||||
Function::Db => not_implemented(args),
|
||||
@@ -1054,7 +980,5 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Eomonth => scalar_arguments(args),
|
||||
Function::Formulatext => not_implemented(args),
|
||||
Function::Geomean => not_implemented(args),
|
||||
Function::Networkdays => not_implemented(args),
|
||||
Function::NetworkdaysIntl => not_implemented(args),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::move_formula::to_string_array_node;
|
||||
use crate::expressions::parser::static_analysis::add_implicit_intersection;
|
||||
use crate::expressions::token::{OpSum, OpUnary};
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
pub enum DisplaceData {
|
||||
@@ -28,11 +28,6 @@ pub enum DisplaceData {
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
RowMove {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
delta: i32,
|
||||
},
|
||||
ColumnMove {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
@@ -164,29 +159,6 @@ pub(crate) fn stringify_reference(
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::RowMove {
|
||||
sheet,
|
||||
row: move_row,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet {
|
||||
if row == *move_row {
|
||||
row += *delta;
|
||||
} else if *delta > 0 {
|
||||
// Moving the row downwards
|
||||
if row > *move_row && row <= *move_row + *delta {
|
||||
// Intermediate rows move up by one position
|
||||
row -= 1;
|
||||
}
|
||||
} else if *delta < 0 {
|
||||
// Moving the row upwards
|
||||
if row < *move_row && row >= *move_row + *delta {
|
||||
// Intermediate rows move down by one position
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ColumnMove {
|
||||
sheet,
|
||||
column: move_column,
|
||||
@@ -195,18 +167,14 @@ pub(crate) fn stringify_reference(
|
||||
if sheet_index == *sheet {
|
||||
if column == *move_column {
|
||||
column += *delta;
|
||||
} else if *delta > 0 {
|
||||
// Moving the column to the right
|
||||
if column > *move_column && column <= *move_column + *delta {
|
||||
// Intermediate columns move left by one position
|
||||
column -= 1;
|
||||
}
|
||||
} else if *delta < 0 {
|
||||
// Moving the column to the left
|
||||
if column < *move_column && column >= *move_column + *delta {
|
||||
// Intermediate columns move right by one position
|
||||
column += 1;
|
||||
}
|
||||
} else if (*delta > 0
|
||||
&& column > *move_column
|
||||
&& column <= *move_column + *delta)
|
||||
|| (*delta < 0
|
||||
&& column < *move_column
|
||||
&& column >= *move_column + *delta)
|
||||
{
|
||||
column -= *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,16 +184,16 @@ pub(crate) fn stringify_reference(
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
let mut row_abs = if absolute_row {
|
||||
format!("${row}")
|
||||
format!("${}", row)
|
||||
} else {
|
||||
format!("{row}")
|
||||
format!("{}", row)
|
||||
};
|
||||
let column = match crate::expressions::utils::number_to_column(column) {
|
||||
Some(s) => s,
|
||||
None => return "#REF!".to_string(),
|
||||
};
|
||||
let mut col_abs = if absolute_column {
|
||||
format!("${column}")
|
||||
format!("${}", column)
|
||||
} else {
|
||||
column
|
||||
};
|
||||
@@ -240,27 +208,27 @@ pub(crate) fn stringify_reference(
|
||||
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
||||
}
|
||||
None => {
|
||||
format!("{col_abs}{row_abs}")
|
||||
format!("{}{}", col_abs, row_abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let row_abs = if absolute_row {
|
||||
format!("R{row}")
|
||||
format!("R{}", row)
|
||||
} else {
|
||||
format!("R[{row}]")
|
||||
format!("R[{}]", row)
|
||||
};
|
||||
let col_abs = if absolute_column {
|
||||
format!("C{column}")
|
||||
format!("C{}", column)
|
||||
} else {
|
||||
format!("C[{column}]")
|
||||
format!("C[{}]", column)
|
||||
};
|
||||
match &sheet_name {
|
||||
Some(name) => {
|
||||
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
||||
}
|
||||
None => {
|
||||
format!("{row_abs}{col_abs}")
|
||||
format!("{}{}", row_abs, col_abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,7 +256,7 @@ fn format_function(
|
||||
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.
|
||||
@@ -324,9 +292,9 @@ fn stringify(
|
||||
) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
|
||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
NumberKind(number) => to_excel_precision_str(*number),
|
||||
StringKind(value) => format!("\"{value}\""),
|
||||
StringKind(value) => format!("\"{}\"", value),
|
||||
WrongReferenceKind {
|
||||
sheet_name,
|
||||
column,
|
||||
@@ -416,7 +384,7 @@ fn stringify(
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{s1}:{s2}")
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
WrongRangeKind {
|
||||
sheet_name,
|
||||
@@ -465,7 +433,7 @@ fn stringify(
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{s1}:{s2}")
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
@@ -483,38 +451,40 @@ fn stringify(
|
||||
kind,
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpSumKind { kind, left, right } => {
|
||||
let left_str = stringify(left, context, displace_data, export_to_excel);
|
||||
// if kind is minus then we need parentheses in the right side if they are OpSumKind or CompareKind
|
||||
let right_str = if (matches!(kind, OpSum::Minus) && matches!(**right, OpSumKind { .. }))
|
||||
| matches!(**right, CompareKind { .. })
|
||||
{
|
||||
format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
)
|
||||
} else {
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
};
|
||||
|
||||
format!("{left_str}{kind}{right_str}")
|
||||
}
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
kind,
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } | CompareKind { .. } => format!(
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
_ => stringify(left, context, displace_data, export_to_excel),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!(
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpProductKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
_ => stringify(right, context, displace_data, export_to_excel),
|
||||
};
|
||||
format!("{x}{kind}{y}")
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => {
|
||||
let x = match **left {
|
||||
@@ -577,7 +547,7 @@ fn stringify(
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
};
|
||||
format!("{x}^{y}")
|
||||
format!("{}^{}", x, y)
|
||||
}
|
||||
InvalidFunctionKind { name, args } => {
|
||||
format_function(name, args, context, displace_data, export_to_excel)
|
||||
@@ -612,50 +582,17 @@ fn stringify(
|
||||
}
|
||||
matrix_string.push_str(&row_string);
|
||||
}
|
||||
format!("{{{matrix_string}}}")
|
||||
format!("{{{}}}", matrix_string)
|
||||
}
|
||||
TableNameKind(value) => value.to_string(),
|
||||
DefinedNameKind((name, ..)) => name.to_string(),
|
||||
WrongVariableKind(name) => name.to_string(),
|
||||
UnaryKind { kind, right } => match kind {
|
||||
OpUnary::Minus => {
|
||||
let needs_parentheses = match **right {
|
||||
BooleanKind(_)
|
||||
| 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)
|
||||
)
|
||||
}
|
||||
format!(
|
||||
"-{}",
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
OpUnary::Percentage => {
|
||||
format!(
|
||||
@@ -664,7 +601,7 @@ fn stringify(
|
||||
)
|
||||
}
|
||||
},
|
||||
ErrorKind(kind) => format!("{kind}"),
|
||||
ErrorKind(kind) => format!("{}", kind),
|
||||
ParseErrorKind {
|
||||
formula,
|
||||
position: _,
|
||||
|
||||
@@ -32,39 +32,3 @@ fn exp_order() {
|
||||
let t = parser.parse("(5)^(4)", &cell_reference);
|
||||
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,6 +211,4 @@ fn test_names() {
|
||||
assert!(!is_valid_identifier("test€"));
|
||||
assert!(!is_valid_identifier("truñe"));
|
||||
assert!(!is_valid_identifier("tr&ue"));
|
||||
|
||||
assert!(!is_valid_identifier("LOG10"));
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use crate::constants::EXCEL_DATE_BASE;
|
||||
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
|
||||
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
|
||||
|
||||
pub const DATE_OUT_OF_RANGE_MESSAGE: &str = "Out of range parameters for date";
|
||||
|
||||
#[inline]
|
||||
fn convert_to_serial_number(date: NaiveDate) -> i32 {
|
||||
date.num_days_from_ce() - EXCEL_DATE_BASE
|
||||
@@ -23,12 +21,14 @@ fn is_date_within_range(date: NaiveDate) -> bool {
|
||||
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
||||
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
return Err(format!(
|
||||
"Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
|
||||
"Excel date must be greater than {}",
|
||||
MINIMUM_DATE_SERIAL_NUMBER
|
||||
));
|
||||
};
|
||||
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
return Err(format!(
|
||||
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
|
||||
"Excel date must be less than {}",
|
||||
MAXIMUM_DATE_SERIAL_NUMBER
|
||||
));
|
||||
};
|
||||
#[allow(clippy::expect_used)]
|
||||
@@ -39,7 +39,7 @@ pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
||||
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
|
||||
match NaiveDate::from_ymd_opt(year, month, day) {
|
||||
Some(native_date) => Ok(convert_to_serial_number(native_date)),
|
||||
None => Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()),
|
||||
None => Err("Out of range parameters for date".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
|
||||
return Ok(MINIMUM_DATE_SERIAL_NUMBER);
|
||||
}
|
||||
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
|
||||
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
|
||||
return Err("Out of range parameters for date".to_string());
|
||||
};
|
||||
|
||||
// One thing to note for example is that even if you started with a year out of range
|
||||
@@ -70,7 +70,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
|
||||
// As a result, we have to run range checks as we parse the date from the biggest unit to the
|
||||
// smallest unit.
|
||||
if !is_date_within_range(date) {
|
||||
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
|
||||
return Err("Out of range parameters for date".to_string());
|
||||
}
|
||||
|
||||
date = {
|
||||
@@ -82,7 +82,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
|
||||
date = date + Months::new(abs_month);
|
||||
}
|
||||
if !is_date_within_range(date) {
|
||||
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
|
||||
return Err("Out of range parameters for date".to_string());
|
||||
}
|
||||
date
|
||||
};
|
||||
@@ -96,7 +96,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
|
||||
date = date + Days::new(abs_day);
|
||||
}
|
||||
if !is_date_within_range(date) {
|
||||
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string());
|
||||
return Err("Out of range parameters for date".to_string());
|
||||
}
|
||||
date
|
||||
};
|
||||
|
||||
@@ -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
|
||||
let value_abs = value.abs();
|
||||
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
||||
let mut text = format!("{value:.9}");
|
||||
let mut text = format!("{:.9}", value);
|
||||
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
Formatted {
|
||||
text,
|
||||
@@ -138,7 +138,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
let exponent = value_abs.log10().floor();
|
||||
value /= 10.0_f64.powf(exponent);
|
||||
let sign = if exponent < 0.0 { '-' } else { '+' };
|
||||
let s = format!("{value:.5}");
|
||||
let s = format!("{:.5}", value);
|
||||
Formatted {
|
||||
text: format!(
|
||||
"{}E{}{:02}",
|
||||
@@ -167,33 +167,33 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
for token in tokens {
|
||||
match token {
|
||||
TextToken::Literal(c) => {
|
||||
text = format!("{text}{c}");
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
TextToken::Text(t) => {
|
||||
text = format!("{text}{t}");
|
||||
text = format!("{}{}", text, t);
|
||||
}
|
||||
TextToken::Ghost(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{text} ");
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Spacer(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{text} ");
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Raw => {
|
||||
text = format!("{text}{value}");
|
||||
text = format!("{}{}", text, value);
|
||||
}
|
||||
TextToken::Digit(_) => {}
|
||||
TextToken::Period => {}
|
||||
TextToken::Day => {
|
||||
let day = date.day() as usize;
|
||||
text = format!("{text}{day}");
|
||||
text = format!("{}{}", text, day);
|
||||
}
|
||||
TextToken::DayPadded => {
|
||||
let day = date.day() as usize;
|
||||
text = format!("{text}{day:02}");
|
||||
text = format!("{}{:02}", text, day);
|
||||
}
|
||||
TextToken::DayNameShort => {
|
||||
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 => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{text}{month}");
|
||||
text = format!("{}{}", text, month);
|
||||
}
|
||||
TextToken::MonthPadded => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{text}{month:02}");
|
||||
text = format!("{}{:02}", text, month);
|
||||
}
|
||||
TextToken::MonthNameShort => {
|
||||
let month = date.month() as usize;
|
||||
@@ -228,7 +228,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
TextToken::MonthLetter => {
|
||||
let month = date.month() as usize;
|
||||
let months_letter = &locale.dates.months_letter[month - 1];
|
||||
text = format!("{text}{months_letter}");
|
||||
text = format!("{}{}", text, months_letter);
|
||||
}
|
||||
TextToken::YearShort => {
|
||||
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) => {
|
||||
let mut text = "".to_string();
|
||||
if let Some(c) = p.currency {
|
||||
text = format!("{c}");
|
||||
text = format!("{}", c);
|
||||
}
|
||||
let tokens = &p.tokens;
|
||||
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 {
|
||||
match token {
|
||||
TextToken::Literal(c) => {
|
||||
text = format!("{text}{c}");
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
TextToken::Text(t) => {
|
||||
text = format!("{text}{t}");
|
||||
text = format!("{}{}", text, t);
|
||||
}
|
||||
TextToken::Ghost(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{text} ");
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Spacer(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{text} ");
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Raw => {
|
||||
text = format!("{text}{value}");
|
||||
text = format!("{}{}", text, value);
|
||||
}
|
||||
TextToken::Period => {
|
||||
text = format!("{text}{decimal_separator}");
|
||||
text = format!("{}{}", text, decimal_separator);
|
||||
}
|
||||
TextToken::Digit(digit) => {
|
||||
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 number_index = ln - digit_count + index;
|
||||
if index == 0 && is_negative {
|
||||
text = format!("-{text}");
|
||||
text = format!("-{}", text);
|
||||
}
|
||||
if ln <= digit_count {
|
||||
// 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 {
|
||||
""
|
||||
};
|
||||
text = format!("{text}{c}{sep}");
|
||||
text = format!("{}{}{}", text, c, sep);
|
||||
}
|
||||
digit_index += 1;
|
||||
} else {
|
||||
@@ -373,18 +373,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
if index < fract_part.len() {
|
||||
text = format!("{}{}", text, fract_part[index]);
|
||||
} else if digit.kind == '0' {
|
||||
text = format!("{text}0");
|
||||
text = format!("{}0", text);
|
||||
} else if digit.kind == '?' {
|
||||
text = format!("{text} ");
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
} else if digit.number == 'e' {
|
||||
// 3. Exponent part
|
||||
let index = digit.index;
|
||||
if index == 0 {
|
||||
if exponent_is_negative {
|
||||
text = format!("{text}E-");
|
||||
text = format!("{}E-", text);
|
||||
} else {
|
||||
text = format!("{text}E+");
|
||||
text = format!("{}E+", text);
|
||||
}
|
||||
}
|
||||
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]
|
||||
};
|
||||
|
||||
text = format!("{text}{c}");
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
} else {
|
||||
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
|
||||
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())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
|
||||
@@ -333,7 +333,7 @@ impl Lexer {
|
||||
} else if s == '-' {
|
||||
Token::ScientificMinus
|
||||
} else {
|
||||
self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
|
||||
self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
@@ -385,14 +385,14 @@ impl Lexer {
|
||||
for c in "eneral".chars() {
|
||||
let cc = self.read_next_char();
|
||||
if Some(c) != cc {
|
||||
self.set_error(&format!("Unexpected character: {x}"));
|
||||
self.set_error(&format!("Unexpected character: {}", x));
|
||||
return Token::ILLEGAL;
|
||||
}
|
||||
}
|
||||
Token::General
|
||||
}
|
||||
_ => {
|
||||
self.set_error(&format!("Unexpected character: {x}"));
|
||||
self.set_error(&format!("Unexpected character: {}", x));
|
||||
Token::ILLEGAL
|
||||
}
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
// numbers > 1e-20 and scientific notation for the rest
|
||||
let y_str = if y.abs() <= 9e-20 {
|
||||
format!("{y:E}")
|
||||
format!("{:E}", y)
|
||||
} else if y == 1.0 {
|
||||
"".to_string()
|
||||
} else if y == -1.0 {
|
||||
"-".to_string()
|
||||
} else {
|
||||
format!("{y}")
|
||||
format!("{}", y)
|
||||
};
|
||||
let x_str = if x.abs() <= 9e-20 {
|
||||
format!("{x:E}")
|
||||
format!("{:E}", x)
|
||||
} else {
|
||||
format!("{x}")
|
||||
format!("{}", x)
|
||||
};
|
||||
if y == 0.0 && x == 0.0 {
|
||||
write!(f, "0")
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
|
||||
} else {
|
||||
let result = format!("{value:X}");
|
||||
let result = format!("{:X}", value);
|
||||
if let Some(places) = places {
|
||||
if places < result.len() as i32 {
|
||||
return CalcResult::new_error(
|
||||
@@ -120,7 +120,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
|
||||
} else {
|
||||
let result = format!("{value:o}");
|
||||
let result = format!("{:o}", value);
|
||||
if let Some(places) = places {
|
||||
if places < result.len() as i32 {
|
||||
return CalcResult::new_error(
|
||||
@@ -163,7 +163,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += 1024;
|
||||
}
|
||||
let result = format!("{value:b}");
|
||||
let result = format!("{:b}", value);
|
||||
if let Some(places) = places {
|
||||
if value_raw > 0.0 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
@@ -202,7 +202,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += HEX_MAX;
|
||||
}
|
||||
let result = format!("{value:X}");
|
||||
let result = format!("{:X}", value);
|
||||
if let Some(places) = places {
|
||||
if value_raw > 0.0 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
@@ -242,7 +242,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += OCT_MAX;
|
||||
}
|
||||
let result = format!("{value:o}");
|
||||
let result = format!("{:o}", value);
|
||||
if let Some(places) = places {
|
||||
if value_raw > 0.0 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
@@ -301,7 +301,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += 1024;
|
||||
}
|
||||
let result = format!("{value:b}");
|
||||
let result = format!("{:b}", value);
|
||||
if let Some(places) = places {
|
||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
@@ -391,7 +391,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += OCT_MAX;
|
||||
}
|
||||
let result = format!("{value:o}");
|
||||
let result = format!("{:o}", value);
|
||||
if let Some(places) = places {
|
||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
@@ -446,7 +446,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += 1024;
|
||||
}
|
||||
let result = format!("{value:b}");
|
||||
let result = format!("{:b}", value);
|
||||
if let Some(places) = places {
|
||||
if value < 512 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
@@ -532,7 +532,7 @@ impl Model {
|
||||
if value < 0 {
|
||||
value += HEX_MAX;
|
||||
}
|
||||
let result = format!("{value:X}");
|
||||
let result = format!("{:X}", value);
|
||||
if let Some(places) = places {
|
||||
if value < HEX_MAX_HALF && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
|
||||
@@ -231,7 +231,7 @@ impl Model {
|
||||
CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
*cell,
|
||||
format!("Invalid worksheet index: '{sheet}'"),
|
||||
format!("Invalid worksheet index: '{}'", sheet),
|
||||
)
|
||||
})?
|
||||
.dimension()
|
||||
@@ -245,7 +245,7 @@ impl Model {
|
||||
CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
*cell,
|
||||
format!("Invalid worksheet index: '{sheet}'"),
|
||||
format!("Invalid worksheet index: '{}'", sheet),
|
||||
)
|
||||
})?
|
||||
.dimension()
|
||||
|
||||
@@ -246,7 +246,7 @@ impl Model {
|
||||
}
|
||||
// None of the cases matched so we return the default
|
||||
// If there is an even number of args is the last one otherwise is #N/A
|
||||
if args_count.is_multiple_of(2) {
|
||||
if args_count % 2 == 0 {
|
||||
return self.evaluate_node_in_context(&args[args_count - 1], cell);
|
||||
}
|
||||
CalcResult::Error {
|
||||
@@ -262,7 +262,7 @@ impl Model {
|
||||
if args_count < 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
if !args_count.is_multiple_of(2) {
|
||||
if args_count % 2 != 0 {
|
||||
// Missing value for last condition
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::cast::NumberOrArray;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::number_format::to_precision;
|
||||
use crate::single_number_fn;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
@@ -312,7 +311,7 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => to_precision(f, 15),
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||
@@ -328,13 +327,12 @@ impl Model {
|
||||
let scale = 10.0_f64.powf(number_of_digits);
|
||||
CalcResult::Number((value * scale).round() / scale)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => to_precision(f, 15),
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||
@@ -354,13 +352,12 @@ impl Model {
|
||||
CalcResult::Number((value * scale).floor() / scale)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => to_precision(f, 15),
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||
@@ -381,16 +378,6 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
single_number_fn!(fn_log10, |f| if f <= 0.0 {
|
||||
Err(Error::NUM)
|
||||
} else {
|
||||
Ok(f64::log10(f))
|
||||
});
|
||||
single_number_fn!(fn_ln, |f| if f <= 0.0 {
|
||||
Err(Error::NUM)
|
||||
} else {
|
||||
Ok(f64::ln(f))
|
||||
});
|
||||
single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
|
||||
single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
|
||||
single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
|
||||
@@ -444,47 +431,6 @@ impl Model {
|
||||
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 {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
|
||||
@@ -54,9 +54,6 @@ pub enum Function {
|
||||
Columns,
|
||||
Cos,
|
||||
Cosh,
|
||||
Log,
|
||||
Log10,
|
||||
Ln,
|
||||
Max,
|
||||
Min,
|
||||
Pi,
|
||||
@@ -148,30 +145,13 @@ pub enum Function {
|
||||
|
||||
// Date and time
|
||||
Date,
|
||||
Datedif,
|
||||
Datevalue,
|
||||
Day,
|
||||
Edate,
|
||||
Eomonth,
|
||||
Month,
|
||||
Time,
|
||||
Timevalue,
|
||||
Hour,
|
||||
Minute,
|
||||
Second,
|
||||
Now,
|
||||
Today,
|
||||
Year,
|
||||
Networkdays,
|
||||
NetworkdaysIntl,
|
||||
Days,
|
||||
Days360,
|
||||
Weekday,
|
||||
Weeknum,
|
||||
Workday,
|
||||
WorkdayIntl,
|
||||
Yearfrac,
|
||||
Isoweeknum,
|
||||
|
||||
// Financial
|
||||
Cumipmt,
|
||||
@@ -270,7 +250,7 @@ pub enum Function {
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 215> {
|
||||
pub fn into_iter() -> IntoIter<Function, 195> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -297,9 +277,6 @@ impl Function {
|
||||
Function::Atanh,
|
||||
Function::Abs,
|
||||
Function::Pi,
|
||||
Function::Ln,
|
||||
Function::Log,
|
||||
Function::Log10,
|
||||
Function::Sqrt,
|
||||
Function::Sqrtpi,
|
||||
Function::Atan2,
|
||||
@@ -379,26 +356,9 @@ impl Function {
|
||||
Function::Month,
|
||||
Function::Eomonth,
|
||||
Function::Date,
|
||||
Function::Datedif,
|
||||
Function::Datevalue,
|
||||
Function::Edate,
|
||||
Function::Networkdays,
|
||||
Function::NetworkdaysIntl,
|
||||
Function::Time,
|
||||
Function::Timevalue,
|
||||
Function::Hour,
|
||||
Function::Minute,
|
||||
Function::Second,
|
||||
Function::Today,
|
||||
Function::Now,
|
||||
Function::Days,
|
||||
Function::Days360,
|
||||
Function::Weekday,
|
||||
Function::Weeknum,
|
||||
Function::Workday,
|
||||
Function::WorkdayIntl,
|
||||
Function::Yearfrac,
|
||||
Function::Isoweeknum,
|
||||
Function::Pmt,
|
||||
Function::Pv,
|
||||
Function::Rate,
|
||||
@@ -528,7 +488,6 @@ impl Function {
|
||||
Function::Isformula => "_xlfn.ISFORMULA".to_string(),
|
||||
Function::Sheet => "_xlfn.SHEET".to_string(),
|
||||
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
|
||||
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
|
||||
_ => self.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -575,10 +534,6 @@ impl Function {
|
||||
"POWER" => Some(Function::Power),
|
||||
"ATAN2" => Some(Function::Atan2),
|
||||
|
||||
"LN" => Some(Function::Ln),
|
||||
"LOG" => Some(Function::Log),
|
||||
"LOG10" => Some(Function::Log10),
|
||||
|
||||
"MAX" => Some(Function::Max),
|
||||
"MIN" => Some(Function::Min),
|
||||
"PRODUCT" => Some(Function::Product),
|
||||
@@ -666,26 +621,9 @@ impl Function {
|
||||
"EOMONTH" => Some(Function::Eomonth),
|
||||
"MONTH" => Some(Function::Month),
|
||||
"DATE" => Some(Function::Date),
|
||||
"DATEDIF" => Some(Function::Datedif),
|
||||
"DATEVALUE" => Some(Function::Datevalue),
|
||||
"EDATE" => Some(Function::Edate),
|
||||
"NETWORKDAYS" => Some(Function::Networkdays),
|
||||
"NETWORKDAYS.INTL" => Some(Function::NetworkdaysIntl),
|
||||
"TIME" => Some(Function::Time),
|
||||
"TIMEVALUE" => Some(Function::Timevalue),
|
||||
"HOUR" => Some(Function::Hour),
|
||||
"MINUTE" => Some(Function::Minute),
|
||||
"SECOND" => Some(Function::Second),
|
||||
"TODAY" => Some(Function::Today),
|
||||
"NOW" => Some(Function::Now),
|
||||
"DAYS" | "_XLFN.DAYS" => Some(Function::Days),
|
||||
"DAYS360" => Some(Function::Days360),
|
||||
"WEEKDAY" => Some(Function::Weekday),
|
||||
"WEEKNUM" => Some(Function::Weeknum),
|
||||
"WORKDAY" => Some(Function::Workday),
|
||||
"WORKDAY.INTL" => Some(Function::WorkdayIntl),
|
||||
"YEARFRAC" => Some(Function::Yearfrac),
|
||||
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
|
||||
// Financial
|
||||
"PMT" => Some(Function::Pmt),
|
||||
"PV" => Some(Function::Pv),
|
||||
@@ -796,9 +734,6 @@ impl fmt::Display for Function {
|
||||
Function::Switch => write!(f, "SWITCH"),
|
||||
Function::True => write!(f, "TRUE"),
|
||||
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::Cos => write!(f, "COS"),
|
||||
Function::Tan => write!(f, "TAN"),
|
||||
@@ -893,26 +828,9 @@ impl fmt::Display for Function {
|
||||
Function::Month => write!(f, "MONTH"),
|
||||
Function::Eomonth => write!(f, "EOMONTH"),
|
||||
Function::Date => write!(f, "DATE"),
|
||||
Function::Datedif => write!(f, "DATEDIF"),
|
||||
Function::Datevalue => write!(f, "DATEVALUE"),
|
||||
Function::Edate => write!(f, "EDATE"),
|
||||
Function::Networkdays => write!(f, "NETWORKDAYS"),
|
||||
Function::NetworkdaysIntl => write!(f, "NETWORKDAYS.INTL"),
|
||||
Function::Time => write!(f, "TIME"),
|
||||
Function::Timevalue => write!(f, "TIMEVALUE"),
|
||||
Function::Hour => write!(f, "HOUR"),
|
||||
Function::Minute => write!(f, "MINUTE"),
|
||||
Function::Second => write!(f, "SECOND"),
|
||||
Function::Today => write!(f, "TODAY"),
|
||||
Function::Now => write!(f, "NOW"),
|
||||
Function::Days => write!(f, "DAYS"),
|
||||
Function::Days360 => write!(f, "DAYS360"),
|
||||
Function::Weekday => write!(f, "WEEKDAY"),
|
||||
Function::Weeknum => write!(f, "WEEKNUM"),
|
||||
Function::Workday => write!(f, "WORKDAY"),
|
||||
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
|
||||
Function::Yearfrac => write!(f, "YEARFRAC"),
|
||||
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
|
||||
Function::Pmt => write!(f, "PMT"),
|
||||
Function::Pv => write!(f, "PV"),
|
||||
Function::Rate => write!(f, "RATE"),
|
||||
@@ -1043,9 +961,6 @@ impl Model {
|
||||
Function::True => self.fn_true(args, cell),
|
||||
Function::Xor => self.fn_xor(args, cell),
|
||||
// Math and trigonometry
|
||||
Function::Log => self.fn_log(args, cell),
|
||||
Function::Log10 => self.fn_log10(args, cell),
|
||||
Function::Ln => self.fn_ln(args, cell),
|
||||
Function::Sin => self.fn_sin(args, cell),
|
||||
Function::Cos => self.fn_cos(args, cell),
|
||||
Function::Tan => self.fn_tan(args, cell),
|
||||
@@ -1151,26 +1066,9 @@ impl Model {
|
||||
Function::Eomonth => self.fn_eomonth(args, cell),
|
||||
Function::Month => self.fn_month(args, cell),
|
||||
Function::Date => self.fn_date(args, cell),
|
||||
Function::Datedif => self.fn_datedif(args, cell),
|
||||
Function::Datevalue => self.fn_datevalue(args, cell),
|
||||
Function::Edate => self.fn_edate(args, cell),
|
||||
Function::Networkdays => self.fn_networkdays(args, cell),
|
||||
Function::NetworkdaysIntl => self.fn_networkdays_intl(args, cell),
|
||||
Function::Time => self.fn_time(args, cell),
|
||||
Function::Timevalue => self.fn_timevalue(args, cell),
|
||||
Function::Hour => self.fn_hour(args, cell),
|
||||
Function::Minute => self.fn_minute(args, cell),
|
||||
Function::Second => self.fn_second(args, cell),
|
||||
Function::Today => self.fn_today(args, cell),
|
||||
Function::Now => self.fn_now(args, cell),
|
||||
Function::Days => self.fn_days(args, cell),
|
||||
Function::Days360 => self.fn_days360(args, cell),
|
||||
Function::Weekday => self.fn_weekday(args, cell),
|
||||
Function::Weeknum => self.fn_weeknum(args, cell),
|
||||
Function::Workday => self.fn_workday(args, cell),
|
||||
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
|
||||
Function::Yearfrac => self.fn_yearfrac(args, cell),
|
||||
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
|
||||
// Financial
|
||||
Function::Pmt => self.fn_pmt(args, cell),
|
||||
Function::Pv => self.fn_pv(args, cell),
|
||||
@@ -1316,7 +1214,7 @@ mod tests {
|
||||
}
|
||||
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
||||
let iter_list = Function::into_iter()
|
||||
.map(|f| format!("{f}").replace('.', ""))
|
||||
.map(|f| format!("{}", f).replace('.', ""))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let len = iter_list.len();
|
||||
|
||||
@@ -350,7 +350,7 @@ impl Model {
|
||||
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
|
||||
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count < 2 || !args_count.is_multiple_of(2) {
|
||||
if args_count < 2 || args_count % 2 == 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
@@ -476,7 +476,7 @@ impl Model {
|
||||
F: FnMut(f64),
|
||||
{
|
||||
let args_count = args.len();
|
||||
if args_count < 3 || args_count.is_multiple_of(2) {
|
||||
if args_count < 3 || args_count % 2 == 0 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Model {
|
||||
|
||||
match cell.get_formula() {
|
||||
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!(
|
||||
node,
|
||||
Node::FunctionKind {
|
||||
|
||||
@@ -55,14 +55,14 @@ impl Model {
|
||||
let mut result = "".to_string();
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::String(value) => result = format!("{result}{value}"),
|
||||
CalcResult::Number(value) => result = format!("{result}{value}"),
|
||||
CalcResult::String(value) => result = format!("{}{}", result, value),
|
||||
CalcResult::Number(value) => result = format!("{}{}", result, value),
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Boolean(value) => {
|
||||
if value {
|
||||
result = format!("{result}TRUE");
|
||||
result = format!("{}TRUE", result);
|
||||
} else {
|
||||
result = format!("{result}FALSE");
|
||||
result = format!("{}FALSE", result);
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
@@ -82,14 +82,16 @@ impl Model {
|
||||
column,
|
||||
}) {
|
||||
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) => {
|
||||
if value {
|
||||
result = format!("{result}TRUE");
|
||||
result = format!("{}TRUE", result);
|
||||
} else {
|
||||
result = format!("{result}FALSE");
|
||||
result = format!("{}FALSE", result);
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
@@ -280,7 +282,7 @@ impl Model {
|
||||
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -315,7 +317,7 @@ impl Model {
|
||||
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -350,7 +352,7 @@ impl Model {
|
||||
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -385,7 +387,7 @@ impl Model {
|
||||
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -439,7 +441,7 @@ impl Model {
|
||||
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -476,7 +478,7 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -558,7 +560,7 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(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::Boolean(b) => {
|
||||
if b {
|
||||
@@ -640,7 +642,7 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(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::Boolean(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?")
|
||||
if exact {
|
||||
return regex::Regex::new(&format!("^{reg}$"));
|
||||
return regex::Regex::new(&format!("^{}$", reg));
|
||||
}
|
||||
regex::Regex::new(reg)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Booleans {
|
||||
@@ -30,17 +31,14 @@ pub struct Language {
|
||||
pub errors: Errors,
|
||||
}
|
||||
|
||||
static LANGUAGES: OnceLock<HashMap<String, Language>> = OnceLock::new();
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
fn get_languages() -> &'static HashMap<String, Language> {
|
||||
LANGUAGES.get_or_init(|| {
|
||||
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
|
||||
})
|
||||
}
|
||||
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
||||
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
|
||||
});
|
||||
|
||||
pub fn get_language(id: &str) -> Result<&Language, String> {
|
||||
get_languages()
|
||||
let language = LANGUAGES
|
||||
.get(id)
|
||||
.ok_or_else(|| format!("Language is not supported: '{id}'"))
|
||||
.ok_or(format!("Language is not supported: '{}'", id))?;
|
||||
Ok(language)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Locale {
|
||||
@@ -64,17 +65,12 @@ pub struct DecimalFormats {
|
||||
pub standard: String,
|
||||
}
|
||||
|
||||
static LOCALES: OnceLock<HashMap<String, Locale>> = OnceLock::new();
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
fn get_locales() -> &'static HashMap<String, Locale> {
|
||||
LOCALES.get_or_init(|| {
|
||||
bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale")
|
||||
})
|
||||
}
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> =
|
||||
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
|
||||
|
||||
pub fn get_locale(id: &str) -> Result<&Locale, String> {
|
||||
get_locales()
|
||||
.get(id)
|
||||
.ok_or_else(|| format!("Invalid locale: '{id}'"))
|
||||
// TODO: pass the locale once we implement locales in Rust
|
||||
let locale = LOCALES.get(id).ok_or("Invalid locale")?;
|
||||
Ok(locale)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ use crate::{
|
||||
lexer::LexerMode,
|
||||
parser::{
|
||||
move_formula::{move_formula, MoveContext},
|
||||
static_analysis::{run_static_analysis_on_node, StaticResult},
|
||||
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},
|
||||
types::*,
|
||||
@@ -99,22 +100,22 @@ pub struct Model {
|
||||
/// A Rust internal representation of an Excel workbook
|
||||
pub workbook: Workbook,
|
||||
/// A list of parsed formulas
|
||||
pub parsed_formulas: Vec<Vec<Node>>,
|
||||
pub parsed_formulas: Vec<Vec<(Node, StaticResult)>>,
|
||||
/// A list of parsed defined names
|
||||
pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
|
||||
/// An optimization to lookup strings faster
|
||||
pub(crate) shared_strings: HashMap<String, usize>,
|
||||
/// An instance of the parser
|
||||
pub(crate) parser: Parser,
|
||||
/// The list of cells with formulas that are evaluated or being evaluated
|
||||
/// The list of cells with formulas that are evaluated of being evaluated
|
||||
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
||||
/// The locale of the model
|
||||
pub(crate) locale: Locale,
|
||||
/// The language used
|
||||
/// Tha language used
|
||||
pub(crate) language: Language,
|
||||
/// The timezone used to evaluate the model
|
||||
pub(crate) tz: Tz,
|
||||
/// The view id. A view consists of a selected sheet and ranges.
|
||||
/// The view id. A view consist of a selected sheet and ranges.
|
||||
pub(crate) view_id: u32,
|
||||
}
|
||||
|
||||
@@ -215,7 +216,7 @@ impl Model {
|
||||
_ => CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Error with Implicit Intersection in cell {cell:?}"),
|
||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||
),
|
||||
},
|
||||
_ => self.evaluate_node_in_context(node, cell),
|
||||
@@ -355,7 +356,7 @@ impl Model {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
let result = format!("{l}{r}");
|
||||
let result = format!("{}{}", l, r);
|
||||
CalcResult::String(result)
|
||||
}
|
||||
OpProductKind { kind, left, right } => match kind {
|
||||
@@ -375,7 +376,7 @@ impl Model {
|
||||
}
|
||||
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
||||
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()),
|
||||
DefinedNameKind((name, scope, _)) => {
|
||||
@@ -391,26 +392,26 @@ impl Model {
|
||||
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
|
||||
Error::NAME,
|
||||
cell,
|
||||
format!("Defined name \"{name}\" is not a reference."),
|
||||
format!("Defined name \"{}\" is not a reference.", name),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
CalcResult::new_error(
|
||||
Error::NAME,
|
||||
cell,
|
||||
format!("Defined name \"{name}\" not found."),
|
||||
format!("Defined name \"{}\" not found.", name),
|
||||
)
|
||||
}
|
||||
}
|
||||
TableNameKind(s) => CalcResult::new_error(
|
||||
Error::NAME,
|
||||
cell,
|
||||
format!("table name \"{s}\" not supported."),
|
||||
format!("table name \"{}\" not supported.", s),
|
||||
),
|
||||
WrongVariableKind(s) => CalcResult::new_error(
|
||||
Error::NAME,
|
||||
cell,
|
||||
format!("Variable name \"{s}\" not found."),
|
||||
format!("Variable name \"{}\" not found.", s),
|
||||
),
|
||||
CompareKind { kind, left, right } => {
|
||||
let l = self.evaluate_node_in_context(left, cell);
|
||||
@@ -487,7 +488,7 @@ impl Model {
|
||||
} => CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Error parsing {formula}: {message}"),
|
||||
format!("Error parsing {}: {}", formula, message),
|
||||
),
|
||||
EmptyArgKind => CalcResult::EmptyArg,
|
||||
ImplicitIntersection {
|
||||
@@ -500,7 +501,7 @@ impl Model {
|
||||
None => CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error with Implicit Intersection in cell {cell:?}"),
|
||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -522,14 +523,195 @@ impl Model {
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Sets `result` in the cell given by `sheet` sheet index, row and column
|
||||
/// Note that will panic if the cell does not exist
|
||||
/// It will do nothing if the cell does not have a formula
|
||||
#[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 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();
|
||||
// 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() {
|
||||
match result {
|
||||
CalcResult::Number(value) => {
|
||||
@@ -594,19 +776,138 @@ impl Model {
|
||||
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 } => {
|
||||
if left.sheet == right.sheet
|
||||
&& left.row == right.row
|
||||
&& left.column == right.column
|
||||
{
|
||||
let intersection_cell = CellReferenceIndex {
|
||||
// There is only one cell
|
||||
let single_cell = CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
};
|
||||
let v = self.evaluate_cell(intersection_cell);
|
||||
self.set_cell_value(cell_reference, &v);
|
||||
let v = self.evaluate_cell(single_cell);
|
||||
self.set_cell_value(cell_reference, &v)?;
|
||||
} 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 {
|
||||
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) {
|
||||
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) {
|
||||
Ok(s) => s,
|
||||
Err(_) => "".to_string(),
|
||||
@@ -620,57 +921,65 @@ impl Model {
|
||||
f,
|
||||
s,
|
||||
o,
|
||||
m: "Implicit Intersection not implemented".to_string(),
|
||||
ei: Error::NIMPL,
|
||||
m: "Result would spill to non empty cells".to_string(),
|
||||
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.
|
||||
@@ -697,7 +1006,7 @@ impl Model {
|
||||
worksheet.color = Some(color.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!("Invalid color: {color}"))
|
||||
Err(format!("Invalid color: {}", color))
|
||||
}
|
||||
|
||||
/// Changes the visibility of a sheet
|
||||
@@ -714,16 +1023,18 @@ impl Model {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// EmptyCell, Boolean, Number, String, Error
|
||||
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||
use Cell::*;
|
||||
match cell {
|
||||
EmptyCell { .. } => CalcResult::EmptyCell,
|
||||
BooleanCell { v, .. } => CalcResult::Boolean(*v),
|
||||
NumberCell { v, .. } => CalcResult::Number(*v),
|
||||
ErrorCell { ei, .. } => {
|
||||
BooleanCell { v, .. } | SpillBooleanCell { v, .. } => CalcResult::Boolean(*v),
|
||||
NumberCell { v, .. } | SpillNumberCell { v, .. } => CalcResult::Number(*v),
|
||||
ErrorCell { ei, .. } | SpillErrorCell { ei, .. } => {
|
||||
let message = ei.to_localized_error_string(&self.language);
|
||||
CalcResult::new_error(ei.clone(), cell_reference, message)
|
||||
}
|
||||
SpillStringCell { v, .. } => CalcResult::String(v.clone()),
|
||||
SharedString { si, .. } => {
|
||||
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
|
||||
CalcResult::String(s.clone())
|
||||
@@ -732,15 +1043,21 @@ impl Model {
|
||||
CalcResult::new_error(Error::ERROR, cell_reference, message)
|
||||
}
|
||||
}
|
||||
CellFormula { .. } => CalcResult::Error {
|
||||
DynamicCellFormula { .. } | CellFormula { .. } => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell_reference,
|
||||
message: "Unevaluated formula".to_string(),
|
||||
},
|
||||
CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v),
|
||||
CellFormulaNumber { v, .. } => CalcResult::Number(*v),
|
||||
CellFormulaString { v, .. } => CalcResult::String(v.clone()),
|
||||
CellFormulaError { ei, o, m, .. } => {
|
||||
DynamicCellFormulaBoolean { v, .. } | CellFormulaBoolean { v, .. } => {
|
||||
CalcResult::Boolean(*v)
|
||||
}
|
||||
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) {
|
||||
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
|
||||
} else {
|
||||
@@ -810,9 +1127,10 @@ impl Model {
|
||||
self.cells.insert(key, CellState::Evaluating);
|
||||
}
|
||||
}
|
||||
let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone();
|
||||
let result = self.evaluate_node_in_context(node, cell_reference);
|
||||
self.set_cell_value(cell_reference, &result);
|
||||
let (node, _static_result) =
|
||||
&self.parsed_formulas[cell_reference.sheet as usize][f as usize];
|
||||
let result = self.evaluate_node_in_context(&node.clone(), cell_reference);
|
||||
let _ = self.set_cell_value(cell_reference, &result);
|
||||
// mark cell as evaluated
|
||||
self.cells.insert(key, CellState::Evaluated);
|
||||
result
|
||||
@@ -1027,7 +1345,7 @@ impl Model {
|
||||
let source_sheet_name = self
|
||||
.workbook
|
||||
.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();
|
||||
if source.sheet != area.sheet {
|
||||
return Err("Source and area are in different sheets".to_string());
|
||||
@@ -1041,7 +1359,7 @@ impl Model {
|
||||
let target_sheet_name = self
|
||||
.workbook
|
||||
.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();
|
||||
if let Some(formula) = value.strip_prefix('=') {
|
||||
let cell_reference = CellReferenceRC {
|
||||
@@ -1061,7 +1379,7 @@ impl Model {
|
||||
column_delta: target.column - source.column,
|
||||
},
|
||||
);
|
||||
Ok(format!("={formula_str}"))
|
||||
Ok(format!("={}", formula_str))
|
||||
} else {
|
||||
Ok(value.to_string())
|
||||
}
|
||||
@@ -1100,7 +1418,7 @@ impl Model {
|
||||
Some(cell) => match cell.get_formula() {
|
||||
None => cell.get_text(&self.workbook.shared_strings, &self.language),
|
||||
Some(i) => {
|
||||
let formula = &self.parsed_formulas[sheet as usize][i as usize];
|
||||
let formula = &self.parsed_formulas[sheet as usize][i as usize].0;
|
||||
let cell_ref = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row: target_row,
|
||||
@@ -1203,7 +1521,8 @@ impl Model {
|
||||
.get(sheet as usize)
|
||||
.ok_or("missing sheet")?
|
||||
.get(formula_index as usize)
|
||||
.ok_or("missing formula")?;
|
||||
.ok_or("missing formula")?
|
||||
.0;
|
||||
let cell_ref = CellReferenceRC {
|
||||
sheet: worksheet.get_name(),
|
||||
row,
|
||||
@@ -1437,6 +1756,25 @@ impl Model {
|
||||
column: i32,
|
||||
value: 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
|
||||
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||
if let Some(new_value) = value.strip_prefix('\'') {
|
||||
@@ -1462,7 +1800,8 @@ impl Model {
|
||||
self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?;
|
||||
// Update the style if needed
|
||||
let cell = CellReferenceIndex { sheet, row, column };
|
||||
let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
||||
let parsed_formula =
|
||||
&self.parsed_formulas[sheet as usize][formula_index as usize].0;
|
||||
if let Some(units) = self.compute_node_units(parsed_formula, &cell) {
|
||||
let new_style_index = self
|
||||
.workbook
|
||||
@@ -1538,12 +1877,13 @@ impl Model {
|
||||
// If the formula fails to parse try adding a parenthesis
|
||||
// SUM(A1:A3 => SUM(A1:A3)
|
||||
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 {
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
_ => parsed_formula = new_parsed_formula,
|
||||
}
|
||||
}
|
||||
let static_result = run_static_analysis_on_node(&parsed_formula);
|
||||
|
||||
let s = to_rc_format(&parsed_formula);
|
||||
let mut formula_index: i32 = -1;
|
||||
@@ -1552,7 +1892,7 @@ impl Model {
|
||||
}
|
||||
if formula_index == -1 {
|
||||
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;
|
||||
}
|
||||
worksheet.set_cell_with_formula(row, column, formula_index, style)?;
|
||||
@@ -1747,7 +2087,7 @@ impl Model {
|
||||
};
|
||||
match cell.get_formula() {
|
||||
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 {
|
||||
sheet: worksheet.get_name(),
|
||||
row,
|
||||
@@ -1762,6 +2102,14 @@ impl Model {
|
||||
/// Returns a list of all cells
|
||||
pub fn get_all_cells(&self) -> Vec<CellIndex> {
|
||||
let mut cells = Vec::new();
|
||||
for (sheet, row, column) in &self.workbook.calc_chain {
|
||||
let cell = CellIndex {
|
||||
row: *row,
|
||||
column: *column,
|
||||
index: *sheet,
|
||||
};
|
||||
cells.push(cell);
|
||||
}
|
||||
for (index, sheet) in self.workbook.worksheets.iter().enumerate() {
|
||||
let mut sorted_rows: Vec<_> = sheet.sheet_data.keys().collect();
|
||||
sorted_rows.sort_unstable();
|
||||
@@ -1788,6 +2136,8 @@ impl Model {
|
||||
|
||||
let cells = self.get_all_cells();
|
||||
|
||||
// First evaluate all dynamic arrays
|
||||
|
||||
for cell in cells {
|
||||
self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: cell.index,
|
||||
@@ -1818,9 +2168,22 @@ impl Model {
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.cell_clear_contents(row, column)?;
|
||||
// If it has a spill formula we need to delete the contents of all the spilled cells
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1845,6 +2208,18 @@ impl Model {
|
||||
/// # }
|
||||
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
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;
|
||||
if let Some(row_data) = sheet_data.get_mut(&row) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::{
|
||||
static_analysis::run_static_analysis_on_node,
|
||||
stringify::{rename_sheet_in_node, to_rc_format, to_string},
|
||||
Parser,
|
||||
},
|
||||
@@ -94,7 +95,8 @@ impl Model {
|
||||
let mut parse_formula = Vec::new();
|
||||
for formula in shared_formulas {
|
||||
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);
|
||||
}
|
||||
@@ -168,11 +170,11 @@ impl Model {
|
||||
.get_worksheet_names()
|
||||
.iter()
|
||||
.map(|s| s.to_uppercase())
|
||||
.any(|x| x == format!("{base_name_uppercase}{index}"))
|
||||
.any(|x| x == format!("{}{}", base_name_uppercase, index))
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
let sheet_name = format!("{base_name}{index}");
|
||||
let sheet_name = format!("{}{}", base_name, index);
|
||||
// Now we need a sheet_id
|
||||
let sheet_id = self.get_new_sheet_id();
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
@@ -192,7 +194,7 @@ impl Model {
|
||||
sheet_id: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
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
|
||||
.workbook
|
||||
@@ -234,7 +236,7 @@ impl Model {
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_name(old_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.
|
||||
@@ -248,10 +250,10 @@ impl Model {
|
||||
new_name: &str,
|
||||
) -> Result<(), String> {
|
||||
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() {
|
||||
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
|
||||
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
|
||||
@@ -362,14 +364,14 @@ impl Model {
|
||||
};
|
||||
let locale = match get_locale(locale_id) {
|
||||
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 seconds = milliseconds / 1000;
|
||||
let dt = match DateTime::from_timestamp(seconds, 0) {
|
||||
Some(s) => s,
|
||||
None => return Err(format!("Invalid timestamp: {milliseconds}")),
|
||||
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
||||
};
|
||||
// "2020-08-06T21:20:53Z
|
||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
@@ -126,7 +126,7 @@ pub fn to_precision_str(value: f64, precision: usize) -> String {
|
||||
let exponent = value.abs().log10().floor();
|
||||
let base = value / 10.0_f64.powf(exponent);
|
||||
let base = format!("{0:.1$}", base, precision - 1);
|
||||
let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
|
||||
let value = format!("{}e{}", base, exponent).parse::<f64>().unwrap_or({
|
||||
// TODO: do this in a way that does not require a possible error
|
||||
0.0
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ impl Styles {
|
||||
return Ok(cell_style.xf_id);
|
||||
}
|
||||
}
|
||||
Err(format!("Style '{style_name}' not found"))
|
||||
Err(format!("Style '{}' not found", style_name))
|
||||
}
|
||||
|
||||
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {
|
||||
|
||||
@@ -7,8 +7,6 @@ mod test_column_width;
|
||||
mod test_criteria;
|
||||
mod test_currency;
|
||||
mod test_date_and_time;
|
||||
mod test_datedif_leap_month_end;
|
||||
mod test_days360_month_end;
|
||||
mod test_error_propagation;
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
@@ -29,7 +27,6 @@ mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_fn_time;
|
||||
mod test_fn_unicode;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
@@ -46,11 +43,8 @@ mod test_sheets;
|
||||
mod test_styles;
|
||||
mod test_trigonometric;
|
||||
mod test_true_false;
|
||||
mod test_weekday_return_types;
|
||||
mod test_weeknum_return_types;
|
||||
mod test_workbook;
|
||||
mod test_worksheet;
|
||||
mod test_yearfrac_basis;
|
||||
pub(crate) mod util;
|
||||
|
||||
mod engineering;
|
||||
@@ -58,20 +52,16 @@ mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_arrays;
|
||||
mod test_dynamic_arrays;
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_fv;
|
||||
mod test_fn_round;
|
||||
mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_geomean;
|
||||
mod test_get_cell_content;
|
||||
mod test_implicit_intersection;
|
||||
mod test_issue_155;
|
||||
mod test_ln;
|
||||
mod test_log;
|
||||
mod test_log10;
|
||||
mod test_networkdays;
|
||||
mod test_percentage;
|
||||
mod test_set_functions_error_handling;
|
||||
mod test_today;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
|
||||
use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN};
|
||||
use crate::model::Model;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::Col;
|
||||
@@ -508,10 +508,6 @@ fn test_move_column_right() {
|
||||
assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)");
|
||||
assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)");
|
||||
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]
|
||||
@@ -536,249 +532,5 @@ fn tets_move_column_error() {
|
||||
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
|
||||
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
/// We can also enter examples that illustrate/document a part of the function
|
||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
||||
|
||||
// Excel uses a serial date system where Jan 1, 1900 = 1 (though it treats 1900 as a leap year)
|
||||
// Most test dates are documented inline, but we define boundary values here:
|
||||
const EXCEL_MAX_DATE: f64 = 2958465.0; // Dec 31, 9999 - used in boundary tests
|
||||
const EXCEL_INVALID_DATE: f64 = 2958466.0; // One day past max - used in error tests
|
||||
|
||||
#[test]
|
||||
fn test_fn_date_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
@@ -221,382 +216,3 @@ fn test_date_early_dates() {
|
||||
Ok(CellValue::Number(61.0))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_days_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Basic functionality
|
||||
model._set("A1", "=DAYS(44570,44561)");
|
||||
model._set("A2", "=DAYS(44561,44570)"); // Reversed order
|
||||
model._set("A3", "=DAYS(44561,44561)");
|
||||
|
||||
// Edge cases
|
||||
model._set("A4", "=DAYS(1,2)"); // Early dates
|
||||
model._set(
|
||||
"A5",
|
||||
&format!("=DAYS({},{})", EXCEL_MAX_DATE, EXCEL_MAX_DATE - 1.0),
|
||||
); // Near max date
|
||||
|
||||
// Error cases - wrong argument count
|
||||
model._set("A6", "=DAYS()");
|
||||
model._set("A7", "=DAYS(44561)");
|
||||
model._set("A8", "=DAYS(44561,44570,1)");
|
||||
|
||||
// Error cases - invalid dates
|
||||
model._set("A9", "=DAYS(-1,44561)");
|
||||
model._set("A10", &format!("=DAYS(44561,{EXCEL_INVALID_DATE})"));
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"9");
|
||||
assert_eq!(model._get_text("A2"), *"-9");
|
||||
assert_eq!(model._get_text("A3"), *"0");
|
||||
assert_eq!(model._get_text("A4"), *"-1"); // DAYS(1,2) = 1-2 = -1
|
||||
assert_eq!(model._get_text("A5"), *"1");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A9"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A10"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days360_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Basic functionality with different basis values
|
||||
model._set("A1", "=DAYS360(44196,44560)"); // Default basis (US 30/360)
|
||||
model._set("A2", "=DAYS360(44196,44560,FALSE)"); // US 30/360 explicitly
|
||||
model._set("A3", "=DAYS360(44196,44560,TRUE)"); // European 30/360
|
||||
|
||||
// Same date
|
||||
model._set("A4", "=DAYS360(44561,44561)");
|
||||
model._set("A5", "=DAYS360(44561,44561,TRUE)");
|
||||
|
||||
// Reverse order (negative result)
|
||||
model._set("A6", "=DAYS360(44560,44196)");
|
||||
model._set("A7", "=DAYS360(44560,44196,TRUE)");
|
||||
|
||||
// Edge cases
|
||||
model._set("A8", "=DAYS360(1,2)");
|
||||
model._set("A9", "=DAYS360(1,2,FALSE)");
|
||||
|
||||
// Error cases - wrong argument count
|
||||
model._set("A10", "=DAYS360()");
|
||||
model._set("A11", "=DAYS360(44561)");
|
||||
model._set("A12", "=DAYS360(44561,44570,TRUE,1)");
|
||||
|
||||
// Error cases - invalid dates
|
||||
model._set("A13", "=DAYS360(-1,44561)");
|
||||
model._set("A14", &format!("=DAYS360(44561,{EXCEL_INVALID_DATE})"));
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"360");
|
||||
assert_eq!(model._get_text("A2"), *"360");
|
||||
assert_eq!(model._get_text("A3"), *"360");
|
||||
assert_eq!(model._get_text("A4"), *"0");
|
||||
assert_eq!(model._get_text("A5"), *"0");
|
||||
assert_eq!(model._get_text("A6"), *"-360");
|
||||
assert_eq!(model._get_text("A7"), *"-360");
|
||||
assert_eq!(model._get_text("A8"), *"1");
|
||||
assert_eq!(model._get_text("A9"), *"1");
|
||||
assert_eq!(model._get_text("A10"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A11"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A12"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A13"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A14"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Test return_type parameter variations with one known date (Friday 44561)
|
||||
model._set("A1", "=WEEKDAY(44561)"); // Default: Sun=1, Fri=6
|
||||
model._set("A2", "=WEEKDAY(44561,2)"); // Mon=1, Fri=5
|
||||
model._set("A3", "=WEEKDAY(44561,3)"); // Mon=0, Fri=4
|
||||
|
||||
// Test boundary days (Sun/Mon) to verify return_type logic
|
||||
model._set("A4", "=WEEKDAY(44556,1)"); // Sunday: should be 1
|
||||
model._set("A5", "=WEEKDAY(44556,2)"); // Sunday: should be 7
|
||||
model._set("A6", "=WEEKDAY(44557,2)"); // Monday: should be 1
|
||||
|
||||
// Error cases
|
||||
model._set("A7", "=WEEKDAY()"); // Wrong arg count
|
||||
model._set("A8", "=WEEKDAY(44561,0)"); // Invalid return_type
|
||||
model._set("A9", "=WEEKDAY(-1)"); // Invalid date
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Core functionality
|
||||
assert_eq!(model._get_text("A1"), *"6"); // Friday default
|
||||
assert_eq!(model._get_text("A2"), *"5"); // Friday Mon=1
|
||||
assert_eq!(model._get_text("A3"), *"4"); // Friday Mon=0
|
||||
|
||||
// Boundary verification
|
||||
assert_eq!(model._get_text("A4"), *"1"); // Sunday Sun=1
|
||||
assert_eq!(model._get_text("A5"), *"7"); // Sunday Mon=1
|
||||
assert_eq!(model._get_text("A6"), *"1"); // Monday Mon=1
|
||||
|
||||
// Error cases
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A8"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A9"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weeknum_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Test different return_type values (1=week starts Sunday, 2=week starts Monday)
|
||||
model._set("A1", "=WEEKNUM(44561)"); // Default return_type=1
|
||||
model._set("A2", "=WEEKNUM(44561,1)"); // Sunday start
|
||||
model._set("A3", "=WEEKNUM(44561,2)"); // Monday start
|
||||
|
||||
// Test year boundaries
|
||||
model._set("A4", "=WEEKNUM(43831,1)"); // Jan 1, 2020 (Wednesday)
|
||||
model._set("A5", "=WEEKNUM(43831,2)"); // Jan 1, 2020 (Wednesday)
|
||||
model._set("A6", "=WEEKNUM(44196,1)"); // Dec 31, 2020 (Thursday)
|
||||
model._set("A7", "=WEEKNUM(44196,2)"); // Dec 31, 2020 (Thursday)
|
||||
|
||||
// Test first and last weeks of year
|
||||
model._set("A8", "=WEEKNUM(44197,1)"); // Jan 1, 2021 (Friday)
|
||||
model._set("A9", "=WEEKNUM(44197,2)"); // Jan 1, 2021 (Friday)
|
||||
model._set("A10", "=WEEKNUM(44561,1)"); // Dec 31, 2021 (Friday)
|
||||
model._set("A11", "=WEEKNUM(44561,2)"); // Dec 31, 2021 (Friday)
|
||||
|
||||
// Error cases - wrong argument count
|
||||
model._set("A12", "=WEEKNUM()");
|
||||
model._set("A13", "=WEEKNUM(44561,1,1)");
|
||||
|
||||
// Error cases - invalid return_type
|
||||
model._set("A14", "=WEEKNUM(44561,0)");
|
||||
model._set("A15", "=WEEKNUM(44561,3)");
|
||||
model._set("A16", "=WEEKNUM(44561,-1)");
|
||||
|
||||
// Error cases - invalid dates
|
||||
model._set("A17", "=WEEKNUM(-1)");
|
||||
model._set("A18", &format!("=WEEKNUM({EXCEL_INVALID_DATE})"));
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Basic functionality
|
||||
assert_eq!(model._get_text("A1"), *"53"); // Week 53
|
||||
assert_eq!(model._get_text("A2"), *"53"); // Week 53 (Sunday start)
|
||||
assert_eq!(model._get_text("A3"), *"53"); // Week 53 (Monday start)
|
||||
|
||||
// Year boundary tests
|
||||
assert_eq!(model._get_text("A4"), *"1"); // Jan 1, 2020 (Sunday start)
|
||||
assert_eq!(model._get_text("A5"), *"1"); // Jan 1, 2020 (Monday start)
|
||||
assert_eq!(model._get_text("A6"), *"53"); // Dec 31, 2020 (Sunday start)
|
||||
assert_eq!(model._get_text("A7"), *"53"); // Dec 31, 2020 (Monday start)
|
||||
|
||||
// 2021 tests
|
||||
assert_eq!(model._get_text("A8"), *"1"); // Jan 1, 2021 (Sunday start)
|
||||
assert_eq!(model._get_text("A9"), *"1"); // Jan 1, 2021 (Monday start)
|
||||
assert_eq!(model._get_text("A10"), *"53"); // Dec 31, 2021 (Sunday start)
|
||||
assert_eq!(model._get_text("A11"), *"53"); // Dec 31, 2021 (Monday start)
|
||||
|
||||
// Error cases
|
||||
assert_eq!(model._get_text("A12"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A13"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A14"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A15"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A16"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A17"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A18"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workday_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Basic functionality
|
||||
model._set("A1", "=WORKDAY(44560,1)");
|
||||
model._set("A2", "=WORKDAY(44561,-1)");
|
||||
model._set("A3", "=WORKDAY(44561,0)");
|
||||
model._set("A4", "=WORKDAY(44560,5)");
|
||||
|
||||
// Test with holidays
|
||||
model._set("B1", "44561");
|
||||
model._set("A5", "=WORKDAY(44560,1,B1)"); // Should skip the holiday
|
||||
model._set("B2", "44562");
|
||||
model._set("B3", "44563");
|
||||
model._set("A6", "=WORKDAY(44560,3,B1:B3)"); // Multiple holidays
|
||||
|
||||
// Test starting on weekend
|
||||
model._set("A7", "=WORKDAY(44562,1)"); // Saturday start
|
||||
model._set("A8", "=WORKDAY(44563,1)"); // Sunday start
|
||||
|
||||
// Test negative workdays
|
||||
model._set("A9", "=WORKDAY(44565,-3)"); // Go backwards 3 days
|
||||
model._set("A10", "=WORKDAY(44565,-5,B1:B3)"); // Backwards with holidays
|
||||
|
||||
// Edge cases
|
||||
model._set("A11", "=WORKDAY(1,1)"); // Early date
|
||||
model._set("A12", "=WORKDAY(100000,10)"); // Large numbers
|
||||
|
||||
// Error cases - wrong argument count
|
||||
model._set("A13", "=WORKDAY()");
|
||||
model._set("A14", "=WORKDAY(44560)");
|
||||
model._set("A15", "=WORKDAY(44560,1,B1,B2)");
|
||||
|
||||
// Error cases - invalid dates
|
||||
model._set("A16", "=WORKDAY(-1,1)");
|
||||
model._set("A17", &format!("=WORKDAY({EXCEL_INVALID_DATE},1)"));
|
||||
|
||||
// Error cases - invalid holiday dates
|
||||
model._set("B4", "-1");
|
||||
model._set("A18", "=WORKDAY(44560,1,B4)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Basic functionality
|
||||
assert_eq!(model._get_text("A1"), *"44561"); // 1 day forward
|
||||
assert_eq!(model._get_text("A2"), *"44560"); // 1 day backward
|
||||
assert_eq!(model._get_text("A3"), *"44561"); // 0 days
|
||||
assert_eq!(model._get_text("A4"), *"44567"); // 5 days forward
|
||||
|
||||
// With holidays
|
||||
assert_eq!(model._get_text("A5"), *"44564"); // Skip holiday, go to Monday
|
||||
assert_eq!(model._get_text("A6"), *"44566"); // Skip multiple holidays
|
||||
|
||||
// Weekend starts
|
||||
assert_eq!(model._get_text("A7"), *"44564"); // From Saturday
|
||||
assert_eq!(model._get_text("A8"), *"44564"); // From Sunday
|
||||
|
||||
// Negative workdays
|
||||
assert_eq!(model._get_text("A9"), *"44560"); // 3 days back
|
||||
assert_eq!(model._get_text("A10"), *"44557"); // 5 days back with holidays
|
||||
|
||||
// Edge cases
|
||||
assert_eq!(model._get_text("A11"), *"2"); // Early date
|
||||
assert_eq!(model._get_text("A12"), *"100014"); // Large numbers
|
||||
|
||||
// Error cases
|
||||
assert_eq!(model._get_text("A13"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A14"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A15"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A16"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A17"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A18"), *"#NUM!"); // Invalid holiday
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workday_intl_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Test key weekend mask types
|
||||
model._set("A1", "=WORKDAY.INTL(44560,1,1)"); // Numeric: standard (Sat-Sun)
|
||||
model._set("A2", "=WORKDAY.INTL(44560,1,2)"); // Numeric: Sun-Mon
|
||||
model._set("A3", "=WORKDAY.INTL(44560,1,\"0000001\")"); // String: Sunday only
|
||||
model._set("A4", "=WORKDAY.INTL(44560,1,\"1100000\")"); // String: Mon-Tue
|
||||
|
||||
// Test with holidays
|
||||
model._set("B1", "44561");
|
||||
model._set("A5", "=WORKDAY.INTL(44560,2,1,B1)"); // Standard + holiday
|
||||
model._set("A6", "=WORKDAY.INTL(44560,2,7,B1)"); // Fri-Sat + holiday
|
||||
|
||||
// Basic edge cases
|
||||
model._set("A7", "=WORKDAY.INTL(44561,0,1)"); // Zero days
|
||||
model._set("A8", "=WORKDAY.INTL(44565,-1,1)"); // Negative days
|
||||
|
||||
// Error cases
|
||||
model._set("A9", "=WORKDAY.INTL()"); // Wrong arg count
|
||||
model._set("A10", "=WORKDAY.INTL(44560,1,0)"); // Invalid weekend mask
|
||||
model._set("A11", "=WORKDAY.INTL(44560,1,\"123\")"); // Invalid string mask
|
||||
model._set("A12", "=WORKDAY.INTL(-1,1,1)"); // Invalid date
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Weekend mask functionality
|
||||
assert_eq!(model._get_text("A1"), *"44561"); // Standard weekend
|
||||
assert_eq!(model._get_text("A2"), *"44561"); // Sun-Mon weekend
|
||||
assert_eq!(model._get_text("A3"), *"44561"); // Sunday only
|
||||
assert_eq!(model._get_text("A4"), *"44561"); // Mon-Tue weekend
|
||||
|
||||
// With holidays
|
||||
assert_eq!(model._get_text("A5"), *"44565"); // Skip holiday + standard weekend
|
||||
assert_eq!(model._get_text("A6"), *"44564"); // Skip holiday + Fri-Sat weekend
|
||||
|
||||
// Edge cases
|
||||
assert_eq!(model._get_text("A7"), *"44561"); // Zero days
|
||||
assert_eq!(model._get_text("A8"), *"44564"); // Negative days
|
||||
|
||||
// Error cases
|
||||
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A10"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A11"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A12"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_yearfrac_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Test key basis values (not exhaustive - just verify parameter works)
|
||||
model._set("A1", "=YEARFRAC(44561,44926)"); // Default (30/360)
|
||||
model._set("A2", "=YEARFRAC(44561,44926,1)"); // Actual/actual
|
||||
model._set("A3", "=YEARFRAC(44561,44926,4)"); // European 30/360
|
||||
|
||||
// Edge cases
|
||||
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
|
||||
model._set("A5", "=YEARFRAC(44926,44561,1)"); // Reverse = negative
|
||||
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
|
||||
|
||||
// Error cases
|
||||
model._set("A7", "=YEARFRAC()"); // Wrong arg count
|
||||
model._set("A8", "=YEARFRAC(44561,44926,5)"); // Invalid basis
|
||||
model._set("A9", "=YEARFRAC(-1,44926,1)"); // Invalid date
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Basic functionality (approximate values expected)
|
||||
assert_eq!(model._get_text("A1"), *"1"); // About 1 year
|
||||
assert_eq!(model._get_text("A2"), *"1"); // About 1 year
|
||||
assert_eq!(model._get_text("A3"), *"1"); // About 1 year
|
||||
|
||||
// Edge cases
|
||||
assert_eq!(model._get_text("A4"), *"0"); // Same date
|
||||
assert_eq!(model._get_text("A5"), *"-1"); // Negative
|
||||
assert_eq!(model._get_text("A6"), *"1"); // Exact year
|
||||
|
||||
// Error cases
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A8"), *"#NUM!"); // Invalid basis should return #NUM!
|
||||
assert_eq!(model._get_text("A9"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isoweeknum_function() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Basic functionality
|
||||
model._set("A1", "=ISOWEEKNUM(44563)"); // Mid-week date
|
||||
model._set("A2", "=ISOWEEKNUM(44561)"); // Year-end date
|
||||
|
||||
// Key ISO week boundaries (just critical cases)
|
||||
model._set("A3", "=ISOWEEKNUM(44197)"); // Jan 1, 2021 (Fri) -> Week 53 of 2020
|
||||
model._set("A4", "=ISOWEEKNUM(44200)"); // Jan 4, 2021 (Mon) -> Week 1 of 2021
|
||||
model._set("A5", "=ISOWEEKNUM(44564)"); // Jan 3, 2022 (Mon) -> Week 1 of 2022
|
||||
|
||||
// Error cases
|
||||
model._set("A6", "=ISOWEEKNUM()"); // Wrong arg count
|
||||
model._set("A7", "=ISOWEEKNUM(-1)"); // Invalid date
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Basic functionality
|
||||
assert_eq!(model._get_text("A1"), *"52");
|
||||
assert_eq!(model._get_text("A2"), *"52");
|
||||
|
||||
// ISO week boundaries
|
||||
assert_eq!(model._get_text("A3"), *"53"); // Week 53 of previous year
|
||||
assert_eq!(model._get_text("A4"), *"1"); // Week 1 of current year
|
||||
assert_eq!(model._get_text("A5"), *"1"); // Week 1 of next year
|
||||
|
||||
// Error cases
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_datedif_yd_leap_year_edge_cases() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// 29 Feb 2020 → 28 Feb 2021 (should be 0 days)
|
||||
model._set("A1", "=DATEDIF(\"29/2/2020\", \"28/2/2021\", \"YD\")");
|
||||
|
||||
// 29 Feb 2020 → 1 Mar 2021 (should be 1 day)
|
||||
model._set("A2", "=DATEDIF(\"29/2/2020\", \"2021-03-01\", \"YD\")");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
assert_eq!(model._get_text("A2"), *"1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_md_month_end_edge_cases() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// 31 Jan 2021 → 28 Feb 2021 (non-leap) => 28
|
||||
model._set("B1", "=DATEDIF(\"31/1/2021\", \"28/2/2021\", \"MD\")");
|
||||
|
||||
// 31 Jan 2020 → 29 Feb 2020 (leap) => 29
|
||||
model._set("B2", "=DATEDIF(\"31/1/2020\", \"29/2/2020\", \"MD\")");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"28");
|
||||
assert_eq!(model._get_text("B2"), *"29");
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_days360_month_end_us() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// 31 Jan 2021 -> 28 Feb 2021 (non-leap)
|
||||
model._set("A1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28))");
|
||||
|
||||
// 31 Jan 2020 -> 28 Feb 2020 (leap year – not last day of Feb)
|
||||
model._set("A2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,28))");
|
||||
|
||||
// 28 Feb 2020 -> 31 Mar 2020 (leap year span crossing month ends)
|
||||
model._set("A3", "=DAYS360(DATE(2020,2,28),DATE(2020,3,31))");
|
||||
|
||||
// 30 Apr 2021 -> 31 May 2021 (end-of-month adjustment rule)
|
||||
model._set("A4", "=DAYS360(DATE(2021,4,30),DATE(2021,5,31))");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"30");
|
||||
assert_eq!(model._get_text("A2"), *"28");
|
||||
assert_eq!(model._get_text("A3"), *"33");
|
||||
assert_eq!(model._get_text("A4"), *"30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days360_month_end_european() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// European basis = TRUE (or 1)
|
||||
model._set("B1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28),TRUE)");
|
||||
|
||||
model._set("B2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,29),TRUE)");
|
||||
|
||||
model._set("B3", "=DAYS360(DATE(2021,8,31),DATE(2021,9,30),TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"28");
|
||||
assert_eq!(model._get_text("B2"), *"29");
|
||||
assert_eq!(model._get_text("B3"), *"30");
|
||||
}
|
||||
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");
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::Cell;
|
||||
|
||||
// Helper to evaluate a formula and return the formatted text
|
||||
fn eval_formula(formula: &str) -> String {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", formula);
|
||||
model.evaluate();
|
||||
model._get_text("A1")
|
||||
}
|
||||
|
||||
// Helper that evaluates a formula and returns the raw value of A1 as a Result<f64, String>
|
||||
fn eval_formula_raw_number(formula: &str) -> Result<f64, String> {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", formula);
|
||||
model.evaluate();
|
||||
match model._get_cell("A1") {
|
||||
Cell::NumberCell { v, .. } => Ok(*v),
|
||||
Cell::BooleanCell { v, .. } => Ok(if *v { 1.0 } else { 0.0 }),
|
||||
Cell::ErrorCell { ei, .. } => Err(format!("{}", ei)),
|
||||
_ => Err(model._get_text("A1")),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_basic_numeric() {
|
||||
// DATEVALUE should return the serial number representing the date, **not** a formatted date
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"2/1/2023\")").unwrap(),
|
||||
44958.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_mmdd_with_leading_zero() {
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"02/01/2023\")").unwrap(),
|
||||
44958.0
|
||||
); // 1-Feb-2023
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_iso() {
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"2023-01-02\")").unwrap(),
|
||||
44928.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_month_name() {
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"2-Jan-23\")").unwrap(),
|
||||
44928.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_ambiguous_ddmm() {
|
||||
// 01/02/2023 interpreted as MM/DD -> 2-Jan-2023
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"01/02/2023\")").unwrap(),
|
||||
44929.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_ddmm_unambiguous() {
|
||||
// 15/01/2023 should be 15-Jan-2023 since 15 cannot be month
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"15/01/2023\")").unwrap(),
|
||||
44941.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_leap_day() {
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"29/02/2020\")").unwrap(),
|
||||
43890.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_year_first_text_month() {
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"2023/Jan/15\")").unwrap(),
|
||||
44941.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_mmdd_with_day_gt_12() {
|
||||
assert_eq!(
|
||||
eval_formula_raw_number("=DATEVALUE(\"6/15/2021\")").unwrap(),
|
||||
44373.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datevalue_error_conditions() {
|
||||
let cases = [
|
||||
"=DATEVALUE(\"31/04/2023\")", // invalid day (Apr has 30 days)
|
||||
"=DATEVALUE(\"13/13/2023\")", // invalid month
|
||||
"=DATEVALUE(\"not a date\")", // non-date text
|
||||
];
|
||||
for formula in cases {
|
||||
let result = eval_formula(formula);
|
||||
assert_eq!(result, *"#VALUE!", "Expected #VALUE! for {}", formula);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set and evaluate a single DATEDIF call
|
||||
fn eval_datedif(unit: &str) -> String {
|
||||
let mut model = new_empty_model();
|
||||
let formula = format!("=DATEDIF(\"2020-01-01\", \"2021-06-15\", \"{}\")", unit);
|
||||
model._set("A1", &formula);
|
||||
model.evaluate();
|
||||
model._get_text("A1")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_y() {
|
||||
assert_eq!(eval_datedif("Y"), *"1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_m() {
|
||||
assert_eq!(eval_datedif("M"), *"17");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_d() {
|
||||
assert_eq!(eval_datedif("D"), *"531");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_ym() {
|
||||
assert_eq!(eval_datedif("YM"), *"5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_yd() {
|
||||
assert_eq!(eval_datedif("YD"), *"165");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_md() {
|
||||
assert_eq!(eval_datedif("MD"), *"14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_edge_and_error_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Leap-year spanning
|
||||
model._set("A1", "=DATEDIF(\"28/2/2020\", \"1/3/2020\", \"D\")");
|
||||
// End date before start date => #NUM!
|
||||
model._set("A2", "=DATEDIF(\"1/2/2021\", \"1/1/2021\", \"D\")");
|
||||
// Invalid unit => #VALUE!
|
||||
model._set("A3", "=DATEDIF(\"1/1/2020\", \"1/1/2021\", \"Z\")");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#VALUE!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_mixed_case_unit() {
|
||||
assert_eq!(eval_datedif("yD"), *"165"); // mixed-case should work
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datedif_error_propagation() {
|
||||
// Invalid date in arguments should propagate #VALUE!
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DATEDIF(\"bad\", \"bad\", \"Y\")");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), *"#VALUE!");
|
||||
}
|
||||
@@ -174,7 +174,7 @@ fn fn_or_xor_no_arguments() {
|
||||
println!("Testing function: {func}");
|
||||
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", &format!("={func}()"));
|
||||
model._set("A1", &format!("={}()", func));
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_round_approximation() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=ROUND(1.05*(0.0284+0.0046)-0.0284,4)");
|
||||
model._set("A2", "=ROUNDDOWN(1.05*(0.0284+0.0046)-0.0284,5)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.0063");
|
||||
assert_eq!(model._get_text("A2"), *"0.00625");
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
// Helper constants for common time values with detailed documentation
|
||||
const MIDNIGHT: &str = "0"; // 00:00:00 = 0/24 = 0
|
||||
const NOON: &str = "0.5"; // 12:00:00 = 12/24 = 0.5
|
||||
const TIME_14_30: &str = "0.604166667"; // 14:30:00 = 14.5/24 ≈ 0.604166667
|
||||
const TIME_14_30_45: &str = "0.6046875"; // 14:30:45 = 14.5125/24 = 0.6046875
|
||||
const TIME_14_30_59: &str = "0.604849537"; // 14:30:59 (from floored fractional inputs)
|
||||
const TIME_23_59_59: &str = "0.999988426"; // 23:59:59 = 23.99972.../24 ≈ 0.999988426
|
||||
|
||||
// Excel documentation test values with explanations
|
||||
const TIME_2_24_AM: &str = "0.1"; // 2:24 AM = 2.4/24 = 0.1
|
||||
const TIME_2_PM: &str = "0.583333333"; // 2:00 PM = 14/24 ≈ 0.583333333
|
||||
const TIME_6_45_PM: &str = "0.78125"; // 6:45 PM = 18.75/24 = 0.78125
|
||||
const TIME_6_35_AM: &str = "0.274305556"; // 6:35 AM = 6.583333.../24 ≈ 0.274305556
|
||||
const TIME_2_30_AM: &str = "0.104166667"; // 2:30 AM = 2.5/24 ≈ 0.104166667
|
||||
const TIME_1_AM: &str = "0.041666667"; // 1:00 AM = 1/24 ≈ 0.041666667
|
||||
const TIME_9_PM: &str = "0.875"; // 9:00 PM = 21/24 = 0.875
|
||||
const TIME_2_AM: &str = "0.083333333"; // 2:00 AM = 2/24 ≈ 0.083333333
|
||||
// Additional helper: 1-second past midnight (00:00:01)
|
||||
const TIME_00_00_01: &str = "0.000011574"; // 1 second = 1/86400 ≈ 0.000011574
|
||||
|
||||
/// Helper function to set up and evaluate a model with time expressions
|
||||
fn test_time_expressions(expressions: &[(&str, &str)]) -> crate::model::Model {
|
||||
let mut model = new_empty_model();
|
||||
for (cell, formula) in expressions {
|
||||
model._set(cell, formula);
|
||||
}
|
||||
model.evaluate();
|
||||
model
|
||||
}
|
||||
|
||||
/// Helper function to test component extraction for a given time value
|
||||
/// Returns (hour, minute, second) as strings
|
||||
fn test_component_extraction(time_value: &str) -> (String, String, String) {
|
||||
let model = test_time_expressions(&[
|
||||
("A1", &format!("=HOUR({time_value})")),
|
||||
("B1", &format!("=MINUTE({time_value})")),
|
||||
("C1", &format!("=SECOND({time_value})")),
|
||||
]);
|
||||
(
|
||||
model._get_text("A1").to_string(),
|
||||
model._get_text("B1").to_string(),
|
||||
model._get_text("C1").to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_excel_timevalue_compatibility() {
|
||||
// Test cases based on Excel's official documentation and examples
|
||||
let model = test_time_expressions(&[
|
||||
// Excel documentation examples
|
||||
("A1", "=TIMEVALUE(\"2:24 AM\")"), // Should be 0.1
|
||||
("A2", "=TIMEVALUE(\"2 PM\")"), // Should be 0.583333... (14/24)
|
||||
("A3", "=TIMEVALUE(\"6:45 PM\")"), // Should be 0.78125 (18.75/24)
|
||||
("A4", "=TIMEVALUE(\"18:45\")"), // Same as above, 24-hour format
|
||||
// Date-time format (date should be ignored)
|
||||
("B1", "=TIMEVALUE(\"22-Aug-2011 6:35 AM\")"), // Should be ~0.2743
|
||||
("B2", "=TIMEVALUE(\"2023-01-01 14:30:00\")"), // Should be 0.604166667
|
||||
// Edge cases that Excel should support
|
||||
("C1", "=TIMEVALUE(\"12:00 AM\")"), // Midnight: 0
|
||||
("C2", "=TIMEVALUE(\"12:00 PM\")"), // Noon: 0.5
|
||||
("C3", "=TIMEVALUE(\"11:59:59 PM\")"), // Almost midnight: 0.999988426
|
||||
// Single digit variations
|
||||
("D1", "=TIMEVALUE(\"1 AM\")"), // 1:00 AM
|
||||
("D2", "=TIMEVALUE(\"9 PM\")"), // 9:00 PM
|
||||
("D3", "=TIMEVALUE(\"12 AM\")"), // Midnight
|
||||
("D4", "=TIMEVALUE(\"12 PM\")"), // Noon
|
||||
]);
|
||||
|
||||
// Excel documentation examples - verify exact values
|
||||
assert_eq!(model._get_text("A1"), *TIME_2_24_AM); // 2:24 AM
|
||||
assert_eq!(model._get_text("A2"), *TIME_2_PM); // 2 PM = 14:00
|
||||
assert_eq!(model._get_text("A3"), *TIME_6_45_PM); // 6:45 PM = 18:45
|
||||
assert_eq!(model._get_text("A4"), *TIME_6_45_PM); // 18:45 (24-hour)
|
||||
|
||||
// Date-time formats (date ignored, extract time only)
|
||||
assert_eq!(model._get_text("B1"), *TIME_6_35_AM); // 6:35 AM ≈ 0.2743
|
||||
assert_eq!(model._get_text("B2"), *TIME_14_30); // 14:30:00
|
||||
|
||||
// Edge cases
|
||||
assert_eq!(model._get_text("C1"), *MIDNIGHT); // 12:00 AM = 00:00
|
||||
assert_eq!(model._get_text("C2"), *NOON); // 12:00 PM = 12:00
|
||||
assert_eq!(model._get_text("C3"), *TIME_23_59_59); // 11:59:59 PM
|
||||
|
||||
// Single digit hours
|
||||
assert_eq!(model._get_text("D1"), *TIME_1_AM); // 1:00 AM
|
||||
assert_eq!(model._get_text("D2"), *TIME_9_PM); // 9:00 PM = 21:00
|
||||
assert_eq!(model._get_text("D3"), *MIDNIGHT); // 12 AM = 00:00
|
||||
assert_eq!(model._get_text("D4"), *NOON); // 12 PM = 12:00
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_function_basic_cases() {
|
||||
let model = test_time_expressions(&[
|
||||
("A1", "=TIME(0,0,0)"), // Midnight
|
||||
("A2", "=TIME(12,0,0)"), // Noon
|
||||
("A3", "=TIME(14,30,0)"), // 2:30 PM
|
||||
("A4", "=TIME(23,59,59)"), // Max time
|
||||
]);
|
||||
|
||||
assert_eq!(model._get_text("A1"), *MIDNIGHT);
|
||||
assert_eq!(model._get_text("A2"), *NOON);
|
||||
assert_eq!(model._get_text("A3"), *TIME_14_30);
|
||||
assert_eq!(model._get_text("A4"), *TIME_23_59_59);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_function_normalization() {
|
||||
let model = test_time_expressions(&[
|
||||
("A1", "=TIME(25,0,0)"), // Hours > 24 wrap around
|
||||
("A2", "=TIME(48,0,0)"), // 48 hours = 0 (2 full days)
|
||||
("A3", "=TIME(0,90,0)"), // 90 minutes = 1.5 hours
|
||||
("A4", "=TIME(0,0,90)"), // 90 seconds = 1.5 minutes
|
||||
("A5", "=TIME(14.9,30.9,59.9)"), // Fractional inputs floored to 14:30:59
|
||||
]);
|
||||
|
||||
assert_eq!(model._get_text("A1"), *TIME_1_AM); // 1:00:00
|
||||
assert_eq!(model._get_text("A2"), *MIDNIGHT); // 0:00:00
|
||||
assert_eq!(model._get_text("A3"), *"0.0625"); // 1:30:00
|
||||
assert_eq!(model._get_text("A4"), *"0.001041667"); // 0:01:30
|
||||
assert_eq!(model._get_text("A5"), *TIME_14_30_59); // 14:30:59 (floored)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_function_precision_edge_cases() {
|
||||
let model = test_time_expressions(&[
|
||||
// High precision fractional seconds
|
||||
("A1", "=TIME(14,30,45.999)"), // Fractional seconds should be floored
|
||||
("A2", "=SECOND(TIME(14,30,45.999))"), // Should extract 45, not 46
|
||||
// Very large normalization values
|
||||
("B1", "=TIME(999,999,999)"), // Extreme normalization test
|
||||
("B2", "=HOUR(999.5)"), // Multiple days, extract hour from fractional part
|
||||
("B3", "=MINUTE(999.75)"), // Multiple days, extract minute
|
||||
// Boundary conditions at rollover points
|
||||
("C1", "=TIME(24,60,60)"), // Should normalize to next day (00:01:00)
|
||||
("C2", "=HOUR(0.999999999)"), // Almost 24 hours should be 23
|
||||
("C3", "=MINUTE(0.999999999)"), // Almost 24 hours, extract minutes
|
||||
("C4", "=SECOND(0.999999999)"), // Almost 24 hours, extract seconds
|
||||
// Precision at boundaries
|
||||
("D1", "=TIME(23,59,59.999)"), // Very close to midnight
|
||||
("D2", "=TIME(0,0,0.001)"), // Just after midnight
|
||||
]);
|
||||
|
||||
// Fractional seconds are floored
|
||||
assert_eq!(model._get_text("A2"), *"45"); // 45.999 floored to 45
|
||||
|
||||
// Multiple days should work with rem_euclid
|
||||
assert_eq!(model._get_text("B2"), *"12"); // 999.5 days, hour = 12 (noon)
|
||||
|
||||
// Boundary normalization
|
||||
assert_eq!(model._get_text("C1"), *"0.042361111"); // 24:60:60 = 01:01:00 (normalized)
|
||||
assert_eq!(model._get_text("C2"), *"23"); // Almost 24 hours = 23:xx:xx
|
||||
|
||||
// High precision should be handled correctly
|
||||
let result_d1 = model._get_text("D1").parse::<f64>().unwrap();
|
||||
assert!(result_d1 < 1.0 && result_d1 > 0.999); // Very close to but less than 1.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_function_errors() {
|
||||
let model = test_time_expressions(&[
|
||||
("A1", "=TIME()"), // Wrong arg count
|
||||
("A2", "=TIME(12)"), // Wrong arg count
|
||||
("A3", "=TIME(12,30,0,0)"), // Wrong arg count
|
||||
("B1", "=TIME(-1,0,0)"), // Negative hour
|
||||
("B2", "=TIME(0,-1,0)"), // Negative minute
|
||||
("B3", "=TIME(0,0,-1)"), // Negative second
|
||||
]);
|
||||
|
||||
// Wrong argument count
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
|
||||
// Negative values should return #NUM! error
|
||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timevalue_function_formats() {
|
||||
let model = test_time_expressions(&[
|
||||
// Basic formats
|
||||
("A1", "=TIMEVALUE(\"14:30\")"),
|
||||
("A2", "=TIMEVALUE(\"14:30:45\")"),
|
||||
("A3", "=TIMEVALUE(\"00:00:00\")"),
|
||||
// AM/PM formats
|
||||
("B1", "=TIMEVALUE(\"2:30 PM\")"),
|
||||
("B2", "=TIMEVALUE(\"2:30 AM\")"),
|
||||
("B3", "=TIMEVALUE(\"12:00 PM\")"), // Noon
|
||||
("B4", "=TIMEVALUE(\"12:00 AM\")"), // Midnight
|
||||
// Single hour with AM/PM (now supported!)
|
||||
("B5", "=TIMEVALUE(\"2 PM\")"),
|
||||
("B6", "=TIMEVALUE(\"2 AM\")"),
|
||||
// Date-time formats (extract time only)
|
||||
("C1", "=TIMEVALUE(\"2023-01-01 14:30:00\")"),
|
||||
("C2", "=TIMEVALUE(\"2023-01-01T14:30:00\")"),
|
||||
// Whitespace handling
|
||||
("D1", "=TIMEVALUE(\" 14:30 \")"),
|
||||
]);
|
||||
|
||||
// Basic formats
|
||||
assert_eq!(model._get_text("A1"), *TIME_14_30);
|
||||
assert_eq!(model._get_text("A2"), *TIME_14_30_45);
|
||||
assert_eq!(model._get_text("A3"), *MIDNIGHT);
|
||||
|
||||
// AM/PM formats
|
||||
assert_eq!(model._get_text("B1"), *TIME_14_30); // 2:30 PM = 14:30
|
||||
assert_eq!(model._get_text("B2"), *TIME_2_30_AM); // 2:30 AM
|
||||
assert_eq!(model._get_text("B3"), *NOON); // 12:00 PM = noon
|
||||
assert_eq!(model._get_text("B4"), *MIDNIGHT); // 12:00 AM = midnight
|
||||
|
||||
// Single hour AM/PM formats (now supported!)
|
||||
assert_eq!(model._get_text("B5"), *TIME_2_PM); // 2 PM = 14:00
|
||||
assert_eq!(model._get_text("B6"), *TIME_2_AM); // 2 AM = 02:00
|
||||
|
||||
// Date-time formats
|
||||
assert_eq!(model._get_text("C1"), *TIME_14_30);
|
||||
assert_eq!(model._get_text("C2"), *TIME_14_30);
|
||||
|
||||
// Whitespace
|
||||
assert_eq!(model._get_text("D1"), *TIME_14_30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timevalue_function_errors() {
|
||||
let model = test_time_expressions(&[
|
||||
("A1", "=TIMEVALUE()"), // Wrong arg count
|
||||
("A2", "=TIMEVALUE(\"14:30\", \"x\")"), // Wrong arg count
|
||||
("B1", "=TIMEVALUE(\"invalid\")"), // Invalid format
|
||||
("B2", "=TIMEVALUE(\"25:00\")"), // Invalid hour
|
||||
("B3", "=TIMEVALUE(\"14:70\")"), // Invalid minute
|
||||
("B4", "=TIMEVALUE(\"\")"), // Empty string
|
||||
("B5", "=TIMEVALUE(\"2PM\")"), // Missing space (still unsupported)
|
||||
]);
|
||||
|
||||
// Wrong argument count should return #ERROR!
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
|
||||
// Invalid formats should return #VALUE!
|
||||
assert_eq!(model._get_text("B1"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("B2"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("B3"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("B4"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("B5"), *"#VALUE!"); // "2PM" no space - not supported
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_component_extraction_comprehensive() {
|
||||
// Test component extraction using helper function for consistency
|
||||
|
||||
// Test basic time values
|
||||
let test_cases = [
|
||||
(MIDNIGHT, ("0", "0", "0")), // 00:00:00
|
||||
(NOON, ("12", "0", "0")), // 12:00:00
|
||||
(TIME_14_30, ("14", "30", "0")), // 14:30:00
|
||||
(TIME_23_59_59, ("23", "59", "59")), // 23:59:59
|
||||
];
|
||||
|
||||
for (time_value, expected) in test_cases {
|
||||
let (hour, minute, second) = test_component_extraction(time_value);
|
||||
assert_eq!(hour, expected.0, "Hour mismatch for {time_value}");
|
||||
assert_eq!(minute, expected.1, "Minute mismatch for {time_value}");
|
||||
assert_eq!(second, expected.2, "Second mismatch for {time_value}");
|
||||
}
|
||||
|
||||
// Test multiple days (extract from fractional part)
|
||||
let (hour, minute, second) = test_component_extraction("1.5"); // Day 2, 12:00
|
||||
assert_eq!(
|
||||
(hour, minute, second),
|
||||
("12".to_string(), "0".to_string(), "0".to_string())
|
||||
);
|
||||
|
||||
let (hour, minute, second) = test_component_extraction("100.604166667"); // Day 101, 14:30
|
||||
assert_eq!(
|
||||
(hour, minute, second),
|
||||
("14".to_string(), "30".to_string(), "0".to_string())
|
||||
);
|
||||
|
||||
// Test precision at boundaries
|
||||
let (hour, _, _) = test_component_extraction("0.041666666"); // Just under 1:00 AM
|
||||
assert_eq!(hour, "0");
|
||||
|
||||
let (hour, _, _) = test_component_extraction("0.041666667"); // Exactly 1:00 AM
|
||||
assert_eq!(hour, "1");
|
||||
|
||||
let (hour, _, _) = test_component_extraction("0.041666668"); // Just over 1:00 AM
|
||||
assert_eq!(hour, "1");
|
||||
|
||||
// Test very large day values
|
||||
let (hour, minute, second) = test_component_extraction("1000000.25"); // Million days + 6 hours
|
||||
assert_eq!(
|
||||
(hour, minute, second),
|
||||
("6".to_string(), "0".to_string(), "0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_component_function_errors() {
|
||||
let model = test_time_expressions(&[
|
||||
// Wrong argument counts
|
||||
("A1", "=HOUR()"), // No arguments
|
||||
("A2", "=MINUTE()"), // No arguments
|
||||
("A3", "=SECOND()"), // No arguments
|
||||
("A4", "=HOUR(1, 2)"), // Too many arguments
|
||||
("A5", "=MINUTE(1, 2)"), // Too many arguments
|
||||
("A6", "=SECOND(1, 2)"), // Too many arguments
|
||||
// Negative values should return #NUM!
|
||||
("B1", "=HOUR(-0.5)"), // Negative value
|
||||
("B2", "=MINUTE(-1)"), // Negative value
|
||||
("B3", "=SECOND(-1)"), // Negative value
|
||||
("B4", "=HOUR(-0.000001)"), // Slightly negative
|
||||
("B5", "=MINUTE(-0.000001)"), // Slightly negative
|
||||
("B6", "=SECOND(-0.000001)"), // Slightly negative
|
||||
]);
|
||||
|
||||
// Wrong argument count should return #ERROR!
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
|
||||
// Negative values should return #NUM!
|
||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("B6"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_functions_integration() {
|
||||
// Test how TIME, TIMEVALUE and component extraction functions work together
|
||||
let model = test_time_expressions(&[
|
||||
// Create times with both functions
|
||||
("A1", "=TIME(14,30,45)"),
|
||||
("A2", "=TIMEVALUE(\"14:30:45\")"),
|
||||
// Extract components from TIME function results
|
||||
("B1", "=HOUR(A1)"),
|
||||
("B2", "=MINUTE(A1)"),
|
||||
("B3", "=SECOND(A1)"),
|
||||
// Extract components from TIMEVALUE function results
|
||||
("C1", "=HOUR(A2)"),
|
||||
("C2", "=MINUTE(A2)"),
|
||||
("C3", "=SECOND(A2)"),
|
||||
// Test additional TIME variations
|
||||
("D1", "=TIME(14,0,0)"), // 14:00:00
|
||||
("E1", "=HOUR(D1)"), // Extract hour from 14:00:00
|
||||
("E2", "=MINUTE(D1)"), // Extract minute from 14:00:00
|
||||
("E3", "=SECOND(D1)"), // Extract second from 14:00:00
|
||||
]);
|
||||
|
||||
// TIME and TIMEVALUE should produce equivalent results
|
||||
assert_eq!(model._get_text("A1"), model._get_text("A2"));
|
||||
|
||||
// Extracting components should work consistently
|
||||
assert_eq!(model._get_text("B1"), *"14");
|
||||
assert_eq!(model._get_text("B2"), *"30");
|
||||
assert_eq!(model._get_text("B3"), *"45");
|
||||
assert_eq!(model._get_text("C1"), *"14");
|
||||
assert_eq!(model._get_text("C2"), *"30");
|
||||
assert_eq!(model._get_text("C3"), *"45");
|
||||
|
||||
// Components from TIME(14,0,0)
|
||||
assert_eq!(model._get_text("E1"), *"14");
|
||||
assert_eq!(model._get_text("E2"), *"0");
|
||||
assert_eq!(model._get_text("E3"), *"0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_function_extreme_values() {
|
||||
// Test missing edge cases: very large fractional inputs
|
||||
let model = test_time_expressions(&[
|
||||
// Extremely large fractional values to TIME function
|
||||
("A1", "=TIME(999999.9, 999999.9, 999999.9)"), // Very large fractional inputs
|
||||
("A2", "=TIME(1e6, 1e6, 1e6)"), // Scientific notation inputs
|
||||
("A3", "=TIME(0.000001, 0.000001, 0.000001)"), // Very small fractional inputs
|
||||
// Large day values for component extraction (stress test)
|
||||
("B1", "=HOUR(999999.999)"), // Almost a million days
|
||||
("B2", "=MINUTE(999999.999)"),
|
||||
("B3", "=SECOND(999999.999)"),
|
||||
// Edge case: exactly 1.0 (should be midnight of next day)
|
||||
("C1", "=HOUR(1.0)"),
|
||||
("C2", "=MINUTE(1.0)"),
|
||||
("C3", "=SECOND(1.0)"),
|
||||
// Very high precision values
|
||||
("D1", "=HOUR(0.999999999999)"), // Almost exactly 24:00:00
|
||||
("D2", "=MINUTE(0.999999999999)"),
|
||||
("D3", "=SECOND(0.999999999999)"),
|
||||
]);
|
||||
|
||||
// Large fractional inputs should be floored and normalized
|
||||
let result_a1 = model._get_text("A1").parse::<f64>().unwrap();
|
||||
assert!(
|
||||
(0.0..1.0).contains(&result_a1),
|
||||
"Result should be valid time fraction"
|
||||
);
|
||||
|
||||
// Component extraction should work with very large values
|
||||
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
|
||||
assert!((0..=23).contains(&hour_b1), "Hour should be 0-23");
|
||||
|
||||
// Exactly 1.0 should be midnight (start of next day)
|
||||
assert_eq!(model._get_text("C1"), *"0");
|
||||
assert_eq!(model._get_text("C2"), *"0");
|
||||
assert_eq!(model._get_text("C3"), *"0");
|
||||
|
||||
// Very high precision should still extract valid components
|
||||
let hour_d1 = model._get_text("D1").parse::<i32>().unwrap();
|
||||
assert!((0..=23).contains(&hour_d1), "Hour should be 0-23");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timevalue_malformed_but_parseable() {
|
||||
// Test missing edge case: malformed but potentially parseable strings
|
||||
let model = test_time_expressions(&[
|
||||
// Test various malformed but potentially parseable time strings
|
||||
("A1", "=TIMEVALUE(\"14:30:00.123\")"), // Milliseconds (might be truncated)
|
||||
("A2", "=TIMEVALUE(\"14:30:00.999\")"), // High precision milliseconds
|
||||
("A3", "=TIMEVALUE(\"02:30:00\")"), // Leading zero hours
|
||||
("A4", "=TIMEVALUE(\"2:05:00\")"), // Single digit hour, zero-padded minute
|
||||
// Boundary cases for AM/PM parsing
|
||||
("B1", "=TIMEVALUE(\"11:59:59 PM\")"), // Just before midnight
|
||||
("B2", "=TIMEVALUE(\"12:00:01 AM\")"), // Just after midnight
|
||||
("B3", "=TIMEVALUE(\"12:00:01 PM\")"), // Just after noon
|
||||
("B4", "=TIMEVALUE(\"11:59:59 AM\")"), // Just before noon
|
||||
// Test various date-time combinations
|
||||
("C1", "=TIMEVALUE(\"2023-12-31T23:59:59\")"), // ISO format at year end
|
||||
("C2", "=TIMEVALUE(\"2023-01-01 00:00:01\")"), // New year, just after midnight
|
||||
// Test potential edge cases that might still be parseable
|
||||
("D1", "=TIMEVALUE(\"24:00:00\")"), // Should error (invalid hour)
|
||||
("D2", "=TIMEVALUE(\"23:60:00\")"), // Should error (invalid minute)
|
||||
("D3", "=TIMEVALUE(\"23:59:60\")"), // Should error (invalid second)
|
||||
]);
|
||||
|
||||
// Milliseconds are not supported, should return a #VALUE! error like Excel
|
||||
assert_eq!(model._get_text("A1"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A2"), *"#VALUE!");
|
||||
|
||||
// Leading zeros should work fine
|
||||
assert_eq!(model._get_text("A3"), *TIME_2_30_AM); // 02:30:00 should parse as 2:30:00
|
||||
|
||||
// AM/PM boundary cases should work
|
||||
let result_b1 = model._get_text("B1").parse::<f64>().unwrap();
|
||||
assert!(
|
||||
result_b1 > 0.99 && result_b1 < 1.0,
|
||||
"11:59:59 PM should be very close to 1.0"
|
||||
);
|
||||
|
||||
let result_b2 = model._get_text("B2").parse::<f64>().unwrap();
|
||||
assert!(
|
||||
result_b2 > 0.0 && result_b2 < 0.01,
|
||||
"12:00:01 AM should be very close to 0.0"
|
||||
);
|
||||
|
||||
// ISO 8601 format with "T" separator should be parsed correctly
|
||||
assert_eq!(model._get_text("C1"), *TIME_23_59_59); // 23:59:59 → almost midnight
|
||||
assert_eq!(model._get_text("C2"), *TIME_00_00_01); // 00:00:01 → one second past midnight
|
||||
|
||||
// Time parser normalizes edge cases to midnight (Excel compatibility)
|
||||
assert_eq!(model._get_text("D1"), *"0"); // 24:00:00 = midnight of next day
|
||||
assert_eq!(model._get_text("D2"), *"0"); // 23:60:00 normalizes to 24:00:00 = midnight
|
||||
assert_eq!(model._get_text("D3"), *"0"); // 23:59:60 normalizes to 24:00:00 = midnight
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_performance_stress_with_extreme_values() {
|
||||
// Test performance/stress cases with extreme values
|
||||
let model = test_time_expressions(&[
|
||||
// Very large numbers that should still work
|
||||
("A1", "=TIME(2147483647, 0, 0)"), // Max i32 hours
|
||||
("A2", "=TIME(0, 2147483647, 0)"), // Max i32 minutes
|
||||
("A3", "=TIME(0, 0, 2147483647)"), // Max i32 seconds
|
||||
// Component extraction with extreme day values
|
||||
("B1", "=HOUR(1e15)"), // Very large day number
|
||||
("B2", "=MINUTE(1e15)"),
|
||||
("B3", "=SECOND(1e15)"),
|
||||
// Edge of floating point precision
|
||||
("C1", "=HOUR(1.7976931348623157e+308)"), // Near max f64
|
||||
("C2", "=HOUR(2.2250738585072014e-308)"), // Near min positive f64
|
||||
// Multiple TIME function calls with large values
|
||||
("D1", "=TIME(1000000, 1000000, 1000000)"), // Large normalized values
|
||||
("D2", "=HOUR(D1)"), // Extract from large TIME result
|
||||
("D3", "=MINUTE(D1)"),
|
||||
("D4", "=SECOND(D1)"),
|
||||
]);
|
||||
|
||||
// All results should be valid (not errors) even with extreme inputs
|
||||
for cell in ["A1", "A2", "A3", "B1", "B2", "B3", "D1", "D2", "D3", "D4"] {
|
||||
let result = model._get_text(cell);
|
||||
assert!(
|
||||
result != *"#ERROR!" && result != *"#NUM!" && result != *"#VALUE!",
|
||||
"Cell {cell} should not error with extreme values: {result}",
|
||||
);
|
||||
}
|
||||
|
||||
// Results should be mathematically valid
|
||||
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
|
||||
let minute_b2 = model._get_text("B2").parse::<i32>().unwrap();
|
||||
let second_b3 = model._get_text("B3").parse::<i32>().unwrap();
|
||||
|
||||
assert!((0..=23).contains(&hour_b1));
|
||||
assert!((0..=59).contains(&minute_b2));
|
||||
assert!((0..=59).contains(&second_b3));
|
||||
|
||||
// TIME function results should be valid time fractions
|
||||
let time_d1 = model._get_text("D1").parse::<f64>().unwrap();
|
||||
assert!(
|
||||
(0.0..1.0).contains(&time_d1),
|
||||
"TIME result should be valid fraction"
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn simple_colum() {
|
||||
fn simple_column() {
|
||||
let mut model = new_empty_model();
|
||||
// We populate cells A1 to A3
|
||||
model._set("A1", "1");
|
||||
@@ -30,7 +30,7 @@ fn return_of_array_is_n_impl() {
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=LN(100)");
|
||||
model._set("A2", "=LN()");
|
||||
model._set("A3", "=LN(100, 10)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"4.605170186");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=LOG(100)");
|
||||
model._set("A2", "=LOG()");
|
||||
model._set("A3", "=LOG(10000, 10)");
|
||||
model._set("A4", "=LOG(100, 10, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"4");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=LOG10(100)");
|
||||
model._set("A2", "=LOG10()");
|
||||
model._set("A3", "=LOG10(100, 10)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_and_function() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=LOG10");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// This is the cell LOG10
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
|
||||
model._set("LOG10", "1000");
|
||||
model._set("A2", "=LOG10(LOG10)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1000");
|
||||
assert_eq!(model._get_text("A2"), *"3");
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
// Test data: Jan 1-10, 2023 week
|
||||
const JAN_1_2023: i32 = 44927; // Sunday
|
||||
const JAN_2_2023: i32 = 44928; // Monday
|
||||
const JAN_6_2023: i32 = 44932; // Friday
|
||||
const JAN_9_2023: i32 = 44935; // Monday
|
||||
const JAN_10_2023: i32 = 44936; // Tuesday
|
||||
|
||||
#[test]
|
||||
fn networkdays_calculates_weekdays_excluding_weekends() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", &format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023})"));
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"7",
|
||||
"Should count 7 weekdays in 10-day span"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_handles_reverse_date_order() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", &format!("=NETWORKDAYS({JAN_10_2023},{JAN_1_2023})"));
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"-7",
|
||||
"Reversed dates should return negative count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_excludes_holidays_from_weekdays() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{JAN_9_2023})"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"6",
|
||||
"Should exclude Monday holiday from 7 weekdays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_handles_same_start_end_date() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", &format!("=NETWORKDAYS({JAN_9_2023},{JAN_9_2023})"));
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"1",
|
||||
"Same weekday date should count as 1 workday"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_accepts_holiday_ranges() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("B1", &JAN_2_2023.to_string());
|
||||
model._set("B2", &JAN_6_2023.to_string());
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B2)"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"5",
|
||||
"Should exclude 2 holidays from 7 weekdays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_uses_standard_weekend_by_default() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023})"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"7",
|
||||
"Default should be Saturday-Sunday weekend"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_supports_numeric_weekend_patterns() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Pattern 2 = Sunday-Monday weekend
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},2)"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"6",
|
||||
"Sunday-Monday weekend should give 6 workdays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_supports_single_day_weekends() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Pattern 11 = Sunday only weekend
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},11)"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"8",
|
||||
"Sunday-only weekend should give 8 workdays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_supports_string_weekend_patterns() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// "0000110" = Friday-Saturday weekend
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\")"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"8",
|
||||
"Friday-Saturday weekend should give 8 workdays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_no_weekends_counts_all_days() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000000\")"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"10",
|
||||
"No weekends should count all 10 days"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_combines_custom_weekends_with_holidays() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\",{JAN_9_2023})"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"7",
|
||||
"Should exclude both weekend and holiday"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_validates_argument_count() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NETWORKDAYS()");
|
||||
model._set("A2", "=NETWORKDAYS(1,2,3,4)");
|
||||
model._set("A3", "=NETWORKDAYS.INTL()");
|
||||
model._set("A4", "=NETWORKDAYS.INTL(1,2,3,4,5)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), "#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), "#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), "#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_rejects_invalid_dates() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NETWORKDAYS(-1,100)");
|
||||
model._set("A2", "=NETWORKDAYS(1,3000000)");
|
||||
model._set("A3", "=NETWORKDAYS(\"text\",100)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "#NUM!");
|
||||
assert_eq!(model._get_text("A2"), "#NUM!");
|
||||
assert_eq!(model._get_text("A3"), "#VALUE!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_intl_rejects_invalid_weekend_patterns() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NETWORKDAYS.INTL(1,10,99)");
|
||||
model._set("A2", "=NETWORKDAYS.INTL(1,10,\"111110\")");
|
||||
model._set("A3", "=NETWORKDAYS.INTL(1,10,\"11111000\")");
|
||||
model._set("A4", "=NETWORKDAYS.INTL(1,10,\"1111102\")");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "#NUM!");
|
||||
assert_eq!(model._get_text("A2"), "#VALUE!");
|
||||
assert_eq!(model._get_text("A3"), "#VALUE!");
|
||||
assert_eq!(model._get_text("A4"), "#VALUE!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_rejects_invalid_holidays() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("B1", "invalid");
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1)"),
|
||||
);
|
||||
model._set(
|
||||
"A2",
|
||||
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},-1)"),
|
||||
);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"#VALUE!",
|
||||
"Should reject non-numeric holidays"
|
||||
);
|
||||
assert_eq!(
|
||||
model._get_text("A2"),
|
||||
"#NUM!",
|
||||
"Should reject out-of-range holidays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_handles_weekend_only_periods() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
let saturday = JAN_1_2023 - 1;
|
||||
model._set("A1", &format!("=NETWORKDAYS({saturday},{JAN_1_2023})"));
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"0",
|
||||
"Weekend-only period should count 0 workdays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_ignores_holidays_outside_date_range() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
let future_holiday = JAN_10_2023 + 100;
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{future_holiday})"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"7",
|
||||
"Out-of-range holidays should be ignored"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_handles_empty_holiday_ranges() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set(
|
||||
"A1",
|
||||
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B3)"),
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"7",
|
||||
"Empty holiday range should be treated as no holidays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_handles_minimum_valid_dates() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NETWORKDAYS(1,7)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"5",
|
||||
"Should handle earliest Excel dates correctly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networkdays_handles_large_date_ranges_efficiently() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NETWORKDAYS(1,365)");
|
||||
model.evaluate();
|
||||
|
||||
assert!(
|
||||
!model._get_text("A1").starts_with('#'),
|
||||
"Large ranges should not error"
|
||||
);
|
||||
}
|
||||
@@ -62,17 +62,3 @@ fn test_create_named_style() {
|
||||
let style = model.get_style_for_cell(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_models_have_two_fills() {
|
||||
let model = new_empty_model();
|
||||
assert_eq!(model.workbook.styles.fills.len(), 2);
|
||||
assert_eq!(
|
||||
model.workbook.styles.fills[0].pattern_type,
|
||||
"none".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
model.workbook.styles.fills[1].pattern_type,
|
||||
"gray125".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_weekday_return_types_11_to_17() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Test date: 44561 corresponds to a Friday (2021-12-31). We verify the
|
||||
// numeric result for each custom week start defined by return_type 11-17.
|
||||
model._set("A1", "=WEEKDAY(44561,11)"); // Monday start
|
||||
model._set("A2", "=WEEKDAY(44561,12)"); // Tuesday start
|
||||
model._set("A3", "=WEEKDAY(44561,13)"); // Wednesday start
|
||||
model._set("A4", "=WEEKDAY(44561,14)"); // Thursday start
|
||||
model._set("A5", "=WEEKDAY(44561,15)"); // Friday start
|
||||
model._set("A6", "=WEEKDAY(44561,16)"); // Saturday start
|
||||
model._set("A7", "=WEEKDAY(44561,17)"); // Sunday start
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"5"); // Mon=1 .. Sun=7 ⇒ Fri=5
|
||||
assert_eq!(model._get_text("A2"), *"4"); // Tue start ⇒ Fri=4
|
||||
assert_eq!(model._get_text("A3"), *"3"); // Wed start ⇒ Fri=3
|
||||
assert_eq!(model._get_text("A4"), *"2"); // Thu start ⇒ Fri=2
|
||||
assert_eq!(model._get_text("A5"), *"1"); // Fri start ⇒ Fri=1
|
||||
assert_eq!(model._get_text("A6"), *"7"); // Sat start ⇒ Fri=7
|
||||
assert_eq!(model._get_text("A7"), *"6"); // Sun start ⇒ Fri=6
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_weeknum_return_types_11_to_17_and_21() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Date 44561 -> 2021-12-31 (Friday). Previously verified as week 53 (Sunday/Monday start).
|
||||
// We verify that custom week-start codes 11-17 all map to week 53 and ISO variant (21) maps to 52.
|
||||
let formulas = [
|
||||
("A1", "=WEEKNUM(44561,11)"),
|
||||
("A2", "=WEEKNUM(44561,12)"),
|
||||
("A3", "=WEEKNUM(44561,13)"),
|
||||
("A4", "=WEEKNUM(44561,14)"),
|
||||
("A5", "=WEEKNUM(44561,15)"),
|
||||
("A6", "=WEEKNUM(44561,16)"),
|
||||
("A7", "=WEEKNUM(44561,17)"),
|
||||
("A8", "=WEEKNUM(44561,21)"), // ISO week numbering
|
||||
];
|
||||
for (cell, formula) in formulas {
|
||||
model._set(cell, formula);
|
||||
}
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// All 11-17 variations should yield 53
|
||||
for cell in ["A1", "A2", "A3", "A4", "A5", "A6", "A7"] {
|
||||
assert_eq!(model._get_text(cell), *"53", "{cell} should be 53");
|
||||
}
|
||||
// ISO week (return_type 21)
|
||||
assert_eq!(model._get_text("A8"), *"52");
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
#![allow(clippy::panic)]
|
||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
||||
|
||||
#[test]
|
||||
fn test_yearfrac_basis_2_actual_360() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Non-leap span of exactly 360 days should result in 1.0
|
||||
model._set("A1", "=YEARFRAC(44561,44921,2)");
|
||||
|
||||
// Leap-year span of 366 days: Jan 1 2020 → Jan 1 2021
|
||||
model._set("A2", "=YEARFRAC(43831,44197,2)");
|
||||
|
||||
// Reverse order should yield negative value
|
||||
model._set("A3", "=YEARFRAC(44921,44561,2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// 360/360
|
||||
assert_eq!(model._get_text("A1"), *"1");
|
||||
|
||||
// 366/360 ≈ 1.0166666667 (tolerance 1e-10)
|
||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!A2") {
|
||||
assert!((v - 1.016_666_666_7).abs() < 1e-10);
|
||||
} else {
|
||||
panic!("Expected numeric value in A2");
|
||||
}
|
||||
|
||||
// Negative symmetric of A1
|
||||
assert_eq!(model._get_text("A3"), *"-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_yearfrac_basis_3_actual_365() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Non-leap span of exactly 365 days should result in 1.0
|
||||
model._set("B1", "=YEARFRAC(44561,44926,3)");
|
||||
|
||||
// Leap-year span of 366 days
|
||||
model._set("B2", "=YEARFRAC(43831,44197,3)");
|
||||
|
||||
// Same date should be 0
|
||||
model._set("B3", "=YEARFRAC(44561,44561,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// 365/365
|
||||
assert_eq!(model._get_text("B1"), *"1");
|
||||
|
||||
// 366/365 ≈ 1.002739726 (tolerance 1e-10)
|
||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B2") {
|
||||
assert!((v - 1.002_739_726).abs() < 1e-10);
|
||||
} else {
|
||||
panic!("Expected numeric value in B2");
|
||||
}
|
||||
|
||||
// Same date
|
||||
assert_eq!(model._get_text("B3"), *"0");
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
mod test_add_delete_sheets;
|
||||
mod test_autofill_columns;
|
||||
mod test_autofill_rows;
|
||||
mod test_batch_row_column_diff;
|
||||
mod test_border;
|
||||
mod test_clear_cells;
|
||||
mod test_column_style;
|
||||
mod test_defined_names;
|
||||
mod test_delete_evaluates;
|
||||
mod test_delete_row_column_formatting;
|
||||
mod test_diff_queue;
|
||||
mod test_dynamic_array;
|
||||
mod test_evaluation;
|
||||
mod test_general;
|
||||
mod test_grid_lines;
|
||||
mod test_keyboard_navigation;
|
||||
mod test_last_empty_cell;
|
||||
mod test_multi_row_column;
|
||||
mod test_on_area_selection;
|
||||
mod test_on_expand_selected_range;
|
||||
mod test_on_paste_styles;
|
||||
|
||||
@@ -1,675 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
use bitcode::decode;
|
||||
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
test::util::new_empty_model,
|
||||
user_model::history::{Diff, QueueDiffs},
|
||||
UserModel,
|
||||
};
|
||||
|
||||
fn last_diff_list(model: &mut UserModel) -> Vec<Diff> {
|
||||
let bytes = model.flush_send_queue();
|
||||
let queue: Vec<QueueDiffs> = decode(&bytes).unwrap();
|
||||
// Get the last operation's diff list
|
||||
queue.last().unwrap().list.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_invariant_insert_rows() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
assert!(model.insert_rows(0, 5, 3).is_ok());
|
||||
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(matches!(
|
||||
&list[0],
|
||||
Diff::InsertRows {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
count: 3
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_invariant_insert_columns() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
assert!(model.insert_columns(0, 2, 4).is_ok());
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(matches!(
|
||||
&list[0],
|
||||
Diff::InsertColumns {
|
||||
sheet: 0,
|
||||
column: 2,
|
||||
count: 4
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_redo_after_batch_delete() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Place values that will shift.
|
||||
model.set_user_input(0, 20, 1, "A").unwrap();
|
||||
model.set_user_input(0, 1, 20, "B").unwrap();
|
||||
|
||||
// Fill some of the rows we are about to delete for testing
|
||||
for r in 10..15 {
|
||||
model.set_user_input(0, r, 1, "tmp").unwrap();
|
||||
}
|
||||
|
||||
// Delete rows 10..14 and columns 5..8
|
||||
assert!(model.delete_rows(0, 10, 5).is_ok());
|
||||
assert!(model.delete_columns(0, 5, 4).is_ok());
|
||||
|
||||
// Verify shift
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap(); // columns
|
||||
model.undo().unwrap(); // rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "A");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 20).unwrap(), "B");
|
||||
|
||||
// Redo
|
||||
model.redo().unwrap(); // rows
|
||||
model.redo().unwrap(); // columns
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_order_delete_rows() {
|
||||
// Verifies that delete diffs are generated with all data preserved
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Populate rows to delete
|
||||
for r in 5..10 {
|
||||
model.set_user_input(0, r, 1, &r.to_string()).unwrap();
|
||||
}
|
||||
|
||||
assert!(model.delete_rows(0, 5, 5).is_ok());
|
||||
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
// Should have one bulk diff with all the row data
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 5);
|
||||
assert_eq!(old_data.len(), 5);
|
||||
// Verify the data was collected for each row
|
||||
for (i, row_data) in old_data.iter().enumerate() {
|
||||
let _expected_value = (5 + i).to_string();
|
||||
assert!(row_data.data.contains_key(&1));
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_operations_with_formulas() {
|
||||
// Verifies formulas update correctly after batch ops
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
model.set_user_input(0, 1, 1, "10").unwrap();
|
||||
model.set_user_input(0, 5, 1, "=A1*2").unwrap(); // Will become A3 after insert
|
||||
|
||||
assert!(model.insert_rows(0, 2, 2).is_ok());
|
||||
|
||||
// Formula should now reference A1 (unchanged) but be in row 7
|
||||
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "20");
|
||||
assert_eq!(model.get_cell_content(0, 7, 1).unwrap(), "=A1*2");
|
||||
|
||||
// Undo and verify formula is back at original position
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_case_single_operation() {
|
||||
// Single row/column operations should still work correctly
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
assert!(model.insert_rows(0, 1, 1).is_ok());
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
assert!(model.insert_columns(0, 1, 1).is_ok());
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_empty_rows() {
|
||||
// Delete multiple empty rows and verify behavior
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set data in rows 1 and 10, leaving rows 5-8 empty
|
||||
model.set_user_input(0, 1, 1, "Before").unwrap();
|
||||
model.set_user_input(0, 10, 1, "After").unwrap();
|
||||
|
||||
// Delete empty rows 5-8
|
||||
assert!(model.delete_rows(0, 5, 4).is_ok());
|
||||
|
||||
// Verify shift
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
|
||||
|
||||
// Verify diffs now use bulk operation
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 4);
|
||||
assert_eq!(old_data.len(), 4);
|
||||
// All rows should be empty
|
||||
for row_data in old_data {
|
||||
assert!(row_data.data.is_empty());
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
|
||||
// Undo/redo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_mixed_empty_and_filled_rows() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Alternating filled and empty rows
|
||||
model.set_user_input(0, 5, 1, "Row5").unwrap();
|
||||
model.set_user_input(0, 7, 1, "Row7").unwrap();
|
||||
model.set_user_input(0, 9, 1, "Row9").unwrap();
|
||||
model.set_user_input(0, 10, 1, "After").unwrap();
|
||||
|
||||
assert!(model.delete_rows(0, 5, 5).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "After");
|
||||
|
||||
// Verify mix of empty and filled row diffs
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 5);
|
||||
assert_eq!(old_data.len(), 5);
|
||||
|
||||
// Count filled rows (should be 3: rows 5, 7, 9)
|
||||
let filled_count = old_data
|
||||
.iter()
|
||||
.filter(|row_data| !row_data.data.is_empty())
|
||||
.count();
|
||||
assert_eq!(filled_count, 3);
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "Row7");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 9, 1).unwrap(), "Row9");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_insert_rows_undo_redo() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up initial data
|
||||
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||
model.set_user_input(0, 2, 1, "A2").unwrap();
|
||||
model.set_user_input(0, 5, 1, "A5").unwrap();
|
||||
|
||||
// Insert 3 rows at position 3
|
||||
assert!(model.insert_rows(0, 3, 3).is_ok());
|
||||
|
||||
// Verify data has shifted
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "A2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // A5 moved to A8
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(matches!(
|
||||
&list[0],
|
||||
Diff::InsertRows {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
count: 3
|
||||
}
|
||||
));
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "A5"); // Back to original position
|
||||
|
||||
// Redo
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // Shifted again
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_insert_columns_undo_redo() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up initial data
|
||||
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||
model.set_user_input(0, 1, 2, "B1").unwrap();
|
||||
model.set_user_input(0, 1, 5, "E1").unwrap();
|
||||
|
||||
// Insert 3 columns at position 3
|
||||
assert!(model.insert_columns(0, 3, 3).is_ok());
|
||||
|
||||
// Verify data has shifted
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "B1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // E1 moved to H1
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(matches!(
|
||||
&list[0],
|
||||
Diff::InsertColumns {
|
||||
sheet: 0,
|
||||
column: 3,
|
||||
count: 3
|
||||
}
|
||||
));
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1"); // Back to original position
|
||||
|
||||
// Redo
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // Shifted again
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_rows_round_trip() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data with styles
|
||||
model.set_user_input(0, 3, 1, "Row3").unwrap();
|
||||
model.set_user_input(0, 4, 1, "Row4").unwrap();
|
||||
model.set_user_input(0, 5, 1, "Row5").unwrap();
|
||||
model.set_user_input(0, 6, 1, "Row6").unwrap();
|
||||
model.set_user_input(0, 7, 1, "After").unwrap();
|
||||
|
||||
// Set some row heights to verify they're preserved
|
||||
model.set_rows_height(0, 4, 4, 30.0).unwrap();
|
||||
model.set_rows_height(0, 5, 5, 40.0).unwrap();
|
||||
|
||||
// Delete rows 3-6
|
||||
assert!(model.delete_rows(0, 3, 4).is_ok());
|
||||
|
||||
// Verify deletion
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 3);
|
||||
assert_eq!(*count, 4);
|
||||
assert_eq!(old_data.len(), 4);
|
||||
// Verify data was preserved
|
||||
assert!(old_data[0].data.contains_key(&1)); // Row3
|
||||
assert!(old_data[1].data.contains_key(&1)); // Row4
|
||||
assert!(old_data[2].data.contains_key(&1)); // Row5
|
||||
assert!(old_data[3].data.contains_key(&1)); // Row6
|
||||
}
|
||||
_ => panic!("Expected DeleteRows diff"),
|
||||
}
|
||||
|
||||
// Undo - should restore all data and row heights
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "After");
|
||||
assert_eq!(model.get_row_height(0, 4).unwrap(), 30.0);
|
||||
assert_eq!(model.get_row_height(0, 5).unwrap(), 40.0);
|
||||
|
||||
// Redo - should delete again
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
|
||||
|
||||
// Final undo to verify round-trip
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_columns_round_trip() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data with styles
|
||||
model.set_user_input(0, 1, 3, "C1").unwrap();
|
||||
model.set_user_input(0, 1, 4, "D1").unwrap();
|
||||
model.set_user_input(0, 1, 5, "E1").unwrap();
|
||||
model.set_user_input(0, 1, 6, "F1").unwrap();
|
||||
model.set_user_input(0, 1, 7, "After").unwrap();
|
||||
|
||||
// Set some column widths to verify they're preserved
|
||||
model.set_columns_width(0, 4, 4, 100.0).unwrap();
|
||||
model.set_columns_width(0, 5, 5, 120.0).unwrap();
|
||||
|
||||
// Delete columns 3-6
|
||||
assert!(model.delete_columns(0, 3, 4).is_ok());
|
||||
|
||||
// Verify deletion
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*column, 3);
|
||||
assert_eq!(*count, 4);
|
||||
assert_eq!(old_data.len(), 4);
|
||||
// Verify data was preserved
|
||||
assert!(old_data[0].data.contains_key(&1)); // C1
|
||||
assert!(old_data[1].data.contains_key(&1)); // D1
|
||||
assert!(old_data[2].data.contains_key(&1)); // E1
|
||||
assert!(old_data[3].data.contains_key(&1)); // F1
|
||||
}
|
||||
_ => panic!("Expected DeleteColumns diff"),
|
||||
}
|
||||
|
||||
// Undo - should restore all data and column widths
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 7).unwrap(), "After");
|
||||
assert_eq!(model.get_column_width(0, 4).unwrap(), 100.0);
|
||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 120.0);
|
||||
|
||||
// Redo - should delete again
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
|
||||
|
||||
// Final undo to verify round-trip
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_bulk_operations_sequence() {
|
||||
// Test a complex sequence of bulk operations
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Initial setup
|
||||
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||
model.set_user_input(0, 2, 2, "B2").unwrap();
|
||||
model.set_user_input(0, 3, 3, "C3").unwrap();
|
||||
|
||||
// Operation 1: Insert 2 rows at position 2
|
||||
model.insert_rows(0, 2, 2).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2"); // B2 moved down
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3"); // C3 moved down
|
||||
|
||||
// Operation 2: Insert 2 columns at position 2
|
||||
model.insert_columns(0, 2, 2).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2"); // B2 moved right
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3"); // C3 moved right
|
||||
|
||||
// Operation 3: Delete the inserted rows
|
||||
model.delete_rows(0, 2, 2).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
|
||||
|
||||
// Undo all operations
|
||||
model.undo().unwrap(); // Undo delete rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3");
|
||||
|
||||
model.undo().unwrap(); // Undo insert columns
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3");
|
||||
|
||||
model.undo().unwrap(); // Undo insert rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 3).unwrap(), "C3");
|
||||
|
||||
// Redo all operations
|
||||
model.redo().unwrap(); // Redo insert rows
|
||||
model.redo().unwrap(); // Redo insert columns
|
||||
model.redo().unwrap(); // Redo delete rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_operations_with_formulas_update() {
|
||||
// Test that formulas update correctly with bulk operations
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data and formulas
|
||||
model.set_user_input(0, 1, 1, "10").unwrap();
|
||||
model.set_user_input(0, 5, 1, "20").unwrap();
|
||||
model.set_user_input(0, 10, 1, "=A1+A5").unwrap(); // Formula referencing A1 and A5
|
||||
|
||||
// Insert 3 rows at position 3
|
||||
model.insert_rows(0, 3, 3).unwrap();
|
||||
|
||||
// Formula should update to reference the shifted cells
|
||||
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "30"); // Formula moved down
|
||||
assert_eq!(model.get_cell_content(0, 13, 1).unwrap(), "=A1+A8"); // A5 became A8
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "30");
|
||||
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1+A5");
|
||||
|
||||
// Now test column insertion
|
||||
model.set_user_input(0, 1, 5, "20").unwrap(); // Add value in E1
|
||||
model.set_user_input(0, 1, 10, "=A1+E1").unwrap();
|
||||
model.insert_columns(0, 3, 2).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 12).unwrap(), "30"); // Formula moved right
|
||||
assert_eq!(model.get_cell_content(0, 1, 12).unwrap(), "=A1+G1"); // E1 became G1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_with_styles() {
|
||||
// Test that cell and row/column styles are preserved
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data with various styles
|
||||
for r in 5..10 {
|
||||
model.set_user_input(0, r, 1, &format!("Row{r}")).unwrap();
|
||||
model.set_rows_height(0, r, r, (r * 10) as f64).unwrap();
|
||||
}
|
||||
|
||||
// Delete and verify style preservation
|
||||
model.delete_rows(0, 5, 5).unwrap();
|
||||
|
||||
// Undo should restore all styles
|
||||
model.undo().unwrap();
|
||||
for r in 5..10 {
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, r, 1).unwrap(),
|
||||
format!("Row{r}")
|
||||
);
|
||||
assert_eq!(model.get_row_height(0, r).unwrap(), (r * 10) as f64);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_operations_large_count() {
|
||||
// Test operations with large counts
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Insert a large number of rows
|
||||
model.set_user_input(0, 1, 1, "Before").unwrap();
|
||||
model.set_user_input(0, 100, 1, "After").unwrap();
|
||||
|
||||
assert!(model.insert_rows(0, 50, 100).is_ok());
|
||||
|
||||
// Verify shift
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After"); // Moved by 100
|
||||
|
||||
// Check diff
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(matches!(
|
||||
&list[0],
|
||||
Diff::InsertRows {
|
||||
sheet: 0,
|
||||
row: 50,
|
||||
count: 100
|
||||
}
|
||||
));
|
||||
|
||||
// Undo and redo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 100, 1).unwrap(), "After");
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_operations_error_cases() {
|
||||
// Test error conditions
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Negative count should fail
|
||||
assert!(model.insert_rows(0, 1, -5).is_err());
|
||||
assert!(model.insert_columns(0, 1, -5).is_err());
|
||||
assert!(model.delete_rows(0, 1, -5).is_err());
|
||||
assert!(model.delete_columns(0, 1, -5).is_err());
|
||||
|
||||
// Zero count should fail
|
||||
assert!(model.insert_rows(0, 1, 0).is_err());
|
||||
assert!(model.insert_columns(0, 1, 0).is_err());
|
||||
assert!(model.delete_rows(0, 1, 0).is_err());
|
||||
assert!(model.delete_columns(0, 1, 0).is_err());
|
||||
|
||||
// Out of bounds operations should fail
|
||||
assert!(model.delete_rows(0, LAST_ROW - 5, 10).is_err());
|
||||
assert!(model.delete_columns(0, LAST_COLUMN - 5, 10).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_diff_serialization() {
|
||||
// Test that bulk diffs can be serialized/deserialized correctly
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Create some data
|
||||
model.set_user_input(0, 1, 1, "Test").unwrap();
|
||||
model.insert_rows(0, 2, 3).unwrap();
|
||||
|
||||
// Flush and get the serialized diffs
|
||||
let bytes = model.flush_send_queue();
|
||||
|
||||
// Create a new model and apply the diffs
|
||||
let base2 = new_empty_model();
|
||||
let mut model2 = UserModel::from_model(base2);
|
||||
|
||||
assert!(model2.apply_external_diffs(&bytes).is_ok());
|
||||
|
||||
// Verify the state matches
|
||||
assert_eq!(model2.get_formatted_cell_value(0, 1, 1).unwrap(), "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_validation() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Test deleting rows beyond valid range
|
||||
assert!(model.delete_rows(0, LAST_ROW, 2).is_err());
|
||||
assert!(model.delete_rows(0, LAST_ROW + 1, 1).is_err());
|
||||
|
||||
// Test deleting columns beyond valid range
|
||||
assert!(model.delete_columns(0, LAST_COLUMN, 2).is_err());
|
||||
assert!(model.delete_columns(0, LAST_COLUMN + 1, 1).is_err());
|
||||
|
||||
// Test valid boundary deletions (should work with our empty row fix)
|
||||
assert!(model.delete_rows(0, LAST_ROW, 1).is_ok());
|
||||
assert!(model.delete_columns(0, LAST_COLUMN, 1).is_ok());
|
||||
}
|
||||
@@ -50,7 +50,10 @@ fn check_borders(model: &UserModel) {
|
||||
assert_eq!(
|
||||
Some(top_border),
|
||||
top_cell_style.border.bottom,
|
||||
"(Top). Sheet: {sheet}, row: {row}, column: {column}"
|
||||
"(Top). Sheet: {}, row: {}, column: {}",
|
||||
sheet,
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +65,10 @@ fn check_borders(model: &UserModel) {
|
||||
assert_eq!(
|
||||
Some(right_border),
|
||||
right_cell_style.border.left,
|
||||
"(Right). Sheet: {sheet}, row: {row}, column: {column}"
|
||||
"(Right). Sheet: {}, row: {}, column: {}",
|
||||
sheet,
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +80,10 @@ fn check_borders(model: &UserModel) {
|
||||
assert_eq!(
|
||||
Some(bottom_border),
|
||||
bottom_cell_style.border.top,
|
||||
"(Bottom). Sheet: {sheet}, row: {row}, column: {column}"
|
||||
"(Bottom). Sheet: {}, row: {}, column: {}",
|
||||
sheet,
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +94,10 @@ fn check_borders(model: &UserModel) {
|
||||
assert_eq!(
|
||||
Some(left_border),
|
||||
left_cell_style.border.right,
|
||||
"(Left). Sheet: {sheet}, row: {row}, column: {column}"
|
||||
"(Left). Sheet: {}, row: {}, column: {}",
|
||||
sheet,
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,67 +89,3 @@ fn clear_all_empty_cell() {
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_454() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_user_input(
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
|
||||
)
|
||||
.unwrap();
|
||||
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("TRUE".to_string())
|
||||
);
|
||||
model
|
||||
.range_clear_contents(&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("FALSE".to_string())
|
||||
);
|
||||
model.undo().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_454b() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_user_input(
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
|
||||
)
|
||||
.unwrap();
|
||||
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("TRUE".to_string())
|
||||
);
|
||||
model
|
||||
.range_clear_all(&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("FALSE".to_string())
|
||||
);
|
||||
model.undo().unwrap();
|
||||
}
|
||||
|
||||
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.insert_rows(0, 3, 1).unwrap();
|
||||
model1.insert_rows(0, 3, 1).unwrap();
|
||||
model1.insert_row(0, 3).unwrap();
|
||||
model1.insert_row(0, 3).unwrap();
|
||||
|
||||
// undo al of them
|
||||
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 => ={34,35,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]
|
||||
fn user_model_debug_message() {
|
||||
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let s = &format!("{model:?}");
|
||||
let s = &format!("{:?}", model);
|
||||
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());
|
||||
|
||||
// remove the row
|
||||
assert!(model.delete_rows(0, 5, 1).is_ok());
|
||||
assert!(model.delete_row(0, 5).is_ok());
|
||||
// Row 5 has now the normal height
|
||||
assert_eq!(model.get_row_height(0, 5), Ok(height));
|
||||
// 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);
|
||||
|
||||
// remove the column
|
||||
assert!(model.delete_columns(0, 5, 1).is_ok());
|
||||
assert!(model.delete_column(0, 5).is_ok());
|
||||
// Column 5 has now the normal width
|
||||
assert_eq!(model.get_column_width(0, 5), Ok(column_width));
|
||||
// There is no value in E5
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::LAST_ROW;
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is the first row, column 5
|
||||
model.set_user_input(0, 3, 5, "todo").unwrap();
|
||||
|
||||
// Row 3 before column 5 should be empty
|
||||
assert_eq!(
|
||||
model
|
||||
.get_last_non_empty_in_row_before_column(0, 3, 4)
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
// Row 3 before column 5 should be 5
|
||||
assert_eq!(
|
||||
model
|
||||
.get_last_non_empty_in_row_before_column(0, 3, 7)
|
||||
.unwrap(),
|
||||
Some(5)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_empty_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Column 7 has a style but it is empty
|
||||
assert_eq!(
|
||||
model
|
||||
.get_last_non_empty_in_row_before_column(0, 3, 14)
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn insert_multiple_rows_shifts_cells() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// Place a value below the insertion point.
|
||||
model.set_user_input(0, 10, 1, "42").unwrap();
|
||||
|
||||
// Insert 3 rows starting at row 5.
|
||||
assert!(model.insert_rows(0, 5, 3).is_ok());
|
||||
|
||||
// The original value should have moved down by 3 rows.
|
||||
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
|
||||
|
||||
// Undo / redo cycle should restore the same behaviour.
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "42");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_rows_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Negative or zero counts are rejected.
|
||||
assert_eq!(
|
||||
model.insert_rows(0, 1, -2),
|
||||
Err("Cannot add a negative number of cells :)".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.insert_rows(0, 1, 0),
|
||||
Err("Cannot add a negative number of cells :)".to_string())
|
||||
);
|
||||
|
||||
// Inserting too many rows so that the sheet would overflow.
|
||||
let too_many = LAST_ROW; // This guarantees max_row + too_many > LAST_ROW.
|
||||
assert_eq!(
|
||||
model.insert_rows(0, 1, too_many),
|
||||
Err(
|
||||
"Cannot shift cells because that would delete cells at the end of a column".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_multiple_columns_shifts_cells() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// Place a value to the right of the insertion point.
|
||||
model.set_user_input(0, 1, 10, "99").unwrap();
|
||||
|
||||
// Insert 3 columns starting at column 5.
|
||||
assert!(model.insert_columns(0, 5, 3).is_ok());
|
||||
|
||||
// The original value should have moved right by 3 columns.
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
|
||||
|
||||
// Undo / redo cycle.
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 10).unwrap(), "99");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_columns_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Negative or zero counts are rejected.
|
||||
assert_eq!(
|
||||
model.insert_columns(0, 1, -2),
|
||||
Err("Cannot add a negative number of cells :)".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.insert_columns(0, 1, 0),
|
||||
Err("Cannot add a negative number of cells :)".to_string())
|
||||
);
|
||||
|
||||
// Overflow to the right.
|
||||
let too_many = LAST_COLUMN; // Ensures max_column + too_many > LAST_COLUMN
|
||||
assert_eq!(
|
||||
model.insert_columns(0, 1, too_many),
|
||||
Err("Cannot shift cells because that would delete cells at the end of a row".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_multiple_rows_shifts_cells_upwards() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Populate rows 10..14 (to be deleted) so that the diff builder does not fail.
|
||||
for r in 10..15 {
|
||||
model.set_user_input(0, r, 1, "del").unwrap();
|
||||
}
|
||||
// Place a value below the deletion range.
|
||||
model.set_user_input(0, 20, 1, "keep").unwrap();
|
||||
|
||||
// Delete 5 rows starting at row 10.
|
||||
assert!(model.delete_rows(0, 10, 5).is_ok());
|
||||
|
||||
// The value originally at row 20 should now be at row 15.
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
|
||||
|
||||
// Undo / redo cycle.
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "keep");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_rows_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Negative or zero counts are rejected.
|
||||
assert_eq!(
|
||||
model.delete_rows(0, 1, -3),
|
||||
Err("Please use insert rows instead".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.delete_rows(0, 1, 0),
|
||||
Err("Please use insert rows instead".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_multiple_columns_shifts_cells_left() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Place a value to the right of the deletion range.
|
||||
model.set_user_input(0, 1, 15, "88").unwrap();
|
||||
|
||||
// Delete 4 columns starting at column 5.
|
||||
assert!(model.delete_columns(0, 5, 4).is_ok());
|
||||
|
||||
// The value originally at column 15 should now be at column 11.
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
|
||||
|
||||
// Undo / redo cycle.
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 15).unwrap(), "88");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_columns_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Negative or zero counts are rejected.
|
||||
assert_eq!(
|
||||
model.delete_columns(0, 1, -4),
|
||||
Err("Please use insert columns instead".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.delete_columns(0, 1, 0),
|
||||
Err("Please use insert columns instead".to_string())
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,6 @@ fn csv_paste() {
|
||||
model.get_formatted_cell_value(0, 7, 7),
|
||||
Ok("21".to_string())
|
||||
);
|
||||
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -46,7 +45,6 @@ fn csv_paste_formula() {
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("2022".to_string())
|
||||
);
|
||||
assert_eq!([1, 1, 1, 1], model.get_selected_view().range);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -71,7 +69,6 @@ fn tsv_crlf_paste() {
|
||||
model.get_formatted_cell_value(0, 7, 7),
|
||||
Ok("21".to_string())
|
||||
);
|
||||
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -167,7 +164,7 @@ fn copy_paste_internal() {
|
||||
let copy = model.copy_to_clipboard().unwrap();
|
||||
assert_eq!(
|
||||
copy.csv,
|
||||
"42\t127\n\"A season of faith\t \"\"perfection\"\"\""
|
||||
"42\t127\n\"A season of faith\t \"\"perfection\"\"\"\t\n"
|
||||
);
|
||||
assert_eq!(copy.range, (1, 1, 2, 2));
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ fn simple_insert_row() {
|
||||
for row in 1..5 {
|
||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
||||
}
|
||||
assert!(model.insert_rows(sheet, 3, 1).is_ok());
|
||||
assert!(model.insert_row(sheet, 3).is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
||||
""
|
||||
@@ -40,7 +40,7 @@ fn simple_insert_column() {
|
||||
for column in 1..5 {
|
||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
||||
}
|
||||
assert!(model.insert_columns(sheet, 3, 1).is_ok());
|
||||
assert!(model.insert_column(sheet, 3).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
||||
|
||||
assert!(model.undo().is_ok());
|
||||
@@ -62,7 +62,7 @@ fn simple_delete_column() {
|
||||
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_columns(0, 5, 1).unwrap();
|
||||
model.delete_column(0, 5).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
|
||||
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
|
||||
@@ -92,20 +92,20 @@ fn delete_column_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.delete_columns(1, 1, 1),
|
||||
model.delete_column(1, 1),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.delete_columns(0, 0, 1),
|
||||
model.delete_column(0, 0),
|
||||
Err("Column number '0' is not valid.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.delete_columns(0, LAST_COLUMN + 1, 1),
|
||||
Err(format!("Column number '{}' is not valid.", LAST_COLUMN + 1))
|
||||
model.delete_column(0, LAST_COLUMN + 1),
|
||||
Err("Column number '16385' is not valid.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.delete_columns(0, LAST_COLUMN, 1), Ok(()));
|
||||
assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -119,7 +119,7 @@ fn simple_delete_row() {
|
||||
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_rows(0, 15, 1).unwrap();
|
||||
model.delete_row(0, 15).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
||||
assert_eq!(model.get_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);
|
||||
model.set_user_input(0, 15, 4, "3").unwrap();
|
||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||
model.delete_rows(0, 15, 1).unwrap();
|
||||
model.delete_row(0, 15).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
||||
}
|
||||
@@ -180,14 +180,14 @@ fn insert_row_evaluates() {
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
|
||||
|
||||
assert!(model.insert_rows(0, 1, 1).is_ok());
|
||||
assert!(model.insert_row(0, 1).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||
|
||||
model.delete_rows(0, 1, 1).unwrap();
|
||||
model.delete_row(0, 1).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||
assert_eq!(model.get_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, 10, 1, "=A1*2").unwrap();
|
||||
|
||||
assert!(model.insert_columns(0, 1, 1).is_ok());
|
||||
assert!(model.insert_column(0, 1).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||
|
||||
model.undo().unwrap();
|
||||
@@ -207,7 +207,7 @@ fn insert_column_evaluates() {
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||
|
||||
model.delete_columns(0, 1, 1).unwrap();
|
||||
model.delete_column(0, 1).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ fn set_the_range_does_not_set_the_cell() {
|
||||
assert_eq!(
|
||||
model.set_selected_range(5, 4, 10, 6),
|
||||
Err(
|
||||
"The selected cell is not in one of the corners. Row: '1' and row range '(5, 10)'"
|
||||
"The selected cells is not in one of the corners. Row: '1' and row range '(5, 10)'"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
@@ -11,10 +11,11 @@ impl UserModel {
|
||||
r##"{{
|
||||
"item": {{
|
||||
"style": "thin",
|
||||
"color": "{color}"
|
||||
"color": "{}"
|
||||
}},
|
||||
"type": "All"
|
||||
}}"##
|
||||
}}"##,
|
||||
color
|
||||
))
|
||||
.unwrap();
|
||||
let range = &Area {
|
||||
@@ -39,10 +40,11 @@ impl UserModel {
|
||||
r##"{{
|
||||
"item": {{
|
||||
"style": "thin",
|
||||
"color": "{color}"
|
||||
"color": "{}"
|
||||
}},
|
||||
"type": "{kind}"
|
||||
}}"##
|
||||
"type": "{}"
|
||||
}}"##,
|
||||
color, kind
|
||||
))
|
||||
.unwrap();
|
||||
let range = &Area {
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Model {
|
||||
if cell.contains('!') {
|
||||
self.parse_reference(cell).unwrap()
|
||||
} else {
|
||||
self.parse_reference(&format!("Sheet1!{cell}")).unwrap()
|
||||
self.parse_reference(&format!("Sheet1!{}", cell)).unwrap()
|
||||
}
|
||||
}
|
||||
pub fn _set(&mut self, cell: &str, value: &str) {
|
||||
|
||||
@@ -51,6 +51,9 @@ pub struct Workbook {
|
||||
pub metadata: Metadata,
|
||||
pub tables: HashMap<String, Table>,
|
||||
pub views: HashMap<u32, WorkbookView>,
|
||||
/// Calculation chain of the dynamic arrays.
|
||||
/// List of tuples (sheet_id, row, column)
|
||||
pub calc_chain: Vec<(u32, i32, i32)>,
|
||||
}
|
||||
|
||||
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
||||
@@ -159,17 +162,17 @@ pub enum CellType {
|
||||
CompoundData = 128,
|
||||
}
|
||||
|
||||
/// Cell types
|
||||
/// s is always the style index of the cell
|
||||
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
|
||||
pub enum Cell {
|
||||
EmptyCell {
|
||||
s: i32,
|
||||
},
|
||||
|
||||
BooleanCell {
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
NumberCell {
|
||||
v: f64,
|
||||
s: i32,
|
||||
@@ -181,6 +184,7 @@ pub enum Cell {
|
||||
},
|
||||
// Always a shared string
|
||||
SharedString {
|
||||
// string index
|
||||
si: i32,
|
||||
s: i32,
|
||||
},
|
||||
@@ -189,13 +193,11 @@ pub enum Cell {
|
||||
f: i32,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaBoolean {
|
||||
f: i32,
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaNumber {
|
||||
f: i32,
|
||||
v: f64,
|
||||
@@ -207,9 +209,9 @@ pub enum Cell {
|
||||
v: String,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaError {
|
||||
f: i32,
|
||||
// error index
|
||||
ei: Error,
|
||||
s: i32,
|
||||
// Origin: Sheet3!C4
|
||||
@@ -217,7 +219,81 @@ pub enum Cell {
|
||||
// Error Message: "Not implemented function"
|
||||
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 {
|
||||
@@ -303,14 +379,7 @@ impl Default for Styles {
|
||||
Styles {
|
||||
num_fmts: vec![],
|
||||
fonts: vec![Default::default()],
|
||||
fills: vec![
|
||||
Default::default(),
|
||||
Fill {
|
||||
pattern_type: "gray125".to_string(),
|
||||
fg_color: None,
|
||||
bg_color: None,
|
||||
},
|
||||
],
|
||||
fills: vec![Default::default()],
|
||||
borders: vec![Default::default()],
|
||||
cell_style_xfs: vec![Default::default()],
|
||||
cell_xfs: vec![Default::default()],
|
||||
|
||||
@@ -24,6 +24,18 @@ use crate::user_model::history::{
|
||||
};
|
||||
|
||||
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
|
||||
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
|
||||
|
||||
@@ -661,12 +673,8 @@ impl UserModel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_column_formatting(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
diff_list: &mut Vec<Diff>,
|
||||
) -> Result<(), String> {
|
||||
fn clear_column_formatting(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
let old_value = self.model.get_column_style(sheet, column)?;
|
||||
self.model.delete_column_style(sheet, column)?;
|
||||
diff_list.push(Diff::DeleteColumnStyle {
|
||||
@@ -745,15 +753,12 @@ impl UserModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_row_formatting(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
diff_list: &mut Vec<Diff>,
|
||||
) -> Result<(), String> {
|
||||
fn clear_row_formatting(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
let old_value = self.model.get_row_style(sheet, row)?;
|
||||
self.model.delete_row_style(sheet, row)?;
|
||||
diff_list.push(Diff::DeleteRowStyle {
|
||||
@@ -800,6 +805,8 @@ impl UserModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -810,21 +817,19 @@ impl UserModel {
|
||||
/// * [UserModel::range_clear_contents]
|
||||
pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
|
||||
let sheet = range.sheet;
|
||||
let mut diff_list = Vec::new();
|
||||
if range.row == 1 && range.height == LAST_ROW {
|
||||
for column in range.column..range.column + range.width {
|
||||
self.clear_column_formatting(sheet, column, &mut diff_list)?;
|
||||
self.clear_column_formatting(sheet, column)?;
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
return Ok(());
|
||||
}
|
||||
if range.column == 1 && range.width == LAST_COLUMN {
|
||||
for row in range.row..range.row + range.height {
|
||||
self.clear_row_formatting(sheet, row, &mut diff_list)?;
|
||||
self.clear_row_formatting(sheet, row)?;
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
return Ok(());
|
||||
}
|
||||
let mut diff_list = Vec::new();
|
||||
for row in range.row..range.row + range.height {
|
||||
for column in range.column..range.column + range.width {
|
||||
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
|
||||
@@ -860,184 +865,105 @@ impl UserModel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts `row_count` blank rows starting at `row` (both 0-based).
|
||||
/// Inserts a row
|
||||
///
|
||||
/// Parameters
|
||||
/// * `sheet` – worksheet index.
|
||||
/// * `row` – first row to insert.
|
||||
/// * `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,
|
||||
}];
|
||||
/// See also:
|
||||
/// * [Model::insert_rows]
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertRow { sheet, row }];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.insert_rows(sheet, row, 1)?;
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts `column_count` blank columns starting at `column` (0-based).
|
||||
/// Deletes a row
|
||||
///
|
||||
/// Parameters
|
||||
/// * `sheet` – worksheet index.
|
||||
/// * `column` – first column to insert.
|
||||
/// * `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> {
|
||||
/// See also:
|
||||
/// * [Model::delete_rows]
|
||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||
let mut row_data = None;
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
let mut old_data = Vec::new();
|
||||
// Collect data for all rows to be deleted
|
||||
for r in row..row + row_count {
|
||||
let mut row_data = None;
|
||||
for rd in &worksheet.rows {
|
||||
if rd.r == r {
|
||||
row_data = Some(rd.clone());
|
||||
break;
|
||||
}
|
||||
for rd in &worksheet.rows {
|
||||
if rd.r == row {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
self.model.delete_rows(sheet, row, row_count)?;
|
||||
|
||||
let diff_list = vec![Diff::DeleteRows {
|
||||
let data = match worksheet.sheet_data.get(&row) {
|
||||
Some(s) => s.clone(),
|
||||
None => return Err(format!("Row number '{row}' is not valid.")),
|
||||
};
|
||||
let old_data = Box::new(RowData {
|
||||
row: row_data,
|
||||
data,
|
||||
});
|
||||
let diff_list = vec![Diff::DeleteRow {
|
||||
sheet,
|
||||
row,
|
||||
count: row_count,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.delete_rows(sheet, row, 1)?;
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes `column_count` columns starting at `column`.
|
||||
/// Inserts a column
|
||||
///
|
||||
/// History: pushes one [`crate::user_model::history::Diff::DeleteColumn`]
|
||||
/// per column, **right → left**, so replaying the list is always safe with
|
||||
/// respect to index shifts.
|
||||
/// See also:
|
||||
/// * [Model::insert_columns]
|
||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertColumn { sheet, column }];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.insert_columns(sheet, column, 1)?;
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a column
|
||||
///
|
||||
/// See also [`Model::delete_columns`].
|
||||
pub fn delete_columns(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
column_count: i32,
|
||||
) -> Result<(), String> {
|
||||
/// See also:
|
||||
/// * [Model::delete_columns]
|
||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
let mut old_data = Vec::new();
|
||||
// Collect data for all columns to be deleted
|
||||
for c in column..column + column_count {
|
||||
let mut column_data = None;
|
||||
for col in &worksheet.cols {
|
||||
if c >= col.min && c <= col.max {
|
||||
column_data = Some(Col {
|
||||
min: c,
|
||||
max: c,
|
||||
width: col.width,
|
||||
custom_width: col.custom_width,
|
||||
style: col.style,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Column number '{column}' is not valid."));
|
||||
}
|
||||
|
||||
let mut data = HashMap::new();
|
||||
for (row_idx, row_data) in &worksheet.sheet_data {
|
||||
if let Some(cell) = row_data.get(&c) {
|
||||
data.insert(*row_idx, cell.clone());
|
||||
}
|
||||
let mut column_data = None;
|
||||
for col in &worksheet.cols {
|
||||
let min = col.min;
|
||||
let max = col.max;
|
||||
if column >= min && column <= max {
|
||||
column_data = Some(Col {
|
||||
min: column,
|
||||
max: column,
|
||||
width: col.width,
|
||||
custom_width: col.custom_width,
|
||||
style: col.style,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
old_data.push(ColumnData {
|
||||
let mut data = HashMap::new();
|
||||
for (row, row_data) in &worksheet.sheet_data {
|
||||
if let Some(cell) = row_data.get(&column) {
|
||||
data.insert(*row, cell.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let diff_list = vec![Diff::DeleteColumn {
|
||||
sheet,
|
||||
column,
|
||||
old_data: Box::new(ColumnData {
|
||||
column: column_data,
|
||||
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.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.model.delete_columns(sheet, column, 1)?;
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
}
|
||||
@@ -1575,10 +1501,10 @@ impl UserModel {
|
||||
return Err(format!("Invalid row: '{first_row}'"));
|
||||
}
|
||||
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) {
|
||||
return Err(format!("Invalid row: '{last_row}'"));
|
||||
return Err(format!("Invalid row: '{}'", last_row));
|
||||
}
|
||||
|
||||
if !is_valid_row(to_column) {
|
||||
@@ -1683,64 +1609,63 @@ impl UserModel {
|
||||
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(
|
||||
/// Returns the geometric structure of a cell
|
||||
pub fn get_cell_array_structure(
|
||||
&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);
|
||||
}
|
||||
) -> 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,
|
||||
))
|
||||
}
|
||||
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));
|
||||
}
|
||||
Cell::DynamicCellFormula { r, .. }
|
||||
| Cell::DynamicCellFormulaBoolean { r, .. }
|
||||
| Cell::DynamicCellFormulaNumber { r, .. }
|
||||
| Cell::DynamicCellFormulaString { r, .. }
|
||||
| Cell::DynamicCellFormulaError { r, .. } => {
|
||||
Ok(CellArrayStructure::DynamicMother(r.0, r.1))
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns a copy of the selected area
|
||||
@@ -1771,18 +1696,18 @@ impl UserModel {
|
||||
text_row.push(text);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
let csv = String::from_utf8(
|
||||
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 {
|
||||
csv: csv.trim().to_string(),
|
||||
csv,
|
||||
data,
|
||||
sheet,
|
||||
range: (row_start, column_start, row_end, column_end),
|
||||
@@ -1950,7 +1875,7 @@ impl UserModel {
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
// select the pasted area
|
||||
self.set_selected_range(area.row, area.column, row - 1, column - 1)?;
|
||||
self.set_selected_range(area.row, area.column, row, column)?;
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
}
|
||||
@@ -2045,6 +1970,24 @@ impl UserModel {
|
||||
old_value,
|
||||
} => {
|
||||
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() {
|
||||
Some(value) => {
|
||||
self.model
|
||||
@@ -2115,56 +2058,45 @@ impl UserModel {
|
||||
self.model.cell_clear_all(*sheet, *row, *column)?;
|
||||
}
|
||||
}
|
||||
Diff::InsertRows { sheet, row, count } => {
|
||||
self.model.delete_rows(*sheet, *row, *count)?;
|
||||
Diff::InsertRow { sheet, row } => {
|
||||
self.model.delete_rows(*sheet, *row, 1)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteRows {
|
||||
Diff::DeleteRow {
|
||||
sheet,
|
||||
row,
|
||||
count: _,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model
|
||||
.insert_rows(*sheet, *row, old_data.len() as i32)?;
|
||||
self.model.insert_rows(*sheet, *row, 1)?;
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for (i, row_data) in old_data.iter().enumerate() {
|
||||
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());
|
||||
if let Some(row_data) = old_data.row.clone() {
|
||||
worksheet.rows.push(row_data);
|
||||
}
|
||||
worksheet.sheet_data.insert(*row, old_data.data.clone());
|
||||
}
|
||||
Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
} => {
|
||||
self.model.delete_columns(*sheet, *column, *count)?;
|
||||
Diff::InsertColumn { sheet, column } => {
|
||||
self.model.delete_columns(*sheet, *column, 1)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteColumns {
|
||||
Diff::DeleteColumn {
|
||||
sheet,
|
||||
column,
|
||||
count: _,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model
|
||||
.insert_columns(*sheet, *column, old_data.len() as i32)?;
|
||||
// inserts an empty column
|
||||
self.model.insert_columns(*sheet, *column, 1)?;
|
||||
// puts all the data back
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for (i, col_data) in old_data.iter().enumerate() {
|
||||
let c = *column + i as i32;
|
||||
for (row, cell) in &col_data.data {
|
||||
worksheet.update_cell(*row, c, cell.clone())?;
|
||||
}
|
||||
if let Some(col) = &col_data.column {
|
||||
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
|
||||
let style = col.style;
|
||||
worksheet.set_column_width_and_style(c, width, style)?;
|
||||
}
|
||||
for (row, cell) in &old_data.data {
|
||||
worksheet.update_cell(*row, *column, cell.clone())?;
|
||||
}
|
||||
// makes sure that the width and style is correct
|
||||
if let Some(col) = &old_data.column {
|
||||
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
|
||||
let style = col.style;
|
||||
worksheet.set_column_width_and_style(*column, width, style)?;
|
||||
}
|
||||
}
|
||||
Diff::SetFrozenRowsCount {
|
||||
@@ -2322,21 +2254,6 @@ impl UserModel {
|
||||
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 {
|
||||
@@ -2405,34 +2322,28 @@ impl UserModel {
|
||||
} => self
|
||||
.model
|
||||
.set_cell_style(*sheet, *row, *column, new_value)?,
|
||||
Diff::InsertRows { sheet, row, count } => {
|
||||
self.model.insert_rows(*sheet, *row, *count)?;
|
||||
Diff::InsertRow { sheet, row } => {
|
||||
self.model.insert_rows(*sheet, *row, 1)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteRows {
|
||||
Diff::DeleteRow {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_rows(*sheet, *row, *count)?;
|
||||
self.model.delete_rows(*sheet, *row, 1)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
} => {
|
||||
self.model.insert_columns(*sheet, *column, *count)?;
|
||||
Diff::InsertColumn { sheet, column } => {
|
||||
needs_evaluation = true;
|
||||
self.model.insert_columns(*sheet, *column, 1)?;
|
||||
}
|
||||
Diff::DeleteColumns {
|
||||
Diff::DeleteColumn {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_columns(*sheet, *column, *count)?;
|
||||
self.model.delete_columns(*sheet, *column, 1)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::SetFrozenRowsCount {
|
||||
@@ -2544,18 +2455,6 @@ impl UserModel {
|
||||
} => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2583,7 +2482,7 @@ mod tests {
|
||||
VerticalAlignment::Top,
|
||||
];
|
||||
for a in all {
|
||||
assert_eq!(vertical(&format!("{a}")), Ok(a));
|
||||
assert_eq!(vertical(&format!("{}", a)), Ok(a));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2600,7 +2499,7 @@ mod tests {
|
||||
HorizontalAlignment::Right,
|
||||
];
|
||||
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};
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||
pub(crate) struct RowData {
|
||||
pub(crate) row: Option<Row>,
|
||||
pub(crate) data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||
pub(crate) struct ColumnData {
|
||||
pub(crate) column: Option<Col>,
|
||||
pub(crate) data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||
pub(crate) enum Diff {
|
||||
// Cell diffs
|
||||
SetCellValue {
|
||||
@@ -87,27 +90,23 @@ pub(crate) enum Diff {
|
||||
row: i32,
|
||||
old_value: Box<Option<Style>>,
|
||||
},
|
||||
InsertRows {
|
||||
InsertRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
count: i32,
|
||||
},
|
||||
DeleteRows {
|
||||
DeleteRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
count: i32,
|
||||
old_data: Vec<RowData>,
|
||||
old_data: Box<RowData>,
|
||||
},
|
||||
InsertColumns {
|
||||
InsertColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
count: i32,
|
||||
},
|
||||
DeleteColumns {
|
||||
DeleteColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
count: i32,
|
||||
old_data: Vec<ColumnData>,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
@@ -165,16 +164,6 @@ pub(crate) enum Diff {
|
||||
new_scope: Option<u32>,
|
||||
new_formula: String,
|
||||
},
|
||||
MoveColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
MoveRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
delta: i32,
|
||||
},
|
||||
// FIXME: we are missing SetViewDiffs
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
mod border;
|
||||
mod border_utils;
|
||||
mod common;
|
||||
pub(crate) mod history;
|
||||
mod history;
|
||||
mod ui;
|
||||
|
||||
pub use common::UserModel;
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
expressions::utils::{is_valid_column_number, is_valid_row},
|
||||
worksheet::NavigationDirection,
|
||||
};
|
||||
use crate::expressions::utils::{is_valid_column_number, is_valid_row};
|
||||
|
||||
use super::common::UserModel;
|
||||
|
||||
@@ -80,7 +76,7 @@ impl UserModel {
|
||||
/// Sets the the selected sheet
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||
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) {
|
||||
view.sheet = sheet;
|
||||
@@ -102,7 +98,7 @@ impl UserModel {
|
||||
return Err(format!("Invalid row: '{row}'"));
|
||||
}
|
||||
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 Some(view) = worksheet.views.get_mut(&0) {
|
||||
@@ -114,7 +110,7 @@ impl UserModel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected range. Note that the selected cell must be in the selected range.
|
||||
/// Sets the selected range. Note that the selected cell must be in one of the corners.
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
@@ -142,38 +138,24 @@ impl UserModel {
|
||||
return Err(format!("Invalid row: '{end_row}'"));
|
||||
}
|
||||
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 Some(view) = worksheet.views.get_mut(&0) {
|
||||
let selected_row = view.row;
|
||||
let selected_column = view.column;
|
||||
if start_row == 1 && end_row == LAST_ROW {
|
||||
// full row selected. The cell must be at the top or the bottom of the range
|
||||
if selected_column != start_column && selected_column != end_column {
|
||||
return Err(format!(
|
||||
"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})'"
|
||||
// The selected cells 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 cells is not in one of the corners. Row: '{}' and row range '({}, {})'",
|
||||
selected_row, start_row, end_row
|
||||
));
|
||||
}
|
||||
if selected_column != start_column && selected_column != end_column {
|
||||
return Err(format!(
|
||||
"The selected cell is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
|
||||
}
|
||||
if selected_column != start_column && selected_column != end_column {
|
||||
return Err(format!(
|
||||
"The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'",
|
||||
selected_column, start_column, end_column
|
||||
));
|
||||
}
|
||||
}
|
||||
view.range = [start_row, start_column, end_row, end_column];
|
||||
}
|
||||
@@ -210,17 +192,6 @@ impl UserModel {
|
||||
return Ok(());
|
||||
};
|
||||
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 {
|
||||
"ArrowRight" => {
|
||||
@@ -336,7 +307,7 @@ impl UserModel {
|
||||
return Err(format!("Invalid row: '{top_row}'"));
|
||||
}
|
||||
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 Some(view) = worksheet.views.get_mut(&0) {
|
||||
@@ -503,7 +474,7 @@ impl UserModel {
|
||||
// if the row is not fully visible we 'scroll' down until it is
|
||||
let mut height = 0.0;
|
||||
let mut row = view.top_row;
|
||||
while row <= new_row + 1 && row <= LAST_ROW {
|
||||
while row <= new_row + 1 {
|
||||
height += self.model.get_row_height(sheet, row)?;
|
||||
row += 1;
|
||||
}
|
||||
@@ -713,94 +684,4 @@ impl UserModel {
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "ironcalc_nodejs"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] }
|
||||
napi-derive = "2.12.2"
|
||||
ironcalc = { path = "../../xlsx", version = "0.6.0" }
|
||||
ironcalc = { path = "../../xlsx", version = "0.5.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -4,20 +4,15 @@
|
||||
Example usage:
|
||||
|
||||
```javascript
|
||||
import { Model } from '@ironcalc/nodejs';
|
||||
import { Model } from '@ironcalc/wasm';
|
||||
|
||||
const model = new Model("Workbook1", "en", "UTC");
|
||||
|
||||
model.setUserInput(0, 1, 1, "=1+1");
|
||||
|
||||
const result1 = model.getFormattedCellValue(0, 1, 1);
|
||||
console.log('Cell value', result1); // "#ERROR"
|
||||
|
||||
model.evaluate();
|
||||
|
||||
const resultAfterEvaluate = model.getFormattedCellValue(0, 1, 1);
|
||||
console.log('Cell value', resultAfterEvaluate); // 2
|
||||
console.log('Cell value', result1);
|
||||
|
||||
let result2 = model.getCellStyle(0, 1, 1);
|
||||
console.log('Cell style', result2);
|
||||
```
|
||||
```
|
||||
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
|
||||
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
|
||||
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
|
||||
insertRows(sheet: number, row: number, rowCount: number): void
|
||||
insertColumns(sheet: number, column: number, columnCount: number): void
|
||||
deleteRows(sheet: number, row: number, rowCount: number): void
|
||||
deleteColumns(sheet: number, column: number, columnCount: number): void
|
||||
insertRow(sheet: number, row: number): void
|
||||
insertColumn(sheet: number, column: number): void
|
||||
deleteRow(sheet: number, row: number): void
|
||||
deleteColumn(sheet: number, column: number): void
|
||||
setRowHeight(sheet: number, row: number, height: number): void
|
||||
setColumnWidth(sheet: number, column: number, width: number): void
|
||||
getRowHeight(sheet: number, row: number): number
|
||||
|
||||
@@ -340,20 +340,4 @@ impl Model {
|
||||
.delete_defined_name(&name, scope)
|
||||
.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,36 +183,24 @@ impl UserModel {
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "insertRows")]
|
||||
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
|
||||
self
|
||||
.model
|
||||
.insert_rows(sheet, row, row_count)
|
||||
.map_err(to_js_error)
|
||||
#[napi(js_name = "insertRow")]
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "insertColumns")]
|
||||
pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
|
||||
self
|
||||
.model
|
||||
.insert_columns(sheet, column, column_count)
|
||||
.map_err(to_js_error)
|
||||
#[napi(js_name = "insertColumn")]
|
||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "deleteRows")]
|
||||
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
|
||||
self
|
||||
.model
|
||||
.delete_rows(sheet, row, row_count)
|
||||
.map_err(to_js_error)
|
||||
#[napi(js_name = "deleteRow")]
|
||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "deleteColumns")]
|
||||
pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
|
||||
self
|
||||
.model
|
||||
.delete_columns(sheet, column, column_count)
|
||||
.map_err(to_js_error)
|
||||
#[napi(js_name = "deleteColumn")]
|
||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "setRowsHeight")]
|
||||
@@ -663,20 +651,4 @@ impl UserModel {
|
||||
.delete_defined_name(&name, scope)
|
||||
.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]
|
||||
name = "pyroncalc"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -12,9 +12,8 @@ crate-type = ["cdylib"]
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.6.0" }
|
||||
pyo3 = { version = "0.25", features = ["extension-module"] }
|
||||
bitcode = "0.6.3"
|
||||
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
|
||||
pyo3 = { version = "0.23", features = ["extension-module"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -10,6 +10,9 @@ You can add cell values, retrieve them and most importantly you can evaluate spr
|
||||
pip install ironcalc
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Compile and test
|
||||
|
||||
To compile this and test it:
|
||||
@@ -25,18 +28,4 @@ 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.
|
||||
|
||||
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/
|
||||
```
|
||||
Hopefully the API is straightforward.
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install patchelf
|
||||
pip install maturin
|
||||
pip install sphinx
|
||||
maturin develop
|
||||
sphinx-build -M html docs html
|
||||
python -m http.server --directory html/html/
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
Raw API Reference
|
||||
-----------------
|
||||
API Reference
|
||||
-------------
|
||||
|
||||
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.
|
||||
|
||||
@@ -28,7 +28,7 @@ In general methods in IronCalc use a 0-index base for the the sheet index and 1-
|
||||
|
||||
.. method:: get_cell_content(sheet: int, row: int, column: int) -> str
|
||||
|
||||
Returns the raw content of a cell. If the cell contains a formula,
|
||||
Returns the raw content of a cell. If the cell contains a formula,
|
||||
the returned string starts with ``"="``.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
@@ -47,7 +47,7 @@ In general methods in IronCalc use a 0-index base for the the sheet index and 1-
|
||||
|
||||
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
|
||||
|
||||
Returns the cell’s value as a formatted string, taking into
|
||||
Returns the cell’s value as a formatted string, taking into
|
||||
account any number/currency/date formatting.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
@@ -167,7 +167,7 @@ In general methods in IronCalc use a 0-index base for the the sheet index and 1-
|
||||
|
||||
.. method:: get_worksheets_properties() -> List[PySheetProperty]
|
||||
|
||||
Returns a list of :class:`PySheetProperty` describing each worksheet’s
|
||||
Returns a list of :class:`PySheetProperty` describing each worksheet’s
|
||||
name, visibility state, ID, and tab color.
|
||||
|
||||
:rtype: list of PySheetProperty
|
||||
@@ -204,7 +204,7 @@ In general methods in IronCalc use a 0-index base for the the sheet index and 1-
|
||||
|
||||
.. method:: test_panic()
|
||||
|
||||
A test method that deliberately panics in Rust.
|
||||
A test method that deliberately panics in Rust.
|
||||
Used for testing panic handling at the method level.
|
||||
|
||||
:raises WorkbookError: (wrapped Rust panic)
|
||||
@@ -8,8 +8,7 @@ IronCalc
|
||||
installation
|
||||
usage_examples
|
||||
top_level_methods
|
||||
raw_api_reference
|
||||
user_api_reference
|
||||
api_reference
|
||||
objects
|
||||
|
||||
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
Top Level Methods
|
||||
-----------------
|
||||
|
||||
This module provides a set of top-level methods for creating and loading IronCalc models.
|
||||
|
||||
.. autofunction:: ironcalc.create
|
||||
.. autofunction:: ironcalc.load_from_xlsx
|
||||
.. 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
|
||||
.. autofunction:: ironcalc.load_from_icalc
|
||||
@@ -1,41 +0,0 @@
|
||||
User API Reference
|
||||
------------------
|
||||
|
||||
This is the "user api". Models here have history, they evaluate automatically with each change and have a "diff" history.
|
||||
|
||||
|
||||
.. method:: save_to_xlsx(file: str)
|
||||
|
||||
Saves the user model to file in the XLSX format.
|
||||
|
||||
::param file: The file path to save the model to.
|
||||
|
||||
.. method:: save_to_icalc(file: str)
|
||||
|
||||
Saves the user model to file in the internal binary ic format.
|
||||
|
||||
::param file: The file path to save the model to.
|
||||
|
||||
.. method:: apply_external_diffs(external_diffs: bytes)
|
||||
|
||||
Applies external diffs to the model. This is used to apply changes from other instances of the model.
|
||||
|
||||
::param external_diffs: The external diffs to apply, as a byte array.
|
||||
|
||||
.. method:: flush_send_queue() -> bytes
|
||||
|
||||
Flushes the send queue and returns the bytes to be sent to the client. This is used to send changes to the client.
|
||||
|
||||
.. method:: set_user_input(sheet: int, row: int, column: int, value: str)
|
||||
|
||||
Sets an input in a cell, as would be done by a user typing into a spreadsheet cell.
|
||||
|
||||
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
|
||||
|
||||
Returns the 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]
|
||||
name = "ironcalc"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
description = "Create, edit and evaluate Excel spreadsheets"
|
||||
requires-python = ">=3.10"
|
||||
keywords = [
|
||||
|
||||
@@ -1,89 +1,19 @@
|
||||
use pyo3::exceptions::PyException;
|
||||
use pyo3::{create_exception, prelude::*, wrap_pyfunction};
|
||||
|
||||
use types::{PyCellType, PySheetProperty, PyStyle};
|
||||
use xlsx::base::types::{Style, Workbook};
|
||||
use xlsx::base::{Model, UserModel};
|
||||
use types::{PySheetProperty, PyStyle};
|
||||
use xlsx::base::types::Style;
|
||||
use xlsx::base::Model;
|
||||
|
||||
use xlsx::export::{save_to_icalc, save_to_xlsx};
|
||||
use xlsx::import;
|
||||
|
||||
mod types;
|
||||
|
||||
use crate::types::PyCellType;
|
||||
|
||||
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
|
||||
#[pyclass]
|
||||
pub struct PyModel {
|
||||
@@ -102,12 +32,6 @@ impl PyModel {
|
||||
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
|
||||
pub fn evaluate(&mut self) {
|
||||
self.model.evaluate()
|
||||
@@ -301,24 +225,6 @@ impl PyModel {
|
||||
.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)]
|
||||
pub fn test_panic(&self) -> PyResult<()> {
|
||||
panic!("This function panics for testing panic handling");
|
||||
@@ -343,19 +249,7 @@ pub fn load_from_icalc(file_name: &str) -> PyResult<PyModel> {
|
||||
Ok(PyModel { model })
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// Creates an empty model
|
||||
#[pyfunction]
|
||||
pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
|
||||
let model =
|
||||
@@ -363,49 +257,6 @@ pub fn create(name: &str, locale: &str, tz: &str) -> PyResult<PyModel> {
|
||||
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]
|
||||
#[allow(clippy::panic)]
|
||||
pub fn test_panic() {
|
||||
@@ -421,14 +272,7 @@ fn ironcalc(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(create, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(load_from_xlsx, 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)?)?;
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
@@ -6,52 +6,3 @@ def test_simple():
|
||||
model.evaluate()
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasm"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
|
||||
description = "IronCalc Web bindings"
|
||||
license = "MIT/Apache-2.0"
|
||||
@@ -14,9 +14,9 @@ crate-type = ["cdylib"]
|
||||
# Uses `../ironcalc/base` when used locally, and uses
|
||||
# the inicated version from crates.io when published.
|
||||
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
||||
ironcalc_base = { path = "../../base", version = "0.6", features = ["use_regex_lite"] }
|
||||
ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen = "0.2.92"
|
||||
serde-wasm-bindgen = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -26,4 +26,4 @@ clean:
|
||||
rm -rf pkg
|
||||
rm -f types.js
|
||||
|
||||
.PHONY: all lint clean tests
|
||||
.PHONY: all lint clean
|
||||
|
||||
@@ -1,24 +1,243 @@
|
||||
# Regrettably at the time of writing there is not a perfect way to
|
||||
# generate the TypeScript types from Rust so we basically fix them manually
|
||||
# Hopefully this will suffice for our needs and one day will be automatic
|
||||
|
||||
header = r"""
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
""".strip()
|
||||
|
||||
def fix_types(text: str):
|
||||
get_tokens_str = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getTokens(formula: string): any;
|
||||
""".strip()
|
||||
|
||||
get_tokens_str_types = r"""
|
||||
* @returns {MarkedToken[]}
|
||||
*/
|
||||
export function getTokens(formula: string): MarkedToken[];
|
||||
""".strip()
|
||||
|
||||
update_style_str = r"""
|
||||
/**
|
||||
* @param {any} range
|
||||
* @param {string} style_path
|
||||
* @param {string} value
|
||||
*/
|
||||
updateRangeStyle(range: any, style_path: string, value: string): void;
|
||||
""".strip()
|
||||
|
||||
update_style_str_types = r"""
|
||||
/**
|
||||
* @param {Area} range
|
||||
* @param {string} style_path
|
||||
* @param {string} value
|
||||
*/
|
||||
updateRangeStyle(range: Area, style_path: string, value: string): void;
|
||||
""".strip()
|
||||
|
||||
properties = r"""
|
||||
/**
|
||||
* @returns {any}
|
||||
*/
|
||||
getWorksheetsProperties(): any;
|
||||
""".strip()
|
||||
|
||||
properties_types = r"""
|
||||
/**
|
||||
* @returns {WorksheetProperties[]}
|
||||
*/
|
||||
getWorksheetsProperties(): WorksheetProperties[];
|
||||
""".strip()
|
||||
|
||||
style = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
getCellStyle(sheet: number, row: number, column: number): any;
|
||||
""".strip()
|
||||
|
||||
style_types = r"""
|
||||
* @returns {CellStyle}
|
||||
*/
|
||||
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
||||
""".strip()
|
||||
|
||||
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[];
|
||||
"""
|
||||
|
||||
cell_structure = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
getCellArrayStructure(sheet: number, row: number, column: number): any;
|
||||
"""
|
||||
|
||||
cell_structure_types = r"""
|
||||
* @returns {CellArrayStructure}
|
||||
*/
|
||||
getCellArrayStructure(sheet: number, row: number, column: number): CellArrayStructure;
|
||||
"""
|
||||
|
||||
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)
|
||||
text = text.replace(cell_structure, cell_structure_types)
|
||||
with open("types.ts") as f:
|
||||
types_str = f.read()
|
||||
header_types = "{}\n\n{}".format(header, types_str)
|
||||
|
||||
text = text.replace(header, header_types)
|
||||
for line in text.splitlines():
|
||||
line = line.lstrip()
|
||||
# Skip internal methods
|
||||
if line.startswith("readonly model_"):
|
||||
continue
|
||||
if line.find("any") != -1:
|
||||
print("There are 'unfixed' public types. Please check.")
|
||||
exit(1)
|
||||
|
||||
if text.find("any") != -1:
|
||||
print("There are 'unfixed' types. Please check.")
|
||||
exit(1)
|
||||
return text
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
types_file = "pkg/wasm.d.ts"
|
||||
@@ -36,4 +255,6 @@ if __name__ == "__main__":
|
||||
|
||||
with open(js_file, "wb") as f:
|
||||
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use wasm_bindgen::{
|
||||
use ironcalc_base::{
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
|
||||
types::{CellType, Style},
|
||||
worksheet::NavigationDirection,
|
||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||
};
|
||||
|
||||
@@ -17,7 +16,7 @@ fn to_js_error(error: String) -> JsError {
|
||||
|
||||
/// Return an array with a list of all the tokens from a formula
|
||||
/// This is used by the UI to color them according to a theme.
|
||||
#[wasm_bindgen(js_name = "getTokens", unchecked_return_type = "MarkedToken[]")]
|
||||
#[wasm_bindgen(js_name = "getTokens")]
|
||||
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
||||
let tokens = tokenizer(formula);
|
||||
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
||||
@@ -196,61 +195,24 @@ impl Model {
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "insertRows")]
|
||||
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.insert_rows(sheet, row, row_count)
|
||||
.map_err(to_js_error)
|
||||
#[wasm_bindgen(js_name = "insertRow")]
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "insertColumns")]
|
||||
pub fn insert_columns(
|
||||
&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 = "insertColumn")]
|
||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "deleteRows")]
|
||||
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.delete_rows(sheet, row, row_count)
|
||||
.map_err(to_js_error)
|
||||
#[wasm_bindgen(js_name = "deleteRow")]
|
||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "deleteColumns")]
|
||||
pub fn delete_columns(
|
||||
&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 = "deleteColumn")]
|
||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setRowsHeight")]
|
||||
@@ -376,7 +338,7 @@ impl Model {
|
||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
||||
pub fn update_range_style(
|
||||
&mut self,
|
||||
#[wasm_bindgen(unchecked_param_type = "Area")] range: JsValue,
|
||||
range: JsValue,
|
||||
style_path: &str,
|
||||
value: &str,
|
||||
) -> Result<(), JsError> {
|
||||
@@ -387,7 +349,7 @@ impl Model {
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getCellStyle", unchecked_return_type = "CellStyle")]
|
||||
#[wasm_bindgen(js_name = "getCellStyle")]
|
||||
pub fn get_cell_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
@@ -403,10 +365,7 @@ impl Model {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onPasteStyles")]
|
||||
pub fn on_paste_styles(
|
||||
&mut self,
|
||||
#[wasm_bindgen(unchecked_param_type = "CellStyle[][]")] styles: JsValue,
|
||||
) -> Result<(), JsError> {
|
||||
pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> {
|
||||
let styles: &Vec<Vec<Style>> =
|
||||
&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)
|
||||
@@ -432,10 +391,7 @@ impl Model {
|
||||
|
||||
// I don't _think_ serializing to JsValue can't fail
|
||||
// FIXME: Remove this clippy directive
|
||||
#[wasm_bindgen(
|
||||
js_name = "getWorksheetsProperties",
|
||||
unchecked_return_type = "WorksheetProperties[]"
|
||||
)]
|
||||
#[wasm_bindgen(js_name = "getWorksheetsProperties")]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
||||
@@ -454,7 +410,7 @@ impl Model {
|
||||
|
||||
// I don't _think_ serializing to JsValue can't fail
|
||||
// FIXME: Remove this clippy directive
|
||||
#[wasm_bindgen(js_name = "getSelectedView", unchecked_return_type = "SelectedView")]
|
||||
#[wasm_bindgen(js_name = "getSelectedView")]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn get_selected_view(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
||||
@@ -513,11 +469,7 @@ impl Model {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
||||
pub fn auto_fill_rows(
|
||||
&mut self,
|
||||
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
|
||||
to_row: i32,
|
||||
) -> Result<(), JsError> {
|
||||
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
@@ -528,7 +480,7 @@ impl Model {
|
||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
||||
pub fn auto_fill_columns(
|
||||
&mut self,
|
||||
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
|
||||
source_area: JsValue,
|
||||
to_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
@@ -568,20 +520,6 @@ impl Model {
|
||||
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")]
|
||||
pub fn set_window_width(&mut self, window_width: f64) {
|
||||
self.model.set_window_width(window_width);
|
||||
@@ -623,8 +561,8 @@ impl Model {
|
||||
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
||||
pub fn set_area_with_border(
|
||||
&mut self,
|
||||
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
|
||||
#[wasm_bindgen(unchecked_param_type = "BorderArea")] border_area: JsValue,
|
||||
area: JsValue,
|
||||
border_area: JsValue,
|
||||
) -> Result<(), JsError> {
|
||||
let range: Area =
|
||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
@@ -651,7 +589,7 @@ impl Model {
|
||||
self.model.set_name(name);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "copyToClipboard", unchecked_return_type = "Clipboard")]
|
||||
#[wasm_bindgen(js_name = "copyToClipboard")]
|
||||
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
|
||||
let data = self
|
||||
.model
|
||||
@@ -665,9 +603,8 @@ impl Model {
|
||||
pub fn paste_from_clipboard(
|
||||
&mut self,
|
||||
source_sheet: u32,
|
||||
#[wasm_bindgen(unchecked_param_type = "[number, number, number, number]")]
|
||||
source_range: JsValue,
|
||||
#[wasm_bindgen(unchecked_param_type = "ClipboardData")] clipboard: JsValue,
|
||||
clipboard: JsValue,
|
||||
is_cut: bool,
|
||||
) -> Result<(), JsError> {
|
||||
let source_range: (i32, i32, i32, i32) =
|
||||
@@ -680,11 +617,7 @@ impl Model {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "pasteCsvText")]
|
||||
pub fn paste_csv_string(
|
||||
&mut self,
|
||||
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
|
||||
csv: &str,
|
||||
) -> Result<(), JsError> {
|
||||
pub fn paste_csv_string(&mut self, area: JsValue, csv: &str) -> Result<(), JsError> {
|
||||
let range: Area =
|
||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
@@ -692,10 +625,7 @@ impl Model {
|
||||
.map_err(|e| to_js_error(e.to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(
|
||||
js_name = "getDefinedNameList",
|
||||
unchecked_return_type = "DefinedName[]"
|
||||
)]
|
||||
#[wasm_bindgen(js_name = "getDefinedNameList")]
|
||||
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
|
||||
let data: Vec<DefinedName> = self
|
||||
.model
|
||||
@@ -743,27 +673,17 @@ impl Model {
|
||||
.map_err(|e| to_js_error(e.to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getLastNonEmptyInRowBeforeColumn")]
|
||||
pub fn get_last_non_empty_in_row_before_column(
|
||||
#[wasm_bindgen(js_name = "getCellArrayStructure")]
|
||||
pub fn get_cell_array_structure(
|
||||
&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)
|
||||
) -> 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,82 +130,5 @@ test("autofill", () => {
|
||||
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;
|
||||
}
|
||||
|
||||
export type CellArrayStructure =
|
||||
| "SingleCell"
|
||||
| { DynamicChild: [number, number, number, number] }
|
||||
| { DynamicMother: [number, number] };
|
||||
|
||||
export interface WorksheetProperties {
|
||||
name: string;
|
||||
color: string;
|
||||
@@ -216,7 +221,7 @@ export interface SelectedView {
|
||||
// };
|
||||
|
||||
// type ClipboardData = Record<string, Record <string, ClipboardCell>>;
|
||||
type ClipboardData = Map<number, Map <number, ClipboardCell>>;
|
||||
type ClipboardData = Map<number, Map<number, ClipboardCell>>;
|
||||
|
||||
export interface ClipboardCell {
|
||||
text: string;
|
||||
@@ -233,4 +238,4 @@ export interface DefinedName {
|
||||
name: string;
|
||||
scope?: number;
|
||||
formula: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
target: server-runtime
|
||||
|
||||
caddy:
|
||||
build:
|
||||
context: .
|
||||
target: caddy-runtime
|
||||
ports:
|
||||
- "2080:2080"
|
||||
1802
docs/package-lock.json
generated
1802
docs/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