Compare commits

..

39 Commits

Author SHA1 Message Date
Daniel
3edb068a01 update: add leftbar to app 2025-06-29 13:30:05 +02:00
Nicolás Hatcher
8a9ae00cad FIX: Set the padding in coor swatches to 0 2025-06-29 11:20:22 +02:00
Nicolás Hatcher
97d3b04772 FIX: Prevent scroll when focus
This are fixes to make the widget embedable
2025-06-29 11:20:22 +02:00
Nicolás Hatcher
5744ae4d77 FIX: Cargo fmt 2025-06-29 11:07:05 +02:00
Nicolás Hatcher
0be7d9b85a FIX: Make clippy happy 2025-06-29 11:07:05 +02:00
Nicolás Hatcher
46ea92966f FIX[import]: Set color black for text properly
I would like to have a text case for this. Preferably automatic.

But manual is ok, maybe we should have a series of workbooks we check
before every large deployment
2025-06-24 00:26:16 +02:00
Nicolás Hatcher
a19124cc16 FIX[import xlsx]: Sets default text color to black
Default text color was transparent white. I am not sure how this didn't
cause more problems :)
2025-06-23 18:03:54 +02:00
Nicolás Hatcher
b0a5e2553a FIX: Include "tests" is the set of phony files
Ooops. First time this actually bites me....
2025-06-15 21:16:09 +02:00
Brian Hung
5ca50f15d7 feat: use wasm-bindgen unchecked_return_type 2025-06-14 11:11:50 +02:00
Brian Hung
03e227fbb2 fix grammar spelling 2025-06-08 08:49:03 +02:00
Nicolás Hatcher
2b3ae8e20f FIX: Remove some unused dependencies and updated dependencies 2025-06-07 15:32:23 +02:00
Nicolás Hatcher
138a483c65 UPDATE: Double click in the outline handle fills column
This also removes React from the equation.
So all event handling is done outside of the React loop.
This simplifies some things and helps us in a possible move
away from React.

This is closer to how we deal with the column and row handle resizers.

I think it works quite well and it is more future proof.

But TBH I just want to try it out and see what is the DX after this.

Fixes #359
2025-06-07 11:12:10 +02:00
Nicolás Hatcher
2eb9266c30 UPDATE: Factor out a couple of helper functions from the main canvas file 2025-06-07 11:12:10 +02:00
Nicolás Hatcher
b9d3f5329b FIX: There are two fills in every new Excel model
Excel expects two default fills. If a different fill is present Excel
removes it and substitutes it with the defaults.

This resulted in models having the default fill for the first style.

Thanks @scandel!
2025-06-05 06:36:34 +02:00
Nicolás Hatcher
af49d7ad96 FIX: Download to png off by one errors 2025-06-02 21:11:18 +02:00
Nicolás Hatcher
3e015bf13a FIX: control+shitf selects area 2025-06-02 20:59:18 +02:00
Nicolás Hatcher
a5d8ee9ef0 FIX: Download all selected area
We were previously downloading only the bounds of the visible cells,
without taking into account the frozen rows/colums.

Fixes #343
2025-05-17 11:49:42 +02:00
Nicolás Hatcher
c554d929f4 FIX: Renaming a sheet updates formula in defined name 2025-04-16 09:56:17 +02:00
Nicolás Hatcher
acdf85dbc3 FIX: thick line should be 3px in the picker 2025-04-15 15:56:25 +02:00
Nicolás Hatcher
6ac8f7e948 FIX: Use local typescript instead of global 2025-04-15 15:52:07 +02:00
Nicolás Hatcher
9a4e798313 FIX: Make clippy happy. Manual fixes 2025-04-15 15:44:56 +02:00
Nicolás Hatcher
7756ef7f48 FIX: Make clippy happy. automatic update 2025-04-15 15:44:56 +02:00
Daniel
793534b190 fix: cleanup code 2025-03-26 18:13:38 +01:00
Nicolás Hatcher
efc925a046 FIX: Adds border to default swatch 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
155f891f8b FIX: Split the color picker in two 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
5683d02b93 FIX: Clean up (anchorOrigin/transformOrigin) logic 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
475c3e9d49 FIX: Add defaultColor to color picker
NB: Dani will have to pass the color from the theme
2025-03-23 16:02:14 +01:00
Nicolás Hatcher
9e65ea3024 FIX: Add 'title' to ColorPicker 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
03ad87cd8f FIX: Removes unused file 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
e2a466c500 FIX: Simplify ColorPicker code a bit
* Removes "renderMenuItem"
* Color is no longer optional
* Removes 'index' unused variable
2025-03-23 16:02:14 +01:00
Daniel
08b3d71e9e update: new color picker 2025-03-23 16:02:14 +01:00
Nicolás Hatcher
e5ec75495a UPDATE: Introducing Arrays
# This PR introduces:

## Parsing arrays:

{1,2,3} and {1;2;3}

Note that array elements can be numbers, booleans and errors (#VALUE!)

## Evaluating arrays in the SUM function

=SUM({1,2,3}) works!

## Evaluating arithmetic operation with arrays

=SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works

This is done with just one function (handle_arithmetic) for most operations

## Some mathematical functions implement arrays

=SUM(SIN({1,2,3})) works

This is done with macros. See fn_single_number
So that implementing new functions that supports array are easy


# Not done in this PR

## Most functions are not supporting arrays

When that happens we either through #N/IMPL! (not implemented error)
or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard

## The final result in a cell cannot be an array

The formula ={1,2,3} in a cell will result in #N/IMPL!

## Exporting arrays to Excel might not work correctly

Excel uses the cm (cell metadata) for formulas that contain dynamic arrays.
Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3}))
is considered a dynamic formula

## There are not a lot of tests in this delivery

The bulk of the tests will be added once we start going function by function# This PR introduces:

## Parsing arrays:

{1,2,3} and {1;2;3}

Note that array elements can be numbers, booleans and errors (#VALUE!)

## Evaluating arrays in the SUM function

=SUM({1,2,3}) works!

## Evaluating arithmetic operation with arrays

=SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works

This is done with just one function (handle_arithmetic) for most operations

## Some mathematical functions implement arrays

=SUM(SIN({1,2,3})) works

This is done with macros. See fn_single_number
So that implementing new functions that supports array are easy


# Not done in this PR

## Most functions are not supporting arrays

When that happens we either through #N/IMPL! (not implemented error)
or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard

## The final result in a cell cannot be an array

The formula ={1,2,3} in a cell will result in #N/IMPL!

## Exporting arrays to Excel might not work correctly

Excel uses the cm (cell metadata) for formulas that contain dynamic arrays.
Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3}))
is considered a dynamic formula

## There are not a lot of tests in this delivery

The bulk of the tests will be added once we start going function by function

## The array parsing does not respect the locale

Locales that use ',' as a decimal separator need to use something different for arrays

## The might introduce a small performance penalty

We haven't been benchmarking, and having closures for every arithmetic operation and every function
evaluation will introduce a performance hit. Fixing that in he future is not so hard writing tailored
code for the operation
2025-03-17 20:04:47 +01:00
Nicolás Hatcher
e07fdd2091 FIX[app.ironcalc.com]: Clean up code for the title 2025-03-12 18:24:15 +01:00
Nicolás Hatcher
cde6f0e49f FIX: Update missing packages 2025-03-06 22:32:23 +01:00
Nicolás Hatcher
da017b6113 UPDATE: Implement the implicit Intersection Operator
The II operator takes a range and returns a single cell that is in the same column or the same row
as the present cell.

This is needed for backwards compatibility with old Excel models and as a first step towards dynamic arrays.

In the past Excel would evaluate `=A1:A10` in cell `C3` as `A3`, but today in results in an array containing all
values in the range. To be compatible with old workbooks Excel inserts the II operator
on those cases.

So this PR performs an static analysis on all formulas inserting on import automatically the II operator
where necessary. This we call the _automatic implicit operator_. When exporting to Excel the operator is striped away.
You can also manually use the II. For instance `=SUM(@A1:A10)` in cell `C3`.
This was not possible before and such a formula would break backwards compatibility with Excel. To Excel that "non automatic"
form of the II is exported as `_xlfn.SINGLE()`.

Th static analysis has to be done for all arithmetic operations and all functions.
This is a bit of a daunting task and it is not done fully in this PR. We also need to implement arrays and dynamic arrays.
My believe is that once the core operations have been implemented we can go formula by formula writing proper tests and documentation.

After this PR formulas like `=A1:A10` for instance will return `#N/IMPL!` instead of performing the implicit intersection
2025-03-03 21:59:42 +01:00
Nicolás Hatcher
90763048bc FIX: Wrap text properly
This fixes a bug where if you were wrapping text,
the first word was repeated.

We really need tests!
2025-03-03 21:06:05 +01:00
Nicolás Hatcher
532386b448 FIX: The glog was wrong :S 2025-03-01 15:34:59 +01:00
Nicolás Hatcher
84b2bdd7c9 FIX: We are back on tial and error, regrettably 2025-03-01 12:25:23 +01:00
Nicolás Hatcher
25bb1ab8dc FIX: Fix CI script to deal with conflicting names 2025-02-28 13:06:10 +01:00
110 changed files with 6694 additions and 4552 deletions

View File

@@ -34,7 +34,7 @@ jobs:
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist path: bindings/python/dist
windows: windows:
@@ -58,7 +58,7 @@ jobs:
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist path: bindings/python/dist
macos: macos:
@@ -81,7 +81,7 @@ jobs:
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-${{ matrix.target }}
path: bindings/python/dist path: bindings/python/dist
sdist: sdist:
@@ -97,7 +97,7 @@ jobs:
- name: Upload sdist - name: Upload sdist
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels-${{ runner.os }}-sdist
path: bindings/python/dist path: bindings/python/dist
publish-to-test-pypi: publish-to-test-pypi:
@@ -107,9 +107,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist] needs: [linux, windows, macos, sdist]
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: wheels
path: bindings/python/ path: bindings/python/
- name: Publish distribution 📦 to Test PyPI - name: Publish distribution 📦 to Test PyPI
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
@@ -118,7 +117,7 @@ jobs:
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/" MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
with: with:
command: upload command: upload
args: --skip-existing * args: "--skip-existing **/*.whl"
working-directory: bindings/python working-directory: bindings/python
publish-pypi: publish-pypi:
@@ -128,9 +127,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist] needs: [linux, windows, macos, sdist]
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: wheels
path: bindings/python/ path: bindings/python/
- name: Publish distribution 📦 to PyPI - name: Publish distribution 📦 to PyPI
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
@@ -139,5 +137,5 @@ jobs:
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/" MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
with: with:
command: upload command: upload
args: --skip-existing * args: "--skip-existing **/*.whl"
working-directory: bindings/python working-directory: bindings/python

32
Cargo.lock generated
View File

@@ -872,6 +872,12 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.17" version = "1.0.17"
@@ -1081,23 +1087,24 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -1118,9 +1125,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1128,9 +1135,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1141,9 +1148,12 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.92" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"

View File

@@ -1,5 +1,6 @@
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::stringify::DisplaceData; use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData};
use crate::expressions::types::CellReferenceRC;
use crate::model::Model; use crate::model::Model;
// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns // NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
@@ -8,16 +9,45 @@ use crate::model::Model;
// I feel this is unimportant for now. // I feel this is unimportant for now.
impl Model { impl Model {
fn shift_cell_formula(
&mut self,
sheet: u32,
row: i32,
column: i32,
displace_data: &DisplaceData,
) -> Result<(), String> {
if let Some(f) = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.and_then(|c| c.get_formula())
{
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,
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
if formula != formula_displaced {
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
}
}
Ok(())
}
/// This function iterates over all cells in the model and shifts their formulas according to the displacement data. /// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude. /// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
fn displace_cells(&mut self, displace_data: &DisplaceData) { fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> {
let cells = self.get_all_cells(); let cells = self.get_all_cells();
for cell in cells { for cell in cells {
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data); self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?;
} }
Ok(())
} }
/// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order. /// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
@@ -134,7 +164,7 @@ impl Model {
column, column,
delta: column_count, delta: column_count,
}), }),
); )?;
// In the list of columns: // In the list of columns:
// * Keep all the columns to the left // * Keep all the columns to the left
@@ -214,7 +244,7 @@ impl Model {
column, column,
delta: -column_count, delta: -column_count,
}), }),
); )?;
let worksheet = &mut self.workbook.worksheet_mut(sheet)?; let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
// deletes all the column styles // deletes all the column styles
@@ -338,7 +368,7 @@ impl Model {
row, row,
delta: row_count, delta: row_count,
}), }),
); )?;
Ok(()) Ok(())
} }
@@ -399,7 +429,7 @@ impl Model {
row, row,
delta: -row_count, delta: -row_count,
}), }),
); )?;
Ok(()) Ok(())
} }
@@ -420,14 +450,14 @@ impl Model {
sheet: u32, sheet: u32,
column: i32, column: i32,
delta: i32, delta: i32,
) -> Result<(), &'static str> { ) -> Result<(), String> {
// Check boundaries // Check boundaries
let target_column = column + delta; let target_column = column + delta;
if !(1..=LAST_COLUMN).contains(&target_column) { if !(1..=LAST_COLUMN).contains(&target_column) {
return Err("Target column out of boundaries"); return Err("Target column out of boundaries".to_string());
} }
if !(1..=LAST_COLUMN).contains(&column) { if !(1..=LAST_COLUMN).contains(&column) {
return Err("Initial column out of boundaries"); return Err("Initial column out of boundaries".to_string());
} }
// TODO: Add the actual displacement of data and styles // TODO: Add the actual displacement of data and styles
@@ -439,7 +469,7 @@ impl Model {
column, column,
delta, delta,
}), }),
); )?;
Ok(()) Ok(())
} }

158
base/src/arithmetic.rs Normal file
View File

