Compare commits

..

52 Commits

Author SHA1 Message Date
Nicolás Hatcher Andrés
fad8bc7d0c UPDATE: Update release to 0.6.0 (#463)
Should we have a single version number at the root?
2025-10-19 18:32:07 +02:00
Nicolás Hatcher Andrés
bbba875da3 FIX: Cleanup CSS code (#462) 2025-10-19 17:57:26 +02:00
Daniel González-Albo
1b34db0bc3 Merge pull request #455 from ironcalc/empty-fix
FIX: Evaluate after deleting content in the user API
2025-10-19 17:25:48 +02:00
Daniel González-Albo
d9aac1d77c Merge pull request #460 from ironcalc/xfln
FIX: DAYS and ISOWEEKNUM are "XLFN" functions
2025-10-19 17:24:40 +02:00
Nicolás Hatcher Andrés
d429bd8f60 FIX: Remove transition so there is no close drawer glitch (#459) 2025-10-19 17:12:49 +02:00
Nicolás Hatcher
2dbc3f4790 FIX: DAYS and ISOWEEKNUM are "XLFN" functions 2025-10-19 17:12:12 +02:00
Daniel González-Albo
292ecafb31 Merge pull request #458 from ironcalc/name-sync
FIX: sync changes of the localstorage in the left drawer
2025-10-19 16:13:38 +02:00
Daniel González-Albo
ead4bc713c docs: update the section 'managing workbooks' with new info about the left sidebar (#457) 2025-10-19 16:11:59 +02:00
Nicolás Hatcher
a9748eafec FIX: sync changes of the localstorage in the left drawer
This is a bit of a HACK. going a bit "against" React philosophy.
2025-10-19 16:05:50 +02:00
Nicolás Hatcher Andrés
330a018202 FIX: Adds test for TIME/HOUR/MINUTE/SECOND (#456) 2025-10-19 15:38:11 +02:00
Daniel González-Albo
d9812876e2 update: show which format is active in FormatMenu (#450)
* update: show which format is active in formatmenu

* update: requested fixes
2025-10-19 12:44:58 +02:00
Nicolás Hatcher
895244ed11 FIX: Evaluate after deleting content in the user API 2025-10-19 12:41:05 +02:00
Daniel González-Albo
f2da24326b update: Add a left drawer to improve workbook management (#453)
* update: add leftbar to app

* style: a few cosmetic changes

* update: allow pinning workbooks

* style: show ellipsis button only on hover

* update: add basic responsiveness

* style: use active state when file and help menus are open

* style: increase transition time

* update: allow duplication of workbooks

* chore: standardize menus
2025-10-19 10:20:31 +02:00
Brian Hung
dd4467f95d date time functions (#425)
* merge networkdays, networkdays.intl #33

* merge time, timevalue, hour, minute, second #35

* merge datedif, datevalue #36

* merge days, days360, weekday, weeknum, workday, workday.intl, yearfrac, isoweeknum #41

* from excel helper

* fix build

* date time macros

* de-dupe weekend

* serial helper

* de-dupe now today

* weekend pattern enum

* remove unused clippy wrong self

* fix docs

* add test coverage

* fix build

* fix cursor comment

* PR coments + xlsx date time
2025-10-19 10:19:19 +02:00
Nicolás Hatcher Andrés
29989b9fd7 UPDATE: Add info in README about the Dockerfile (#452)
* UPDATE: Add info in README about the Dockerfile

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-15 22:51:18 +02:00
Nicolás Hatcher
1efc921ce6 UPDATE: First Docker images! 2025-10-15 22:19:25 +02:00
Nicolás Hatcher
ed64716f0f FIX: Refactor both dialogs to get common code 2025-10-12 14:23:12 +02:00
Nicolás Hatcher
dd29287c5a FIX: Lifts TemplateDialog to App.tsx 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
7841abe2d2 update: use a different dialog for templates only 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
49c3d1e03a chore: remove optional props 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b709041f9d chore: move new ic icon to icons folder 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b177a33815 fix: remove obsolete css prefixes 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b506ccf908 update: rename function 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
eb3e92ffd8 update: make dialogs content change depending on scenario 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
0b925a4d6a update: add a second New from template action to FileMenu 2025-10-12 14:23:12 +02:00
Nicolás Hatcher
6a3e37f4c1 FIX: Integration of Welcome dialog 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
2496227344 fix: lint 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
72355a5201 update: add a compontent for the list items 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
81901ec717 update: add a welcome dialog to the app 2025-10-12 14:23:12 +02:00
Dmitry S
aa664a95a1 Fix example in README to reference Model correctly.
Since https://github.com/ironcalc/IronCalc/pull/27, using
`base::model::Model` complains about `model` being a private modulde,
and `base::Model` should be used instead.
2025-10-08 08:56:03 +02:00
Daniel Gonzalez Albo
c1aa743763 fix: typo in format presets 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
6321030ac8 style: add offset to arrow's tooltips 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
c2c5751ee3 style: add nicer tooltips 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
6c27ae1355 update: show scroll arrows on narrow displays 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
7bcd978998 style: tiny adjustment in dividers 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
3f083d9882 adjust spacing and group button groups 2025-10-07 20:19:57 +02:00
Nicolás Hatcher
8844b80c51 FIX: cargo fmt issue 2025-09-28 17:33:17 +02:00
Nicolás Hatcher
0f8f345aae UPDATE: have xlsx_2_icalc specify its output name
This is nice for deployments
2025-09-28 17:33:17 +02:00
Nicolás Hatcher
3191e12b93 FIX: Make clippy happy 2025-09-28 17:33:17 +02:00
Nicolás Hatcher
61cecb7af5 FIX: Fixes case with unicode characters
This is an ugly bug in ugly code. Pretty much technical deb in here
2025-09-28 12:46:16 +02:00
Nicolás Hatcher
fdeae2c771 UPDATE: Add templates 2025-09-28 12:46:16 +02:00
Matt Lehrer
3e9c69f122 add model.evaluate() call 2025-09-28 12:03:28 +02:00
Tom
c1c43143cc Adds information about references and corrected syntax on Column
This is a test commit
2025-09-25 19:25:32 +02:00
Tom
763b43a590 UPDATE: Added documentation for the Column Function 2025-09-25 19:25:32 +02:00
Daniel González-Albo
8dbfe07392 Merge pull request #443 from elsaminsut/mathfunctions
docs: adds ATAN2, ASINH, ACOSH, ATANH documentation pages
2025-09-24 20:26:53 +02:00
Elsa Minsut
e39bfe912a docs: improve consistency in ATAN2, ASINH, ACOSH, ATANH documentation 2025-09-24 19:36:30 +02:00
Elsa Minsut
9bbf94e033 update: Math and Trigonometry main page links 2025-09-24 19:19:16 +02:00
Elsa Minsut
0194912845 update: adds ATANH documentation page 2025-09-24 18:05:18 +02:00
Elsa Minsut
1d4d84bb57 update: adds ASINH documentation page 2025-09-24 17:46:56 +02:00
Elsa Minsut
e841c17aca update: adds ACOSH documentation page 2025-09-24 17:19:37 +02:00
Elsa Minsut
f2c43f2070 update: adds ATAN2 documentation page 2025-09-24 17:06:20 +02:00
Nicolás Hatcher
32b1f8ef4e FIX: Update documentation of some documented functions 2025-09-23 18:01:17 +02:00
122 changed files with 6139 additions and 2218 deletions

10
Cargo.lock generated
View File

@@ -414,7 +414,7 @@ dependencies = [
[[package]]
name = "ironcalc"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"bitcode",
"chrono",
@@ -430,7 +430,7 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"bitcode",
"chrono",
@@ -447,7 +447,7 @@ dependencies = [
[[package]]
name = "ironcalc_nodejs"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"ironcalc",
"napi",
@@ -782,7 +782,7 @@ dependencies = [
[[package]]
name = "pyroncalc"
version = "0.5.7"
version = "0.6.0"
dependencies = [
"bitcode",
"ironcalc",
@@ -1075,7 +1075,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"ironcalc_base",
"serde",

93
Dockerfile Normal file
View File

@@ -0,0 +1,93 @@
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,12 +31,7 @@ clean: remove-artifacts
rm -r -f base/target
rm -r -f xlsx/target
rm -r -f bindings/python/target
rm -r -f bindings/wasm/target
rm -r -f bindings/wasm/pkg
rm -r -f webapp/IronCalc/node_modules
rm -r -f webapp/IronCalc/dist
rm -r -f webapp/app.ironcalc.com/frontend/node_modules
rm -r -f webapp/app.ironcalc.com/frontend/dist
rm -r -f bindings/wasm/targets
rm -f cargo-test-*
rm -f base/cargo-test-*
rm -f xlsx/cargo-test-*

View File

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

View File

@@ -1,61 +0,0 @@
# Evaluation Strategy
We have a list of the spill cells:
```
// Checks if the array starting at cell will cover cells whose values
// has been requested
def CheckSpill(cell, array):
for c in cell+array:
support CellHasBeenRequested(c):
if support is not empty:
return support
return []
// Fills cells with the result (an array)
def FillCells(cell, result):
def EvaluateNodeInContext(node, context):
match node:
case OP(left, right, op):
l = EvaluateNodeInContext(left, context)?
r = EvaluateNodeInContext(left, context)?
return op(l, r)
case FUNCTION(args, fn):
...
case CELL(cell):
EvaluateCell(cell)
case RANGE(start, end):
...
def EvaluateCell(cell):
if IsCellEvaluating(cell):
return CIRC
MarkEvaluating(cell)
result = EvaluateNodeInContext(cell.formula, cell)
if isSpill(result):
CheckSpill(cell, array)?
FillCells(result)
def EvaluateWorkbook():
spill_cells = [cell_1, ...., cell_n];
for cell in spill_cells:
result = evaluate(cell)
```
# When updating a cell value
If it was a spill cell we nee

View File

@@ -1,6 +1,6 @@
[package]
name = "ironcalc_base"
version = "0.5.0"
version = "0.6.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
homepage = "https://www.ironcalc.com"

View File

@@ -22,7 +22,7 @@ impl Model {
.cell(row, column)
.and_then(|c| c.get_formula())
{
let node = &self.parsed_formulas[sheet as usize][f as usize].0.clone();
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row,

View File

@@ -77,6 +77,8 @@ impl Model {
match to_f64(&node) {
Ok(f2) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
Err(e) => data_row.push(ArrayNode::Error(e)),
},
Err(err) => data_row.push(ArrayNode::Error(err)),
@@ -98,6 +100,8 @@ impl Model {
match to_f64(&node) {
Ok(f1) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)),
Err(e) => data_row.push(ArrayNode::Error(e)),
},
Err(err) => data_row.push(ArrayNode::Error(err)),
@@ -133,6 +137,10 @@ impl Model {
(Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) {
(Ok(f1), Ok(f2)) => match op(f1, f2) {
Ok(x) => data_row.push(ArrayNode::Number(x)),
Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)),
Err(Error::VALUE) => {
data_row.push(ArrayNode::Error(Error::VALUE))
}
Err(e) => data_row.push(ArrayNode::Error(e)),
},
(Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)),

View File

@@ -64,50 +64,12 @@ impl Cell {
/// Returns the formula of a cell if any.
pub fn get_formula(&self) -> Option<i32> {
match self {
Cell::CellFormula { f, .. }
| Cell::CellFormulaBoolean { f, .. }
| Cell::CellFormulaNumber { f, .. }
| Cell::CellFormulaString { f, .. }
| Cell::CellFormulaError { f, .. }
| Cell::DynamicCellFormula { f, .. }
| Cell::DynamicCellFormulaBoolean { f, .. }
| Cell::DynamicCellFormulaNumber { f, .. }
| Cell::DynamicCellFormulaString { f, .. }
| Cell::DynamicCellFormulaError { f, .. } => Some(*f),
Cell::EmptyCell { .. }
| Cell::BooleanCell { .. }
| Cell::NumberCell { .. }
| Cell::ErrorCell { .. }
| Cell::SharedString { .. }
| Cell::SpillNumberCell { .. }
| Cell::SpillBooleanCell { .. }
| Cell::SpillErrorCell { .. }
| Cell::SpillStringCell { .. } => None,
}
}
/// Returns the dynamic range of a cell if any.
pub fn get_dynamic_range(&self) -> Option<(i32, i32)> {
match self {
Cell::DynamicCellFormula { r, .. } => Some(*r),
Cell::DynamicCellFormulaBoolean { r, .. } => Some(*r),
Cell::DynamicCellFormulaNumber { r, .. } => Some(*r),
Cell::DynamicCellFormulaString { r, .. } => Some(*r),
Cell::DynamicCellFormulaError { r, .. } => Some(*r),
Cell::EmptyCell { .. }
| Cell::BooleanCell { .. }
| Cell::NumberCell { .. }
| Cell::ErrorCell { .. }
| Cell::SharedString { .. }
| Cell::CellFormula { .. }
| Cell::CellFormulaBoolean { .. }
| Cell::CellFormulaNumber { .. }
| Cell::CellFormulaString { .. }
| Cell::CellFormulaError { .. }
| Cell::SpillNumberCell { .. }
| Cell::SpillBooleanCell { .. }
| Cell::SpillErrorCell { .. }
| Cell::SpillStringCell { .. } => None,
Cell::CellFormula { f, .. } => Some(*f),
Cell::CellFormulaBoolean { f, .. } => Some(*f),
Cell::CellFormulaNumber { f, .. } => Some(*f),
Cell::CellFormulaString { f, .. } => Some(*f),
Cell::CellFormulaError { f, .. } => Some(*f),
_ => None,
}
}
@@ -127,15 +89,6 @@ impl Cell {
Cell::CellFormulaNumber { s, .. } => *s = style,
Cell::CellFormulaString { s, .. } => *s = style,
Cell::CellFormulaError { s, .. } => *s = style,
Cell::SpillBooleanCell { s, .. } => *s = style,
Cell::SpillNumberCell { s, .. } => *s = style,
Cell::SpillStringCell { s, .. } => *s = style,
Cell::SpillErrorCell { s, .. } => *s = style,
Cell::DynamicCellFormula { s, .. } => *s = style,
Cell::DynamicCellFormulaBoolean { s, .. } => *s = style,
Cell::DynamicCellFormulaNumber { s, .. } => *s = style,
Cell::DynamicCellFormulaString { s, .. } => *s = style,
Cell::DynamicCellFormulaError { s, .. } => *s = style,
};
}
@@ -151,15 +104,6 @@ impl Cell {
Cell::CellFormulaNumber { s, .. } => *s,
Cell::CellFormulaString { s, .. } => *s,
Cell::CellFormulaError { s, .. } => *s,
Cell::SpillBooleanCell { s, .. } => *s,
Cell::SpillNumberCell { s, .. } => *s,
Cell::SpillStringCell { s, .. } => *s,
Cell::SpillErrorCell { s, .. } => *s,
Cell::DynamicCellFormula { s, .. } => *s,
Cell::DynamicCellFormulaBoolean { s, .. } => *s,
Cell::DynamicCellFormulaNumber { s, .. } => *s,
Cell::DynamicCellFormulaString { s, .. } => *s,
Cell::DynamicCellFormulaError { s, .. } => *s,
}
}
@@ -175,15 +119,6 @@ impl Cell {
Cell::CellFormulaNumber { .. } => CellType::Number,
Cell::CellFormulaString { .. } => CellType::Text,
Cell::CellFormulaError { .. } => CellType::ErrorValue,
Cell::SpillBooleanCell { .. } => CellType::LogicalValue,
Cell::SpillNumberCell { .. } => CellType::Number,
Cell::SpillStringCell { .. } => CellType::Text,
Cell::SpillErrorCell { .. } => CellType::ErrorValue,
Cell::DynamicCellFormula { .. } => CellType::Number,
Cell::DynamicCellFormulaBoolean { .. } => CellType::LogicalValue,
Cell::DynamicCellFormulaNumber { .. } => CellType::Number,
Cell::DynamicCellFormulaString { .. } => CellType::Text,
Cell::DynamicCellFormulaError { .. } => CellType::ErrorValue,
}
}
@@ -201,7 +136,7 @@ impl Cell {
Cell::EmptyCell { .. } => CellValue::None,
Cell::BooleanCell { v, s: _ } => CellValue::Boolean(*v),
Cell::NumberCell { v, s: _ } => CellValue::Number(*v),
Cell::ErrorCell { ei, .. } | Cell::SpillErrorCell { ei, .. } => {
Cell::ErrorCell { ei, .. } => {
let v = ei.to_localized_error_string(language);
CellValue::String(v)
}
@@ -213,25 +148,14 @@ impl Cell {
};
CellValue::String(v)
}
Cell::DynamicCellFormula { .. } | Cell::CellFormula { .. } => {
CellValue::String("#ERROR!".to_string())
}
Cell::DynamicCellFormulaBoolean { v, .. } | Cell::CellFormulaBoolean { v, .. } => {
CellValue::Boolean(*v)
}
Cell::DynamicCellFormulaNumber { v, .. } | Cell::CellFormulaNumber { v, .. } => {
CellValue::Number(*v)
}
Cell::DynamicCellFormulaString { v, .. } | Cell::CellFormulaString { v, .. } => {
CellValue::String(v.clone())
}
Cell::DynamicCellFormulaError { ei, .. } | Cell::CellFormulaError { ei, .. } => {
Cell::CellFormula { .. } => CellValue::String("#ERROR!".to_string()),
Cell::CellFormulaBoolean { v, .. } => CellValue::Boolean(*v),
Cell::CellFormulaNumber { v, .. } => CellValue::Number(*v),
Cell::CellFormulaString { v, .. } => CellValue::String(v.clone()),
Cell::CellFormulaError { ei, .. } => {
let v = ei.to_localized_error_string(language);
CellValue::String(v)
}
Cell::SpillBooleanCell { v, .. } => CellValue::Boolean(*v),
Cell::SpillNumberCell { v, .. } => CellValue::Number(*v),
Cell::SpillStringCell { v, .. } => CellValue::String(v.clone()),
}
}

View File

@@ -717,7 +717,7 @@ impl Parser {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
message: format!("sheet not found: {}", context.sheet),
};
}
};
@@ -850,7 +850,7 @@ impl Parser {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
message: format!("sheet not found: {}", context.sheet),
};
}
};
@@ -878,7 +878,7 @@ impl Parser {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
message: format!("table sheet not found: {}", table.sheet_name),
};
}
};

View File

@@ -186,8 +186,7 @@ pub fn add_implicit_intersection(node: &mut Node, add: bool) {
};
}
#[derive(Clone)]
pub enum StaticResult {
pub(crate) enum StaticResult {
Scalar,
Array(i32, i32),
Range(i32, i32),
@@ -223,7 +222,7 @@ fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
// * Array(a, b) if we know it will be an a x b array.
// * Range(a, b) if we know it will be a a x b range.
// * Unknown if we cannot guaranty either
pub(crate) fn run_static_analysis_on_node(node: &Node) -> StaticResult {
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
match node {
Node::BooleanKind(_)
| Node::NumberKind(_)
@@ -342,7 +341,8 @@ fn static_analysis_offset(args: &[Node]) -> StaticResult {
}
_ => return StaticResult::Unknown,
};
StaticResult::Unknown
// Both height and width are explicitly 1, so OFFSET will return a single cell
StaticResult::Scalar
}
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
@@ -576,6 +576,37 @@ fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
}
}
// NETWORKDAYS(start_date, end_date, [holidays])
// Parameters: start_date (scalar), end_date (scalar), holidays (optional vector)
fn args_signature_networkdays(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
// NETWORKDAYS.INTL(start_date, end_date, [weekend], [holidays])
// Parameters: start_date (scalar), end_date (scalar), weekend (optional scalar), holidays (optional vector)
fn args_signature_networkdays_intl(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
Signature::Vector,
]
} else {
vec![Signature::Error; arg_count]
}
}
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
// 1. When computing the function
// 2. Checking the arguments to see if we need to insert the implicit intersection operator
@@ -691,13 +722,28 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Maxifs => vec![Signature::Vector; arg_count],
Function::Minifs => vec![Signature::Vector; arg_count],
Function::Date => args_signature_scalars(arg_count, 3, 0),
Function::Datedif => args_signature_scalars(arg_count, 3, 0),
Function::Datevalue => args_signature_scalars(arg_count, 1, 0),
Function::Day => args_signature_scalars(arg_count, 1, 0),
Function::Edate => args_signature_scalars(arg_count, 2, 0),
Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
Function::Month => args_signature_scalars(arg_count, 1, 0),
Function::Time => args_signature_scalars(arg_count, 3, 0),
Function::Timevalue => args_signature_scalars(arg_count, 1, 0),
Function::Hour => args_signature_scalars(arg_count, 1, 0),
Function::Minute => args_signature_scalars(arg_count, 1, 0),
Function::Second => args_signature_scalars(arg_count, 1, 0),
Function::Now => args_signature_no_args(arg_count),
Function::Today => args_signature_no_args(arg_count),
Function::Year => args_signature_scalars(arg_count, 1, 0),
Function::Days => args_signature_scalars(arg_count, 2, 0),
Function::Days360 => args_signature_scalars(arg_count, 2, 1),
Function::Weekday => args_signature_scalars(arg_count, 1, 1),
Function::Weeknum => args_signature_scalars(arg_count, 1, 1),
Function::Workday => args_signature_scalars(arg_count, 2, 1),
Function::WorkdayIntl => args_signature_scalars(arg_count, 2, 2),
Function::Yearfrac => args_signature_scalars(arg_count, 2, 1),
Function::Isoweeknum => args_signature_scalars(arg_count, 1, 0),
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
Function::Db => args_signature_scalars(arg_count, 4, 1),
@@ -786,6 +832,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
Function::Unicode => args_signature_scalars(arg_count, 1, 0),
Function::Geomean => vec![Signature::Vector; arg_count],
Function::Networkdays => args_signature_networkdays(arg_count),
Function::NetworkdaysIntl => args_signature_networkdays_intl(arg_count),
}
}
@@ -897,12 +945,27 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Maxifs => not_implemented(args),
Function::Minifs => not_implemented(args),
Function::Date => not_implemented(args),
Function::Datedif => not_implemented(args),
Function::Datevalue => not_implemented(args),
Function::Day => not_implemented(args),
Function::Edate => not_implemented(args),
Function::Month => not_implemented(args),
Function::Time => not_implemented(args),
Function::Timevalue => not_implemented(args),
Function::Hour => not_implemented(args),
Function::Minute => not_implemented(args),
Function::Second => not_implemented(args),
Function::Now => not_implemented(args),
Function::Today => not_implemented(args),
Function::Year => not_implemented(args),
Function::Days => not_implemented(args),
Function::Days360 => not_implemented(args),
Function::Weekday => not_implemented(args),
Function::Weeknum => not_implemented(args),
Function::Workday => not_implemented(args),
Function::WorkdayIntl => not_implemented(args),
Function::Yearfrac => not_implemented(args),
Function::Isoweeknum => not_implemented(args),
Function::Cumipmt => not_implemented(args),
Function::Cumprinc => not_implemented(args),
Function::Db => not_implemented(args),
@@ -991,5 +1054,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args),
Function::Networkdays => not_implemented(args),
Function::NetworkdaysIntl => not_implemented(args),
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -148,13 +148,30 @@ pub enum Function {
// Date and time
Date,
Datedif,
Datevalue,
Day,
Edate,
Eomonth,
Month,
Time,
Timevalue,
Hour,
Minute,
Second,
Now,
Today,
Year,
Networkdays,
NetworkdaysIntl,
Days,
Days360,
Weekday,
Weeknum,
Workday,
WorkdayIntl,
Yearfrac,
Isoweeknum,
// Financial
Cumipmt,
@@ -253,7 +270,7 @@ pub enum Function {
}
impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 215> {
[
Function::And,
Function::False,
@@ -362,9 +379,26 @@ impl Function {
Function::Month,
Function::Eomonth,
Function::Date,
Function::Datedif,
Function::Datevalue,
Function::Edate,
Function::Networkdays,
Function::NetworkdaysIntl,
Function::Time,
Function::Timevalue,
Function::Hour,
Function::Minute,
Function::Second,
Function::Today,
Function::Now,
Function::Days,
Function::Days360,
Function::Weekday,
Function::Weeknum,
Function::Workday,
Function::WorkdayIntl,
Function::Yearfrac,
Function::Isoweeknum,
Function::Pmt,
Function::Pv,
Function::Rate,
@@ -494,6 +528,7 @@ impl Function {
Function::Isformula => "_xlfn.ISFORMULA".to_string(),
Function::Sheet => "_xlfn.SHEET".to_string(),
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
_ => self.to_string(),
}
}
@@ -631,9 +666,26 @@ impl Function {
"EOMONTH" => Some(Function::Eomonth),
"MONTH" => Some(Function::Month),
"DATE" => Some(Function::Date),
"DATEDIF" => Some(Function::Datedif),
"DATEVALUE" => Some(Function::Datevalue),
"EDATE" => Some(Function::Edate),
"NETWORKDAYS" => Some(Function::Networkdays),
"NETWORKDAYS.INTL" => Some(Function::NetworkdaysIntl),
"TIME" => Some(Function::Time),
"TIMEVALUE" => Some(Function::Timevalue),
"HOUR" => Some(Function::Hour),
"MINUTE" => Some(Function::Minute),
"SECOND" => Some(Function::Second),
"TODAY" => Some(Function::Today),
"NOW" => Some(Function::Now),
"DAYS" | "_XLFN.DAYS" => Some(Function::Days),
"DAYS360" => Some(Function::Days360),
"WEEKDAY" => Some(Function::Weekday),
"WEEKNUM" => Some(Function::Weeknum),
"WORKDAY" => Some(Function::Workday),
"WORKDAY.INTL" => Some(Function::WorkdayIntl),
"YEARFRAC" => Some(Function::Yearfrac),
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
// Financial
"PMT" => Some(Function::Pmt),
"PV" => Some(Function::Pv),
@@ -841,9 +893,26 @@ impl fmt::Display for Function {
Function::Month => write!(f, "MONTH"),
Function::Eomonth => write!(f, "EOMONTH"),
Function::Date => write!(f, "DATE"),
Function::Datedif => write!(f, "DATEDIF"),
Function::Datevalue => write!(f, "DATEVALUE"),
Function::Edate => write!(f, "EDATE"),
Function::Networkdays => write!(f, "NETWORKDAYS"),
Function::NetworkdaysIntl => write!(f, "NETWORKDAYS.INTL"),
Function::Time => write!(f, "TIME"),
Function::Timevalue => write!(f, "TIMEVALUE"),
Function::Hour => write!(f, "HOUR"),
Function::Minute => write!(f, "MINUTE"),
Function::Second => write!(f, "SECOND"),
Function::Today => write!(f, "TODAY"),
Function::Now => write!(f, "NOW"),
Function::Days => write!(f, "DAYS"),
Function::Days360 => write!(f, "DAYS360"),
Function::Weekday => write!(f, "WEEKDAY"),
Function::Weeknum => write!(f, "WEEKNUM"),
Function::Workday => write!(f, "WORKDAY"),
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
Function::Yearfrac => write!(f, "YEARFRAC"),
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
Function::Pmt => write!(f, "PMT"),
Function::Pv => write!(f, "PV"),
Function::Rate => write!(f, "RATE"),
@@ -1082,9 +1151,26 @@ impl Model {
Function::Eomonth => self.fn_eomonth(args, cell),
Function::Month => self.fn_month(args, cell),
Function::Date => self.fn_date(args, cell),
Function::Datedif => self.fn_datedif(args, cell),
Function::Datevalue => self.fn_datevalue(args, cell),
Function::Edate => self.fn_edate(args, cell),
Function::Networkdays => self.fn_networkdays(args, cell),
Function::NetworkdaysIntl => self.fn_networkdays_intl(args, cell),
Function::Time => self.fn_time(args, cell),
Function::Timevalue => self.fn_timevalue(args, cell),
Function::Hour => self.fn_hour(args, cell),
Function::Minute => self.fn_minute(args, cell),
Function::Second => self.fn_second(args, cell),
Function::Today => self.fn_today(args, cell),
Function::Now => self.fn_now(args, cell),
Function::Days => self.fn_days(args, cell),
Function::Days360 => self.fn_days360(args, cell),
Function::Weekday => self.fn_weekday(args, cell),
Function::Weeknum => self.fn_weeknum(args, cell),
Function::Workday => self.fn_workday(args, cell),
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
Function::Yearfrac => self.fn_yearfrac(args, cell),
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
// Financial
Function::Pmt => self.fn_pmt(args, cell),
Function::Pv => self.fn_pv(args, cell),

View File

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

View File

@@ -96,7 +96,7 @@ impl Model {
match cell.get_formula() {
Some(f) => {
let node = &self.parsed_formulas[sheet_index as usize][f as usize].0;
let node = &self.parsed_formulas[sheet_index as usize][f as usize];
matches!(
node,
Node::FunctionKind {

View File

@@ -11,9 +11,8 @@ use crate::{
lexer::LexerMode,
parser::{
move_formula::{move_formula, MoveContext},
static_analysis::{run_static_analysis_on_node, StaticResult},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
ArrayNode, Node, Parser,
Node, Parser,
},
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*,
@@ -84,24 +83,6 @@ pub(crate) enum ParsedDefinedName {
InvalidDefinedNameFormula,
}
/// A support node is either a cell or a range of cells
pub(crate) enum SupportNode {
/// (sheet, row, column)
Cell((u32, i32, i32)),
/// (sheet, row, column, height, width)
Range((u32, i32, i32, u32, u32))
}
/// The state of the computation
pub(crate) enum EvaluationState {
/// the model is ready for a new evaluation
Ready,
/// the model is evaluating cells that might spill
EvaluatingSpills,
/// the model is evaluating cells normally
Evaluating
}
/// A dynamical IronCalc model.
///
/// Its is composed of a `Workbook`. Everything else are dynamical quantities:
@@ -118,13 +99,15 @@ pub struct Model {
/// A Rust internal representation of an Excel workbook
pub workbook: Workbook,
/// A list of parsed formulas
pub parsed_formulas: Vec<Vec<(Node, StaticResult)>>,
pub parsed_formulas: Vec<Vec<Node>>,
/// A list of parsed defined names
pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
/// An optimization to lookup strings faster
pub(crate) shared_strings: HashMap<String, usize>,
/// An instance of the parser
pub(crate) parser: Parser,
/// The list of cells with formulas that are evaluated or being evaluated
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The locale of the model
pub(crate) locale: Locale,
/// The language used
@@ -133,16 +116,6 @@ pub struct Model {
pub(crate) tz: Tz,
/// The view id. A view consists of a selected sheet and ranges.
pub(crate) view_id: u32,
/// ** Runtime ***
/// The list of cells with formulas that are evaluated or being evaluated
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The support graph. For a given cell (sheet, row, column) the list of cells and ranges that were requested
pub(crate) support_graph: HashMap<(u32, i32, i32), Vec<SupportNode>>,
/// If the model is in a switch state then spill cells in the indices should be switched and recalculation redone
pub(crate) switch_cells: Option<(i32, i32)>,
/// Stack of cells being evaluated
pub(crate) stack: Vec<(u32, i32, i32)>,
pub(crate) state: EvaluationState,
}
// FIXME: Maybe this should be the same as CellReference
@@ -549,203 +522,14 @@ impl Model {
}
Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row))
}
/// Sets sheet, target_row, target_column, (width, height), &v
#[allow(clippy::too_many_arguments)]
fn set_spill_cell_with_formula_value(
&mut self,
sheet: u32,
row: i32,
column: i32,
r: (i32, i32),
v: &CalcResult,
s: i32,
f: i32,
) -> Result<(), String> {
let new_cell = match v {
CalcResult::EmptyCell => Cell::DynamicCellFormulaNumber {
f,
v: 0.0,
s,
r,
a: false,
},
CalcResult::String(v) => Cell::DynamicCellFormulaString {
f,
v: v.clone(),
s,
r,
a: false,
},
CalcResult::Number(v) => Cell::DynamicCellFormulaNumber {
v: *v,
s,
r,
f,
a: false,
},
CalcResult::Boolean(b) => Cell::DynamicCellFormulaBoolean {
v: *b,
s,
r,
f,
a: false,
},
CalcResult::Error { error, .. } => Cell::DynamicCellFormulaError {
ei: error.clone(),
s,
r,
f,
a: false,
o: "".to_string(),
m: "".to_string(),
},
// These cannot happen
// FIXME: Maybe the type of get_cell_value should be different
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
Cell::DynamicCellFormulaError {
ei: Error::ERROR,
s,
r,
f,
a: false,
o: "".to_string(),
m: "".to_string(),
}
}
};
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
match sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
sheet_data.insert(row, column_data);
}
}
Ok(())
}
/// Sets a cell with a "spill" value
fn set_spill_cell_with_value(
&mut self,
sheet: u32,
row: i32,
column: i32,
m: (i32, i32),
v: &CalcResult,
) -> Result<(), String> {
let style_index = self.get_cell_style_index(sheet, row, column)?;
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
self.workbook
.styles
.get_style_without_quote_prefix(style_index)?
} else {
style_index
};
let new_cell = match v {
CalcResult::EmptyCell => Cell::SpillNumberCell {
v: 0.0,
s: style_index,
m,
},
CalcResult::String(s) => Cell::SpillStringCell {
v: s.clone(),
s: new_style_index,
m,
},
CalcResult::Number(f) => Cell::SpillNumberCell {
v: *f,
s: new_style_index,
m,
},
CalcResult::Boolean(b) => Cell::SpillBooleanCell {
v: *b,
s: new_style_index,
m,
},
CalcResult::Error { error, .. } => Cell::SpillErrorCell {
ei: error.clone(),
s: style_index,
m,
},
// These cannot happen
// FIXME: Maybe the type of get_cell_value should be different
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
Cell::SpillErrorCell {
ei: Error::ERROR,
s: style_index,
m,
}
}
};
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
match sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
sheet_data.insert(row, column_data);
}
}
Ok(())
}
/// Returns `None` if no cell has called this cell, otherwise returns the dependent cell
fn get_support_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Option<&Cell>, String> {
self.workbook.supporting_cells.get(&(sheet, row, column)).map(|c| Some(c)).ok_or_else(|| "Cell not found".into())
}
/// Sets `result` in the cell given by `sheet` sheet index, row and column
/// Note that will panic if the cell does not exist
/// It will do nothing if the cell does not have a formula
/// If the cell is an array or a range it will check if it is possible to spill to other cells
/// if it is not it will return an error.
/// Then it will check if any of the cells has been requested before.
#[allow(clippy::expect_used)]
fn set_cell_value(
&mut self,
cell_reference: CellReferenceIndex,
result: &CalcResult,
) -> Result<(), String> {
fn set_cell_value(&mut self, cell_reference: CellReferenceIndex, result: &CalcResult) {
let CellReferenceIndex { sheet, column, row } = cell_reference;
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column];
let s = cell.get_style();
// If the cell is a dynamic cell we need to delete all the cells in the range
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
// skip the "mother" cell
if r == row && c == column {
continue;
}
self.cell_clear_contents(sheet, r, c)?;
}
}
}
if let Some(f) = cell.get_formula() {
match result {
CalcResult::Number(value) => {
@@ -810,145 +594,19 @@ impl Model {
ei: error.clone(),
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
}
CalcResult::Range { left, right } => {
if left.sheet == right.sheet
&& left.row == right.row
&& left.column == right.column
{
// There is only one cell
let single_cell = CellReferenceIndex {
let intersection_cell = CellReferenceIndex {
sheet: left.sheet,
column: left.column,
row: left.row,
};
let v = self.evaluate_cell(single_cell);
self.set_cell_value(cell_reference, &v)?;
let v = self.evaluate_cell(intersection_cell);
self.set_cell_value(cell_reference, &v);
} else {
// We need to check if all the cells are empty, otherwise we mark the cell as #SPILL!
let mut all_empty = true;
for r in row..=row + right.row - left.row {
for c in column..=column + right.column - left.column {
if r == row && c == column {
// skip the "mother" cell
continue;
}
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
all_empty = false;
break;
}
if let Some(support) = self.get_support_cell(sheet, r, c) {
all_empty = false;
}
}
if !all_empty {
break;
}
}
if !all_empty {
let o = match self.cell_reference_to_string(&cell_reference) {
Ok(s) => s,
Err(_) => "".to_string(),
};
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::DynamicCellFormulaError {
f,
s,
o,
m: "Result would spill to non empty cells".to_string(),
ei: Error::SPILL,
r: (1, 1),
a: false,
};
return Ok(());
}
// evaluate all the cells in that range
for r in left.row..=right.row {
for c in left.column..=right.column {
let cell_reference = CellReferenceIndex {
sheet: left.sheet,
row: r,
column: c,
};
// FIXME: We ned to return an error
self.evaluate_cell(cell_reference);
}
}
// now write the result in the target
for r in left.row..=right.row {
let row_delta = r - left.row;
for c in left.column..=right.column {
let column_delta = c - left.column;
// We need to put whatever is in (left.sheet, r, c) in
// (sheet, row + row_delta, column + column_delta)
// But we need to preserve the style
let target_row = row + row_delta;
let target_column = column + column_delta;
let cell = self
.workbook
.worksheet(left.sheet)?
.cell(r, c)
.cloned()
.unwrap_or_default();
let cell_reference = CellReferenceIndex {
sheet: left.sheet,
row: r,
column: c,
};
let v = self.get_cell_value(&cell, cell_reference);
if row == target_row && column == target_column {
// let cell_reference = CellReferenceIndex { sheet, row, column };
// self.set_cell_value(cell_reference, &v);
self.set_spill_cell_with_formula_value(
sheet,
target_row,
target_column,
(right.column - left.column + 1, right.row - left.row + 1),
&v,
s,
f,
)?;
continue;
}
self.set_spill_cell_with_value(
sheet,
target_row,
target_column,
(row, column),
&v,
)?;
}
}
}
}
CalcResult::Array(array) => {
let width = array[0].len() as i32;
let height = array.len() as i32;
// First we check that we don't spill:
let mut all_empty = true;
for r in row..row + height {
for c in column..column + width {
if r == row && c == column {
continue;
}
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
all_empty = false;
break;
}
}
}
if !all_empty {
let o = match self.cell_reference_to_string(&cell_reference) {
Ok(s) => s,
Err(_) => "".to_string(),
@@ -962,65 +620,57 @@ impl Model {
f,
s,
o,
m: "Result would spill to non empty cells".to_string(),
ei: Error::SPILL,
m: "Implicit Intersection not implemented".to_string(),
ei: Error::NIMPL,
};
return Ok(());
}
let mut target_row = row;
for data_row in array {
let mut target_column = column;
for value in data_row {
if row == target_row && column == target_column {
// This is the root cell of the dynamic array
let cell_reference = CellReferenceIndex { sheet, row, column };
let v = match value {
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
ArrayNode::Number(f) => CalcResult::Number(*f),
ArrayNode::String(s) => CalcResult::String(s.clone()),
ArrayNode::Error(error) => CalcResult::new_error(
error.clone(),
cell_reference,
error.to_localized_error_string(&self.language),
),
};
self.set_spill_cell_with_formula_value(
sheet,
target_row,
target_column,
(width, height),
&v,
s,
f,
)?;
target_column += 1;
continue;
}
let v = match value {
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
ArrayNode::Number(f) => CalcResult::Number(*f),
ArrayNode::String(s) => CalcResult::String(s.clone()),
ArrayNode::Error(error) => CalcResult::new_error(
error.clone(),
cell_reference,
error.to_localized_error_string(&self.language),
),
};
self.set_spill_cell_with_value(
sheet,
target_row,
target_column,
(row, column),
&v,
)?;
target_column += 1;
}
target_row += 1;
}
// if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
// {
// let v = self.evaluate_cell(intersection_cell);
// self.set_cell_value(cell_reference, &v);
// } else {
// let o = match self.cell_reference_to_string(&cell_reference) {
// Ok(s) => s,
// Err(_) => "".to_string(),
// };
// *self.workbook.worksheets[sheet as usize]
// .sheet_data
// .get_mut(&row)
// .expect("expected a row")
// .get_mut(&column)
// .expect("expected a column") = Cell::CellFormulaError {
// f,
// s,
// o,
// m: "Invalid reference".to_string(),
// ei: Error::VALUE,
// };
// }
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
}
CalcResult::Array(_) => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaError {
f,
s,
o: "".to_string(),
m: "Arrays not supported yet".to_string(),
ei: Error::NIMPL,
};
}
}
}
Ok(())
}
/// Sets the color of the sheet tab.
@@ -1064,18 +714,16 @@ impl Model {
Ok(())
}
// EmptyCell, Boolean, Number, String, Error
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
use Cell::*;
match cell {
EmptyCell { .. } => CalcResult::EmptyCell,
BooleanCell { v, .. } | SpillBooleanCell { v, .. } => CalcResult::Boolean(*v),
NumberCell { v, .. } | SpillNumberCell { v, .. } => CalcResult::Number(*v),
ErrorCell { ei, .. } | SpillErrorCell { ei, .. } => {
BooleanCell { v, .. } => CalcResult::Boolean(*v),
NumberCell { v, .. } => CalcResult::Number(*v),
ErrorCell { ei, .. } => {
let message = ei.to_localized_error_string(&self.language);
CalcResult::new_error(ei.clone(), cell_reference, message)
}
SpillStringCell { v, .. } => CalcResult::String(v.clone()),
SharedString { si, .. } => {
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
CalcResult::String(s.clone())
@@ -1084,21 +732,15 @@ impl Model {
CalcResult::new_error(Error::ERROR, cell_reference, message)
}
}
DynamicCellFormula { .. } | CellFormula { .. } => CalcResult::Error {
CellFormula { .. } => CalcResult::Error {
error: Error::ERROR,
origin: cell_reference,
message: "Unevaluated formula".to_string(),
},
DynamicCellFormulaBoolean { v, .. } | CellFormulaBoolean { v, .. } => {
CalcResult::Boolean(*v)
}
DynamicCellFormulaNumber { v, .. } | CellFormulaNumber { v, .. } => {
CalcResult::Number(*v)
}
DynamicCellFormulaString { v, .. } | CellFormulaString { v, .. } => {
CalcResult::String(v.clone())
}
DynamicCellFormulaError { ei, o, m, .. } | CellFormulaError { ei, o, m, .. } => {
CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v),
CellFormulaNumber { v, .. } => CalcResult::Number(*v),
CellFormulaString { v, .. } => CalcResult::String(v.clone()),
CellFormulaError { ei, o, m, .. } => {
if let Some(cell_reference) = self.parse_reference(o) {
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
} else {
@@ -1130,8 +772,6 @@ impl Model {
self.workbook.worksheet(sheet)?.is_empty_cell(row, column)
}
/// Evaluates the cell. After the evaluation is done puts the value in the cell and other cells if it spills.
/// If when writing a spill cell encounter a cell whose value has been requested marks the model as "dirty"
pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult {
let row_data = match self.workbook.worksheets[cell_reference.sheet as usize]
.sheet_data
@@ -1170,10 +810,9 @@ impl Model {
self.cells.insert(key, CellState::Evaluating);
}
}
let (node, _static_result) =
&self.parsed_formulas[cell_reference.sheet as usize][f as usize];
let result = self.evaluate_node_in_context(&node.clone(), cell_reference);
let _ = self.set_cell_value(cell_reference, &result);
let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone();
let result = self.evaluate_node_in_context(node, cell_reference);
self.set_cell_value(cell_reference, &result);
// mark cell as evaluated
self.cells.insert(key, CellState::Evaluated);
result
@@ -1283,10 +922,6 @@ impl Model {
locale,
tz,
view_id: 0,
support_graph: HashMap::new(),
switch_cells: None,
stack: Vec::new(),
state: EvaluationState::Ready,
};
model.parse_formulas();
@@ -1465,8 +1100,7 @@ impl Model {
Some(cell) => match cell.get_formula() {
None => cell.get_text(&self.workbook.shared_strings, &self.language),
Some(i) => {
let (formula, static_result) =
&self.parsed_formulas[sheet as usize][i as usize];
let formula = &self.parsed_formulas[sheet as usize][i as usize];
let cell_ref = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row: target_row,
@@ -1569,8 +1203,7 @@ impl Model {
.get(sheet as usize)
.ok_or("missing sheet")?
.get(formula_index as usize)
.ok_or("missing formula")?
.0;
.ok_or("missing formula")?;
let cell_ref = CellReferenceRC {
sheet: worksheet.get_name(),
row,
@@ -1804,25 +1437,6 @@ impl Model {
column: i32,
value: String,
) -> Result<(), String> {
// We need to check if the cell is part of a dynamic array
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
// If the cell is a dynamic cell we need to delete all the cells in the range
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
// skip the "mother" cell
if r == row && c == column {
continue;
}
self.cell_clear_contents(sheet, r, c)?;
}
}
}
// If value starts with "'" then we force the style to be quote_prefix
let style_index = self.get_cell_style_index(sheet, row, column)?;
if let Some(new_value) = value.strip_prefix('\'') {
@@ -1848,9 +1462,8 @@ impl Model {
self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?;
// Update the style if needed
let cell = CellReferenceIndex { sheet, row, column };
let (parsed_formula, static_result) =
self.parsed_formulas[sheet as usize][formula_index as usize].clone();
if let Some(units) = self.compute_node_units(&parsed_formula, &cell) {
let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
if let Some(units) = self.compute_node_units(parsed_formula, &cell) {
let new_style_index = self
.workbook
.styles
@@ -1858,14 +1471,6 @@ impl Model {
let style = self.workbook.styles.get_style(new_style_index)?;
self.set_cell_style(sheet, row, column, &style)?
}
match static_result {
StaticResult::Scalar => {}
StaticResult::Array(_, _)
| StaticResult::Range(_, _)
| StaticResult::Unknown => {
self.workbook.spill_cells.push((sheet, row, column));
}
}
} else {
// The list of currencies is '$', '€' and the local currency
let mut currencies = vec!["$", ""];
@@ -1939,7 +1544,6 @@ impl Model {
_ => parsed_formula = new_parsed_formula,
}
}
let static_result = run_static_analysis_on_node(&parsed_formula);
let s = to_rc_format(&parsed_formula);
let mut formula_index: i32 = -1;
@@ -1948,7 +1552,7 @@ impl Model {
}
if formula_index == -1 {
shared_formulas.push(s);
self.parsed_formulas[sheet as usize].push((parsed_formula, static_result));
self.parsed_formulas[sheet as usize].push(parsed_formula);
formula_index = (shared_formulas.len() as i32) - 1;
}
worksheet.set_cell_with_formula(row, column, formula_index, style)?;
@@ -2143,7 +1747,7 @@ impl Model {
};
match cell.get_formula() {
Some(formula_index) => {
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize].0;
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
let cell_ref = CellReferenceRC {
sheet: worksheet.get_name(),
row,
@@ -2179,34 +1783,9 @@ impl Model {
/// Evaluates the model with a top-down recursive algorithm
pub fn evaluate(&mut self) {
// We first evaluate all the cells that might spill to other cells
let mut spills_computed = false;
self.state = EvaluationState::EvaluatingSpills;
while !spills_computed {
spills_computed = true;
// clear all computation artifacts
self.cells.clear();
// Evaluate all the cells that might spill
let spill_cells = self.workbook.spill_cells.clone();
for (sheet, row, column) in spill_cells {
self.evaluate_cell(CellReferenceIndex { sheet, row, column });
if self.switch_cells.is_some() {
spills_computed = false;
break;
}
}
if let Some((index1, index2)) = self.switch_cells {
spills_computed = false;
// switch the cells indices in the spill_cells
let cell1 = self.workbook.spill_cells[index1 as usize];
let cell2 = self.workbook.spill_cells[index2 as usize];
self.workbook.spill_cells[index1 as usize] = cell2;
self.workbook.spill_cells[index2 as usize] = cell1;
}
}
self.state = EvaluationState::Evaluating;
// clear all computation artifacts
self.cells.clear();
// Now we compute all the rest
let cells = self.get_all_cells();
for cell in cells {
@@ -2216,7 +1795,6 @@ impl Model {
column: cell.column,
});
}
self.state = EvaluationState::Ready;
}
/// Removes the content of the cell but leaves the style.
@@ -2240,22 +1818,9 @@ impl Model {
/// # }
/// ```
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
// If it has a spill formula we need to delete the contents of all the spilled cells
let worksheet = self.workbook.worksheet_mut(sheet)?;
if let Some(cell) = worksheet.cell(row, column) {
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
if row == r && column == c {
// we skip the root cell
continue;
}
worksheet.cell_clear_contents(r, c)?;
}
}
}
}
worksheet.cell_clear_contents(row, column)?;
self.workbook
.worksheet_mut(sheet)?
.cell_clear_contents(row, column)?;
Ok(())
}
@@ -2280,18 +1845,6 @@ impl Model {
/// # }
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
// Delete the contents of spilled cells if any
if let Some(cell) = worksheet.cell(row, column) {
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
if row == r && c == column {
worksheet.cell_clear_contents(r, c)?;
}
}
}
}
}
let sheet_data = &mut worksheet.sheet_data;
if let Some(row_data) = sheet_data.get_mut(&row) {

View File

@@ -8,7 +8,6 @@ use crate::{
expressions::{
lexer::LexerMode,
parser::{
static_analysis::run_static_analysis_on_node,
stringify::{rename_sheet_in_node, to_rc_format, to_string},
Parser,
},
@@ -16,7 +15,7 @@ use crate::{
},
language::get_language,
locale::get_locale,
model::{get_milliseconds_since_epoch, EvaluationState, Model, ParsedDefinedName},
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{
DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
WorksheetView,
@@ -95,8 +94,7 @@ impl Model {
let mut parse_formula = Vec::new();
for formula in shared_formulas {
let t = self.parser.parse(formula, &cell_reference);
let static_result = run_static_analysis_on_node(&t);
parse_formula.push((t, static_result));
parse_formula.push(t);
}
self.parsed_formulas.push(parse_formula);
}
@@ -407,7 +405,6 @@ impl Model {
},
tables: HashMap::new(),
views,
spill_cells: Vec::new(),
};
let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets;
@@ -430,10 +427,6 @@ impl Model {
language,
tz,
view_id: 0,
support_graph: HashMap::new(),
switch_cells: None,
stack: Vec::new(),
state: EvaluationState::Ready,
};
model.parse_formulas();
Ok(model)

View File

@@ -7,6 +7,8 @@ mod test_column_width;
mod test_criteria;
mod test_currency;
mod test_date_and_time;
mod test_datedif_leap_month_end;
mod test_days360_month_end;
mod test_error_propagation;
mod test_fn_average;
mod test_fn_averageifs;
@@ -27,6 +29,7 @@ mod test_fn_sum;
mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_fn_time;
mod test_fn_unicode;
mod test_frozen_rows_columns;
mod test_general;
@@ -43,8 +46,11 @@ mod test_sheets;
mod test_styles;
mod test_trigonometric;
mod test_true_false;
mod test_weekday_return_types;
mod test_weeknum_return_types;
mod test_workbook;
mod test_worksheet;
mod test_yearfrac_basis;
pub(crate) mod util;
mod engineering;
@@ -52,7 +58,6 @@ mod test_fn_offset;
mod test_number_format;
mod test_arrays;
mod test_dynamic_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_fv;
@@ -66,6 +71,7 @@ mod test_issue_155;
mod test_ln;
mod test_log;
mod test_log10;
mod test_networkdays;
mod test_percentage;
mod test_set_functions_error_handling;
mod test_today;

View File

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

View File

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,43 @@
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,50 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn they_spill() {
let mut model = new_empty_model();
model._set("A1", "42");
model._set("A2", "5");
model._set("A3", "7");
model._set("B1", "=A1:A3");
model.evaluate();
assert_eq!(model._get_text("B1"), *"42");
assert_eq!(model._get_text("B2"), *"5");
assert_eq!(model._get_text("B3"), *"7");
}
#[test]
fn spill_error() {
let mut model = new_empty_model();
model._set("A1", "42");
model._set("A2", "5");
model._set("A3", "7");
model._set("B1", "=A1:A3");
model._set("B2", "4");
model.evaluate();
assert_eq!(model._get_text("B1"), *"#SPILL!");
assert_eq!(model._get_text("B2"), *"4");
assert_eq!(model._get_text("B3"), *"");
}
#[test]
fn second_evaluation() {
let mut model = new_empty_model();
model._set("C3", "={1,2,3}");
model.evaluate();
assert_eq!(model._get_text("D3"), "2");
model._set("D8", "23");
model.evaluate();
assert_eq!(model._get_text("D3"), "2");
}

View File

@@ -0,0 +1,182 @@
#![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

@@ -91,12 +91,12 @@ fn fn_or_xor() {
model._set("A10", &format!("={func}(X99:Z99"));
// Reference to cell with reference to empty range
model._set("B11", "=@X99:Z99");
model._set("B11", "=X99:Z99");
model._set("A11", &format!("={func}(B11)"));
// Reference to cell with non-empty range
model._set("X12", "1");
model._set("B12", "=@X12:Z12");
model._set("B12", "=X12:Z12");
model._set("A12", &format!("={func}(B12)"));
// Reference to text cell

View File

@@ -0,0 +1,520 @@
#![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

@@ -3,7 +3,7 @@
use crate::test::util::new_empty_model;
#[test]
fn simple_column() {
fn simple_colum() {
let mut model = new_empty_model();
// We populate cells A1 to A3
model._set("A1", "1");
@@ -30,7 +30,7 @@ fn return_of_array_is_n_impl() {
model.evaluate();
assert_eq!(model._get_text("C2"), "1".to_string());
assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string());
assert_eq!(model._get_text("D2"), "1.89188842".to_string());
}

View File

@@ -0,0 +1,347 @@
#![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

@@ -0,0 +1,26 @@
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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,60 @@
#![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

@@ -6,10 +6,8 @@ mod test_border;
mod test_clear_cells;
mod test_column_style;
mod test_defined_names;
mod test_delete_evaluates;
mod test_delete_row_column_formatting;
mod test_diff_queue;
mod test_dynamic_array;
mod test_evaluation;
mod test_general;
mod test_grid_lines;

View File

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

View File

@@ -1,47 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::{expressions::types::Area, UserModel};
#[test]
fn clear_cell_contents_evaluates() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("42".to_string())
);
model
.range_clear_contents(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("0".to_string()));
}
#[test]
fn clear_cell_all_evaluates() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("42".to_string())
);
model
.range_clear_all(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("0".to_string()));
}

View File

@@ -1,130 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::{expressions::types::Area, UserModel};
// Tests basic behavour.
#[test]
fn basic() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
// We put a value by the dynamic array to check the border conditions
model.set_user_input(0, 2, 1, "22").unwrap();
model.set_user_input(0, 1, 1, "={34,35,3}").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 1),
Ok("34".to_string())
);
}
// Test that overwriting a dynamic array with a single value dissolves the array
#[test]
fn sett_user_input_mother() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "={34,35,3}").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("35".to_string())
);
model.set_user_input(0, 1, 1, "123").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("".to_string()));
}
#[test]
fn set_user_input_sibling() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "={43,55,34}").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("55".to_string())
);
// This does nothing
model.set_user_input(0, 1, 2, "123").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("55".to_string())
);
}
#[test]
fn basic_undo_redo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "={34,35,3}").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("35".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("".to_string()));
model.redo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("35".to_string())
);
}
#[test]
fn mixed_spills() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
// D9 => ={1,2,3}
model.set_user_input(0, 9, 4, "={34,35,3}").unwrap();
// F6 => ={1;2;3;4}
model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap();
// F6 should be #SPILL!
assert_eq!(
model.get_formatted_cell_value(0, 6, 6),
Ok("#SPILL!".to_string())
);
// We delete D9
model
.range_clear_contents(&Area {
sheet: 0,
row: 9,
column: 4,
width: 1,
height: 1,
})
.unwrap();
// F6 should be 1
assert_eq!(model.get_formatted_cell_value(0, 6, 6), Ok("1".to_string()));
// Now we undo that
model.undo().unwrap();
// F6 should be #SPILL!
assert_eq!(
model.get_formatted_cell_value(0, 6, 6),
Ok("#SPILL!".to_string())
);
}
#[test]
fn spill_order_d9_f6() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
// D9 => ={1,2,3}
model.set_user_input(0, 9, 4, "={34,35,3}").unwrap();
// F6 => ={1;2;3;4}
model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap();
// F6 should be #SPILL!
assert_eq!(
model.get_formatted_cell_value(0, 6, 6),
Ok("#SPILL!".to_string())
);
}
#[test]
fn spill_order_f6_d9() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
// F6 => ={1;2;3;4}
model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap();
// D9 => ={1,2,3}
model.set_user_input(0, 9, 4, "={34,35,3}").unwrap();
// D9 should be #SPILL!
assert_eq!(
model.get_formatted_cell_value(0, 9, 4),
Ok("#SPILL!".to_string())
);
}

