Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolás Hatcher
6740a43fe6 UPDATE: Add "Export Area to markdown" 2025-06-08 12:40:54 +02:00
209 changed files with 3817 additions and 12224 deletions

View File

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

69
Cargo.lock generated
View File

@@ -414,7 +414,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -430,13 +430,14 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"csv", "csv",
"js-sys", "js-sys",
"once_cell",
"rand", "rand",
"regex", "regex",
"regex-lite", "regex-lite",
@@ -447,7 +448,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"napi", "napi",
@@ -720,10 +721,11 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4" checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
dependencies = [ dependencies = [
"cfg-if",
"indoc", "indoc",
"libc", "libc",
"memoffset", "memoffset",
@@ -737,9 +739,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d" checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@@ -747,9 +749,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e" checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@@ -757,9 +759,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214" checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -769,9 +771,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.25.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e" checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -782,9 +784,8 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode",
"ironcalc", "ironcalc",
"pyo3", "pyo3",
"serde", "serde",
@@ -871,12 +872,6 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.17" version = "1.0.17"
@@ -984,9 +979,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.13.2" version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
@@ -1075,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",
@@ -1086,24 +1081,23 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -1124,9 +1118,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1134,9 +1128,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1147,12 +1141,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.100" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"

View File

@@ -1,93 +0,0 @@
FROM rust:latest AS builder
WORKDIR /app
COPY . .
# Tools + wasm toolchain + Node via nvm
RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl ca-certificates make \
&& rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& bash -lc "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash -" \
&& bash -lc '\
export NVM_DIR="$HOME/.nvm" && \
source "$NVM_DIR/nvm.sh" && \
nvm install 22 && nvm alias default 22 && \
nroot="$NVM_DIR/versions/node/$(nvm version default)/bin" && \
ln -sf "$nroot/node" /usr/local/bin/node && \
ln -sf "$nroot/npm" /usr/local/bin/npm && \
ln -sf "$nroot/npx" /usr/local/bin/npx \
' \
&& npm install typescript \
&& rm -rf /var/lib/apt/lists/*
# build the server
RUN cargo build --release --manifest-path webapp/app.ironcalc.com/server/Cargo.toml
# build the wasm
RUN make -C bindings/wasm
# build the widget
WORKDIR /app/webapp/IronCalc
RUN npm install && npm run build
# build the frontend app
WORKDIR /app/webapp/app.ironcalc.com/frontend
RUN npm install && npm run build
# build the xlsx_2_icalc binary (we don't need the release version here)
WORKDIR /app/xlsx
RUN cargo build
WORKDIR /app
# copy the artifacts to a dist/ directory
RUN mkdir dist
RUN mkdir dist/frontend
RUN cp -r webapp/app.ironcalc.com/frontend/dist/* dist/frontend/
RUN mkdir dist/server
RUN cp webapp/app.ironcalc.com/server/target/release/ironcalc_server dist/server/
RUN cp webapp/app.ironcalc.com/server/Rocket.toml dist/server/
RUN cp webapp/app.ironcalc.com/server/ironcalc.sqlite dist/server/
# Create ic files in docs
RUN mkdir -p dist/frontend/models
# Loop over all xlsx files in xlsx/tests/docs & templates and convert them to .ic
RUN bash -lc 'set -euo pipefail; \
mkdir -p dist/frontend/models; \
shopt -s nullglob; \
for xlsx_file in xlsx/tests/docs/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done; \
for xlsx_file in xlsx/tests/templates/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done'
# ---------- server runtime ----------
FROM debian:bookworm-slim AS server-runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy EVERYTHING you put in dist/server (binary + Rocket.toml + DB)
COPY --from=builder /app/dist/server/ ./
# Make sure Rocket binds to the container IP; explicitly point to the config file
ENV ROCKET_ADDRESS=0.0.0.0 \
ROCKET_PORT=8000 \
ROCKET_CONFIG=/app/Rocket.toml
EXPOSE 8000
# Run from /app so relative paths in Rocket.toml/DB work
CMD ["./ironcalc_server"]
# ---------- caddy runtime (serves frontend + reverse-proxy /api) ----------
FROM caddy:latest AS caddy-runtime
WORKDIR /srv
# Copy the frontend build output to /srv
COPY --from=builder /app/dist/frontend/ /srv/
# Copy the Caddyfile
COPY --from=builder /app/webapp/app.ironcalc.com/Caddyfile.compose /etc/caddy/Caddyfile

View File

@@ -31,17 +31,7 @@ This repository contains the main engine and the xlsx reader and writer.
Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go. Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go.
We will build different _skins_: in the terminal, as a desktop application or use it in your own web application. We will build different _skins_: in the terminal, as a desktop application or use it in you own web application.
# Docker
If you have docker installed just run:
```bash
docker compose up --build
```
head over to <http://localhost:2080> to test the application.
# Building # Building
@@ -94,7 +84,7 @@ And then use this code in `main.rs`:
```rust ```rust
use ironcalc::{ use ironcalc::{
base::{expressions::utils::number_to_column, Model}, base::{expressions::utils::number_to_column, model::Model},
export::save_to_xlsx, export::save_to_xlsx,
}; };

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.5.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021" edition = "2021"
homepage = "https://www.ironcalc.com" homepage = "https://www.ironcalc.com"
@@ -17,6 +17,7 @@ chrono = "0.4"
chrono-tz = "0.10" chrono-tz = "0.10"
regex = { version = "1.0", optional = true} regex = { version = "1.0", optional = true}
regex-lite = { version = "0.1.6", optional = true} regex-lite = { version = "0.1.6", optional = true}
once_cell = "1.16.0"
bitcode = "0.6.3" bitcode = "0.6.3"
csv = "1.3.0" csv = "1.3.0"

View File

@@ -212,12 +212,6 @@ impl Model {
if column_count <= 0 { if column_count <= 0 {
return Err("Please use insert columns instead".to_string()); return Err("Please use insert columns instead".to_string());
} }
if !(1..=LAST_COLUMN).contains(&column) {
return Err(format!("Column number '{column}' is not valid."));
}
if column + column_count - 1 > LAST_COLUMN {
return Err("Cannot delete columns beyond the last column of the sheet".to_string());
}
// first column being deleted // first column being deleted
let column_start = column; let column_start = column;
@@ -390,13 +384,6 @@ impl Model {
if row_count <= 0 { if row_count <= 0 {
return Err("Please use insert rows instead".to_string()); return Err("Please use insert rows instead".to_string());
} }
if !(1..=LAST_ROW).contains(&row) {
return Err(format!("Row number '{row}' is not valid."));
}
if row + row_count - 1 > LAST_ROW {
return Err("Cannot delete rows beyond the last row of the sheet".to_string());
}
// Move cells // Move cells
let worksheet = &self.workbook.worksheet(sheet)?; let worksheet = &self.workbook.worksheet(sheet)?;
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect(); let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
@@ -457,7 +444,7 @@ impl Model {
/// * Column is one of the extremes of the range. The new extreme would be target_column. /// * Column is one of the extremes of the range. The new extreme would be target_column.
/// Range is then normalized /// Range is then normalized
/// * Any other case, range is left unchanged. /// * Any other case, range is left unchanged.
/// NOTE: This moves the data and column styles along with the formulas /// NOTE: This does NOT move the data in the columns or move the colum styles
pub fn move_column_action( pub fn move_column_action(
&mut self, &mut self,
sheet: u32, sheet: u32,
@@ -473,70 +460,7 @@ impl Model {
return Err("Initial column out of boundaries".to_string()); return Err("Initial column out of boundaries".to_string());
} }
if delta == 0 { // TODO: Add the actual displacement of data and styles
return Ok(());
}
// Preserve cell contents, width and style of the column being moved
let original_refs = self
.workbook
.worksheet(sheet)?
.column_cell_references(column)?;
let mut original_cells = Vec::new();
for r in &original_refs {
let cell = self
.workbook
.worksheet(sheet)?
.cell(r.row, column)
.ok_or("Expected Cell to exist")?;
let style_idx = cell.get_style();
let formula_or_value = self
.get_cell_formula(sheet, r.row, column)?
.unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language));
original_cells.push((r.row, formula_or_value, style_idx));
self.cell_clear_all(sheet, r.row, column)?;
}
let width = self.workbook.worksheet(sheet)?.get_column_width(column)?;
let style = self.workbook.worksheet(sheet)?.get_column_style(column)?;
if delta > 0 {
for c in column + 1..=target_column {
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
for r in refs {
self.move_cell(sheet, r.row, c, r.row, c - 1)?;
}
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(c - 1, w, s)?;
}
} else {
for c in (target_column..=column - 1).rev() {
let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
for r in refs {
self.move_cell(sheet, r.row, c, r.row, c + 1)?;
}
let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(c + 1, w, s)?;
}
}
for (r, value, style_idx) in original_cells {
self.set_user_input(sheet, r, target_column, value)?;
self.workbook
.worksheet_mut(sheet)?
.set_cell_style(r, target_column, style_idx)?;
}
self.workbook
.worksheet_mut(sheet)?
.set_column_width_and_style(target_column, width, style)?;
// Update all formulas in the workbook // Update all formulas in the workbook
self.displace_cells( self.displace_cells(
@@ -549,88 +473,4 @@ impl Model {
Ok(()) Ok(())
} }
/// Displaces cells due to a move row action
/// from initial_row to target_row = initial_row + row_delta
/// References will be updated following the same rules as move_column_action
/// NOTE: This moves the data and row styles along with the formulas
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
// Check boundaries
let target_row = row + delta;
if !(1..=LAST_ROW).contains(&target_row) {
return Err("Target row out of boundaries".to_string());
}
if !(1..=LAST_ROW).contains(&row) {
return Err("Initial row out of boundaries".to_string());
}
if delta == 0 {
return Ok(());
}
let original_cols = self.get_columns_for_row(sheet, row, false)?;
let mut original_cells = Vec::new();
for c in &original_cols {
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, *c)
.ok_or("Expected Cell to exist")?;
let style_idx = cell.get_style();
let formula_or_value = self
.get_cell_formula(sheet, row, *c)?
.unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language));
original_cells.push((*c, formula_or_value, style_idx));
self.cell_clear_all(sheet, row, *c)?;
}
if delta > 0 {
for r in row + 1..=target_row {
let cols = self.get_columns_for_row(sheet, r, false)?;
for c in cols {
self.move_cell(sheet, r, c, r - 1, c)?;
}
}
} else {
for r in (target_row..=row - 1).rev() {
let cols = self.get_columns_for_row(sheet, r, false)?;
for c in cols {
self.move_cell(sheet, r, c, r + 1, c)?;
}
}
}
for (c, value, style_idx) in original_cells {
self.set_user_input(sheet, target_row, c, value)?;
self.workbook
.worksheet_mut(sheet)?
.set_cell_style(target_row, c, style_idx)?;
}
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let mut new_rows = Vec::new();
for r in worksheet.rows.iter() {
if r.r == row {
let mut nr = r.clone();
nr.r = target_row;
new_rows.push(nr);
} else if delta > 0 && r.r > row && r.r <= target_row {
let mut nr = r.clone();
nr.r -= 1;
new_rows.push(nr);
} else if delta < 0 && r.r < row && r.r >= target_row {
let mut nr = r.clone();
nr.r += 1;
new_rows.push(nr);
} else {
new_rows.push(r.clone());
}
}
worksheet.rows = new_rows;
// Update all formulas in the workbook
self.displace_cells(&(DisplaceData::RowMove { sheet, row, delta }))?;
Ok(())
}
} }

View File

@@ -159,7 +159,7 @@ impl Model {
// FIXME: I think when casting a number we should convert it to_precision(x, 15) // FIXME: I think when casting a number we should convert it to_precision(x, 15)
// See function Exact // See function Exact
match result { match result {
CalcResult::Number(f) => Ok(format!("{f}")), CalcResult::Number(f) => Ok(format!("{}", f)),
CalcResult::String(s) => Ok(s), CalcResult::String(s) => Ok(s),
CalcResult::Boolean(f) => { CalcResult::Boolean(f) => {
if f { if f {

View File

@@ -142,7 +142,7 @@ impl Lexer {
pub fn expect(&mut self, tk: TokenType) -> Result<()> { pub fn expect(&mut self, tk: TokenType) -> Result<()> {
let nt = self.next_token(); let nt = self.next_token();
if mem::discriminant(&nt) != mem::discriminant(&tk) { if mem::discriminant(&nt) != mem::discriminant(&tk) {
return Err(self.set_error(&format!("Error, expected {tk:?}"), self.position)); return Err(self.set_error(&format!("Error, expected {:?}", tk), self.position));
} }
Ok(()) Ok(())
} }
@@ -314,9 +314,6 @@ impl Lexer {
} else if name_upper == self.language.booleans.r#false { } else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false); return TokenType::Boolean(false);
} }
if self.peek_char() == Some('(') {
return TokenType::Ident(name);
}
if self.mode == LexerMode::A1 { if self.mode == LexerMode::A1 {
let parsed_reference = utils::parse_reference_a1(&name_upper); let parsed_reference = utils::parse_reference_a1(&name_upper);
if parsed_reference.is_some() if parsed_reference.is_some()
@@ -514,7 +511,7 @@ impl Lexer {
self.position = position; self.position = position;
chars.parse::<i32>().map_err(|_| LexerError { chars.parse::<i32>().map_err(|_| LexerError {
position, position,
message: format!("Failed to parse to int: {chars}"), message: format!("Failed to parse to int: {}", chars),
}) })
} }
@@ -575,7 +572,9 @@ impl Lexer {
} }
self.position = position; self.position = position;
match chars.parse::<f64>() { 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), Ok(v) => Ok(v),
} }
} }

View File

@@ -148,16 +148,15 @@ impl Lexer {
let row_left = match row_left.parse::<i32>() { let row_left = match row_left.parse::<i32>() {
Ok(n) => n, Ok(n) => n,
Err(_) => { Err(_) => {
return Err( return Err(self
self.set_error(&format!("Failed parsing row {row_left}"), position) .set_error(&format!("Failed parsing row {}", row_left), position))
)
} }
}; };
let row_right = match row_right.parse::<i32>() { let row_right = match row_right.parse::<i32>() {
Ok(n) => n, Ok(n) => n,
Err(_) => { Err(_) => {
return Err(self return Err(self
.set_error(&format!("Failed parsing row {row_right}"), position)) .set_error(&format!("Failed parsing row {}", row_right), position))
} }
}; };
if row_left > LAST_ROW { if row_left > LAST_ROW {

View File

@@ -1,6 +1,5 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::expressions::utils::column_to_number;
use crate::language::get_language; use crate::language::get_language;
use crate::locale::get_locale; use crate::locale::get_locale;
@@ -686,29 +685,3 @@ fn test_comparisons() {
assert_eq!(lx.next_token(), Number(7.0)); assert_eq!(lx.next_token(), Number(7.0));
assert_eq!(lx.next_token(), EOF); assert_eq!(lx.next_token(), EOF);
} }
#[test]
fn test_log10_is_cell_reference() {
let mut lx = new_lexer("LOG10", true);
assert_eq!(
lx.next_token(),
Reference {
sheet: None,
column: column_to_number("LOG").unwrap(),
row: 10,
absolute_column: false,
absolute_row: false,
}
);
assert_eq!(lx.next_token(), EOF);
}
#[test]
fn test_log10_is_function() {
let mut lx = new_lexer("LOG10(100)", true);
assert_eq!(lx.next_token(), Ident("LOG10".to_string()));
assert_eq!(lx.next_token(), LeftParenthesis);
assert_eq!(lx.next_token(), Number(100.0));
assert_eq!(lx.next_token(), RightParenthesis);
assert_eq!(lx.next_token(), EOF);
}

View File

@@ -717,7 +717,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("sheet not found: {}", context.sheet), message: "sheet not found".to_string(),
}; };
} }
}; };
@@ -828,7 +828,7 @@ impl Parser {
| TokenType::Percent => Node::ParseErrorKind { | TokenType::Percent => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("Unexpected token: '{next_token:?}'"), message: format!("Unexpected token: '{:?}'", next_token),
}, },
TokenType::LeftBracket => Node::ParseErrorKind { TokenType::LeftBracket => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
@@ -850,7 +850,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("sheet not found: {}", context.sheet), message: "sheet not found".to_string(),
}; };
} }
}; };
@@ -878,7 +878,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("table sheet not found: {}", table.sheet_name), message: "sheet not found".to_string(),
}; };
} }
}; };

View File

@@ -53,24 +53,24 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
arguments = to_string_moved(el, move_context); arguments = to_string_moved(el, move_context);
} }
} }
format!("{name}({arguments})") format!("{}({})", name, arguments)
} }
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String { pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
match node { match node {
ArrayNode::Boolean(value) => format!("{value}").to_ascii_uppercase(), ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(),
ArrayNode::Number(number) => to_excel_precision_str(*number), ArrayNode::Number(number) => to_excel_precision_str(*number),
ArrayNode::String(value) => format!("\"{value}\""), ArrayNode::String(value) => format!("\"{}\"", value),
ArrayNode::Error(kind) => format!("{kind}"), ArrayNode::Error(kind) => format!("{}", kind),
} }
} }
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{value}").to_ascii_uppercase(), BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{value}\""), StringKind(value) => format!("\"{}\"", value),
ReferenceKind { ReferenceKind {
sheet_name, sheet_name,
sheet_index, sheet_index,
@@ -241,7 +241,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
@@ -325,7 +325,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
@@ -358,7 +358,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
_ => to_string_moved(right, move_context), _ => to_string_moved(right, move_context),
}; };
format!("{x}{kind}{y}") format!("{}{}{}", x, kind, y)
} }
OpPowerKind { left, right } => format!( OpPowerKind { left, right } => format!(
"{}^{}", "{}^{}",
@@ -403,7 +403,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
// Enclose the whole matrix in braces // Enclose the whole matrix in braces
format!("{{{matrix_string}}}") format!("{{{}}}", matrix_string)
} }
DefinedNameKind((name, ..)) => name.to_string(), DefinedNameKind((name, ..)) => name.to_string(),
TableNameKind(name) => name.to_string(), TableNameKind(name) => name.to_string(),
@@ -418,7 +418,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)), OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)), OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
}, },
ErrorKind(kind) => format!("{kind}"), ErrorKind(kind) => format!("{}", kind),
ParseErrorKind { ParseErrorKind {
formula, formula,
message: _, message: _,

View File

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

View File

@@ -2,7 +2,7 @@ use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::move_formula::to_string_array_node; use crate::expressions::parser::move_formula::to_string_array_node;
use crate::expressions::parser::static_analysis::add_implicit_intersection; use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::{OpSum, OpUnary}; use crate::expressions::token::OpUnary;
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str}; use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
pub enum DisplaceData { pub enum DisplaceData {
@@ -28,11 +28,6 @@ pub enum DisplaceData {
column: i32, column: i32,
delta: i32, delta: i32,
}, },
RowMove {
sheet: u32,
row: i32,
delta: i32,
},
ColumnMove { ColumnMove {
sheet: u32, sheet: u32,
column: i32, column: i32,
@@ -164,29 +159,6 @@ pub(crate) fn stringify_reference(
} }
} }
} }
DisplaceData::RowMove {
sheet,
row: move_row,
delta,
} => {
if sheet_index == *sheet {
if row == *move_row {
row += *delta;
} else if *delta > 0 {
// Moving the row downwards
if row > *move_row && row <= *move_row + *delta {
// Intermediate rows move up by one position
row -= 1;
}
} else if *delta < 0 {
// Moving the row upwards
if row < *move_row && row >= *move_row + *delta {
// Intermediate rows move down by one position
row += 1;
}
}
}
}
DisplaceData::ColumnMove { DisplaceData::ColumnMove {
sheet, sheet,
column: move_column, column: move_column,
@@ -195,18 +167,14 @@ pub(crate) fn stringify_reference(
if sheet_index == *sheet { if sheet_index == *sheet {
if column == *move_column { if column == *move_column {
column += *delta; column += *delta;
} else if *delta > 0 { } else if (*delta > 0
// Moving the column to the right && column > *move_column
if column > *move_column && column <= *move_column + *delta { && column <= *move_column + *delta)
// Intermediate columns move left by one position || (*delta < 0
column -= 1; && column < *move_column
} && column >= *move_column + *delta)
} else if *delta < 0 { {
// Moving the column to the left column -= *delta;
if column < *move_column && column >= *move_column + *delta {
// Intermediate columns move right by one position
column += 1;
}
} }
} }
} }
@@ -216,16 +184,16 @@ pub(crate) fn stringify_reference(
return "#REF!".to_string(); return "#REF!".to_string();
} }
let mut row_abs = if absolute_row { let mut row_abs = if absolute_row {
format!("${row}") format!("${}", row)
} else { } else {
format!("{row}") format!("{}", row)
}; };
let column = match crate::expressions::utils::number_to_column(column) { let column = match crate::expressions::utils::number_to_column(column) {
Some(s) => s, Some(s) => s,
None => return "#REF!".to_string(), None => return "#REF!".to_string(),
}; };
let mut col_abs = if absolute_column { let mut col_abs = if absolute_column {
format!("${column}") format!("${}", column)
} else { } else {
column column
}; };
@@ -240,27 +208,27 @@ pub(crate) fn stringify_reference(
format!("{}!{}{}", quote_name(name), col_abs, row_abs) format!("{}!{}{}", quote_name(name), col_abs, row_abs)
} }
None => { None => {
format!("{col_abs}{row_abs}") format!("{}{}", col_abs, row_abs)
} }
} }
} }
None => { None => {
let row_abs = if absolute_row { let row_abs = if absolute_row {
format!("R{row}") format!("R{}", row)
} else { } else {
format!("R[{row}]") format!("R[{}]", row)
}; };
let col_abs = if absolute_column { let col_abs = if absolute_column {
format!("C{column}") format!("C{}", column)
} else { } else {
format!("C[{column}]") format!("C[{}]", column)
}; };
match &sheet_name { match &sheet_name {
Some(name) => { Some(name) => {
format!("{}!{}{}", quote_name(name), row_abs, col_abs) format!("{}!{}{}", quote_name(name), row_abs, col_abs)
} }
None => { None => {
format!("{row_abs}{col_abs}") format!("{}{}", row_abs, col_abs)
} }
} }
} }
@@ -288,7 +256,7 @@ fn format_function(
arguments = stringify(el, context, displace_data, export_to_excel); arguments = stringify(el, context, displace_data, export_to_excel);
} }
} }
format!("{name}({arguments})") format!("{}({})", name, arguments)
} }
// There is just one representation in the AST (Abstract Syntax Tree) of a formula. // There is just one representation in the AST (Abstract Syntax Tree) of a formula.
@@ -324,9 +292,9 @@ fn stringify(
) -> String { ) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{value}").to_ascii_uppercase(), BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{value}\""), StringKind(value) => format!("\"{}\"", value),
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
column, column,
@@ -416,7 +384,7 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
WrongRangeKind { WrongRangeKind {
sheet_name, sheet_name,
@@ -465,7 +433,7 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{s1}:{s2}") format!("{}:{}", s1, s2)
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
@@ -483,38 +451,40 @@ fn stringify(
kind, kind,
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, export_to_excel)
), ),
OpSumKind { kind, left, right } => { OpSumKind { kind, left, right } => format!(
let left_str = stringify(left, context, displace_data, export_to_excel); "{}{}{}",
// if kind is minus then we need parentheses in the right side if they are OpSumKind or CompareKind stringify(left, context, displace_data, export_to_excel),
let right_str = if (matches!(kind, OpSum::Minus) && matches!(**right, OpSumKind { .. })) kind,
| matches!(**right, CompareKind { .. }) stringify(right, context, displace_data, export_to_excel)
{ ),
format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
)
} else {
stringify(right, context, displace_data, export_to_excel)
};
format!("{left_str}{kind}{right_str}")
}
OpProductKind { kind, left, right } => { OpProductKind { kind, left, right } => {
let x = match **left { let x = match **left {
OpSumKind { .. } | CompareKind { .. } => format!( OpSumKind { .. } => format!(
"({})",
stringify(left, context, displace_data, export_to_excel)
),
CompareKind { .. } => format!(
"({})", "({})",
stringify(left, context, displace_data, export_to_excel) stringify(left, context, displace_data, export_to_excel)
), ),
_ => stringify(left, context, displace_data, export_to_excel), _ => stringify(left, context, displace_data, export_to_excel),
}; };
let y = match **right { let y = match **right {
OpSumKind { .. } | CompareKind { .. } | OpProductKind { .. } => format!( OpSumKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
),
CompareKind { .. } => format!(
"({})",
stringify(right, context, displace_data, export_to_excel)
),
OpProductKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, export_to_excel)
), ),
_ => stringify(right, context, displace_data, export_to_excel), _ => stringify(right, context, displace_data, export_to_excel),
}; };
format!("{x}{kind}{y}") format!("{}{}{}", x, kind, y)
} }
OpPowerKind { left, right } => { OpPowerKind { left, right } => {
let x = match **left { let x = match **left {
@@ -577,7 +547,7 @@ fn stringify(
stringify(right, context, displace_data, export_to_excel) stringify(right, context, displace_data, export_to_excel)
), ),
}; };
format!("{x}^{y}") format!("{}^{}", x, y)
} }
InvalidFunctionKind { name, args } => { InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, export_to_excel) format_function(name, args, context, displace_data, export_to_excel)
@@ -612,50 +582,17 @@ fn stringify(
} }
matrix_string.push_str(&row_string); matrix_string.push_str(&row_string);
} }
format!("{{{matrix_string}}}") format!("{{{}}}", matrix_string)
} }
TableNameKind(value) => value.to_string(), TableNameKind(value) => value.to_string(),
DefinedNameKind((name, ..)) => name.to_string(), DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
let needs_parentheses = match **right { format!(
BooleanKind(_) "-{}",
| NumberKind(_) stringify(right, context, displace_data, export_to_excel)
| StringKind(_) )
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| WrongRangeKind { .. }
| OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| ImplicitIntersection { .. }
| CompareKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| EmptyArgKind => false,
OpSumKind { .. } | UnaryKind { .. } => true,
};
if needs_parentheses {
format!(
"-({})",
stringify(right, context, displace_data, export_to_excel)
)
} else {
format!(
"-{}",
stringify(right, context, displace_data, export_to_excel)
)
}
} }
OpUnary::Percentage => { OpUnary::Percentage => {
format!( format!(
@@ -664,7 +601,7 @@ fn stringify(
) )
} }
}, },
ErrorKind(kind) => format!("{kind}"), ErrorKind(kind) => format!("{}", kind),
ParseErrorKind { ParseErrorKind {
formula, formula,
position: _, position: _,

View File

@@ -32,39 +32,3 @@ fn exp_order() {
let t = parser.parse("(5)^(4)", &cell_reference); let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4"); assert_eq!(to_string(&t, &cell_reference), "5^4");
} }
#[test]
fn correct_parenthesis() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let t = parser.parse("-(1 + 1)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "-(1+1)");
let t = parser.parse("1 - (3 + 4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-(3+4)");
let t = parser.parse("-(1.05*(0.0284 + 0.0046) - 0.0284)", &cell_reference);
assert_eq!(
to_string(&t, &cell_reference),
"-(1.05*(0.0284+0.0046)-0.0284)"
);
let t = parser.parse("1 + (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1+3+5");
let t = parser.parse("1 - (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-(3+5)");
let t = parser.parse("(1 - 3) - (3+5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1-3-(3+5)");
let t = parser.parse("1 + (3<5)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "1+(3<5)");
}

View File

@@ -211,6 +211,4 @@ fn test_names() {
assert!(!is_valid_identifier("test€")); assert!(!is_valid_identifier("test€"));
assert!(!is_valid_identifier("truñe")); assert!(!is_valid_identifier("truñe"));
assert!(!is_valid_identifier("tr&ue")); assert!(!is_valid_identifier("tr&ue"));
assert!(!is_valid_identifier("LOG10"));
} }

View File

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

View File

@@ -120,7 +120,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
// We should have different codepaths for general formatting and errors // We should have different codepaths for general formatting and errors
let value_abs = value.abs(); let value_abs = value.abs();
if (1.0e-8..1.0e+11).contains(&value_abs) { if (1.0e-8..1.0e+11).contains(&value_abs) {
let mut text = format!("{value:.9}"); let mut text = format!("{:.9}", value);
text = text.trim_end_matches('0').trim_end_matches('.').to_string(); text = text.trim_end_matches('0').trim_end_matches('.').to_string();
Formatted { Formatted {
text, text,
@@ -138,7 +138,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let exponent = value_abs.log10().floor(); let exponent = value_abs.log10().floor();
value /= 10.0_f64.powf(exponent); value /= 10.0_f64.powf(exponent);
let sign = if exponent < 0.0 { '-' } else { '+' }; let sign = if exponent < 0.0 { '-' } else { '+' };
let s = format!("{value:.5}"); let s = format!("{:.5}", value);
Formatted { Formatted {
text: format!( text: format!(
"{}E{}{:02}", "{}E{}{:02}",
@@ -167,33 +167,33 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
text = format!("{text}{c}"); text = format!("{}{}", text, c);
} }
TextToken::Text(t) => { TextToken::Text(t) => {
text = format!("{text}{t}"); text = format!("{}{}", text, t);
} }
TextToken::Ghost(_) => { TextToken::Ghost(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Spacer(_) => { TextToken::Spacer(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Raw => { TextToken::Raw => {
text = format!("{text}{value}"); text = format!("{}{}", text, value);
} }
TextToken::Digit(_) => {} TextToken::Digit(_) => {}
TextToken::Period => {} TextToken::Period => {}
TextToken::Day => { TextToken::Day => {
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{text}{day}"); text = format!("{}{}", text, day);
} }
TextToken::DayPadded => { TextToken::DayPadded => {
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{text}{day:02}"); text = format!("{}{:02}", text, day);
} }
TextToken::DayNameShort => { TextToken::DayNameShort => {
let mut day = date.weekday().number_from_monday() as usize; let mut day = date.weekday().number_from_monday() as usize;
@@ -211,11 +211,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} }
TextToken::Month => { TextToken::Month => {
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{text}{month}"); text = format!("{}{}", text, month);
} }
TextToken::MonthPadded => { TextToken::MonthPadded => {
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{text}{month:02}"); text = format!("{}{:02}", text, month);
} }
TextToken::MonthNameShort => { TextToken::MonthNameShort => {
let month = date.month() as usize; let month = date.month() as usize;
@@ -228,7 +228,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => { TextToken::MonthLetter => {
let month = date.month() as usize; let month = date.month() as usize;
let months_letter = &locale.dates.months_letter[month - 1]; let months_letter = &locale.dates.months_letter[month - 1];
text = format!("{text}{months_letter}"); text = format!("{}{}", text, months_letter);
} }
TextToken::YearShort => { TextToken::YearShort => {
text = format!("{}{}", text, date.format("%y")); text = format!("{}{}", text, date.format("%y"));
@@ -247,7 +247,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Number(p) => { ParsePart::Number(p) => {
let mut text = "".to_string(); let mut text = "".to_string();
if let Some(c) = p.currency { if let Some(c) = p.currency {
text = format!("{c}"); text = format!("{}", c);
} }
let tokens = &p.tokens; let tokens = &p.tokens;
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma)); value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
@@ -295,26 +295,26 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
text = format!("{text}{c}"); text = format!("{}{}", text, c);
} }
TextToken::Text(t) => { TextToken::Text(t) => {
text = format!("{text}{t}"); text = format!("{}{}", text, t);
} }
TextToken::Ghost(_) => { TextToken::Ghost(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Spacer(_) => { TextToken::Spacer(_) => {
// we just leave a whitespace // we just leave a whitespace
// This is what the TEXT function does // This is what the TEXT function does
text = format!("{text} "); text = format!("{} ", text);
} }
TextToken::Raw => { TextToken::Raw => {
text = format!("{text}{value}"); text = format!("{}{}", text, value);
} }
TextToken::Period => { TextToken::Period => {
text = format!("{text}{decimal_separator}"); text = format!("{}{}", text, decimal_separator);
} }
TextToken::Digit(digit) => { TextToken::Digit(digit) => {
if digit.number == 'i' { if digit.number == 'i' {
@@ -322,7 +322,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
let index = digit.index; let index = digit.index;
let number_index = ln - digit_count + index; let number_index = ln - digit_count + index;
if index == 0 && is_negative { if index == 0 && is_negative {
text = format!("-{text}"); text = format!("-{}", text);
} }
if ln <= digit_count { if ln <= digit_count {
// The number of digits is less or equal than the number of digit tokens // The number of digits is less or equal than the number of digit tokens
@@ -347,7 +347,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} else { } else {
"" ""
}; };
text = format!("{text}{c}{sep}"); text = format!("{}{}{}", text, c, sep);
} }
digit_index += 1; digit_index += 1;
} else { } else {
@@ -373,18 +373,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if index < fract_part.len() { if index < fract_part.len() {
text = format!("{}{}", text, fract_part[index]); text = format!("{}{}", text, fract_part[index]);
} else if digit.kind == '0' { } else if digit.kind == '0' {
text = format!("{text}0"); text = format!("{}0", text);
} else if digit.kind == '?' { } else if digit.kind == '?' {
text = format!("{text} "); text = format!("{} ", text);
} }
} else if digit.number == 'e' { } else if digit.number == 'e' {
// 3. Exponent part // 3. Exponent part
let index = digit.index; let index = digit.index;
if index == 0 { if index == 0 {
if exponent_is_negative { if exponent_is_negative {
text = format!("{text}E-"); text = format!("{}E-", text);
} else { } else {
text = format!("{text}E+"); text = format!("{}E+", text);
} }
} }
let number_index = l_exp - (p.exponent_digit_count - index); let number_index = l_exp - (p.exponent_digit_count - index);
@@ -400,7 +400,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
exponent_part[number_index as usize] exponent_part[number_index as usize]
}; };
text = format!("{text}{c}"); text = format!("{}{}", text, c);
} }
} else { } else {
for i in 0..number_index + 1 { for i in 0..number_index + 1 {
@@ -614,7 +614,7 @@ pub(crate) fn parse_formatted_number(
// check if it is a currency in currencies // check if it is a currency in currencies
for currency in currencies { for currency in currencies {
if let Some(p) = value.strip_prefix(&format!("-{currency}")) { if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
let (f, options) = parse_number(p.trim())?; let (f, options) = parse_number(p.trim())?;
if options.is_scientific { if options.is_scientific {
return Ok((f, Some(scientific_format.to_string()))); return Ok((f, Some(scientific_format.to_string())));

View File

@@ -333,7 +333,7 @@ impl Lexer {
} else if s == '-' { } else if s == '-' {
Token::ScientificMinus Token::ScientificMinus
} else { } else {
self.set_error(&format!("Unexpected char: {s}. Expected + or -")); self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
Token::ILLEGAL Token::ILLEGAL
} }
} else { } else {
@@ -385,14 +385,14 @@ impl Lexer {
for c in "eneral".chars() { for c in "eneral".chars() {
let cc = self.read_next_char(); let cc = self.read_next_char();
if Some(c) != cc { if Some(c) != cc {
self.set_error(&format!("Unexpected character: {x}")); self.set_error(&format!("Unexpected character: {}", x));
return Token::ILLEGAL; return Token::ILLEGAL;
} }
} }
Token::General Token::General
} }
_ => { _ => {
self.set_error(&format!("Unexpected character: {x}")); self.set_error(&format!("Unexpected character: {}", x));
Token::ILLEGAL Token::ILLEGAL
} }
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -46,18 +46,18 @@ impl fmt::Display for Complex {
// it is a bit weird what Excel does but it seems it uses general notation for // it is a bit weird what Excel does but it seems it uses general notation for
// numbers > 1e-20 and scientific notation for the rest // numbers > 1e-20 and scientific notation for the rest
let y_str = if y.abs() <= 9e-20 { let y_str = if y.abs() <= 9e-20 {
format!("{y:E}") format!("{:E}", y)
} else if y == 1.0 { } else if y == 1.0 {
"".to_string() "".to_string()
} else if y == -1.0 { } else if y == -1.0 {
"-".to_string() "-".to_string()
} else { } else {
format!("{y}") format!("{}", y)
}; };
let x_str = if x.abs() <= 9e-20 { let x_str = if x.abs() <= 9e-20 {
format!("{x:E}") format!("{:E}", x)
} else { } else {
format!("{x}") format!("{}", x)
}; };
if y == 0.0 && x == 0.0 { if y == 0.0 && x == 0.0 {
write!(f, "0") write!(f, "0")

View File

@@ -76,7 +76,7 @@ impl Model {
if value < 0 { if value < 0 {
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9)) CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
} else { } else {
let result = format!("{value:X}"); let result = format!("{:X}", value);
if let Some(places) = places { if let Some(places) = places {
if places < result.len() as i32 { if places < result.len() as i32 {
return CalcResult::new_error( return CalcResult::new_error(
@@ -120,7 +120,7 @@ impl Model {
if value < 0 { if value < 0 {
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9)) CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
} else { } else {
let result = format!("{value:o}"); let result = format!("{:o}", value);
if let Some(places) = places { if let Some(places) = places {
if places < result.len() as i32 { if places < result.len() as i32 {
return CalcResult::new_error( return CalcResult::new_error(
@@ -163,7 +163,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{value:b}"); let result = format!("{:b}", value);
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -202,7 +202,7 @@ impl Model {
if value < 0 { if value < 0 {
value += HEX_MAX; value += HEX_MAX;
} }
let result = format!("{value:X}"); let result = format!("{:X}", value);
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -242,7 +242,7 @@ impl Model {
if value < 0 { if value < 0 {
value += OCT_MAX; value += OCT_MAX;
} }
let result = format!("{value:o}"); let result = format!("{:o}", value);
if let Some(places) = places { if let Some(places) = places {
if value_raw > 0.0 && places < result.len() as i32 { if value_raw > 0.0 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -301,7 +301,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{value:b}"); let result = format!("{:b}", value);
if let Some(places) = places { if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) { if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -391,7 +391,7 @@ impl Model {
if value < 0 { if value < 0 {
value += OCT_MAX; value += OCT_MAX;
} }
let result = format!("{value:o}"); let result = format!("{:o}", value);
if let Some(places) = places { if let Some(places) = places {
if places <= 0 || (value > 0 && places < result.len() as i32) { if places <= 0 || (value > 0 && places < result.len() as i32) {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -446,7 +446,7 @@ impl Model {
if value < 0 { if value < 0 {
value += 1024; value += 1024;
} }
let result = format!("{value:b}"); let result = format!("{:b}", value);
if let Some(places) = places { if let Some(places) = places {
if value < 512 && places < result.len() as i32 { if value < 512 && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
@@ -532,7 +532,7 @@ impl Model {
if value < 0 { if value < 0 {
value += HEX_MAX; value += HEX_MAX;
} }
let result = format!("{value:X}"); let result = format!("{:X}", value);
if let Some(places) = places { if let Some(places) = places {
if value < HEX_MAX_HALF && places < result.len() as i32 { if value < HEX_MAX_HALF && places < result.len() as i32 {
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string()); return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());

View File

@@ -231,7 +231,7 @@ impl Model {
CalcResult::new_error( CalcResult::new_error(
Error::ERROR, Error::ERROR,
*cell, *cell,
format!("Invalid worksheet index: '{sheet}'"), format!("Invalid worksheet index: '{}'", sheet),
) )
})? })?
.dimension() .dimension()
@@ -245,7 +245,7 @@ impl Model {
CalcResult::new_error( CalcResult::new_error(
Error::ERROR, Error::ERROR,
*cell, *cell,
format!("Invalid worksheet index: '{sheet}'"), format!("Invalid worksheet index: '{}'", sheet),
) )
})? })?
.dimension() .dimension()

View File

@@ -246,7 +246,7 @@ impl Model {
} }
// None of the cases matched so we return the default // None of the cases matched so we return the default
// If there is an even number of args is the last one otherwise is #N/A // If there is an even number of args is the last one otherwise is #N/A
if args_count.is_multiple_of(2) { if args_count % 2 == 0 {
return self.evaluate_node_in_context(&args[args_count - 1], cell); return self.evaluate_node_in_context(&args[args_count - 1], cell);
} }
CalcResult::Error { CalcResult::Error {
@@ -262,7 +262,7 @@ impl Model {
if args_count < 2 { if args_count < 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
if !args_count.is_multiple_of(2) { if args_count % 2 != 0 {
// Missing value for last condition // Missing value for last condition
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }

View File

@@ -2,7 +2,6 @@ use crate::cast::NumberOrArray;
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode; use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex; use crate::expressions::types::CellReferenceIndex;
use crate::number_format::to_precision;
use crate::single_number_fn; use crate::single_number_fn;
use crate::{ use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
@@ -312,7 +311,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let value = match self.get_number(&args[0], cell) { let value = match self.get_number(&args[0], cell) {
Ok(f) => to_precision(f, 15), Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
let number_of_digits = match self.get_number(&args[1], cell) { let number_of_digits = match self.get_number(&args[1], cell) {
@@ -328,13 +327,12 @@ impl Model {
let scale = 10.0_f64.powf(number_of_digits); let scale = 10.0_f64.powf(number_of_digits);
CalcResult::Number((value * scale).round() / scale) CalcResult::Number((value * scale).round() / scale)
} }
pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let value = match self.get_number(&args[0], cell) { let value = match self.get_number(&args[0], cell) {
Ok(f) => to_precision(f, 15), Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
let number_of_digits = match self.get_number(&args[1], cell) { let number_of_digits = match self.get_number(&args[1], cell) {
@@ -354,13 +352,12 @@ impl Model {
CalcResult::Number((value * scale).floor() / scale) CalcResult::Number((value * scale).floor() / scale)
} }
} }
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let value = match self.get_number(&args[0], cell) { let value = match self.get_number(&args[0], cell) {
Ok(f) => to_precision(f, 15), Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
let number_of_digits = match self.get_number(&args[1], cell) { let number_of_digits = match self.get_number(&args[1], cell) {
@@ -381,16 +378,6 @@ impl Model {
} }
} }
single_number_fn!(fn_log10, |f| if f <= 0.0 {
Err(Error::NUM)
} else {
Ok(f64::log10(f))
});
single_number_fn!(fn_ln, |f| if f <= 0.0 {
Err(Error::NUM)
} else {
Ok(f64::ln(f))
});
single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
single_number_fn!(fn_cos, |f| Ok(f64::cos(f))); single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
single_number_fn!(fn_tan, |f| Ok(f64::tan(f))); single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
@@ -444,47 +431,6 @@ impl Model {
CalcResult::Number(f64::atan2(y, x)) CalcResult::Number(f64::atan2(y, x))
} }
pub(crate) fn fn_log(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let n_args = args.len();
if !(1..=2).contains(&n_args) {
return CalcResult::new_args_number_error(cell);
}
let x = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let y = if n_args == 1 {
10.0
} else {
match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(s) => return s,
}
};
if x <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Number must be positive".to_string(),
};
}
if y == 1.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Logarithm base cannot be 1".to_string(),
};
}
if y <= 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Logarithm base must be positive".to_string(),
};
}
CalcResult::Number(f64::log(x, y))
}
pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);

View File

@@ -54,9 +54,6 @@ pub enum Function {
Columns, Columns,
Cos, Cos,
Cosh, Cosh,
Log,
Log10,
Ln,
Max, Max,
Min, Min,
Pi, Pi,
@@ -148,30 +145,13 @@ pub enum Function {
// Date and time // Date and time
Date, Date,
Datedif,
Datevalue,
Day, Day,
Edate, Edate,
Eomonth, Eomonth,
Month, Month,
Time,
Timevalue,
Hour,
Minute,
Second,
Now, Now,
Today, Today,
Year, Year,
Networkdays,
NetworkdaysIntl,
Days,
Days360,
Weekday,
Weeknum,
Workday,
WorkdayIntl,
Yearfrac,
Isoweeknum,
// Financial // Financial
Cumipmt, Cumipmt,
@@ -270,7 +250,7 @@ pub enum Function {
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 215> { pub fn into_iter() -> IntoIter<Function, 195> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -297,9 +277,6 @@ impl Function {
Function::Atanh, Function::Atanh,
Function::Abs, Function::Abs,
Function::Pi, Function::Pi,
Function::Ln,
Function::Log,
Function::Log10,
Function::Sqrt, Function::Sqrt,
Function::Sqrtpi, Function::Sqrtpi,
Function::Atan2, Function::Atan2,
@@ -379,26 +356,9 @@ impl Function {
Function::Month, Function::Month,
Function::Eomonth, Function::Eomonth,
Function::Date, Function::Date,
Function::Datedif,
Function::Datevalue,
Function::Edate, Function::Edate,
Function::Networkdays,
Function::NetworkdaysIntl,
Function::Time,
Function::Timevalue,
Function::Hour,
Function::Minute,
Function::Second,
Function::Today, Function::Today,
Function::Now, Function::Now,
Function::Days,
Function::Days360,
Function::Weekday,
Function::Weeknum,
Function::Workday,
Function::WorkdayIntl,
Function::Yearfrac,
Function::Isoweeknum,
Function::Pmt, Function::Pmt,
Function::Pv, Function::Pv,
Function::Rate, Function::Rate,
@@ -528,7 +488,6 @@ impl Function {
Function::Isformula => "_xlfn.ISFORMULA".to_string(), Function::Isformula => "_xlfn.ISFORMULA".to_string(),
Function::Sheet => "_xlfn.SHEET".to_string(), Function::Sheet => "_xlfn.SHEET".to_string(),
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(), Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
_ => self.to_string(), _ => self.to_string(),
} }
} }
@@ -575,10 +534,6 @@ impl Function {
"POWER" => Some(Function::Power), "POWER" => Some(Function::Power),
"ATAN2" => Some(Function::Atan2), "ATAN2" => Some(Function::Atan2),
"LN" => Some(Function::Ln),
"LOG" => Some(Function::Log),
"LOG10" => Some(Function::Log10),
"MAX" => Some(Function::Max), "MAX" => Some(Function::Max),
"MIN" => Some(Function::Min), "MIN" => Some(Function::Min),
"PRODUCT" => Some(Function::Product), "PRODUCT" => Some(Function::Product),
@@ -666,26 +621,9 @@ impl Function {
"EOMONTH" => Some(Function::Eomonth), "EOMONTH" => Some(Function::Eomonth),
"MONTH" => Some(Function::Month), "MONTH" => Some(Function::Month),
"DATE" => Some(Function::Date), "DATE" => Some(Function::Date),
"DATEDIF" => Some(Function::Datedif),
"DATEVALUE" => Some(Function::Datevalue),
"EDATE" => Some(Function::Edate), "EDATE" => Some(Function::Edate),
"NETWORKDAYS" => Some(Function::Networkdays),
"NETWORKDAYS.INTL" => Some(Function::NetworkdaysIntl),
"TIME" => Some(Function::Time),
"TIMEVALUE" => Some(Function::Timevalue),
"HOUR" => Some(Function::Hour),
"MINUTE" => Some(Function::Minute),
"SECOND" => Some(Function::Second),
"TODAY" => Some(Function::Today), "TODAY" => Some(Function::Today),
"NOW" => Some(Function::Now), "NOW" => Some(Function::Now),
"DAYS" | "_XLFN.DAYS" => Some(Function::Days),
"DAYS360" => Some(Function::Days360),
"WEEKDAY" => Some(Function::Weekday),
"WEEKNUM" => Some(Function::Weeknum),
"WORKDAY" => Some(Function::Workday),
"WORKDAY.INTL" => Some(Function::WorkdayIntl),
"YEARFRAC" => Some(Function::Yearfrac),
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
// Financial // Financial
"PMT" => Some(Function::Pmt), "PMT" => Some(Function::Pmt),
"PV" => Some(Function::Pv), "PV" => Some(Function::Pv),
@@ -796,9 +734,6 @@ impl fmt::Display for Function {
Function::Switch => write!(f, "SWITCH"), Function::Switch => write!(f, "SWITCH"),
Function::True => write!(f, "TRUE"), Function::True => write!(f, "TRUE"),
Function::Xor => write!(f, "XOR"), Function::Xor => write!(f, "XOR"),
Function::Log => write!(f, "LOG"),
Function::Log10 => write!(f, "LOG10"),
Function::Ln => write!(f, "LN"),
Function::Sin => write!(f, "SIN"), Function::Sin => write!(f, "SIN"),
Function::Cos => write!(f, "COS"), Function::Cos => write!(f, "COS"),
Function::Tan => write!(f, "TAN"), Function::Tan => write!(f, "TAN"),
@@ -893,26 +828,9 @@ impl fmt::Display for Function {
Function::Month => write!(f, "MONTH"), Function::Month => write!(f, "MONTH"),
Function::Eomonth => write!(f, "EOMONTH"), Function::Eomonth => write!(f, "EOMONTH"),
Function::Date => write!(f, "DATE"), Function::Date => write!(f, "DATE"),
Function::Datedif => write!(f, "DATEDIF"),
Function::Datevalue => write!(f, "DATEVALUE"),
Function::Edate => write!(f, "EDATE"), Function::Edate => write!(f, "EDATE"),
Function::Networkdays => write!(f, "NETWORKDAYS"),
Function::NetworkdaysIntl => write!(f, "NETWORKDAYS.INTL"),
Function::Time => write!(f, "TIME"),
Function::Timevalue => write!(f, "TIMEVALUE"),
Function::Hour => write!(f, "HOUR"),
Function::Minute => write!(f, "MINUTE"),
Function::Second => write!(f, "SECOND"),
Function::Today => write!(f, "TODAY"), Function::Today => write!(f, "TODAY"),
Function::Now => write!(f, "NOW"), Function::Now => write!(f, "NOW"),
Function::Days => write!(f, "DAYS"),
Function::Days360 => write!(f, "DAYS360"),
Function::Weekday => write!(f, "WEEKDAY"),
Function::Weeknum => write!(f, "WEEKNUM"),
Function::Workday => write!(f, "WORKDAY"),
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
Function::Yearfrac => write!(f, "YEARFRAC"),
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
Function::Pmt => write!(f, "PMT"), Function::Pmt => write!(f, "PMT"),
Function::Pv => write!(f, "PV"), Function::Pv => write!(f, "PV"),
Function::Rate => write!(f, "RATE"), Function::Rate => write!(f, "RATE"),
@@ -1043,9 +961,6 @@ impl Model {
Function::True => self.fn_true(args, cell), Function::True => self.fn_true(args, cell),
Function::Xor => self.fn_xor(args, cell), Function::Xor => self.fn_xor(args, cell),
// Math and trigonometry // Math and trigonometry
Function::Log => self.fn_log(args, cell),
Function::Log10 => self.fn_log10(args, cell),
Function::Ln => self.fn_ln(args, cell),
Function::Sin => self.fn_sin(args, cell), Function::Sin => self.fn_sin(args, cell),
Function::Cos => self.fn_cos(args, cell), Function::Cos => self.fn_cos(args, cell),
Function::Tan => self.fn_tan(args, cell), Function::Tan => self.fn_tan(args, cell),
@@ -1151,26 +1066,9 @@ impl Model {
Function::Eomonth => self.fn_eomonth(args, cell), Function::Eomonth => self.fn_eomonth(args, cell),
Function::Month => self.fn_month(args, cell), Function::Month => self.fn_month(args, cell),
Function::Date => self.fn_date(args, cell), Function::Date => self.fn_date(args, cell),
Function::Datedif => self.fn_datedif(args, cell),
Function::Datevalue => self.fn_datevalue(args, cell),
Function::Edate => self.fn_edate(args, cell), Function::Edate => self.fn_edate(args, cell),
Function::Networkdays => self.fn_networkdays(args, cell),
Function::NetworkdaysIntl => self.fn_networkdays_intl(args, cell),
Function::Time => self.fn_time(args, cell),
Function::Timevalue => self.fn_timevalue(args, cell),
Function::Hour => self.fn_hour(args, cell),
Function::Minute => self.fn_minute(args, cell),
Function::Second => self.fn_second(args, cell),
Function::Today => self.fn_today(args, cell), Function::Today => self.fn_today(args, cell),
Function::Now => self.fn_now(args, cell), Function::Now => self.fn_now(args, cell),
Function::Days => self.fn_days(args, cell),
Function::Days360 => self.fn_days360(args, cell),
Function::Weekday => self.fn_weekday(args, cell),
Function::Weeknum => self.fn_weeknum(args, cell),
Function::Workday => self.fn_workday(args, cell),
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
Function::Yearfrac => self.fn_yearfrac(args, cell),
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
// Financial // Financial
Function::Pmt => self.fn_pmt(args, cell), Function::Pmt => self.fn_pmt(args, cell),
Function::Pv => self.fn_pv(args, cell), Function::Pv => self.fn_pv(args, cell),
@@ -1316,7 +1214,7 @@ mod tests {
} }
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE // We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
let iter_list = Function::into_iter() let iter_list = Function::into_iter()
.map(|f| format!("{f}").replace('.', "")) .map(|f| format!("{}", f).replace('.', ""))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let len = iter_list.len(); let len = iter_list.len();

View File

@@ -350,7 +350,7 @@ impl Model {
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them? // FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len(); let args_count = args.len();
if args_count < 2 || !args_count.is_multiple_of(2) { if args_count < 2 || args_count % 2 == 1 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
@@ -476,7 +476,7 @@ impl Model {
F: FnMut(f64), F: FnMut(f64),
{ {
let args_count = args.len(); let args_count = args.len();
if args_count < 3 || args_count.is_multiple_of(2) { if args_count < 3 || args_count % 2 == 0 {
return Err(CalcResult::new_args_number_error(cell)); return Err(CalcResult::new_args_number_error(cell));
} }
let arg_0 = self.evaluate_node_in_context(&args[0], cell); let arg_0 = self.evaluate_node_in_context(&args[0], cell);

View File

@@ -55,14 +55,14 @@ impl Model {
let mut result = "".to_string(); let mut result = "".to_string();
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::String(value) => result = format!("{result}{value}"), CalcResult::String(value) => result = format!("{}{}", result, value),
CalcResult::Number(value) => result = format!("{result}{value}"), CalcResult::Number(value) => result = format!("{}{}", result, value),
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{result}TRUE"); result = format!("{}TRUE", result);
} else { } else {
result = format!("{result}FALSE"); result = format!("{}FALSE", result);
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
@@ -82,14 +82,16 @@ impl Model {
column, column,
}) { }) {
CalcResult::String(value) => { CalcResult::String(value) => {
result = format!("{result}{value}"); result = format!("{}{}", result, value);
}
CalcResult::Number(value) => {
result = format!("{}{}", result, value)
} }
CalcResult::Number(value) => result = format!("{result}{value}"),
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{result}TRUE"); result = format!("{}TRUE", result);
} else { } else {
result = format!("{result}FALSE"); result = format!("{}FALSE", result);
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
@@ -280,7 +282,7 @@ impl Model {
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -315,7 +317,7 @@ impl Model {
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -350,7 +352,7 @@ impl Model {
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -385,7 +387,7 @@ impl Model {
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -439,7 +441,7 @@ impl Model {
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -476,7 +478,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -558,7 +560,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -640,7 +642,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"), CalcResult::Number(v) => format!("{}", v),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {

View File

@@ -110,7 +110,7 @@ pub(crate) fn from_wildcard_to_regex(
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?") // And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
if exact { if exact {
return regex::Regex::new(&format!("^{reg}$")); return regex::Regex::new(&format!("^{}$", reg));
} }
regex::Regex::new(reg) regex::Regex::new(reg)
} }

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, sync::OnceLock}; use std::collections::HashMap;
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Booleans { pub struct Booleans {
@@ -30,17 +31,14 @@ pub struct Language {
pub errors: Errors, pub errors: Errors,
} }
static LANGUAGES: OnceLock<HashMap<String, Language>> = OnceLock::new();
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fn get_languages() -> &'static HashMap<String, Language> { static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
LANGUAGES.get_or_init(|| { bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file") });
})
}
pub fn get_language(id: &str) -> Result<&Language, String> { pub fn get_language(id: &str) -> Result<&Language, String> {
get_languages() let language = LANGUAGES
.get(id) .get(id)
.ok_or_else(|| format!("Language is not supported: '{id}'")) .ok_or(format!("Language is not supported: '{}'", id))?;
Ok(language)
} }

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, sync::OnceLock};
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
use std::collections::HashMap;
#[derive(Encode, Decode, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Locale { pub struct Locale {
@@ -64,17 +65,12 @@ pub struct DecimalFormats {
pub standard: String, pub standard: String,
} }
static LOCALES: OnceLock<HashMap<String, Locale>> = OnceLock::new();
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fn get_locales() -> &'static HashMap<String, Locale> { static LOCALES: Lazy<HashMap<String, Locale>> =
LOCALES.get_or_init(|| { Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale")
})
}
pub fn get_locale(id: &str) -> Result<&Locale, String> { pub fn get_locale(id: &str) -> Result<&Locale, String> {
get_locales() // TODO: pass the locale once we implement locales in Rust
.get(id) let locale = LOCALES.get(id).ok_or("Invalid locale")?;
.ok_or_else(|| format!("Invalid locale: '{id}'")) Ok(locale)
} }

View File

@@ -106,15 +106,15 @@ pub struct Model {
pub(crate) shared_strings: HashMap<String, usize>, pub(crate) shared_strings: HashMap<String, usize>,
/// An instance of the parser /// An instance of the parser
pub(crate) parser: Parser, pub(crate) parser: Parser,
/// The list of cells with formulas that are evaluated or being evaluated /// The list of cells with formulas that are evaluated of being evaluated
pub(crate) cells: HashMap<(u32, i32, i32), CellState>, pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The locale of the model /// The locale of the model
pub(crate) locale: Locale, pub(crate) locale: Locale,
/// The language used /// Tha language used
pub(crate) language: Language, pub(crate) language: Language,
/// The timezone used to evaluate the model /// The timezone used to evaluate the model
pub(crate) tz: Tz, pub(crate) tz: Tz,
/// The view id. A view consists of a selected sheet and ranges. /// The view id. A view consist of a selected sheet and ranges.
pub(crate) view_id: u32, pub(crate) view_id: u32,
} }
@@ -215,7 +215,7 @@ impl Model {
_ => CalcResult::new_error( _ => CalcResult::new_error(
Error::ERROR, Error::ERROR,
cell, cell,
format!("Error with Implicit Intersection in cell {cell:?}"), format!("Error with Implicit Intersection in cell {:?}", cell),
), ),
}, },
_ => self.evaluate_node_in_context(node, cell), _ => self.evaluate_node_in_context(node, cell),
@@ -355,7 +355,7 @@ impl Model {
return s; return s;
} }
}; };
let result = format!("{l}{r}"); let result = format!("{}{}", l, r);
CalcResult::String(result) CalcResult::String(result)
} }
OpProductKind { kind, left, right } => match kind { OpProductKind { kind, left, right } => match kind {
@@ -375,7 +375,7 @@ impl Model {
} }
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell), FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
InvalidFunctionKind { name, args: _ } => { InvalidFunctionKind { name, args: _ } => {
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {name}")) CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
} }
ArrayKind(s) => CalcResult::Array(s.to_owned()), ArrayKind(s) => CalcResult::Array(s.to_owned()),
DefinedNameKind((name, scope, _)) => { DefinedNameKind((name, scope, _)) => {
@@ -391,26 +391,26 @@ impl Model {
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{name}\" is not a reference."), format!("Defined name \"{}\" is not a reference.", name),
), ),
} }
} else { } else {
CalcResult::new_error( CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{name}\" not found."), format!("Defined name \"{}\" not found.", name),
) )
} }
} }
TableNameKind(s) => CalcResult::new_error( TableNameKind(s) => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("table name \"{s}\" not supported."), format!("table name \"{}\" not supported.", s),
), ),
WrongVariableKind(s) => CalcResult::new_error( WrongVariableKind(s) => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Variable name \"{s}\" not found."), format!("Variable name \"{}\" not found.", s),
), ),
CompareKind { kind, left, right } => { CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell); let l = self.evaluate_node_in_context(left, cell);
@@ -487,7 +487,7 @@ impl Model {
} => CalcResult::new_error( } => CalcResult::new_error(
Error::ERROR, Error::ERROR,
cell, cell,
format!("Error parsing {formula}: {message}"), format!("Error parsing {}: {}", formula, message),
), ),
EmptyArgKind => CalcResult::EmptyArg, EmptyArgKind => CalcResult::EmptyArg,
ImplicitIntersection { ImplicitIntersection {
@@ -500,7 +500,7 @@ impl Model {
None => CalcResult::new_error( None => CalcResult::new_error(
Error::VALUE, Error::VALUE,
cell, cell,
format!("Error with Implicit Intersection in cell {cell:?}"), format!("Error with Implicit Intersection in cell {:?}", cell),
), ),
} }
} }
@@ -697,7 +697,7 @@ impl Model {
worksheet.color = Some(color.to_string()); worksheet.color = Some(color.to_string());
return Ok(()); return Ok(());
} }
Err(format!("Invalid color: {color}")) Err(format!("Invalid color: {}", color))
} }
/// Changes the visibility of a sheet /// Changes the visibility of a sheet
@@ -1027,7 +1027,7 @@ impl Model {
let source_sheet_name = self let source_sheet_name = self
.workbook .workbook
.worksheet(source.sheet) .worksheet(source.sheet)
.map_err(|e| format!("Could not find source worksheet: {e}"))? .map_err(|e| format!("Could not find source worksheet: {}", e))?
.get_name(); .get_name();
if source.sheet != area.sheet { if source.sheet != area.sheet {
return Err("Source and area are in different sheets".to_string()); return Err("Source and area are in different sheets".to_string());
@@ -1041,7 +1041,7 @@ impl Model {
let target_sheet_name = self let target_sheet_name = self
.workbook .workbook
.worksheet(target.sheet) .worksheet(target.sheet)
.map_err(|e| format!("Could not find target worksheet: {e}"))? .map_err(|e| format!("Could not find target worksheet: {}", e))?
.get_name(); .get_name();
if let Some(formula) = value.strip_prefix('=') { if let Some(formula) = value.strip_prefix('=') {
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -1061,7 +1061,7 @@ impl Model {
column_delta: target.column - source.column, column_delta: target.column - source.column,
}, },
); );
Ok(format!("={formula_str}")) Ok(format!("={}", formula_str))
} else { } else {
Ok(value.to_string()) Ok(value.to_string())
} }
@@ -1538,7 +1538,7 @@ impl Model {
// If the formula fails to parse try adding a parenthesis // If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3) // SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula { if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self.parser.parse(&format!("{formula})"), &cell_reference); let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference);
match new_parsed_formula { match new_parsed_formula {
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula, _ => parsed_formula = new_parsed_formula,
@@ -1931,16 +1931,32 @@ impl Model {
} }
/// Returns markup representation of the given `sheet`. /// Returns markup representation of the given `sheet`.
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> { pub fn get_sheet_markup(
let worksheet = self.workbook.worksheet(sheet)?; &self,
let dimension = worksheet.dimension(); sheet: u32,
start_row: i32,
start_column: i32,
width: i32,
height: i32,
) -> Result<String, String> {
let mut table: Vec<Vec<String>> = Vec::new();
if start_row < 1 || start_column < 1 {
return Err("Start row and column must be positive".to_string());
}
if start_row + height >= LAST_ROW || start_column + width >= LAST_COLUMN {
return Err("Start row and column exceed the maximum allowed".to_string());
}
if height <= 0 || width <= 0 {
return Err("Height must be positive and width must be positive".to_string());
}
let mut rows = Vec::new(); // a mutable vector to store the column widths of length `width + 1`
let mut column_widths: Vec<f64> = vec![0.0; (width + 1) as usize];
for row in 1..(dimension.max_row + 1) { for row in start_row..(start_row + height + 1) {
let mut row_markup: Vec<String> = Vec::new(); let mut row_markup: Vec<String> = Vec::new();
for column in 1..(dimension.max_column + 1) { for column in start_column..(start_column + width + 1) {
let mut cell_markup = match self.get_cell_formula(sheet, row, column)? { let mut cell_markup = match self.get_cell_formula(sheet, row, column)? {
Some(formula) => formula, Some(formula) => formula,
None => self.get_formatted_cell_value(sheet, row, column)?, None => self.get_formatted_cell_value(sheet, row, column)?,
@@ -1949,12 +1965,34 @@ impl Model {
if style.font.b { if style.font.b {
cell_markup = format!("**{cell_markup}**") cell_markup = format!("**{cell_markup}**")
} }
column_widths[(column - start_column) as usize] =
column_widths[(column - start_column) as usize].max(cell_markup.len() as f64);
row_markup.push(cell_markup); row_markup.push(cell_markup);
} }
rows.push(row_markup.join("|")); table.push(row_markup);
} }
let mut rows = Vec::new();
for (j, row) in table.iter().enumerate() {
if j == 1 {
let mut row_markup = String::new();
for i in 0..(width + 1) {
row_markup.push('|');
let wide = column_widths[i as usize] as usize;
row_markup.push_str(&"-".repeat(wide));
}
rows.push(row_markup);
}
let mut row_markup = String::new();
for (i, cell) in row.iter().enumerate() {
row_markup.push('|');
let wide = column_widths[i] as usize;
// Add padding to the cell content
row_markup.push_str(&format!("{:<wide$}", cell, wide = wide));
}
rows.push(row_markup);
}
Ok(rows.join("\n")) Ok(rows.join("\n"))
} }

View File

@@ -168,11 +168,11 @@ impl Model {
.get_worksheet_names() .get_worksheet_names()
.iter() .iter()
.map(|s| s.to_uppercase()) .map(|s| s.to_uppercase())
.any(|x| x == format!("{base_name_uppercase}{index}")) .any(|x| x == format!("{}{}", base_name_uppercase, index))
{ {
index += 1; index += 1;
} }
let sheet_name = format!("{base_name}{index}"); let sheet_name = format!("{}{}", base_name, index);
// Now we need a sheet_id // Now we need a sheet_id
let sheet_id = self.get_new_sheet_id(); let sheet_id = self.get_new_sheet_id();
let view_ids: Vec<&u32> = self.workbook.views.keys().collect(); let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
@@ -192,7 +192,7 @@ impl Model {
sheet_id: Option<u32>, sheet_id: Option<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(sheet_name) { if !is_valid_sheet_name(sheet_name) {
return Err(format!("Invalid name for a sheet: '{sheet_name}'")); return Err(format!("Invalid name for a sheet: '{}'", sheet_name));
} }
if self if self
.workbook .workbook
@@ -234,7 +234,7 @@ impl Model {
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) { if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
return self.rename_sheet_by_index(sheet_index, new_name); return self.rename_sheet_by_index(sheet_index, new_name);
} }
Err(format!("Could not find sheet {old_name}")) Err(format!("Could not find sheet {}", old_name))
} }
/// Renames a sheet and updates all existing references to that sheet. /// Renames a sheet and updates all existing references to that sheet.
@@ -248,10 +248,10 @@ impl Model {
new_name: &str, new_name: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(new_name) { if !is_valid_sheet_name(new_name) {
return Err(format!("Invalid name for a sheet: '{new_name}'.")); return Err(format!("Invalid name for a sheet: '{}'.", new_name));
} }
if self.get_sheet_index_by_name(new_name).is_some() { if self.get_sheet_index_by_name(new_name).is_some() {
return Err(format!("Sheet already exists: '{new_name}'.")); return Err(format!("Sheet already exists: '{}'.", new_name));
} }
// Gets the new name and checks that a sheet with that index exists // Gets the new name and checks that a sheet with that index exists
let old_name = self.workbook.worksheet(sheet_index)?.get_name(); let old_name = self.workbook.worksheet(sheet_index)?.get_name();
@@ -362,14 +362,14 @@ impl Model {
}; };
let locale = match get_locale(locale_id) { let locale = match get_locale(locale_id) {
Ok(l) => l.clone(), Ok(l) => l.clone(),
Err(_) => return Err(format!("Invalid locale: {locale_id}")), Err(_) => return Err(format!("Invalid locale: {}", locale_id)),
}; };
let milliseconds = get_milliseconds_since_epoch(); let milliseconds = get_milliseconds_since_epoch();
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match DateTime::from_timestamp(seconds, 0) { let dt = match DateTime::from_timestamp(seconds, 0) {
Some(s) => s, Some(s) => s,
None => return Err(format!("Invalid timestamp: {milliseconds}")), None => return Err(format!("Invalid timestamp: {}", milliseconds)),
}; };
// "2020-08-06T21:20:53Z // "2020-08-06T21:20:53Z
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();

View File

@@ -126,7 +126,7 @@ pub fn to_precision_str(value: f64, precision: usize) -> String {
let exponent = value.abs().log10().floor(); let exponent = value.abs().log10().floor();
let base = value / 10.0_f64.powf(exponent); let base = value / 10.0_f64.powf(exponent);
let base = format!("{0:.1$}", base, precision - 1); let base = format!("{0:.1$}", base, precision - 1);
let value = format!("{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 // TODO: do this in a way that does not require a possible error
0.0 0.0
}); });

View File

@@ -154,7 +154,7 @@ impl Styles {
return Ok(cell_style.xf_id); 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> { pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {

View File

@@ -7,8 +7,6 @@ mod test_column_width;
mod test_criteria; mod test_criteria;
mod test_currency; mod test_currency;
mod test_date_and_time; mod test_date_and_time;
mod test_datedif_leap_month_end;
mod test_days360_month_end;
mod test_error_propagation; mod test_error_propagation;
mod test_fn_average; mod test_fn_average;
mod test_fn_averageifs; mod test_fn_averageifs;
@@ -29,7 +27,6 @@ mod test_fn_sum;
mod test_fn_sumifs; mod test_fn_sumifs;
mod test_fn_textbefore; mod test_fn_textbefore;
mod test_fn_textjoin; mod test_fn_textjoin;
mod test_fn_time;
mod test_fn_unicode; mod test_fn_unicode;
mod test_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
@@ -46,11 +43,8 @@ mod test_sheets;
mod test_styles; mod test_styles;
mod test_trigonometric; mod test_trigonometric;
mod test_true_false; mod test_true_false;
mod test_weekday_return_types;
mod test_weeknum_return_types;
mod test_workbook; mod test_workbook;
mod test_worksheet; mod test_worksheet;
mod test_yearfrac_basis;
pub(crate) mod util; pub(crate) mod util;
mod engineering; mod engineering;
@@ -61,17 +55,12 @@ mod test_arrays;
mod test_escape_quotes; mod test_escape_quotes;
mod test_extend; mod test_extend;
mod test_fn_fv; mod test_fn_fv;
mod test_fn_round;
mod test_fn_type; mod test_fn_type;
mod test_frozen_rows_and_columns; mod test_frozen_rows_and_columns;
mod test_geomean; mod test_geomean;
mod test_get_cell_content; mod test_get_cell_content;
mod test_implicit_intersection; mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_ln;
mod test_log;
mod test_log10;
mod test_networkdays;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;
mod test_today; mod test_today;

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW}; use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN};
use crate::model::Model; use crate::model::Model;
use crate::test::util::new_empty_model; use crate::test::util::new_empty_model;
use crate::types::Col; use crate::types::Col;
@@ -508,10 +508,6 @@ fn test_move_column_right() {
assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)"); assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)");
assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)"); assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)");
assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)"); assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)");
// Data moved as well
assert_eq!(model._get_text("G1"), "1");
assert_eq!(model._get_text("H1"), "3");
} }
#[test] #[test]
@@ -536,249 +532,5 @@ fn tets_move_column_error() {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test]
fn test_move_row_down() {
let mut model = new_empty_model();
populate_table(&mut model);
// Formulas referencing rows 3 and 4
model._set("E3", "=G3");
model._set("E4", "=G4");
model._set("E5", "=SUM(G3:J3)");
model._set("E6", "=SUM(G3:G3)");
model._set("E7", "=SUM(G4:G4)");
model.evaluate();
// Move row 3 down by one position
let result = model.move_row_action(0, 3, 1);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=G3");
assert_eq!(model._get_formula("E4"), "=G4");
assert_eq!(model._get_formula("E5"), "=SUM(G4:J4)");
assert_eq!(model._get_formula("E6"), "=SUM(G4:G4)");
assert_eq!(model._get_formula("E7"), "=SUM(G3:G3)");
// Data moved as well
assert_eq!(model._get_text("G4"), "-2");
assert_eq!(model._get_text("G3"), "");
}
#[test]
fn test_move_row_up() {
let mut model = new_empty_model();
populate_table(&mut model);
// Formulas referencing rows 4 and 5
model._set("E4", "=G4");
model._set("E5", "=G5");
model._set("E6", "=SUM(G4:J4)");
model._set("E7", "=SUM(G4:G4)");
model._set("E8", "=SUM(G5:G5)");
model.evaluate();
// Move row 5 up by one position
let result = model.move_row_action(0, 5, -1);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("E4"), "=G4");
assert_eq!(model._get_formula("E5"), "=G5");
assert_eq!(model._get_formula("E6"), "=SUM(G5:J5)");
assert_eq!(model._get_formula("E7"), "=SUM(G5:G5)");
assert_eq!(model._get_formula("E8"), "=SUM(G4:G4)");
// Data moved as well
assert_eq!(model._get_text("G4"), "");
assert_eq!(model._get_text("G5"), "");
}
#[test]
fn test_move_row_error() {
let mut model = new_empty_model();
model.evaluate();
let result = model.move_row_action(0, 7, -10);
assert!(result.is_err());
let result = model.move_row_action(0, -7, 20);
assert!(result.is_err());
let result = model.move_row_action(0, LAST_ROW, 1);
assert!(result.is_err());
let result = model.move_row_action(0, LAST_ROW + 1, -10);
assert!(result.is_err());
// This works
let result = model.move_row_action(0, LAST_ROW, -1);
assert!(result.is_ok());
}
#[test]
fn test_move_row_down_absolute_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
// Absolute references
model._set("E3", "=$G$3");
model._set("E4", "=$G$4");
model._set("E5", "=SUM($G$3:$J$3)");
model._set("E6", "=SUM($G$3:$G$3)");
model._set("E7", "=SUM($G$4:$G$4)");
model.evaluate();
assert!(model.move_row_action(0, 3, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$G$3");
assert_eq!(model._get_formula("E4"), "=$G$4");
assert_eq!(model._get_formula("E5"), "=SUM($G$4:$J$4)");
assert_eq!(model._get_formula("E6"), "=SUM($G$4:$G$4)");
assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$3)");
}
#[test]
fn test_move_column_right_absolute_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
// Absolute references
model._set("E3", "=$G$3");
model._set("E4", "=$H$3");
model._set("E5", "=SUM($G$3:$J$7)");
model._set("E6", "=SUM($G$3:$G$7)");
model._set("E7", "=SUM($H$3:$H$7)");
model.evaluate();
assert!(model.move_column_action(0, 7, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$H$3");
assert_eq!(model._get_formula("E4"), "=$G$3");
assert_eq!(model._get_formula("E5"), "=SUM($H$3:$J$7)");
assert_eq!(model._get_formula("E6"), "=SUM($H$3:$H$7)");
assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$7)");
}
#[test]
fn test_move_row_down_mixed_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
model._set("E3", "=$G3"); // absolute col, relative row
model._set("E4", "=$G4");
model._set("E5", "=SUM($G3:$J3)");
model._set("E6", "=SUM($G3:$G3)");
model._set("E7", "=SUM($G4:$G4)");
model._set("F3", "=H$3"); // relative col, absolute row
model._set("F4", "=G$3");
model.evaluate();
assert!(model.move_row_action(0, 3, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$G3");
assert_eq!(model._get_formula("E4"), "=$G4");
assert_eq!(model._get_formula("E5"), "=SUM($G4:$J4)");
assert_eq!(model._get_formula("E6"), "=SUM($G4:$G4)");
assert_eq!(model._get_formula("E7"), "=SUM($G3:$G3)");
assert_eq!(model._get_formula("F3"), "=G$4");
assert_eq!(model._get_formula("F4"), "=H$4");
}
#[test]
fn test_move_column_right_mixed_refs() {
let mut model = new_empty_model();
populate_table(&mut model);
model._set("E3", "=$G3");
model._set("E4", "=$H3");
model._set("E5", "=SUM($G3:$J7)");
model._set("E6", "=SUM($G3:$G7)");
model._set("E7", "=SUM($H3:$H7)");
model._set("F3", "=H$3");
model._set("F4", "=H$3");
model.evaluate();
assert!(model.move_column_action(0, 7, 1).is_ok());
model.evaluate();
assert_eq!(model._get_formula("E3"), "=$H3");
assert_eq!(model._get_formula("E4"), "=$G3");
assert_eq!(model._get_formula("E5"), "=SUM($H3:$J7)");
assert_eq!(model._get_formula("E6"), "=SUM($H3:$H7)");
assert_eq!(model._get_formula("E7"), "=SUM($G3:$G7)");
assert_eq!(model._get_formula("F3"), "=G$3");
assert_eq!(model._get_formula("F4"), "=G$3");
}
#[test]
fn test_move_row_height() {
let mut model = new_empty_model();
let sheet = 0;
let custom_height = DEFAULT_ROW_HEIGHT * 2.0;
// Set a custom height for row 3
model
.workbook
.worksheet_mut(sheet)
.unwrap()
.set_row_height(3, custom_height)
.unwrap();
// Record the original height of row 4 (should be the default)
let original_row4_height = model.get_row_height(sheet, 4).unwrap();
// Move row 3 down by one position
assert!(model.move_row_action(sheet, 3, 1).is_ok());
// The custom height should now be on row 4
assert_eq!(model.get_row_height(sheet, 4), Ok(custom_height));
// Row 3 should now have the previous height of row 4
assert_eq!(model.get_row_height(sheet, 3), Ok(original_row4_height));
}
/// Moving a row down by two positions should shift formulas on intermediate
/// rows by only one (the row that gets skipped), not by the full delta this
/// guards against the regression fixed in the RowMove displacement logic.
#[test]
fn test_row_move_down_two_updates_intermediate_refs_by_one() {
let mut model = new_empty_model();
populate_table(&mut model);
// Set up formulas to verify intermediate rows shift by 1 (not full delta).
model._set("E3", "=G3"); // target row
model._set("E4", "=G4"); // intermediate row
model._set("E5", "=SUM(G3:J3)");
model.evaluate();
// Move row 3 down by two positions (row 3 -> row 5)
assert!(model.move_row_action(0, 3, 2).is_ok());
model.evaluate();
// Assert that references for the moved row and intermediate row are correct.
assert_eq!(model._get_formula("E3"), "=G3");
assert_eq!(model._get_formula("E5"), "=G5");
assert_eq!(model._get_formula("E4"), "=SUM(G5:J5)");
}
/// Moving a column right by two positions should shift formulas on
/// intermediate columns by only one, ensuring the ColumnMove displacement
/// logic handles multi-position moves correctly.
#[test]
fn test_column_move_right_two_updates_intermediate_refs_by_one() {
let mut model = new_empty_model();
populate_table(&mut model);
// Set up formulas to verify intermediate columns shift by 1 (not full delta).
model._set("E3", "=$G3"); // target column
model._set("E4", "=$H3"); // intermediate column
model._set("E5", "=SUM($G3:$J7)");
model.evaluate();
// Move column G (7) right by two positions (G -> I)
assert!(model.move_column_action(0, 7, 2).is_ok());
model.evaluate();
// Assert that references for moved and intermediate columns are correct.
assert_eq!(model._get_formula("E3"), "=$I3");
assert_eq!(model._get_formula("E4"), "=$G3");
assert_eq!(model._get_formula("E5"), "=SUM($I3:$J7)");
}
// A B C D E F G H I J K L M N O P Q R // A B C D E F G H I J K L M N O P Q R
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

View File

@@ -6,11 +6,6 @@
/// We can also enter examples that illustrate/document a part of the function /// We can also enter examples that illustrate/document a part of the function
use crate::{cell::CellValue, test::util::new_empty_model}; use crate::{cell::CellValue, test::util::new_empty_model};
// Excel uses a serial date system where Jan 1, 1900 = 1 (though it treats 1900 as a leap year)
// Most test dates are documented inline, but we define boundary values here:
const EXCEL_MAX_DATE: f64 = 2958465.0; // Dec 31, 9999 - used in boundary tests
const EXCEL_INVALID_DATE: f64 = 2958466.0; // One day past max - used in error tests
#[test] #[test]
fn test_fn_date_arguments() { fn test_fn_date_arguments() {
let mut model = new_empty_model(); let mut model = new_empty_model();
@@ -221,382 +216,3 @@ fn test_date_early_dates() {
Ok(CellValue::Number(61.0)) Ok(CellValue::Number(61.0))
); );
} }
#[test]
fn test_days_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=DAYS(44570,44561)");
model._set("A2", "=DAYS(44561,44570)"); // Reversed order
model._set("A3", "=DAYS(44561,44561)");
// Edge cases
model._set("A4", "=DAYS(1,2)"); // Early dates
model._set(
"A5",
&format!("=DAYS({},{})", EXCEL_MAX_DATE, EXCEL_MAX_DATE - 1.0),
); // Near max date
// Error cases - wrong argument count
model._set("A6", "=DAYS()");
model._set("A7", "=DAYS(44561)");
model._set("A8", "=DAYS(44561,44570,1)");
// Error cases - invalid dates
model._set("A9", "=DAYS(-1,44561)");
model._set("A10", &format!("=DAYS(44561,{EXCEL_INVALID_DATE})"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"9");
assert_eq!(model._get_text("A2"), *"-9");
assert_eq!(model._get_text("A3"), *"0");
assert_eq!(model._get_text("A4"), *"-1"); // DAYS(1,2) = 1-2 = -1
assert_eq!(model._get_text("A5"), *"1");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#ERROR!");
assert_eq!(model._get_text("A9"), *"#NUM!");
assert_eq!(model._get_text("A10"), *"#NUM!");
}
#[test]
fn test_days360_function() {
let mut model = new_empty_model();
// Basic functionality with different basis values
model._set("A1", "=DAYS360(44196,44560)"); // Default basis (US 30/360)
model._set("A2", "=DAYS360(44196,44560,FALSE)"); // US 30/360 explicitly
model._set("A3", "=DAYS360(44196,44560,TRUE)"); // European 30/360
// Same date
model._set("A4", "=DAYS360(44561,44561)");
model._set("A5", "=DAYS360(44561,44561,TRUE)");
// Reverse order (negative result)
model._set("A6", "=DAYS360(44560,44196)");
model._set("A7", "=DAYS360(44560,44196,TRUE)");
// Edge cases
model._set("A8", "=DAYS360(1,2)");
model._set("A9", "=DAYS360(1,2,FALSE)");
// Error cases - wrong argument count
model._set("A10", "=DAYS360()");
model._set("A11", "=DAYS360(44561)");
model._set("A12", "=DAYS360(44561,44570,TRUE,1)");
// Error cases - invalid dates
model._set("A13", "=DAYS360(-1,44561)");
model._set("A14", &format!("=DAYS360(44561,{EXCEL_INVALID_DATE})"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"360");
assert_eq!(model._get_text("A2"), *"360");
assert_eq!(model._get_text("A3"), *"360");
assert_eq!(model._get_text("A4"), *"0");
assert_eq!(model._get_text("A5"), *"0");
assert_eq!(model._get_text("A6"), *"-360");
assert_eq!(model._get_text("A7"), *"-360");
assert_eq!(model._get_text("A8"), *"1");
assert_eq!(model._get_text("A9"), *"1");
assert_eq!(model._get_text("A10"), *"#ERROR!");
assert_eq!(model._get_text("A11"), *"#ERROR!");
assert_eq!(model._get_text("A12"), *"#ERROR!");
assert_eq!(model._get_text("A13"), *"#NUM!");
assert_eq!(model._get_text("A14"), *"#NUM!");
}
#[test]
fn test_weekday_function() {
let mut model = new_empty_model();
// Test return_type parameter variations with one known date (Friday 44561)
model._set("A1", "=WEEKDAY(44561)"); // Default: Sun=1, Fri=6
model._set("A2", "=WEEKDAY(44561,2)"); // Mon=1, Fri=5
model._set("A3", "=WEEKDAY(44561,3)"); // Mon=0, Fri=4
// Test boundary days (Sun/Mon) to verify return_type logic
model._set("A4", "=WEEKDAY(44556,1)"); // Sunday: should be 1
model._set("A5", "=WEEKDAY(44556,2)"); // Sunday: should be 7
model._set("A6", "=WEEKDAY(44557,2)"); // Monday: should be 1
// Error cases
model._set("A7", "=WEEKDAY()"); // Wrong arg count
model._set("A8", "=WEEKDAY(44561,0)"); // Invalid return_type
model._set("A9", "=WEEKDAY(-1)"); // Invalid date
model.evaluate();
// Core functionality
assert_eq!(model._get_text("A1"), *"6"); // Friday default
assert_eq!(model._get_text("A2"), *"5"); // Friday Mon=1
assert_eq!(model._get_text("A3"), *"4"); // Friday Mon=0
// Boundary verification
assert_eq!(model._get_text("A4"), *"1"); // Sunday Sun=1
assert_eq!(model._get_text("A5"), *"7"); // Sunday Mon=1
assert_eq!(model._get_text("A6"), *"1"); // Monday Mon=1
// Error cases
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#VALUE!");
assert_eq!(model._get_text("A9"), *"#NUM!");
}
#[test]
fn test_weeknum_function() {
let mut model = new_empty_model();
// Test different return_type values (1=week starts Sunday, 2=week starts Monday)
model._set("A1", "=WEEKNUM(44561)"); // Default return_type=1
model._set("A2", "=WEEKNUM(44561,1)"); // Sunday start
model._set("A3", "=WEEKNUM(44561,2)"); // Monday start
// Test year boundaries
model._set("A4", "=WEEKNUM(43831,1)"); // Jan 1, 2020 (Wednesday)
model._set("A5", "=WEEKNUM(43831,2)"); // Jan 1, 2020 (Wednesday)
model._set("A6", "=WEEKNUM(44196,1)"); // Dec 31, 2020 (Thursday)
model._set("A7", "=WEEKNUM(44196,2)"); // Dec 31, 2020 (Thursday)
// Test first and last weeks of year
model._set("A8", "=WEEKNUM(44197,1)"); // Jan 1, 2021 (Friday)
model._set("A9", "=WEEKNUM(44197,2)"); // Jan 1, 2021 (Friday)
model._set("A10", "=WEEKNUM(44561,1)"); // Dec 31, 2021 (Friday)
model._set("A11", "=WEEKNUM(44561,2)"); // Dec 31, 2021 (Friday)
// Error cases - wrong argument count
model._set("A12", "=WEEKNUM()");
model._set("A13", "=WEEKNUM(44561,1,1)");
// Error cases - invalid return_type
model._set("A14", "=WEEKNUM(44561,0)");
model._set("A15", "=WEEKNUM(44561,3)");
model._set("A16", "=WEEKNUM(44561,-1)");
// Error cases - invalid dates
model._set("A17", "=WEEKNUM(-1)");
model._set("A18", &format!("=WEEKNUM({EXCEL_INVALID_DATE})"));
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"53"); // Week 53
assert_eq!(model._get_text("A2"), *"53"); // Week 53 (Sunday start)
assert_eq!(model._get_text("A3"), *"53"); // Week 53 (Monday start)
// Year boundary tests
assert_eq!(model._get_text("A4"), *"1"); // Jan 1, 2020 (Sunday start)
assert_eq!(model._get_text("A5"), *"1"); // Jan 1, 2020 (Monday start)
assert_eq!(model._get_text("A6"), *"53"); // Dec 31, 2020 (Sunday start)
assert_eq!(model._get_text("A7"), *"53"); // Dec 31, 2020 (Monday start)
// 2021 tests
assert_eq!(model._get_text("A8"), *"1"); // Jan 1, 2021 (Sunday start)
assert_eq!(model._get_text("A9"), *"1"); // Jan 1, 2021 (Monday start)
assert_eq!(model._get_text("A10"), *"53"); // Dec 31, 2021 (Sunday start)
assert_eq!(model._get_text("A11"), *"53"); // Dec 31, 2021 (Monday start)
// Error cases
assert_eq!(model._get_text("A12"), *"#ERROR!");
assert_eq!(model._get_text("A13"), *"#ERROR!");
assert_eq!(model._get_text("A14"), *"#VALUE!");
assert_eq!(model._get_text("A15"), *"#VALUE!");
assert_eq!(model._get_text("A16"), *"#VALUE!");
assert_eq!(model._get_text("A17"), *"#NUM!");
assert_eq!(model._get_text("A18"), *"#NUM!");
}
#[test]
fn test_workday_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=WORKDAY(44560,1)");
model._set("A2", "=WORKDAY(44561,-1)");
model._set("A3", "=WORKDAY(44561,0)");
model._set("A4", "=WORKDAY(44560,5)");
// Test with holidays
model._set("B1", "44561");
model._set("A5", "=WORKDAY(44560,1,B1)"); // Should skip the holiday
model._set("B2", "44562");
model._set("B3", "44563");
model._set("A6", "=WORKDAY(44560,3,B1:B3)"); // Multiple holidays
// Test starting on weekend
model._set("A7", "=WORKDAY(44562,1)"); // Saturday start
model._set("A8", "=WORKDAY(44563,1)"); // Sunday start
// Test negative workdays
model._set("A9", "=WORKDAY(44565,-3)"); // Go backwards 3 days
model._set("A10", "=WORKDAY(44565,-5,B1:B3)"); // Backwards with holidays
// Edge cases
model._set("A11", "=WORKDAY(1,1)"); // Early date
model._set("A12", "=WORKDAY(100000,10)"); // Large numbers
// Error cases - wrong argument count
model._set("A13", "=WORKDAY()");
model._set("A14", "=WORKDAY(44560)");
model._set("A15", "=WORKDAY(44560,1,B1,B2)");
// Error cases - invalid dates
model._set("A16", "=WORKDAY(-1,1)");
model._set("A17", &format!("=WORKDAY({EXCEL_INVALID_DATE},1)"));
// Error cases - invalid holiday dates
model._set("B4", "-1");
model._set("A18", "=WORKDAY(44560,1,B4)");
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"44561"); // 1 day forward
assert_eq!(model._get_text("A2"), *"44560"); // 1 day backward
assert_eq!(model._get_text("A3"), *"44561"); // 0 days
assert_eq!(model._get_text("A4"), *"44567"); // 5 days forward
// With holidays
assert_eq!(model._get_text("A5"), *"44564"); // Skip holiday, go to Monday
assert_eq!(model._get_text("A6"), *"44566"); // Skip multiple holidays
// Weekend starts
assert_eq!(model._get_text("A7"), *"44564"); // From Saturday
assert_eq!(model._get_text("A8"), *"44564"); // From Sunday
// Negative workdays
assert_eq!(model._get_text("A9"), *"44560"); // 3 days back
assert_eq!(model._get_text("A10"), *"44557"); // 5 days back with holidays
// Edge cases
assert_eq!(model._get_text("A11"), *"2"); // Early date
assert_eq!(model._get_text("A12"), *"100014"); // Large numbers
// Error cases
assert_eq!(model._get_text("A13"), *"#ERROR!");
assert_eq!(model._get_text("A14"), *"#ERROR!");
assert_eq!(model._get_text("A15"), *"#ERROR!");
assert_eq!(model._get_text("A16"), *"#NUM!");
assert_eq!(model._get_text("A17"), *"#NUM!");
assert_eq!(model._get_text("A18"), *"#NUM!"); // Invalid holiday
}
#[test]
fn test_workday_intl_function() {
let mut model = new_empty_model();
// Test key weekend mask types
model._set("A1", "=WORKDAY.INTL(44560,1,1)"); // Numeric: standard (Sat-Sun)
model._set("A2", "=WORKDAY.INTL(44560,1,2)"); // Numeric: Sun-Mon
model._set("A3", "=WORKDAY.INTL(44560,1,\"0000001\")"); // String: Sunday only
model._set("A4", "=WORKDAY.INTL(44560,1,\"1100000\")"); // String: Mon-Tue
// Test with holidays
model._set("B1", "44561");
model._set("A5", "=WORKDAY.INTL(44560,2,1,B1)"); // Standard + holiday
model._set("A6", "=WORKDAY.INTL(44560,2,7,B1)"); // Fri-Sat + holiday
// Basic edge cases
model._set("A7", "=WORKDAY.INTL(44561,0,1)"); // Zero days
model._set("A8", "=WORKDAY.INTL(44565,-1,1)"); // Negative days
// Error cases
model._set("A9", "=WORKDAY.INTL()"); // Wrong arg count
model._set("A10", "=WORKDAY.INTL(44560,1,0)"); // Invalid weekend mask
model._set("A11", "=WORKDAY.INTL(44560,1,\"123\")"); // Invalid string mask
model._set("A12", "=WORKDAY.INTL(-1,1,1)"); // Invalid date
model.evaluate();
// Weekend mask functionality
assert_eq!(model._get_text("A1"), *"44561"); // Standard weekend
assert_eq!(model._get_text("A2"), *"44561"); // Sun-Mon weekend
assert_eq!(model._get_text("A3"), *"44561"); // Sunday only
assert_eq!(model._get_text("A4"), *"44561"); // Mon-Tue weekend
// With holidays
assert_eq!(model._get_text("A5"), *"44565"); // Skip holiday + standard weekend
assert_eq!(model._get_text("A6"), *"44564"); // Skip holiday + Fri-Sat weekend
// Edge cases
assert_eq!(model._get_text("A7"), *"44561"); // Zero days
assert_eq!(model._get_text("A8"), *"44564"); // Negative days
// Error cases
assert_eq!(model._get_text("A9"), *"#ERROR!");
assert_eq!(model._get_text("A10"), *"#NUM!");
assert_eq!(model._get_text("A11"), *"#VALUE!");
assert_eq!(model._get_text("A12"), *"#NUM!");
}
#[test]
fn test_yearfrac_function() {
let mut model = new_empty_model();
// Test key basis values (not exhaustive - just verify parameter works)
model._set("A1", "=YEARFRAC(44561,44926)"); // Default (30/360)
model._set("A2", "=YEARFRAC(44561,44926,1)"); // Actual/actual
model._set("A3", "=YEARFRAC(44561,44926,4)"); // European 30/360
// Edge cases
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
model._set("A5", "=YEARFRAC(44926,44561,1)"); // Reverse = negative
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
// Error cases
model._set("A7", "=YEARFRAC()"); // Wrong arg count
model._set("A8", "=YEARFRAC(44561,44926,5)"); // Invalid basis
model._set("A9", "=YEARFRAC(-1,44926,1)"); // Invalid date
model.evaluate();
// Basic functionality (approximate values expected)
assert_eq!(model._get_text("A1"), *"1"); // About 1 year
assert_eq!(model._get_text("A2"), *"1"); // About 1 year
assert_eq!(model._get_text("A3"), *"1"); // About 1 year
// Edge cases
assert_eq!(model._get_text("A4"), *"0"); // Same date
assert_eq!(model._get_text("A5"), *"-1"); // Negative
assert_eq!(model._get_text("A6"), *"1"); // Exact year
// Error cases
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#NUM!"); // Invalid basis should return #NUM!
assert_eq!(model._get_text("A9"), *"#NUM!");
}
#[test]
fn test_isoweeknum_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=ISOWEEKNUM(44563)"); // Mid-week date
model._set("A2", "=ISOWEEKNUM(44561)"); // Year-end date
// Key ISO week boundaries (just critical cases)
model._set("A3", "=ISOWEEKNUM(44197)"); // Jan 1, 2021 (Fri) -> Week 53 of 2020
model._set("A4", "=ISOWEEKNUM(44200)"); // Jan 4, 2021 (Mon) -> Week 1 of 2021
model._set("A5", "=ISOWEEKNUM(44564)"); // Jan 3, 2022 (Mon) -> Week 1 of 2022
// Error cases
model._set("A6", "=ISOWEEKNUM()"); // Wrong arg count
model._set("A7", "=ISOWEEKNUM(-1)"); // Invalid date
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"52");
assert_eq!(model._get_text("A2"), *"52");
// ISO week boundaries
assert_eq!(model._get_text("A3"), *"53"); // Week 53 of previous year
assert_eq!(model._get_text("A4"), *"1"); // Week 1 of current year
assert_eq!(model._get_text("A5"), *"1"); // Week 1 of next year
// Error cases
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#NUM!");
}

View File

@@ -1,33 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_datedif_yd_leap_year_edge_cases() {
let mut model = new_empty_model();
// 29 Feb 2020 → 28 Feb 2021 (should be 0 days)
model._set("A1", "=DATEDIF(\"29/2/2020\", \"28/2/2021\", \"YD\")");
// 29 Feb 2020 → 1 Mar 2021 (should be 1 day)
model._set("A2", "=DATEDIF(\"29/2/2020\", \"2021-03-01\", \"YD\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0");
assert_eq!(model._get_text("A2"), *"1");
}
#[test]
fn test_datedif_md_month_end_edge_cases() {
let mut model = new_empty_model();
// 31 Jan 2021 → 28 Feb 2021 (non-leap) => 28
model._set("B1", "=DATEDIF(\"31/1/2021\", \"28/2/2021\", \"MD\")");
// 31 Jan 2020 → 29 Feb 2020 (leap) => 29
model._set("B2", "=DATEDIF(\"31/1/2020\", \"29/2/2020\", \"MD\")");
model.evaluate();
assert_eq!(model._get_text("B1"), *"28");
assert_eq!(model._get_text("B2"), *"29");
}

View File

@@ -1,43 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_days360_month_end_us() {
let mut model = new_empty_model();
// 31 Jan 2021 -> 28 Feb 2021 (non-leap)
model._set("A1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28))");
// 31 Jan 2020 -> 28 Feb 2020 (leap year not last day of Feb)
model._set("A2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,28))");
// 28 Feb 2020 -> 31 Mar 2020 (leap year span crossing month ends)
model._set("A3", "=DAYS360(DATE(2020,2,28),DATE(2020,3,31))");
// 30 Apr 2021 -> 31 May 2021 (end-of-month adjustment rule)
model._set("A4", "=DAYS360(DATE(2021,4,30),DATE(2021,5,31))");
model.evaluate();
assert_eq!(model._get_text("A1"), *"30");
assert_eq!(model._get_text("A2"), *"28");
assert_eq!(model._get_text("A3"), *"33");
assert_eq!(model._get_text("A4"), *"30");
}
#[test]
fn test_days360_month_end_european() {
let mut model = new_empty_model();
// European basis = TRUE (or 1)
model._set("B1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28),TRUE)");
model._set("B2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,29),TRUE)");
model._set("B3", "=DAYS360(DATE(2021,8,31),DATE(2021,9,30),TRUE)");
model.evaluate();
assert_eq!(model._get_text("B1"), *"28");
assert_eq!(model._get_text("B2"), *"29");
assert_eq!(model._get_text("B3"), *"30");
}

View File

@@ -1,182 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::types::Cell;
// Helper to evaluate a formula and return the formatted text
fn eval_formula(formula: &str) -> String {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
model._get_text("A1")
}
// Helper that evaluates a formula and returns the raw value of A1 as a Result<f64, String>
fn eval_formula_raw_number(formula: &str) -> Result<f64, String> {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
match model._get_cell("A1") {
Cell::NumberCell { v, .. } => Ok(*v),
Cell::BooleanCell { v, .. } => Ok(if *v { 1.0 } else { 0.0 }),
Cell::ErrorCell { ei, .. } => Err(format!("{}", ei)),
_ => Err(model._get_text("A1")),
}
}
#[test]
fn test_datevalue_basic_numeric() {
// DATEVALUE should return the serial number representing the date, **not** a formatted date
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2/1/2023\")").unwrap(),
44958.0
);
}
#[test]
fn test_datevalue_mmdd_with_leading_zero() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"02/01/2023\")").unwrap(),
44958.0
); // 1-Feb-2023
}
#[test]
fn test_datevalue_iso() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023-01-02\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_month_name() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2-Jan-23\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_ambiguous_ddmm() {
// 01/02/2023 interpreted as MM/DD -> 2-Jan-2023
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"01/02/2023\")").unwrap(),
44929.0
);
}
#[test]
fn test_datevalue_ddmm_unambiguous() {
// 15/01/2023 should be 15-Jan-2023 since 15 cannot be month
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"15/01/2023\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_leap_day() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"29/02/2020\")").unwrap(),
43890.0
);
}
#[test]
fn test_datevalue_year_first_text_month() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023/Jan/15\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_mmdd_with_day_gt_12() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"6/15/2021\")").unwrap(),
44373.0
);
}
#[test]
fn test_datevalue_error_conditions() {
let cases = [
"=DATEVALUE(\"31/04/2023\")", // invalid day (Apr has 30 days)
"=DATEVALUE(\"13/13/2023\")", // invalid month
"=DATEVALUE(\"not a date\")", // non-date text
];
for formula in cases {
let result = eval_formula(formula);
assert_eq!(result, *"#VALUE!", "Expected #VALUE! for {}", formula);
}
}
// Helper to set and evaluate a single DATEDIF call
fn eval_datedif(unit: &str) -> String {
let mut model = new_empty_model();
let formula = format!("=DATEDIF(\"2020-01-01\", \"2021-06-15\", \"{}\")", unit);
model._set("A1", &formula);
model.evaluate();
model._get_text("A1")
}
#[test]
fn test_datedif_y() {
assert_eq!(eval_datedif("Y"), *"1");
}
#[test]
fn test_datedif_m() {
assert_eq!(eval_datedif("M"), *"17");
}
#[test]
fn test_datedif_d() {
assert_eq!(eval_datedif("D"), *"531");
}
#[test]
fn test_datedif_ym() {
assert_eq!(eval_datedif("YM"), *"5");
}
#[test]
fn test_datedif_yd() {
assert_eq!(eval_datedif("YD"), *"165");
}
#[test]
fn test_datedif_md() {
assert_eq!(eval_datedif("MD"), *"14");
}
#[test]
fn test_datedif_edge_and_error_cases() {
let mut model = new_empty_model();
// Leap-year spanning
model._set("A1", "=DATEDIF(\"28/2/2020\", \"1/3/2020\", \"D\")");
// End date before start date => #NUM!
model._set("A2", "=DATEDIF(\"1/2/2021\", \"1/1/2021\", \"D\")");
// Invalid unit => #VALUE!
model._set("A3", "=DATEDIF(\"1/1/2020\", \"1/1/2021\", \"Z\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"#VALUE!");
}
#[test]
fn test_datedif_mixed_case_unit() {
assert_eq!(eval_datedif("yD"), *"165"); // mixed-case should work
}
#[test]
fn test_datedif_error_propagation() {
// Invalid date in arguments should propagate #VALUE!
let mut model = new_empty_model();
model._set("A1", "=DATEDIF(\"bad\", \"bad\", \"Y\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
}

View File

@@ -174,7 +174,7 @@ fn fn_or_xor_no_arguments() {
println!("Testing function: {func}"); println!("Testing function: {func}");
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("A1", &format!("={func}()")); model._set("A1", &format!("={}()", func));
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A1"), *"#ERROR!");
} }

View File

@@ -1,15 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_round_approximation() {
let mut model = new_empty_model();
model._set("A1", "=ROUND(1.05*(0.0284+0.0046)-0.0284,4)");
model._set("A2", "=ROUNDDOWN(1.05*(0.0284+0.0046)-0.0284,5)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0.0063");
assert_eq!(model._get_text("A2"), *"0.00625");
}

View File

@@ -1,520 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
// Helper constants for common time values with detailed documentation
const MIDNIGHT: &str = "0"; // 00:00:00 = 0/24 = 0
const NOON: &str = "0.5"; // 12:00:00 = 12/24 = 0.5
const TIME_14_30: &str = "0.604166667"; // 14:30:00 = 14.5/24 ≈ 0.604166667
const TIME_14_30_45: &str = "0.6046875"; // 14:30:45 = 14.5125/24 = 0.6046875
const TIME_14_30_59: &str = "0.604849537"; // 14:30:59 (from floored fractional inputs)
const TIME_23_59_59: &str = "0.999988426"; // 23:59:59 = 23.99972.../24 ≈ 0.999988426
// Excel documentation test values with explanations
const TIME_2_24_AM: &str = "0.1"; // 2:24 AM = 2.4/24 = 0.1
const TIME_2_PM: &str = "0.583333333"; // 2:00 PM = 14/24 ≈ 0.583333333
const TIME_6_45_PM: &str = "0.78125"; // 6:45 PM = 18.75/24 = 0.78125
const TIME_6_35_AM: &str = "0.274305556"; // 6:35 AM = 6.583333.../24 ≈ 0.274305556
const TIME_2_30_AM: &str = "0.104166667"; // 2:30 AM = 2.5/24 ≈ 0.104166667
const TIME_1_AM: &str = "0.041666667"; // 1:00 AM = 1/24 ≈ 0.041666667
const TIME_9_PM: &str = "0.875"; // 9:00 PM = 21/24 = 0.875
const TIME_2_AM: &str = "0.083333333"; // 2:00 AM = 2/24 ≈ 0.083333333
// Additional helper: 1-second past midnight (00:00:01)
const TIME_00_00_01: &str = "0.000011574"; // 1 second = 1/86400 ≈ 0.000011574
/// Helper function to set up and evaluate a model with time expressions
fn test_time_expressions(expressions: &[(&str, &str)]) -> crate::model::Model {
let mut model = new_empty_model();
for (cell, formula) in expressions {
model._set(cell, formula);
}
model.evaluate();
model
}
/// Helper function to test component extraction for a given time value
/// Returns (hour, minute, second) as strings
fn test_component_extraction(time_value: &str) -> (String, String, String) {
let model = test_time_expressions(&[
("A1", &format!("=HOUR({time_value})")),
("B1", &format!("=MINUTE({time_value})")),
("C1", &format!("=SECOND({time_value})")),
]);
(
model._get_text("A1").to_string(),
model._get_text("B1").to_string(),
model._get_text("C1").to_string(),
)
}
#[test]
fn test_excel_timevalue_compatibility() {
// Test cases based on Excel's official documentation and examples
let model = test_time_expressions(&[
// Excel documentation examples
("A1", "=TIMEVALUE(\"2:24 AM\")"), // Should be 0.1
("A2", "=TIMEVALUE(\"2 PM\")"), // Should be 0.583333... (14/24)
("A3", "=TIMEVALUE(\"6:45 PM\")"), // Should be 0.78125 (18.75/24)
("A4", "=TIMEVALUE(\"18:45\")"), // Same as above, 24-hour format
// Date-time format (date should be ignored)
("B1", "=TIMEVALUE(\"22-Aug-2011 6:35 AM\")"), // Should be ~0.2743
("B2", "=TIMEVALUE(\"2023-01-01 14:30:00\")"), // Should be 0.604166667
// Edge cases that Excel should support
("C1", "=TIMEVALUE(\"12:00 AM\")"), // Midnight: 0
("C2", "=TIMEVALUE(\"12:00 PM\")"), // Noon: 0.5
("C3", "=TIMEVALUE(\"11:59:59 PM\")"), // Almost midnight: 0.999988426
// Single digit variations
("D1", "=TIMEVALUE(\"1 AM\")"), // 1:00 AM
("D2", "=TIMEVALUE(\"9 PM\")"), // 9:00 PM
("D3", "=TIMEVALUE(\"12 AM\")"), // Midnight
("D4", "=TIMEVALUE(\"12 PM\")"), // Noon
]);
// Excel documentation examples - verify exact values
assert_eq!(model._get_text("A1"), *TIME_2_24_AM); // 2:24 AM
assert_eq!(model._get_text("A2"), *TIME_2_PM); // 2 PM = 14:00
assert_eq!(model._get_text("A3"), *TIME_6_45_PM); // 6:45 PM = 18:45
assert_eq!(model._get_text("A4"), *TIME_6_45_PM); // 18:45 (24-hour)
// Date-time formats (date ignored, extract time only)
assert_eq!(model._get_text("B1"), *TIME_6_35_AM); // 6:35 AM ≈ 0.2743
assert_eq!(model._get_text("B2"), *TIME_14_30); // 14:30:00
// Edge cases
assert_eq!(model._get_text("C1"), *MIDNIGHT); // 12:00 AM = 00:00
assert_eq!(model._get_text("C2"), *NOON); // 12:00 PM = 12:00
assert_eq!(model._get_text("C3"), *TIME_23_59_59); // 11:59:59 PM
// Single digit hours
assert_eq!(model._get_text("D1"), *TIME_1_AM); // 1:00 AM
assert_eq!(model._get_text("D2"), *TIME_9_PM); // 9:00 PM = 21:00
assert_eq!(model._get_text("D3"), *MIDNIGHT); // 12 AM = 00:00
assert_eq!(model._get_text("D4"), *NOON); // 12 PM = 12:00
}
#[test]
fn test_time_function_basic_cases() {
let model = test_time_expressions(&[
("A1", "=TIME(0,0,0)"), // Midnight
("A2", "=TIME(12,0,0)"), // Noon
("A3", "=TIME(14,30,0)"), // 2:30 PM
("A4", "=TIME(23,59,59)"), // Max time
]);
assert_eq!(model._get_text("A1"), *MIDNIGHT);
assert_eq!(model._get_text("A2"), *NOON);
assert_eq!(model._get_text("A3"), *TIME_14_30);
assert_eq!(model._get_text("A4"), *TIME_23_59_59);
}
#[test]
fn test_time_function_normalization() {
let model = test_time_expressions(&[
("A1", "=TIME(25,0,0)"), // Hours > 24 wrap around
("A2", "=TIME(48,0,0)"), // 48 hours = 0 (2 full days)
("A3", "=TIME(0,90,0)"), // 90 minutes = 1.5 hours
("A4", "=TIME(0,0,90)"), // 90 seconds = 1.5 minutes
("A5", "=TIME(14.9,30.9,59.9)"), // Fractional inputs floored to 14:30:59
]);
assert_eq!(model._get_text("A1"), *TIME_1_AM); // 1:00:00
assert_eq!(model._get_text("A2"), *MIDNIGHT); // 0:00:00
assert_eq!(model._get_text("A3"), *"0.0625"); // 1:30:00
assert_eq!(model._get_text("A4"), *"0.001041667"); // 0:01:30
assert_eq!(model._get_text("A5"), *TIME_14_30_59); // 14:30:59 (floored)
}
#[test]
fn test_time_function_precision_edge_cases() {
let model = test_time_expressions(&[
// High precision fractional seconds
("A1", "=TIME(14,30,45.999)"), // Fractional seconds should be floored
("A2", "=SECOND(TIME(14,30,45.999))"), // Should extract 45, not 46
// Very large normalization values
("B1", "=TIME(999,999,999)"), // Extreme normalization test
("B2", "=HOUR(999.5)"), // Multiple days, extract hour from fractional part
("B3", "=MINUTE(999.75)"), // Multiple days, extract minute
// Boundary conditions at rollover points
("C1", "=TIME(24,60,60)"), // Should normalize to next day (00:01:00)
("C2", "=HOUR(0.999999999)"), // Almost 24 hours should be 23
("C3", "=MINUTE(0.999999999)"), // Almost 24 hours, extract minutes
("C4", "=SECOND(0.999999999)"), // Almost 24 hours, extract seconds
// Precision at boundaries
("D1", "=TIME(23,59,59.999)"), // Very close to midnight
("D2", "=TIME(0,0,0.001)"), // Just after midnight
]);
// Fractional seconds are floored
assert_eq!(model._get_text("A2"), *"45"); // 45.999 floored to 45
// Multiple days should work with rem_euclid
assert_eq!(model._get_text("B2"), *"12"); // 999.5 days, hour = 12 (noon)
// Boundary normalization
assert_eq!(model._get_text("C1"), *"0.042361111"); // 24:60:60 = 01:01:00 (normalized)
assert_eq!(model._get_text("C2"), *"23"); // Almost 24 hours = 23:xx:xx
// High precision should be handled correctly
let result_d1 = model._get_text("D1").parse::<f64>().unwrap();
assert!(result_d1 < 1.0 && result_d1 > 0.999); // Very close to but less than 1.0
}
#[test]
fn test_time_function_errors() {
let model = test_time_expressions(&[
("A1", "=TIME()"), // Wrong arg count
("A2", "=TIME(12)"), // Wrong arg count
("A3", "=TIME(12,30,0,0)"), // Wrong arg count
("B1", "=TIME(-1,0,0)"), // Negative hour
("B2", "=TIME(0,-1,0)"), // Negative minute
("B3", "=TIME(0,0,-1)"), // Negative second
]);
// Wrong argument count
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
// Negative values should return #NUM! error
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn test_timevalue_function_formats() {
let model = test_time_expressions(&[
// Basic formats
("A1", "=TIMEVALUE(\"14:30\")"),
("A2", "=TIMEVALUE(\"14:30:45\")"),
("A3", "=TIMEVALUE(\"00:00:00\")"),
// AM/PM formats
("B1", "=TIMEVALUE(\"2:30 PM\")"),
("B2", "=TIMEVALUE(\"2:30 AM\")"),
("B3", "=TIMEVALUE(\"12:00 PM\")"), // Noon
("B4", "=TIMEVALUE(\"12:00 AM\")"), // Midnight
// Single hour with AM/PM (now supported!)
("B5", "=TIMEVALUE(\"2 PM\")"),
("B6", "=TIMEVALUE(\"2 AM\")"),
// Date-time formats (extract time only)
("C1", "=TIMEVALUE(\"2023-01-01 14:30:00\")"),
("C2", "=TIMEVALUE(\"2023-01-01T14:30:00\")"),
// Whitespace handling
("D1", "=TIMEVALUE(\" 14:30 \")"),
]);
// Basic formats
assert_eq!(model._get_text("A1"), *TIME_14_30);
assert_eq!(model._get_text("A2"), *TIME_14_30_45);
assert_eq!(model._get_text("A3"), *MIDNIGHT);
// AM/PM formats
assert_eq!(model._get_text("B1"), *TIME_14_30); // 2:30 PM = 14:30
assert_eq!(model._get_text("B2"), *TIME_2_30_AM); // 2:30 AM
assert_eq!(model._get_text("B3"), *NOON); // 12:00 PM = noon
assert_eq!(model._get_text("B4"), *MIDNIGHT); // 12:00 AM = midnight
// Single hour AM/PM formats (now supported!)
assert_eq!(model._get_text("B5"), *TIME_2_PM); // 2 PM = 14:00
assert_eq!(model._get_text("B6"), *TIME_2_AM); // 2 AM = 02:00
// Date-time formats
assert_eq!(model._get_text("C1"), *TIME_14_30);
assert_eq!(model._get_text("C2"), *TIME_14_30);
// Whitespace
assert_eq!(model._get_text("D1"), *TIME_14_30);
}
#[test]
fn test_timevalue_function_errors() {
let model = test_time_expressions(&[
("A1", "=TIMEVALUE()"), // Wrong arg count
("A2", "=TIMEVALUE(\"14:30\", \"x\")"), // Wrong arg count
("B1", "=TIMEVALUE(\"invalid\")"), // Invalid format
("B2", "=TIMEVALUE(\"25:00\")"), // Invalid hour
("B3", "=TIMEVALUE(\"14:70\")"), // Invalid minute
("B4", "=TIMEVALUE(\"\")"), // Empty string
("B5", "=TIMEVALUE(\"2PM\")"), // Missing space (still unsupported)
]);
// Wrong argument count should return #ERROR!
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
// Invalid formats should return #VALUE!
assert_eq!(model._get_text("B1"), *"#VALUE!");
assert_eq!(model._get_text("B2"), *"#VALUE!");
assert_eq!(model._get_text("B3"), *"#VALUE!");
assert_eq!(model._get_text("B4"), *"#VALUE!");
assert_eq!(model._get_text("B5"), *"#VALUE!"); // "2PM" no space - not supported
}
#[test]
fn test_time_component_extraction_comprehensive() {
// Test component extraction using helper function for consistency
// Test basic time values
let test_cases = [
(MIDNIGHT, ("0", "0", "0")), // 00:00:00
(NOON, ("12", "0", "0")), // 12:00:00
(TIME_14_30, ("14", "30", "0")), // 14:30:00
(TIME_23_59_59, ("23", "59", "59")), // 23:59:59
];
for (time_value, expected) in test_cases {
let (hour, minute, second) = test_component_extraction(time_value);
assert_eq!(hour, expected.0, "Hour mismatch for {time_value}");
assert_eq!(minute, expected.1, "Minute mismatch for {time_value}");
assert_eq!(second, expected.2, "Second mismatch for {time_value}");
}
// Test multiple days (extract from fractional part)
let (hour, minute, second) = test_component_extraction("1.5"); // Day 2, 12:00
assert_eq!(
(hour, minute, second),
("12".to_string(), "0".to_string(), "0".to_string())
);
let (hour, minute, second) = test_component_extraction("100.604166667"); // Day 101, 14:30
assert_eq!(
(hour, minute, second),
("14".to_string(), "30".to_string(), "0".to_string())
);
// Test precision at boundaries
let (hour, _, _) = test_component_extraction("0.041666666"); // Just under 1:00 AM
assert_eq!(hour, "0");
let (hour, _, _) = test_component_extraction("0.041666667"); // Exactly 1:00 AM
assert_eq!(hour, "1");
let (hour, _, _) = test_component_extraction("0.041666668"); // Just over 1:00 AM
assert_eq!(hour, "1");
// Test very large day values
let (hour, minute, second) = test_component_extraction("1000000.25"); // Million days + 6 hours
assert_eq!(
(hour, minute, second),
("6".to_string(), "0".to_string(), "0".to_string())
);
}
#[test]
fn test_time_component_function_errors() {
let model = test_time_expressions(&[
// Wrong argument counts
("A1", "=HOUR()"), // No arguments
("A2", "=MINUTE()"), // No arguments
("A3", "=SECOND()"), // No arguments
("A4", "=HOUR(1, 2)"), // Too many arguments
("A5", "=MINUTE(1, 2)"), // Too many arguments
("A6", "=SECOND(1, 2)"), // Too many arguments
// Negative values should return #NUM!
("B1", "=HOUR(-0.5)"), // Negative value
("B2", "=MINUTE(-1)"), // Negative value
("B3", "=SECOND(-1)"), // Negative value
("B4", "=HOUR(-0.000001)"), // Slightly negative
("B5", "=MINUTE(-0.000001)"), // Slightly negative
("B6", "=SECOND(-0.000001)"), // Slightly negative
]);
// Wrong argument count should return #ERROR!
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
// Negative values should return #NUM!
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
assert_eq!(model._get_text("B4"), *"#NUM!");
assert_eq!(model._get_text("B5"), *"#NUM!");
assert_eq!(model._get_text("B6"), *"#NUM!");
}
#[test]
fn test_time_functions_integration() {
// Test how TIME, TIMEVALUE and component extraction functions work together
let model = test_time_expressions(&[
// Create times with both functions
("A1", "=TIME(14,30,45)"),
("A2", "=TIMEVALUE(\"14:30:45\")"),
// Extract components from TIME function results
("B1", "=HOUR(A1)"),
("B2", "=MINUTE(A1)"),
("B3", "=SECOND(A1)"),
// Extract components from TIMEVALUE function results
("C1", "=HOUR(A2)"),
("C2", "=MINUTE(A2)"),
("C3", "=SECOND(A2)"),
// Test additional TIME variations
("D1", "=TIME(14,0,0)"), // 14:00:00
("E1", "=HOUR(D1)"), // Extract hour from 14:00:00
("E2", "=MINUTE(D1)"), // Extract minute from 14:00:00
("E3", "=SECOND(D1)"), // Extract second from 14:00:00
]);
// TIME and TIMEVALUE should produce equivalent results
assert_eq!(model._get_text("A1"), model._get_text("A2"));
// Extracting components should work consistently
assert_eq!(model._get_text("B1"), *"14");
assert_eq!(model._get_text("B2"), *"30");
assert_eq!(model._get_text("B3"), *"45");
assert_eq!(model._get_text("C1"), *"14");
assert_eq!(model._get_text("C2"), *"30");
assert_eq!(model._get_text("C3"), *"45");
// Components from TIME(14,0,0)
assert_eq!(model._get_text("E1"), *"14");
assert_eq!(model._get_text("E2"), *"0");
assert_eq!(model._get_text("E3"), *"0");
}
#[test]
fn test_time_function_extreme_values() {
// Test missing edge cases: very large fractional inputs
let model = test_time_expressions(&[
// Extremely large fractional values to TIME function
("A1", "=TIME(999999.9, 999999.9, 999999.9)"), // Very large fractional inputs
("A2", "=TIME(1e6, 1e6, 1e6)"), // Scientific notation inputs
("A3", "=TIME(0.000001, 0.000001, 0.000001)"), // Very small fractional inputs
// Large day values for component extraction (stress test)
("B1", "=HOUR(999999.999)"), // Almost a million days
("B2", "=MINUTE(999999.999)"),
("B3", "=SECOND(999999.999)"),
// Edge case: exactly 1.0 (should be midnight of next day)
("C1", "=HOUR(1.0)"),
("C2", "=MINUTE(1.0)"),
("C3", "=SECOND(1.0)"),
// Very high precision values
("D1", "=HOUR(0.999999999999)"), // Almost exactly 24:00:00
("D2", "=MINUTE(0.999999999999)"),
("D3", "=SECOND(0.999999999999)"),
]);
// Large fractional inputs should be floored and normalized
let result_a1 = model._get_text("A1").parse::<f64>().unwrap();
assert!(
(0.0..1.0).contains(&result_a1),
"Result should be valid time fraction"
);
// Component extraction should work with very large values
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_b1), "Hour should be 0-23");
// Exactly 1.0 should be midnight (start of next day)
assert_eq!(model._get_text("C1"), *"0");
assert_eq!(model._get_text("C2"), *"0");
assert_eq!(model._get_text("C3"), *"0");
// Very high precision should still extract valid components
let hour_d1 = model._get_text("D1").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_d1), "Hour should be 0-23");
}
#[test]
fn test_timevalue_malformed_but_parseable() {
// Test missing edge case: malformed but potentially parseable strings
let model = test_time_expressions(&[
// Test various malformed but potentially parseable time strings
("A1", "=TIMEVALUE(\"14:30:00.123\")"), // Milliseconds (might be truncated)
("A2", "=TIMEVALUE(\"14:30:00.999\")"), // High precision milliseconds
("A3", "=TIMEVALUE(\"02:30:00\")"), // Leading zero hours
("A4", "=TIMEVALUE(\"2:05:00\")"), // Single digit hour, zero-padded minute
// Boundary cases for AM/PM parsing
("B1", "=TIMEVALUE(\"11:59:59 PM\")"), // Just before midnight
("B2", "=TIMEVALUE(\"12:00:01 AM\")"), // Just after midnight
("B3", "=TIMEVALUE(\"12:00:01 PM\")"), // Just after noon
("B4", "=TIMEVALUE(\"11:59:59 AM\")"), // Just before noon
// Test various date-time combinations
("C1", "=TIMEVALUE(\"2023-12-31T23:59:59\")"), // ISO format at year end
("C2", "=TIMEVALUE(\"2023-01-01 00:00:01\")"), // New year, just after midnight
// Test potential edge cases that might still be parseable
("D1", "=TIMEVALUE(\"24:00:00\")"), // Should error (invalid hour)
("D2", "=TIMEVALUE(\"23:60:00\")"), // Should error (invalid minute)
("D3", "=TIMEVALUE(\"23:59:60\")"), // Should error (invalid second)
]);
// Milliseconds are not supported, should return a #VALUE! error like Excel
assert_eq!(model._get_text("A1"), *"#VALUE!");
assert_eq!(model._get_text("A2"), *"#VALUE!");
// Leading zeros should work fine
assert_eq!(model._get_text("A3"), *TIME_2_30_AM); // 02:30:00 should parse as 2:30:00
// AM/PM boundary cases should work
let result_b1 = model._get_text("B1").parse::<f64>().unwrap();
assert!(
result_b1 > 0.99 && result_b1 < 1.0,
"11:59:59 PM should be very close to 1.0"
);
let result_b2 = model._get_text("B2").parse::<f64>().unwrap();
assert!(
result_b2 > 0.0 && result_b2 < 0.01,
"12:00:01 AM should be very close to 0.0"
);
// ISO 8601 format with "T" separator should be parsed correctly
assert_eq!(model._get_text("C1"), *TIME_23_59_59); // 23:59:59 → almost midnight
assert_eq!(model._get_text("C2"), *TIME_00_00_01); // 00:00:01 → one second past midnight
// Time parser normalizes edge cases to midnight (Excel compatibility)
assert_eq!(model._get_text("D1"), *"0"); // 24:00:00 = midnight of next day
assert_eq!(model._get_text("D2"), *"0"); // 23:60:00 normalizes to 24:00:00 = midnight
assert_eq!(model._get_text("D3"), *"0"); // 23:59:60 normalizes to 24:00:00 = midnight
}
#[test]
fn test_performance_stress_with_extreme_values() {
// Test performance/stress cases with extreme values
let model = test_time_expressions(&[
// Very large numbers that should still work
("A1", "=TIME(2147483647, 0, 0)"), // Max i32 hours
("A2", "=TIME(0, 2147483647, 0)"), // Max i32 minutes
("A3", "=TIME(0, 0, 2147483647)"), // Max i32 seconds
// Component extraction with extreme day values
("B1", "=HOUR(1e15)"), // Very large day number
("B2", "=MINUTE(1e15)"),
("B3", "=SECOND(1e15)"),
// Edge of floating point precision
("C1", "=HOUR(1.7976931348623157e+308)"), // Near max f64
("C2", "=HOUR(2.2250738585072014e-308)"), // Near min positive f64
// Multiple TIME function calls with large values
("D1", "=TIME(1000000, 1000000, 1000000)"), // Large normalized values
("D2", "=HOUR(D1)"), // Extract from large TIME result
("D3", "=MINUTE(D1)"),
("D4", "=SECOND(D1)"),
]);
// All results should be valid (not errors) even with extreme inputs
for cell in ["A1", "A2", "A3", "B1", "B2", "B3", "D1", "D2", "D3", "D4"] {
let result = model._get_text(cell);
assert!(
result != *"#ERROR!" && result != *"#NUM!" && result != *"#VALUE!",
"Cell {cell} should not error with extreme values: {result}",
);
}
// Results should be mathematically valid
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
let minute_b2 = model._get_text("B2").parse::<i32>().unwrap();
let second_b3 = model._get_text("B3").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_b1));
assert!((0..=59).contains(&minute_b2));
assert!((0..=59).contains(&second_b3));
// TIME function results should be valid time fractions
let time_d1 = model._get_text("D1").parse::<f64>().unwrap();
assert!(
(0.0..1.0).contains(&time_d1),
"TIME result should be valid fraction"
);
}

View File

@@ -1,17 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=LN(100)");
model._set("A2", "=LN()");
model._set("A3", "=LN(100, 10)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"4.605170186");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
}

View File

@@ -1,19 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=LOG(100)");
model._set("A2", "=LOG()");
model._set("A3", "=LOG(10000, 10)");
model._set("A4", "=LOG(100, 10, 1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"4");
assert_eq!(model._get_text("A4"), *"#ERROR!");
}

View File

@@ -1,35 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=LOG10(100)");
model._set("A2", "=LOG10()");
model._set("A3", "=LOG10(100, 10)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
}
#[test]
fn cell_and_function() {
let mut model = new_empty_model();
model._set("A1", "=LOG10");
model.evaluate();
// This is the cell LOG10
assert_eq!(model._get_text("A1"), *"0");
model._set("LOG10", "1000");
model._set("A2", "=LOG10(LOG10)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"1000");
assert_eq!(model._get_text("A2"), *"3");
}

View File

@@ -1,347 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
// Test data: Jan 1-10, 2023 week
const JAN_1_2023: i32 = 44927; // Sunday
const JAN_2_2023: i32 = 44928; // Monday
const JAN_6_2023: i32 = 44932; // Friday
const JAN_9_2023: i32 = 44935; // Monday
const JAN_10_2023: i32 = 44936; // Tuesday
#[test]
fn networkdays_calculates_weekdays_excluding_weekends() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Should count 7 weekdays in 10-day span"
);
}
#[test]
fn networkdays_handles_reverse_date_order() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_10_2023},{JAN_1_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"-7",
"Reversed dates should return negative count"
);
}
#[test]
fn networkdays_excludes_holidays_from_weekdays() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{JAN_9_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"6",
"Should exclude Monday holiday from 7 weekdays"
);
}
#[test]
fn networkdays_handles_same_start_end_date() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_9_2023},{JAN_9_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"1",
"Same weekday date should count as 1 workday"
);
}
#[test]
fn networkdays_accepts_holiday_ranges() {
let mut model = new_empty_model();
model._set("B1", &JAN_2_2023.to_string());
model._set("B2", &JAN_6_2023.to_string());
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B2)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"5",
"Should exclude 2 holidays from 7 weekdays"
);
}
#[test]
fn networkdays_intl_uses_standard_weekend_by_default() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Default should be Saturday-Sunday weekend"
);
}
#[test]
fn networkdays_intl_supports_numeric_weekend_patterns() {
let mut model = new_empty_model();
// Pattern 2 = Sunday-Monday weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},2)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"6",
"Sunday-Monday weekend should give 6 workdays"
);
}
#[test]
fn networkdays_intl_supports_single_day_weekends() {
let mut model = new_empty_model();
// Pattern 11 = Sunday only weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},11)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"8",
"Sunday-only weekend should give 8 workdays"
);
}
#[test]
fn networkdays_intl_supports_string_weekend_patterns() {
let mut model = new_empty_model();
// "0000110" = Friday-Saturday weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\")"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"8",
"Friday-Saturday weekend should give 8 workdays"
);
}
#[test]
fn networkdays_intl_no_weekends_counts_all_days() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000000\")"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"10",
"No weekends should count all 10 days"
);
}
#[test]
fn networkdays_intl_combines_custom_weekends_with_holidays() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\",{JAN_9_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Should exclude both weekend and holiday"
);
}
#[test]
fn networkdays_validates_argument_count() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS()");
model._set("A2", "=NETWORKDAYS(1,2,3,4)");
model._set("A3", "=NETWORKDAYS.INTL()");
model._set("A4", "=NETWORKDAYS.INTL(1,2,3,4,5)");
model.evaluate();
assert_eq!(model._get_text("A1"), "#ERROR!");
assert_eq!(model._get_text("A2"), "#ERROR!");
assert_eq!(model._get_text("A3"), "#ERROR!");
assert_eq!(model._get_text("A4"), "#ERROR!");
}
#[test]
fn networkdays_rejects_invalid_dates() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(-1,100)");
model._set("A2", "=NETWORKDAYS(1,3000000)");
model._set("A3", "=NETWORKDAYS(\"text\",100)");
model.evaluate();
assert_eq!(model._get_text("A1"), "#NUM!");
assert_eq!(model._get_text("A2"), "#NUM!");
assert_eq!(model._get_text("A3"), "#VALUE!");
}
#[test]
fn networkdays_intl_rejects_invalid_weekend_patterns() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS.INTL(1,10,99)");
model._set("A2", "=NETWORKDAYS.INTL(1,10,\"111110\")");
model._set("A3", "=NETWORKDAYS.INTL(1,10,\"11111000\")");
model._set("A4", "=NETWORKDAYS.INTL(1,10,\"1111102\")");
model.evaluate();
assert_eq!(model._get_text("A1"), "#NUM!");
assert_eq!(model._get_text("A2"), "#VALUE!");
assert_eq!(model._get_text("A3"), "#VALUE!");
assert_eq!(model._get_text("A4"), "#VALUE!");
}
#[test]
fn networkdays_rejects_invalid_holidays() {
let mut model = new_empty_model();
model._set("B1", "invalid");
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1)"),
);
model._set(
"A2",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},-1)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"#VALUE!",
"Should reject non-numeric holidays"
);
assert_eq!(
model._get_text("A2"),
"#NUM!",
"Should reject out-of-range holidays"
);
}
#[test]
fn networkdays_handles_weekend_only_periods() {
let mut model = new_empty_model();
let saturday = JAN_1_2023 - 1;
model._set("A1", &format!("=NETWORKDAYS({saturday},{JAN_1_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"0",
"Weekend-only period should count 0 workdays"
);
}
#[test]
fn networkdays_ignores_holidays_outside_date_range() {
let mut model = new_empty_model();
let future_holiday = JAN_10_2023 + 100;
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{future_holiday})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Out-of-range holidays should be ignored"
);
}
#[test]
fn networkdays_handles_empty_holiday_ranges() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B3)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Empty holiday range should be treated as no holidays"
);
}
#[test]
fn networkdays_handles_minimum_valid_dates() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(1,7)");
model.evaluate();
assert_eq!(
model._get_text("A1"),
"5",
"Should handle earliest Excel dates correctly"
);
}
#[test]
fn networkdays_handles_large_date_ranges_efficiently() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(1,365)");
model.evaluate();
assert!(
!model._get_text("A1").starts_with('#'),
"Large ranges should not error"
);
}

View File

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

View File

@@ -1,26 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_weekday_return_types_11_to_17() {
let mut model = new_empty_model();
// Test date: 44561 corresponds to a Friday (2021-12-31). We verify the
// numeric result for each custom week start defined by return_type 11-17.
model._set("A1", "=WEEKDAY(44561,11)"); // Monday start
model._set("A2", "=WEEKDAY(44561,12)"); // Tuesday start
model._set("A3", "=WEEKDAY(44561,13)"); // Wednesday start
model._set("A4", "=WEEKDAY(44561,14)"); // Thursday start
model._set("A5", "=WEEKDAY(44561,15)"); // Friday start
model._set("A6", "=WEEKDAY(44561,16)"); // Saturday start
model._set("A7", "=WEEKDAY(44561,17)"); // Sunday start
model.evaluate();
assert_eq!(model._get_text("A1"), *"5"); // Mon=1 .. Sun=7 ⇒ Fri=5
assert_eq!(model._get_text("A2"), *"4"); // Tue start ⇒ Fri=4
assert_eq!(model._get_text("A3"), *"3"); // Wed start ⇒ Fri=3
assert_eq!(model._get_text("A4"), *"2"); // Thu start ⇒ Fri=2
assert_eq!(model._get_text("A5"), *"1"); // Fri start ⇒ Fri=1
assert_eq!(model._get_text("A6"), *"7"); // Sat start ⇒ Fri=7
assert_eq!(model._get_text("A7"), *"6"); // Sun start ⇒ Fri=6
}

View File

@@ -1,31 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_weeknum_return_types_11_to_17_and_21() {
let mut model = new_empty_model();
// Date 44561 -> 2021-12-31 (Friday). Previously verified as week 53 (Sunday/Monday start).
// We verify that custom week-start codes 11-17 all map to week 53 and ISO variant (21) maps to 52.
let formulas = [
("A1", "=WEEKNUM(44561,11)"),
("A2", "=WEEKNUM(44561,12)"),
("A3", "=WEEKNUM(44561,13)"),
("A4", "=WEEKNUM(44561,14)"),
("A5", "=WEEKNUM(44561,15)"),
("A6", "=WEEKNUM(44561,16)"),
("A7", "=WEEKNUM(44561,17)"),
("A8", "=WEEKNUM(44561,21)"), // ISO week numbering
];
for (cell, formula) in formulas {
model._set(cell, formula);
}
model.evaluate();
// All 11-17 variations should yield 53
for cell in ["A1", "A2", "A3", "A4", "A5", "A6", "A7"] {
assert_eq!(model._get_text(cell), *"53", "{cell} should be 53");
}
// ISO week (return_type 21)
assert_eq!(model._get_text("A8"), *"52");
}

View File

@@ -1,60 +0,0 @@
#![allow(clippy::panic)]
use crate::{cell::CellValue, test::util::new_empty_model};
#[test]
fn test_yearfrac_basis_2_actual_360() {
let mut model = new_empty_model();
// Non-leap span of exactly 360 days should result in 1.0
model._set("A1", "=YEARFRAC(44561,44921,2)");
// Leap-year span of 366 days: Jan 1 2020 → Jan 1 2021
model._set("A2", "=YEARFRAC(43831,44197,2)");
// Reverse order should yield negative value
model._set("A3", "=YEARFRAC(44921,44561,2)");
model.evaluate();
// 360/360
assert_eq!(model._get_text("A1"), *"1");
// 366/360 ≈ 1.0166666667 (tolerance 1e-10)
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!A2") {
assert!((v - 1.016_666_666_7).abs() < 1e-10);
} else {
panic!("Expected numeric value in A2");
}
// Negative symmetric of A1
assert_eq!(model._get_text("A3"), *"-1");
}
#[test]
fn test_yearfrac_basis_3_actual_365() {
let mut model = new_empty_model();
// Non-leap span of exactly 365 days should result in 1.0
model._set("B1", "=YEARFRAC(44561,44926,3)");
// Leap-year span of 366 days
model._set("B2", "=YEARFRAC(43831,44197,3)");
// Same date should be 0
model._set("B3", "=YEARFRAC(44561,44561,3)");
model.evaluate();
// 365/365
assert_eq!(model._get_text("B1"), *"1");
// 366/365 ≈ 1.002739726 (tolerance 1e-10)
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B2") {
assert!((v - 1.002_739_726).abs() < 1e-10);
} else {
panic!("Expected numeric value in B2");
}
// Same date
assert_eq!(model._get_text("B3"), *"0");
}

View File

@@ -1,7 +1,6 @@
mod test_add_delete_sheets; mod test_add_delete_sheets;
mod test_autofill_columns; mod test_autofill_columns;
mod test_autofill_rows; mod test_autofill_rows;
mod test_batch_row_column_diff;
mod test_border; mod test_border;
mod test_clear_cells; mod test_clear_cells;
mod test_column_style; mod test_column_style;
@@ -12,8 +11,6 @@ mod test_evaluation;
mod test_general; mod test_general;
mod test_grid_lines; mod test_grid_lines;
mod test_keyboard_navigation; mod test_keyboard_navigation;
mod test_last_empty_cell;
mod test_multi_row_column;
mod test_on_area_selection; mod test_on_area_selection;
mod test_on_expand_selected_range; mod test_on_expand_selected_range;
mod test_on_paste_styles; mod test_on_paste_styles;

View File

@@ -1,675 +0,0 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use bitcode::decode;
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
user_model::history::{Diff, QueueDiffs},
UserModel,
};
fn last_diff_list(model: &mut UserModel) -> Vec<Diff> {
let bytes = model.flush_send_queue();
let queue: Vec<QueueDiffs> = decode(&bytes).unwrap();
// Get the last operation's diff list
queue.last().unwrap().list.clone()
}
#[test]
fn diff_invariant_insert_rows() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
assert!(model.insert_rows(0, 5, 3).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertRows {
sheet: 0,
row: 5,
count: 3
}
));
}
#[test]
fn diff_invariant_insert_columns() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
assert!(model.insert_columns(0, 2, 4).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertColumns {
sheet: 0,
column: 2,
count: 4
}
));
}
#[test]
fn undo_redo_after_batch_delete() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Place values that will shift.
model.set_user_input(0, 20, 1, "A").unwrap();
model.set_user_input(0, 1, 20, "B").unwrap();
// Fill some of the rows we are about to delete for testing
for r in 10..15 {
model.set_user_input(0, r, 1, "tmp").unwrap();
}
// Delete rows 10..14 and columns 5..8
assert!(model.delete_rows(0, 10, 5).is_ok());
assert!(model.delete_columns(0, 5, 4).is_ok());
// Verify shift
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
// Undo
model.undo().unwrap(); // columns
model.undo().unwrap(); // rows
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "A");
assert_eq!(model.get_formatted_cell_value(0, 1, 20).unwrap(), "B");
// Redo
model.redo().unwrap(); // rows
model.redo().unwrap(); // columns
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "A");
assert_eq!(model.get_formatted_cell_value(0, 1, 16).unwrap(), "B");
}
#[test]
fn diff_order_delete_rows() {
// Verifies that delete diffs are generated with all data preserved
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Populate rows to delete
for r in 5..10 {
model.set_user_input(0, r, 1, &r.to_string()).unwrap();
}
assert!(model.delete_rows(0, 5, 5).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
// Should have one bulk diff with all the row data
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 5);
assert_eq!(*count, 5);
assert_eq!(old_data.len(), 5);
// Verify the data was collected for each row
for (i, row_data) in old_data.iter().enumerate() {
let _expected_value = (5 + i).to_string();
assert!(row_data.data.contains_key(&1));
}
}
_ => panic!("Unexpected diff variant"),
}
}
#[test]
fn batch_operations_with_formulas() {
// Verifies formulas update correctly after batch ops
let base = new_empty_model();
let mut model = UserModel::from_model(base);
model.set_user_input(0, 1, 1, "10").unwrap();
model.set_user_input(0, 5, 1, "=A1*2").unwrap(); // Will become A3 after insert
assert!(model.insert_rows(0, 2, 2).is_ok());
// Formula should now reference A1 (unchanged) but be in row 7
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "20");
assert_eq!(model.get_cell_content(0, 7, 1).unwrap(), "=A1*2");
// Undo and verify formula is back at original position
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "20");
}
#[test]
fn edge_case_single_operation() {
// Single row/column operations should still work correctly
let base = new_empty_model();
let mut model = UserModel::from_model(base);
assert!(model.insert_rows(0, 1, 1).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(model.insert_columns(0, 1, 1).is_ok());
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
}
#[test]
fn delete_empty_rows() {
// Delete multiple empty rows and verify behavior
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set data in rows 1 and 10, leaving rows 5-8 empty
model.set_user_input(0, 1, 1, "Before").unwrap();
model.set_user_input(0, 10, 1, "After").unwrap();
// Delete empty rows 5-8
assert!(model.delete_rows(0, 5, 4).is_ok());
// Verify shift
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
// Verify diffs now use bulk operation
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 5);
assert_eq!(*count, 4);
assert_eq!(old_data.len(), 4);
// All rows should be empty
for row_data in old_data {
assert!(row_data.data.is_empty());
}
}
_ => panic!("Unexpected diff variant"),
}
// Undo/redo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
}
#[test]
fn delete_mixed_empty_and_filled_rows() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Alternating filled and empty rows
model.set_user_input(0, 5, 1, "Row5").unwrap();
model.set_user_input(0, 7, 1, "Row7").unwrap();
model.set_user_input(0, 9, 1, "Row9").unwrap();
model.set_user_input(0, 10, 1, "After").unwrap();
assert!(model.delete_rows(0, 5, 5).is_ok());
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "After");
// Verify mix of empty and filled row diffs
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 5);
assert_eq!(*count, 5);
assert_eq!(old_data.len(), 5);
// Count filled rows (should be 3: rows 5, 7, 9)
let filled_count = old_data
.iter()
.filter(|row_data| !row_data.data.is_empty())
.count();
assert_eq!(filled_count, 3);
}
_ => panic!("Unexpected diff variant"),
}
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "Row7");
assert_eq!(model.get_formatted_cell_value(0, 9, 1).unwrap(), "Row9");
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
}
#[test]
fn bulk_insert_rows_undo_redo() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up initial data
model.set_user_input(0, 1, 1, "A1").unwrap();
model.set_user_input(0, 2, 1, "A2").unwrap();
model.set_user_input(0, 5, 1, "A5").unwrap();
// Insert 3 rows at position 3
assert!(model.insert_rows(0, 3, 3).is_ok());
// Verify data has shifted
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "A2");
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // A5 moved to A8
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertRows {
sheet: 0,
row: 3,
count: 3
}
));
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "A5"); // Back to original position
// Redo
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // Shifted again
}
#[test]
fn bulk_insert_columns_undo_redo() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up initial data
model.set_user_input(0, 1, 1, "A1").unwrap();
model.set_user_input(0, 1, 2, "B1").unwrap();
model.set_user_input(0, 1, 5, "E1").unwrap();
// Insert 3 columns at position 3
assert!(model.insert_columns(0, 3, 3).is_ok());
// Verify data has shifted
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "B1");
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // E1 moved to H1
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertColumns {
sheet: 0,
column: 3,
count: 3
}
));
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1"); // Back to original position
// Redo
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // Shifted again
}
#[test]
fn bulk_delete_rows_round_trip() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data with styles
model.set_user_input(0, 3, 1, "Row3").unwrap();
model.set_user_input(0, 4, 1, "Row4").unwrap();
model.set_user_input(0, 5, 1, "Row5").unwrap();
model.set_user_input(0, 6, 1, "Row6").unwrap();
model.set_user_input(0, 7, 1, "After").unwrap();
// Set some row heights to verify they're preserved
model.set_rows_height(0, 4, 4, 30.0).unwrap();
model.set_rows_height(0, 5, 5, 40.0).unwrap();
// Delete rows 3-6
assert!(model.delete_rows(0, 3, 4).is_ok());
// Verify deletion
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteRows {
sheet,
row,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*row, 3);
assert_eq!(*count, 4);
assert_eq!(old_data.len(), 4);
// Verify data was preserved
assert!(old_data[0].data.contains_key(&1)); // Row3
assert!(old_data[1].data.contains_key(&1)); // Row4
assert!(old_data[2].data.contains_key(&1)); // Row5
assert!(old_data[3].data.contains_key(&1)); // Row6
}
_ => panic!("Expected DeleteRows diff"),
}
// Undo - should restore all data and row heights
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "After");
assert_eq!(model.get_row_height(0, 4).unwrap(), 30.0);
assert_eq!(model.get_row_height(0, 5).unwrap(), 40.0);
// Redo - should delete again
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
// Final undo to verify round-trip
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
}
#[test]
fn bulk_delete_columns_round_trip() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data with styles
model.set_user_input(0, 1, 3, "C1").unwrap();
model.set_user_input(0, 1, 4, "D1").unwrap();
model.set_user_input(0, 1, 5, "E1").unwrap();
model.set_user_input(0, 1, 6, "F1").unwrap();
model.set_user_input(0, 1, 7, "After").unwrap();
// Set some column widths to verify they're preserved
model.set_columns_width(0, 4, 4, 100.0).unwrap();
model.set_columns_width(0, 5, 5, 120.0).unwrap();
// Delete columns 3-6
assert!(model.delete_columns(0, 3, 4).is_ok());
// Verify deletion
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
// Check diff structure
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
match &list[0] {
Diff::DeleteColumns {
sheet,
column,
count,
old_data,
} => {
assert_eq!(*sheet, 0);
assert_eq!(*column, 3);
assert_eq!(*count, 4);
assert_eq!(old_data.len(), 4);
// Verify data was preserved
assert!(old_data[0].data.contains_key(&1)); // C1
assert!(old_data[1].data.contains_key(&1)); // D1
assert!(old_data[2].data.contains_key(&1)); // E1
assert!(old_data[3].data.contains_key(&1)); // F1
}
_ => panic!("Expected DeleteColumns diff"),
}
// Undo - should restore all data and column widths
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
assert_eq!(model.get_formatted_cell_value(0, 1, 7).unwrap(), "After");
assert_eq!(model.get_column_width(0, 4).unwrap(), 100.0);
assert_eq!(model.get_column_width(0, 5).unwrap(), 120.0);
// Redo - should delete again
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
// Final undo to verify round-trip
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
}
#[test]
fn complex_bulk_operations_sequence() {
// Test a complex sequence of bulk operations
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Initial setup
model.set_user_input(0, 1, 1, "A1").unwrap();
model.set_user_input(0, 2, 2, "B2").unwrap();
model.set_user_input(0, 3, 3, "C3").unwrap();
// Operation 1: Insert 2 rows at position 2
model.insert_rows(0, 2, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2"); // B2 moved down
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3"); // C3 moved down
// Operation 2: Insert 2 columns at position 2
model.insert_columns(0, 2, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2"); // B2 moved right
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3"); // C3 moved right
// Operation 3: Delete the inserted rows
model.delete_rows(0, 2, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
// Undo all operations
model.undo().unwrap(); // Undo delete rows
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3");
model.undo().unwrap(); // Undo insert columns
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3");
model.undo().unwrap(); // Undo insert rows
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 3, 3).unwrap(), "C3");
// Redo all operations
model.redo().unwrap(); // Redo insert rows
model.redo().unwrap(); // Redo insert columns
model.redo().unwrap(); // Redo delete rows
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
}
#[test]
fn bulk_operations_with_formulas_update() {
// Test that formulas update correctly with bulk operations
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data and formulas
model.set_user_input(0, 1, 1, "10").unwrap();
model.set_user_input(0, 5, 1, "20").unwrap();
model.set_user_input(0, 10, 1, "=A1+A5").unwrap(); // Formula referencing A1 and A5
// Insert 3 rows at position 3
model.insert_rows(0, 3, 3).unwrap();
// Formula should update to reference the shifted cells
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "30"); // Formula moved down
assert_eq!(model.get_cell_content(0, 13, 1).unwrap(), "=A1+A8"); // A5 became A8
// Undo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "30");
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1+A5");
// Now test column insertion
model.set_user_input(0, 1, 5, "20").unwrap(); // Add value in E1
model.set_user_input(0, 1, 10, "=A1+E1").unwrap();
model.insert_columns(0, 3, 2).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 12).unwrap(), "30"); // Formula moved right
assert_eq!(model.get_cell_content(0, 1, 12).unwrap(), "=A1+G1"); // E1 became G1
}
#[test]
fn bulk_delete_with_styles() {
// Test that cell and row/column styles are preserved
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Set up data with various styles
for r in 5..10 {
model.set_user_input(0, r, 1, &format!("Row{r}")).unwrap();
model.set_rows_height(0, r, r, (r * 10) as f64).unwrap();
}
// Delete and verify style preservation
model.delete_rows(0, 5, 5).unwrap();
// Undo should restore all styles
model.undo().unwrap();
for r in 5..10 {
assert_eq!(
model.get_formatted_cell_value(0, r, 1).unwrap(),
format!("Row{r}")
);
assert_eq!(model.get_row_height(0, r).unwrap(), (r * 10) as f64);
}
}
#[test]
fn bulk_operations_large_count() {
// Test operations with large counts
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Insert a large number of rows
model.set_user_input(0, 1, 1, "Before").unwrap();
model.set_user_input(0, 100, 1, "After").unwrap();
assert!(model.insert_rows(0, 50, 100).is_ok());
// Verify shift
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After"); // Moved by 100
// Check diff
let list = last_diff_list(&mut model);
assert_eq!(list.len(), 1);
assert!(matches!(
&list[0],
Diff::InsertRows {
sheet: 0,
row: 50,
count: 100
}
));
// Undo and redo
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 100, 1).unwrap(), "After");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After");
}
#[test]
fn bulk_operations_error_cases() {
// Test error conditions
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Negative count should fail
assert!(model.insert_rows(0, 1, -5).is_err());
assert!(model.insert_columns(0, 1, -5).is_err());
assert!(model.delete_rows(0, 1, -5).is_err());
assert!(model.delete_columns(0, 1, -5).is_err());
// Zero count should fail
assert!(model.insert_rows(0, 1, 0).is_err());
assert!(model.insert_columns(0, 1, 0).is_err());
assert!(model.delete_rows(0, 1, 0).is_err());
assert!(model.delete_columns(0, 1, 0).is_err());
// Out of bounds operations should fail
assert!(model.delete_rows(0, LAST_ROW - 5, 10).is_err());
assert!(model.delete_columns(0, LAST_COLUMN - 5, 10).is_err());
}
#[test]
fn bulk_diff_serialization() {
// Test that bulk diffs can be serialized/deserialized correctly
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Create some data
model.set_user_input(0, 1, 1, "Test").unwrap();
model.insert_rows(0, 2, 3).unwrap();
// Flush and get the serialized diffs
let bytes = model.flush_send_queue();
// Create a new model and apply the diffs
let base2 = new_empty_model();
let mut model2 = UserModel::from_model(base2);
assert!(model2.apply_external_diffs(&bytes).is_ok());
// Verify the state matches
assert_eq!(model2.get_formatted_cell_value(0, 1, 1).unwrap(), "Test");
}
#[test]
fn boundary_validation() {
let base = new_empty_model();
let mut model = UserModel::from_model(base);
// Test deleting rows beyond valid range
assert!(model.delete_rows(0, LAST_ROW, 2).is_err());
assert!(model.delete_rows(0, LAST_ROW + 1, 1).is_err());
// Test deleting columns beyond valid range
assert!(model.delete_columns(0, LAST_COLUMN, 2).is_err());
assert!(model.delete_columns(0, LAST_COLUMN + 1, 1).is_err());
// Test valid boundary deletions (should work with our empty row fix)
assert!(model.delete_rows(0, LAST_ROW, 1).is_ok());
assert!(model.delete_columns(0, LAST_COLUMN, 1).is_ok());
}

View File

@@ -50,7 +50,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(top_border), Some(top_border),
top_cell_style.border.bottom, top_cell_style.border.bottom,
"(Top). Sheet: {sheet}, row: {row}, column: {column}" "(Top). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -62,7 +65,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(right_border), Some(right_border),
right_cell_style.border.left, right_cell_style.border.left,
"(Right). Sheet: {sheet}, row: {row}, column: {column}" "(Right). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -74,7 +80,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(bottom_border), Some(bottom_border),
bottom_cell_style.border.top, bottom_cell_style.border.top,
"(Bottom). Sheet: {sheet}, row: {row}, column: {column}" "(Bottom). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }
@@ -85,7 +94,10 @@ fn check_borders(model: &UserModel) {
assert_eq!( assert_eq!(
Some(left_border), Some(left_border),
left_cell_style.border.right, left_cell_style.border.right,
"(Left). Sheet: {sheet}, row: {row}, column: {column}" "(Left). Sheet: {}, row: {}, column: {}",
sheet,
row,
column
); );
} }
} }

View File

@@ -89,67 +89,3 @@ fn clear_all_empty_cell() {
model.undo().unwrap(); model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
} }
#[test]
fn issue_454() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_user_input(
0,
1,
1,
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
)
.unwrap();
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("TRUE".to_string())
);
model
.range_clear_contents(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("FALSE".to_string())
);
model.undo().unwrap();
}
#[test]
fn issue_454b() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_user_input(
0,
1,
1,
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
)
.unwrap();
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("TRUE".to_string())
);
model
.range_clear_all(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("FALSE".to_string())
);
model.undo().unwrap();
}

View File

@@ -65,8 +65,8 @@ fn queue_undo_redo_multiple() {
model1.set_user_input(0, row, 17, "=ROW()").unwrap(); model1.set_user_input(0, row, 17, "=ROW()").unwrap();
} }
model1.insert_rows(0, 3, 1).unwrap(); model1.insert_row(0, 3).unwrap();
model1.insert_rows(0, 3, 1).unwrap(); model1.insert_row(0, 3).unwrap();
// undo al of them // undo al of them
while model1.can_undo() { while model1.can_undo() {

View File

@@ -26,7 +26,7 @@ fn set_user_input_errors() {
#[test] #[test]
fn user_model_debug_message() { fn user_model_debug_message() {
let model = UserModel::new_empty("model", "en", "UTC").unwrap(); let model = UserModel::new_empty("model", "en", "UTC").unwrap();
let s = &format!("{model:?}"); let s = &format!("{:?}", model);
assert_eq!(s, "UserModel"); assert_eq!(s, "UserModel");
} }
@@ -62,7 +62,7 @@ fn insert_remove_rows() {
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok()); assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
// remove the row // remove the row
assert!(model.delete_rows(0, 5, 1).is_ok()); assert!(model.delete_row(0, 5).is_ok());
// Row 5 has now the normal height // Row 5 has now the normal height
assert_eq!(model.get_row_height(0, 5), Ok(height)); assert_eq!(model.get_row_height(0, 5), Ok(height));
// There is no value in A5 // There is no value in A5
@@ -99,7 +99,7 @@ fn insert_remove_columns() {
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width); assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
// remove the column // remove the column
assert!(model.delete_columns(0, 5, 1).is_ok()); assert!(model.delete_column(0, 5).is_ok());
// Column 5 has now the normal width // Column 5 has now the normal width
assert_eq!(model.get_column_width(0, 5), Ok(column_width)); assert_eq!(model.get_column_width(0, 5), Ok(column_width));
// There is no value in E5 // There is no value in E5

View File

@@ -1,55 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::constants::LAST_ROW;
use crate::expressions::types::Area;
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_tests() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// This is the first row, column 5
model.set_user_input(0, 3, 5, "todo").unwrap();
// Row 3 before column 5 should be empty
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 4)
.unwrap(),
None
);
// Row 3 before column 5 should be 5
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 7)
.unwrap(),
Some(5)
);
}
#[test]
fn test_last_empty_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let column_g_range = Area {
sheet: 0,
row: 1,
column: 7,
width: 1,
height: LAST_ROW,
};
model
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
.unwrap();
// Column 7 has a style but it is empty
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 14)
.unwrap(),
None
);
}

View File

@@ -1,173 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
UserModel,
};
#[test]
fn insert_multiple_rows_shifts_cells() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Place a value below the insertion point.
model.set_user_input(0, 10, 1, "42").unwrap();
// Insert 3 rows starting at row 5.
assert!(model.insert_rows(0, 5, 3).is_ok());
// The original value should have moved down by 3 rows.
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
// Undo / redo cycle should restore the same behaviour.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "42");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "42");
}
#[test]
fn insert_rows_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.insert_rows(0, 1, -2),
Err("Cannot add a negative number of cells :)".to_string())
);
assert_eq!(
model.insert_rows(0, 1, 0),
Err("Cannot add a negative number of cells :)".to_string())
);
// Inserting too many rows so that the sheet would overflow.
let too_many = LAST_ROW; // This guarantees max_row + too_many > LAST_ROW.
assert_eq!(
model.insert_rows(0, 1, too_many),
Err(
"Cannot shift cells because that would delete cells at the end of a column".to_string()
)
);
}
#[test]
fn insert_multiple_columns_shifts_cells() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Place a value to the right of the insertion point.
model.set_user_input(0, 1, 10, "99").unwrap();
// Insert 3 columns starting at column 5.
assert!(model.insert_columns(0, 5, 3).is_ok());
// The original value should have moved right by 3 columns.
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
// Undo / redo cycle.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 10).unwrap(), "99");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 13).unwrap(), "99");
}
#[test]
fn insert_columns_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.insert_columns(0, 1, -2),
Err("Cannot add a negative number of cells :)".to_string())
);
assert_eq!(
model.insert_columns(0, 1, 0),
Err("Cannot add a negative number of cells :)".to_string())
);
// Overflow to the right.
let too_many = LAST_COLUMN; // Ensures max_column + too_many > LAST_COLUMN
assert_eq!(
model.insert_columns(0, 1, too_many),
Err("Cannot shift cells because that would delete cells at the end of a row".to_string())
);
}
#[test]
fn delete_multiple_rows_shifts_cells_upwards() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Populate rows 10..14 (to be deleted) so that the diff builder does not fail.
for r in 10..15 {
model.set_user_input(0, r, 1, "del").unwrap();
}
// Place a value below the deletion range.
model.set_user_input(0, 20, 1, "keep").unwrap();
// Delete 5 rows starting at row 10.
assert!(model.delete_rows(0, 10, 5).is_ok());
// The value originally at row 20 should now be at row 15.
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
// Undo / redo cycle.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 20, 1).unwrap(), "keep");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 15, 1).unwrap(), "keep");
}
#[test]
fn delete_rows_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.delete_rows(0, 1, -3),
Err("Please use insert rows instead".to_string())
);
assert_eq!(
model.delete_rows(0, 1, 0),
Err("Please use insert rows instead".to_string())
);
}
#[test]
fn delete_multiple_columns_shifts_cells_left() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Place a value to the right of the deletion range.
model.set_user_input(0, 1, 15, "88").unwrap();
// Delete 4 columns starting at column 5.
assert!(model.delete_columns(0, 5, 4).is_ok());
// The value originally at column 15 should now be at column 11.
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
// Undo / redo cycle.
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 15).unwrap(), "88");
model.redo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 11).unwrap(), "88");
}
#[test]
fn delete_columns_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// Negative or zero counts are rejected.
assert_eq!(
model.delete_columns(0, 1, -4),
Err("Please use insert columns instead".to_string())
);
assert_eq!(
model.delete_columns(0, 1, 0),
Err("Please use insert columns instead".to_string())
);
}

View File

@@ -24,7 +24,6 @@ fn csv_paste() {
model.get_formatted_cell_value(0, 7, 7), model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string()) Ok("21".to_string())
); );
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
} }
#[test] #[test]
@@ -46,7 +45,6 @@ fn csv_paste_formula() {
model.get_formatted_cell_value(0, 1, 1), model.get_formatted_cell_value(0, 1, 1),
Ok("2022".to_string()) Ok("2022".to_string())
); );
assert_eq!([1, 1, 1, 1], model.get_selected_view().range);
} }
#[test] #[test]
@@ -71,7 +69,6 @@ fn tsv_crlf_paste() {
model.get_formatted_cell_value(0, 7, 7), model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string()) Ok("21".to_string())
); );
assert_eq!([4, 2, 5, 4], model.get_selected_view().range);
} }
#[test] #[test]
@@ -167,7 +164,7 @@ fn copy_paste_internal() {
let copy = model.copy_to_clipboard().unwrap(); let copy = model.copy_to_clipboard().unwrap();
assert_eq!( assert_eq!(
copy.csv, copy.csv,
"42\t127\n\"A season of faith\t \"\"perfection\"\"\"" "42\t127\n\"A season of faith\t \"\"perfection\"\"\"\t\n"
); );
assert_eq!(copy.range, (1, 1, 2, 2)); assert_eq!(copy.range, (1, 1, 2, 2));

View File

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

View File

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

View File

@@ -11,10 +11,11 @@ impl UserModel {
r##"{{ r##"{{
"item": {{ "item": {{
"style": "thin", "style": "thin",
"color": "{color}" "color": "{}"
}}, }},
"type": "All" "type": "All"
}}"## }}"##,
color
)) ))
.unwrap(); .unwrap();
let range = &Area { let range = &Area {
@@ -39,10 +40,11 @@ impl UserModel {
r##"{{ r##"{{
"item": {{ "item": {{
"style": "thin", "style": "thin",
"color": "{color}" "color": "{}"
}}, }},
"type": "{kind}" "type": "{}"
}}"## }}"##,
color, kind
)) ))
.unwrap(); .unwrap();
let range = &Area { let range = &Area {

View File

@@ -13,7 +13,7 @@ impl Model {
if cell.contains('!') { if cell.contains('!') {
self.parse_reference(cell).unwrap() self.parse_reference(cell).unwrap()
} else { } else {
self.parse_reference(&format!("Sheet1!{cell}")).unwrap() self.parse_reference(&format!("Sheet1!{}", cell)).unwrap()
} }
} }
pub fn _set(&mut self, cell: &str, value: &str) { pub fn _set(&mut self, cell: &str, value: &str) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,10 +59,10 @@ export declare class UserModel {
setSheetColor(sheet: number, color: string): void setSheetColor(sheet: number, color: string): void
rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void rangeClearAll(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void rangeClearContents(sheet: number, startRow: number, startColumn: number, endRow: number, endColumn: number): void
insertRows(sheet: number, row: number, rowCount: number): void insertRow(sheet: number, row: number): void
insertColumns(sheet: number, column: number, columnCount: number): void insertColumn(sheet: number, column: number): void
deleteRows(sheet: number, row: number, rowCount: number): void deleteRow(sheet: number, row: number): void
deleteColumns(sheet: number, column: number, columnCount: number): void deleteColumn(sheet: number, column: number): void
setRowHeight(sheet: number, row: number, height: number): void setRowHeight(sheet: number, row: number, height: number): void
setColumnWidth(sheet: number, column: number, width: number): void setColumnWidth(sheet: number, column: number, width: number): void
getRowHeight(sheet: number, row: number): number getRowHeight(sheet: number, row: number): number

View File

@@ -340,20 +340,4 @@ impl Model {
.delete_defined_name(&name, scope) .delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[napi(js_name = "moveColumn")]
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
self
.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[napi(js_name = "moveRow")]
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
self
.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
}
} }

View File

@@ -183,36 +183,24 @@ impl UserModel {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[napi(js_name = "insertRows")] #[napi(js_name = "insertRow")]
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> { pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
self self.model.insert_row(sheet, row).map_err(to_js_error)
.model
.insert_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[napi(js_name = "insertColumns")] #[napi(js_name = "insertColumn")]
pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> { pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
self self.model.insert_column(sheet, column).map_err(to_js_error)
.model
.insert_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[napi(js_name = "deleteRows")] #[napi(js_name = "deleteRow")]
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> { pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
self self.model.delete_row(sheet, row).map_err(to_js_error)
.model
.delete_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[napi(js_name = "deleteColumns")] #[napi(js_name = "deleteColumn")]
pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> { pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
self self.model.delete_column(sheet, column).map_err(to_js_error)
.model
.delete_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[napi(js_name = "setRowsHeight")] #[napi(js_name = "setRowsHeight")]
@@ -663,20 +651,4 @@ impl UserModel {
.delete_defined_name(&name, scope) .delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[napi(js_name = "moveColumn")]
pub fn move_column(&mut self, sheet: u32, column: i32, delta: i32) -> Result<()> {
self
.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[napi(js_name = "moveRow")]
pub fn move_row(&mut self, sheet: u32, row: i32, delta: i32) -> Result<()> {
self
.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,4 +26,4 @@ clean:
rm -rf pkg rm -rf pkg
rm -f types.js rm -f types.js
.PHONY: all lint clean tests .PHONY: all lint clean

View File

@@ -1,25 +1,231 @@
# Regrettably at the time of writing there is not a perfect way to
# generate the TypeScript types from Rust so we basically fix them manually
# Hopefully this will suffice for our needs and one day will be automatic
header = r""" header = r"""
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
""".strip() """.strip()
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[];
"""
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
text = text.replace(properties, properties_types)
text = text.replace(style, style_types)
text = text.replace(view, view_types)
text = text.replace(autofill_rows, autofill_rows_types)
text = text.replace(autofill_columns, autofill_columns_types)
text = text.replace(set_cell_style, set_cell_style_types)
text = text.replace(set_area_border, set_area_border_types)
text = text.replace(paste_csv_string, paste_csv_string_types)
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
with open("types.ts") as f: with open("types.ts") as f:
types_str = f.read() types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str) header_types = "{}\n\n{}".format(header, types_str)
text = text.replace(header, header_types) text = text.replace(header, header_types)
for line in text.splitlines(): if text.find("any") != -1:
line = line.lstrip() print("There are 'unfixed' types. Please check.")
# Skip internal methods exit(1)
if line.startswith("readonly model_"):
continue
if line.find("any") != -1:
print("There are 'unfixed' public types. Please check.")
exit(1)
return text return text
if __name__ == "__main__": if __name__ == "__main__":
types_file = "pkg/wasm.d.ts" types_file = "pkg/wasm.d.ts"
with open(types_file) as f: with open(types_file) as f:
@@ -37,3 +243,5 @@ if __name__ == "__main__":
with open(js_file, "wb") as f: with open(js_file, "wb") as f:
f.write(bytes("{}\n{}".format(text_js, text), "utf8")) f.write(bytes("{}\n{}".format(text_js, text), "utf8"))

View File

@@ -7,7 +7,6 @@ use wasm_bindgen::{
use ironcalc_base::{ use ironcalc_base::{
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column}, expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
types::{CellType, Style}, types::{CellType, Style},
worksheet::NavigationDirection,
BorderArea, ClipboardData, UserModel as BaseModel, BorderArea, ClipboardData, UserModel as BaseModel,
}; };
@@ -17,7 +16,7 @@ fn to_js_error(error: String) -> JsError {
/// Return an array with a list of all the tokens from a formula /// Return an array with a list of all the tokens from a formula
/// This is used by the UI to color them according to a theme. /// This is used by the UI to color them according to a theme.
#[wasm_bindgen(js_name = "getTokens", unchecked_return_type = "MarkedToken[]")] #[wasm_bindgen(js_name = "getTokens")]
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> { pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
let tokens = tokenizer(formula); let tokens = tokenizer(formula);
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from) serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
@@ -196,61 +195,24 @@ impl Model {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "insertRows")] #[wasm_bindgen(js_name = "insertRow")]
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> { pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
self.model self.model.insert_row(sheet, row).map_err(to_js_error)
.insert_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "insertColumns")] #[wasm_bindgen(js_name = "insertColumn")]
pub fn insert_columns( pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
&mut self, self.model.insert_column(sheet, column).map_err(to_js_error)
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), JsError> {
self.model
.insert_columns(sheet, column, column_count)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "deleteRows")] #[wasm_bindgen(js_name = "deleteRow")]
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), JsError> { pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
self.model self.model.delete_row(sheet, row).map_err(to_js_error)
.delete_rows(sheet, row, row_count)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "deleteColumns")] #[wasm_bindgen(js_name = "deleteColumn")]
pub fn delete_columns( pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
&mut self, self.model.delete_column(sheet, column).map_err(to_js_error)
sheet: u32,
column: i32,
column_count: i32,
) -> Result<(), JsError> {
self.model
.delete_columns(sheet, column, column_count)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "moveColumn")]
pub fn move_column_action(
&mut self,
sheet: u32,
column: i32,
delta: i32,
) -> Result<(), JsError> {
self.model
.move_column_action(sheet, column, delta)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "moveRow")]
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), JsError> {
self.model
.move_row_action(sheet, row, delta)
.map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "setRowsHeight")] #[wasm_bindgen(js_name = "setRowsHeight")]
@@ -376,7 +338,7 @@ impl Model {
#[wasm_bindgen(js_name = "updateRangeStyle")] #[wasm_bindgen(js_name = "updateRangeStyle")]
pub fn update_range_style( pub fn update_range_style(
&mut self, &mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] range: JsValue, range: JsValue,
style_path: &str, style_path: &str,
value: &str, value: &str,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
@@ -387,7 +349,7 @@ impl Model {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "getCellStyle", unchecked_return_type = "CellStyle")] #[wasm_bindgen(js_name = "getCellStyle")]
pub fn get_cell_style( pub fn get_cell_style(
&mut self, &mut self,
sheet: u32, sheet: u32,
@@ -403,10 +365,7 @@ impl Model {
} }
#[wasm_bindgen(js_name = "onPasteStyles")] #[wasm_bindgen(js_name = "onPasteStyles")]
pub fn on_paste_styles( pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> {
&mut self,
#[wasm_bindgen(unchecked_param_type = "CellStyle[][]")] styles: JsValue,
) -> Result<(), JsError> {
let styles: &Vec<Vec<Style>> = let styles: &Vec<Vec<Style>> =
&serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?; &serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?;
self.model.on_paste_styles(styles).map_err(to_js_error) self.model.on_paste_styles(styles).map_err(to_js_error)
@@ -432,10 +391,7 @@ impl Model {
// I don't _think_ serializing to JsValue can't fail // I don't _think_ serializing to JsValue can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[wasm_bindgen( #[wasm_bindgen(js_name = "getWorksheetsProperties")]
js_name = "getWorksheetsProperties",
unchecked_return_type = "WorksheetProperties[]"
)]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&self) -> JsValue { pub fn get_worksheets_properties(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap() serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
@@ -454,7 +410,7 @@ impl Model {
// I don't _think_ serializing to JsValue can't fail // I don't _think_ serializing to JsValue can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[wasm_bindgen(js_name = "getSelectedView", unchecked_return_type = "SelectedView")] #[wasm_bindgen(js_name = "getSelectedView")]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_selected_view(&self) -> JsValue { pub fn get_selected_view(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap() serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
@@ -513,11 +469,7 @@ impl Model {
} }
#[wasm_bindgen(js_name = "autoFillRows")] #[wasm_bindgen(js_name = "autoFillRows")]
pub fn auto_fill_rows( pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
&mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
to_row: i32,
) -> Result<(), JsError> {
let area: Area = let area: Area =
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
self.model self.model
@@ -528,7 +480,7 @@ impl Model {
#[wasm_bindgen(js_name = "autoFillColumns")] #[wasm_bindgen(js_name = "autoFillColumns")]
pub fn auto_fill_columns( pub fn auto_fill_columns(
&mut self, &mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue, source_area: JsValue,
to_column: i32, to_column: i32,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let area: Area = let area: Area =
@@ -568,20 +520,6 @@ impl Model {
self.model.on_page_up().map_err(to_js_error) self.model.on_page_up().map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "onNavigateToEdgeInDirection")]
pub fn on_navigate_to_edge_in_direction(&mut self, direction: &str) -> Result<(), JsError> {
let direction = match direction {
"ArrowLeft" => NavigationDirection::Left,
"ArrowRight" => NavigationDirection::Right,
"ArrowUp" => NavigationDirection::Up,
"ArrowDown" => NavigationDirection::Down,
_ => return Err(JsError::new(&format!("Invalid direction: {direction}"))),
};
self.model
.on_navigate_to_edge_in_direction(direction)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setWindowWidth")] #[wasm_bindgen(js_name = "setWindowWidth")]
pub fn set_window_width(&mut self, window_width: f64) { pub fn set_window_width(&mut self, window_width: f64) {
self.model.set_window_width(window_width); self.model.set_window_width(window_width);
@@ -623,8 +561,8 @@ impl Model {
#[wasm_bindgen(js_name = "setAreaWithBorder")] #[wasm_bindgen(js_name = "setAreaWithBorder")]
pub fn set_area_with_border( pub fn set_area_with_border(
&mut self, &mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue, area: JsValue,
#[wasm_bindgen(unchecked_param_type = "BorderArea")] border_area: JsValue, border_area: JsValue,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let range: Area = let range: Area =
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
@@ -651,7 +589,7 @@ impl Model {
self.model.set_name(name); 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> { pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
let data = self let data = self
.model .model
@@ -665,9 +603,8 @@ impl Model {
pub fn paste_from_clipboard( pub fn paste_from_clipboard(
&mut self, &mut self,
source_sheet: u32, source_sheet: u32,
#[wasm_bindgen(unchecked_param_type = "[number, number, number, number]")]
source_range: JsValue, source_range: JsValue,
#[wasm_bindgen(unchecked_param_type = "ClipboardData")] clipboard: JsValue, clipboard: JsValue,
is_cut: bool, is_cut: bool,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let source_range: (i32, i32, i32, i32) = let source_range: (i32, i32, i32, i32) =
@@ -680,11 +617,7 @@ impl Model {
} }
#[wasm_bindgen(js_name = "pasteCsvText")] #[wasm_bindgen(js_name = "pasteCsvText")]
pub fn paste_csv_string( pub fn paste_csv_string(&mut self, area: JsValue, csv: &str) -> Result<(), JsError> {
&mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
csv: &str,
) -> Result<(), JsError> {
let range: Area = let range: Area =
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
self.model self.model
@@ -692,10 +625,7 @@ impl Model {
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[wasm_bindgen( #[wasm_bindgen(js_name = "getDefinedNameList")]
js_name = "getDefinedNameList",
unchecked_return_type = "DefinedName[]"
)]
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> { pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
let data: Vec<DefinedName> = self let data: Vec<DefinedName> = self
.model .model
@@ -743,27 +673,17 @@ impl Model {
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[wasm_bindgen(js_name = "getLastNonEmptyInRowBeforeColumn")] #[wasm_bindgen(js_name = "getSheetMarkup")]
pub fn get_last_non_empty_in_row_before_column( pub fn get_sheet_markup(
&self, &self,
sheet: u32, sheet: u32,
row: i32, start_row: i32,
column: i32, start_column: i32,
) -> Result<Option<i32>, JsError> { end_row: i32,
end_column: i32,
) -> Result<String, JsError> {
self.model self.model
.get_last_non_empty_in_row_before_column(sheet, row, column) .get_sheet_markup(sheet, start_row, start_column, end_row, end_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) .map_err(to_js_error)
} }
} }

View File

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

View File

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

1802
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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