@@ -0,0 +1,158 @@
use crate::{
calc_result::CalcResult,
cast::NumberOrArray,
expressions::{
parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
model::Model,
};
/// Unify how we map booleans/strings to f64
fn to_f64(value: &ArrayNode) -> Result<f64, Error> {
match value {
ArrayNode::Number(f) => Ok(*f),
ArrayNode::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
ArrayNode::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(f),
Err(_) => Err(Error::VALUE),
},
ArrayNode::Error(err) => Err(err.clone()),
}
}
impl Model {
/// Applies `op` elementwise for arrays/numbers.
pub(crate) fn handle_arithmetic(
&mut self,
left: &Node,
right: &Node,
cell: CellReferenceIndex,
op: &dyn Fn(f64, f64) -> Result<f64, Error>,
) -> CalcResult {
let l = match self.get_number_or_array(left, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number_or_array(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
match (l, r) {
// -----------------------------------------------------
// Case 1: Both are numbers
// -----------------------------------------------------
(NumberOrArray::Number(f1), NumberOrArray::Number(f2)) => match op(f1, f2) {
Ok(x) => CalcResult::Number(x),
Err(Error::DIV) => CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Divide by 0".to_string(),
},
Err(Error::VALUE) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid number".to_string(),
},
Err(e) => CalcResult::Error {
error: e,
origin: cell,
message: "Unknown error".to_string(),
},
},
// -----------------------------------------------------
// Case 2: left is Number, right is Array
// -----------------------------------------------------
(NumberOrArray::Number(f1), NumberOrArray::Array(a2)) => {
let mut array = Vec::new();
for row in a2 {
let mut data_row = Vec::new();
for node in row {
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)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------------------
// Case 3: left is Array, right is Number
// -----------------------------------------------------
(NumberOrArray::Array(a1), NumberOrArray::Number(f2)) => {
let mut array = Vec::new();
for row in a1 {
let mut data_row = Vec::new();
for node in row {
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)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------------------
// Case 4: Both are arrays
// -----------------------------------------------------
(NumberOrArray::Array(a1), NumberOrArray::Array(a2)) => {
let n1 = a1.len();
let m1 = a1.first().map(|r| r.len()).unwrap_or(0);
let n2 = a2.len();
let m2 = a2.first().map(|r| r.len()).unwrap_or(0);
let n = n1.max(n2);
let m = m1.max(m2);
let mut array = Vec::new();
for i in 0..n {
let row1 = a1.get(i);
let row2 = a2.get(i);
let mut data_row = Vec::new();
for j in 0..m {
let val1 = row1.and_then(|r| r.get(j));
let val2 = row2.and_then(|r| r.get(j));
match (val1, val2) {
(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)),
},
// Mismatched dimensions => #VALUE!
_ => data_row.push(ArrayNode::Error(Error::VALUE)),
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::expressions::{token::Error, types::CellReferenceIndex}; use crate::expressions::{parser::ArrayNode, token::Error, types::CellReferenceIndex};
#[derive(Clone)] #[derive(Clone)]
pub struct Range { pub struct Range {
@@ -24,6 +24,7 @@ pub(crate) enum CalcResult {
}, },
EmptyCell, EmptyCell,
EmptyArg, EmptyArg,
Array(Vec<Vec<ArrayNode>>),
} }
impl CalcResult { impl CalcResult {

View File

@@ -1,11 +1,85 @@
use crate::{ use crate::{
calc_result::{CalcResult, Range}, calc_result::{CalcResult, Range},
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{
implicit_intersection::implicit_intersection, parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
model::Model, model::Model,
}; };
pub(crate) enum NumberOrArray {
Number(f64),
Array(Vec<Vec<ArrayNode>>),
}
impl Model { impl Model {
pub(crate) fn get_number_or_array(
&mut self,
node: &Node,
cell: CellReferenceIndex,
) -> Result<NumberOrArray, CalcResult> {
match self.evaluate_node_in_context(node, cell) {
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
CalcResult::String(s) => match s.parse::<f64>() {
Ok(f) => Ok(NumberOrArray::Number(f)),
_ => Err(CalcResult::new_error(
Error::VALUE,
cell,
"Expecting number".to_string(),
)),
},
CalcResult::Boolean(f) => {
if f {
Ok(NumberOrArray::Number(1.0))
} else {
Ok(NumberOrArray::Number(0.0))
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(NumberOrArray::Number(0.0)),
CalcResult::Range { left, right } => {
let sheet = left.sheet;
if sheet != right.sheet {
return Err(CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "3D ranges are not allowed".to_string(),
});
}
// we need to convert the range into an array
let mut array = Vec::new();
for row in left.row..=right.row {
let mut row_data = Vec::new();
for column in left.column..=right.column {
let value =
match self.evaluate_cell(CellReferenceIndex { sheet, column, row }) {
CalcResult::String(s) => ArrayNode::String(s),
CalcResult::Number(f) => ArrayNode::Number(f),
CalcResult::Boolean(b) => ArrayNode::Boolean(b),
CalcResult::Error { error, .. } => ArrayNode::Error(error),
CalcResult::Range { .. } => {
// if we do things right this can never happen.
// the evaluation of a cell should never return a range
ArrayNode::Number(0.0)
}
CalcResult::EmptyCell => ArrayNode::Number(0.0),
CalcResult::EmptyArg => ArrayNode::Number(0.0),
CalcResult::Array(_) => {
// if we do things right this can never happen.
// the evaluation of a cell should never return an array
ArrayNode::Number(0.0)
}
};
row_data.push(value);
}
array.push(row_data);
}
Ok(NumberOrArray::Array(array))
}
CalcResult::Array(s) => Ok(NumberOrArray::Array(s)),
error @ CalcResult::Error { .. } => Err(error),
}
}
pub(crate) fn get_number( pub(crate) fn get_number(
&mut self, &mut self,
node: &Node, node: &Node,
@@ -39,19 +113,16 @@ impl Model {
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => { CalcResult::Range { .. } => Err(CalcResult::Error {
match implicit_intersection(&cell, &Range { left, right }) { error: Error::NIMPL,
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_number(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell, origin: cell,
message: "Invalid reference (number)".to_string(), message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}), }),
}
}
} }
} }
@@ -88,7 +159,7 @@ impl Model {
// FIXME: I think when casting a number we should convert it to_precision(x, 15) // FIXME: I think when casting a number we should convert it to_precision(x, 15)
// See function Exact // See function Exact
match result { match result {
CalcResult::Number(f) => Ok(format!("{}", f)), CalcResult::Number(f) => Ok(format!("{f}")),
CalcResult::String(s) => Ok(s), CalcResult::String(s) => Ok(s),
CalcResult::Boolean(f) => { CalcResult::Boolean(f) => {
if f { if f {
@@ -99,19 +170,16 @@ impl Model {
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => { CalcResult::Range { .. } => Err(CalcResult::Error {
match implicit_intersection(&cell, &Range { left, right }) { error: Error::NIMPL,
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_string(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell, origin: cell,
message: "Invalid reference (string)".to_string(), message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}), }),
}
}
} }
} }
@@ -151,19 +219,16 @@ impl Model {
CalcResult::Boolean(b) => Ok(b), CalcResult::Boolean(b) => Ok(b),
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
error @ CalcResult::Error { .. } => Err(error), error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => { CalcResult::Range { .. } => Err(CalcResult::Error {
match implicit_intersection(&cell, &Range { left, right }) { error: Error::NIMPL,
Some(cell_reference) => {
let result = self.evaluate_cell(cell_reference);
self.cast_to_bool(result, cell_reference)
}
None => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell, origin: cell,
message: "Invalid reference (bool)".to_string(), message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}), }),
}
}
} }
} }

View File

@@ -1,138 +0,0 @@
use crate::{
expressions::{
parser::{
move_formula::ref_is_in_area,
stringify::{to_string, to_string_displaced, DisplaceData},
walk::forward_references,
},
types::{Area, CellReferenceIndex, CellReferenceRC},
},
model::Model,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged, deny_unknown_fields)]
pub enum CellValue {
Value(String),
None,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SetCellValue {
cell: CellReferenceIndex,
new_value: CellValue,
old_value: CellValue,
}
impl Model {
#[allow(clippy::expect_used)]
pub(crate) fn shift_cell_formula(
&mut self,
sheet: u32,
row: i32,
column: i32,
displace_data: &DisplaceData,
) {
if let Some(f) = self
.workbook
.worksheet(sheet)
.expect("Worksheet must exist")
.cell(row, column)
.expect("Cell must exist")
.get_formula()
{
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,
column,
};
// FIXME: This is not a very performant way if the formula has changed :S.
let formula = to_string(node, &cell_reference);
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
if formula != formula_displaced {
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))
.expect("Failed to shift cell formula");
}
}
}
#[allow(clippy::expect_used)]
pub fn forward_references(
&mut self,
source_area: &Area,
target: &CellReferenceIndex,
) -> Result<Vec<SetCellValue>, String> {
let mut diff_list: Vec<SetCellValue> = Vec::new();
let target_area = &Area {
sheet: target.sheet,
row: target.row,
column: target.column,
width: source_area.width,
height: source_area.height,
};
// Walk over every formula
let cells = self.get_all_cells();
for cell in cells {
if let Some(f) = self
.workbook
.worksheet(cell.index)
.expect("Worksheet must exist")
.cell(cell.row, cell.column)
.expect("Cell must exist")
.get_formula()
{
let sheet = cell.index;
let row = cell.row;
let column = cell.column;
// If cell is in the source or target area, skip
if ref_is_in_area(sheet, row, column, source_area)
|| ref_is_in_area(sheet, row, column, target_area)
{
continue;
}
// Get the formula
// Get a copy of the AST
let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
let cell_reference = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
column: cell.column,
row: cell.row,
};
let context = CellReferenceIndex { sheet, column, row };
let formula = to_string(node, &cell_reference);
let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
forward_references(
node,
&context,
source_area,
target.sheet,
target_sheet_name,
target.row,
target.column,
);
// If the string representation of the formula has changed update the cell
let updated_formula = to_string(node, &cell_reference);
if formula != updated_formula {
self.update_cell_with_formula(
sheet,
row,
column,
format!("={updated_formula}"),
)?;
// Update the diff list
diff_list.push(SetCellValue {
cell: CellReferenceIndex { sheet, column, row },
new_value: CellValue::Value(format!("={}", updated_formula)),
old_value: CellValue::Value(format!("={}", formula)),
});
}
}
}
Ok(diff_list)
}
}

View File

@@ -142,7 +142,7 @@ impl Lexer {
pub fn expect(&mut self, tk: TokenType) -> Result<()> { pub fn expect(&mut self, tk: TokenType) -> Result<()> {
let nt = self.next_token(); let nt = self.next_token();
if mem::discriminant(&nt) != mem::discriminant(&tk) { if mem::discriminant(&nt) != mem::discriminant(&tk) {
return Err(self.set_error(&format!("Error, expected {:?}", tk), self.position)); return Err(self.set_error(&format!("Error, expected {tk:?}"), self.position));
} }
Ok(()) Ok(())
} }
@@ -187,6 +187,7 @@ impl Lexer {
']' => TokenType::RightBracket, ']' => TokenType::RightBracket,
':' => TokenType::Colon, ':' => TokenType::Colon,
';' => TokenType::Semicolon, ';' => TokenType::Semicolon,
'@' => TokenType::At,
',' => { ',' => {
if self.locale.numbers.symbols.decimal == "," { if self.locale.numbers.symbols.decimal == "," {
match self.consume_number(',') { match self.consume_number(',') {
@@ -510,7 +511,7 @@ impl Lexer {
self.position = position; self.position = position;
chars.parse::<i32>().map_err(|_| LexerError { chars.parse::<i32>().map_err(|_| LexerError {
position, position,
message: format!("Failed to parse to int: {}", chars), message: format!("Failed to parse to int: {chars}"),
}) })
} }
@@ -571,9 +572,7 @@ impl Lexer {
} }
self.position = position; self.position = position;
match chars.parse::<f64>() { match chars.parse::<f64>() {
Err(_) => { Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)),
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
}
Ok(v) => Ok(v), Ok(v) => Ok(v),
} }
} }

View File

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

View File

@@ -23,19 +23,19 @@ impl Lexer {
// TODO(TD): There are better ways of doing this :) // TODO(TD): There are better ways of doing this :)
let rest_of_formula: String = self.chars[self.position..self.len].iter().collect(); let rest_of_formula: String = self.chars[self.position..self.len].iter().collect();
let specifier = if rest_of_formula.starts_with("#This Row]") { let specifier = if rest_of_formula.starts_with("#This Row]") {
self.position += "#This Row]".bytes().len(); self.position += "#This Row]".len();
TableSpecifier::ThisRow TableSpecifier::ThisRow
} else if rest_of_formula.starts_with("#All]") { } else if rest_of_formula.starts_with("#All]") {
self.position += "#All]".bytes().len(); self.position += "#All]".len();
TableSpecifier::All TableSpecifier::All
} else if rest_of_formula.starts_with("#Data]") { } else if rest_of_formula.starts_with("#Data]") {
self.position += "#Data]".bytes().len(); self.position += "#Data]".len();
TableSpecifier::Data TableSpecifier::Data
} else if rest_of_formula.starts_with("#Headers]") { } else if rest_of_formula.starts_with("#Headers]") {
self.position += "#Headers]".bytes().len(); self.position += "#Headers]".len();
TableSpecifier::Headers TableSpecifier::Headers
} else if rest_of_formula.starts_with("#Totals]") { } else if rest_of_formula.starts_with("#Totals]") {
self.position += "#Totals]".bytes().len(); self.position += "#Totals]".len();
TableSpecifier::Totals TableSpecifier::Totals
} else { } else {
return Err(LexerError { return Err(LexerError {

View File

@@ -1,4 +1,5 @@
mod test_common; mod test_common;
mod test_implicit_intersection;
mod test_language; mod test_language;
mod test_locale; mod test_locale;
mod test_ranges; mod test_ranges;

View File

@@ -0,0 +1,25 @@
#![allow(clippy::unwrap_used)]
use crate::expressions::{
lexer::{Lexer, LexerMode},
token::TokenType::*,
};
use crate::language::get_language;
use crate::locale::get_locale;
fn new_lexer(formula: &str) -> Lexer {
let locale = get_locale("en").unwrap();
let language = get_language("en").unwrap();
Lexer::new(formula, LexerMode::A1, locale, language)
}
#[test]
fn sum_implicit_intersection() {
let mut lx = new_lexer("sum(@A1:A3)");
assert_eq!(lx.next_token(), Ident("sum".to_string()));
assert_eq!(lx.next_token(), LeftParenthesis);
assert_eq!(lx.next_token(), At);
assert!(matches!(lx.next_token(), Range { .. }));
assert_eq!(lx.next_token(), RightParenthesis);
assert_eq!(lx.next_token(), EOF);
}

View File

@@ -1,5 +1,5 @@
/*! /*!
# GRAMAR # GRAMMAR
<pre class="rust"> <pre class="rust">
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>' opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
@@ -12,7 +12,8 @@ term => factor (opFactor factor)*
factor => prod (opProd prod)* factor => prod (opProd prod)*
prod => power ('^' power)* prod => power ('^' power)*
power => (unaryOp)* range '%'* power => (unaryOp)* range '%'*
range => primary (':' primary)? range => implicit (':' primary)?
implicit=> '@' primary | primary
primary => '(' expr ')' primary => '(' expr ')'
=> number => number
=> function '(' f_args ')' => function '(' f_args ')'
@@ -45,8 +46,8 @@ use super::utils::number_to_column;
use token::OpCompare; use token::OpCompare;
pub mod move_formula; pub mod move_formula;
pub mod static_analysis;
pub mod stringify; pub mod stringify;
pub mod walk;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@@ -81,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
None None
} }
// DefinedNameS is a tuple with the name of the defined name, the index of the sheet and the formula
pub type DefinedNameS = (String, Option<u32>, String);
pub(crate) struct Reference<'a> { pub(crate) struct Reference<'a> {
sheet_name: &'a Option<String>, sheet_name: &'a Option<String>,
sheet_index: u32, sheet_index: u32,
@@ -90,6 +94,14 @@ pub(crate) struct Reference<'a> {
column: i32, column: i32,
} }
#[derive(PartialEq, Clone, Debug)]
pub enum ArrayNode {
Boolean(bool),
Number(f64),
String(String),
Error(token::Error),
}
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub enum Node { pub enum Node {
BooleanKind(bool), BooleanKind(bool),
@@ -163,10 +175,14 @@ pub enum Node {
name: String, name: String,
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Node>), ArrayKind(Vec<Vec<ArrayNode>>),
DefinedNameKind((String, Option<u32>)), DefinedNameKind(DefinedNameS),
TableNameKind(String), TableNameKind(String),
WrongVariableKind(String), WrongVariableKind(String),
ImplicitIntersection {
automatic: bool,
child: Box<Node>,
},
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -189,7 +205,7 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, defined_names: Vec<DefinedNameS>,
context: CellReferenceRC, context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
@@ -197,7 +213,7 @@ pub struct Parser {
impl Parser { impl Parser {
pub fn new( pub fn new(
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
) -> Parser { ) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
@@ -228,7 +244,7 @@ impl Parser {
pub fn set_worksheets_and_names( pub fn set_worksheets_and_names(
&mut self, &mut self,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, defined_names: Vec<DefinedNameS>,
) { ) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names; self.defined_names = defined_names;
@@ -252,17 +268,17 @@ impl Parser {
// Returns: // Returns:
// * None: If there is no defined name by that name // * None: If there is no defined name by that name
// * Some(Some(index)): If there is a defined name local to that sheet // * Some((Some(index), formula)): If there is a defined name local to that sheet
// * Some(None): If there is a global defined name // * Some(None): If there is a global defined name
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> { fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
for (df_name, df_scope) in &self.defined_names { for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) { if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope); return Some((*df_scope, df_formula.to_owned()));
} }
} }
for (df_name, df_scope) in &self.defined_names { for (df_name, df_scope, df_formula) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() { if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some(None); return Some((None, df_formula.to_owned()));
} }
} }
None None
@@ -411,7 +427,7 @@ impl Parser {
} }
fn parse_range(&mut self) -> Node { fn parse_range(&mut self) -> Node {
let t = self.parse_primary(); let t = self.parse_implicit();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
return t; return t;
} }
@@ -430,6 +446,65 @@ impl Parser {
t t
} }
fn parse_implicit(&mut self) -> Node {
let next_token = self.lexer.peek_token();
if next_token == TokenType::At {
self.lexer.advance_token();
let t = self.parse_primary();
if let Node::ParseErrorKind { .. } = t {
return t;
}
return Node::ImplicitIntersection {
automatic: false,
child: Box::new(t),
};
}
self.parse_primary()
}
fn parse_array_row(&mut self) -> Result<Vec<ArrayNode>, Node> {
let mut row = Vec::new();
// and array can only have numbers, string or booleans
// otherwise it is a syntax error
let first_element = match self.parse_expr() {
Node::BooleanKind(s) => ArrayNode::Boolean(s),
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
};
row.push(first_element);
let mut next_token = self.lexer.peek_token();
// FIXME: this is not respecting the locale
while next_token == TokenType::Comma {
self.lexer.advance_token();
let value = match self.parse_expr() {
Node::BooleanKind(s) => ArrayNode::Boolean(s),
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
};
row.push(value);
next_token = self.lexer.peek_token();
}
Ok(row)
}
fn parse_primary(&mut self) -> Node { fn parse_primary(&mut self) -> Node {
let next_token = self.lexer.next_token(); let next_token = self.lexer.next_token();
match next_token { match next_token {
@@ -451,21 +526,35 @@ impl Parser {
TokenType::Number(s) => Node::NumberKind(s), TokenType::Number(s) => Node::NumberKind(s),
TokenType::String(s) => Node::StringKind(s), TokenType::String(s) => Node::StringKind(s),
TokenType::LeftBrace => { TokenType::LeftBrace => {
let t = self.parse_expr(); // It's an array. It's a collection of rows all of the same dimension
if let Node::ParseErrorKind { .. } = t {
return t; let first_row = match self.parse_array_row() {
} Ok(s) => s,
Err(error) => return error,
};
let length = first_row.len();
let mut matrix = Vec::new();
matrix.push(first_row);
// FIXME: this is not respecting the locale
let mut next_token = self.lexer.peek_token(); let mut next_token = self.lexer.peek_token();
let mut args: Vec<Node> = vec![t];
while next_token == TokenType::Semicolon { while next_token == TokenType::Semicolon {
self.lexer.advance_token(); self.lexer.advance_token();
let p = self.parse_expr(); let row = match self.parse_array_row() {
if let Node::ParseErrorKind { .. } = p { Ok(s) => s,
return p; Err(error) => return error,
} };
next_token = self.lexer.peek_token(); next_token = self.lexer.peek_token();
args.push(p); if row.len() != length {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "All rows in an array should be the same length".to_string(),
};
} }
matrix.push(row);
}
if let Err(err) = self.lexer.expect(TokenType::RightBrace) { if let Err(err) = self.lexer.expect(TokenType::RightBrace) {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
@@ -473,7 +562,7 @@ impl Parser {
message: err.message, message: err.message,
}; };
} }
Node::ArrayKind(args) Node::ArrayKind(matrix)
} }
TokenType::Reference { TokenType::Reference {
sheet, sheet,
@@ -604,6 +693,20 @@ impl Parser {
args, args,
}; };
} }
if &name == "_xlfn.SINGLE" {
if args.len() != 1 {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Implicit Intersection requires just one argument"
.to_string(),
};
}
return Node::ImplicitIntersection {
automatic: false,
child: Box::new(args[0].clone()),
};
}
return Node::InvalidFunctionKind { name, args }; return Node::InvalidFunctionKind { name, args };
} }
let context = &self.context; let context = &self.context;
@@ -620,8 +723,8 @@ impl Parser {
}; };
// Could be a defined name or a table // Could be a defined name or a table
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) { if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope)); return Node::DefinedNameKind((name, scope, formula));
} }
let name_lower = name.to_lowercase(); let name_lower = name.to_lowercase();
for table_name in self.tables.keys() { for table_name in self.tables.keys() {
@@ -706,6 +809,14 @@ impl Parser {
message: "Unexpected token: 'POWER'".to_string(), message: "Unexpected token: 'POWER'".to_string(),
} }
} }
TokenType::At => {
// A primary Node cannot start with an operator
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Unexpected token: '@'".to_string(),
}
}
TokenType::RightParenthesis TokenType::RightParenthesis
| TokenType::RightBracket | TokenType::RightBracket
| TokenType::Colon | TokenType::Colon
@@ -717,7 +828,7 @@ impl Parser {
| TokenType::Percent => Node::ParseErrorKind { | TokenType::Percent => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: format!("Unexpected token: '{:?}'", next_token), message: format!("Unexpected token: '{next_token:?}'"),
}, },
TokenType::LeftBracket => Node::ParseErrorKind { TokenType::LeftBracket => Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),

View File

@@ -1,6 +1,6 @@
use super::{ use super::{
stringify::{stringify_reference, DisplaceData}, stringify::{stringify_reference, DisplaceData},
Node, Reference, ArrayNode, Node, Reference,
}; };
use crate::{ use crate::{
constants::{LAST_COLUMN, LAST_ROW}, constants::{LAST_COLUMN, LAST_ROW},
@@ -53,15 +53,24 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
arguments = to_string_moved(el, move_context); arguments = to_string_moved(el, move_context);
} }
} }
format!("{}({})", name, arguments) format!("{name}({arguments})")
}
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
match node {
ArrayNode::Boolean(value) => format!("{value}").to_ascii_uppercase(),
ArrayNode::Number(number) => to_excel_precision_str(*number),
ArrayNode::String(value) => format!("\"{value}\""),
ArrayNode::Error(kind) => format!("{kind}"),
}
} }
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(), BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value), StringKind(value) => format!("\"{value}\""),
ReferenceKind { ReferenceKind {
sheet_name, sheet_name,
sheet_index, sheet_index,
@@ -232,7 +241,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
@@ -316,7 +325,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
@@ -349,7 +358,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
_ => to_string_moved(right, move_context), _ => to_string_moved(right, move_context),
}; };
format!("{}{}{}", x, kind, y) format!("{x}{kind}{y}")
} }
OpPowerKind { left, right } => format!( OpPowerKind { left, right } => format!(
"{}^{}", "{}^{}",
@@ -362,20 +371,41 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
move_function(name, args, move_context) move_function(name, args, move_context)
} }
ArrayKind(args) => { ArrayKind(args) => {
// This code is a placeholder. Arrays are not yet implemented let mut first_row = true;
let mut first = true; let mut matrix_string = String::new();
let mut arguments = "".to_string();
for el in args { // Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
if !first { for row in args {
arguments = format!("{},{}", arguments, to_string_moved(el, move_context)); if !first_row {
matrix_string.push(',');
} else { } else {
first = false; first_row = false;
arguments = to_string_moved(el, move_context);
} }
// Build the string for the current row
let mut first_col = true;
let mut row_string = String::new();
for el in row {
if !first_col {
row_string.push(',');
} else {
first_col = false;
} }
format!("{{{}}}", arguments)
// Reuse your existing element-stringification function
row_string.push_str(&to_string_array_node(el));
} }
DefinedNameKind((name, _)) => name.to_string(),
// Enclose the row in braces
matrix_string.push('{');
matrix_string.push_str(&row_string);
matrix_string.push('}');
}
// Enclose the whole matrix in braces
format!("{{{matrix_string}}}")
}
DefinedNameKind((name, ..)) => name.to_string(),
TableNameKind(name) => name.to_string(), TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
@@ -388,12 +418,18 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)), OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)), OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
}, },
ErrorKind(kind) => format!("{}", kind), ErrorKind(kind) => format!("{kind}"),
ParseErrorKind { ParseErrorKind {
formula, formula,
message: _, message: _,
position: _, position: _,
} => formula.to_string(), } => formula.to_string(),
EmptyArgKind => "".to_string(), EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
format!("@{}", to_string_moved(child, move_context))
}
} }
} }

View File

@@ -0,0 +1,984 @@
use crate::functions::Function;
use super::Node;
use once_cell::sync::Lazy;
use regex::Regex;
#[allow(clippy::expect_used)]
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
fn is_range_reference(s: &str) -> bool {
RE.is_match(s)
}
/*
# NOTES on the Implicit Intersection operator: @
Sometimes we obtain a range where we expected a single argument. This can happen:
* As an argument of a function, eg: `SIN(A1:A5)`
* As the result of a computation of a formula `=A1:A5`
In previous versions of the Friendly Giant the spreadsheet engine would perform an operation called _implicit intersection_
that tries to find a single cell within the range. It works by picking a cell in the range that is the same row or the same column
as the cell. If there is just one we return that otherwise we return the `#REF!` error.
Examples:
* Siting on `C3` the formula `=D1:D5` will return `D3`
* Sitting on `C3` the formula `=D:D` will return `D3`
* Sitting on `C3` the formula `=A1:A7` will return `A3`
* Sitting on `C3` the formula `=A5:A8` will return `#REF!`
* Sitting on `C3` the formula `D1:G7` will return `#REF!`
Today's version of the engine will result in a dynamic array spilling the result through several cells.
To force the old behaviour we can use the _implicit intersection operator_: @
* `=@A1:A7` or `=SIN(@A1:A7)
When parsing formulas that come form old workbooks this is done automatically.
We call this version of the II operator the _automatic_ II operator.
We can also insert the II operator in places where before was impossible:
* `=SUM(@A1:A7)`
This formulas will not be compatible with old versions of the engine. The FG will stringify this as `=SUM(_xlfn.SIMPLE(A1:A7))`.
*/
/// Transverses the formula tree adding the implicit intersection operator in all arguments of functions that
/// expect a scalar but get a range.
/// * A:A => @A:A
/// * SIN(A1:D1) => SIN(@A1:D1)
///
/// Assumes formula return a scalar
pub fn add_implicit_intersection(node: &mut Node, add: bool) {
match node {
Node::BooleanKind(_)
| Node::NumberKind(_)
| Node::StringKind(_)
| Node::ErrorKind(_)
| Node::EmptyArgKind
| Node::ParseErrorKind { .. }
| Node::WrongReferenceKind { .. }
| Node::WrongRangeKind { .. }
| Node::InvalidFunctionKind { .. }
| Node::ArrayKind(_)
| Node::ReferenceKind { .. } => {}
Node::ImplicitIntersection { child, .. } => {
// We need to check wether the II can be automatic or not
let mut new_node = child.as_ref().clone();
add_implicit_intersection(&mut new_node, add);
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
*node = new_node
}
}
Node::RangeKind {
row1,
column1,
row2,
column2,
sheet_name,
sheet_index,
absolute_row1,
absolute_column1,
absolute_row2,
absolute_column2,
} => {
if add {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::RangeKind {
sheet_name: sheet_name.clone(),
sheet_index: *sheet_index,
absolute_row1: *absolute_row1,
absolute_column1: *absolute_column1,
row1: *row1,
column1: *column1,
absolute_row2: *absolute_row2,
absolute_column2: *absolute_column2,
row2: *row2,
column2: *column2,
}),
};
}
}
Node::OpRangeKind { left, right } => {
if add {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::OpRangeKind {
left: left.clone(),
right: right.clone(),
}),
}
}
}
// operations
Node::UnaryKind { right, .. } => add_implicit_intersection(right, add),
Node::OpConcatenateKind { left, right }
| Node::OpSumKind { left, right, .. }
| Node::OpProductKind { left, right, .. }
| Node::OpPowerKind { left, right, .. }
| Node::CompareKind { left, right, .. } => {
add_implicit_intersection(left, add);
add_implicit_intersection(right, add);
}
Node::DefinedNameKind(v) => {
if add {
// Not all defined names deserve the II operator
// For instance =Sheet1!A1 doesn't need to be intersected
if is_range_reference(&v.2) {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::DefinedNameKind(v.to_owned())),
}
}
}
}
Node::WrongVariableKind(v) => {
if add {
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(Node::WrongVariableKind(v.to_owned())),
}
}
}
Node::TableNameKind(_) => {
// noop for now
}
Node::FunctionKind { kind, args } => {
let arg_count = args.len();
let signature = get_function_args_signature(kind, arg_count);
for index in 0..arg_count {
if matches!(signature[index], Signature::Scalar)
&& matches!(
run_static_analysis_on_node(&args[index]),
StaticResult::Range(_, _) | StaticResult::Unknown
)
{
add_implicit_intersection(&mut args[index], true);
} else {
add_implicit_intersection(&mut args[index], false);
}
}
if add
&& matches!(
run_static_analysis_on_node(node),
StaticResult::Range(_, _) | StaticResult::Unknown
)
{
*node = Node::ImplicitIntersection {
automatic: true,
child: Box::new(node.clone()),
}
}
}
};
}
pub(crate) enum StaticResult {
Scalar,
Array(i32, i32),
Range(i32, i32),
Unknown,
// TODO: What if one of the dimensions is known?
// what if the dimensions are unknown but bounded?
}
fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
let lhs = run_static_analysis_on_node(left);
let rhs = run_static_analysis_on_node(right);
match (lhs, rhs) {
(StaticResult::Scalar, StaticResult::Scalar) => StaticResult::Scalar,
(StaticResult::Scalar, StaticResult::Array(a, b) | StaticResult::Range(a, b)) => {
StaticResult::Array(a, b)
}
(StaticResult::Array(a, b) | StaticResult::Range(a, b), StaticResult::Scalar) => {
StaticResult::Array(a, b)
}
(
StaticResult::Array(a1, b1) | StaticResult::Range(a1, b1),
StaticResult::Array(a2, b2) | StaticResult::Range(a2, b2),
) => StaticResult::Array(a1.max(a2), b1.max(b2)),
(_, StaticResult::Unknown) => StaticResult::Unknown,
(StaticResult::Unknown, _) => StaticResult::Unknown,
}
}
// Returns:
// * Scalar if we can proof the result of the evaluation is a scalar
// * Array(a, b) if we know it will be an a x b array.
// * Range(a, b) if we know it will be a a x b range.
// * Unknown if we cannot guaranty either
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
match node {
Node::BooleanKind(_)
| Node::NumberKind(_)
| Node::StringKind(_)
| Node::ErrorKind(_)
| Node::EmptyArgKind => StaticResult::Scalar,
Node::UnaryKind { right, .. } => run_static_analysis_on_node(right),
Node::ParseErrorKind { .. } => {
// StaticResult::Unknown is also valid
StaticResult::Scalar
}
Node::WrongReferenceKind { .. } => {
// StaticResult::Unknown is also valid
StaticResult::Scalar
}
Node::WrongRangeKind { .. } => {
// StaticResult::Unknown or Array is also valid
StaticResult::Scalar
}
Node::InvalidFunctionKind { .. } => {
// StaticResult::Unknown is also valid
StaticResult::Scalar
}
Node::ArrayKind(array) => {
let n = array.len() as i32;
// FIXME: This is a placeholder until we implement arrays
StaticResult::Array(n, 1)
}
Node::RangeKind {
row1,
column1,
row2,
column2,
..
} => StaticResult::Range(row2 - row1, column2 - column1),
Node::OpRangeKind { .. } => {
// TODO: We could do a bit better here
StaticResult::Unknown
}
Node::ReferenceKind { .. } => StaticResult::Scalar,
// binary operations
Node::OpConcatenateKind { left, right } => static_analysis_op_nodes(left, right),
Node::OpSumKind { left, right, .. } => static_analysis_op_nodes(left, right),
Node::OpProductKind { left, right, .. } => static_analysis_op_nodes(left, right),
Node::OpPowerKind { left, right, .. } => static_analysis_op_nodes(left, right),
Node::CompareKind { left, right, .. } => static_analysis_op_nodes(left, right),
// defined names
Node::DefinedNameKind(_) => StaticResult::Unknown,
Node::WrongVariableKind(_) => StaticResult::Unknown,
Node::TableNameKind(_) => StaticResult::Unknown,
Node::FunctionKind { kind, args } => static_analysis_on_function(kind, args),
Node::ImplicitIntersection { .. } => StaticResult::Scalar,
}
}
// If all the arguments are scalars the function will return a scalar
// If any of the arguments is a range or an array it will return an array
fn scalar_arguments(args: &[Node]) -> StaticResult {
let mut n = 0;
let mut m = 0;
for arg in args {
match run_static_analysis_on_node(arg) {
StaticResult::Scalar => {
// noop
}
StaticResult::Array(a, b) | StaticResult::Range(a, b) => {
n = n.max(a);
m = m.max(b);
}
StaticResult::Unknown => return StaticResult::Unknown,
}
}
if n == 0 && m == 0 {
return StaticResult::Scalar;
}
StaticResult::Array(n, m)
}
// We only care if the function can return a range or not
fn not_implemented(_args: &[Node]) -> StaticResult {
StaticResult::Scalar
}
fn static_analysis_offset(args: &[Node]) -> StaticResult {
// If first argument is a single cell reference and there are no4th and 5th argument,
// or they are 1, then it is a scalar
let arg_count = args.len();
if arg_count < 3 {
// Actually an error
return StaticResult::Scalar;
}
if !matches!(args[0], Node::ReferenceKind { .. }) {
return StaticResult::Unknown;
}
if arg_count == 3 {
return StaticResult::Scalar;
}
match args[3] {
Node::NumberKind(f) => {
if f != 1.0 {
return StaticResult::Unknown;
}
}
_ => return StaticResult::Unknown,
};
if arg_count == 4 {
return StaticResult::Scalar;
}
match args[4] {
Node::NumberKind(f) => {
if f != 1.0 {
return StaticResult::Unknown;
}
}
_ => return StaticResult::Unknown,
};
StaticResult::Unknown
}
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
// // We will always insert the @ in CHOOSE, but technically it is only needed if one of the elements is a range
// StaticResult::Unknown
// }
fn static_analysis_indirect(_args: &[Node]) -> StaticResult {
// We will always insert the @, but we don't need to do that in every scenario`
StaticResult::Unknown
}
fn static_analysis_index(_args: &[Node]) -> StaticResult {
// INDEX has two forms, but they are indistinguishable at parse time.
StaticResult::Unknown
}
#[derive(Clone)]
enum Signature {
Scalar,
Vector,
Error,
}
fn args_signature_no_args(arg_count: usize) -> Vec<Signature> {
if arg_count == 0 {
vec![]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_scalars(
arg_count: usize,
required_count: usize,
optional_count: usize,
) -> Vec<Signature> {
if arg_count >= required_count && arg_count <= required_count + optional_count {
vec![Signature::Scalar; arg_count]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_one_vector(arg_count: usize) -> Vec<Signature> {
if arg_count == 1 {
vec![Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_sumif(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
// 1 or none scalars
fn args_signature_sheet(arg_count: usize) -> Vec<Signature> {
if arg_count == 0 {
vec![]
} else if arg_count == 1 {
vec![Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_hlookup(arg_count: usize) -> Vec<Signature> {
if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Vector,
Signature::Vector,
Signature::Scalar,
Signature::Vector,
]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_index(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_lookup(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_match(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector, Signature::Vector]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_offset(arg_count: usize) -> Vec<Signature> {
if arg_count == 3 {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
]
} else if arg_count == 5 {
vec![
Signature::Vector,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_row(arg_count: usize) -> Vec<Signature> {
if arg_count == 0 {
vec![]
} else if arg_count == 1 {
vec![Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_xlookup(arg_count: usize) -> Vec<Signature> {
if !(3..=6).contains(&arg_count) {
return vec![Signature::Error; arg_count];
}
let mut result = vec![Signature::Scalar; arg_count];
result[0] = Signature::Vector;
result[1] = Signature::Vector;
result[2] = Signature::Vector;
result
}
fn args_signature_textafter(arg_count: usize) -> Vec<Signature> {
if !(2..=6).contains(&arg_count) {
vec![Signature::Scalar; arg_count]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_textjoin(arg_count: usize) -> Vec<Signature> {
if arg_count >= 3 {
let mut result = vec![Signature::Vector; arg_count];
result[0] = Signature::Scalar;
result[1] = Signature::Scalar;
result
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_npv(arg_count: usize) -> Vec<Signature> {
if arg_count < 2 {
return vec![Signature::Error; arg_count];
}
let mut result = vec![Signature::Vector; arg_count];
result[0] = Signature::Scalar;
result
}
fn args_signature_irr(arg_count: usize) -> Vec<Signature> {
if arg_count > 2 {
vec![Signature::Error; arg_count]
} else if arg_count == 1 {
vec![Signature::Vector]
} else {
vec![Signature::Vector, Signature::Scalar]
}
}
fn args_signature_xirr(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Vector; arg_count]
} else if arg_count == 3 {
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
} else {
vec![Signature::Error; arg_count]
}
}
fn args_signature_mirr(arg_count: usize) -> Vec<Signature> {
if arg_count != 3 {
vec![Signature::Error; arg_count]
} else {
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
}
}
fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
if arg_count != 3 {
vec![Signature::Error; arg_count]
} else {
vec![Signature::Scalar, Signature::Vector, Signature::Vector]
}
}
// 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
// 3. Understanding the return value
//
// The signature of the functions should be defined only once
// Given a function and a number of arguments this returns the arguments at each position
// are expected to be scalars or vectors (array/ranges).
// Sets signature::Error to all arguments if the number of arguments is incorrect.
fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signature> {
match kind {
Function::And => vec![Signature::Vector; arg_count],
Function::False => args_signature_no_args(arg_count),
Function::If => args_signature_scalars(arg_count, 2, 1),
Function::Iferror => args_signature_scalars(arg_count, 2, 0),
Function::Ifna => args_signature_scalars(arg_count, 2, 0),
Function::Ifs => vec![Signature::Scalar; arg_count],
Function::Not => args_signature_scalars(arg_count, 1, 0),
Function::Or => vec![Signature::Vector; arg_count],
Function::Switch => vec![Signature::Scalar; arg_count],
Function::True => args_signature_no_args(arg_count),
Function::Xor => vec![Signature::Vector; arg_count],
Function::Abs => args_signature_scalars(arg_count, 1, 0),
Function::Acos => args_signature_scalars(arg_count, 1, 0),
Function::Acosh => args_signature_scalars(arg_count, 1, 0),
Function::Asin => args_signature_scalars(arg_count, 1, 0),
Function::Asinh => args_signature_scalars(arg_count, 1, 0),
Function::Atan => args_signature_scalars(arg_count, 1, 0),
Function::Atan2 => args_signature_scalars(arg_count, 2, 0),
Function::Atanh => args_signature_scalars(arg_count, 1, 0),
Function::Choose => vec![Signature::Scalar; arg_count],
Function::Column => args_signature_row(arg_count),
Function::Columns => args_signature_one_vector(arg_count),
Function::Cos => args_signature_scalars(arg_count, 1, 0),
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
Function::Max => vec![Signature::Vector; arg_count],
Function::Min => vec![Signature::Vector; arg_count],
Function::Pi => args_signature_no_args(arg_count),
Function::Power => args_signature_scalars(arg_count, 2, 0),
Function::Product => vec![Signature::Vector; arg_count],
Function::Round => args_signature_scalars(arg_count, 2, 0),
Function::Rounddown => args_signature_scalars(arg_count, 2, 0),
Function::Roundup => args_signature_scalars(arg_count, 2, 0),
Function::Sin => args_signature_scalars(arg_count, 1, 0),
Function::Sinh => args_signature_scalars(arg_count, 1, 0),
Function::Sqrt => args_signature_scalars(arg_count, 1, 0),
Function::Sqrtpi => args_signature_scalars(arg_count, 1, 0),
Function::Sum => vec![Signature::Vector; arg_count],
Function::Sumif => args_signature_sumif(arg_count),
Function::Sumifs => vec![Signature::Vector; arg_count],
Function::Tan => args_signature_scalars(arg_count, 1, 0),
Function::Tanh => args_signature_scalars(arg_count, 1, 0),
Function::ErrorType => args_signature_scalars(arg_count, 1, 0),
Function::Isblank => args_signature_scalars(arg_count, 1, 0),
Function::Iserr => args_signature_scalars(arg_count, 1, 0),
Function::Iserror => args_signature_scalars(arg_count, 1, 0),
Function::Iseven => args_signature_scalars(arg_count, 1, 0),
Function::Isformula => args_signature_scalars(arg_count, 1, 0),
Function::Islogical => args_signature_scalars(arg_count, 1, 0),
Function::Isna => args_signature_scalars(arg_count, 1, 0),
Function::Isnontext => args_signature_scalars(arg_count, 1, 0),
Function::Isnumber => args_signature_scalars(arg_count, 1, 0),
Function::Isodd => args_signature_scalars(arg_count, 1, 0),
Function::Isref => args_signature_one_vector(arg_count),
Function::Istext => args_signature_scalars(arg_count, 1, 0),
Function::Na => args_signature_no_args(arg_count),
Function::Sheet => args_signature_sheet(arg_count),
Function::Type => args_signature_one_vector(arg_count),
Function::Hlookup => args_signature_hlookup(arg_count),
Function::Index => args_signature_index(arg_count),
Function::Indirect => args_signature_scalars(arg_count, 1, 0),
Function::Lookup => args_signature_lookup(arg_count),
Function::Match => args_signature_match(arg_count),
Function::Offset => args_signature_offset(arg_count),
Function::Row => args_signature_row(arg_count),
Function::Rows => args_signature_one_vector(arg_count),
Function::Vlookup => args_signature_hlookup(arg_count),
Function::Xlookup => args_signature_xlookup(arg_count),
Function::Concat => vec![Signature::Vector; arg_count],
Function::Concatenate => vec![Signature::Scalar; arg_count],
Function::Exact => args_signature_scalars(arg_count, 2, 0),
Function::Find => args_signature_scalars(arg_count, 2, 1),
Function::Left => args_signature_scalars(arg_count, 1, 1),
Function::Len => args_signature_scalars(arg_count, 1, 0),
Function::Lower => args_signature_scalars(arg_count, 1, 0),
Function::Mid => args_signature_scalars(arg_count, 3, 0),
Function::Rept => args_signature_scalars(arg_count, 2, 0),
Function::Right => args_signature_scalars(arg_count, 2, 1),
Function::Search => args_signature_scalars(arg_count, 2, 1),
Function::Substitute => args_signature_scalars(arg_count, 3, 1),
Function::T => args_signature_scalars(arg_count, 1, 0),
Function::Text => args_signature_scalars(arg_count, 2, 0),
Function::Textafter => args_signature_textafter(arg_count),
Function::Textbefore => args_signature_textafter(arg_count),
Function::Textjoin => args_signature_textjoin(arg_count),
Function::Trim => args_signature_scalars(arg_count, 1, 0),
Function::Upper => args_signature_scalars(arg_count, 1, 0),
Function::Value => args_signature_scalars(arg_count, 1, 0),
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
Function::Average => vec![Signature::Vector; arg_count],
Function::Averagea => vec![Signature::Vector; arg_count],
Function::Averageif => args_signature_sumif(arg_count),
Function::Averageifs => vec![Signature::Vector; arg_count],
Function::Count => vec![Signature::Vector; arg_count],
Function::Counta => vec![Signature::Vector; arg_count],
Function::Countblank => vec![Signature::Vector; arg_count],
Function::Countif => args_signature_sumif(arg_count),
Function::Countifs => vec![Signature::Vector; arg_count],
Function::Maxifs => vec![Signature::Vector; arg_count],
Function::Minifs => vec![Signature::Vector; arg_count],
Function::Date => args_signature_scalars(arg_count, 3, 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::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::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),
Function::Ddb => args_signature_scalars(arg_count, 4, 1),
Function::Dollarde => args_signature_scalars(arg_count, 2, 0),
Function::Dollarfr => args_signature_scalars(arg_count, 2, 0),
Function::Effect => args_signature_scalars(arg_count, 2, 0),
Function::Fv => args_signature_scalars(arg_count, 3, 2),
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
Function::Irr => args_signature_irr(arg_count),
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
Function::Mirr => args_signature_mirr(arg_count),
Function::Nominal => args_signature_scalars(arg_count, 2, 0),
Function::Nper => args_signature_scalars(arg_count, 3, 2),
Function::Npv => args_signature_npv(arg_count),
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
Function::Pv => args_signature_scalars(arg_count, 3, 2),
Function::Rate => args_signature_scalars(arg_count, 3, 3),
Function::Rri => args_signature_scalars(arg_count, 3, 0),
Function::Sln => args_signature_scalars(arg_count, 3, 0),
Function::Syd => args_signature_scalars(arg_count, 4, 0),
Function::Tbilleq => args_signature_scalars(arg_count, 3, 0),
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
Function::Xirr => args_signature_xirr(arg_count),
Function::Xnpv => args_signature_xnpv(arg_count),
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
Function::Besselj => args_signature_scalars(arg_count, 2, 0),
Function::Besselk => args_signature_scalars(arg_count, 2, 0),
Function::Bessely => args_signature_scalars(arg_count, 2, 0),
Function::Erf => args_signature_scalars(arg_count, 1, 1),
Function::Erfc => args_signature_scalars(arg_count, 1, 0),
Function::ErfcPrecise => args_signature_scalars(arg_count, 1, 0),
Function::ErfPrecise => args_signature_scalars(arg_count, 1, 0),
Function::Bin2dec => args_signature_scalars(arg_count, 1, 0),
Function::Bin2hex => args_signature_scalars(arg_count, 1, 0),
Function::Bin2oct => args_signature_scalars(arg_count, 1, 0),
Function::Dec2Bin => args_signature_scalars(arg_count, 1, 0),
Function::Dec2hex => args_signature_scalars(arg_count, 1, 0),
Function::Dec2oct => args_signature_scalars(arg_count, 1, 0),
Function::Hex2bin => args_signature_scalars(arg_count, 1, 0),
Function::Hex2dec => args_signature_scalars(arg_count, 1, 0),
Function::Hex2oct => args_signature_scalars(arg_count, 1, 0),
Function::Oct2bin => args_signature_scalars(arg_count, 1, 0),
Function::Oct2dec => args_signature_scalars(arg_count, 1, 0),
Function::Oct2hex => args_signature_scalars(arg_count, 1, 0),
Function::Bitand => args_signature_scalars(arg_count, 2, 0),
Function::Bitlshift => args_signature_scalars(arg_count, 2, 0),
Function::Bitor => args_signature_scalars(arg_count, 2, 0),
Function::Bitrshift => args_signature_scalars(arg_count, 2, 0),
Function::Bitxor => args_signature_scalars(arg_count, 2, 0),
Function::Complex => args_signature_scalars(arg_count, 2, 1),
Function::Imabs => args_signature_scalars(arg_count, 1, 0),
Function::Imaginary => args_signature_scalars(arg_count, 1, 0),
Function::Imargument => args_signature_scalars(arg_count, 1, 0),
Function::Imconjugate => args_signature_scalars(arg_count, 1, 0),
Function::Imcos => args_signature_scalars(arg_count, 1, 0),
Function::Imcosh => args_signature_scalars(arg_count, 1, 0),
Function::Imcot => args_signature_scalars(arg_count, 1, 0),
Function::Imcsc => args_signature_scalars(arg_count, 1, 0),
Function::Imcsch => args_signature_scalars(arg_count, 1, 0),
Function::Imdiv => args_signature_scalars(arg_count, 2, 0),
Function::Imexp => args_signature_scalars(arg_count, 1, 0),
Function::Imln => args_signature_scalars(arg_count, 1, 0),
Function::Imlog10 => args_signature_scalars(arg_count, 1, 0),
Function::Imlog2 => args_signature_scalars(arg_count, 1, 0),
Function::Impower => args_signature_scalars(arg_count, 2, 0),
Function::Improduct => args_signature_scalars(arg_count, 2, 0),
Function::Imreal => args_signature_scalars(arg_count, 1, 0),
Function::Imsec => args_signature_scalars(arg_count, 1, 0),
Function::Imsech => args_signature_scalars(arg_count, 1, 0),
Function::Imsin => args_signature_scalars(arg_count, 1, 0),
Function::Imsinh => args_signature_scalars(arg_count, 1, 0),
Function::Imsqrt => args_signature_scalars(arg_count, 1, 0),
Function::Imsub => args_signature_scalars(arg_count, 2, 0),
Function::Imsum => args_signature_scalars(arg_count, 2, 0),
Function::Imtan => args_signature_scalars(arg_count, 1, 0),
Function::Convert => args_signature_scalars(arg_count, 3, 0),
Function::Delta => args_signature_scalars(arg_count, 1, 1),
Function::Gestep => args_signature_scalars(arg_count, 1, 1),
Function::Subtotal => args_signature_npv(arg_count),
Function::Rand => args_signature_no_args(arg_count),
Function::Randbetween => args_signature_scalars(arg_count, 2, 0),
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],
}
}
// Returns the type of the result (Scalar, Array or Range) depending on the arguments
fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
match kind {
Function::And => StaticResult::Scalar,
Function::False => StaticResult::Scalar,
Function::If => scalar_arguments(args),
Function::Iferror => scalar_arguments(args),
Function::Ifna => scalar_arguments(args),
Function::Ifs => not_implemented(args),
Function::Not => StaticResult::Scalar,
Function::Or => StaticResult::Scalar,
Function::Switch => not_implemented(args),
Function::True => StaticResult::Scalar,
Function::Xor => StaticResult::Scalar,
Function::Abs => scalar_arguments(args),
Function::Acos => scalar_arguments(args),
Function::Acosh => scalar_arguments(args),
Function::Asin => scalar_arguments(args),
Function::Asinh => scalar_arguments(args),
Function::Atan => scalar_arguments(args),
Function::Atan2 => scalar_arguments(args),
Function::Atanh => scalar_arguments(args),
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
Function::Column => not_implemented(args),
Function::Columns => not_implemented(args),
Function::Cos => scalar_arguments(args),
Function::Cosh => scalar_arguments(args),
Function::Max => StaticResult::Scalar,
Function::Min => StaticResult::Scalar,
Function::Pi => StaticResult::Scalar,
Function::Power => scalar_arguments(args),
Function::Product => not_implemented(args),
Function::Round => scalar_arguments(args),
Function::Rounddown => scalar_arguments(args),
Function::Roundup => scalar_arguments(args),
Function::Sin => scalar_arguments(args),
Function::Sinh => scalar_arguments(args),
Function::Sqrt => scalar_arguments(args),
Function::Sqrtpi => StaticResult::Scalar,
Function::Sum => StaticResult::Scalar,
Function::Sumif => not_implemented(args),
Function::Sumifs => not_implemented(args),
Function::Tan => scalar_arguments(args),
Function::Tanh => scalar_arguments(args),
Function::ErrorType => not_implemented(args),
Function::Isblank => not_implemented(args),
Function::Iserr => not_implemented(args),
Function::Iserror => not_implemented(args),
Function::Iseven => not_implemented(args),
Function::Isformula => not_implemented(args),
Function::Islogical => not_implemented(args),
Function::Isna => not_implemented(args),
Function::Isnontext => not_implemented(args),
Function::Isnumber => not_implemented(args),
Function::Isodd => not_implemented(args),
Function::Isref => not_implemented(args),
Function::Istext => not_implemented(args),
Function::Na => StaticResult::Scalar,
Function::Sheet => StaticResult::Scalar,
Function::Type => not_implemented(args),
Function::Hlookup => not_implemented(args),
Function::Index => static_analysis_index(args),
Function::Indirect => static_analysis_indirect(args),
Function::Lookup => not_implemented(args),
Function::Match => not_implemented(args),
Function::Offset => static_analysis_offset(args),
// FIXME: Row could return an array
Function::Row => StaticResult::Scalar,
Function::Rows => not_implemented(args),
Function::Vlookup => not_implemented(args),
Function::Xlookup => not_implemented(args),
Function::Concat => not_implemented(args),
Function::Concatenate => not_implemented(args),
Function::Exact => not_implemented(args),
Function::Find => not_implemented(args),
Function::Left => not_implemented(args),
Function::Len => not_implemented(args),
Function::Lower => not_implemented(args),
Function::Mid => not_implemented(args),
Function::Rept => not_implemented(args),
Function::Right => not_implemented(args),
Function::Search => not_implemented(args),
Function::Substitute => not_implemented(args),
Function::T => not_implemented(args),
Function::Text => not_implemented(args),
Function::Textafter => not_implemented(args),
Function::Textbefore => not_implemented(args),
Function::Textjoin => not_implemented(args),
Function::Trim => not_implemented(args),
Function::Unicode => not_implemented(args),
Function::Upper => not_implemented(args),
Function::Value => not_implemented(args),
Function::Valuetotext => not_implemented(args),
Function::Average => not_implemented(args),
Function::Averagea => not_implemented(args),
Function::Averageif => not_implemented(args),
Function::Averageifs => not_implemented(args),
Function::Count => not_implemented(args),
Function::Counta => not_implemented(args),
Function::Countblank => not_implemented(args),
Function::Countif => not_implemented(args),
Function::Countifs => not_implemented(args),
Function::Maxifs => not_implemented(args),
Function::Minifs => not_implemented(args),
Function::Date => not_implemented(args),
Function::Day => not_implemented(args),
Function::Edate => not_implemented(args),
Function::Month => not_implemented(args),
Function::Now => not_implemented(args),
Function::Today => not_implemented(args),
Function::Year => not_implemented(args),
Function::Cumipmt => not_implemented(args),
Function::Cumprinc => not_implemented(args),
Function::Db => not_implemented(args),
Function::Ddb => not_implemented(args),
Function::Dollarde => not_implemented(args),
Function::Dollarfr => not_implemented(args),
Function::Effect => not_implemented(args),
Function::Fv => not_implemented(args),
Function::Ipmt => not_implemented(args),
Function::Irr => not_implemented(args),
Function::Ispmt => not_implemented(args),
Function::Mirr => not_implemented(args),
Function::Nominal => not_implemented(args),
Function::Nper => not_implemented(args),
Function::Npv => not_implemented(args),
Function::Pduration => not_implemented(args),
Function::Pmt => not_implemented(args),
Function::Ppmt => not_implemented(args),
Function::Pv => not_implemented(args),
Function::Rate => not_implemented(args),
Function::Rri => not_implemented(args),
Function::Sln => not_implemented(args),
Function::Syd => not_implemented(args),
Function::Tbilleq => not_implemented(args),
Function::Tbillprice => not_implemented(args),
Function::Tbillyield => not_implemented(args),
Function::Xirr => not_implemented(args),
Function::Xnpv => not_implemented(args),
Function::Besseli => scalar_arguments(args),
Function::Besselj => scalar_arguments(args),
Function::Besselk => scalar_arguments(args),
Function::Bessely => scalar_arguments(args),
Function::Erf => scalar_arguments(args),
Function::Erfc => scalar_arguments(args),
Function::ErfcPrecise => scalar_arguments(args),
Function::ErfPrecise => scalar_arguments(args),
Function::Bin2dec => scalar_arguments(args),
Function::Bin2hex => scalar_arguments(args),
Function::Bin2oct => scalar_arguments(args),
Function::Dec2Bin => scalar_arguments(args),
Function::Dec2hex => scalar_arguments(args),
Function::Dec2oct => scalar_arguments(args),
Function::Hex2bin => scalar_arguments(args),
Function::Hex2dec => scalar_arguments(args),
Function::Hex2oct => scalar_arguments(args),
Function::Oct2bin => scalar_arguments(args),
Function::Oct2dec => scalar_arguments(args),
Function::Oct2hex => scalar_arguments(args),
Function::Bitand => scalar_arguments(args),
Function::Bitlshift => scalar_arguments(args),
Function::Bitor => scalar_arguments(args),
Function::Bitrshift => scalar_arguments(args),
Function::Bitxor => scalar_arguments(args),
Function::Complex => scalar_arguments(args),
Function::Imabs => scalar_arguments(args),
Function::Imaginary => scalar_arguments(args),
Function::Imargument => scalar_arguments(args),
Function::Imconjugate => scalar_arguments(args),
Function::Imcos => scalar_arguments(args),
Function::Imcosh => scalar_arguments(args),
Function::Imcot => scalar_arguments(args),
Function::Imcsc => scalar_arguments(args),
Function::Imcsch => scalar_arguments(args),
Function::Imdiv => scalar_arguments(args),
Function::Imexp => scalar_arguments(args),
Function::Imln => scalar_arguments(args),
Function::Imlog10 => scalar_arguments(args),
Function::Imlog2 => scalar_arguments(args),
Function::Impower => scalar_arguments(args),
Function::Improduct => scalar_arguments(args),
Function::Imreal => scalar_arguments(args),
Function::Imsec => scalar_arguments(args),
Function::Imsech => scalar_arguments(args),
Function::Imsin => scalar_arguments(args),
Function::Imsinh => scalar_arguments(args),
Function::Imsqrt => scalar_arguments(args),
Function::Imsub => scalar_arguments(args),
Function::Imsum => scalar_arguments(args),
Function::Imtan => scalar_arguments(args),
Function::Convert => not_implemented(args),
Function::Delta => not_implemented(args),
Function::Gestep => not_implemented(args),
Function::Subtotal => not_implemented(args),
Function::Rand => not_implemented(args),
Function::Randbetween => scalar_arguments(args),
Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args),
}
}

View File

@@ -1,5 +1,7 @@
use super::{super::utils::quote_name, Node, Reference}; use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::move_formula::to_string_array_node;
use crate::expressions::parser::static_analysis::add_implicit_intersection;
use crate::expressions::token::OpUnary; use crate::expressions::token::OpUnary;
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str}; use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
@@ -34,10 +36,21 @@ pub enum DisplaceData {
None, None,
} }
/// This is the internal mode in IronCalc
pub fn to_rc_format(node: &Node) -> String { pub fn to_rc_format(node: &Node) -> String {
stringify(node, None, &DisplaceData::None, false) stringify(node, None, &DisplaceData::None, false)
} }
/// This is the mode used to display the formula in the UI
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false)
}
/// This is the mode used to export the formula to Excel
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true)
}
pub fn to_string_displaced( pub fn to_string_displaced(
node: &Node, node: &Node,
context: &CellReferenceRC, context: &CellReferenceRC,
@@ -46,18 +59,10 @@ pub fn to_string_displaced(
stringify(node, Some(context), displace_data, false) stringify(node, Some(context), displace_data, false)
} }
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false)
}
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true)
}
/// Converts a local reference to a string applying some displacement if needed. /// Converts a local reference to a string applying some displacement if needed.
/// It uses A1 style if context is not None. If context is None it uses R1C1 style /// It uses A1 style if context is not None. If context is None it uses R1C1 style
/// If full_row is true then the row details will be omitted in the A1 case /// If full_row is true then the row details will be omitted in the A1 case
/// If full_colum is true then column details will be omitted. /// If full_column is true then column details will be omitted.
pub(crate) fn stringify_reference( pub(crate) fn stringify_reference(
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
@@ -179,16 +184,16 @@ pub(crate) fn stringify_reference(
return "#REF!".to_string(); return "#REF!".to_string();
} }
let mut row_abs = if absolute_row { let mut row_abs = if absolute_row {
format!("${}", row) format!("${row}")
} else { } else {
format!("{}", row) format!("{row}")
}; };
let column = match crate::expressions::utils::number_to_column(column) { let column = match crate::expressions::utils::number_to_column(column) {
Some(s) => s, Some(s) => s,
None => return "#REF!".to_string(), None => return "#REF!".to_string(),
}; };
let mut col_abs = if absolute_column { let mut col_abs = if absolute_column {
format!("${}", column) format!("${column}")
} else { } else {
column column
}; };
@@ -203,27 +208,27 @@ pub(crate) fn stringify_reference(
format!("{}!{}{}", quote_name(name), col_abs, row_abs) format!("{}!{}{}", quote_name(name), col_abs, row_abs)
} }
None => { None => {
format!("{}{}", col_abs, row_abs) format!("{col_abs}{row_abs}")
} }
} }
} }
None => { None => {
let row_abs = if absolute_row { let row_abs = if absolute_row {
format!("R{}", row) format!("R{row}")
} else { } else {
format!("R[{}]", row) format!("R[{row}]")
}; };
let col_abs = if absolute_column { let col_abs = if absolute_column {
format!("C{}", column) format!("C{column}")
} else { } else {
format!("C[{}]", column) format!("C[{column}]")
}; };
match &sheet_name { match &sheet_name {
Some(name) => { Some(name) => {
format!("{}!{}{}", quote_name(name), row_abs, col_abs) format!("{}!{}{}", quote_name(name), row_abs, col_abs)
} }
None => { None => {
format!("{}{}", row_abs, col_abs) format!("{row_abs}{col_abs}")
} }
} }
} }
@@ -235,7 +240,7 @@ fn format_function(
args: &Vec<Node>, args: &Vec<Node>,
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
use_original_name: bool, export_to_excel: bool,
) -> String { ) -> String {
let mut first = true; let mut first = true;
let mut arguments = "".to_string(); let mut arguments = "".to_string();
@@ -244,27 +249,52 @@ fn format_function(
arguments = format!( arguments = format!(
"{},{}", "{},{}",
arguments, arguments,
stringify(el, context, displace_data, use_original_name) stringify(el, context, displace_data, export_to_excel)
); );
} else { } else {
first = false; first = false;
arguments = stringify(el, context, displace_data, use_original_name); arguments = stringify(el, context, displace_data, export_to_excel);
} }
} }
format!("{}({})", name, arguments) format!("{name}({arguments})")
} }
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
// But three different ways to convert it to a string.
//
// To stringify a formula we need a "context", that is in which cell are we doing the "stringifying"
//
// But there are three ways to stringify a formula:
//
// * To show it to the IronCalc user
// * To store internally
// * To export to Excel
//
// There are, of course correspondingly three "modes" when parsing a formula.
//
// The internal representation is the more different as references are stored in the RC representation.
// The the AST of the formula is kept close to this representation we don't need a context
//
// In the export to Excel representation certain things are different:
// * We add a _xlfn. in front of some (more modern) functions
// * We remove the Implicit Intersection operator when it is automatic and add _xlfn.SINGLE when it is not
//
// Examples:
// * =A1+B2
// * =RC+R1C1
// * =A1+B1
fn stringify( fn stringify(
node: &Node, node: &Node,
context: Option<&CellReferenceRC>, context: Option<&CellReferenceRC>,
displace_data: &DisplaceData, displace_data: &DisplaceData,
use_original_name: bool, export_to_excel: bool,
) -> String { ) -> String {
use self::Node::*; use self::Node::*;
match node { match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(), BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number), NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value), StringKind(value) => format!("\"{value}\""),
WrongReferenceKind { WrongReferenceKind {
sheet_name, sheet_name,
column, column,
@@ -354,7 +384,7 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
WrongRangeKind { WrongRangeKind {
sheet_name, sheet_name,
@@ -403,58 +433,58 @@ fn stringify(
full_row, full_row,
full_column, full_column,
); );
format!("{}:{}", s1, s2) format!("{s1}:{s2}")
} }
OpRangeKind { left, right } => format!( OpRangeKind { left, right } => format!(
"{}:{}", "{}:{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
OpConcatenateKind { left, right } => format!( OpConcatenateKind { left, right } => format!(
"{}&{}", "{}&{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
kind, kind,
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
OpSumKind { kind, left, right } => format!( OpSumKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
stringify(left, context, displace_data, use_original_name), stringify(left, context, displace_data, export_to_excel),
kind, kind,
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
OpProductKind { kind, left, right } => { OpProductKind { kind, left, right } => {
let x = match **left { let x = match **left {
OpSumKind { .. } => format!( OpSumKind { .. } => format!(
"({})", "({})",
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, export_to_excel)
), ),
CompareKind { .. } => format!( CompareKind { .. } => format!(
"({})", "({})",
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, export_to_excel)
), ),
_ => stringify(left, context, displace_data, use_original_name), _ => stringify(left, context, displace_data, export_to_excel),
}; };
let y = match **right { let y = match **right {
OpSumKind { .. } => format!( OpSumKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
CompareKind { .. } => format!( CompareKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
OpProductKind { .. } => format!( OpProductKind { .. } => format!(
"({})", "({})",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
_ => stringify(right, context, displace_data, use_original_name), _ => stringify(right, context, displace_data, export_to_excel),
}; };
format!("{}{}{}", x, kind, y) format!("{x}{kind}{y}")
} }
OpPowerKind { left, right } => { OpPowerKind { left, right } => {
let x = match **left { let x = match **left {
@@ -467,9 +497,7 @@ fn stringify(
| DefinedNameKind(_) | DefinedNameKind(_)
| TableNameKind(_) | TableNameKind(_)
| WrongVariableKind(_) | WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
stringify(left, context, displace_data, use_original_name)
}
OpRangeKind { .. } OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
| OpProductKind { .. } | OpProductKind { .. }
@@ -482,9 +510,10 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
| CompareKind { .. } | CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!( | EmptyArgKind => format!(
"({})", "({})",
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, export_to_excel)
), ),
}; };
let y = match **right { let y = match **right {
@@ -498,7 +527,7 @@ fn stringify(
| TableNameKind(_) | TableNameKind(_)
| WrongVariableKind(_) | WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
} }
OpRangeKind { .. } OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
@@ -512,65 +541,96 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
| CompareKind { .. } | CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!( | EmptyArgKind => format!(
"({})", "({})",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
), ),
}; };
format!("{}^{}", x, y) format!("{x}^{y}")
} }
InvalidFunctionKind { name, args } => { InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, use_original_name) format_function(name, args, context, displace_data, export_to_excel)
} }
FunctionKind { kind, args } => { FunctionKind { kind, args } => {
let name = if use_original_name { let name = if export_to_excel {
kind.to_xlsx_string() kind.to_xlsx_string()
} else { } else {
kind.to_string() kind.to_string()
}; };
format_function(&name, args, context, displace_data, use_original_name) format_function(&name, args, context, displace_data, export_to_excel)
} }
ArrayKind(args) => { ArrayKind(args) => {
let mut first = true; let mut first_row = true;
let mut arguments = "".to_string(); let mut matrix_string = String::new();
for el in args {
if !first { for row in args {
arguments = format!( if !first_row {
"{},{}", matrix_string.push(';');
arguments,
stringify(el, context, displace_data, use_original_name)
);
} else { } else {
first = false; first_row = false;
arguments = stringify(el, context, displace_data, use_original_name);
} }
let mut first_column = true;
let mut row_string = String::new();
for el in row {
if !first_column {
row_string.push(',');
} else {
first_column = false;
} }
format!("{{{}}}", arguments) row_string.push_str(&to_string_array_node(el));
}
matrix_string.push_str(&row_string);
}
format!("{{{matrix_string}}}")
} }
TableNameKind(value) => value.to_string(), TableNameKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(), DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
format!( format!(
"-{}", "-{}",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
) )
} }
OpUnary::Percentage => { OpUnary::Percentage => {
format!( format!(
"{}%", "{}%",
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, export_to_excel)
) )
} }
}, },
ErrorKind(kind) => format!("{}", kind), ErrorKind(kind) => format!("{kind}"),
ParseErrorKind { ParseErrorKind {
formula, formula,
position: _, position: _,
message: _, message: _,
} => formula.to_string(), } => formula.to_string(),
EmptyArgKind => "".to_string(), EmptyArgKind => "".to_string(),
ImplicitIntersection {
automatic: _,
child,
} => {
if export_to_excel {
// We need to check wether the II can be automatic or not
let mut new_node = child.as_ref().clone();
add_implicit_intersection(&mut new_node, true);
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
return stringify(child, context, displace_data, export_to_excel);
}
return format!(
"_xlfn.SINGLE({})",
stringify(child, context, displace_data, export_to_excel)
);
}
format!(
"@{}",
stringify(child, context, displace_data, export_to_excel)
)
}
} }
} }
@@ -658,6 +718,12 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::UnaryKind { kind: _, right } => { Node::UnaryKind { kind: _, right } => {
rename_sheet_in_node(right, sheet_index, new_name); rename_sheet_in_node(right, sheet_index, new_name);
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_sheet_in_node(child, sheet_index, new_name);
}
// Do nothing // Do nothing
Node::BooleanKind(_) => {} Node::BooleanKind(_) => {}
@@ -681,7 +747,7 @@ pub(crate) fn rename_defined_name_in_node(
) { ) {
match node { match node {
// Rename // Rename
Node::DefinedNameKind((n, s)) => { Node::DefinedNameKind((n, s, _)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope { if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string(); *n = new_name.to_string();
} }
@@ -736,6 +802,12 @@ pub(crate) fn rename_defined_name_in_node(
Node::UnaryKind { kind: _, right } => { Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name); rename_defined_name_in_node(right, name, scope, new_name);
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_defined_name_in_node(child, name, scope, new_name);
}
// Do nothing // Do nothing
Node::BooleanKind(_) => {} Node::BooleanKind(_) => {}

View File

@@ -1,4 +1,7 @@
mod test_add_implicit_intersection;
mod test_arrays;
mod test_general; mod test_general;
mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_move_formula; mod test_move_formula;
mod test_ranges; mod test_ranges;

View File

@@ -0,0 +1,80 @@
use std::collections::HashMap;
use crate::expressions::{
parser::{
stringify::{to_excel_string, to_string},
Parser,
},
types::CellReferenceRC,
};
use crate::expressions::parser::static_analysis::add_implicit_intersection;
#[test]
fn simple_test() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let cases = vec![
("A1:A10*SUM(A1:A10)", "@A1:A10*SUM(A1:A10)"),
("A1:A10", "@A1:A10"),
// Math and trigonometry functions
("SUM(A1:A10)", "SUM(A1:A10)"),
("SIN(A1:A10)", "SIN(@A1:A10)"),
("COS(A1:A10)", "COS(@A1:A10)"),
("TAN(A1:A10)", "TAN(@A1:A10)"),
("ASIN(A1:A10)", "ASIN(@A1:A10)"),
("ACOS(A1:A10)", "ACOS(@A1:A10)"),
("ATAN(A1:A10)", "ATAN(@A1:A10)"),
("SINH(A1:A10)", "SINH(@A1:A10)"),
("COSH(A1:A10)", "COSH(@A1:A10)"),
("TANH(A1:A10)", "TANH(@A1:A10)"),
("ASINH(A1:A10)", "ASINH(@A1:A10)"),
("ACOSH(A1:A10)", "ACOSH(@A1:A10)"),
("ATANH(A1:A10)", "ATANH(@A1:A10)"),
("ATAN2(A1:A10,B1:B10)", "ATAN2(@A1:A10,@B1:B10)"),
("ATAN2(A1:A10,A1)", "ATAN2(@A1:A10,A1)"),
("SQRT(A1:A10)", "SQRT(@A1:A10)"),
("SQRTPI(A1:A10)", "SQRTPI(@A1:A10)"),
("POWER(A1:A10,A1)", "POWER(@A1:A10,A1)"),
("POWER(A1:A10,B1:B10)", "POWER(@A1:A10,@B1:B10)"),
("MAX(A1:A10)", "MAX(A1:A10)"),
("MIN(A1:A10)", "MIN(A1:A10)"),
("ABS(A1:A10)", "ABS(@A1:A10)"),
("FALSE()", "FALSE()"),
("TRUE()", "TRUE()"),
// Defined names
("BADNMAE", "@BADNMAE"),
// Logical
("AND(A1:A10)", "AND(A1:A10)"),
("OR(A1:A10)", "OR(A1:A10)"),
("NOT(A1:A10)", "NOT(@A1:A10)"),
("IF(A1:A10,B1:B10,C1:C10)", "IF(@A1:A10,@B1:B10,@C1:C10)"),
// Information
// ("ISBLANK(A1:A10)", "ISBLANK(A1:A10)"),
// ("ISERR(A1:A10)", "ISERR(A1:A10)"),
// ("ISERROR(A1:A10)", "ISERROR(A1:A10)"),
// ("ISEVEN(A1:A10)", "ISEVEN(A1:A10)"),
// ("ISLOGICAL(A1:A10)", "ISLOGICAL(A1:A10)"),
// ("ISNA(A1:A10)", "ISNA(A1:A10)"),
// ("ISNONTEXT(A1:A10)", "ISNONTEXT(A1:A10)"),
// ("ISNUMBER(A1:A10)", "ISNUMBER(A1:A10)"),
// ("ISODD(A1:A10)", "ISODD(A1:A10)"),
// ("ISREF(A1:A10)", "ISREF(A1:A10)"),
// ("ISTEXT(A1:A10)", "ISTEXT(A1:A10)"),
];
for (formula, expected) in cases {
let mut t = parser.parse(formula, &cell_reference);
add_implicit_intersection(&mut t, true);
let r = to_string(&t, &cell_reference);
assert_eq!(r, expected);
let excel_formula = to_excel_string(&t, &cell_reference);
assert_eq!(excel_formula, formula);
}
}

View File

@@ -0,0 +1,92 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::{ArrayNode, Node, Parser};
use crate::expressions::types::CellReferenceRC;
#[test]
fn simple_horizontal() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let horizontal = parser.parse("{1, 2, 3}", &cell_reference);
assert_eq!(
horizontal,
Node::ArrayKind(vec![vec![
ArrayNode::Number(1.0),
ArrayNode::Number(2.0),
ArrayNode::Number(3.0)
]])
);
assert_eq!(to_rc_format(&horizontal), "{1,2,3}");
assert_eq!(to_string(&horizontal, &cell_reference), "{1,2,3}");
}
#[test]
fn simple_vertical() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let vertical = parser.parse("{1;2; 3}", &cell_reference);
assert_eq!(
vertical,
Node::ArrayKind(vec![
vec![ArrayNode::Number(1.0)],
vec![ArrayNode::Number(2.0)],
vec![ArrayNode::Number(3.0)]
])
);
assert_eq!(to_rc_format(&vertical), "{1;2;3}");
assert_eq!(to_string(&vertical, &cell_reference), "{1;2;3}");
}
#[test]
fn simple_matrix() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let matrix = parser.parse("{1,2,3; 4, 5, 6; 7,8,9}", &cell_reference);
assert_eq!(
matrix,
Node::ArrayKind(vec![
vec![
ArrayNode::Number(1.0),
ArrayNode::Number(2.0),
ArrayNode::Number(3.0)
],
vec![
ArrayNode::Number(4.0),
ArrayNode::Number(5.0),
ArrayNode::Number(6.0)
],
vec![
ArrayNode::Number(7.0),
ArrayNode::Number(8.0),
ArrayNode::Number(9.0)
]
])
);
assert_eq!(to_rc_format(&matrix), "{1,2,3;4,5,6;7,8,9}");
assert_eq!(to_string(&matrix, &cell_reference), "{1,2,3;4,5,6;7,8,9}");
}

View File

@@ -0,0 +1,75 @@
#![allow(clippy::panic)]
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
use std::collections::HashMap;
#[test]
fn simple() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 3,
column: 2,
};
let t = parser.parse("@A1:A10", &cell_reference);
let child = Node::RangeKind {
sheet_name: None,
sheet_index: 0,
absolute_row1: false,
absolute_column1: false,
row1: -2,
column1: -1,
absolute_row2: false,
absolute_column2: false,
row2: 7,
column2: -1,
};
assert_eq!(
t,
Node::ImplicitIntersection {
automatic: false,
child: Box::new(child)
}
)
}
#[test]
fn simple_add() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!B3
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 3,
column: 2,
};
let t = parser.parse("@A1:A10+12", &cell_reference);
let child = Node::RangeKind {
sheet_name: None,
sheet_index: 0,
absolute_row1: false,
absolute_column1: false,
row1: -2,
column1: -1,
absolute_row2: false,
absolute_column2: false,
row2: 7,
column2: -1,
};
assert_eq!(
t,
Node::OpSumKind {
kind: crate::expressions::token::OpSum::Add,
left: Box::new(Node::ImplicitIntersection {
automatic: false,
child: Box::new(child)
}),
right: Box::new(Node::NumberKind(12.0))
}
)
}