View File

@@ -51,8 +51,6 @@ pub struct Workbook {
pub metadata: Metadata,
pub tables: HashMap<String, Table>,
pub views: HashMap<u32, WorkbookView>,
/// The list of cells that spill in the order of evaluation
pub spill_cells: Vec<(u32, i32, i32)>,
}
/// A defined name. The `sheet_id` is the sheet index in case the name is local
@@ -161,17 +159,17 @@ pub enum CellType {
CompoundData = 128,
}
/// Cell types
/// s is always the style index of the cell
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub enum Cell {
EmptyCell {
s: i32,
},
BooleanCell {
v: bool,
s: i32,
},
NumberCell {
v: f64,
s: i32,
@@ -183,7 +181,6 @@ pub enum Cell {
},
// Always a shared string
SharedString {
// string index
si: i32,
s: i32,
},
@@ -192,11 +189,13 @@ pub enum Cell {
f: i32,
s: i32,
},
CellFormulaBoolean {
f: i32,
v: bool,
s: i32,
},
CellFormulaNumber {
f: i32,
v: f64,
@@ -208,9 +207,9 @@ pub enum Cell {
v: String,
s: i32,
},
CellFormulaError {
f: i32,
// error index
ei: Error,
s: i32,
// Origin: Sheet3!C4
@@ -218,81 +217,7 @@ pub enum Cell {
// Error Message: "Not implemented function"
m: String,
},
// All Spill/dynamic cells have a boolean, a for array, if true it is an array formula
// Spill cells point to a mother cell (row, column)
SpillNumberCell {
v: f64,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
SpillBooleanCell {
v: bool,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
SpillErrorCell {
ei: Error,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
SpillStringCell {
v: String,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
// Dynamic cell formulas have a range (width, height)
DynamicCellFormula {
f: i32,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaBoolean {
f: i32,
v: bool,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaNumber {
f: i32,
v: f64,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaString {
f: i32,
v: String,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaError {
f: i32,
ei: Error,
s: i32,
// Cell origin of the error
o: String,
// Error message in text
m: String,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
// TODO: Array formulas
}
impl Default for Cell {

View File

@@ -24,18 +24,6 @@ use crate::user_model::history::{
};
use super::border_utils::is_max_border;
#[derive(Serialize, Deserialize)]
pub enum CellArrayStructure {
// It s just a single cell
SingleCell,
// It is part o a dynamic array
// (mother_row, mother_column, width, height)
DynamicChild(i32, i32, i32, i32),
// Mother of a dynamic array (width, height)
DynamicMother(i32, i32),
}
/// Data for the clipboard
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
@@ -1755,65 +1743,6 @@ impl UserModel {
Ok(None)
}
/// Returns the geometric structure of a cell
pub fn get_cell_array_structure(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<CellArrayStructure, String> {
let cell = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
match cell {
Cell::EmptyCell { .. }
| Cell::BooleanCell { .. }
| Cell::NumberCell { .. }
| Cell::ErrorCell { .. }
| Cell::SharedString { .. }
| Cell::CellFormula { .. }
| Cell::CellFormulaBoolean { .. }
| Cell::CellFormulaNumber { .. }
| Cell::CellFormulaString { .. }
| Cell::CellFormulaError { .. } => Ok(CellArrayStructure::SingleCell),
Cell::SpillNumberCell { m, .. }
| Cell::SpillBooleanCell { m, .. }
| Cell::SpillErrorCell { m, .. }
| Cell::SpillStringCell { m, .. } => {
let (m_row, m_column) = m;
let m_cell = self
.model
.workbook
.worksheet(sheet)?
.cell(m_row, m_column)
.cloned()
.unwrap_or_default();
let (width, height) = match m_cell {
Cell::DynamicCellFormula { r, .. }
| Cell::DynamicCellFormulaBoolean { r, .. }
| Cell::DynamicCellFormulaNumber { r, .. }
| Cell::DynamicCellFormulaString { r, .. }
| Cell::DynamicCellFormulaError { r, .. } => (r.0, r.1),
_ => return Err("Invalid structure".to_string()),
};
Ok(CellArrayStructure::DynamicChild(
m_row, m_column, width, height,
))
}
Cell::DynamicCellFormula { r, .. }
| Cell::DynamicCellFormulaBoolean { r, .. }
| Cell::DynamicCellFormulaNumber { r, .. }
| Cell::DynamicCellFormulaString { r, .. }
| Cell::DynamicCellFormulaError { r, .. } => {
Ok(CellArrayStructure::DynamicMother(r.0, r.1))
}
}
}
/// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view();
@@ -2116,24 +2045,6 @@ impl UserModel {
old_value,
} => {
needs_evaluation = true;
let cell = self
.model
.workbook
.worksheet(*sheet)?
.cell(*row, *column)
.cloned()
.unwrap_or_default();
if let Some((width, height)) = cell.get_dynamic_range() {
for r in *row..*row + height {
for c in *column..*column + width {
// skip the "mother" cell
if r == *row && c == *column {
continue;
}
self.model.cell_clear_contents(*sheet, r, c)?;
}
}
}
match *old_value.clone() {
Some(value) => {
self.model

View File

@@ -5,21 +5,18 @@ use bitcode::{Decode, Encode};
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
#[derive(Clone, Encode, Decode)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(crate) struct RowData {
pub(crate) row: Option<Row>,
pub(crate) data: HashMap<i32, Cell>,
}
#[derive(Clone, Encode, Decode)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(crate) struct ColumnData {
pub(crate) column: Option<Col>,
pub(crate) data: HashMap<i32, Cell>,
}
#[derive(Clone, Encode, Decode)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(crate) enum Diff {
// Cell diffs
SetCellValue {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -766,21 +766,4 @@ impl Model {
.get_first_non_empty_in_row_after_column(sheet, row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(
js_name = "getCellArrayStructure",
unchecked_return_type = "CellArrayStructure"
)]
pub fn get_cell_array_structure(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<JsValue, JsError> {
let cell_structure = self
.model
.get_cell_array_structure(sheet, row, column)
.map_err(|e| to_js_error(e.to_string()))?;
serde_wasm_bindgen::to_value(&cell_structure).map_err(JsError::from)
}
}

View File

@@ -109,11 +109,6 @@ export interface MarkedToken {
end: number;
}
export type CellArrayStructure =
| "SingleCell"
| { DynamicChild: [number, number, number, number] }
| { DynamicMother: [number, number] };
export interface WorksheetProperties {
name: string;
color: string;
@@ -221,7 +216,7 @@ export interface SelectedView {
// };
// type ClipboardData = Record<string, Record <string, ClipboardCell>>;
type ClipboardData = Map<number, Map<number, ClipboardCell>>;
type ClipboardData = Map<number, Map <number, ClipboardCell>>;
export interface ClipboardCell {
text: string;
@@ -238,4 +233,4 @@ export interface DefinedName {
name: string;
scope?: number;
formula: string;
}
}

12
docker-compose.yml Normal file
View File

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

View File

@@ -66,4 +66,8 @@ Using IronCalc, a complex number is a string of the form "1+j3".
"#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 |
| ---------------- | ---------------------------------------------- | ------------- |
| DATE | <Badge type="tip" text="Available" /> | |
| DATEDIF | <Badge type="info" text="Not implemented yet" /> | |
| DATEVALUE | <Badge type="info" text="Not implemented yet" /> | |
| DATEDIF | <Badge type="tip" text="Available" /> | [DATEDIF](date_and_time/datedif) |
| DATEVALUE | <Badge type="tip" text="Available" /> | [DATEVALUE](date_and_time/datevalue) |
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
| DAYS | <Badge type="info" text="Not implemented yet" /> | |
| DAYS360 | <Badge type="info" text="Not implemented yet" /> | |
| DAYS | <Badge type="tip" text="Available" /> | |
| DAYS360 | <Badge type="tip" text="Available" /> | |
| EDATE | <Badge type="tip" text="Available" /> | |
| EOMONTH | <Badge type="tip" text="Available" /> | |
| HOUR | <Badge type="info" text="Not implemented yet" /> | |
| ISOWEEKNUM | <Badge type="info" text="Not implemented yet" /> | |
| MINUTE | <Badge type="info" text="Not implemented yet" /> | |
| HOUR | <Badge type="tip" text="Available" /> | [HOUR](date_and_time/hour) |
| ISOWEEKNUM | <Badge type="tip" text="Available" /> | |
| MINUTE | <Badge type="tip" text="Available" /> | [MINUTE](date_and_time/minute) |
| MONTH | <Badge type="tip" text="Available" /> | [MONTH](date_and_time/month) |
| NETWORKDAYS | <Badge type="info" text="Not implemented yet" /> | |
| NETWORKDAYS.INTL | <Badge type="info" text="Not implemented yet" /> | |
| NETWORKDAYS | <Badge type="tip" text="Available" /> | [NETWORKDAYS](date_and_time/networkdays) |
| NETWORKDAYS.INTL | <Badge type="tip" text="Available" /> | [NETWORKDAYS.INTL](date_and_time/networkdays.intl) |
| NOW | <Badge type="tip" text="Available" /> | |
| SECOND | <Badge type="info" text="Not implemented yet" /> | |
| TIME | <Badge type="info" text="Not implemented yet" /> | |
| TIMEVALUE | <Badge type="info" text="Not implemented yet" /> | |
| SECOND | <Badge type="tip" text="Available" /> | [SECOND](date_and_time/second) |
| TIME | <Badge type="tip" text="Available" /> | [TIME](date_and_time/time) |
| TIMEVALUE | <Badge type="tip" text="Available" /> | [TIMEVALUE](date_and_time/timevalue) |
| TODAY | <Badge type="tip" text="Available" /> | |
| WEEKDAY | <Badge type="info" text="Not implemented yet" /> | |
| WEEKNUM | <Badge type="info" text="Not implemented yet" /> | |
| WORKDAY | <Badge type="info" text="Not implemented yet" /> | |
| WORKDAY.INTL | <Badge type="info" text="Not implemented yet" /> | |
| WEEKDAY | <Badge type="tip" text="Available" /> | |
| WEEKNUM | <Badge type="tip" text="Available" /> | |
| WORKDAY | <Badge type="tip" text="Available" /> | |
| WORKDAY.INTL | <Badge type="tip" text="Available" /> | |
| YEAR | <Badge type="tip" text="Available" /> | [YEAR](date_and_time/year) |
| YEARFRAC | <Badge type="info" text="Not implemented yet" /> | |
| YEARFRAC | <Badge type="tip" text="Available" /> | |

View File

@@ -7,6 +7,5 @@ lang: en-US
# DATEDIF
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# DATEVALUE
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# DAYS
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# DAYS360
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

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

View File

@@ -7,6 +7,5 @@ lang: en-US
# ISOWEEKNUM
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

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

View File

@@ -4,9 +4,73 @@ outline: deep
lang: en-US
---
# NETWORKDAYS.INTL
# NETWORKDAYS.INTL function
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
**Note:** This draft page is under construction 🚧
:::
## Overview
NETWORKDAYS.INTL is a function of the Date and Time category that calculates the number of working days between two dates, with customizable weekend definitions and optionally specified holidays.
## Usage
### Syntax
**NETWORKDAYS.INTL(<span title="Number" style="color:#1E88E5">start_date</span>, <span title="Number" style="color:#1E88E5">end_date</span>, [<span title="Number or String" style="color:#4CAF50">weekend</span>], [<span title="Array" style="color:#E91E63">holidays</span>]) => <span title="Number" style="color:#1E88E5">workdays</span>**
### Argument descriptions
* *start_date* ([number](/features/value-types#numbers), required). The start date expressed as a [serial number](/features/serial-numbers.md).
* *end_date* ([number](/features/value-types#numbers), required). The end date expressed as a [serial number](/features/serial-numbers.md).
* *weekend* ([number](/features/value-types#numbers) or [string](/features/value-types#strings), optional). Defines which days are considered weekends. Default is 1 (Saturday-Sunday).
* *holidays* ([array](/features/value-types#arrays) or [range](/features/ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
### Weekend parameter options
The _weekend_ parameter can be specified in two ways:
**Numeric codes:**
- 1 (default): Saturday and Sunday
- 2: Sunday and Monday
- 3: Monday and Tuesday
- 4: Tuesday and Wednesday
- 5: Wednesday and Thursday
- 6: Thursday and Friday
- 7: Friday and Saturday
- 11: Sunday only
- 12: Monday only
- 13: Tuesday only
- 14: Wednesday only
- 15: Thursday only
- 16: Friday only
- 17: Saturday only
**String pattern:** A 7-character string of "0" and "1" where "1" indicates a weekend day. The string represents Monday through Sunday. For example, "0000011" means Saturday and Sunday are weekends.
### Additional guidance
- If the supplied _start_date_ and _end_date_ arguments have fractional parts, NETWORKDAYS.INTL uses their [floor values](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions).
- If _start_date_ is later than _end_date_, the function returns a negative number.
- Empty cells in the _holidays_ array are ignored.
- The calculation includes both the start and end dates if they are workdays.
### Returned value
NETWORKDAYS.INTL returns a [number](/features/value-types#numbers) representing the count of working days between the two dates.
### Error conditions
* In common with many other IronCalc functions, NETWORKDAYS.INTL propagates errors that are found in its arguments.
* If fewer than 2 or more than 4 arguments are supplied, then NETWORKDAYS.INTL returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the *start_date* or *end_date* arguments are not (or cannot be converted to) [numbers](/features/value-types#numbers), then NETWORKDAYS.INTL returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *start_date* or *end_date* values are outside the valid date range, then NETWORKDAYS.INTL returns the [`#NUM!`](/features/error-types.md#num) error.
* If the *weekend* parameter is an invalid numeric code or an improperly formatted string, then NETWORKDAYS.INTL returns the [`#NUM!`](/features/error-types.md#num) or [`#VALUE!`](/features/error-types.md#value) error.
* If the *holidays* array contains non-numeric values, then NETWORKDAYS.INTL returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
IronCalc utilizes Rust's [chrono](https://docs.rs/chrono/latest/chrono/) crate to implement the NETWORKDAYS.INTL function. This function provides more flexibility than NETWORKDAYS by allowing custom weekend definitions.
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=networkdays-intl).
## Links
* See also IronCalc's [NETWORKDAYS](/functions/date_and_time/networkdays.md) function for the basic version with fixed weekends.
* Visit Microsoft Excel's [NETWORKDAYS.INTL function](https://support.microsoft.com/en-us/office/networkdays-intl-function-a9b26239-4f20-46a1-9ab8-4e925bfd5e28) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093019) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/NETWORKDAYS.INTL) provide versions of the NETWORKDAYS.INTL function.

View File

@@ -4,9 +4,51 @@ outline: deep
lang: en-US
---
# NETWORKDAYS
# NETWORKDAYS function
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
**Note:** This draft page is under construction 🚧
:::
## Overview
NETWORKDAYS is a function of the Date and Time category that calculates the number of working days between two dates, excluding weekends (Saturday and Sunday by default) and optionally specified holidays.
## Usage
### Syntax
**NETWORKDAYS(<span title="Number" style="color:#1E88E5">start_date</span>, <span title="Number" style="color:#1E88E5">end_date</span>, [<span title="Array" style="color:#E91E63">holidays</span>]) => <span title="Number" style="color:#1E88E5">workdays</span>**
### Argument descriptions
* *start_date* ([number](/features/value-types#numbers), required). The start date expressed as a [serial number](/features/serial-numbers.md).
* *end_date* ([number](/features/value-types#numbers), required). The end date expressed as a [serial number](/features/serial-numbers.md).
* *holidays* ([array](/features/value-types#arrays) or [range](/features/ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
### Additional guidance
- If the supplied _start_date_ and _end_date_ arguments have fractional parts, NETWORKDAYS uses their [floor values](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions).
- If _start_date_ is later than _end_date_, the function returns a negative number.
- Weekend days are Saturday and Sunday by default. Use [NETWORKDAYS.INTL](networkdays.intl) for custom weekend definitions.
- Empty cells in the _holidays_ array are ignored.
- The calculation includes both the start and end dates if they are workdays.
### Returned value
NETWORKDAYS returns a [number](/features/value-types#numbers) representing the count of working days between the two dates.
### Error conditions
* In common with many other IronCalc functions, NETWORKDAYS propagates errors that are found in its arguments.
* If fewer than 2 or more than 3 arguments are supplied, then NETWORKDAYS returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the *start_date* or *end_date* arguments are not (or cannot be converted to) [numbers](/features/value-types#numbers), then NETWORKDAYS returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *start_date* or *end_date* values are outside the valid date range, then NETWORKDAYS returns the [`#NUM!`](/features/error-types.md#num) error.
* If the *holidays* array contains non-numeric values, then NETWORKDAYS returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
IronCalc utilizes Rust's [chrono](https://docs.rs/chrono/latest/chrono/) crate to implement the NETWORKDAYS function. The function treats Saturday and Sunday as weekend days.
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=networkdays).
## Links
* See also IronCalc's [NETWORKDAYS.INTL](/functions/date_and_time/networkdays.intl.md) function for custom weekend definitions.
* Visit Microsoft Excel's [NETWORKDAYS function](https://support.microsoft.com/en-us/office/networkdays-function-48e717bf-a7a3-495f-969e-5005e3eb18e7) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093018) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/NETWORKDAYS) provide versions of the NETWORKDAYS function.

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,5 @@ lang: en-US
# WEEKDAY
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# WEEKNUM
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# WORKDAY.INTL
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# WORKDAY
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# YEARFRAC
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -16,7 +16,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| CHOOSE | <Badge type="tip" text="Available" /> | |
| CHOOSECOLS | <Badge type="info" text="Not implemented yet" /> | |
| CHOOSEROWS | <Badge type="info" text="Not implemented yet" /> | |
| COLUMN | <Badge type="tip" text="Available" /> | |
| COLUMN | <Badge type="tip" text="Available" /> | [COLUMN](lookup_and_reference/column) |
| COLUMNS | <Badge type="tip" text="Available" /> | |
| DROP | <Badge type="info" text="Not implemented yet" /> | |
| EXPAND | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -4,8 +4,28 @@ outline: deep
lang: en-US
---
# COLUMN
::: 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).
:::
# COLUMN function
## Overview
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced Column or Cell.
## Usage
### Syntax
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
### Argument descriptions
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
### Additional guidance
* When referencing a range of cells, only the column number of the left most cell will be returned.
* You are also able to reference complete columns instead of individual cells.
### Returned value
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
### Error conditions
* IronCalc currently does not support the referencing of cells with names.
## Details
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
## Examples
### No Cell Reference
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
### With Cell Reference
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
### Range References
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
## Links

View File

@@ -13,16 +13,16 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| --------------- | ---------------------------------------------- | ------------- |
| ABS | <Badge type="tip" text="Available" /> | |
| ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) |
| ACOSH | <Badge type="tip" text="Available" /> | |
| ACOSH | <Badge type="tip" text="Available" /> | [ACOSH](math_and_trigonometry/acosh) |
| ACOT | <Badge type="info" text="Not implemented yet" /> | |
| ACOTH | <Badge type="info" text="Not implemented yet" /> | |
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | |
| ARABIC | <Badge type="info" text="Not implemented yet" /> | |
| ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) |
| ASINH | <Badge type="tip" text="Available" /> | |
| ASINH | <Badge type="tip" text="Available" /> | [ASINH](math_and_trigonometry/asinh) |
| ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) |
| ATAN2 | <Badge type="tip" text="Available" /> | |
| ATANH | <Badge type="tip" text="Available" /> | |
| ATAN2 | <Badge type="tip" text="Available" /> | [ATAN2](math_and_trigonometry/atan2) |
| ATANH | <Badge type="tip" text="Available" /> | [ATANH](math_and_trigonometry/atanh) |
| BASE | <Badge type="info" text="Not implemented yet" /> | |
| CEILING | <Badge type="info" text="Not implemented yet" /> | |
| CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | |
@@ -49,9 +49,9 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | |
| LCM | <Badge type="info" text="Not implemented yet" /> | |
| LET | <Badge type="info" text="Not implemented yet" /> | |
| LN | <Badge type="info" text="Not implemented yet" /> | |
| LOG | <Badge type="info" text="Not implemented yet" /> | |
| LOG10 | <Badge type="info" text="Not implemented yet" /> | |
| LN | <Badge type="info" text="Available" /> | |
| LOG | <Badge type="info" text="Available" /> | |
| LOG10 | <Badge type="info" text="Available" /> | |
| MDETERM | <Badge type="info" text="Not implemented yet" /> | |
| MINVERSE | <Badge type="info" text="Not implemented yet" /> | |
| MMULT | <Badge type="info" text="Not implemented yet" /> | |
@@ -80,11 +80,11 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
| SQRT | <Badge type="tip" text="Available" /> | |
| SQRTPI | <Badge type="info" text="Not implemented yet" /> | |
| SQRTPI | <Badge type="info" text="Available" /> | |
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | |
| SUM | <Badge type="tip" text="Available" /> | |
| SUMIF | <Badge type="tip" text="Available" /> | |
| SUMIFS | <Badge type="info" text="Not implemented yet" /> | |
| SUMIFS | <Badge type="info" text="Available" /> | |
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | |
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | |
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -3,9 +3,40 @@ layout: doc
outline: deep
lang: en-US
---
# ACOSH function
## Overview
ACOSH is a function of the Math and Trigonometry category that calculates the inverse hyperbolic cosine (hyperbolic arccosine) of a number, returning a non-negative value in the range [0, +∞).
## Usage
### Syntax
**ACOSH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">acosh</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The value whose hyperbolic arccosine is to be calculated. The value must be greater than or equal to 1.
# ACOSH
### Additional guidance
The hyperbolic arccosine function is defined as:
::: 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).
:::
$$
\operatorname{acosh}(x) = \ln(x + \sqrt{x^2 - 1})
$$
### Returned value
ACOSH returns a [number](/features/value-types#numbers) in the range [0, +∞) that is the hyperbolic arccosine of the specified value, expressed in radians.
### Error conditions
* In common with many other IronCalc functions, ACOSH propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ACOSH returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ACOSH returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the value of the *number* argument is less than 1, then ACOSH returns the [`#NUM!`](/features/error-types.md#num) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ACOSH function utilizes the *acosh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
* The figure below illustrates the output of the ACOSH function for values $x \geq 1$ in the range [0, +∞).
<center><img src="/functions/images/hyperbolicarccosine-curve.png" width="350" alt="Graph showing acosh(x) for x ≥ 1."></center>
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=acosh).
## Links
* For more information about inverse hyperbolic functions, visit Wikipedia's [Inverse hyperbolic functions](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) page.
* See also IronCalc's [COSH](/functions/math_and_trigonometry/cosh), [ASINH](/functions/math_and_trigonometry/asinh) and [ATANH](/functions/math_and_trigonometry/atanh) functions.
* Visit Microsoft Excel's [ACOSH function](https://support.microsoft.com/en-us/office/acosh-function-e3992cc1-103f-4e72-9f04-624b9ef5ebfe) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093391) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ACOSH) provide versions of the ACOSH function.

View File

@@ -4,8 +4,36 @@ outline: deep
lang: en-US
---
# ASINH
# ASINH function
## Overview
ASINH is a function of the Math and Trigonometry category that calculates the inverse hyperbolic sine (hyperbolic arcsine) of a number, returning the hyperbolic angle expressed in radians.
## Usage
### Syntax
**ASINH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">asinh</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The value whose inverse hyperbolic sine is to be calculated.
### Additional guidance
The hyperbolic arcsine function is defined as:
$$
\operatorname{asinh}(x) = \ln\!\left(x + \sqrt{x^2 + 1}\,\right)
$$
### Returned value
ASINH returns a real [number](/features/value-types#numbers) in the range (-∞, +∞) that is the hyperbolic arcsine of the specified value, expressed in radians.
### Error conditions
* In common with many other IronCalc functions, ASINH propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ASINH returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ASINH returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ASINH function utilizes the *asinh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
* The figure below illustrates the output of the ASINH function.
<center><img src="/functions/images/hyperbolicarcsine-curve.png" width="350" alt="Graph showing asinh(x)."></center>
::: 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).
:::
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=asinh).
## Links
* For more information about inverse hyperbolic functions, visit Wikipedia's [Inverse hyperbolic functions](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) page.
* See also IronCalc's [SINH](/functions/math_and_trigonometry/sinh), [ACOSH](/functions/math_and_trigonometry/acosh) and [ATANH](/functions/math_and_trigonometry/atanh) functions.
* Visit Microsoft Excel's [ASINH function](https://support.microsoft.com/de-de/office/asinh-function-62b4f5b6-d9cc-4c17-9d04-aa5371806c74) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093393) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ASINH) provide versions of the ASINH function.

View File

@@ -4,8 +4,34 @@ outline: deep
lang: en-US
---
# ATAN2
# ATAN2 function
## Overview
ATAN2 is a function of the Math and Trigonometry category that calculates the inverse tangent (arctangent) for the specified *x* and *y* coordinates. The arctangent returns the angle defined by the x-axis and a line defined by the origin and a point with coordinates (x,y). The returned angle is expressed in radians, in the range (-$\pi$, +$\pi$].
## Usage
### Syntax
**ATAN2(<span title="Number" style="color:#1E88E5">x,y</span>) => <span title="Number" style="color:#1E88E5">atan2</span>**
### Argument descriptions
* *x* ([number](/features/value-types#numbers), required). Value of the x coordinate.
* *y* ([number](/features/value-types#numbers), required). Value of the y coordinate.
### Additional guidance
If the returned value is positive, it represents a counterclockwise angle from the x-axis, while a negative value represents a clockwise angle.
ATAN2(x,y) is equivalent to ATAN(y/x), with the difference that the x argument in ATAN2 can be 0.
### Returned value
ATAN2 returns a number in radians in the range (-$\pi$, +$\pi$] that is the inverse tangent for the specified x and y coordinates.
### Error conditions
* In common with many other IronCalc functions, ATAN2 propagates errors that are found in its argument.
* If no argument, or arguments other than 2, are supplied, then ATAN2 returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of either the *x* or *y* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ATAN2 returns the [`#VALUE!`](/features/error-types.md#value) error.
* If both *x* and *y* are equal to 0, ATAN2 returns a [`#DIV/0!`](/features/error-types.md#div-0) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ATAN2 function utilizes the *atan2()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=atan2).
## Links
* For more information about inverse trigonometric functions, visit Wikipedia's [Inverse trigonometric functions](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) page.
* See also IronCalc's [ATAN](/functions/math_and_trigonometry/atan), [TAN](/functions/math_and_trigonometry/tan) and [ASIN](/functions/math_and_trigonometry/asin) functions.
* Visit Microsoft Excel's [ATAN2 function](https://support.microsoft.com/en-us/office/atan2-function-51123ced-348c-416a-b2e2-833f7868569f) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093468) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ATAN2) provide versions of the ATAN2 function.
::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::

View File

@@ -4,8 +4,37 @@ outline: deep
lang: en-US
---
# ATANH
# ATANH function
## Overview
ATANH is a function of the Math and Trigonometry category that calculates the inverse hyperbolic tangent (hyperbolic arctangent) of a number in the range (-1, +1), returning the hyperbolic angle expressed in radians.
## Usage
### Syntax
**ATANH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">atanh</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The value whose inverse hyperbolic tangent is to be calculated, in the range (-1,+1).
### Additional guidance
The hyperbolic arctangent function is defined as:
$$
\operatorname{atanh}(x) = \tfrac{1}{2}\,\ln\!\left(\dfrac{1+x}{1-x}\right),\quad |x| < 1
$$
### Returned value
ATANH returns a real [number](/features/value-types#numbers) in the range (-∞, +∞) that is the hyperbolic arctangent of the specified value, expressed in radians.
### Error conditions
* In common with many other IronCalc functions, ATANH propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ATANH returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ATANH returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the value of the *number* argument lies outside the domain (-1, +1), then ATANH returns the [`#NUM!`](/features/error-types.md#num) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ATANH function utilizes the *atanh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
* The figure below illustrates the output of the ATANH function.
<center><img src="/functions/images/hyperbolicarctangent-curve.png" width="350" alt="Graph showing atanh(x)."></center>
::: 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).
:::
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=atanh).
## Links
* For more information about inverse hyperbolic functions, visit Wikipedia's [Inverse hyperbolic functions](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) page.
* See also IronCalc's [ASINH](/functions/math_and_trigonometry/asinh), [ACOSH](/functions/math_and_trigonometry/acosh) and [TANH](/functions/math_and_trigonometry/tanh) functions.
* Visit Microsoft Excel's [ATANH function](https://support.microsoft.com/de-de/office/atanh-function-453534d1-76a5-4f17-8c04-c3f2feee0dd5) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093397) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ATANH) provide versions of the ATANH function.

View File

@@ -7,6 +7,5 @@ lang: en-US
# LN
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# LOG
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# LOG10
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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).
:::

View File

@@ -0,0 +1,18 @@
---
layout: doc
outline: deep
lang: en-US
---
# Managing Workbooks
All your created sheets can be accessed from the **Workbook Sidebar**. To display it, click the button in the top-left corner of the window. From there, you can:
- View all existing workbooks, ordered by date (today, last 30 days, and older)
- Create new blank workbooks
- Download the existing workbook as .xlsx
- Pin workbooks to the top of the list
- Duplicate workbooks
- Delete workbooks
This helps you keep your workbooks organized and makes sharing simple and straightforward.

View File

@@ -25,13 +25,4 @@ IronCalc makes it easy to share your files with others. Follow these steps to sh
When the URL is used to open the sheet, it displays the version of the sheet that was visible when the **"Share"** button was clicked. Please note that **any changes made to the original sheet after sharing will not be reflected in the shared version**.
- **Changes in the Shared Version**:
Any modifications made in the shared sheet (even if its an existing sheet) will **not** overwrite or affect the original sheet.
## Managing Sheets
All your created sheets can be accessed from the **"File"** section in the top-left corner of the window. From here, you can:
- View all existing sheets.
- Delete sheets as needed.
This helps you keep your files organized and makes sharing simple and straightforward.
Any modifications made in the shared sheet (even if its an existing sheet) will **not** overwrite or affect the original sheet.

View File

@@ -1,8 +1,9 @@
import { Menu, MenuItem, styled } from "@mui/material";
import { Check } from "lucide-react";
import { type ComponentProps, useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import FormatPicker from "./FormatPicker";
import { NumberFormats } from "./formatUtil";
import { KNOWN_FORMATS, NumberFormats } from "./formatUtil";
type FormatMenuProps = {
children: React.ReactNode;
@@ -26,6 +27,8 @@ const FormatMenu = (properties: FormatMenuProps) => {
[properties.onChange],
);
const isCustomFormat = !KNOWN_FORMATS.has(properties.numFmt);
return (
<>
<ChildrenWrapper
@@ -48,11 +51,17 @@ const FormatMenu = (properties: FormatMenuProps) => {
}}
>
<MenuItemWrapper onClick={(): void => onSelect(NumberFormats.AUTO)}>
<MenuItemText>{t("toolbar.format_menu.auto")}</MenuItemText>
<MenuItemText>
<CheckIcon $active={properties.numFmt === NumberFormats.AUTO} />
{t("toolbar.format_menu.auto")}
</MenuItemText>
</MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper onClick={(): void => onSelect(NumberFormats.NUMBER)}>
<MenuItemText>{t("toolbar.format_menu.number")}</MenuItemText>
<MenuItemText>
<CheckIcon $active={properties.numFmt === NumberFormats.NUMBER} />
{t("toolbar.format_menu.number")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.number_example")}
</MenuItemExample>
@@ -60,7 +69,12 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper
onClick={(): void => onSelect(NumberFormats.PERCENTAGE)}
>
<MenuItemText>{t("toolbar.format_menu.percentage")}</MenuItemText>
<MenuItemText>
<CheckIcon
$active={properties.numFmt === NumberFormats.PERCENTAGE}
/>
{t("toolbar.format_menu.percentage")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.percentage_example")}
</MenuItemExample>
@@ -70,7 +84,12 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper
onClick={(): void => onSelect(NumberFormats.CURRENCY_EUR)}
>
<MenuItemText>{t("toolbar.format_menu.currency_eur")}</MenuItemText>
<MenuItemText>
<CheckIcon
$active={properties.numFmt === NumberFormats.CURRENCY_EUR}
/>
{t("toolbar.format_menu.currency_eur")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.currency_eur_example")}
</MenuItemExample>
@@ -78,7 +97,12 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper
onClick={(): void => onSelect(NumberFormats.CURRENCY_USD)}
>
<MenuItemText>{t("toolbar.format_menu.currency_usd")}</MenuItemText>
<MenuItemText>
<CheckIcon
$active={properties.numFmt === NumberFormats.CURRENCY_USD}
/>
{t("toolbar.format_menu.currency_usd")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.currency_usd_example")}
</MenuItemExample>
@@ -86,7 +110,12 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper
onClick={(): void => onSelect(NumberFormats.CURRENCY_GBP)}
>
<MenuItemText>{t("toolbar.format_menu.currency_gbp")}</MenuItemText>
<MenuItemText>
<CheckIcon
$active={properties.numFmt === NumberFormats.CURRENCY_GBP}
/>
{t("toolbar.format_menu.currency_gbp")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.currency_gbp_example")}
</MenuItemExample>
@@ -96,7 +125,12 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper
onClick={(): void => onSelect(NumberFormats.DATE_SHORT)}
>
<MenuItemText>{t("toolbar.format_menu.date_short")}</MenuItemText>
<MenuItemText>
<CheckIcon
$active={properties.numFmt === NumberFormats.DATE_SHORT}
/>
{t("toolbar.format_menu.date_short")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.date_short_example")}
</MenuItemExample>
@@ -104,7 +138,12 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper
onClick={(): void => onSelect(NumberFormats.DATE_LONG)}
>
<MenuItemText>{t("toolbar.format_menu.date_long")}</MenuItemText>
<MenuItemText>
<CheckIcon
$active={properties.numFmt === NumberFormats.DATE_LONG}
/>
{t("toolbar.format_menu.date_long")}
</MenuItemText>
<MenuItemExample>
{t("toolbar.format_menu.date_long_example")}
</MenuItemExample>
@@ -112,7 +151,10 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuDivider />
<MenuItemWrapper onClick={(): void => setPickerOpen(true)}>
<MenuItemText>{t("toolbar.format_menu.custom")}</MenuItemText>
<MenuItemText>
<CheckIcon $active={isCustomFormat} />
{t("toolbar.format_menu.custom")}
</MenuItemText>
</MenuItemWrapper>
</StyledMenu>
<FormatPicker
@@ -139,6 +181,7 @@ const StyledMenu = styled(Menu)`
const MenuItemWrapper = styled(MenuItem)`
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
width: calc(100% - 8px);
@@ -161,8 +204,18 @@ const MenuDivider = styled("div")`
border-top: 1px solid #eeeeee;
`;
const CheckIcon = styled(Check)<{ $active: boolean }>`
width: 16px;
height: 16px;
color: ${(props) => (props.$active ? "currentColor" : "transparent")};
margin-right: 8px;
flex-shrink: 0;
`;
const MenuItemText = styled("div")`
color: #000;
display: flex;
align-items: center;
`;
const MenuItemExample = styled("div")`

View File

@@ -42,3 +42,5 @@ export enum NumberFormats {
PERCENTAGE = "0.00%",
NUMBER = "#,##0.00",
}
export const KNOWN_FORMATS = new Set<string>(Object.values(NumberFormats));

View File

@@ -13,7 +13,6 @@ import type { WorkbookState } from "../workbookState";
type FormulaBarProps = {
cellAddress: string;
formulaValue: string;
isPartOfArray: boolean;
model: Model;
workbookState: WorkbookState;
onChange: () => void;
@@ -24,7 +23,6 @@ function FormulaBar(properties: FormulaBarProps) {
const {
cellAddress,
formulaValue,
isPartOfArray,
model,
onChange,
onTextUpdated,
@@ -64,9 +62,6 @@ function FormulaBar(properties: FormulaBarProps) {
event.stopPropagation();
event.preventDefault();
}}
sx={{
color: isPartOfArray ? "grey" : "black",
}}
>
<Editor
originalText={formulaValue}
@@ -104,10 +99,9 @@ const FormulaSymbolButton = styled(StyledButton)`
const Divider = styled("div")`
background-color: ${theme.palette.grey["300"]};
width: 1px;
height: 20px;
margin-left: 16px;
margin-right: 16px;
min-width: 1px;
height: 16px;
margin: 0px 16px;
`;
const FormulaContainer = styled("div")`

File diff suppressed because it is too large Load Diff

View File

@@ -362,19 +362,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
return model.getCellContent(sheet, row, column);
};
// returns true if it is either single cell or the root cell of an array
const isRootCellOfArray = () => {
const { sheet, row, column } = model.getSelectedView();
const r = model.getCellArrayStructure(sheet, row, column);
if (r === "SingleCell") {
return false;
}
if ("DynamicMother" in r) {
return false;
}
return true;
};
const getCellStyle = useCallback(() => {
const { sheet, row, column } = model.getSelectedView();
return model.getCellStyle(sheet, row, column);
@@ -718,7 +705,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
model={model}
workbookState={workbookState}
isPartOfArray={isRootCellOfArray()}
/>
<Worksheet
model={model}

View File

@@ -14,7 +14,6 @@ import {
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
cellArrayStructureColor,
outlineBackgroundColor,
outlineColor,
} from "../WorksheetCanvas/constants";
@@ -60,7 +59,6 @@ const Worksheet = forwardRef(
const spacerElement = useRef<HTMLDivElement>(null);
const cellOutline = useRef<HTMLDivElement>(null);
const areaOutline = useRef<HTMLDivElement>(null);
const cellArrayStructure = useRef<HTMLDivElement>(null);
const extendToOutline = useRef<HTMLDivElement>(null);
const columnResizeGuide = useRef<HTMLDivElement>(null);
const rowResizeGuide = useRef<HTMLDivElement>(null);
@@ -87,7 +85,6 @@ const Worksheet = forwardRef(
const outline = cellOutline.current;
const area = areaOutline.current;
const arrayStructure = cellArrayStructure.current;
const extendTo = extendToOutline.current;
const editor = editorElement.current;
@@ -101,8 +98,7 @@ const Worksheet = forwardRef(
!area ||
!extendTo ||
!scrollElement.current ||
!editor ||
!arrayStructure
!editor
)
return;
// FIXME: This two need to be computed.
@@ -119,7 +115,6 @@ const Worksheet = forwardRef(
rowGuide: rowGuideRef,
columnHeaders: columnHeadersRef,
cellOutline: outline,
cellArrayStructure: arrayStructure,
areaOutline: area,
extendToOutline: extendTo,
editor: editor,
@@ -334,7 +329,6 @@ const Worksheet = forwardRef(
/>
</EditorWrapper>
<AreaOutline ref={areaOutline} />
<CellArrayStructure ref={cellArrayStructure} />
<ExtendToOutline ref={extendToOutline} />
<ColumnResizeGuide ref={columnResizeGuide} />
<RowResizeGuide ref={rowResizeGuide} />
@@ -520,12 +514,6 @@ const AreaOutline = styled("div")`
background-color: ${outlineBackgroundColor};
`;
const CellArrayStructure = styled("div")`
position: absolute;
border: 1px solid ${cellArrayStructureColor};
border-radius: 3px;
`;
const CellOutline = styled("div")`
position: absolute;
border: 2px solid ${outlineColor};

View File

@@ -13,7 +13,6 @@ export const defaultTextColor = "#2E414D";
export const outlineColor = "#F2994A";
export const outlineBackgroundColor = "#F2994A1A";
export const cellArrayStructureColor = "#64BDFDA1";
export const LAST_COLUMN = 16_384;
export const LAST_ROW = 1_048_576;

View File

@@ -31,7 +31,6 @@ export interface CanvasSettings {
canvas: HTMLCanvasElement;
cellOutline: HTMLDivElement;
areaOutline: HTMLDivElement;
cellArrayStructure: HTMLDivElement;
extendToOutline: HTMLDivElement;
columnGuide: HTMLDivElement;
rowGuide: HTMLDivElement;
@@ -91,8 +90,6 @@ export default class WorksheetCanvas {
cellOutlineHandle: HTMLDivElement;
cellArrayStructure: HTMLDivElement;
extendToOutline: HTMLDivElement;
workbookState: WorkbookState;
@@ -127,7 +124,6 @@ export default class WorksheetCanvas {
this.refresh = options.refresh;
this.cellOutline = options.elements.cellOutline;
this.cellArrayStructure = options.elements.cellArrayStructure;
this.areaOutline = options.elements.areaOutline;
this.extendToOutline = options.elements.extendToOutline;
this.rowGuide = options.elements.rowGuide;
@@ -1519,20 +1515,16 @@ export default class WorksheetCanvas {
}
private drawCellOutline(): void {
const { cellArrayStructure, cellOutline, areaOutline, cellOutlineHandle } =
this;
const { cellOutline, areaOutline, cellOutlineHandle } = this;
if (this.workbookState.getEditingCell()) {
cellOutline.style.visibility = "hidden";
cellOutlineHandle.style.visibility = "hidden";
areaOutline.style.visibility = "hidden";
cellArrayStructure.style.visibility = "hidden";
return;
}
cellOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";
areaOutline.style.visibility = "visible";
// This one is hidden by default
cellArrayStructure.style.visibility = "hidden";
const [selectedSheet, selectedRow, selectedColumn] =
this.model.getSelectedCell();
@@ -1588,34 +1580,6 @@ export default class WorksheetCanvas {
[handleX, handleY] = this.getCoordinatesByCell(rowStart, columnStart);
handleX += this.getColumnWidth(selectedSheet, columnStart);
handleY += this.getRowHeight(selectedSheet, rowStart);
// we draw the array structure if needed only in this case
const arrayStructure = this.model.getCellArrayStructure(
selectedSheet,
selectedRow,
selectedColumn,
);
let array = null;
if (arrayStructure === "SingleCell") {
// nothing to see here
} else if ("DynamicMother" in arrayStructure) {
cellArrayStructure.style.visibility = "visible";
const [arrayWidth, arrayHeight] = arrayStructure.DynamicMother;
array = [selectedRow, selectedColumn, arrayWidth, arrayHeight];
} else {
cellArrayStructure.style.visibility = "visible";
array = arrayStructure.DynamicChild;
}
if (array !== null) {
const [arrayX, arrayY] = this.getCoordinatesByCell(array[0], array[1]);
const [arrayX1, arrayY1] = this.getCoordinatesByCell(
array[0] + array[3],
array[1] + array[2],
);
cellArrayStructure.style.left = `${arrayX}px`;
cellArrayStructure.style.top = `${arrayY}px`;
cellArrayStructure.style.width = `${arrayX1 - arrayX}px`;
cellArrayStructure.style.height = `${arrayY1 - arrayY}px`;
}
} else {
areaOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";

View File

@@ -1,3 +1,3 @@
export const TOOLBAR_HEIGHT = 48;
export const TOOLBAR_HEIGHT = 40;
export const FORMULA_BAR_HEIGHT = 40;
export const NAVIGATION_HEIGHT = 40;

View File

@@ -18,6 +18,7 @@ import InsertRowAboveIcon from "./insert-row-above.svg?react";
import InsertRowBelow from "./insert-row-below.svg?react";
import IronCalcIcon from "./ironcalc_icon.svg?react";
import IronCalcIconWhite from "./ironcalc_icon_white.svg?react";
import IronCalcLogo from "./orange+black.svg?react";
import Fx from "./fx.svg?react";
@@ -41,6 +42,7 @@ export {
InsertRowAboveIcon,
InsertRowBelow,
IronCalcIcon,
IronCalcIconWhite,
IronCalcLogo,
Fx,
};

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.8" d="M9.95898 8.08594C9.60893 8.35318 9.27389 8.64313 8.95898 8.95801C7.09126 10.8257 6.042 13.3586 6.04199 16H6.04102V7.91406C6.39142 7.64662 6.72781 7.35715 7.04297 7.04199C8.90157 5.18307 9.9492 2.6648 9.95898 0.0371094V8.08594Z" fill="white"/>
<path opacity="0.8" d="M6.04102 7.91406C4.31493 9.23162 2.19571 9.95898 0 9.95898V6.04102C1.60208 6.04102 3.13861 5.40429 4.27148 4.27148C5.40436 3.13861 6.04101 1.60213 6.04102 0L6.04102 7.91406Z" fill="white"/>
<path opacity="0.8" d="M9.95947 8.08594C11.6856 6.76838 13.8048 6.04102 16.0005 6.04102V9.95898C14.3984 9.95898 12.8619 10.5957 11.729 11.7285C10.5961 12.8614 9.95948 14.3979 9.95947 16L9.95947 8.08594Z" fill="white"/>
<path d="M9.95898 0C9.95898 2.64126 8.90957 5.17429 7.04199 7.04199C6.727 7.35698 6.39119 7.64674 6.04102 7.91406L6.04102 0H9.95898Z" fill="white"/>
<path d="M6.04102 16C6.04102 13.3587 7.09042 10.8257 8.95801 8.95801C9.273 8.64302 9.60881 8.35326 9.95898 8.08594V16H6.04102Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,5 +1,5 @@
import init, { Model } from "@ironcalc/wasm";
import IronCalc from "./IronCalc";
import { IronCalcIcon, IronCalcLogo } from "./icons";
import { IronCalcIcon, IronCalcIconWhite, IronCalcLogo } from "./icons";
export { init, Model, IronCalc, IronCalcIcon, IronCalcLogo };
export { init, Model, IronCalc, IronCalcIcon, IronCalcIconWhite, IronCalcLogo };

View File

@@ -27,13 +27,15 @@
"vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG",
"wrap_text": "Wrap text",
"scroll_left": "Scroll left",
"scroll_right": "Scroll right",
"format_menu": {
"auto": "Auto",
"number": "Number",
"percentage": "Percentage",
"currency_eur": "Euro (EUR)",
"currency_usd": "Dollar (USD)",
"currency_gbp": "British Pound (GBD)",
"currency_gbp": "British Pound (GBP)",
"date_short": "Short date",
"date_long": "Long date",
"custom": "Custom",

View File

@@ -0,0 +1,17 @@
:2080 {
log {
output stdout
level INFO
}
@api path /api/*
handle @api {
reverse_proxy server:8000
}
handle {
root * /srv
try_files {path} /index.html
file_server
}
}

View File

@@ -2,6 +2,8 @@ import "./App.css";
import styled from "@emotion/styled";
import { useEffect, useState } from "react";
import { FileBar } from "./components/FileBar";
import LeftDrawer from "./components/LeftDrawer/LeftDrawer";
import WelcomeDialog from "./components/WelcomeDialog/WelcomeDialog";
import {
get_documentation_model,
get_model,
@@ -9,8 +11,10 @@ import {
} from "./components/rpc";
import {
createNewModel,
deleteModelByUuid,
deleteSelectedModel,
loadModelFromStorageOrCreate,
isStorageEmpty,
loadSelectedModelFromStorage,
saveModelToStorage,
saveSelectedModelInStorage,
selectModelFromStorage,
@@ -18,9 +22,15 @@ import {
// From IronCalc
import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook";
import { Modal } from "@mui/material";
import TemplatesDialog from "./components/WelcomeDialog/TemplatesDialog";
function App() {
const [model, setModel] = useState<Model | null>(null);
const [showWelcomeDialog, setShowWelcomeDialog] = useState(false);
const [isTemplatesDialogOpen, setTemplatesDialogOpen] = useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [localStorageId, setLocalStorageId] = useState<number>(1);
useEffect(() => {
async function start() {
@@ -52,8 +62,14 @@ function App() {
}
} else {
// try to load from local storage
const newModel = loadModelFromStorageOrCreate();
setModel(newModel);
const newModel = loadSelectedModelFromStorage();
if (!newModel) {
setShowWelcomeDialog(true);
const createdModel = new Model("template", "en", "UTC");
setModel(createdModel);
} else {
setModel(newModel);
}
}
}
start();
@@ -76,51 +92,157 @@ function App() {
}
}, 1000);
// Handlers for model changes that also update our models state
const handleNewModel = () => {
const newModel = createNewModel();
setModel(newModel);
};
const handleSetModel = (uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
}
};
const handleDeleteModel = () => {
const newModel = deleteSelectedModel();
if (newModel) {
setModel(newModel);
}
};
const handleDeleteModelByUuid = (uuid: string) => {
const newModel = deleteModelByUuid(uuid);
if (newModel) {
setModel(newModel);
}
};
// We could use context for model, but the problem is that it should initialized to null.
// Passing the property down makes sure it is always defined.
return (
<Wrapper>
<FileBar
model={model}
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
const blob = await uploadFile(arrayBuffer, fileName);
const bytes = new Uint8Array(await blob.arrayBuffer());
const newModel = Model.from_bytes(bytes);
saveModelToStorage(newModel);
setModel(newModel);
}}
newModel={() => {
setModel(createNewModel());
}}
setModel={(uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
}
}}
onDelete={() => {
const newModel = deleteSelectedModel();
if (newModel) {
setModel(newModel);
}
}}
<LeftDrawer
open={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
newModel={handleNewModel}
setModel={handleSetModel}
onDelete={handleDeleteModelByUuid}
localStorageId={localStorageId}
/>
<IronCalc model={model} />
<MainContent isDrawerOpen={isDrawerOpen}>
{isDrawerOpen && (
<MobileOverlay onClick={() => setIsDrawerOpen(false)} />
)}
<FileBar
model={model}
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
const blob = await uploadFile(arrayBuffer, fileName);
const bytes = new Uint8Array(await blob.arrayBuffer());
const newModel = Model.from_bytes(bytes);
saveModelToStorage(newModel);
setModel(newModel);
}}
newModel={handleNewModel}
newModelFromTemplate={() => {
setTemplatesDialogOpen(true);
}}
setModel={handleSetModel}
onDelete={handleDeleteModel}
isDrawerOpen={isDrawerOpen}
setIsDrawerOpen={setIsDrawerOpen}
setLocalStorageId={setLocalStorageId}
/>
<IronCalc model={model} />
</MainContent>
{showWelcomeDialog && (
<WelcomeDialog
onClose={() => {
if (isStorageEmpty()) {
const createdModel = createNewModel();
setModel(createdModel);
}
setShowWelcomeDialog(false);
}}
onSelectTemplate={async (templateId) => {
switch (templateId) {
case "blank": {
const createdModel = createNewModel();
setModel(createdModel);
break;
}
default: {
const model_bytes = await get_documentation_model(templateId);
const importedModel = Model.from_bytes(model_bytes);
saveModelToStorage(importedModel);
setModel(importedModel);
break;
}
}
setShowWelcomeDialog(false);
}}
/>
)}
<Modal
open={isTemplatesDialogOpen}
onClose={() => setTemplatesDialogOpen(false)}
aria-labelledby="templates-dialog-title"
aria-describedby="templates-dialog-description"
>
<TemplatesDialog
onClose={() => setTemplatesDialogOpen(false)}
onSelectTemplate={async (fileName) => {
const model_bytes = await get_documentation_model(fileName);
const importedModel = Model.from_bytes(model_bytes);
saveModelToStorage(importedModel);
setModel(importedModel);
setTemplatesDialogOpen(false);
}}
/>
</Modal>
</Wrapper>
);
}
const Wrapper = styled("div")`
margin: 0px;
padding: 0px;
display: flex;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
`;
const DRAWER_WIDTH = 264;
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
display: flex;
flex-direction: column;
position: relative;
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
}
`;
const MobileOverlay = styled("div")`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
cursor: pointer;
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
display: none;
}
`;
const Loading = styled("div")`

View File

@@ -12,19 +12,9 @@ function DeleteWorkbookDialog(properties: DeleteWorkbookDialogProperties) {
const deleteButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(2px)";
}
if (deleteButtonRef.current) {
deleteButtonRef.current.focus();
}
return () => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "none";
}
};
}, []);
return (

View File

@@ -1,6 +1,7 @@
import styled from "@emotion/styled";
import type { Model } from "@ironcalc/workbook";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
import { IconButton, Tooltip } from "@mui/material";
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { useLayoutEffect, useRef, useState } from "react";
import { FileMenu } from "./FileMenu";
import { HelpMenu } from "./HelpMenu";
@@ -27,9 +28,13 @@ function useWindowWidth() {
export function FileBar(properties: {
model: Model;
newModel: () => void;
newModelFromTemplate: () => void;
setModel: (key: string) => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void;
isDrawerOpen: boolean;
setIsDrawerOpen: (open: boolean) => void;
setLocalStorageId: (updater: (id: number) => number) => void;
}) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const spacerRef = useRef<HTMLDivElement>(null);
@@ -47,28 +52,51 @@ export function FileBar(properties: {
return (
<FileBarWrapper>
<StyledDesktopLogo />
<StyledIronCalcIcon />
<Divider />
<FileMenu
newModel={properties.newModel}
setModel={properties.setModel}
onModelUpload={properties.onModelUpload}
onDownload={async () => {
const model = properties.model;
const bytes = model.toBytes();
const fileName = model.getName();
await downloadModel(bytes, fileName);
<Tooltip
title="Toggle sidebar"
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
},
}}
onDelete={properties.onDelete}
/>
<HelpMenu />
>
<DrawerButton
onClick={() => properties.setIsDrawerOpen(!properties.isDrawerOpen)}
disableRipple
>
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
</DrawerButton>
</Tooltip>
{width > 440 && (
<FileMenu
newModel={properties.newModel}
newModelFromTemplate={properties.newModelFromTemplate}
setModel={properties.setModel}
onModelUpload={properties.onModelUpload}
onDownload={async () => {
const model = properties.model;
const bytes = model.toBytes();
const fileName = model.getName();
await downloadModel(bytes, fileName);
}}
onDelete={properties.onDelete}
/>
)}
{width > 440 && <HelpMenu />}
<WorkbookTitleWrapper>
<WorkbookTitle
name={properties.model.getName()}
onNameChange={(name) => {
properties.model.setName(name);
updateNameSelectedWorkbook(properties.model, name);
properties.setLocalStorageId((id) => id + 1);
}}
maxWidth={maxTitleWidth}
/>
@@ -101,35 +129,38 @@ const Spacer = styled("div")`
flex-grow: 1;
`;
const StyledDesktopLogo = styled(IronCalcLogo)`
width: 120px;
margin-left: 12px;
@media (max-width: 769px) {
display: none;
}
`;
// const DrawerButton = styled(IconButton)<{ $isDrawerOpen: boolean }>`
// cursor: ${(props) => (props.$isDrawerOpen ? "w-resize" : "e-resize")};
const DrawerButton = styled(IconButton)`
margin-left: 8px;
height: 32px;
width: 32px;
padding: 8px;
border-radius: 4px;
const StyledIronCalcIcon = styled(IronCalcIcon)`
width: 36px;
margin-left: 10px;
@media (min-width: 769px) {
display: none;
svg {
stroke-width: 2px;
stroke: #757575;
width: 16px;
height: 16px;
}
&:hover {
background-color: #f2f2f2;
}
&:active {
background-color: #e0e0e0;
}
`;
const Divider = styled("div")`
margin: 0px 8px 0px 16px;
height: 12px;
border-left: 1px solid #e0e0e0;
`;
// The container must be relative positioned so we can position the title absolutely
const FileBarWrapper = styled("div")`
position: relative;
height: 60px;
min-height: 60px;
width: 100%;
background: #fff;
display: flex;
gap: 2px;
align-items: center;
border-bottom: 1px solid #e0e0e0;
justify-content: space-between;

View File

@@ -1,13 +1,15 @@
import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { FileDown, FileUp, Plus, Table2, Trash2 } from "lucide-react";
import { useRef, useState } from "react";
import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
import UploadFileDialog from "./UploadFileDialog";
// import TemplatesDialog from "./WelcomeDialog/TemplatesDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage";
export function FileMenu(props: {
newModel: () => void;
newModelFromTemplate: () => void;
setModel: (key: string) => void;
onDownload: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
@@ -17,42 +19,9 @@ export function FileMenu(props: {
const [isImportMenuOpen, setImportMenuOpen] = useState(false);
const anchorElement = useRef<HTMLButtonElement>(null);
const models = getModelsMetadata();
const uuids = Object.keys(models);
const selectedUuid = getSelectedUuid();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const elements = [];
for (const uuid of uuids) {
elements.push(
<MenuItemWrapper
key={uuid}
onClick={() => {
props.setModel(uuid);
setMenuOpen(false);
}}
>
<CheckIndicator>
{uuid === selectedUuid ? (
<StyledIcon>
<Check />
</StyledIcon>
) : (
""
)}
</CheckIndicator>
<MenuItemText
style={{
maxWidth: "240px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{models[uuid]}
</MenuItemText>
</MenuItemWrapper>,
);
}
return (
<>
<FileMenuWrapper
@@ -89,10 +58,17 @@ export function FileMenu(props: {
setMenuOpen(false);
}}
>
<StyledIcon>
<Plus />
</StyledIcon>
<MenuItemText>New</MenuItemText>
<Plus />
New blank workbook
</MenuItemWrapper>
<MenuItemWrapper
onClick={() => {
props.newModelFromTemplate();
setMenuOpen(false);
}}
>
<Table2 />
New from template
</MenuItemWrapper>
<MenuItemWrapper
onClick={() => {
@@ -100,30 +76,23 @@ export function FileMenu(props: {
setMenuOpen(false);
}}
>
<StyledIcon>
<FileUp />
</StyledIcon>
<MenuItemText>Import</MenuItemText>
<FileUp />
Import
</MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper onClick={props.onDownload}>
<StyledIcon>
<FileDown />
</StyledIcon>
<MenuItemText>Download (.xlsx)</MenuItemText>
<FileDown />
Download (.xlsx)
</MenuItemWrapper>
<MenuItemWrapper
<DeleteButton
onClick={() => {
setDeleteDialogOpen(true);
setMenuOpen(false);
}}
>
<StyledIcon>
<Trash2 />
</StyledIcon>
<MenuItemText>Delete workbook</MenuItemText>
</MenuItemWrapper>
<MenuDivider />
{elements}
<Trash2 />
Delete workbook
</DeleteButton>
</Menu>
<Modal
open={isImportMenuOpen}
@@ -149,25 +118,14 @@ export function FileMenu(props: {
<DeleteWorkbookDialog
onClose={() => setDeleteDialogOpen(false)}
onConfirm={props.onDelete}
workbookName={selectedUuid ? models[selectedUuid] : ""}
workbookName={selectedUuid ? models[selectedUuid].name : ""}
/>
</Modal>
</>
);
}
const StyledIcon = styled.div`
display: flex;
align-items: center;
svg {
width: 16px;
height: 100%;
color: #757575;
padding-right: 10px;
}
`;
const MenuDivider = styled.div`
export const MenuDivider = styled.div`
width: 100%;
margin: auto;
margin-top: 4px;
@@ -175,12 +133,7 @@ const MenuDivider = styled.div`
border-top: 1px solid #eeeeee;
`;
const MenuItemText = styled.div`
color: #000;
font-size: 12px;
`;
const MenuItemWrapper = styled(MenuItem)`
export const MenuItemWrapper = styled(MenuItem)`
display: flex;
justify-content: flex-start;
font-size: 14px;
@@ -190,6 +143,14 @@ const MenuItemWrapper = styled(MenuItem)`
border-radius: 4px;
padding: 8px;
height: 32px;
color: #000;
font-size: 12px;
gap: 8px;
svg {
width: 16px;
height: 100%;
color: #757575;
}
`;
const FileMenuWrapper = styled.button<{ $isActive: boolean }>`
@@ -202,14 +163,20 @@ const FileMenuWrapper = styled.button<{ $isActive: boolean }>`
cursor: pointer;
background-color: ${(props) => (props.$isActive ? "#e6e6e6" : "transparent")};
border: none;
background: none;
&:hover {
background-color: #f2f2f2;
}
`;
const CheckIndicator = styled.span`
display: flex;
justify-content: center;
min-width: 26px;
export const DeleteButton = styled(MenuItemWrapper)`
color: #EB5757;
svg {
color: #EB5757;
}
&:hover {
background-color: #EB57571A;
}
&:active {
background-color: #EB57571A;
}
`;

View File

@@ -1,7 +1,8 @@
import styled from "@emotion/styled";
import { Menu, MenuItem } from "@mui/material";
import { Menu } from "@mui/material";
import { BookOpen, Keyboard } from "lucide-react";
import { useRef, useState } from "react";
import { MenuItemWrapper } from "./FileMenu";
export function HelpMenu() {
const [isMenuOpen, setMenuOpen] = useState(false);
@@ -61,10 +62,8 @@ export function HelpMenu() {
);
}}
>
<StyledIcon>
<BookOpen />
</StyledIcon>
<MenuItemText>Documentation</MenuItemText>
<BookOpen />
Documentation
</MenuItemWrapper>
<MenuItemWrapper
onClick={() => {
@@ -76,10 +75,8 @@ export function HelpMenu() {
);
}}
>
<StyledIcon>
<Keyboard />
</StyledIcon>
<MenuItemText>Keyboard Shortcuts</MenuItemText>
<Keyboard />
Keyboard Shortcuts
</MenuItemWrapper>
</Menu>
</div>
@@ -96,37 +93,7 @@ const HelpButton = styled.button<{ $isActive?: boolean }>`
cursor: pointer;
background-color: ${(props) => (props.$isActive ? "#e6e6e6" : "transparent")};
border: none;
background: none;
&:hover {
background-color: #f2f2f2;
}
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 14px;
width: calc(100% - 8px);
min-width: 172px;
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
`;
const StyledIcon = styled.div`
display: flex;
align-items: center;
svg {
width: 16px;
height: 100%;
color: #757575;
padding-right: 10px;
}
`;
const MenuItemText = styled.div`
color: #000;
font-size: 12px;
`;

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