View File

@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
width: 4, width: 4,
height: 5, height: 5,
}; };
let node = parser.parse("X9^C2-F4*H2", context); let node = parser.parse("X9^C2-F4*H2+SUM(F2:H4)+SUM(C2:F6)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -400,7 +400,7 @@ fn test_move_formula_misc() {
column_delta: 10, column_delta: 10,
}, },
); );
assert_eq!(t, "X9^M12-P14*H2"); assert_eq!(t, "X9^M12-P14*H2+SUM(F2:H4)+SUM(M12:P16)");
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context); let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
let t = move_formula( let t = move_formula(
@@ -475,3 +475,77 @@ fn test_move_formula_another_sheet() {
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)" "Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
); );
} }
#[test]
fn move_formula_implicit_intersetion() {
// context is E4
let row = 4;
let column = 5;
let context = &CellReferenceRC {
sheet: "Sheet1".to_string(),
row,
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
sheet: 0,
row: 2,
column: 3,
width: 4,
height: 5,
};
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)", context);
let t = move_formula(
&node,
&MoveContext {
source_sheet_name: "Sheet1",
row,
column,
area,
target_sheet_name: "Sheet1",
row_delta: 10,
column_delta: 10,
},
);
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)");
}
#[test]
fn move_formula_implicit_intersetion_with_ranges() {
// context is E4
let row = 4;
let column = 5;
let context = &CellReferenceRC {
sheet: "Sheet1".to_string(),
row,
column,
};
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6
let area = &Area {
sheet: 0,
row: 2,
column: 3,
width: 4,
height: 5,
};
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)+SUM(@A1, @X9, @$D$5)", context);
let t = move_formula(
&node,
&MoveContext {
source_sheet_name: "Sheet1",
row,
column,
area,
target_sheet_name: "Sheet1",
row_delta: 10,
column_delta: 10,
},
);
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)+SUM(@A1,@X9,@$N$15)");
}

View File

@@ -3,11 +3,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string; use crate::expressions::parser::stringify::to_string;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
use crate::expressions::parser::Parser; use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC; use crate::expressions::types::CellReferenceRC;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
fn create_test_table( fn create_test_table(
table_name: &str, table_name: &str,

View File

@@ -1,278 +0,0 @@
use super::{move_formula::ref_is_in_area, Node};
use crate::expressions::types::{Area, CellReferenceIndex};
pub(crate) fn forward_references(
node: &mut Node,
context: &CellReferenceIndex,
source_area: &Area,
target_sheet: u32,
target_sheet_name: &str,
target_row: i32,
target_column: i32,
) {
match node {
Node::ReferenceKind {
sheet_name,
sheet_index: reference_sheet,
absolute_row,
absolute_column,
row: reference_row,
column: reference_column,
} => {
let reference_row_absolute = if *absolute_row {
*reference_row
} else {
*reference_row + context.row
};
let reference_column_absolute = if *absolute_column {
*reference_column
} else {
*reference_column + context.column
};
if ref_is_in_area(
*reference_sheet,
reference_row_absolute,
reference_column_absolute,
source_area,
) {
if *reference_sheet != target_sheet {
*sheet_name = Some(target_sheet_name.to_string());
*reference_sheet = target_sheet;
}
*reference_row = target_row + *reference_row - source_area.row;
*reference_column = target_column + *reference_column - source_area.column;
}
}
Node::RangeKind {
sheet_name,
sheet_index,
absolute_row1,
absolute_column1,
row1,
column1,
absolute_row2,
absolute_column2,
row2,
column2,
} => {
let reference_row1 = if *absolute_row1 {
*row1
} else {
*row1 + context.row
};
let reference_column1 = if *absolute_column1 {
*column1
} else {
*column1 + context.column
};
let reference_row2 = if *absolute_row2 {
*row2
} else {
*row2 + context.row
};
let reference_column2 = if *absolute_column2 {
*column2
} else {
*column2 + context.column
};
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
{
if *sheet_index != target_sheet {
*sheet_index = target_sheet;
*sheet_name = Some(target_sheet_name.to_string());
}
*row1 = target_row + *row1 - source_area.row;
*column1 = target_column + *column1 - source_area.column;
*row2 = target_row + *row2 - source_area.row;
*column2 = target_column + *column2 - source_area.column;
}
}
// Recurse
Node::OpRangeKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpConcatenateKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::OpPowerKind { left, right } => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
forward_references(
arg,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
forward_references(
arg,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
forward_references(
left,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
Node::UnaryKind { kind: _, right } => {
forward_references(
right,
context,
source_area,
target_sheet,
target_sheet_name,
target_row,
target_column,
);
}
// TODO: Not implemented
Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {}
Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {}
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
}
}

View File

@@ -197,7 +197,7 @@ pub fn is_english_error_string(name: &str) -> bool {
"#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!", "#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!",
"#CALC!", "#CIRC!", "#NULL!", "#CALC!", "#CIRC!", "#NULL!",
]; ];
names.iter().any(|e| *e == name) names.contains(&name)
} }
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -240,6 +240,7 @@ pub enum TokenType {
Bang, // ! Bang, // !
Percent, // % Percent, // %
And, // & And, // &
At, // @
Reference { Reference {
sheet: Option<String>, sheet: Option<String>,
row: i32, row: i32,

View File

@@ -21,14 +21,12 @@ fn is_date_within_range(date: NaiveDate) -> bool {
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> { pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 { if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!( return Err(format!(
"Excel date must be greater than {}", "Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
MINIMUM_DATE_SERIAL_NUMBER
)); ));
}; };
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 { if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
return Err(format!( return Err(format!(
"Excel date must be less than {}", "Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
MAXIMUM_DATE_SERIAL_NUMBER
)); ));
}; };
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]

View File

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

View File

@@ -178,10 +178,7 @@ impl Lexer {
} }
} }
self.position = position; self.position = position;
match chars.parse::<f64>() { chars.parse::<f64>().ok()
Err(_) => None,
Ok(v) => Some(v),
}
} }
fn consume_condition(&mut self) -> Option<(Compare, f64)> { fn consume_condition(&mut self) -> Option<(Compare, f64)> {
@@ -336,7 +333,7 @@ impl Lexer {
} else if s == '-' { } else if s == '-' {
Token::ScientificMinus Token::ScientificMinus
} else { } else {
self.set_error(&format!("Unexpected char: {}. Expected + or -", s)); self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
Token::ILLEGAL Token::ILLEGAL
} }
} else { } else {
@@ -388,14 +385,14 @@ impl Lexer {
for c in "eneral".chars() { for c in "eneral".chars() {
let cc = self.read_next_char(); let cc = self.read_next_char();
if Some(c) != cc { if Some(c) != cc {
self.set_error(&format!("Unexpected character: {}", x)); self.set_error(&format!("Unexpected character: {x}"));
return Token::ILLEGAL; return Token::ILLEGAL;
} }
} }
Token::General Token::General
} }
_ => { _ => {
self.set_error(&format!("Unexpected character: {}", x)); self.set_error(&format!("Unexpected character: {x}"));
Token::ILLEGAL Token::ILLEGAL
} }
}, },

View File

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

View File

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

View File

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

View File

@@ -235,6 +235,11 @@ impl Model {
// This cannot happen // This cannot happen
CalcResult::Number(1.0) CalcResult::Number(1.0)
} }
CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
} }
} }
pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -249,7 +254,7 @@ impl Model {
// The arg could be a defined name or a table // The arg could be a defined name or a table
// let = &args[0]; // let = &args[0];
match &args[0] { match &args[0] {
Node::DefinedNameKind((name, scope)) => { Node::DefinedNameKind((name, scope, _)) => {
// Let's see if it is a defined name // Let's see if it is a defined name
if let Some(defined_name) = self if let Some(defined_name) = self
.parsed_defined_names .parsed_defined_names

View File

@@ -161,6 +161,13 @@ impl Model {
CalcResult::Range { .. } CalcResult::Range { .. }
| CalcResult::String { .. } | CalcResult::String { .. }
| CalcResult::EmptyCell => {} | CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
if let (Some(current_result), Some(short_circuit_value)) = if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value) (result, short_circuit_value)
@@ -185,6 +192,13 @@ impl Model {
} }
// References to empty cells are ignored. If all args are ignored the result is #VALUE! // References to empty cells are ignored. If all args are ignored the result is #VALUE!
CalcResult::EmptyCell => {} CalcResult::EmptyCell => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value) if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)

View File

@@ -855,7 +855,7 @@ impl Model {
if left.row != right.row || left.column != right.column { if left.row != right.row || left.column != right.column {
// FIXME: Implicit intersection or dynamic arrays // FIXME: Implicit intersection or dynamic arrays
return CalcResult::Error { return CalcResult::Error {
error: Error::ERROR, error: Error::NIMPL,
origin: cell, origin: cell,
message: "argument must be a reference to a single cell".to_string(), message: "argument must be a reference to a single cell".to_string(),
}; };

View File

@@ -0,0 +1,100 @@
#[macro_export]
macro_rules! single_number_fn {
// The macro takes:
// 1) A function name to define (e.g. fn_sin)
// 2) The operation to apply (e.g. f64::sin)
($fn_name:ident, $op:expr) => {
pub(crate) fn $fn_name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// 1) Check exactly one argument
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
// 2) Try to get a "NumberOrArray"
match self.get_number_or_array(&args[0], cell) {
// -----------------------------------------
// Case A: It's a single number
// -----------------------------------------
Ok(NumberOrArray::Number(f)) => match $op(f) {
Ok(x) => CalcResult::Number(x),
Err(Error::DIV) => CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Divide by 0".to_string(),
},
Err(Error::VALUE) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid number".to_string(),
},
Err(e) => CalcResult::Error {
error: e,
origin: cell,
message: "Unknown error".to_string(),
},
},
// -----------------------------------------
// Case B: It's an array, so apply $op
// element-by-element.
// -----------------------------------------
Ok(NumberOrArray::Array(a)) => {
let mut array = Vec::new();
for row in a {
let mut data_row = Vec::with_capacity(row.len());
for value in row {
match value {
// If Boolean, treat as 0.0 or 1.0
ArrayNode::Boolean(b) => {
let n = if b { 1.0 } else { 0.0 };
match $op(n) {
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)),
}
}
// If Number, apply directly
ArrayNode::Number(n) => match $op(n) {
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)),
},
// If String, parse to f64 then apply or #VALUE! error
ArrayNode::String(s) => {
let node = match s.parse::<f64>() {
Ok(f) => match $op(f) {
Ok(x) => ArrayNode::Number(x),
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
Err(e) => ArrayNode::Error(e),
},
Err(_) => ArrayNode::Error(Error::VALUE),
};
data_row.push(node);
}
// If Error, propagate the error
e @ ArrayNode::Error(_) => {
data_row.push(e);
}
}
}
array.push(data_row);
}
CalcResult::Array(array)
}
// -----------------------------------------
// Case C: It's an Error => just return it
// -----------------------------------------
Err(err_result) => err_result,
}
}
};
}

View File

@@ -1,5 +1,8 @@
use crate::cast::NumberOrArray;
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex; use crate::expressions::types::CellReferenceIndex;
use crate::single_number_fn;
use crate::{ use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
}; };
@@ -169,6 +172,27 @@ impl Model {
} }
} }
} }
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
result += value;
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// We ignore booleans and strings
}
}
}
}
}
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
_ => { _ => {
// We ignore booleans and strings // We ignore booleans and strings
@@ -354,187 +378,29 @@ impl Model {
} }
} }
pub(crate) fn fn_sin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
if args.len() != 1 { single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
return CalcResult::new_args_number_error(cell); single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
} single_number_fn!(fn_sinh, |f| Ok(f64::sinh(f)));
let value = match self.get_number(&args[0], cell) { single_number_fn!(fn_cosh, |f| Ok(f64::cosh(f)));
Ok(f) => f, single_number_fn!(fn_tanh, |f| Ok(f64::tanh(f)));
Err(s) => return s, single_number_fn!(fn_asin, |f| Ok(f64::asin(f)));
}; single_number_fn!(fn_acos, |f| Ok(f64::acos(f)));
let result = value.sin(); single_number_fn!(fn_atan, |f| Ok(f64::atan(f)));
CalcResult::Number(result) single_number_fn!(fn_asinh, |f| Ok(f64::asinh(f)));
} single_number_fn!(fn_acosh, |f| Ok(f64::acosh(f)));
pub(crate) fn fn_cos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { single_number_fn!(fn_atanh, |f| Ok(f64::atanh(f)));
if args.len() != 1 { single_number_fn!(fn_abs, |f| Ok(f64::abs(f)));
return CalcResult::new_args_number_error(cell); single_number_fn!(fn_sqrt, |f| if f < 0.0 {
} Err(Error::NUM)
let value = match self.get_number(&args[0], cell) { } else {
Ok(f) => f, Ok(f64::sqrt(f))
Err(s) => return s, });
}; single_number_fn!(fn_sqrtpi, |f: f64| if f < 0.0 {
let result = value.cos(); Err(Error::NUM)
CalcResult::Number(result) } else {
} Ok((f * PI).sqrt())
});
pub(crate) fn fn_tan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.tan();
CalcResult::Number(result)
}
pub(crate) fn fn_sinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.sinh();
CalcResult::Number(result)
}
pub(crate) fn fn_cosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.cosh();
CalcResult::Number(result)
}
pub(crate) fn fn_tanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.tanh();
CalcResult::Number(result)
}
pub(crate) fn fn_asin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.asin();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ASIN".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_acos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.acos();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for COS".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_atan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.atan();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ATAN".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_asinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.asinh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ASINH".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_acosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.acosh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ACOSH".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_atanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let result = value.atanh();
if result.is_nan() || result.is_infinite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid argument for ATANH".to_string(),
};
}
CalcResult::Number(result)
}
pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() { if !args.is_empty() {
@@ -543,53 +409,6 @@ impl Model {
CalcResult::Number(PI) CalcResult::Number(PI)
} }
pub(crate) fn fn_abs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
CalcResult::Number(value.abs())
}
pub(crate) fn fn_sqrtpi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
if value < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Argument of SQRTPI should be >= 0".to_string(),
};
}
CalcResult::Number((value * PI).sqrt())
}
pub(crate) fn fn_sqrt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
if value < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Argument of SQRT should be >= 0".to_string(),
};
}
CalcResult::Number(value.sqrt())
}
pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 { if args.len() != 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);

View File

@@ -15,6 +15,7 @@ mod financial_util;
mod information; mod information;
mod logical; mod logical;
mod lookup_and_reference; mod lookup_and_reference;
mod macros;
mod mathematical; mod mathematical;
mod statistical; mod statistical;
mod subtotal; mod subtotal;
@@ -1213,7 +1214,7 @@ mod tests {
} }
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE // We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
let iter_list = Function::into_iter() let iter_list = Function::into_iter()
.map(|f| format!("{}", f).replace('.', "")) .map(|f| format!("{f}").replace('.', ""))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let len = iter_list.len(); let len = iter_list.len();

View File

@@ -134,6 +134,13 @@ impl Model {
); );
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -165,6 +172,13 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
if count == 0.0 { if count == 0.0 {

View File

@@ -182,6 +182,13 @@ impl Model {
} }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0),
CalcResult::Array(_) => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
} }
} }
} }
@@ -426,6 +433,13 @@ impl Model {
| CalcResult::Number(_) | CalcResult::Number(_)
| CalcResult::Boolean(_) | CalcResult::Boolean(_)
| CalcResult::Error { .. } => counta += 1, | CalcResult::Error { .. } => counta += 1,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }

View File

@@ -55,14 +55,14 @@ impl Model {
let mut result = "".to_string(); let mut result = "".to_string();
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::String(value) => result = format!("{}{}", result, value), CalcResult::String(value) => result = format!("{result}{value}"),
CalcResult::Number(value) => result = format!("{}{}", result, value), CalcResult::Number(value) => result = format!("{result}{value}"),
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{}TRUE", result); result = format!("{result}TRUE");
} else { } else {
result = format!("{}FALSE", result); result = format!("{result}FALSE");
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
@@ -82,25 +82,37 @@ impl Model {
column, column,
}) { }) {
CalcResult::String(value) => { CalcResult::String(value) => {
result = format!("{}{}", result, value); result = format!("{result}{value}");
}
CalcResult::Number(value) => {
result = format!("{}{}", result, value)
} }
CalcResult::Number(value) => result = format!("{result}{value}"),
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => {
if value { if value {
result = format!("{}TRUE", result); result = format!("{result}TRUE");
} else { } else {
result = format!("{}FALSE", result); result = format!("{result}FALSE");
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
CalcResult::Range { .. } => {} CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
} }
} }
} }
} }
}
}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
CalcResult::String(result) CalcResult::String(result)
@@ -125,6 +137,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let format_code = match self.get_string(&args[1], cell) { let format_code = match self.get_string(&args[1], cell) {
Ok(s) => s, Ok(s) => s,
@@ -261,7 +280,7 @@ impl Model {
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -280,6 +299,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::Number(s.chars().count() as f64); return CalcResult::Number(s.chars().count() as f64);
} }
@@ -289,7 +315,7 @@ impl Model {
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -308,6 +334,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.trim().to_owned()); return CalcResult::String(s.trim().to_owned());
} }
@@ -317,7 +350,7 @@ impl Model {
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -336,6 +369,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.to_lowercase()); return CalcResult::String(s.to_lowercase());
} }
@@ -345,7 +385,7 @@ impl Model {
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -370,6 +410,13 @@ impl Model {
message: "Empty cell".to_string(), message: "Empty cell".to_string(),
} }
} }
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
match s.chars().next() { match s.chars().next() {
@@ -392,7 +439,7 @@ impl Model {
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 { if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -411,6 +458,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
return CalcResult::String(s.to_uppercase()); return CalcResult::String(s.to_uppercase());
} }
@@ -422,7 +476,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -441,6 +495,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let num_chars = if args.len() == 2 { let num_chars = if args.len() == 2 {
match self.evaluate_node_in_context(&args[1], cell) { match self.evaluate_node_in_context(&args[1], cell) {
@@ -471,6 +532,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} else { } else {
1 1
@@ -490,7 +558,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -509,6 +577,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let num_chars = if args.len() == 2 { let num_chars = if args.len() == 2 {
match self.evaluate_node_in_context(&args[1], cell) { match self.evaluate_node_in_context(&args[1], cell) {
@@ -539,6 +614,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} else { } else {
1 1
@@ -558,7 +640,7 @@ impl Model {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let s = match self.evaluate_node_in_context(&args[0], cell) { let s = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{}", v), CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v, CalcResult::String(v) => v,
CalcResult::Boolean(b) => { CalcResult::Boolean(b) => {
if b { if b {
@@ -577,6 +659,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let start_num = match self.evaluate_node_in_context(&args[1], cell) { let start_num = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Number(v) => { CalcResult::Number(v) => {
@@ -641,6 +730,13 @@ impl Model {
}; };
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => 0, CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
let mut result = "".to_string(); let mut result = "".to_string();
let mut count: usize = 0; let mut count: usize = 0;
@@ -983,6 +1079,13 @@ impl Model {
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg | CalcResult::Range { .. } => {} CalcResult::EmptyArg | CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
} }
} }
} }
@@ -1002,6 +1105,13 @@ impl Model {
} }
} }
CalcResult::EmptyArg => {} CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}; };
} }
let result = values.join(&delimiter); let result = values.join(&delimiter);
@@ -1125,6 +1235,11 @@ impl Model {
} }
} }
CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0), CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0),
CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
} }
} }

View File

@@ -110,7 +110,7 @@ pub(crate) fn from_wildcard_to_regex(
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?") // And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
if exact { if exact {
return regex::Regex::new(&format!("^{}$", reg)); return regex::Regex::new(&format!("^{reg}$"));
} }
regex::Regex::new(reg) regex::Regex::new(reg)
} }
@@ -393,10 +393,8 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResul
// An error will match an error (never a string that is an error) // An error will match an error (never a string that is an error)
Box::new(move |x| result_is_equal_to_error(x, &error.to_string())) Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
} }
CalcResult::Range { left: _, right: _ } => { CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false),
// TODO: Implicit Intersection CalcResult::Array(_) => Box::new(move |_x| false),
Box::new(move |_x| false)
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty), CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
} }
} }

View File

@@ -39,6 +39,6 @@ static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
pub fn get_language(id: &str) -> Result<&Language, String> { pub fn get_language(id: &str) -> Result<&Language, String> {
let language = LANGUAGES let language = LANGUAGES
.get(id) .get(id)
.ok_or(format!("Language is not supported: '{}'", id))?; .ok_or(format!("Language is not supported: '{id}'"))?;
Ok(language) Ok(language)
} }

View File

@@ -39,9 +39,9 @@ pub mod types;
pub mod worksheet; pub mod worksheet;
mod actions; mod actions;
mod arithmetic;
mod cast; mod cast;
mod constants; mod constants;
mod diffs;
mod functions; mod functions;
mod implicit_intersection; mod implicit_intersection;
mod model; mod model;

View File

@@ -106,15 +106,15 @@ pub struct Model {
pub(crate) shared_strings: HashMap<String, usize>, pub(crate) shared_strings: HashMap<String, usize>,
/// An instance of the parser /// An instance of the parser
pub(crate) parser: Parser, pub(crate) parser: Parser,
/// The list of cells with formulas that are evaluated of being evaluated /// The list of cells with formulas that are evaluated or being evaluated
pub(crate) cells: HashMap<(u32, i32, i32), CellState>, pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The locale of the model /// The locale of the model
pub(crate) locale: Locale, pub(crate) locale: Locale,
/// Tha language used /// The language used
pub(crate) language: Language, pub(crate) language: Language,
/// The timezone used to evaluate the model /// The timezone used to evaluate the model
pub(crate) tz: Tz, pub(crate) tz: Tz,
/// The view id. A view consist of a selected sheet and ranges. /// The view id. A view consists of a selected sheet and ranges.
pub(crate) view_id: u32, pub(crate) view_id: u32,
} }
@@ -207,6 +207,17 @@ impl Model {
}, },
} }
} }
Node::ImplicitIntersection {
automatic: _,
child,
} => match self.evaluate_node_with_reference(child, cell) {
CalcResult::Range { left, right } => CalcResult::Range { left, right },
_ => CalcResult::new_error(
Error::ERROR,
cell,
format!("Error with Implicit Intersection in cell {cell:?}"),
),
},
_ => self.evaluate_node_in_context(node, cell), _ => self.evaluate_node_in_context(node, cell),
} }
} }
@@ -256,27 +267,10 @@ impl Model {
) -> CalcResult { ) -> CalcResult {
use Node::*; use Node::*;
match node { match node {
OpSumKind { kind, left, right } => { OpSumKind { kind, left, right } => match kind {
// In the future once the feature try trait stabilizes we could use the '?' operator for this :) OpSum::Add => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 + f2)),
// See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5 OpSum::Minus => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 - f2)),
let l = match self.get_number(left, cell) { },
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
let result = match kind {
OpSum::Add => l + r,
OpSum::Minus => l - r,
};
CalcResult::Number(result)
}
NumberKind(value) => CalcResult::Number(*value), NumberKind(value) => CalcResult::Number(*value),
StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)), StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)),
BooleanKind(value) => CalcResult::Boolean(*value), BooleanKind(value) => CalcResult::Boolean(*value),
@@ -361,62 +355,30 @@ impl Model {
return s; return s;
} }
}; };
let result = format!("{}{}", l, r); let result = format!("{l}{r}");
CalcResult::String(result) CalcResult::String(result)
} }
OpProductKind { kind, left, right } => { OpProductKind { kind, left, right } => match kind {
let l = match self.get_number(left, cell) { OpProduct::Times => {
Ok(f) => f, self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 * f2))
Err(s) => {
return s;
} }
}; OpProduct::Divide => self.handle_arithmetic(left, right, cell, &|f1, f2| {
let r = match self.get_number(right, cell) { if f2 == 0.0 {
Ok(f) => f, Err(Error::DIV)
Err(s) => { } else {
return s; Ok(f1 / f2)
}
};
let result = match kind {
OpProduct::Times => l * r,
OpProduct::Divide => {
if r == 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"Divide by Zero".to_string(),
);
}
l / r
}
};
CalcResult::Number(result)
} }
}),
},
OpPowerKind { left, right } => { OpPowerKind { left, right } => {
let l = match self.get_number(left, cell) { self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1.powf(f2)))
Ok(f) => f,
Err(s) => {
return s;
}
};
let r = match self.get_number(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
// Deal with errors properly
CalcResult::Number(l.powf(r))
} }
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell), FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
InvalidFunctionKind { name, args: _ } => { InvalidFunctionKind { name, args: _ } => {
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name)) CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {name}"))
} }
ArrayKind(_) => { ArrayKind(s) => CalcResult::Array(s.to_owned()),
// TODO: NOT IMPLEMENTED DefinedNameKind((name, scope, _)) => {
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
}
DefinedNameKind((name, scope)) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) { if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
match parsed_defined_name { match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
@@ -429,26 +391,26 @@ impl Model {
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" is not a reference.", name), format!("Defined name \"{name}\" is not a reference."),
), ),
} }
} else { } else {
CalcResult::new_error( CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" not found.", name), format!("Defined name \"{name}\" not found."),
) )
} }
} }
TableNameKind(s) => CalcResult::new_error( TableNameKind(s) => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("table name \"{}\" not supported.", s), format!("table name \"{s}\" not supported."),
), ),
WrongVariableKind(s) => CalcResult::new_error( WrongVariableKind(s) => CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Variable name \"{}\" not found.", s), format!("Variable name \"{s}\" not found."),
), ),
CompareKind { kind, left, right } => { CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell); let l = self.evaluate_node_in_context(left, cell);
@@ -525,9 +487,25 @@ impl Model {
} => CalcResult::new_error( } => CalcResult::new_error(
Error::ERROR, Error::ERROR,
cell, cell,
format!("Error parsing {}: {}", formula, message), format!("Error parsing {formula}: {message}"),
), ),
EmptyArgKind => CalcResult::EmptyArg, EmptyArgKind => CalcResult::EmptyArg,
ImplicitIntersection {
automatic: _,
child,
} => match self.evaluate_node_with_reference(child, cell) {
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
Some(cell_reference) => self.evaluate_cell(cell_reference),
None => CalcResult::new_error(
Error::VALUE,
cell,
format!("Error with Implicit Intersection in cell {cell:?}"),
),
}
}
_ => self.evaluate_node_in_context(child, cell),
},
} }
} }
@@ -617,12 +595,15 @@ impl Model {
}; };
} }
CalcResult::Range { left, right } => { CalcResult::Range { left, right } => {
let range = Range { if left.sheet == right.sheet
left: *left, && left.row == right.row
right: *right, && left.column == right.column
};
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
{ {
let intersection_cell = CellReferenceIndex {
sheet: left.sheet,
column: left.column,
row: left.row,
};
let v = self.evaluate_cell(intersection_cell); let v = self.evaluate_cell(intersection_cell);
self.set_cell_value(cell_reference, &v); self.set_cell_value(cell_reference, &v);
} else { } else {
@@ -639,10 +620,32 @@ impl Model {
f, f,
s, s,
o, o,
m: "Invalid reference".to_string(), m: "Implicit Intersection not implemented".to_string(),
ei: Error::VALUE, ei: Error::NIMPL,
}; };
} }
// 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 => { CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize] *self.workbook.worksheets[sheet as usize]
@@ -652,6 +655,20 @@ impl Model {
.get_mut(&column) .get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 }; .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,
};
}
} }
} }
} }
@@ -680,7 +697,7 @@ impl Model {
worksheet.color = Some(color.to_string()); worksheet.color = Some(color.to_string());
return Ok(()); return Ok(());
} }
Err(format!("Invalid color: {}", color)) Err(format!("Invalid color: {color}"))
} }
/// Changes the visibility of a sheet /// Changes the visibility of a sheet
@@ -865,11 +882,7 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook let defined_names = workbook.get_defined_names_with_scope();
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
// add all tables // add all tables
// let mut tables = Vec::new(); // let mut tables = Vec::new();
// for worksheet in worksheets { // for worksheet in worksheets {
@@ -1014,7 +1027,7 @@ impl Model {
let source_sheet_name = self let source_sheet_name = self
.workbook .workbook
.worksheet(source.sheet) .worksheet(source.sheet)
.map_err(|e| format!("Could not find source worksheet: {}", e))? .map_err(|e| format!("Could not find source worksheet: {e}"))?
.get_name(); .get_name();
if source.sheet != area.sheet { if source.sheet != area.sheet {
return Err("Source and area are in different sheets".to_string()); return Err("Source and area are in different sheets".to_string());
@@ -1028,7 +1041,7 @@ impl Model {
let target_sheet_name = self let target_sheet_name = self
.workbook .workbook
.worksheet(target.sheet) .worksheet(target.sheet)
.map_err(|e| format!("Could not find target worksheet: {}", e))? .map_err(|e| format!("Could not find target worksheet: {e}"))?
.get_name(); .get_name();
if let Some(formula) = value.strip_prefix('=') { if let Some(formula) = value.strip_prefix('=') {
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -1048,7 +1061,7 @@ impl Model {
column_delta: target.column - source.column, column_delta: target.column - source.column,
}, },
); );
Ok(format!("={}", formula_str)) Ok(format!("={formula_str}"))
} else { } else {
Ok(value.to_string()) Ok(value.to_string())
} }
@@ -1525,7 +1538,7 @@ impl Model {
// If the formula fails to parse try adding a parenthesis // If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3) // SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula { if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference); let new_parsed_formula = self.parser.parse(&format!("{formula})"), &cell_reference);
match new_parsed_formula { match new_parsed_formula {
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula, _ => parsed_formula = new_parsed_formula,

View File

@@ -8,7 +8,7 @@ use crate::{
expressions::{ expressions::{
lexer::LexerMode, lexer::LexerMode,
parser::{ parser::{
stringify::{rename_sheet_in_node, to_rc_format}, stringify::{rename_sheet_in_node, to_rc_format, to_string},
Parser, Parser,
}, },
types::CellReferenceRC, types::CellReferenceRC,
@@ -17,7 +17,8 @@ use crate::{
locale::get_locale, locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName}, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{ types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView, DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
WorksheetView,
}, },
utils::ParsedReference, utils::ParsedReference,
}; };
@@ -144,12 +145,7 @@ impl Model {
/// Reparses all formulas and defined names /// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) { pub(crate) fn reset_parsed_structures(&mut self) {
let defined_names = self let defined_names = self.workbook.get_defined_names_with_scope();
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
self.parser self.parser
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names); .set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
self.parsed_formulas = vec![]; self.parsed_formulas = vec![];
@@ -172,11 +168,11 @@ impl Model {
.get_worksheet_names() .get_worksheet_names()
.iter() .iter()
.map(|s| s.to_uppercase()) .map(|s| s.to_uppercase())
.any(|x| x == format!("{}{}", base_name_uppercase, index)) .any(|x| x == format!("{base_name_uppercase}{index}"))
{ {
index += 1; index += 1;
} }
let sheet_name = format!("{}{}", base_name, index); let sheet_name = format!("{base_name}{index}");
// Now we need a sheet_id // Now we need a sheet_id
let sheet_id = self.get_new_sheet_id(); let sheet_id = self.get_new_sheet_id();
let view_ids: Vec<&u32> = self.workbook.views.keys().collect(); let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
@@ -196,7 +192,7 @@ impl Model {
sheet_id: Option<u32>, sheet_id: Option<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(sheet_name) { if !is_valid_sheet_name(sheet_name) {
return Err(format!("Invalid name for a sheet: '{}'", sheet_name)); return Err(format!("Invalid name for a sheet: '{sheet_name}'"));
} }
if self if self
.workbook .workbook
@@ -238,12 +234,12 @@ impl Model {
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) { if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
return self.rename_sheet_by_index(sheet_index, new_name); return self.rename_sheet_by_index(sheet_index, new_name);
} }
Err(format!("Could not find sheet {}", old_name)) Err(format!("Could not find sheet {old_name}"))
} }
/// Renames a sheet and updates all existing references to that sheet. /// Renames a sheet and updates all existing references to that sheet.
/// It can fail if: /// It can fail if:
/// * The original index is too large /// * The original index is out of bounds
/// * The target sheet name already exists /// * The target sheet name already exists
/// * The target sheet name is invalid /// * The target sheet name is invalid
pub fn rename_sheet_by_index( pub fn rename_sheet_by_index(
@@ -252,22 +248,20 @@ impl Model {
new_name: &str, new_name: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_sheet_name(new_name) { if !is_valid_sheet_name(new_name) {
return Err(format!("Invalid name for a sheet: '{}'.", new_name)); return Err(format!("Invalid name for a sheet: '{new_name}'."));
} }
if self.get_sheet_index_by_name(new_name).is_some() { if self.get_sheet_index_by_name(new_name).is_some() {
return Err(format!("Sheet already exists: '{}'.", new_name)); return Err(format!("Sheet already exists: '{new_name}'."));
}
let worksheets = &self.workbook.worksheets;
let sheet_count = worksheets.len() as u32;
if sheet_index >= sheet_count {
return Err("Sheet index out of bounds".to_string());
} }
// Gets the new name and checks that a sheet with that index exists
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
// Parse all formulas with the old name // Parse all formulas with the old name
// All internal formulas are R1C1 // All internal formulas are R1C1
self.parser.set_lexer_mode(LexerMode::R1C1); self.parser.set_lexer_mode(LexerMode::R1C1);
// We use iter because the default would be a mut_iter and we don't need a mutable reference
let worksheets = &mut self.workbook.worksheets; for worksheet in &mut self.workbook.worksheets {
for worksheet in worksheets { // R1C1 formulas are not tied to a cell (but are tied to a cell)
let cell_reference = &CellReferenceRC { let cell_reference = &CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
@@ -281,11 +275,32 @@ impl Model {
} }
worksheet.shared_formulas = formulas; worksheet.shared_formulas = formulas;
} }
// Se the mode back to A1
// Set the mode back to A1
self.parser.set_lexer_mode(LexerMode::A1); self.parser.set_lexer_mode(LexerMode::A1);
// We reparse all the defined names formulas
let mut defined_names = Vec::new();
// Defined names do not have a context, we can use anything
let cell_reference = &CellReferenceRC {
sheet: old_name.clone(),
row: 1,
column: 1,
};
for defined_name in &mut self.workbook.defined_names {
let mut t = self.parser.parse(&defined_name.formula, cell_reference);
rename_sheet_in_node(&mut t, sheet_index, new_name);
let formula = to_string(&t, cell_reference);
defined_names.push(DefinedName {
name: defined_name.name.clone(),
formula,
sheet_id: defined_name.sheet_id,
});
}
self.workbook.defined_names = defined_names;
// Update the name of the worksheet // Update the name of the worksheet
let worksheets = &mut self.workbook.worksheets; self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
worksheets[sheet_index as usize].set_name(new_name);
self.reset_parsed_structures(); self.reset_parsed_structures();
Ok(()) Ok(())
} }
@@ -347,14 +362,14 @@ impl Model {
}; };
let locale = match get_locale(locale_id) { let locale = match get_locale(locale_id) {
Ok(l) => l.clone(), Ok(l) => l.clone(),
Err(_) => return Err(format!("Invalid locale: {}", locale_id)), Err(_) => return Err(format!("Invalid locale: {locale_id}")),
}; };
let milliseconds = get_milliseconds_since_epoch(); let milliseconds = get_milliseconds_since_epoch();
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match DateTime::from_timestamp(seconds, 0) { let dt = match DateTime::from_timestamp(seconds, 0) {
Some(s) => s, Some(s) => s,
None => return Err(format!("Invalid timestamp: {}", milliseconds)), None => return Err(format!("Invalid timestamp: {milliseconds}")),
}; };
// "2020-08-06T21:20:53Z // "2020-08-06T21:20:53Z
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();

View File

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

View File

@@ -154,7 +154,7 @@ impl Styles {
return Ok(cell_style.xf_id); return Ok(cell_style.xf_id);
} }
} }
Err(format!("Style '{}' not found", style_name)) Err(format!("Style '{style_name}' not found"))
} }
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> { pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {

View File

@@ -28,7 +28,6 @@ mod test_fn_sumifs;
mod test_fn_textbefore; mod test_fn_textbefore;
mod test_fn_textjoin; mod test_fn_textjoin;
mod test_fn_unicode; mod test_fn_unicode;
mod test_forward_references;
mod test_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
mod test_math; mod test_math;
@@ -52,6 +51,7 @@ mod engineering;
mod test_fn_offset; mod test_fn_offset;
mod test_number_format; mod test_number_format;
mod test_arrays;
mod test_escape_quotes; mod test_escape_quotes;
mod test_extend; mod test_extend;
mod test_fn_fv; mod test_fn_fv;
@@ -59,6 +59,7 @@ mod test_fn_type;
mod test_frozen_rows_and_columns; mod test_frozen_rows_and_columns;
mod test_geomean; mod test_geomean;
mod test_get_cell_content; mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;

View File

@@ -0,0 +1,13 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn sum_arrays() {
let mut model = new_empty_model();
model._set("A1", "=SUM({1,2,3}+{3,4,5})");
model.evaluate();
assert_eq!(model._get_text("A1"), *"18");
}

View File

@@ -22,13 +22,14 @@ fn fn_concatenate() {
model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#); model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#);
// This will break once we implement the implicit intersection operator // This will break once we implement the implicit intersection operator
// It should be: // It should be:
// model._set("B2", r#"=CONCATENATE(@A1:A3, "!")"#); model._set("C2", r#"=CONCATENATE(@A1:A3, "!")"#);
model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#); model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#);
model._set("B3", r#"=CONCAT(A1:A3, "!")"#); model._set("B3", r#"=CONCAT(A1:A3, "!")"#);
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("B1"), *"Hello my World!"); assert_eq!(model._get_text("B1"), *"Hello my World!");
assert_eq!(model._get_text("B2"), *" my !"); assert_eq!(model._get_text("B2"), *"#N/IMPL!");
assert_eq!(model._get_text("B3"), *"Hello my World!"); assert_eq!(model._get_text("B3"), *"Hello my World!");
assert_eq!(model._get_text("C2"), *" my !");
} }

View File

@@ -30,8 +30,18 @@ fn implicit_intersection() {
model._set("A2", "=FORMULATEXT(D1:E1)"); model._set("A2", "=FORMULATEXT(D1:E1)");
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A1"), *"#N/IMPL!");
assert_eq!(model._get_text("A2"), *"#ERROR!"); assert_eq!(model._get_text("A2"), *"#N/IMPL!");
}
#[test]
fn implicit_intersection_operator() {
let mut model = new_empty_model();
model._set("A1", "=1 + 2");
model._set("B1", "=FORMULATEXT(@A:A)");
model.evaluate();
assert_eq!(model._get_text("B1"), *"#N/IMPL!");
} }
#[test] #[test]

View File

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

View File

@@ -17,3 +17,19 @@ fn test_fn_sum_arguments() {
assert_eq!(model._get_text("A3"), *"1"); assert_eq!(model._get_text("A3"), *"1");
assert_eq!(model._get_text("A4"), *"4"); assert_eq!(model._get_text("A4"), *"4");
} }
#[test]
fn arrays() {
let mut model = new_empty_model();
model._set("A1", "=SUM({1, 2, 3})");
model._set("A2", "=SUM({1; 2; 3})");
model._set("A3", "=SUM({1, 2; 3, 4})");
model._set("A4", "=SUM({1, 2; 3, 4; 5, 6})");
model.evaluate();
assert_eq!(model._get_text("A1"), *"6");
assert_eq!(model._get_text("A2"), *"6");
assert_eq!(model._get_text("A3"), *"10");
assert_eq!(model._get_text("A4"), *"21");
}

View File

@@ -1,121 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::expressions::types::{Area, CellReferenceIndex};
use crate::test::util::new_empty_model;
#[test]
fn test_forward_references() {
let mut model = new_empty_model();
// test single ref changed nd not changed
model._set("H8", "=F6*G9");
// tests areas
model._set("H9", "=SUM(D4:F6)");
// absolute coordinates
model._set("H10", "=$F$6");
// area larger than the source area
model._set("H11", "=SUM(D3:F6)");
// Test arguments and concat
model._set("H12", "=SUM(F6, D4:F6) & D4");
// Test range operator. This is syntax error for now.
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
// Test operations
model._set("H14", "=-D4+D5*F6/F5");
model.evaluate();
// Source Area is D4:F6
let source_area = &Area {
sheet: 0,
row: 4,
column: 4,
width: 3,
height: 3,
};
// We paste in B10
let target_row = 10;
let target_column = 2;
let result = model.forward_references(
source_area,
&CellReferenceIndex {
sheet: 0,
row: target_row,
column: target_column,
},
);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("H8"), "=D12*G9");
assert_eq!(model._get_formula("H9"), "=SUM(B10:D12)");
assert_eq!(model._get_formula("H10"), "=$D$12");
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
assert_eq!(model._get_formula("H12"), "=SUM(D12,B10:D12)&B10");
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
assert_eq!(model._get_formula("H14"), "=-B10+B11*D12/D11");
}
#[test]
fn test_different_sheet() {
let mut model = new_empty_model();
// test single ref changed not changed
model._set("H8", "=F6*G9");
// tests areas
model._set("H9", "=SUM(D4:F6)");
// absolute coordinates
model._set("H10", "=$F$6");
// area larger than the source area
model._set("H11", "=SUM(D3:F6)");
// Test arguments and concat
model._set("H12", "=SUM(F6, D4:F6) & D4");
// Test range operator. This is syntax error for now.
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
// Test operations
model._set("H14", "=-D4+D5*F6/F5");
// Adds a new sheet
assert!(model.add_sheet("Sheet2").is_ok());
model.evaluate();
// Source Area is D4:F6
let source_area = &Area {
sheet: 0,
row: 4,
column: 4,
width: 3,
height: 3,
};
// We paste in Sheet2!B10
let target_row = 10;
let target_column = 2;
let result = model.forward_references(
source_area,
&CellReferenceIndex {
sheet: 1,
row: target_row,
column: target_column,
},
);
assert!(result.is_ok());
model.evaluate();
assert_eq!(model._get_formula("H8"), "=Sheet2!D12*G9");
assert_eq!(model._get_formula("H9"), "=SUM(Sheet2!B10:D12)");
assert_eq!(model._get_formula("H10"), "=Sheet2!$D$12");
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
assert_eq!(
model._get_formula("H12"),
"=SUM(Sheet2!D12,Sheet2!B10:D12)&Sheet2!B10"
);
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
assert_eq!(
model._get_formula("H14"),
"=-Sheet2!B10+Sheet2!B11*Sheet2!D12/Sheet2!D11"
);
}

View File

@@ -0,0 +1,50 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn simple_colum() {
let mut model = new_empty_model();
// We populate cells A1 to A3
model._set("A1", "1");
model._set("A2", "2");
model._set("A3", "3");
model._set("C2", "=@A1:A3");
model.evaluate();
assert_eq!(model._get_text("C2"), "2".to_string());
}
#[test]
fn return_of_array_is_n_impl() {
let mut model = new_empty_model();
// We populate cells A1 to A3
model._set("A1", "1");
model._set("A2", "2");
model._set("A3", "3");
model._set("C2", "=A1:A3");
model._set("D2", "=SUM(SIN(A:A)");
model.evaluate();
assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string());
assert_eq!(model._get_text("D2"), "1.89188842".to_string());
}
#[test]
fn concat() {
let mut model = new_empty_model();
model._set("A1", "=CONCAT(@B1:B3)");
model._set("A2", "=CONCAT(B1:B3)");
model._set("B1", "Hello");
model._set("B2", " ");
model._set("B3", "world!");
model.evaluate();
assert_eq!(model._get_text("A1"), *"Hello");
assert_eq!(model._get_text("A2"), *"Hello world!");
}

View File

@@ -62,3 +62,17 @@ fn test_create_named_style() {
let style = model.get_style_for_cell(0, 1, 1).unwrap(); let style = model.get_style_for_cell(0, 1, 1).unwrap();
assert!(style.font.b); assert!(style.font.b);
} }
#[test]
fn empty_models_have_two_fills() {
let model = new_empty_model();
assert_eq!(model.workbook.styles.fills.len(), 2);
assert_eq!(
model.workbook.styles.fills[0].pattern_type,
"none".to_string()
);
assert_eq!(
model.workbook.styles.fills[1].pattern_type,
"gray125".to_string()
);
}

View File

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

View File

@@ -423,3 +423,30 @@ fn change_scope_to_first_sheet() {
Ok("#NAME?".to_string()) Ok("#NAME?".to_string())
); );
} }
#[test]
fn rename_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.new_sheet().unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
model.rename_sheet(0, "AnotherName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
}

View File

@@ -26,7 +26,7 @@ fn set_user_input_errors() {
#[test] #[test]
fn user_model_debug_message() { fn user_model_debug_message() {
let model = UserModel::new_empty("model", "en", "UTC").unwrap(); let model = UserModel::new_empty("model", "en", "UTC").unwrap();
let s = &format!("{:?}", model); let s = &format!("{model:?}");
assert_eq!(s, "UserModel"); assert_eq!(s, "UserModel");
} }

View File

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

View File

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

View File

@@ -303,7 +303,14 @@ impl Default for Styles {
Styles { Styles {
num_fmts: vec![], num_fmts: vec![],
fonts: vec![Default::default()], fonts: vec![Default::default()],
fills: vec![Default::default()], fills: vec![
Default::default(),
Fill {
pattern_type: "gray125".to_string(),
fg_color: None,
bg_color: None,
},
],
borders: vec![Default::default()], borders: vec![Default::default()],
cell_style_xfs: vec![Default::default()], cell_style_xfs: vec![Default::default()],
cell_xfs: vec![Default::default()], cell_xfs: vec![Default::default()],

View File

@@ -299,6 +299,7 @@ impl Model {
Node::WrongVariableKind(_) => None, Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None, Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None, Node::OpPowerKind { .. } => None,
Node::ImplicitIntersection { .. } => None,
} }
} }

View File

@@ -1487,10 +1487,10 @@ impl UserModel {
return Err(format!("Invalid row: '{first_row}'")); return Err(format!("Invalid row: '{first_row}'"));
} }
if !is_valid_column_number(last_column) { if !is_valid_column_number(last_column) {
return Err(format!("Invalid column: '{}'", last_column)); return Err(format!("Invalid column: '{last_column}'"));
} }
if !is_valid_row(last_row) { if !is_valid_row(last_row) {
return Err(format!("Invalid row: '{}'", last_row)); return Err(format!("Invalid row: '{last_row}'"));
} }
if !is_valid_row(to_column) { if !is_valid_row(to_column) {
@@ -1623,15 +1623,15 @@ impl UserModel {
text_row.push(text); text_row.push(text);
} }
wtr.write_record(text_row) wtr.write_record(text_row)
.map_err(|e| format!("Error while processing csv: {}", e))?; .map_err(|e| format!("Error while processing csv: {e}"))?;
data.insert(row, data_row); data.insert(row, data_row);
} }
let csv = String::from_utf8( let csv = String::from_utf8(
wtr.into_inner() wtr.into_inner()
.map_err(|e| format!("Processing error: '{}'", e))?, .map_err(|e| format!("Processing error: '{e}'"))?,
) )
.map_err(|e| format!("Error converting from utf8: '{}'", e))?; .map_err(|e| format!("Error converting from utf8: '{e}'"))?;
Ok(Clipboard { Ok(Clipboard {
csv, csv,
@@ -2391,7 +2391,7 @@ mod tests {
VerticalAlignment::Top, VerticalAlignment::Top,
]; ];
for a in all { for a in all {
assert_eq!(vertical(&format!("{}", a)), Ok(a)); assert_eq!(vertical(&format!("{a}")), Ok(a));
} }
} }
@@ -2408,7 +2408,7 @@ mod tests {
HorizontalAlignment::Right, HorizontalAlignment::Right,
]; ];
for a in all { for a in all {
assert_eq!(horizontal(&format!("{}", a)), Ok(a)); assert_eq!(horizontal(&format!("{a}")), Ok(a));
} }
} }
} }

View File

@@ -76,7 +76,7 @@ impl UserModel {
/// Sets the the selected sheet /// Sets the the selected sheet
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> { pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Some(view) = self.model.workbook.views.get_mut(&0) { if let Some(view) = self.model.workbook.views.get_mut(&0) {
view.sheet = sheet; view.sheet = sheet;
@@ -98,7 +98,7 @@ impl UserModel {
return Err(format!("Invalid row: '{row}'")); return Err(format!("Invalid row: '{row}'"));
} }
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {
@@ -138,7 +138,7 @@ impl UserModel {
return Err(format!("Invalid row: '{end_row}'")); return Err(format!("Invalid row: '{end_row}'"));
} }
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {
@@ -147,14 +147,12 @@ impl UserModel {
// The selected cells must be on one of the corners of the selected range: // The selected cells must be on one of the corners of the selected range:
if selected_row != start_row && selected_row != end_row { if selected_row != start_row && selected_row != end_row {
return Err(format!( return Err(format!(
"The selected cells is not in one of the corners. Row: '{}' and row range '({}, {})'", "The selected cells is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
selected_row, start_row, end_row
)); ));
} }
if selected_column != start_column && selected_column != end_column { if selected_column != start_column && selected_column != end_column {
return Err(format!( return Err(format!(
"The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'", "The selected cells is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
selected_column, start_column, end_column
)); ));
} }
view.range = [start_row, start_column, end_row, end_column]; view.range = [start_row, start_column, end_row, end_column];
@@ -307,7 +305,7 @@ impl UserModel {
return Err(format!("Invalid row: '{top_row}'")); return Err(format!("Invalid row: '{top_row}'"));
} }
if self.model.workbook.worksheet(sheet).is_err() { if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet)); return Err(format!("Invalid worksheet index {sheet}"));
} }
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) { if let Some(view) = worksheet.views.get_mut(&0) {

View File

@@ -1,6 +1,6 @@
use std::vec::Vec; use std::vec::Vec;
use crate::types::*; use crate::{expressions::parser::DefinedNameS, types::*};
impl Workbook { impl Workbook {
pub fn get_worksheet_names(&self) -> Vec<String> { pub fn get_worksheet_names(&self) -> Vec<String> {
@@ -29,7 +29,7 @@ impl Workbook {
} }
/// Returns the a list of defined names in the workbook with their scope /// Returns the a list of defined names in the workbook with their scope
pub fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> { pub fn get_defined_names_with_scope(&self) -> Vec<DefinedNameS> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect(); let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self let defined_names = self

View File

@@ -16,7 +16,7 @@ crate-type = ["cdylib"]
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] } ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2.92" wasm-bindgen = "0.2.100"
serde-wasm-bindgen = "0.4" serde-wasm-bindgen = "0.4"
[dev-dependencies] [dev-dependencies]

View File

@@ -9,7 +9,7 @@ endif
all: all:
wasm-pack build --target web --scope ironcalc --release wasm-pack build --target web --scope ironcalc --release
cp README.pkg.md pkg/README.md cp README.pkg.md pkg/README.md
tsc types.ts --target esnext --module esnext npx tsc types.ts --target esnext --module esnext
$(PYTHON) fix_types.py $(PYTHON) fix_types.py
rm -f types.js rm -f types.js
@@ -26,4 +26,4 @@ clean:
rm -rf pkg rm -rf pkg
rm -f types.js rm -f types.js
.PHONY: all lint clean .PHONY: all lint clean tests

View File

@@ -1,231 +1,25 @@
# Regrettably at the time of writing there is not a perfect way to
# generate the TypeScript types from Rust so we basically fix them manually
# Hopefully this will suffice for our needs and one day will be automatic
header = r""" header = r"""
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
""".strip() """.strip()
get_tokens_str = r""" def fix_types(text: str):
* @returns {any}
*/
export function getTokens(formula: string): any;
""".strip()
get_tokens_str_types = r"""
* @returns {MarkedToken[]}
*/
export function getTokens(formula: string): MarkedToken[];
""".strip()
update_style_str = r"""
/**
* @param {any} range
* @param {string} style_path
* @param {string} value
*/
updateRangeStyle(range: any, style_path: string, value: string): void;
""".strip()
update_style_str_types = r"""
/**
* @param {Area} range
* @param {string} style_path
* @param {string} value
*/
updateRangeStyle(range: Area, style_path: string, value: string): void;
""".strip()
properties = r"""
/**
* @returns {any}
*/
getWorksheetsProperties(): any;
""".strip()
properties_types = r"""
/**
* @returns {WorksheetProperties[]}
*/
getWorksheetsProperties(): WorksheetProperties[];
""".strip()
style = r"""
* @returns {any}
*/
getCellStyle(sheet: number, row: number, column: number): any;
""".strip()
style_types = r"""
* @returns {CellStyle}
*/
getCellStyle(sheet: number, row: number, column: number): CellStyle;
""".strip()
view = r"""
* @returns {any}
*/
getSelectedView(): any;
""".strip()
view_types = r"""
* @returns {CellStyle}
*/
getSelectedView(): SelectedView;
""".strip()
autofill_rows = r"""
/**
* @param {any} source_area
* @param {number} to_row
*/
autoFillRows(source_area: any, to_row: number): void;
"""
autofill_rows_types = r"""
/**
* @param {Area} source_area
* @param {number} to_row
*/
autoFillRows(source_area: Area, to_row: number): void;
"""
autofill_columns = r"""
/**
* @param {any} source_area
* @param {number} to_column
*/
autoFillColumns(source_area: any, to_column: number): void;
"""
autofill_columns_types = r"""
/**
* @param {Area} source_area
* @param {number} to_column
*/
autoFillColumns(source_area: Area, to_column: number): void;
"""
set_cell_style = r"""
/**
* @param {any} styles
*/
onPasteStyles(styles: any): void;
"""
set_cell_style_types = r"""
/**
* @param {CellStyle[][]} styles
*/
onPasteStyles(styles: CellStyle[][]): void;
"""
set_area_border = r"""
/**
* @param {any} area
* @param {any} border_area
*/
setAreaWithBorder(area: any, border_area: any): void;
"""
set_area_border_types = r"""
/**
* @param {Area} area
* @param {BorderArea} border_area
*/
setAreaWithBorder(area: Area, border_area: BorderArea): void;
"""
paste_csv_string = r"""
/**
* @param {any} area
* @param {string} csv
*/
pasteCsvText(area: any, csv: string): void;
"""
paste_csv_string_types = r"""
/**
* @param {Area} area
* @param {string} csv
*/
pasteCsvText(area: Area, csv: string): void;
"""
clipboard = r"""
/**
* @returns {any}
*/
copyToClipboard(): any;
"""
clipboard_types = r"""
/**
* @returns {Clipboard}
*/
copyToClipboard(): Clipboard;
"""
paste_from_clipboard = r"""
/**
* @param {number} source_sheet
* @param {any} source_range
* @param {any} clipboard
* @param {boolean} is_cut
*/
pasteFromClipboard(source_sheet: number, source_range: any, clipboard: any, is_cut: boolean): void;
"""
paste_from_clipboard_types = r"""
/**
* @param {number} source_sheet
* @param {[number, number, number, number]} source_range
* @param {ClipboardData} clipboard
* @param {boolean} is_cut
*/
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
"""
defined_name_list = r"""
/**
* @returns {any}
*/
getDefinedNameList(): any;
"""
defined_name_list_types = r"""
/**
* @returns {DefinedName[]}
*/
getDefinedNameList(): DefinedName[];
"""
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
text = text.replace(properties, properties_types)
text = text.replace(style, style_types)
text = text.replace(view, view_types)
text = text.replace(autofill_rows, autofill_rows_types)
text = text.replace(autofill_columns, autofill_columns_types)
text = text.replace(set_cell_style, set_cell_style_types)
text = text.replace(set_area_border, set_area_border_types)
text = text.replace(paste_csv_string, paste_csv_string_types)
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
with open("types.ts") as f: with open("types.ts") as f:
types_str = f.read() types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str) header_types = "{}\n\n{}".format(header, types_str)
text = text.replace(header, header_types) text = text.replace(header, header_types)
if text.find("any") != -1: for line in text.splitlines():
print("There are 'unfixed' types. Please check.") line = line.lstrip()
# Skip internal methods
if line.startswith("readonly model_"):
continue
if line.find("any") != -1:
print("There are 'unfixed' public types. Please check.")
exit(1) exit(1)
return text return text
if __name__ == "__main__": if __name__ == "__main__":
types_file = "pkg/wasm.d.ts" types_file = "pkg/wasm.d.ts"
with open(types_file) as f: with open(types_file) as f:
@@ -243,5 +37,3 @@ if __name__ == "__main__":
with open(js_file, "wb") as f: with open(js_file, "wb") as f:
f.write(bytes("{}\n{}".format(text_js, text), "utf8")) f.write(bytes("{}\n{}".format(text_js, text), "utf8"))

View File

@@ -16,7 +16,7 @@ fn to_js_error(error: String) -> JsError {
/// Return an array with a list of all the tokens from a formula /// Return an array with a list of all the tokens from a formula
/// This is used by the UI to color them according to a theme. /// This is used by the UI to color them according to a theme.
#[wasm_bindgen(js_name = "getTokens")] #[wasm_bindgen(js_name = "getTokens", unchecked_return_type = "MarkedToken[]")]
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> { pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
let tokens = tokenizer(formula); let tokens = tokenizer(formula);
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from) serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
@@ -338,7 +338,7 @@ impl Model {
#[wasm_bindgen(js_name = "updateRangeStyle")] #[wasm_bindgen(js_name = "updateRangeStyle")]
pub fn update_range_style( pub fn update_range_style(
&mut self, &mut self,
range: JsValue, #[wasm_bindgen(unchecked_param_type = "Area")] range: JsValue,
style_path: &str, style_path: &str,
value: &str, value: &str,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
@@ -349,7 +349,7 @@ impl Model {
.map_err(to_js_error) .map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "getCellStyle")] #[wasm_bindgen(js_name = "getCellStyle", unchecked_return_type = "CellStyle")]
pub fn get_cell_style( pub fn get_cell_style(
&mut self, &mut self,
sheet: u32, sheet: u32,
@@ -365,7 +365,10 @@ impl Model {
} }
#[wasm_bindgen(js_name = "onPasteStyles")] #[wasm_bindgen(js_name = "onPasteStyles")]
pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> { pub fn on_paste_styles(
&mut self,
#[wasm_bindgen(unchecked_param_type = "CellStyle[][]")] styles: JsValue,
) -> Result<(), JsError> {
let styles: &Vec<Vec<Style>> = let styles: &Vec<Vec<Style>> =
&serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?; &serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?;
self.model.on_paste_styles(styles).map_err(to_js_error) self.model.on_paste_styles(styles).map_err(to_js_error)
@@ -391,7 +394,10 @@ impl Model {
// I don't _think_ serializing to JsValue can't fail // I don't _think_ serializing to JsValue can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[wasm_bindgen(js_name = "getWorksheetsProperties")] #[wasm_bindgen(
js_name = "getWorksheetsProperties",
unchecked_return_type = "WorksheetProperties[]"
)]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_worksheets_properties(&self) -> JsValue { pub fn get_worksheets_properties(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap() serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
@@ -410,7 +416,7 @@ impl Model {
// I don't _think_ serializing to JsValue can't fail // I don't _think_ serializing to JsValue can't fail
// FIXME: Remove this clippy directive // FIXME: Remove this clippy directive
#[wasm_bindgen(js_name = "getSelectedView")] #[wasm_bindgen(js_name = "getSelectedView", unchecked_return_type = "SelectedView")]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
pub fn get_selected_view(&self) -> JsValue { pub fn get_selected_view(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap() serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
@@ -469,7 +475,11 @@ impl Model {
} }
#[wasm_bindgen(js_name = "autoFillRows")] #[wasm_bindgen(js_name = "autoFillRows")]
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> { pub fn auto_fill_rows(
&mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
to_row: i32,
) -> Result<(), JsError> {
let area: Area = let area: Area =
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
self.model self.model
@@ -480,7 +490,7 @@ impl Model {
#[wasm_bindgen(js_name = "autoFillColumns")] #[wasm_bindgen(js_name = "autoFillColumns")]
pub fn auto_fill_columns( pub fn auto_fill_columns(
&mut self, &mut self,
source_area: JsValue, #[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
to_column: i32, to_column: i32,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let area: Area = let area: Area =
@@ -561,8 +571,8 @@ impl Model {
#[wasm_bindgen(js_name = "setAreaWithBorder")] #[wasm_bindgen(js_name = "setAreaWithBorder")]
pub fn set_area_with_border( pub fn set_area_with_border(
&mut self, &mut self,
area: JsValue, #[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
border_area: JsValue, #[wasm_bindgen(unchecked_param_type = "BorderArea")] border_area: JsValue,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let range: Area = let range: Area =
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
@@ -589,7 +599,7 @@ impl Model {
self.model.set_name(name); self.model.set_name(name);
} }
#[wasm_bindgen(js_name = "copyToClipboard")] #[wasm_bindgen(js_name = "copyToClipboard", unchecked_return_type = "Clipboard")]
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> { pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
let data = self let data = self
.model .model
@@ -603,8 +613,9 @@ impl Model {
pub fn paste_from_clipboard( pub fn paste_from_clipboard(
&mut self, &mut self,
source_sheet: u32, source_sheet: u32,
#[wasm_bindgen(unchecked_param_type = "[number, number, number, number]")]
source_range: JsValue, source_range: JsValue,
clipboard: JsValue, #[wasm_bindgen(unchecked_param_type = "ClipboardData")] clipboard: JsValue,
is_cut: bool, is_cut: bool,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let source_range: (i32, i32, i32, i32) = let source_range: (i32, i32, i32, i32) =
@@ -617,7 +628,11 @@ impl Model {
} }
#[wasm_bindgen(js_name = "pasteCsvText")] #[wasm_bindgen(js_name = "pasteCsvText")]
pub fn paste_csv_string(&mut self, area: JsValue, csv: &str) -> Result<(), JsError> { pub fn paste_csv_string(
&mut self,
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
csv: &str,
) -> Result<(), JsError> {
let range: Area = let range: Area =
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
self.model self.model
@@ -625,7 +640,10 @@ impl Model {
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[wasm_bindgen(js_name = "getDefinedNameList")] #[wasm_bindgen(
js_name = "getDefinedNameList",
unchecked_return_type = "DefinedName[]"
)]
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> { pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
let data: Vec<DefinedName> = self let data: Vec<DefinedName> = self
.model .model

View File

@@ -2,11 +2,7 @@ import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [ addons: [],
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",
options: {}, options: {},

File diff suppressed because it is too large Load Diff

View File

@@ -18,31 +18,26 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@ironcalc/wasm": "file:../../bindings/wasm/pkg", "@ironcalc/wasm": "file:../../bindings/wasm/pkg",
"@mui/material": "^6.4", "@mui/material": "^7.1.1",
"@mui/system": "^6.4", "@mui/system": "^7.1.1",
"i18next": "^23.11.1", "i18next": "^25.2.1",
"lucide-react": "^0.473.0", "lucide-react": "^0.513.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-i18next": "^15.4.0" "react-i18next": "^15.5.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.4", "@storybook/react": "^9.0.5",
"@storybook/addon-essentials": "^8.6.0", "@storybook/react-vite": "^9.0.5",
"@storybook/addon-interactions": "^8.6.0",
"@storybook/blocks": "^8.6.0",
"@storybook/react": "^8.6.0",
"@storybook/react-vite": "^8.6.0",
"@storybook/test": "^8.6.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"react": "^19.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"storybook": "^8.6.0", "storybook": "^9.0.5",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "~5.6.2", "typescript": "~5.8.3",
"vite": "^6.2.0", "vite": "^6.3.5",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vitest": "^3.0.7" "vitest": "^3.2.2"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0", "@types/react": "^18.0.0 || ^19.0.0",

View File

@@ -1,6 +1,6 @@
import "./index.css"; import "./index.css";
import type { Model } from "@ironcalc/wasm"; import type { Model } from "@ironcalc/wasm";
import ThemeProvider from "@mui/material/styles/ThemeProvider"; import { ThemeProvider } from "@mui/material";
import Workbook from "./components/Workbook/Workbook.tsx"; import Workbook from "./components/Workbook/Workbook.tsx";
import { WorkbookState } from "./components/workbookState.ts"; import { WorkbookState } from "./components/workbookState.ts";
import { theme } from "./theme.ts"; import { theme } from "./theme.ts";

View File

@@ -262,6 +262,8 @@ const BorderPicker = (properties: BorderPickerProps) => {
</BorderPickerDialog> </BorderPickerDialog>
<ColorPicker <ColorPicker
color={borderColor} color={borderColor}
defaultColor="#000000"
title={t("color_picker.default")}
onChange={(color): void => { onChange={(color): void => {
setBorderColor(color); setBorderColor(color);
setColorPickerOpen(false); setColorPickerOpen(false);
@@ -359,7 +361,7 @@ const MediumLine = styled("div")`
`; `;
const ThickLine = styled("div")` const ThickLine = styled("div")`
width: 68px; width: 68px;
border-top: 1px solid ${theme.palette.grey["900"]}; border-top: 3px solid ${theme.palette.grey["900"]};
`; `;
const Divider = styled("div")` const Divider = styled("div")`

View File

@@ -0,0 +1,292 @@
import styled from "@emotion/styled";
import { Popover, type PopoverOrigin } from "@mui/material";
import { Check } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
type AdvancedColorPickerProps = {
color: string;
onAccept: (color: string) => void;
onCancel: () => void;
anchorEl: React.RefObject<HTMLElement | null>;
anchorOrigin: PopoverOrigin;
transformOrigin: PopoverOrigin;
open: boolean;
};
const AdvancedColorPicker = ({
color,
onAccept,
onCancel,
anchorEl,
anchorOrigin,
transformOrigin,
open,
}: AdvancedColorPickerProps) => {
const [selectedColor, setSelectedColor] = useState<string>(color);
const recentColors = useRef<string[]>([]);
const { t } = useTranslation();
useEffect(() => {
setSelectedColor(color);
}, [color]);
const handleColorSelect = (color: string) => {
if (!recentColors.current.includes(color)) {
const maxRecentColors = 14;
recentColors.current = [color, ...recentColors.current].slice(
0,
maxRecentColors,
);
}
setSelectedColor(color);
onAccept(color);
};
return (
<StylePopover
open={open}
onClose={onCancel}
anchorEl={anchorEl.current}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
>
<ColorPickerDialog>
<HexColorPicker
color={selectedColor}
onChange={(newColor): void => {
setSelectedColor(newColor);
}}
/>
<HorizontalDivider />
<ColorPickerInput>
<HexWrapper>
<HexLabel>{"Hex"}</HexLabel>
<HexColorInputBox>
<HashLabel>{"#"}</HashLabel>
<StyledHexColorInput
color={selectedColor}
onChange={(newColor): void => {
setSelectedColor(newColor);
}}
tabIndex={0}
/>
</HexColorInputBox>
</HexWrapper>
<Swatch $color={selectedColor} />
</ColorPickerInput>
<HorizontalDivider />
<Buttons>
<CancelButton onClick={onCancel}>
{t("color_picker.cancel")}
</CancelButton>
<StyledButton
onClick={(): void => {
handleColorSelect(selectedColor);
onCancel();
}}
>
<Check />
{t("color_picker.apply")}
</StyledButton>
</Buttons>
</ColorPickerDialog>
</StylePopover>
);
};
const StylePopover = styled(Popover)`
& .MuiPaper-root {
border-radius: 8px;
padding: 0px;
margin-left: -4px;
max-width: 220px;
}
`;
const HorizontalDivider = styled.div`
height: 0px;
width: 100%;
border-top: 1px solid ${theme.palette.grey["200"]};
`;
// Color Picker Dialog Styles
const ColorPickerDialog = styled.div`
background: ${theme.palette.background.default};
width: 240px;
padding: 0px;
display: flex;
flex-direction: column;
max-width: 100%;
& .react-colorful {
height: 160px;
width: 100%;
}
& .react-colorful__saturation {
border-bottom: none;
border-radius: 8px 8px 0px 0px;
}
& .react-colorful__hue {
height: 8px;
margin: 8px;
border-radius: 5px;
}
& .react-colorful__saturation-pointer {
width: 14px;
height: 14px;
}
& .react-colorful__hue-pointer {
width: 7px;
border-radius: 8px;
height: 16px;
width: 16px;
}
`;
const Buttons = styled.div`
display: flex;
justify-content: flex-end;
margin: 8px;
gap: 8px;
`;
const StyledButton = styled("div")`
cursor: pointer;
width: 100%;
color: ${theme.palette.primary.contrastText};
background: ${theme.palette.primary.main};
padding: 0px 10px;
height: 28px;
border-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 12px;
&:hover {
background: #d68742;
}
svg {
max-width: 12px;
max-height: 12px;
}
`;
const CancelButton = styled("div")`
cursor: pointer;
width: 100%;
color: ${theme.palette.grey[700]};
background: ${theme.palette.grey[200]};
padding: 0px 10px;
height: 28px;
border-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 12px;
&:hover {
background: ${theme.palette.grey[300]};
}
svg {
max-width: 12px;
max-height: 12px;
}
`;
const HashLabel = styled.div`
margin: auto 0px auto 10px;
font-size: 13px;
color: #333;
font-family: ${theme.typography.button.fontFamily};
`;
const HexLabel = styled.div`
margin: auto 0px;
font-size: 12px;
display: inline-flex;
font-family: ${theme.typography.button.fontFamily};
`;
const HexColorInputBox = styled.div`
display: inline-flex;
flex-grow: 1;
width: 100%;
height: 28px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 5px;
&:hover {
border: 1px solid ${theme.palette.grey["600"]};
}
&:focus-within {
outline: 2px solid ${theme.palette.secondary.main};
outline-offset: 1px;
}
`;
const StyledHexColorInput = styled(HexColorInput)`
width: 100%;
border: none;
background: transparent;
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
&:focus {
border-color: #4298ef;
}
`;
const HexWrapper = styled.div`
display: flex;
gap: 8px;
flex-grow: 1;
& input {
min-width: 0px;
border: 0px;
background: ${theme.palette.background.default};
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
}
& input:focus {
border-color: #4298ef;
}
`;
const Swatch = styled.div<{ $color: string }>`
display: inline-flex;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["300"]};`;
}
return `border: 1px solid ${$color};`;
}}
background-color: ${({ $color }): string => $color};
min-width: 28px;
height: 28px;
border-radius: 5px;
`;
const ColorPickerInput = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin: 8px;
gap: 8px;
`;
export default AdvancedColorPicker;

View File

@@ -1,187 +1,278 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import Popover, { type PopoverOrigin } from "@mui/material/Popover"; import { Menu, MenuItem, type PopoverOrigin } from "@mui/material";
import { Check } from "lucide-react"; import { Plus } from "lucide-react";
import type React from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { theme } from "../../theme"; import { theme } from "../../theme";
import AdvancedColorPicker from "./AdvancedColorPicker";
type ColorPickerProps = { type ColorPickerProps = {
color: string; color: string;
defaultColor: string;
title: string;
onChange: (color: string) => void; onChange: (color: string) => void;
onClose: () => void; onClose: () => void;
anchorEl: React.RefObject<HTMLElement | null>; anchorEl: React.RefObject<HTMLElement | null>;
anchorOrigin?: PopoverOrigin; anchorOrigin: PopoverOrigin;
transformOrigin?: PopoverOrigin; transformOrigin: PopoverOrigin;
open: boolean; open: boolean;
}; };
const colorPickerWidth = 240; const ColorPicker = ({
const colorfulHeight = 240; color,
defaultColor,
const ColorPicker = (properties: ColorPickerProps) => { title,
const [color, setColor] = useState<string>(properties.color); onChange,
onClose,
anchorEl,
anchorOrigin,
transformOrigin,
open,
}: ColorPickerProps) => {
const [selectedColor, setSelectedColor] = useState<string>(color);
const [isPickerOpen, setPickerOpen] = useState(false);
const recentColors = useRef<string[]>([]); const recentColors = useRef<string[]>([]);
const { t } = useTranslation(); const { t } = useTranslation();
const closePicker = (newColor: string): void => {
const maxRecentColors = 14;
const colors = recentColors.current.filter((c) => c !== newColor);
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
properties.onChange(newColor);
};
const handleClose = (): void => {
properties.onClose();
};
useEffect(() => { useEffect(() => {
setColor(properties.color); setSelectedColor(color);
}, [properties.color]); }, [color]);
const presetColors = [ const handleColorSelect = (color: string) => {
if (!recentColors.current.includes(color)) {
const maxRecentColors = 14;
recentColors.current = [color, ...recentColors.current].slice(
0,
maxRecentColors,
);
}
setSelectedColor(color || theme.palette.common.black);
onChange(color);
setPickerOpen(false);
};
const handleClose = () => {
setPickerOpen(false);
onClose();
};
// Colors definitions
const mainColors = [
"#FFFFFF", "#FFFFFF",
"#272525",
"#1B717E", "#1B717E",
"#59B9BC",
"#3BB68A", "#3BB68A",
"#8CB354", "#8CB354",
"#F8CD3C", "#F8CD3C",
"#F2994A", "#F2994A",
"#EC5753", "#EC5753",
"#D03627",
"#523E93", "#523E93",
"#3358B7", "#3358B7",
]; ];
const lightTones = [
theme.palette.grey[50],
theme.palette.grey[100],
theme.palette.grey[200],
theme.palette.grey[300],
theme.palette.grey[400],
];
const darkTones = [
theme.palette.grey[500],
theme.palette.grey[600],
theme.palette.grey[700],
theme.palette.grey[800],
theme.palette.grey[900],
];
const tealTones = ["#BBD4D8", "#82B1B8", "#498D98", "#1E5A63", "#224348"];
const greenTones = ["#C4E9DC", "#93D7BF", "#62C5A1", "#358A6C", "#2F5F4D"];
const limeTones = ["#DDE8CC", "#C0D5A1", "#A3C276", "#6E8846", "#4F5E38"];
const yellowTones = ["#FDF0C5", "#FBE394", "#F9D764", "#B99A36", "#7A682E"];
const orangeTones = ["#FBE0C9", "#F8C79B", "#F5AD6E", "#B5763F", "#785334"];
const redTones = ["#F9CDCB", "#F5A3A0", "#F07975", "#B14845", "#763937"];
const purpleTones = ["#CBC5DF", "#A095C4", "#7565A9", "#453672", "#382F51"];
const blueTones = ["#C2CDE9", "#8FA3D7", "#5D79C5", "#30498B", "#2C395F"];
const toneArrays = [
lightTones,
darkTones,
tealTones,
greenTones,
limeTones,
yellowTones,
orangeTones,
redTones,
purpleTones,
blueTones,
];
if (!open) {
return null;
}
if (isPickerOpen) {
return ( return (
<Popover <AdvancedColorPicker
open={properties.open} color={selectedColor}
onAccept={handleColorSelect}
onCancel={() => setPickerOpen(false)}
anchorEl={anchorEl}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
open={true}
/>
);
}
return (
<StyledMenu
anchorEl={anchorEl.current}
open={true}
onClose={handleClose} onClose={handleClose}
anchorEl={properties.anchorEl.current} anchorOrigin={anchorOrigin}
anchorOrigin={ transformOrigin={transformOrigin}
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
}
transformOrigin={
properties.transformOrigin || { vertical: "top", horizontal: "left" }
}
> >
<ColorPickerDialog> <MenuItemWrapper onClick={() => handleColorSelect(defaultColor)}>
<HexColorPicker <MenuItemSquare style={{ backgroundColor: defaultColor }} />
color={color} <MenuItemText>{title}</MenuItemText>
onChange={(newColor): void => { </MenuItemWrapper>
setColor(newColor);
}}
/>
<HorizontalDivider />
<ColorPickerInput>
<HexWrapper>
<HexLabel>{"Hex"}</HexLabel>
<HexColorInputBox>
<HashLabel>{"#"}</HashLabel>
<HexColorInput
color={color}
onChange={(newColor): void => {
setColor(newColor);
}}
/>
</HexColorInputBox>
</HexWrapper>
<Swatch $color={color} />
</ColorPickerInput>
<HorizontalDivider /> <HorizontalDivider />
<ColorsWrapper>
<ColorList> <ColorList>
{presetColors.map((presetColor) => ( {mainColors.map((presetColor) => (
<RecentColorButton <ColorSwatch
key={presetColor} key={presetColor}
$color={presetColor} $color={presetColor}
onClick={(): void => { onClick={(): void => {
setColor(presetColor); setSelectedColor(presetColor);
handleColorSelect(presetColor);
}} }}
/> />
))} ))}
</ColorList> </ColorList>
<ColorGrid>
{toneArrays.map((tones) => (
<ColorGridCol key={tones.join("-")}>
{tones.map((presetColor) => (
<ColorSwatch
key={presetColor}
$color={presetColor}
onClick={(): void => {
setSelectedColor(presetColor);
handleColorSelect(presetColor);
}}
/>
))}
</ColorGridCol>
))}
</ColorGrid>
</ColorsWrapper>
<HorizontalDivider />
<RecentLabel>{t("color_picker.custom")}</RecentLabel>
<RecentColorsList>
{recentColors.current.length > 0 ? ( {recentColors.current.length > 0 ? (
<> <>
<HorizontalDivider />
<RecentLabel>{"Recent"}</RecentLabel>
<ColorList>
{recentColors.current.map((recentColor) => ( {recentColors.current.map((recentColor) => (
<RecentColorButton <ColorSwatch
key={recentColor} key={recentColor}
$color={recentColor} $color={recentColor}
onClick={(): void => { onClick={(): void => {
setColor(recentColor); setSelectedColor(recentColor);
handleColorSelect(recentColor);
}} }}
/> />
))} ))}
</ColorList>
</> </>
) : ( ) : (
<div /> <EmptyContainer />
)} )}
<Buttons> <StyledPlusButton
<StyledButton onClick={() => setPickerOpen(true)}
onClick={(): void => { title={t("color_picker.add")}
closePicker(color);
}}
> >
<Check <Plus />
style={{ width: "16px", height: "16px", marginRight: "8px" }} </StyledPlusButton>
/> </RecentColorsList>
{t("color_picker.apply")} </StyledMenu>
</StyledButton>
</Buttons>
</ColorPickerDialog>
</Popover>
); );
}; };
const Buttons = styled.div` // Styled Components
display: flex; const StyledMenu = styled(Menu)`
justify-content: flex-end; & .MuiPaper-root {
margin: 8px; border-radius: 8px;
`; padding: 4px 0px;
margin-left: -4px;
const StyledButton = styled("div")` max-width: 220px;
cursor: pointer; }
color: #ffffff; & .MuiList-root {
background: #f2994a; padding: 0;
padding: 0px 10px;
height: 36px;
line-height: 36px;
border-radius: 4px;
display: flex;
align-items: center;
font-family: "Inter";
font-size: 14px;
&:hover {
background: #d68742;
} }
`; `;
const RecentLabel = styled.div` const MenuItemWrapper = styled(MenuItem)`
font-family: "Inter"; display: flex;
flex-direction: row;
justify-content: flex-start;
font-size: 12px; font-size: 12px;
font-family: Inter; gap: 8px;
margin: 8px 8px 0px 8px; width: calc(100% - 8px);
color: ${theme.palette.text.secondary}; min-width: 172px;
margin: 0px 4px 4px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
`;
const MenuItemText = styled("div")`
color: #000;
`;
const MenuItemSquare = styled.div`
width: 16px;
height: 16px;
box-sizing: border-box;
margin-top: 0px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 4px;
`;
const ColorsWrapper = styled.div`
display: flex;
flex-direction: column;
margin: 4px;
`; `;
const ColorList = styled.div` const ColorList = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
margin: 8px; margin: 8px 8px 0px 8px;
justify-content: flex-start; justify-content: flex-start;
gap: 4.7px; gap: 4px;
`; `;
const RecentColorButton = styled.button<{ $color: string }>` const ColorGrid = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
margin: 8px;
gap: 4px;
`;
const ColorGridCol = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 4px;
`;
const ColorSwatch = styled.button<{ $color: string }>`
width: 16px; width: 16px;
height: 16px; height: 16px;
padding: 0px;
${({ $color }): string => { ${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") { if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["300"]};`; return `border: 1px solid ${theme.palette.grey["300"]};`;
@@ -189,7 +280,7 @@ const RecentColorButton = styled.button<{ $color: string }>`
return `border: 1px solid ${$color};`; return `border: 1px solid ${$color};`;
}} }}
background-color: ${({ $color }): string => { background-color: ${({ $color }): string => {
return $color; return $color === "transparent" ? "none" : $color;
}}; }};
box-sizing: border-box; box-sizing: border-box;
margin-top: 0px; margin-top: 0px;
@@ -207,111 +298,50 @@ const HorizontalDivider = styled.div`
border-top: 1px solid ${theme.palette.grey["200"]}; border-top: 1px solid ${theme.palette.grey["200"]};
`; `;
const ColorPickerDialog = styled.div` const RecentLabel = styled.div`
background: ${theme.palette.background.default}; font-family: "Inter";
width: ${colorPickerWidth}px; font-size: 12px;
padding: 0px; font-family: Inter;
display: flex; margin: 8px 12px 0px 12px;
flex-direction: column; color: ${theme.palette.text.secondary};
`;
& .react-colorful { const RecentColorsList = styled.div`
height: ${colorfulHeight}px; display: flex;
width: ${colorPickerWidth}px; flex-wrap: wrap;
} flex-direction: row;
& .react-colorful__saturation { padding: 8px;
border-bottom: none; margin: 0px 4px;
border-radius: 0px; justify-content: flex-start;
} gap: 4px;
& .react-colorful__hue { `;
height: 8px;
margin: 8px; const StyledPlusButton = styled("button")`
border-radius: 5px; display: flex;
} justify-content: center;
& .react-colorful__saturation-pointer { flex-wrap: wrap;
width: 14px; align-items: center;
height: 14px; border: none;
} background: none;
& .react-colorful__hue-pointer { font-size: 12px;
width: 7px;
border-radius: 8px;
height: 16px; height: 16px;
width: 16px; width: 16px;
border-bottom: 1px solid #eee; margin: 0;
padding: 0;
border-radius: 4px;
svg {
width: 16px;
height: 16px;
} }
`;
const HashLabel = styled.div`
margin: auto 0px auto 10px;
font-size: 13px;
color: #333;
font-family: ${theme.typography.button.fontFamily};
`;
const HexLabel = styled.div`
margin: auto 0px;
font-size: 12px;
display: inline-flex;
font-family: ${theme.typography.button.fontFamily};
`;
const HexColorInputBox = styled.div`
display: inline-flex;
flex-grow: 1;
width: 140px;
height: 28px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 5px;
&:hover { &:hover {
border: 1px solid ${theme.palette.grey["600"]}; cursor: pointer;
} outline: 1px solid ${theme.palette.grey["300"]};
&:focus-within {
outline: 2px solid ${theme.palette.secondary.main};
outline-offset: 1px; outline-offset: 1px;
} }
`; `;
const HexWrapper = styled.div` const EmptyContainer = styled.div`
display: flex; display: none;
gap: 8px;
flex-grow: 1;
& input {
min-width: 0px;
border: 0px;
background: ${theme.palette.background.default};
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
}
& input:focus {
border-color: #4298ef;
}
`;
const Swatch = styled.div<{ $color: string }>`
display: inline-flex;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["300"]};`;
}
return `border: 1px solid ${$color};`;
}}
background-color: ${({ $color }): string => $color};
width: 28px;
height: 28px;
border-radius: 5px;
`;
const ColorPickerInput = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin: 8px;
gap: 8px;
`; `;
export default ColorPicker; export default ColorPicker;

View File

@@ -114,7 +114,7 @@ const Editor = (options: EditorOptions) => {
} }
} }
if (type === cell.focus) { if (type === cell.focus) {
textareaRef.current?.focus(); textareaRef.current?.focus({ preventScroll: true });
} }
}); });

View File

@@ -2,6 +2,7 @@ import { Button, Menu, MenuItem, styled } from "@mui/material";
import type { MenuItemProps } from "@mui/material"; import type { MenuItemProps } from "@mui/material";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme"; import { theme } from "../../theme";
import ColorPicker from "../ColorPicker/ColorPicker"; import ColorPicker from "../ColorPicker/ColorPicker";
import { isInReferenceMode } from "../Editor/util"; import { isInReferenceMode } from "../Editor/util";
@@ -28,6 +29,7 @@ function SheetTab(props: SheetTabProps) {
const [colorPickerOpen, setColorPickerOpen] = useState(false); const [colorPickerOpen, setColorPickerOpen] = useState(false);
const colorButton = useRef<HTMLDivElement>(null); const colorButton = useRef<HTMLDivElement>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const { t } = useTranslation();
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => { const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
@@ -136,6 +138,8 @@ function SheetTab(props: SheetTabProps) {
/> />
<ColorPicker <ColorPicker
color={color} color={color}
defaultColor="#FFFFFF"
title={t("color_picker.no_fill")}
onChange={(color): void => { onChange={(color): void => {
props.onColorChanged(color); props.onColorChanged(color);
setColorPickerOpen(false); setColorPickerOpen(false);
@@ -145,6 +149,8 @@ function SheetTab(props: SheetTabProps) {
}} }}
anchorEl={colorButton} anchorEl={colorButton}
open={colorPickerOpen} open={colorPickerOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: "bottom", horizontal: "left" }}
/> />
<SheetDeleteDialog <SheetDeleteDialog
open={deleteDialogOpen} open={deleteDialogOpen}

View File

@@ -410,10 +410,10 @@ function Toolbar(properties: ToolbarProperties) {
<StyledButton <StyledButton
type="button" type="button"
$pressed={false} $pressed={false}
disabled={!canEdit}
onClick={() => { onClick={() => {
properties.onClearFormatting(); properties.onClearFormatting();
}} }}
disabled={!canEdit}
title={t("toolbar.clear_formatting")} title={t("toolbar.clear_formatting")}
> >
<RemoveFormatting /> <RemoveFormatting />
@@ -421,10 +421,10 @@ function Toolbar(properties: ToolbarProperties) {
<StyledButton <StyledButton
type="button" type="button"
$pressed={false} $pressed={false}
disabled={!canEdit}
onClick={() => { onClick={() => {
properties.onDownloadPNG(); properties.onDownloadPNG();
}} }}
disabled={!canEdit}
title={t("toolbar.selected_png")} title={t("toolbar.selected_png")}
> >
<ImageDown /> <ImageDown />
@@ -432,6 +432,8 @@ function Toolbar(properties: ToolbarProperties) {
<ColorPicker <ColorPicker
color={properties.fontColor} color={properties.fontColor}
defaultColor="#000000"
title={t("color_picker.default")}
onChange={(color): void => { onChange={(color): void => {
properties.onTextColorPicked(color); properties.onTextColorPicked(color);
setFontColorPickerOpen(false); setFontColorPickerOpen(false);
@@ -441,11 +443,17 @@ function Toolbar(properties: ToolbarProperties) {
}} }}
anchorEl={fontColorButton} anchorEl={fontColorButton}
open={fontColorPickerOpen} open={fontColorPickerOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
/> />
<ColorPicker <ColorPicker
color={properties.fillColor} color={properties.fillColor}
defaultColor="#FFFFFF"
title={t("color_picker.default")}
onChange={(color): void => { onChange={(color): void => {
if (color !== null) {
properties.onFillColorPicked(color); properties.onFillColorPicked(color);
}
setFillColorPickerOpen(false); setFillColorPickerOpen(false);
}} }}
onClose={() => { onClose={() => {
@@ -453,6 +461,8 @@ function Toolbar(properties: ToolbarProperties) {
}} }}
anchorEl={fillColorButton} anchorEl={fillColorButton}
open={fillColorPickerOpen} open={fillColorPickerOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
/> />
<BorderPicker <BorderPicker
onChange={(border): void => { onChange={(border): void => {

View File

@@ -48,7 +48,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
); );
const focusWorkbook = useCallback(() => { const focusWorkbook = useCallback(() => {
if (rootRef.current) { if (rootRef.current) {
rootRef.current.focus(); rootRef.current.focus({ preventScroll: true });
// HACK: We need to select something inside the root for onCopy to work // HACK: We need to select something inside the root for onCopy to work
const selection = window.getSelection(); const selection = window.getSelection();
if (selection) { if (selection) {
@@ -567,19 +567,15 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const { const {
range: [rowStart, columnStart, rowEnd, columnEnd], range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView(); } = model.getSelectedView();
const { topLeftCell, bottomRightCell } = // NB: cells outside of the displayed area are not rendered
worksheetCanvas.getVisibleCells(); // I think the only reasonable way to do this would be server side.
const firstRow = Math.max(rowStart, topLeftCell.row);
const firstColumn = Math.max(columnStart, topLeftCell.column);
const lastRow = Math.min(rowEnd, bottomRightCell.row);
const lastColumn = Math.min(columnEnd, bottomRightCell.column);
let [x, y] = worksheetCanvas.getCoordinatesByCell( let [x, y] = worksheetCanvas.getCoordinatesByCell(
firstRow, rowStart,
firstColumn, columnStart,
); );
const [x1, y1] = worksheetCanvas.getCoordinatesByCell( const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
lastRow + 1, rowEnd + 1,
lastColumn + 1, columnEnd + 1,
); );
const width = (x1 - x) * devicePixelRatio; const width = (x1 - x) * devicePixelRatio;
const height = (y1 - y) * devicePixelRatio; const height = (y1 - y) * devicePixelRatio;

View File

@@ -24,7 +24,7 @@ import {
TOOLBAR_HEIGHT, TOOLBAR_HEIGHT,
} from "../constants"; } from "../constants";
import type { Cell } from "../types"; import type { Cell } from "../types";
import { AreaType, type WorkbookState } from "../workbookState"; import type { WorkbookState } from "../workbookState";
import CellContextMenu from "./CellContextMenu"; import CellContextMenu from "./CellContextMenu";
import usePointer from "./usePointer"; import usePointer from "./usePointer";
@@ -59,7 +59,6 @@ const Worksheet = forwardRef(
const spacerElement = useRef<HTMLDivElement>(null); const spacerElement = useRef<HTMLDivElement>(null);
const cellOutline = useRef<HTMLDivElement>(null); const cellOutline = useRef<HTMLDivElement>(null);
const areaOutline = useRef<HTMLDivElement>(null); const areaOutline = useRef<HTMLDivElement>(null);
const cellOutlineHandle = useRef<HTMLDivElement>(null);
const extendToOutline = useRef<HTMLDivElement>(null); const extendToOutline = useRef<HTMLDivElement>(null);
const columnResizeGuide = useRef<HTMLDivElement>(null); const columnResizeGuide = useRef<HTMLDivElement>(null);
const rowResizeGuide = useRef<HTMLDivElement>(null); const rowResizeGuide = useRef<HTMLDivElement>(null);
@@ -85,7 +84,6 @@ const Worksheet = forwardRef(
const worksheetRef = worksheetElement.current; const worksheetRef = worksheetElement.current;
const outline = cellOutline.current; const outline = cellOutline.current;
const handle = cellOutlineHandle.current;
const area = areaOutline.current; const area = areaOutline.current;
const extendTo = extendToOutline.current; const extendTo = extendToOutline.current;
const editor = editorElement.current; const editor = editorElement.current;
@@ -97,7 +95,6 @@ const Worksheet = forwardRef(
!columnHeadersRef || !columnHeadersRef ||
!worksheetRef || !worksheetRef ||
!outline || !outline ||
!handle ||
!area || !area ||
!extendTo || !extendTo ||
!scrollElement.current || !scrollElement.current ||
@@ -118,7 +115,6 @@ const Worksheet = forwardRef(
rowGuide: rowGuideRef, rowGuide: rowGuideRef,
columnHeaders: columnHeadersRef, columnHeaders: columnHeadersRef,
cellOutline: outline, cellOutline: outline,
cellOutlineHandle: handle,
areaOutline: area, areaOutline: area,
extendToOutline: extendTo, extendToOutline: extendTo,
editor: editor, editor: editor,
@@ -191,8 +187,7 @@ const Worksheet = forwardRef(
worksheetCanvas.current = canvas; worksheetCanvas.current = canvas;
}); });
const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } = const { onPointerMove, onPointerDown, onPointerUp } = usePointer({
usePointer({
model, model,
workbookState, workbookState,
refresh, refresh,
@@ -256,134 +251,6 @@ const Worksheet = forwardRef(
} }
refresh(); refresh();
}, },
onExtendToCell: (cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
// We are either extending by rows or by columns
// And we could be doing it in the positive direction (downwards or right)
// or the negative direction (upwards or left)
if (
row > rowEnd &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < row - rowEnd) ||
(column > columnEnd && column - columnEnd < row - rowEnd))
) {
// rows downwards
const area = {
type: AreaType.rowsDown,
rowStart: rowEnd + 1,
rowEnd: row,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
row < rowStart &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < rowStart - row) ||
(column > columnEnd && column - columnEnd < rowStart - row))
) {
// rows upwards
const area = {
type: AreaType.rowsUp,
rowStart: row,
rowEnd: rowStart,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column > columnEnd &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < column - columnEnd) ||
(row > rowEnd && row - rowEnd < column - columnEnd))
) {
// columns right
const area = {
type: AreaType.columnsRight,
rowStart,
rowEnd,
columnStart: columnEnd + 1,
columnEnd: column,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column < columnStart &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < columnStart - column) ||
(row > rowEnd && row - rowEnd < columnStart - column))
) {
// columns left
const area = {
type: AreaType.columnsLeft,
rowStart,
rowEnd,
columnStart: column,
columnEnd: columnStart,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
}
},
onExtendToEnd: () => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { sheet, range } = model.getSelectedView();
const extendedArea = workbookState.getExtendToArea();
if (!extendedArea) {
return;
}
const rowStart = Math.min(range[0], range[2]);
const height = Math.abs(range[2] - range[0]) + 1;
const width = Math.abs(range[3] - range[1]) + 1;
const columnStart = Math.min(range[1], range[3]);
const area = {
sheet,
row: rowStart,
column: columnStart,
width,
height,
};
switch (extendedArea.type) {
case AreaType.rowsDown:
model.autoFillRows(area, extendedArea.rowEnd);
break;
case AreaType.rowsUp: {
model.autoFillRows(area, extendedArea.rowStart);
break;
}
case AreaType.columnsRight: {
model.autoFillColumns(area, extendedArea.columnEnd);
break;
}
case AreaType.columnsLeft: {
model.autoFillColumns(area, extendedArea.columnStart);
break;
}
}
model.setSelectedRange(
Math.min(rowStart, extendedArea.rowStart),
Math.min(columnStart, extendedArea.columnStart),
Math.max(rowStart + height - 1, extendedArea.rowEnd),
Math.max(columnStart + width - 1, extendedArea.columnEnd),
);
workbookState.clearExtendToArea();
canvas.renderSheet();
},
canvasElement, canvasElement,
worksheetElement, worksheetElement,
worksheetCanvas, worksheetCanvas,
@@ -463,10 +330,6 @@ const Worksheet = forwardRef(
</EditorWrapper> </EditorWrapper>
<AreaOutline ref={areaOutline} /> <AreaOutline ref={areaOutline} />
<ExtendToOutline ref={extendToOutline} /> <ExtendToOutline ref={extendToOutline} />
<CellOutlineHandle
ref={cellOutlineHandle}
onPointerDown={onPointerHandleDown}
/>
<ColumnResizeGuide ref={columnResizeGuide} /> <ColumnResizeGuide ref={columnResizeGuide} />
<RowResizeGuide ref={rowResizeGuide} /> <RowResizeGuide ref={rowResizeGuide} />
<ColumnHeaders ref={columnHeaders} /> <ColumnHeaders ref={columnHeaders} />
@@ -640,15 +503,6 @@ const CellOutline = styled("div")`
display: flex; display: flex;
`; `;
const CellOutlineHandle = styled("div")`
position: absolute;
width: 5px;
height: 5px;
background: ${outlineColor};
cursor: crosshair;
border-radius: 1px;
`;
const ExtendToOutline = styled("div")` const ExtendToOutline = styled("div")`
position: absolute; position: absolute;
border: 1px dashed ${outlineColor}; border: 1px dashed ${outlineColor};

View File

@@ -20,8 +20,6 @@ interface PointerSettings {
onAllSheetSelected: () => void; onAllSheetSelected: () => void;
onAreaSelecting: (cell: Cell) => void; onAreaSelecting: (cell: Cell) => void;
onAreaSelected: () => void; onAreaSelected: () => void;
onExtendToCell: (cell: Cell) => void;
onExtendToEnd: () => void;
model: Model; model: Model;
workbookState: WorkbookState; workbookState: WorkbookState;
refresh: () => void; refresh: () => void;
@@ -31,12 +29,10 @@ interface PointerEvents {
onPointerDown: (event: PointerEvent) => void; onPointerDown: (event: PointerEvent) => void;
onPointerMove: (event: PointerEvent) => void; onPointerMove: (event: PointerEvent) => void;
onPointerUp: (event: PointerEvent) => void; onPointerUp: (event: PointerEvent) => void;
onPointerHandleDown: (event: PointerEvent) => void;
} }
const usePointer = (options: PointerSettings): PointerEvents => { const usePointer = (options: PointerSettings): PointerEvents => {
const isSelecting = useRef(false); const isSelecting = useRef(false);
const isExtending = useRef(false);
const isInsertingRef = useRef(false); const isInsertingRef = useRef(false);
const onPointerMove = useCallback( const onPointerMove = useCallback(
@@ -47,9 +43,7 @@ const usePointer = (options: PointerSettings): PointerEvents => {
return; return;
} }
if ( if (!(isSelecting.current || isInsertingRef.current)) {
!(isSelecting.current || isExtending.current || isInsertingRef.current)
) {
return; return;
} }
const { canvasElement, model, worksheetCanvas } = options; const { canvasElement, model, worksheetCanvas } = options;
@@ -70,8 +64,6 @@ const usePointer = (options: PointerSettings): PointerEvents => {
if (isSelecting.current) { if (isSelecting.current) {
options.onAreaSelecting(cell); options.onAreaSelecting(cell);
} else if (isExtending.current) {
options.onExtendToCell(cell);
} else if (isInsertingRef.current) { } else if (isInsertingRef.current) {
const { refresh, workbookState } = options; const { refresh, workbookState } = options;
const editingCell = workbookState.getEditingCell(); const editingCell = workbookState.getEditingCell();
@@ -103,11 +95,6 @@ const usePointer = (options: PointerSettings): PointerEvents => {
isSelecting.current = false; isSelecting.current = false;
worksheetElement.current?.releasePointerCapture(event.pointerId); worksheetElement.current?.releasePointerCapture(event.pointerId);
options.onAreaSelected(); options.onAreaSelected();
} else if (isExtending.current) {
const { worksheetElement } = options;
isExtending.current = false;
worksheetElement.current?.releasePointerCapture(event.pointerId);
options.onExtendToEnd();
} else if (isInsertingRef.current) { } else if (isInsertingRef.current) {
const { worksheetElement } = options; const { worksheetElement } = options;
isInsertingRef.current = false; isInsertingRef.current = false;
@@ -120,10 +107,14 @@ const usePointer = (options: PointerSettings): PointerEvents => {
const onPointerDown = useCallback( const onPointerDown = useCallback(
(event: PointerEvent) => { (event: PointerEvent) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target !== null && target.className === "column-resize-handle") { if (target.className === "column-resize-handle") {
// we are resizing a column // we are resizing a column
return; return;
} }
if (target.className.includes("ironcalc-cell-handle")) {
// we are extending values
return;
}
let x = event.clientX; let x = event.clientX;
let y = event.clientY; let y = event.clientY;
const { const {
@@ -236,25 +227,17 @@ const usePointer = (options: PointerSettings): PointerEvents => {
); );
// we continue to select the new cell // we continue to select the new cell
} }
if (event.shiftKey) {
// We are extending the selection
options.onAreaSelecting(cell);
options.onAreaSelected();
} else {
// We are selecting a single cell
options.onCellSelected(cell, event); options.onCellSelected(cell, event);
isSelecting.current = true; isSelecting.current = true;
worksheetWrapper.setPointerCapture(event.pointerId); worksheetWrapper.setPointerCapture(event.pointerId);
} }
},
[options],
);
const onPointerHandleDown = useCallback(
(event: PointerEvent) => {
const worksheetWrapper = options.worksheetElement.current;
// Silence the linter
if (!worksheetWrapper) {
return;
} }
isExtending.current = true;
worksheetWrapper.setPointerCapture(event.pointerId);
event.stopPropagation();
event.preventDefault();
}, },
[options], [options],
); );
@@ -263,7 +246,6 @@ const usePointer = (options: PointerSettings): PointerEvents => {
onPointerDown, onPointerDown,
onPointerMove, onPointerMove,
onPointerUp, onPointerUp,
onPointerHandleDown,
}; };
}; };

View File

@@ -0,0 +1,211 @@
import { AreaType } from "../workbookState";
import { LAST_COLUMN, LAST_ROW, outlineColor } from "./constants";
import type WorksheetCanvas from "./worksheetCanvas";
export function attachOutlineHandle(
worksheet: WorksheetCanvas,
): HTMLDivElement {
// There is *always* a parent
const parent = worksheet.canvas.parentElement as HTMLDivElement;
// Remove any existing cell outline handles
for (const handle of parent.querySelectorAll(".ironcalc-cell-handle")) {
handle.remove();
}
// Create a new cell outline handle
const cellOutlineHandle = document.createElement("div");
cellOutlineHandle.className = "ironcalc-cell-handle";
parent.appendChild(cellOutlineHandle);
worksheet.cellOutlineHandle = cellOutlineHandle;
Object.assign(cellOutlineHandle.style, {
position: "absolute",
width: "5px",
height: "5px",
background: outlineColor,
cursor: "crosshair",
borderRadius: "1px",
});
// cell handle events
const resizeHandleMove = (event: MouseEvent): void => {
const canvasRect = worksheet.canvas.getBoundingClientRect();
const x = event.clientX - canvasRect.x;
const y = event.clientY - canvasRect.y;
const cell = worksheet.getCellByCoordinates(x, y);
if (!cell) {
return;
}
const { row, column } = cell;
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = worksheet.model.getSelectedView();
// We are either extending by rows or by columns
// And we could be doing it in the positive direction (downwards or right)
// or the negative direction (upwards or left)
if (
row > rowEnd &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < row - rowEnd) ||
(column > columnEnd && column - columnEnd < row - rowEnd))
) {
// rows downwards
const area = {
type: AreaType.rowsDown,
rowStart: rowEnd + 1,
rowEnd: row,
columnStart,
columnEnd,
};
worksheet.workbookState.setExtendToArea(area);
worksheet.renderSheet();
} else if (
row < rowStart &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < rowStart - row) ||
(column > columnEnd && column - columnEnd < rowStart - row))
) {
// rows upwards
const area = {
type: AreaType.rowsUp,
rowStart: row,
rowEnd: rowStart,
columnStart,
columnEnd,
};
worksheet.workbookState.setExtendToArea(area);
worksheet.renderSheet();
} else if (
column > columnEnd &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < column - columnEnd) ||
(row > rowEnd && row - rowEnd < column - columnEnd))
) {
// columns right
const area = {
type: AreaType.columnsRight,
rowStart,
rowEnd,
columnStart: columnEnd + 1,
columnEnd: column,
};
worksheet.workbookState.setExtendToArea(area);
worksheet.renderSheet();
} else if (
column < columnStart &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < columnStart - column) ||
(row > rowEnd && row - rowEnd < columnStart - column))
) {
// columns left
const area = {
type: AreaType.columnsLeft,
rowStart,
rowEnd,
columnStart: column,
columnEnd: columnStart,
};
worksheet.workbookState.setExtendToArea(area);
worksheet.renderSheet();
}
};
const resizeHandleUp = (_event: MouseEvent): void => {
document.removeEventListener("pointermove", resizeHandleMove);
document.removeEventListener("pointerup", resizeHandleUp);
const { sheet, range } = worksheet.model.getSelectedView();
const extendedArea = worksheet.workbookState.getExtendToArea();
if (!extendedArea) {
return;
}
const rowStart = Math.min(range[0], range[2]);
const height = Math.abs(range[2] - range[0]) + 1;
const width = Math.abs(range[3] - range[1]) + 1;
const columnStart = Math.min(range[1], range[3]);
const area = {
sheet,
row: rowStart,
column: columnStart,
width,
height,
};
switch (extendedArea.type) {
case AreaType.rowsDown:
worksheet.model.autoFillRows(area, extendedArea.rowEnd);
break;
case AreaType.rowsUp: {
worksheet.model.autoFillRows(area, extendedArea.rowStart);
break;
}
case AreaType.columnsRight: {
worksheet.model.autoFillColumns(area, extendedArea.columnEnd);
break;
}
case AreaType.columnsLeft: {
worksheet.model.autoFillColumns(area, extendedArea.columnStart);
break;
}
}
worksheet.model.setSelectedRange(
Math.min(rowStart, extendedArea.rowStart),
Math.min(columnStart, extendedArea.columnStart),
Math.max(rowStart + height - 1, extendedArea.rowEnd),
Math.max(columnStart + width - 1, extendedArea.columnEnd),
);
worksheet.workbookState.clearExtendToArea();
worksheet.renderSheet();
};
cellOutlineHandle.addEventListener("pointerdown", () => {
document.addEventListener("pointermove", resizeHandleMove);
document.addEventListener("pointerup", resizeHandleUp);
});
cellOutlineHandle.addEventListener("dblclick", (event) => {
// On double-click, we will auto-fill the rows below the selected cell
const [sheet, row, column] = worksheet.model.getSelectedCell();
let lastUsedRow = row + 1;
let testColumn = column - 1;
// The "test column" is the column to the left of the selected cell or the next column if the left one is empty
if (
testColumn < 1 ||
worksheet.model.getFormattedCellValue(sheet, row, column - 1) === ""
) {
testColumn = column + 1;
if (
testColumn > LAST_COLUMN ||
worksheet.model.getFormattedCellValue(sheet, row, testColumn) === ""
) {
return;
}
}
// Find the last used row in the "test column"
for (let r = row + 1; r <= LAST_ROW; r += 1) {
if (worksheet.model.getFormattedCellValue(sheet, r, testColumn) === "") {
break;
}
lastUsedRow = r;
}
const area = {
sheet,
row: row,
column: column,
width: 1,
height: 1,
};
worksheet.model.autoFillRows(area, lastUsedRow);
event.stopPropagation();
worksheet.renderSheet();
});
return cellOutlineHandle;
}

View File

@@ -0,0 +1,63 @@
// Get a 10% transparency of an hex color
export function hexToRGBA10Percent(colorHex: string): string {
// Remove the leading hash (#) if present
const hex = colorHex.replace(/^#/, "");
// Parse the hex color
const red = Number.parseInt(hex.substring(0, 2), 16);
const green = Number.parseInt(hex.substring(2, 4), 16);
const blue = Number.parseInt(hex.substring(4, 6), 16);
// Set the alpha (opacity) to 0.1 (10%)
const alpha = 0.1;
// Return the RGBA color string
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
/**
* Splits the given text into multiple lines. If `wrapText` is true, it applies word-wrapping
* based on the specified canvas context, maximum width, and horizontal padding.
*
* - First, the text is split by newline characters so that explicit newlines are respected.
* - If wrapping is enabled, each line is further split into words and measured against the
* available width. Whenever adding an extra word would exceed
* this limit, a new line is started.
*
* @param text The text to split into lines.
* @param wrapText Whether to apply word-wrapping or just return text split by newlines.
* @param context The `CanvasRenderingContext2D` used for measuring text width.
* @param width The maximum width for each line.
* @returns An array of lines (strings), each fitting within the specified width if wrapping is enabled.
*/
export function computeWrappedLines(
text: string,
wrapText: boolean,
context: CanvasRenderingContext2D,
width: number,
): string[] {
// Split the text into lines
const rawLines = text.split("\n");
if (!wrapText) {
// If there is no wrapping, return the raw lines
return rawLines;
}
const wrappedLines = [];
for (const line of rawLines) {
const words = line.split(" ");
let currentLine = words[0];
for (let i = 1; i < words.length; i += 1) {
const word = words[i];
const testLine = `${currentLine} ${word}`;
const textWidth = context.measureText(testLine).width;
if (textWidth < width) {
currentLine = testLine;
} else {
wrappedLines.push(currentLine);
currentLine = word;
}
}
wrappedLines.push(currentLine);
}
return wrappedLines;
}

View File

@@ -18,6 +18,8 @@ import {
headerTextColor, headerTextColor,
outlineColor, outlineColor,
} from "./constants"; } from "./constants";
import { attachOutlineHandle } from "./outlineHandle";
import { computeWrappedLines, hexToRGBA10Percent } from "./util";
export interface CanvasSettings { export interface CanvasSettings {
model: Model; model: Model;
@@ -28,7 +30,6 @@ export interface CanvasSettings {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
cellOutline: HTMLDivElement; cellOutline: HTMLDivElement;
areaOutline: HTMLDivElement; areaOutline: HTMLDivElement;
cellOutlineHandle: HTMLDivElement;
extendToOutline: HTMLDivElement; extendToOutline: HTMLDivElement;
columnGuide: HTMLDivElement; columnGuide: HTMLDivElement;
rowGuide: HTMLDivElement; rowGuide: HTMLDivElement;
@@ -53,69 +54,6 @@ export const defaultCellFontFamily = fonts.regular;
export const headerFontFamily = fonts.regular; export const headerFontFamily = fonts.regular;
export const frozenSeparatorWidth = 3; export const frozenSeparatorWidth = 3;
// Get a 10% transparency of an hex color
function hexToRGBA10Percent(colorHex: string): string {
// Remove the leading hash (#) if present
const hex = colorHex.replace(/^#/, "");
// Parse the hex color
const red = Number.parseInt(hex.substring(0, 2), 16);
const green = Number.parseInt(hex.substring(2, 4), 16);
const blue = Number.parseInt(hex.substring(4, 6), 16);
// Set the alpha (opacity) to 0.1 (10%)
const alpha = 0.1;
// Return the RGBA color string
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
/**
* Splits the given text into multiple lines. If `wrapText` is true, it applies word-wrapping
* based on the specified canvas context, maximum width, and horizontal padding.
*
* - First, the text is split by newline characters so that explicit newlines are respected.
* - If wrapping is enabled, each line is further split into words and measured against the
* available width. Whenever adding an extra word would exceed
* this limit, a new line is started.
*
* @param text The text to split into lines.
* @param wrapText Whether to apply word-wrapping or just return text split by newlines.
* @param context The `CanvasRenderingContext2D` used for measuring text width.
* @param width The maximum width for each line.
* @returns An array of lines (strings), each fitting within the specified width if wrapping is enabled.
*/
function computeWrappedLines(
text: string,
wrapText: boolean,
context: CanvasRenderingContext2D,
width: number,
): string[] {
// Split the text into lines
const rawLines = text.split("\n");
if (!wrapText) {
// If there is no wrapping, return the raw lines
return rawLines;
}
const wrappedLines = [];
for (const line of rawLines) {
const words = line.split(" ");
let currentLine = words[0];
for (const word of words) {
const testLine = `${currentLine} ${word}`;
const textWidth = context.measureText(testLine).width;
if (textWidth < width) {
currentLine = testLine;
} else {
wrappedLines.push(currentLine);
currentLine = word;
}
}
wrappedLines.push(currentLine);
}
return wrappedLines;
}
export default class WorksheetCanvas { export default class WorksheetCanvas {
sheetWidth: number; sheetWidth: number;
@@ -168,7 +106,6 @@ export default class WorksheetCanvas {
this.refresh = options.refresh; this.refresh = options.refresh;
this.cellOutline = options.elements.cellOutline; this.cellOutline = options.elements.cellOutline;
this.cellOutlineHandle = options.elements.cellOutlineHandle;
this.areaOutline = options.elements.areaOutline; this.areaOutline = options.elements.areaOutline;
this.extendToOutline = options.elements.extendToOutline; this.extendToOutline = options.elements.extendToOutline;
this.rowGuide = options.elements.rowGuide; this.rowGuide = options.elements.rowGuide;
@@ -178,6 +115,7 @@ export default class WorksheetCanvas {
this.onColumnWidthChanges = options.onColumnWidthChanges; this.onColumnWidthChanges = options.onColumnWidthChanges;
this.onRowHeightChanges = options.onRowHeightChanges; this.onRowHeightChanges = options.onRowHeightChanges;
this.resetHeaders(); this.resetHeaders();
this.cellOutlineHandle = attachOutlineHandle(this);
} }
setScrollPosition(scrollPosition: { left: number; top: number }): void { setScrollPosition(scrollPosition: { left: number; top: number }): void {

View File

@@ -121,6 +121,11 @@
"insert_column": "Insert column" "insert_column": "Insert column"
}, },
"color_picker": { "color_picker": {
"apply": "Apply" "apply": "Add color",
"cancel": "Cancel",
"add": "Add new color",
"default": "Default color",
"no_fill": "No fill",
"custom": "Custom"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -14,19 +14,19 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@ironcalc/workbook": "file:../../IronCalc/", "@ironcalc/workbook": "file:../../IronCalc/",
"@mui/material": "^6.4", "@mui/material": "^7.1.1",
"lucide-react": "^0.473.0", "lucide-react": "^0.513.0",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0" "react-dom": "^19.1.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@types/react": "^19.0.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2", "typescript": "~5.8.3",
"vite": "^6.0.5", "vite": "^6.3.5",
"vite-plugin-svgr": "^4.2.0" "vite-plugin-svgr": "^4.2.0"
} }
} }

View File

@@ -1,7 +1,8 @@
import "./App.css"; import "./App.css";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { FileBar } from "./components/FileBar"; import { FileBar } from "./components/FileBar";
import LeftDrawer from "./components/LeftDrawer/LeftDrawer";
import { import {
get_documentation_model, get_documentation_model,
get_model, get_model,
@@ -10,6 +11,8 @@ import {
import { import {
createNewModel, createNewModel,
deleteSelectedModel, deleteSelectedModel,
getModelsMetadata,
getSelectedUuid,
loadModelFromStorageOrCreate, loadModelFromStorageOrCreate,
saveModelToStorage, saveModelToStorage,
saveSelectedModelInStorage, saveSelectedModelInStorage,
@@ -21,6 +24,14 @@ import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook";
function App() { function App() {
const [model, setModel] = useState<Model | null>(null); const [model, setModel] = useState<Model | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [modelsMetadata, setModelsMetadata] = useState(getModelsMetadata());
const [selectedUuid, setSelectedUuid] = useState(getSelectedUuid());
const refreshModelsData = useCallback(() => {
setModelsMetadata(getModelsMetadata());
setSelectedUuid(getSelectedUuid());
}, []);
useEffect(() => { useEffect(() => {
async function start() { async function start() {
@@ -38,6 +49,7 @@ function App() {
const importedModel = Model.from_bytes(model_bytes); const importedModel = Model.from_bytes(model_bytes);
localStorage.removeItem("selected"); localStorage.removeItem("selected");
setModel(importedModel); setModel(importedModel);
refreshModelsData();
} catch (e) { } catch (e) {
alert("Model not found, or failed to load"); alert("Model not found, or failed to load");
} }
@@ -47,6 +59,7 @@ function App() {
const importedModel = Model.from_bytes(model_bytes); const importedModel = Model.from_bytes(model_bytes);
localStorage.removeItem("selected"); localStorage.removeItem("selected");
setModel(importedModel); setModel(importedModel);
refreshModelsData();
} catch (e) { } catch (e) {
alert("Example file not found, or failed to load"); alert("Example file not found, or failed to load");
} }
@@ -54,10 +67,11 @@ function App() {
// try to load from local storage // try to load from local storage
const newModel = loadModelFromStorageOrCreate(); const newModel = loadModelFromStorageOrCreate();
setModel(newModel); setModel(newModel);
refreshModelsData();
} }
} }
start(); start();
}, []); }, [refreshModelsData]);
if (!model) { if (!model) {
return ( return (
@@ -79,48 +93,80 @@ function App() {
// We could use context for model, but the problem is that it should initialized to null. // 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. // Passing the property down makes sure it is always defined.
// Handlers for model changes that also update our models state
const handleNewModel = () => {
const newModel = createNewModel();
setModel(newModel);
refreshModelsData();
};
const handleSetModel = (uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
refreshModelsData();
}
};
const handleDeleteModel = () => {
const newModel = deleteSelectedModel();
if (newModel) {
setModel(newModel);
refreshModelsData();
}
};
return ( return (
<Wrapper> <AppContainer>
<LeftDrawer
open={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
newModel={handleNewModel}
setModel={handleSetModel}
models={modelsMetadata}
selectedUuid={selectedUuid}
setDeleteDialogOpen={() => {}}
/>
<MainContent isDrawerOpen={isDrawerOpen}>
<FileBar <FileBar
model={model} model={model}
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => { onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
const blob = await uploadFile(arrayBuffer, fileName); const blob = await uploadFile(arrayBuffer, fileName);
const bytes = new Uint8Array(await blob.arrayBuffer()); const bytes = new Uint8Array(await blob.arrayBuffer());
const newModel = Model.from_bytes(bytes); const newModel = Model.from_bytes(bytes);
saveModelToStorage(newModel); saveModelToStorage(newModel);
setModel(newModel); setModel(newModel);
refreshModelsData();
}} }}
newModel={() => { newModel={handleNewModel}
setModel(createNewModel()); setModel={handleSetModel}
}} onDelete={handleDeleteModel}
setModel={(uuid: string) => { isDrawerOpen={isDrawerOpen}
const newModel = selectModelFromStorage(uuid); setIsDrawerOpen={setIsDrawerOpen}
if (newModel) { refreshModelsData={refreshModelsData}
setModel(newModel);
}
}}
onDelete={() => {
const newModel = deleteSelectedModel();
if (newModel) {
setModel(newModel);
}
}}
/> />
<IronCalc model={model} /> <IronCalc model={model} />
</Wrapper> </MainContent>
</AppContainer>
); );
} }
const Wrapper = styled("div")` const AppContainer = styled("div")`
margin: 0px; display: flex;
padding: 0px;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative;
overflow: hidden;
`;
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : "-264px")};
transition: margin-left 0.3s ease;
width: ${({ isDrawerOpen }) =>
isDrawerOpen ? "calc(100% - 264px)" : "100%"};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute;
`; `;
const Loading = styled("div")` const Loading = styled("div")`

View File

@@ -1,59 +1,107 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import type { Model } from "@ironcalc/workbook"; import type { Model } from "@ironcalc/workbook";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook"; import { Button, IconButton } from "@mui/material";
import { useRef, useState } from "react"; import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { FileMenu } from "./FileMenu"; import { useLayoutEffect, useRef, useState } from "react";
import { DesktopMenu, MobileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton"; import { ShareButton } from "./ShareButton";
import ShareWorkbookDialog from "./ShareWorkbookDialog"; import ShareWorkbookDialog from "./ShareWorkbookDialog";
import { WorkbookTitle } from "./WorkbookTitle"; import { WorkbookTitle } from "./WorkbookTitle";
import { downloadModel } from "./rpc"; import { downloadModel } from "./rpc";
import { updateNameSelectedWorkbook } from "./storage"; import { updateNameSelectedWorkbook } from "./storage";
// This hook is used to get the width of the window
function useWindowWidth() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
function updateWidth() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", updateWidth);
updateWidth();
return () => window.removeEventListener("resize", updateWidth);
}, []);
return width;
}
export function FileBar(properties: { export function FileBar(properties: {
model: Model; model: Model;
newModel: () => void; newModel: () => void;
setModel: (key: string) => void; setModel: (key: string) => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void; onDelete: () => void;
isDrawerOpen: boolean;
setIsDrawerOpen: (open: boolean) => void;
refreshModelsData: () => void; // Add this new prop
}) { }) {
const hiddenInputRef = useRef<HTMLInputElement>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const spacerRef = useRef<HTMLDivElement>(null);
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
const width = useWindowWidth();
const fileButtonRef = useRef<HTMLButtonElement>(null);
return ( // biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
<FileBarWrapper> useLayoutEffect(() => {
<StyledDesktopLogo /> const el = spacerRef.current;
<StyledIronCalcIcon /> if (el) {
<Divider /> const bb = el.getBoundingClientRect();
<FileMenu setMaxTitleWidth(bb.right - bb.left - 50);
newModel={properties.newModel} }
setModel={properties.setModel} }, [width]);
onModelUpload={properties.onModelUpload}
onDownload={async () => { // Common handler functions for both menu types
const handleDownload = async () => {
const model = properties.model; const model = properties.model;
const bytes = model.toBytes(); const bytes = model.toBytes();
const fileName = model.getName(); const fileName = model.getName();
await downloadModel(bytes, fileName); await downloadModel(bytes, fileName);
}} };
return (
<FileBarWrapper>
<DrawerButton
onClick={() => properties.setIsDrawerOpen(!properties.isDrawerOpen)}
disableRipple
>
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
</DrawerButton>
<DesktopButtonsWrapper>
<DesktopMenu
newModel={properties.newModel}
setModel={properties.setModel}
onModelUpload={properties.onModelUpload}
onDownload={handleDownload}
onDelete={properties.onDelete} onDelete={properties.onDelete}
/> />
<HelpButton <FileBarButton
disableRipple
onClick={() => window.open("https://docs.ironcalc.com", "_blank")} onClick={() => window.open("https://docs.ironcalc.com", "_blank")}
> >
Help Help
</HelpButton> </FileBarButton>
</DesktopButtonsWrapper>
<MobileButtonsWrapper>
<MobileMenu
newModel={properties.newModel}
setModel={properties.setModel}
onModelUpload={properties.onModelUpload}
onDownload={handleDownload}
onDelete={properties.onDelete}
/>
</MobileButtonsWrapper>
<Spacer ref={spacerRef} />
<WorkbookTitleWrapper>
<WorkbookTitle <WorkbookTitle
name={properties.model.getName()} name={properties.model.getName()}
onNameChange={(name) => { onNameChange={(name) => {
properties.model.setName(name); properties.model.setName(name);
updateNameSelectedWorkbook(properties.model, name); updateNameSelectedWorkbook(properties.model, name);
properties.refreshModelsData();
}} }}
maxWidth={maxTitleWidth}
/> />
<input </WorkbookTitleWrapper>
ref={hiddenInputRef} <Spacer ref={spacerRef} />
type="text"
style={{ position: "absolute", left: -9999, top: -9999 }}
/>
<div style={{ marginLeft: "auto" }} />
<DialogContainer> <DialogContainer>
<ShareButton onClick={() => setIsDialogOpen(true)} /> <ShareButton onClick={() => setIsDialogOpen(true)} />
{isDialogOpen && ( {isDialogOpen && (
@@ -68,50 +116,92 @@ export function FileBar(properties: {
); );
} }
const StyledDesktopLogo = styled(IronCalcLogo)` const WorkbookTitleWrapper = styled("div")`
width: 120px; position: relative;
margin-left: 12px;
@media (max-width: 769px) {
display: none;
}
`; `;
const StyledIronCalcIcon = styled(IronCalcIcon)` // The "Spacer" component occupies as much space as possible between the menu and the share button
width: 36px; const Spacer = styled("div")`
margin-left: 10px; flex-grow: 1;
@media (min-width: 769px) {
display: none;
}
`; `;
const HelpButton = styled("div")` const DrawerButton = styled(IconButton)`
display: flex; margin-left: 8px;
align-items: center; height: 32px;
font-size: 12px; width: 32px;
font-family: Inter;
padding: 8px; padding: 8px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; svg {
stroke-width: 2px;
stroke: #757575;
width: 16px;
height: 16px;
}
&:hover { &:hover {
background-color: #f2f2f2; background-color: #f2f2f2;
} }
&:active {
background-color: #e0e0e0;
}
`; `;
const Divider = styled("div")` // The container must be relative positioned so we can position the title absolutely
margin: 0px 8px 0px 16px;
height: 12px;
border-left: 1px solid #e0e0e0;
`;
const FileBarWrapper = styled("div")` const FileBarWrapper = styled("div")`
position: relative;
height: 60px; height: 60px;
min-height: 60px;
width: 100%; width: 100%;
background: #fff; background: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
position: relative;
justify-content: space-between; justify-content: space-between;
box-sizing: border-box;
`;
const DesktopButtonsWrapper = styled("div")`
display: flex;
gap: 4px;
margin-left: 8px;
@media (max-width: 600px) {
display: none;
}
`;
const MobileButtonsWrapper = styled("div")`
display: flex;
gap: 4px;
@media (min-width: 601px) {
display: none;
}
@media (max-width: 600px) {
display: flex;
}
`;
const FileBarButtonContainer = styled("div")`
position: relative;
display: inline-block;
`;
const FileBarButton = styled(Button)`
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
height: 32px;
width: auto;
padding: 4px 8px;
font-weight: 400;
min-width: 0px;
text-transform: capitalize;
color: #333333;
&:hover {
background-color: #f2f2f2;
}
&:active {
background-color: #e0e0e0;
}
`; `;
const DialogContainer = styled("div")` const DialogContainer = styled("div")`

View File

@@ -1,76 +1,165 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material"; import { Button, IconButton, Menu, MenuItem, Modal } from "@mui/material";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react"; import {
ChevronRight,
EllipsisVertical,
FileDown,
FileUp,
Plus,
Trash2,
} from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import DeleteWorkbookDialog from "./DeleteWorkbookDialog"; import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
import UploadFileDialog from "./UploadFileDialog"; import UploadFileDialog from "./UploadFileDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage"; import { getModelsMetadata, getSelectedUuid } from "./storage";
export function DesktopMenu(props: {
newModel: () => void;
setModel: (key: string) => void;
onDownload: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void;
}) {
const [isFileMenuOpen, setFileMenuOpen] = useState(false);
const anchorElement = useRef<HTMLButtonElement>(
null as unknown as HTMLButtonElement,
);
return (
<>
<FileBarButton
onClick={(): void => setFileMenuOpen(!isFileMenuOpen)}
ref={anchorElement}
disableRipple
isOpen={isFileMenuOpen}
>
File
</FileBarButton>
<FileMenu
newModel={props.newModel}
setModel={props.setModel}
onDownload={props.onDownload}
onModelUpload={props.onModelUpload}
onDelete={props.onDelete}
isFileMenuOpen={isFileMenuOpen}
setFileMenuOpen={setFileMenuOpen}
setMobileMenuOpen={() => {}}
anchorElement={anchorElement}
/>
</>
);
}
export function MobileMenu(props: {
newModel: () => void;
setModel: (key: string) => void;
onDownload: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void;
}) {
const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isFileMenuOpen, setFileMenuOpen] = useState(false);
const anchorElement = useRef<HTMLButtonElement>(
null as unknown as HTMLButtonElement,
);
const [fileMenuAnchorEl, setFileMenuAnchorEl] = useState<HTMLElement | null>(
null,
);
return (
<>
<MenuButton
onClick={(): void => setMobileMenuOpen(true)}
ref={anchorElement}
disableRipple
>
<EllipsisVertical />
</MenuButton>
<StyledMenu
open={isMobileMenuOpen}
onClose={(): void => setMobileMenuOpen(false)}
anchorEl={anchorElement.current}
>
<MenuItemWrapper
onClick={(event) => {
setFileMenuOpen(true);
setFileMenuAnchorEl(event.currentTarget);
}}
disableRipple
>
<MenuItemText>File</MenuItemText>
<ChevronRight />
</MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper
onClick={() => {
window.open("https://docs.ironcalc.com", "_blank");
setMobileMenuOpen(false);
}}
disableRipple
>
<MenuItemText>Help</MenuItemText>
</MenuItemWrapper>
</StyledMenu>
<FileMenu
newModel={props.newModel}
setModel={props.setModel}
onDownload={props.onDownload}
onModelUpload={props.onModelUpload}
onDelete={props.onDelete}
isFileMenuOpen={isFileMenuOpen}
setFileMenuOpen={setFileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen}
anchorElement={anchorElement}
/>
</>
);
}
export function FileMenu(props: { export function FileMenu(props: {
newModel: () => void; newModel: () => void;
setModel: (key: string) => void; setModel: (key: string) => void;
onDownload: () => void; onDownload: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void; onDelete: () => void;
isFileMenuOpen: boolean;
setFileMenuOpen: (open: boolean) => void;
setMobileMenuOpen: (open: boolean) => void;
anchorElement: React.RefObject<HTMLButtonElement>;
}) { }) {
const [isMenuOpen, setMenuOpen] = useState(false);
const [isImportMenuOpen, setImportMenuOpen] = useState(false); const [isImportMenuOpen, setImportMenuOpen] = useState(false);
const anchorElement = useRef<HTMLDivElement>(null);
const models = getModelsMetadata(); const models = getModelsMetadata();
const uuids = Object.keys(models);
const selectedUuid = getSelectedUuid(); const selectedUuid = getSelectedUuid();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); 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 ? <StyledCheck /> : ""}
</CheckIndicator>
<MenuItemText
style={{
maxWidth: "240px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{models[uuid]}
</MenuItemText>
</MenuItemWrapper>,
);
}
return ( return (
<> <>
<FileMenuWrapper <StyledMenu
onClick={(): void => setMenuOpen(true)} open={props.isFileMenuOpen}
ref={anchorElement} onClose={(): void => props.setFileMenuOpen(false)}
> anchorEl={props.anchorElement.current}
File anchorOrigin={{
</FileMenuWrapper> vertical: "bottom",
<Menu horizontal: "left",
open={isMenuOpen} }}
onClose={(): void => setMenuOpen(false)} transformOrigin={{
anchorEl={anchorElement.current} vertical: "top",
sx={{ horizontal: "left",
"& .MuiPaper-root": { borderRadius: "8px", padding: "4px 0px" }, }}
"& .MuiList-root": { padding: "0" }, // To prevent closing parent menu when interacting with submenu
onMouseLeave={() => {
if (!isImportMenuOpen && !isDeleteDialogOpen) {
props.setFileMenuOpen(false);
}
}} }}
// anchorOrigin={properties.anchorOrigin}
> >
<MenuItemWrapper <MenuItemWrapper
onClick={() => { onClick={() => {
props.newModel(); props.newModel();
setMenuOpen(false); props.setFileMenuOpen(false);
props.setMobileMenuOpen(false);
}} }}
disableRipple
> >
<StyledPlus /> <StyledPlus />
<MenuItemText>New</MenuItemText> <MenuItemText>New</MenuItemText>
@@ -78,30 +167,37 @@ export function FileMenu(props: {
<MenuItemWrapper <MenuItemWrapper
onClick={() => { onClick={() => {
setImportMenuOpen(true); setImportMenuOpen(true);
setMenuOpen(false); props.setFileMenuOpen(false);
props.setMobileMenuOpen(false);
}} }}
disableRipple
> >
<StyledFileUp /> <StyledFileUp />
<MenuItemText>Import</MenuItemText> <MenuItemText>Import</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuItemWrapper> <MenuItemWrapper
onClick={() => {
props.onDownload();
props.setMobileMenuOpen(false);
}}
disableRipple
>
<StyledFileDown /> <StyledFileDown />
<MenuItemText onClick={props.onDownload}> <MenuItemText>Download (.xlsx)</MenuItemText>
Download (.xlsx)
</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper <MenuItemWrapper
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
setMenuOpen(false); props.setFileMenuOpen(false);
props.setMobileMenuOpen(false);
}} }}
disableRipple
> >
<StyledTrash /> <StyledTrash />
<MenuItemText>Delete workbook</MenuItemText> <MenuItemText>Delete workbook</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuDivider /> </StyledMenu>
{elements}
</Menu>
<Modal <Modal
open={isImportMenuOpen} open={isImportMenuOpen}
onClose={() => { onClose={() => {
@@ -133,6 +229,46 @@ export function FileMenu(props: {
); );
} }
const MenuButton = styled(IconButton)`
height: 32px;
width: 32px;
padding: 8px;
border-radius: 4px;
svg {
stroke-width: 2px;
stroke: #757575;
width: 16px;
height: 16px;
}
&:hover {
background-color: #f2f2f2;
}
&:active {
background-color: #e0e0e0;
}
`;
const FileBarButton = styled(Button)<{ isOpen: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
height: 32px;
width: auto;
padding: 4px 8px;
font-weight: 400;
min-width: 0px;
text-transform: capitalize;
color: #333333;
background-color: ${({ isOpen }) => (isOpen ? "#f2f2f2" : "none")};
&:hover {
background-color: #f2f2f2;
}
&:active {
background-color: #e0e0e0;
}
`;
const StyledPlus = styled(Plus)` const StyledPlus = styled(Plus)`
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -161,13 +297,6 @@ const StyledTrash = styled(Trash2)`
padding-right: 10px; padding-right: 10px;
`; `;
const StyledCheck = styled(Check)`
width: 16px;
height: 16px;
color: #333333;
padding-right: 10px;
`;
const MenuDivider = styled("div")` const MenuDivider = styled("div")`
width: 100%; width: 100%;
margin: auto; margin: auto;
@@ -179,6 +308,7 @@ const MenuDivider = styled("div")`
const MenuItemText = styled("div")` const MenuItemText = styled("div")`
color: #000; color: #000;
font-size: 12px; font-size: 12px;
flex-grow: 1;
`; `;
const MenuItemWrapper = styled(MenuItem)` const MenuItemWrapper = styled(MenuItem)`
@@ -191,23 +321,19 @@ const MenuItemWrapper = styled(MenuItem)`
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
height: 32px; height: 32px;
`; min-height: 32px;
svg {
const FileMenuWrapper = styled("div")` width: 16px;
display: flex; height: 16px;
align-items: center;
font-size: 12px;
font-family: Inter;
padding: 8px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
} }
`; `;
const CheckIndicator = styled("span")` const StyledMenu = styled(Menu)`
display: flex; .MuiPaper-root {
justify-content: center; border-radius: 8px;
min-width: 26px; padding: 4px 0px;
},
.MuiList-root {
padding: 0;
},
`; `;

View File

@@ -0,0 +1,384 @@
import styled from "@emotion/styled";
import { IronCalcLogo } from "@ironcalc/workbook";
import { Avatar, Drawer, IconButton, Menu, MenuItem } from "@mui/material";
import {
EllipsisVertical,
FileDown,
FileSpreadsheet,
Plus,
Trash2,
} from "lucide-react";
import type React from "react";
import { useState } from "react";
import UserMenu from "../UserMenu";
interface LeftDrawerProps {
open: boolean;
onClose: () => void;
newModel: () => void;
setModel: (key: string) => void;
models: { [key: string]: string };
selectedUuid: string | null;
}
const LeftDrawer: React.FC<LeftDrawerProps> = ({
open,
onClose,
newModel,
setModel,
models,
selectedUuid,
}) => {
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [selectedWorkbookUuid, setSelectedWorkbookUuid] = useState<
string | null
>(null);
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState<null | HTMLElement>(
null,
);
const handleMenuOpen = (
event: React.MouseEvent<HTMLButtonElement>,
uuid: string,
) => {
console.log("Menu open", uuid);
event.stopPropagation();
setSelectedWorkbookUuid(uuid);
setMenuAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
setSelectedWorkbookUuid(null);
};
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchorEl(event.currentTarget);
};
const handleUserMenuClose = () => {
setUserMenuAnchorEl(null);
};
const elements = Object.keys(models)
.reverse()
.map((uuid) => {
const isMenuOpen = menuAnchorEl !== null && selectedWorkbookUuid === uuid;
return (
<WorkbookListItem
key={uuid}
onClick={() => {
setModel(uuid);
}}
selected={uuid === selectedUuid}
disableRipple
>
<StorageIndicator>
<FileSpreadsheet />
</StorageIndicator>
<WorkbookListText>{models[uuid]}</WorkbookListText>
<EllipsisButton
onClick={(e) => handleMenuOpen(e, uuid)}
disableRipple
isOpen={isMenuOpen}
>
<EllipsisVertical />
</EllipsisButton>
<StyledMenu
anchorEl={menuAnchorEl}
open={Boolean(menuAnchorEl)}
onClose={handleMenuClose}
MenuListProps={{
dense: true,
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
disablePortal
>
<MenuItemWrapper
onClick={() => {
handleMenuClose();
}}
disableRipple
>
<FileDown />
Download (.xlsx)
</MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper
selected={false}
onClick={() => {
handleMenuClose();
}}
disableRipple
>
<Trash2 size={16} />
Delete workbook
</MenuItemWrapper>
</StyledMenu>
</WorkbookListItem>
);
});
return (
<DrawerWrapper
variant="persistent"
anchor="left"
open={open}
onClose={onClose}
>
<DrawerHeader>
<StyledDesktopLogo />
<AddButton
onClick={() => {
newModel();
}}
>
<PlusIcon />
</AddButton>
</DrawerHeader>
<DrawerContent>
<DrawerContentTitle>Your workbooks</DrawerContentTitle>
{elements}
</DrawerContent>
<DrawerFooter>
<UserWrapper
disableRipple
onClick={handleUserMenuOpen}
selected={Boolean(userMenuAnchorEl)}
>
<StyledAvatar
alt="Nikola Tesla"
src="/path/to/avatar.jpg"
sx={{ bgcolor: "#f2994a", width: 24, height: 24 }}
/>
<Username>Nikola Tesla</Username>
</UserWrapper>
<UserMenu
anchorEl={userMenuAnchorEl}
onClose={handleUserMenuClose}
onPreferences={() => {
console.log("Preferences clicked");
handleUserMenuClose();
}}
onLogout={() => {
console.log("Logout clicked");
handleUserMenuClose();
}}
/>
</DrawerFooter>
</DrawerWrapper>
);
};
const DrawerWrapper = styled(Drawer)`
width: 264px;
height: 100%;
flex-shrink: 0;
font-family: "Inter", sans-serif;
.MuiDrawer-paper {
width: 264px;
background-color: #f5f5f5;
overflow: hidden;
border-right: 1px solid #e0e0e0;
}
`;
const DrawerHeader = styled("div")`
display: flex;
align-items: center;
padding: 12px 8px 12px 16px;
justify-content: space-between;
max-height: 60px;
min-height: 60px;
border-bottom: 1px solid #e0e0e0;
box-sizing: border-box;
`;
const StyledDesktopLogo = styled(IronCalcLogo)`
width: 120px;
height: 28px;
`;
const AddButton = styled(IconButton)`
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
height: 32px;
width: 32px;
border-radius: 4px;
margin-left: 10px;
color: #333333;
stroke-width: 2px;
&:hover {
background-color: #e0e0e0;
}
`;
const PlusIcon = styled(Plus)`
width: 16px;
height: 16px;
`;
const DrawerContent = styled("div")`
display: flex;
flex-direction: column;
gap: 4px;
padding: 16px 12px;
height: 100%;
overflow: scroll;
font-size: 12px;
`;
const DrawerContentTitle = styled("div")`
font-weight: 600;
color: #9e9e9e;
margin-bottom: 8px;
padding: 0px 8px;
`;
const StorageIndicator = styled("div")`
height: 16px;
width: 16px;
svg {
height: 16px;
width: 16px;
stroke: #9e9e9e;
}
`;
const EllipsisButton = styled(IconButton)<{ isOpen: boolean }>`
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
height: 24px;
width: 24px;
border-radius: 4px;
color: #333333;
stroke-width: 2px;
background-color: ${({ isOpen }) => (isOpen ? "#E0E0E0" : "none")};
opacity: ${({ isOpen }) => (isOpen ? "1" : "0.5")};
transition: opacity 0.3s, background-color 0.3s;
&:hover {
background: none;
opacity: 1;
}
&:active {
background: #bdbdbd;
opacity: 1;
}
`;
const WorkbookListItem = styled(MenuItem)<{ selected: boolean }>`
display: flex;
gap: 8px;
justify-content: flex-start;
font-size: 14px;
width: 100%;
min-width: 172px;
border-radius: 8px;
padding: 8px 4px 8px 8px;
height: 32px;
min-height: 32px;
transition: gap 0.5s;
background-color: ${({ selected }) =>
selected ? "#e0e0e0 !important" : "transparent"};
`;
const WorkbookListText = styled("div")`
color: #000;
font-size: 12px;
width: 100%;
max-width: 240px;
overflow: hidden;
text-overflow: ellipsis;
`;
const StyledMenu = styled(Menu)`
.MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.01);
},
.MuiList-root {
padding: 0;
},
`;
const MenuDivider = styled("div")`
width: 100%;
margin: auto;
margin-top: 4px;
margin-bottom: 4px;
border-top: 1px solid #eeeeee;
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
justify-content: flex-start;
font-size: 12px;
width: calc(100% - 8px);
min-width: 140px;
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
gap: 8px;
svg {
width: 16px;
height: 16px;
}
`;
const DrawerFooter = styled("div")`
display: none;
align-items: center;
padding: 12px;
justify-content: space-between;
max-height: 60px;
height: 60px;
border-top: 1px solid #e0e0e0;
box-sizing: border-box;
`;
const UserWrapper = styled(MenuItem)<{ selected: boolean }>`
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
padding: 8px;
border-radius: 8px;
max-width: 100%;
background-color: ${({ selected }) =>
selected ? "#e0e0e0 !important" : "transparent"};
`;
const StyledAvatar = styled(Avatar)`
font-size: 14px;
`;
const Username = styled("div")`
font-size: 12px;
flex-grow: 1;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
`;
export default LeftDrawer;

View File

@@ -0,0 +1,90 @@
import styled from "@emotion/styled";
import { Menu, MenuItem } from "@mui/material";
import { LogOut, Settings } from "lucide-react";
interface UserMenuProps {
anchorEl: null | HTMLElement;
onClose: () => void;
onPreferences: () => void;
onLogout: () => void;
}
const UserMenu: React.FC<UserMenuProps> = ({
anchorEl,
onClose,
onPreferences,
onLogout,
}) => {
return (
<StyledMenu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
MenuListProps={{
dense: true,
}}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<MenuItemWrapper onClick={onPreferences} sx={{ gap: 1, fontSize: 14 }}>
<Settings size={16} />
<MenuItemText>Preferences</MenuItemText>
</MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper onClick={onLogout} sx={{ gap: 1, fontSize: 14 }}>
<LogOut size={16} />
<MenuItemText>Log out</MenuItemText>
</MenuItemWrapper>
</StyledMenu>
);
};
const StyledMenu = styled(Menu)`
& .MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
margin-top: -4px;
margin-left: 4px;
}
& .MuiList-root {
padding: 0;
}
`;
const MenuItemText = styled("div")`
color: #000;
font-size: 12px;
flex-grow: 1;
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
justify-content: flex-start;
font-size: 14px;
width: calc(100% - 8px);
min-width: 172px;
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
svg {
width: 16px;
height: 16px;
}
`;
const MenuDivider = styled("div")`
width: 100%;
margin: auto;
margin-top: 4px;
margin-bottom: 4px;
border-top: 1px solid #eeeeee;
`;
export default UserMenu;

View File

@@ -1,89 +1,105 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { type ChangeEvent, useEffect, useRef, useState } from "react"; import {
type ChangeEvent,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
export function WorkbookTitle(props: { // This element has a in situ editable text
// We use a virtual element to compute the size of the input
export function WorkbookTitle(properties: {
name: string; name: string;
onNameChange: (name: string) => void; onNameChange: (name: string) => void;
maxWidth: number;
}) { }) {
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const [value, setValue] = useState(props.name); const [name, setName] = useState(properties.name);
const mirrorDivRef = useRef<HTMLDivElement>(null); const mirrorDivRef = useRef<HTMLDivElement>(null);
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value); setName(event.target.value);
if (mirrorDivRef.current) { if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth); setWidth(mirrorDivRef.current.scrollWidth);
} }
}; };
useEffect(() => { useEffect(() => {
setName(properties.name);
}, [properties.name]);
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to change the width with every value change
useLayoutEffect(() => {
if (mirrorDivRef.current) { if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth); setWidth(mirrorDivRef.current.scrollWidth);
} }
}, []); }, [name]);
useEffect(() => {
setValue(props.name);
}, [props.name]);
return ( return (
<div <Container
style={{ style={{
position: "absolute", width: Math.min(width, properties.maxWidth),
left: "50%",
textAlign: "center",
transform: "translateX(-50%)",
// height: "60px",
// lineHeight: "60px",
padding: "8px",
fontSize: "14px",
fontWeight: "700",
fontFamily: "Inter",
width,
}} }}
> >
<TitleWrapper <TitleInput
value={value} value={name}
rows={1} onInput={handleChange}
onChange={handleChange}
onBlur={(event) => { onBlur={(event) => {
props.onNameChange(event.target.value); properties.onNameChange(event.target.value);
}} }}
style={{ width: width }} onKeyDown={(event) => {
switch (event.key) {
case "Enter": {
// If we hit "Enter" finish editing
event.currentTarget.blur();
break;
}
case "Escape": {
// revert changes
setName(properties.name);
break;
}
}
}}
style={{ width: Math.min(width, properties.maxWidth) }}
spellCheck="false" spellCheck="false"
> />
{value} <MirrorDiv ref={mirrorDivRef}>{name}</MirrorDiv>
</TitleWrapper> </Container>
<div
ref={mirrorDivRef}
style={{
position: "absolute",
top: "-9999px",
left: "-9999px",
whiteSpace: "pre-wrap",
textWrap: "nowrap",
visibility: "hidden",
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
padding: "inherit",
border: "inherit",
}}
>
{value}
</div>
</div>
); );
} }
const TitleWrapper = styled("textarea")` const Container = styled("div")`
text-align: left;
padding: 6px 4px;
font-size: 14px;
font-weight: 600;
font-family: Inter;
`;
const MirrorDiv = styled("div")`
position: absolute;
top: -9999px;
left: -9999px;
white-space: pre-wrap;
text-wrap: nowrap;
visibility: hidden;
font-family: inherit;
font-size: inherit;
line-height: inherit;
padding: inherit;
border: inherit;
`;
const TitleInput = styled("input")`
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
height: 20px; height: 20px;
line-height: 20px; line-height: 20px;
border-radius: 4px; border-radius: 4px;
padding: inherit; padding: inherit;
overflow: hidden;
outline: none; outline: none;
resize: none; resize: none;
text-wrap: nowrap; text-wrap: nowrap;
@@ -92,13 +108,11 @@ const TitleWrapper = styled("textarea")`
background-color: #f2f2f2; background-color: #f2f2f2;
} }
&:focus { &:focus {
border: 1px solid grey; outline: 1px solid grey;
} }
font-weight: inherit; font-weight: inherit;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
max-width: 520px; overflow: ellipsis;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis;
`; `;

View File

@@ -925,7 +925,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.2.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -940,7 +940,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.2.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -956,7 +956,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_server" name = "ironcalc_server"
version = "0.1.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"rand", "rand",

View File

@@ -9,14 +9,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
for row in 1..100 { for row in 1..100 {
for column in 1..100 { for column in 1..100 {
let value = row * column; let value = row * column;
model.set_user_input(0, row, column, format!("{}", value))?; model.set_user_input(0, row, column, format!("{value}"))?;
} }
} }
// Adds a new sheet // Adds a new sheet
model.add_sheet("Calculation")?; model.add_sheet("Calculation")?;
// column 100 is CV // column 100 is CV
let last_column = number_to_column(100).ok_or("Invalid column number")?; let last_column = number_to_column(100).ok_or("Invalid column number")?;
let formula = format!("=SUM(Sheet1!A1:{}100)", last_column); let formula = format!("=SUM(Sheet1!A1:{last_column}100)");
model.set_user_input(1, 1, 1, formula)?; model.set_user_input(1, 1, 1, formula)?;
// evaluates // evaluates

View File

@@ -22,7 +22,7 @@ fn main() {
let file_name = &args[1]; let file_name = &args[1];
println!("Testing file: {file_name}"); println!("Testing file: {file_name}");
if let Err(message) = test_file(file_name) { if let Err(message) = test_file(file_name) {
println!("{}", message); println!("{message}");
panic!("Model was evaluated inconsistently with XLSX data.") panic!("Model was evaluated inconsistently with XLSX data.")
} }

View File

@@ -176,7 +176,7 @@ pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
diff.reason diff.reason
); );
} }
Err(format!("Models are different: {}", message)) Err(format!("Models are different: {message}"))
} }
} }
Err(r) => Err(format!("Models are different: {}", r.message)), Err(r) => Err(format!("Models are different: {}", r.message)),

View File

@@ -19,10 +19,9 @@ pub(crate) fn get_app_xml(_: &Workbook) -> String {
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \ <Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\ xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\
<Application>{}</Application>\ <Application>{APPLICATION}</Application>\
<AppVersion>{}</AppVersion>\ <AppVersion>{APP_VERSION}</AppVersion>\
</Properties>", </Properties>"
APPLICATION, APP_VERSION
) )
} }
@@ -38,12 +37,7 @@ pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<Str
let seconds = milliseconds / 1000; let seconds = milliseconds / 1000;
let dt = match DateTime::from_timestamp(seconds, 0) { let dt = match DateTime::from_timestamp(seconds, 0) {
Some(s) => s, Some(s) => s,
None => { None => return Err(XlsxError::Xml(format!("Invalid timestamp: {milliseconds}"))),
return Err(XlsxError::Xml(format!(
"Invalid timestamp: {}",
milliseconds
)))
}
}; };
let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
Ok(format!( Ok(format!(
@@ -54,16 +48,15 @@ pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<Str
xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \ xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \ xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \
<dc:title></dc:title><dc:subject></dc:subject>\ <dc:title></dc:title><dc:subject></dc:subject>\
<dc:creator>{}</dc:creator>\ <dc:creator>{creator}</dc:creator>\
<cp:keywords></cp:keywords>\ <cp:keywords></cp:keywords>\
<dc:description></dc:description>\ <dc:description></dc:description>\
<cp:lastModifiedBy>{}</cp:lastModifiedBy>\ <cp:lastModifiedBy>{last_modified_by}</cp:lastModifiedBy>\
<cp:revision></cp:revision>\ <cp:revision></cp:revision>\
<dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>\ <dcterms:created xsi:type=\"dcterms:W3CDTF\">{created}</dcterms:created>\
<dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>\ <dcterms:modified xsi:type=\"dcterms:W3CDTF\">{last_modified}</dcterms:modified>\
<cp:category></cp:category>\ <cp:category></cp:category>\
<cp:contentStatus></cp:contentStatus>\ <cp:contentStatus></cp:contentStatus>\
</cp:coreProperties>", </cp:coreProperties>"
creator, last_modified_by, created, last_modified
)) ))
} }

View File

@@ -59,7 +59,7 @@ fn get_content_types_xml(workbook: &Workbook) -> String {
pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> { pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
let file_path = std::path::Path::new(&file_name); let file_path = std::path::Path::new(&file_name);
if file_path.exists() { if file_path.exists() {
return Err(XlsxError::IO(format!("file {} already exists", file_name))); return Err(XlsxError::IO(format!("file {file_name} already exists")));
} }
let file = fs::File::create(file_path).unwrap(); let file = fs::File::create(file_path).unwrap();
let writer = BufWriter::new(file); let writer = BufWriter::new(file);
@@ -140,7 +140,7 @@ pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<
pub fn save_to_icalc(model: &Model, file_name: &str) -> Result<(), XlsxError> { pub fn save_to_icalc(model: &Model, file_name: &str) -> Result<(), XlsxError> {
let file_path = std::path::Path::new(&file_name); let file_path = std::path::Path::new(&file_name);
if file_path.exists() { if file_path.exists() {
return Err(XlsxError::IO(format!("file {} already exists", file_name))); return Err(XlsxError::IO(format!("file {file_name} already exists")));
} }
let s = bitcode::encode(&model.workbook); let s = bitcode::encode(&model.workbook);
let mut file = fs::File::create(file_path)?; let mut file = fs::File::create(file_path)?;

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