Compare commits

...

28 Commits

Author SHA1 Message Date
Nicolás Hatcher
945897a455 WIP 2025-04-23 11:43:36 +02:00
Nicolás Hatcher
6f577575c7 UPDATE: Dynamic arrays! 2025-04-17 05:24:26 +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
Nicolás Hatcher
5c13f241c6 FIX: Fixes for the CI builds 2025-02-28 12:00:54 +01:00
Nicolás Hatcher
26b20eea43 UPDATE: Bump versions to 0.5 2025-02-28 01:00:50 +01:00
Nicolás Hatcher
b62256963a UPDATE: Adds wrapping! 2025-02-28 00:29:44 +01:00
Nicolás Hatcher
4f627b4363 FIX: More sensible decrease/increase font-size 2025-02-28 00:29:44 +01:00
93 changed files with 5398 additions and 2308 deletions

View File

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

10
Cargo.lock generated
View File

@@ -414,7 +414,7 @@ dependencies = [
[[package]]
name = "ironcalc"
version = "0.3.0"
version = "0.5.0"
dependencies = [
"bitcode",
"chrono",
@@ -430,7 +430,7 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.3.0"
version = "0.5.0"
dependencies = [
"bitcode",
"chrono",
@@ -448,7 +448,7 @@ dependencies = [
[[package]]
name = "ironcalc_nodejs"
version = "0.3.1"
version = "0.5.0"
dependencies = [
"ironcalc",
"napi",
@@ -784,7 +784,7 @@ dependencies = [
[[package]]
name = "pyroncalc"
version = "0.3.0"
version = "0.5.0"
dependencies = [
"ironcalc",
"pyo3",
@@ -1070,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.3.2"
version = "0.5.0"
dependencies = [
"ironcalc_base",
"serde",

View File

@@ -77,7 +77,7 @@ And visit <http://0.0.0.0:8000/ironcalc/>
Add the dependency to `Cargo.toml`:
```toml
[dependencies]
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"}
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.5"}
```
And then use this code in `main.rs`:

View File

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

View File

@@ -1,5 +1,6 @@
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;
// 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.
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].0.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.
///
/// # Arguments
///
/// * `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();
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.
@@ -134,7 +164,7 @@ impl Model {
column,
delta: column_count,
}),
);
)?;
// In the list of columns:
// * Keep all the columns to the left
@@ -214,7 +244,7 @@ impl Model {
column,
delta: -column_count,
}),
);
)?;
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
// deletes all the column styles
@@ -338,7 +368,7 @@ impl Model {
row,
delta: row_count,
}),
);
)?;
Ok(())
}
@@ -399,7 +429,7 @@ impl Model {
row,
delta: -row_count,
}),
);
)?;
Ok(())
}
@@ -420,14 +450,14 @@ impl Model {
sheet: u32,
column: i32,
delta: i32,
) -> Result<(), &'static str> {
) -> Result<(), String> {
// Check boundaries
let target_column = column + delta;
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) {
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
@@ -439,7 +469,7 @@ impl Model {
column,
delta,
}),
);
)?;
Ok(())
}

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

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

View File

@@ -1,11 +1,85 @@
use crate::{
calc_result::{CalcResult, Range},
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
implicit_intersection::implicit_intersection,
expressions::{
parser::{ArrayNode, Node},
token::Error,
types::CellReferenceIndex,
},
model::Model,
};
pub(crate) enum NumberOrArray {
Number(f64),
Array(Vec<Vec<ArrayNode>>),
}
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(
&mut self,
node: &Node,
@@ -39,19 +113,16 @@ impl Model {
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
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,
message: "Invalid reference (number)".to_string(),
}),
}
}
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
}
}
@@ -99,19 +170,16 @@ impl Model {
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
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,
message: "Invalid reference (string)".to_string(),
}),
}
}
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
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::EmptyCell | CalcResult::EmptyArg => Ok(false),
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { left, right } => {
match implicit_intersection(&cell, &Range { left, right }) {
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,
message: "Invalid reference (bool)".to_string(),
}),
}
}
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
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

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

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

@@ -187,6 +187,7 @@ impl Lexer {
']' => TokenType::RightBracket,
':' => TokenType::Colon,
';' => TokenType::Semicolon,
'@' => TokenType::At,
',' => {
if self.locale.numbers.symbols.decimal == "," {
match self.consume_number(',') {

View File

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

View File

@@ -1,4 +1,5 @@
mod test_common;
mod test_implicit_intersection;
mod test_language;
mod test_locale;
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">
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
@@ -12,7 +12,8 @@ term => factor (opFactor factor)*
factor => prod (opProd prod)*
prod => power ('^' power)*
power => (unaryOp)* range '%'*
range => primary (':' primary)?
range => implicit (':' primary)?
implicit=> '@' primary | primary
primary => '(' expr ')'
=> number
=> function '(' f_args ')'
@@ -45,8 +46,8 @@ use super::utils::number_to_column;
use token::OpCompare;
pub mod move_formula;
pub mod static_analysis;
pub mod stringify;
pub mod walk;
#[cfg(test)]
mod tests;
@@ -81,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
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> {
sheet_name: &'a Option<String>,
sheet_index: u32,
@@ -90,6 +94,14 @@ pub(crate) struct Reference<'a> {
column: i32,
}
#[derive(PartialEq, Clone, Debug)]
pub enum ArrayNode {
Boolean(bool),
Number(f64),
String(String),
Error(token::Error),
}
#[derive(PartialEq, Clone, Debug)]
pub enum Node {
BooleanKind(bool),
@@ -163,10 +175,14 @@ pub enum Node {
name: String,
args: Vec<Node>,
},
ArrayKind(Vec<Node>),
DefinedNameKind((String, Option<u32>)),
ArrayKind(Vec<Vec<ArrayNode>>),
DefinedNameKind(DefinedNameS),
TableNameKind(String),
WrongVariableKind(String),
ImplicitIntersection {
automatic: bool,
child: Box<Node>,
},
CompareKind {
kind: OpCompare,
left: Box<Node>,
@@ -189,7 +205,7 @@ pub enum Node {
pub struct Parser {
lexer: lexer::Lexer,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
context: CellReferenceRC,
tables: HashMap<String, Table>,
}
@@ -197,7 +213,7 @@ pub struct Parser {
impl Parser {
pub fn new(
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new(
@@ -228,7 +244,7 @@ impl Parser {
pub fn set_worksheets_and_names(
&mut self,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
) {
self.worksheets = worksheets;
self.defined_names = defined_names;
@@ -252,17 +268,17 @@ impl Parser {
// Returns:
// * 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
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> {
for (df_name, df_scope) in &self.defined_names {
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
for (df_name, df_scope, df_formula) in &self.defined_names {
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() {
return Some(None);
return Some((None, df_formula.to_owned()));
}
}
None
@@ -411,7 +427,7 @@ impl Parser {
}
fn parse_range(&mut self) -> Node {
let t = self.parse_primary();
let t = self.parse_implicit();
if let Node::ParseErrorKind { .. } = t {
return t;
}
@@ -430,6 +446,65 @@ impl Parser {
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 {
let next_token = self.lexer.next_token();
match next_token {
@@ -451,21 +526,35 @@ impl Parser {
TokenType::Number(s) => Node::NumberKind(s),
TokenType::String(s) => Node::StringKind(s),
TokenType::LeftBrace => {
let t = self.parse_expr();
if let Node::ParseErrorKind { .. } = t {
return t;
}
// It's an array. It's a collection of rows all of the same dimension
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 args: Vec<Node> = vec![t];
while next_token == TokenType::Semicolon {
self.lexer.advance_token();
let p = self.parse_expr();
if let Node::ParseErrorKind { .. } = p {
return p;
}
let row = match self.parse_array_row() {
Ok(s) => s,
Err(error) => return error,
};
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) {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
@@ -473,7 +562,7 @@ impl Parser {
message: err.message,
};
}
Node::ArrayKind(args)
Node::ArrayKind(matrix)
}
TokenType::Reference {
sheet,
@@ -604,6 +693,20 @@ impl Parser {
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 };
}
let context = &self.context;
@@ -620,8 +723,8 @@ impl Parser {
};
// Could be a defined name or a table
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope));
if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope, formula));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
@@ -706,6 +809,14 @@ impl Parser {
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::RightBracket
| TokenType::Colon

View File

@@ -1,6 +1,6 @@
use super::{
stringify::{stringify_reference, DisplaceData},
Node, Reference,
ArrayNode, Node, Reference,
};
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
@@ -56,6 +56,15 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
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 {
use self::Node::*;
match node {
@@ -362,20 +371,41 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
move_function(name, args, move_context)
}
ArrayKind(args) => {
// This code is a placeholder. Arrays are not yet implemented
let mut first = true;
let mut arguments = "".to_string();
for el in args {
if !first {
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
let mut first_row = true;
let mut matrix_string = String::new();
// Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
for row in args {
if !first_row {
matrix_string.push(',');
} else {
first = false;
arguments = to_string_moved(el, move_context);
first_row = false;
}
// 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;
}
// Reuse your existing element-stringification function
row_string.push_str(&to_string_array_node(el));
}
// Enclose the row in braces
matrix_string.push('{');
matrix_string.push_str(&row_string);
matrix_string.push('}');
}
format!("{{{}}}", arguments)
// Enclose the whole matrix in braces
format!("{{{}}}", matrix_string)
}
DefinedNameKind((name, _)) => name.to_string(),
DefinedNameKind((name, ..)) => name.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!(
@@ -395,5 +425,11 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
position: _,
} => formula.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 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
pub(crate) 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 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::types::CellReferenceRC, number_format::to_excel_precision_str};
@@ -34,10 +36,21 @@ pub enum DisplaceData {
None,
}
/// This is the internal mode in IronCalc
pub fn to_rc_format(node: &Node) -> String {
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(
node: &Node,
context: &CellReferenceRC,
@@ -46,18 +59,10 @@ pub fn to_string_displaced(
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.
/// 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_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(
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
@@ -235,7 +240,7 @@ fn format_function(
args: &Vec<Node>,
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
use_original_name: bool,
export_to_excel: bool,
) -> String {
let mut first = true;
let mut arguments = "".to_string();
@@ -244,21 +249,46 @@ fn format_function(
arguments = format!(
"{},{}",
arguments,
stringify(el, context, displace_data, use_original_name)
stringify(el, context, displace_data, export_to_excel)
);
} else {
first = false;
arguments = stringify(el, context, displace_data, use_original_name);
arguments = stringify(el, context, displace_data, export_to_excel);
}
}
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(
node: &Node,
context: Option<&CellReferenceRC>,
displace_data: &DisplaceData,
use_original_name: bool,
export_to_excel: bool,
) -> String {
use self::Node::*;
match node {
@@ -407,52 +437,52 @@ fn stringify(
}
OpRangeKind { left, right } => format!(
"{}:{}",
stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, export_to_excel)
),
OpConcatenateKind { left, right } => format!(
"{}&{}",
stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel),
stringify(right, context, displace_data, export_to_excel)
),
CompareKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, use_original_name),
stringify(left, context, displace_data, export_to_excel),
kind,
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
OpSumKind { kind, left, right } => format!(
"{}{}{}",
stringify(left, context, displace_data, use_original_name),
stringify(left, context, displace_data, export_to_excel),
kind,
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
OpProductKind { kind, left, right } => {
let x = match **left {
OpSumKind { .. } => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel)
),
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 {
OpSumKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
CompareKind { .. } => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
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)
}
@@ -467,9 +497,7 @@ fn stringify(
| DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name)
}
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
@@ -482,9 +510,10 @@ fn stringify(
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
stringify(left, context, displace_data, export_to_excel)
),
};
let y = match **right {
@@ -498,7 +527,7 @@ fn stringify(
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
}
OpRangeKind { .. }
| OpConcatenateKind { .. }
@@ -512,55 +541,63 @@ fn stringify(
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| ImplicitIntersection { .. }
| EmptyArgKind => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
),
};
format!("{}^{}", x, y)
}
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 } => {
let name = if use_original_name {
let name = if export_to_excel {
kind.to_xlsx_string()
} else {
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) => {
let mut first = true;
let mut arguments = "".to_string();
for el in args {
if !first {
arguments = format!(
"{},{}",
arguments,
stringify(el, context, displace_data, use_original_name)
);
let mut first_row = true;
let mut matrix_string = String::new();
for row in args {
if !first_row {
matrix_string.push(';');
} else {
first = false;
arguments = stringify(el, context, displace_data, use_original_name);
first_row = false;
}
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;
}
row_string.push_str(&to_string_array_node(el));
}
matrix_string.push_str(&row_string);
}
format!("{{{}}}", arguments)
format!("{{{}}}", matrix_string)
}
TableNameKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(),
DefinedNameKind((name, ..)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind {
OpUnary::Minus => {
format!(
"-{}",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
)
}
OpUnary::Percentage => {
format!(
"{}%",
stringify(right, context, displace_data, use_original_name)
stringify(right, context, displace_data, export_to_excel)
)
}
},
@@ -571,6 +608,29 @@ fn stringify(
message: _,
} => formula.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 } => {
rename_sheet_in_node(right, sheet_index, new_name);
}
Node::ImplicitIntersection {
automatic: _,
child,
} => {
rename_sheet_in_node(child, sheet_index, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}
@@ -681,7 +747,7 @@ pub(crate) fn rename_defined_name_in_node(
) {
match node {
// Rename
Node::DefinedNameKind((n, s)) => {
Node::DefinedNameKind((n, s, _)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string();
}
@@ -736,6 +802,12 @@ pub(crate) fn rename_defined_name_in_node(
Node::UnaryKind { kind: _, right } => {
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
Node::BooleanKind(_) => {}

View File

@@ -1,4 +1,7 @@
mod test_add_implicit_intersection;
mod test_arrays;
mod test_general;
mod test_implicit_intersection;
mod test_issue_155;
mod test_move_formula;
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,
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(
&node,
&MoveContext {
@@ -400,7 +400,7 @@ fn test_move_formula_misc() {
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 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)"
);
}
#[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 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::types::CellReferenceRC;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
fn create_test_table(
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!",
"#CALC!", "#CIRC!", "#NULL!",
];
names.iter().any(|e| *e == name)
names.contains(&name)
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -240,6 +240,7 @@ pub enum TokenType {
Bang, // !
Percent, // %
And, // &
At, // @
Reference {
sheet: Option<String>,
row: i32,

View File

@@ -178,10 +178,7 @@ impl Lexer {
}
}
self.position = position;
match chars.parse::<f64>() {
Err(_) => None,
Ok(v) => Some(v),
}
chars.parse::<f64>().ok()
}
fn consume_condition(&mut self) -> Option<(Compare, f64)> {

View File

@@ -235,6 +235,11 @@ impl Model {
// This cannot happen
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 {
@@ -249,7 +254,7 @@ impl Model {
// The arg could be a defined name or a table
// let = &args[0];
match &args[0] {
Node::DefinedNameKind((name, scope)) => {
Node::DefinedNameKind((name, scope, _)) => {
// Let's see if it is a defined name
if let Some(defined_name) = self
.parsed_defined_names

View File

@@ -161,6 +161,13 @@ impl Model {
CalcResult::Range { .. }
| CalcResult::String { .. }
| 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)
@@ -185,6 +192,13 @@ impl Model {
}
// References to empty cells are ignored. If all args are ignored the result is #VALUE!
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)

View File

@@ -855,7 +855,7 @@ impl Model {
if left.row != right.row || left.column != right.column {
// FIXME: Implicit intersection or dynamic arrays
return CalcResult::Error {
error: Error::ERROR,
error: Error::NIMPL,
origin: cell,
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::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::single_number_fn;
use crate::{
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,
_ => {
// We ignore booleans and strings
@@ -354,187 +378,29 @@ impl Model {
}
}
pub(crate) fn fn_sin(&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.sin();
CalcResult::Number(result)
}
pub(crate) fn fn_cos(&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.cos();
CalcResult::Number(result)
}
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)
}
single_number_fn!(fn_sin, |f| Ok(f64::sin(f)));
single_number_fn!(fn_cos, |f| Ok(f64::cos(f)));
single_number_fn!(fn_tan, |f| Ok(f64::tan(f)));
single_number_fn!(fn_sinh, |f| Ok(f64::sinh(f)));
single_number_fn!(fn_cosh, |f| Ok(f64::cosh(f)));
single_number_fn!(fn_tanh, |f| Ok(f64::tanh(f)));
single_number_fn!(fn_asin, |f| Ok(f64::asin(f)));
single_number_fn!(fn_acos, |f| Ok(f64::acos(f)));
single_number_fn!(fn_atan, |f| Ok(f64::atan(f)));
single_number_fn!(fn_asinh, |f| Ok(f64::asinh(f)));
single_number_fn!(fn_acosh, |f| Ok(f64::acosh(f)));
single_number_fn!(fn_atanh, |f| Ok(f64::atanh(f)));
single_number_fn!(fn_abs, |f| Ok(f64::abs(f)));
single_number_fn!(fn_sqrt, |f| if f < 0.0 {
Err(Error::NUM)
} else {
Ok(f64::sqrt(f))
});
single_number_fn!(fn_sqrtpi, |f: f64| if f < 0.0 {
Err(Error::NUM)
} else {
Ok((f * PI).sqrt())
});
pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() {
@@ -543,53 +409,6 @@ impl Model {
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 {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);

View File

@@ -15,6 +15,7 @@ mod financial_util;
mod information;
mod logical;
mod lookup_and_reference;
mod macros;
mod mathematical;
mod statistical;
mod subtotal;

View File

@@ -134,6 +134,13 @@ impl Model {
);
}
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,
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 {

View File

@@ -96,7 +96,7 @@ impl Model {
match cell.get_formula() {
Some(f) => {
let node = &self.parsed_formulas[sheet_index as usize][f as usize];
let node = &self.parsed_formulas[sheet_index as usize][f as usize].0;
matches!(
node,
Node::FunctionKind {
@@ -182,6 +182,13 @@ impl Model {
}
}
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::Boolean(_)
| CalcResult::Error { .. } => counta += 1,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
}
}

View File

@@ -97,10 +97,24 @@ impl Model {
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
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)
@@ -125,6 +139,13 @@ impl Model {
};
}
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) {
Ok(s) => s,
@@ -280,6 +301,13 @@ impl Model {
};
}
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);
}
@@ -308,6 +336,13 @@ impl Model {
};
}
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());
}
@@ -336,6 +371,13 @@ impl Model {
};
}
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());
}
@@ -370,6 +412,13 @@ impl Model {
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() {
@@ -411,6 +460,13 @@ impl Model {
};
}
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());
}
@@ -441,6 +497,13 @@ impl Model {
};
}
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 {
match self.evaluate_node_in_context(&args[1], cell) {
@@ -471,6 +534,13 @@ impl Model {
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
} else {
1
@@ -509,6 +579,13 @@ impl Model {
};
}
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 {
match self.evaluate_node_in_context(&args[1], cell) {
@@ -539,6 +616,13 @@ impl Model {
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
} else {
1
@@ -577,6 +661,13 @@ impl Model {
};
}
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) {
CalcResult::Number(v) => {
@@ -641,6 +732,13 @@ impl Model {
};
}
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 count: usize = 0;
@@ -983,6 +1081,13 @@ impl Model {
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg | CalcResult::Range { .. } => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
}
}
}
@@ -1002,6 +1107,13 @@ impl Model {
}
}
CalcResult::EmptyArg => {}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
}
let result = values.join(&delimiter);
@@ -1125,6 +1237,11 @@ impl Model {
}
}
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

@@ -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)
Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
}
CalcResult::Range { left: _, right: _ } => {
// TODO: Implicit Intersection
Box::new(move |_x| false)
}
CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false),
CalcResult::Array(_) => Box::new(move |_x| false),
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
}
}

View File

@@ -141,9 +141,9 @@ impl Model {
/// * 1 - Perform a search starting at the first item. This is the default.
/// * -1 - Perform a reverse search starting at the last item.
/// * 2 - Perform a binary search that relies on lookup_array being sorted
/// in ascending order. If not sorted, invalid results will be returned.
/// in ascending order. If not sorted, invalid results will be returned.
/// * -2 - Perform a binary search that relies on lookup_array being sorted
/// in descending order. If not sorted, invalid results will be returned.
/// in descending order. If not sorted, invalid results will be returned.
pub(crate) fn fn_xlookup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() < 3 || args.len() > 6 {
return CalcResult::new_args_number_error(cell);

View File

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

View File

@@ -11,8 +11,9 @@ use crate::{
lexer::LexerMode,
parser::{
move_formula::{move_formula, MoveContext},
static_analysis::{run_static_analysis_on_node, StaticResult},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
Node, Parser,
ArrayNode, Node, Parser,
},
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*,
@@ -99,7 +100,7 @@ pub struct Model {
/// A Rust internal representation of an Excel workbook
pub workbook: Workbook,
/// A list of parsed formulas
pub parsed_formulas: Vec<Vec<Node>>,
pub parsed_formulas: Vec<Vec<(Node, StaticResult)>>,
/// A list of parsed defined names
pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
/// An optimization to lookup strings faster
@@ -207,6 +208,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),
}
}
@@ -256,27 +268,10 @@ impl Model {
) -> CalcResult {
use Node::*;
match node {
OpSumKind { kind, left, right } => {
// In the future once the feature try trait stabilizes we could use the '?' operator for this :)
// See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5
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)
}
OpSumKind { kind, left, right } => match kind {
OpSum::Add => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 + f2)),
OpSum::Minus => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 - f2)),
},
NumberKind(value) => CalcResult::Number(*value),
StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)),
BooleanKind(value) => CalcResult::Boolean(*value),
@@ -364,59 +359,27 @@ impl Model {
let result = format!("{}{}", l, r);
CalcResult::String(result)
}
OpProductKind { kind, left, right } => {
let l = match self.get_number(left, cell) {
Ok(f) => f,
Err(s) => {
return s;
OpProductKind { kind, left, right } => match kind {
OpProduct::Times => {
self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 * f2))
}
OpProduct::Divide => self.handle_arithmetic(left, right, cell, &|f1, f2| {
if f2 == 0.0 {
Err(Error::DIV)
} else {
Ok(f1 / f2)
}
};
let r = match self.get_number(right, cell) {
Ok(f) => f,
Err(s) => {
return s;
}
};
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 } => {
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;
}
};
// Deal with errors properly
CalcResult::Number(l.powf(r))
self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1.powf(f2)))
}
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
InvalidFunctionKind { name, args: _ } => {
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
}
ArrayKind(_) => {
// TODO: NOT IMPLEMENTED
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
}
DefinedNameKind((name, scope)) => {
ArrayKind(s) => CalcResult::Array(s.to_owned()),
DefinedNameKind((name, scope, _)) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => {
@@ -528,6 +491,22 @@ impl Model {
format!("Error parsing {}: {}", formula, message),
),
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),
},
}
}
@@ -544,14 +523,195 @@ impl Model {
}
Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row))
}
/// Sets sheet, target_row, target_column, (width, height), &v
#[allow(clippy::too_many_arguments)]
fn set_spill_cell_with_formula_value(
&mut self,
sheet: u32,
row: i32,
column: i32,
r: (i32, i32),
v: &CalcResult,
s: i32,
f: i32,
) -> Result<(), String> {
let new_cell = match v {
CalcResult::EmptyCell => Cell::DynamicCellFormulaNumber {
f,
v: 0.0,
s,
r,
a: false,
},
CalcResult::String(v) => Cell::DynamicCellFormulaString {
f,
v: v.clone(),
s,
r,
a: false,
},
CalcResult::Number(v) => Cell::DynamicCellFormulaNumber {
v: *v,
s,
r,
f,
a: false,
},
CalcResult::Boolean(b) => Cell::DynamicCellFormulaBoolean {
v: *b,
s,
r,
f,
a: false,
},
CalcResult::Error { error, .. } => Cell::DynamicCellFormulaError {
ei: error.clone(),
s,
r,
f,
a: false,
o: "".to_string(),
m: "".to_string(),
},
// These cannot happen
// FIXME: Maybe the type of get_cell_value should be different
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
Cell::DynamicCellFormulaError {
ei: Error::ERROR,
s,
r,
f,
a: false,
o: "".to_string(),
m: "".to_string(),
}
}
};
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
match sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
sheet_data.insert(row, column_data);
}
}
Ok(())
}
/// Sets a cell with a "spill" value
fn set_spill_cell_with_value(
&mut self,
sheet: u32,
row: i32,
column: i32,
m: (i32, i32),
v: &CalcResult,
) -> Result<(), String> {
let style_index = self.get_cell_style_index(sheet, row, column)?;
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
self.workbook
.styles
.get_style_without_quote_prefix(style_index)?
} else {
style_index
};
let new_cell = match v {
CalcResult::EmptyCell => Cell::SpillNumberCell {
v: 0.0,
s: style_index,
m,
},
CalcResult::String(s) => Cell::SpillStringCell {
v: s.clone(),
s: new_style_index,
m,
},
CalcResult::Number(f) => Cell::SpillNumberCell {
v: *f,
s: new_style_index,
m,
},
CalcResult::Boolean(b) => Cell::SpillBooleanCell {
v: *b,
s: new_style_index,
m,
},
CalcResult::Error { error, .. } => Cell::SpillErrorCell {
ei: error.clone(),
s: style_index,
m,
},
// These cannot happen
// FIXME: Maybe the type of get_cell_value should be different
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
Cell::SpillErrorCell {
ei: Error::ERROR,
s: style_index,
m,
}
}
};
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
match sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
sheet_data.insert(row, column_data);
}
}
Ok(())
}
/// Sets `result` in the cell given by `sheet` sheet index, row and column
/// Note that will panic if the cell does not exist
/// It will do nothing if the cell does not have a formula
#[allow(clippy::expect_used)]
fn set_cell_value(&mut self, cell_reference: CellReferenceIndex, result: &CalcResult) {
fn set_cell_value(
&mut self,
cell_reference: CellReferenceIndex,
result: &CalcResult,
) -> Result<(), String> {
let CellReferenceIndex { sheet, column, row } = cell_reference;
let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column];
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
let s = cell.get_style();
// If the cell is a dynamic cell we need to delete all the cells in the range
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
// skip the "mother" cell
if r == row && c == column {
continue;
}
self.cell_clear_contents(sheet, r, c)?;
}
}
}
if let Some(f) = cell.get_formula() {
match result {
CalcResult::Number(value) => {
@@ -616,16 +776,138 @@ impl Model {
ei: error.clone(),
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
}
CalcResult::Range { left, right } => {
let range = Range {
left: *left,
right: *right,
};
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
if left.sheet == right.sheet
&& left.row == right.row
&& left.column == right.column
{
let v = self.evaluate_cell(intersection_cell);
self.set_cell_value(cell_reference, &v);
// There is only one cell
let single_cell = CellReferenceIndex {
sheet: left.sheet,
column: left.column,
row: left.row,
};
let v = self.evaluate_cell(single_cell);
self.set_cell_value(cell_reference, &v)?;
} else {
// We need to check if all the cells are empty, otherwise we mark the cell as #SPILL!
let mut all_empty = true;
for r in row..=row + right.row - left.row {
for c in column..=column + right.column - left.column {
if r == row && c == column {
continue;
}
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
all_empty = false;
break;
}
}
}
if !all_empty {
let o = match self.cell_reference_to_string(&cell_reference) {
Ok(s) => s,
Err(_) => "".to_string(),
};
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::DynamicCellFormulaError {
f,
s,
o,
m: "Result would spill to non empty cells".to_string(),
ei: Error::SPILL,
r: (1, 1),
a: false,
};
return Ok(());
}
// evaluate all the cells in that range
for r in left.row..=right.row {
for c in left.column..=right.column {
let cell_reference = CellReferenceIndex {
sheet: left.sheet,
row: r,
column: c,
};
// FIXME: We ned to return an error
self.evaluate_cell(cell_reference);
}
}
// now write the result in the target
for r in left.row..=right.row {
let row_delta = r - left.row;
for c in left.column..=right.column {
let column_delta = c - left.column;
// We need to put whatever is in (left.sheet, r, c) in
// (sheet, row + row_delta, column + column_delta)
// But we need to preserve the style
let target_row = row + row_delta;
let target_column = column + column_delta;
let cell = self
.workbook
.worksheet(left.sheet)?
.cell(r, c)
.cloned()
.unwrap_or_default();
let cell_reference = CellReferenceIndex {
sheet: left.sheet,
row: r,
column: c,
};
let v = self.get_cell_value(&cell, cell_reference);
if row == target_row && column == target_column {
// let cell_reference = CellReferenceIndex { sheet, row, column };
// self.set_cell_value(cell_reference, &v);
self.set_spill_cell_with_formula_value(
sheet,
target_row,
target_column,
(right.column - left.column + 1, right.row - left.row + 1),
&v,
s,
f,
)?;
continue;
}
self.set_spill_cell_with_value(
sheet,
target_row,
target_column,
(row, column),
&v,
)?;
}
}
}
}
CalcResult::Array(array) => {
let width = array[0].len() as i32;
let height = array.len() as i32;
// First we check that we don't spill:
let mut all_empty = true;
for r in row..row + height {
for c in column..column + width {
if r == row && c == column {
continue;
}
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
all_empty = false;
break;
}
}
}
if !all_empty {
let o = match self.cell_reference_to_string(&cell_reference) {
Ok(s) => s,
Err(_) => "".to_string(),
@@ -639,21 +921,65 @@ impl Model {
f,
s,
o,
m: "Invalid reference".to_string(),
ei: Error::VALUE,
m: "Result would spill to non empty cells".to_string(),
ei: Error::SPILL,
};
return Ok(());
}
let mut target_row = row;
for data_row in array {
let mut target_column = column;
for value in data_row {
if row == target_row && column == target_column {
// This is the root cell of the dynamic array
let cell_reference = CellReferenceIndex { sheet, row, column };
let v = match value {
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
ArrayNode::Number(f) => CalcResult::Number(*f),
ArrayNode::String(s) => CalcResult::String(s.clone()),
ArrayNode::Error(error) => CalcResult::new_error(
error.clone(),
cell_reference,
error.to_localized_error_string(&self.language),
),
};
self.set_spill_cell_with_formula_value(
sheet,
target_row,
target_column,
(width, height),
&v,
s,
f,
)?;
target_column += 1;
continue;
}
let v = match value {
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
ArrayNode::Number(f) => CalcResult::Number(*f),
ArrayNode::String(s) => CalcResult::String(s.clone()),
ArrayNode::Error(error) => CalcResult::new_error(
error.clone(),
cell_reference,
error.to_localized_error_string(&self.language),
),
};
self.set_spill_cell_with_value(
sheet,
target_row,
target_column,
(row, column),
&v,
)?;
target_column += 1;
}
target_row += 1;
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
}
}
}
Ok(())
}
/// Sets the color of the sheet tab.
@@ -697,16 +1023,18 @@ impl Model {
Ok(())
}
// EmptyCell, Boolean, Number, String, Error
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
use Cell::*;
match cell {
EmptyCell { .. } => CalcResult::EmptyCell,
BooleanCell { v, .. } => CalcResult::Boolean(*v),
NumberCell { v, .. } => CalcResult::Number(*v),
ErrorCell { ei, .. } => {
BooleanCell { v, .. } | SpillBooleanCell { v, .. } => CalcResult::Boolean(*v),
NumberCell { v, .. } | SpillNumberCell { v, .. } => CalcResult::Number(*v),
ErrorCell { ei, .. } | SpillErrorCell { ei, .. } => {
let message = ei.to_localized_error_string(&self.language);
CalcResult::new_error(ei.clone(), cell_reference, message)
}
SpillStringCell { v, .. } => CalcResult::String(v.clone()),
SharedString { si, .. } => {
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
CalcResult::String(s.clone())
@@ -715,15 +1043,21 @@ impl Model {
CalcResult::new_error(Error::ERROR, cell_reference, message)
}
}
CellFormula { .. } => CalcResult::Error {
DynamicCellFormula { .. } | CellFormula { .. } => CalcResult::Error {
error: Error::ERROR,
origin: cell_reference,
message: "Unevaluated formula".to_string(),
},
CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v),
CellFormulaNumber { v, .. } => CalcResult::Number(*v),
CellFormulaString { v, .. } => CalcResult::String(v.clone()),
CellFormulaError { ei, o, m, .. } => {
DynamicCellFormulaBoolean { v, .. } | CellFormulaBoolean { v, .. } => {
CalcResult::Boolean(*v)
}
DynamicCellFormulaNumber { v, .. } | CellFormulaNumber { v, .. } => {
CalcResult::Number(*v)
}
DynamicCellFormulaString { v, .. } | CellFormulaString { v, .. } => {
CalcResult::String(v.clone())
}
DynamicCellFormulaError { ei, o, m, .. } | CellFormulaError { ei, o, m, .. } => {
if let Some(cell_reference) = self.parse_reference(o) {
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
} else {
@@ -793,9 +1127,10 @@ impl Model {
self.cells.insert(key, CellState::Evaluating);
}
}
let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone();
let result = self.evaluate_node_in_context(node, cell_reference);
self.set_cell_value(cell_reference, &result);
let (node, _static_result) =
&self.parsed_formulas[cell_reference.sheet as usize][f as usize];
let result = self.evaluate_node_in_context(&node.clone(), cell_reference);
let _ = self.set_cell_value(cell_reference, &result);
// mark cell as evaluated
self.cells.insert(key, CellState::Evaluated);
result
@@ -865,11 +1200,7 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
let defined_names = workbook.get_defined_names_with_scope();
// add all tables
// let mut tables = Vec::new();
// for worksheet in worksheets {
@@ -1087,7 +1418,7 @@ impl Model {
Some(cell) => match cell.get_formula() {
None => cell.get_text(&self.workbook.shared_strings, &self.language),
Some(i) => {
let formula = &self.parsed_formulas[sheet as usize][i as usize];
let formula = &self.parsed_formulas[sheet as usize][i as usize].0;
let cell_ref = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row: target_row,
@@ -1190,7 +1521,8 @@ impl Model {
.get(sheet as usize)
.ok_or("missing sheet")?
.get(formula_index as usize)
.ok_or("missing formula")?;
.ok_or("missing formula")?
.0;
let cell_ref = CellReferenceRC {
sheet: worksheet.get_name(),
row,
@@ -1424,6 +1756,25 @@ impl Model {
column: i32,
value: String,
) -> Result<(), String> {
// We need to check if the cell is part of a dynamic array
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
// If the cell is a dynamic cell we need to delete all the cells in the range
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
// skip the "mother" cell
if r == row && c == column {
continue;
}
self.cell_clear_contents(sheet, r, c)?;
}
}
}
// If value starts with "'" then we force the style to be quote_prefix
let style_index = self.get_cell_style_index(sheet, row, column)?;
if let Some(new_value) = value.strip_prefix('\'') {
@@ -1449,7 +1800,8 @@ impl Model {
self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?;
// Update the style if needed
let cell = CellReferenceIndex { sheet, row, column };
let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
let parsed_formula =
&self.parsed_formulas[sheet as usize][formula_index as usize].0;
if let Some(units) = self.compute_node_units(parsed_formula, &cell) {
let new_style_index = self
.workbook
@@ -1531,6 +1883,7 @@ impl Model {
_ => parsed_formula = new_parsed_formula,
}
}
let static_result = run_static_analysis_on_node(&parsed_formula);
let s = to_rc_format(&parsed_formula);
let mut formula_index: i32 = -1;
@@ -1539,7 +1892,7 @@ impl Model {
}
if formula_index == -1 {
shared_formulas.push(s);
self.parsed_formulas[sheet as usize].push(parsed_formula);
self.parsed_formulas[sheet as usize].push((parsed_formula, static_result));
formula_index = (shared_formulas.len() as i32) - 1;
}
worksheet.set_cell_with_formula(row, column, formula_index, style)?;
@@ -1734,7 +2087,7 @@ impl Model {
};
match cell.get_formula() {
Some(formula_index) => {
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize].0;
let cell_ref = CellReferenceRC {
sheet: worksheet.get_name(),
row,
@@ -1749,6 +2102,14 @@ impl Model {
/// Returns a list of all cells
pub fn get_all_cells(&self) -> Vec<CellIndex> {
let mut cells = Vec::new();
for (sheet, row, column) in &self.workbook.calc_chain {
let cell = CellIndex {
row: *row,
column: *column,
index: *sheet,
};
cells.push(cell);
}
for (index, sheet) in self.workbook.worksheets.iter().enumerate() {
let mut sorted_rows: Vec<_> = sheet.sheet_data.keys().collect();
sorted_rows.sort_unstable();
@@ -1775,6 +2136,8 @@ impl Model {
let cells = self.get_all_cells();
// First evaluate all dynamic arrays
for cell in cells {
self.evaluate_cell(CellReferenceIndex {
sheet: cell.index,
@@ -1805,9 +2168,22 @@ impl Model {
/// # }
/// ```
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
self.workbook
.worksheet_mut(sheet)?
.cell_clear_contents(row, column)?;
// If it has a spill formula we need to delete the contents of all the spilled cells
let worksheet = self.workbook.worksheet_mut(sheet)?;
if let Some(cell) = worksheet.cell(row, column) {
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
if row == r && column == c {
// we skip the root cell
continue;
}
worksheet.cell_clear_contents(r, c)?;
}
}
}
}
worksheet.cell_clear_contents(row, column)?;
Ok(())
}
@@ -1832,6 +2208,18 @@ impl Model {
/// # }
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
// Delete the contents of spilled cells if any
if let Some(cell) = worksheet.cell(row, column) {
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
if row == r && c == column {
worksheet.cell_clear_contents(r, c)?;
}
}
}
}
}
let sheet_data = &mut worksheet.sheet_data;
if let Some(row_data) = sheet_data.get_mut(&row) {

View File

@@ -8,7 +8,8 @@ use crate::{
expressions::{
lexer::LexerMode,
parser::{
stringify::{rename_sheet_in_node, to_rc_format},
static_analysis::run_static_analysis_on_node,
stringify::{rename_sheet_in_node, to_rc_format, to_string},
Parser,
},
types::CellReferenceRC,
@@ -17,7 +18,8 @@ use crate::{
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
WorksheetView,
},
utils::ParsedReference,
};
@@ -93,7 +95,8 @@ impl Model {
let mut parse_formula = Vec::new();
for formula in shared_formulas {
let t = self.parser.parse(formula, &cell_reference);
parse_formula.push(t);
let static_result = run_static_analysis_on_node(&t);
parse_formula.push((t, static_result));
}
self.parsed_formulas.push(parse_formula);
}
@@ -144,12 +147,7 @@ impl Model {
/// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) {
let defined_names = self
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
let defined_names = self.workbook.get_defined_names_with_scope();
self.parser
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
self.parsed_formulas = vec![];
@@ -243,7 +241,7 @@ impl Model {
/// Renames a sheet and updates all existing references to that sheet.
/// 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 is invalid
pub fn rename_sheet_by_index(
@@ -257,17 +255,15 @@ impl Model {
if self.get_sheet_index_by_name(new_name).is_some() {
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
// All internal formulas are 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 worksheets {
for worksheet in &mut self.workbook.worksheets {
// R1C1 formulas are not tied to a cell (but are tied to a cell)
let cell_reference = &CellReferenceRC {
sheet: worksheet.get_name(),
row: 1,
@@ -281,11 +277,32 @@ impl Model {
}
worksheet.shared_formulas = formulas;
}
// Se the mode back to A1
// Set the mode back to 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
let worksheets = &mut self.workbook.worksheets;
worksheets[sheet_index as usize].set_name(new_name);
self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
self.reset_parsed_structures();
Ok(())
}

View File

@@ -28,7 +28,6 @@ mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_fn_unicode;
mod test_forward_references;
mod test_frozen_rows_columns;
mod test_general;
mod test_math;
@@ -52,6 +51,8 @@ mod engineering;
mod test_fn_offset;
mod test_number_format;
mod test_arrays;
mod test_dynamic_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_fv;
@@ -59,6 +60,7 @@ mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_geomean;
mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155;
mod test_percentage;
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

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

View File

@@ -22,13 +22,14 @@ fn fn_concatenate() {
model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#);
// This will break once we implement the implicit intersection operator
// 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("B3", r#"=CONCAT(A1:A3, "!")"#);
model.evaluate();
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("C2"), *" my !");
}

View File

@@ -30,8 +30,18 @@ fn implicit_intersection() {
model._set("A2", "=FORMULATEXT(D1:E1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A1"), *"#N/IMPL!");
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]

View File

@@ -17,3 +17,19 @@ fn test_fn_sum_arguments() {
assert_eq!(model._get_text("A3"), *"1");
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_column() {
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"), "1".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

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

View File

@@ -423,3 +423,30 @@ fn change_scope_to_first_sheet() {
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

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

View File

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

View File

@@ -51,6 +51,9 @@ pub struct Workbook {
pub metadata: Metadata,
pub tables: HashMap<String, Table>,
pub views: HashMap<u32, WorkbookView>,
/// Calculation chain of the dynamic arrays.
/// List of tuples (sheet_id, row, column)
pub calc_chain: Vec<(u32, i32, i32)>,
}
/// A defined name. The `sheet_id` is the sheet index in case the name is local
@@ -62,8 +65,8 @@ pub struct DefinedName {
}
/// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum SheetState {
Visible,
@@ -159,17 +162,17 @@ pub enum CellType {
CompoundData = 128,
}
/// Cell types
/// s is always the style index of the cell
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub enum Cell {
EmptyCell {
s: i32,
},
BooleanCell {
v: bool,
s: i32,
},
NumberCell {
v: f64,
s: i32,
@@ -181,6 +184,7 @@ pub enum Cell {
},
// Always a shared string
SharedString {
// string index
si: i32,
s: i32,
},
@@ -189,13 +193,11 @@ pub enum Cell {
f: i32,
s: i32,
},
CellFormulaBoolean {
f: i32,
v: bool,
s: i32,
},
CellFormulaNumber {
f: i32,
v: f64,
@@ -207,9 +209,9 @@ pub enum Cell {
v: String,
s: i32,
},
CellFormulaError {
f: i32,
// error index
ei: Error,
s: i32,
// Origin: Sheet3!C4
@@ -217,7 +219,81 @@ pub enum Cell {
// Error Message: "Not implemented function"
m: String,
},
// TODO: Array formulas
// All Spill/dynamic cells have a boolean, a for array, if true it is an array formula
// Spill cells point to a mother cell (row, column)
SpillNumberCell {
v: f64,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
SpillBooleanCell {
v: bool,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
SpillErrorCell {
ei: Error,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
SpillStringCell {
v: String,
s: i32,
// mother cell (row, column)
m: (i32, i32),
},
// Dynamic cell formulas have a range (width, height)
DynamicCellFormula {
f: i32,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaBoolean {
f: i32,
v: bool,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaNumber {
f: i32,
v: f64,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaString {
f: i32,
v: String,
s: i32,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
DynamicCellFormulaError {
f: i32,
ei: Error,
s: i32,
// Cell origin of the error
o: String,
// Error message in text
m: String,
// range of the formula (width, height)
r: (i32, i32),
// true if the formula is a CSE formula
a: bool,
},
}
impl Default for Cell {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
use std::vec::Vec;
use crate::types::*;
use crate::{expressions::parser::DefinedNameS, types::*};
impl Workbook {
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
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 defined_names = self

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@ironcalc/nodejs",
"version": "0.3.1",
"version": "0.5.1",
"main": "index.js",
"types": "index.d.ts",
"napi": {

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ endif
all:
wasm-pack build --target web --scope ironcalc --release
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
rm -f types.js

View File

@@ -201,6 +201,18 @@ defined_name_list_types = r"""
getDefinedNameList(): DefinedName[];
"""
cell_structure = r"""
* @returns {any}
*/
getCellArrayStructure(sheet: number, row: number, column: number): any;
"""
cell_structure_types = r"""
* @returns {CellArrayStructure}
*/
getCellArrayStructure(sheet: number, row: number, column: number): CellArrayStructure;
"""
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
@@ -215,6 +227,7 @@ def fix_types(text):
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
text = text.replace(cell_structure, cell_structure_types)
with open("types.ts") as f:
types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str)

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -262,6 +262,8 @@ const BorderPicker = (properties: BorderPickerProps) => {
</BorderPickerDialog>
<ColorPicker
color={borderColor}
defaultColor="#000000"
title={t("color_picker.default")}
onChange={(color): void => {
setBorderColor(color);
setColorPickerOpen(false);
@@ -359,7 +361,7 @@ const MediumLine = styled("div")`
`;
const ThickLine = styled("div")`
width: 68px;
border-top: 1px solid ${theme.palette.grey["900"]};
border-top: 3px solid ${theme.palette.grey["900"]};
`;
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,185 +1,275 @@
import styled from "@emotion/styled";
import Popover, { type PopoverOrigin } from "@mui/material/Popover";
import { Check } from "lucide-react";
import type React from "react";
import { Menu, MenuItem, type PopoverOrigin } from "@mui/material";
import { Plus } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import AdvancedColorPicker from "./AdvancedColorPicker";
type ColorPickerProps = {
color: string;
defaultColor: string;
title: string;
onChange: (color: string) => void;
onClose: () => void;
anchorEl: React.RefObject<HTMLElement | null>;
anchorOrigin?: PopoverOrigin;
transformOrigin?: PopoverOrigin;
anchorOrigin: PopoverOrigin;
transformOrigin: PopoverOrigin;
open: boolean;
};
const colorPickerWidth = 240;
const colorfulHeight = 240;
const ColorPicker = (properties: ColorPickerProps) => {
const [color, setColor] = useState<string>(properties.color);
const ColorPicker = ({
color,
defaultColor,
title,
onChange,
onClose,
anchorEl,
anchorOrigin,
transformOrigin,
open,
}: ColorPickerProps) => {
const [selectedColor, setSelectedColor] = useState<string>(color);
const [isPickerOpen, setPickerOpen] = useState(false);
const recentColors = useRef<string[]>([]);
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(() => {
setColor(properties.color);
}, [properties.color]);
setSelectedColor(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",
"#272525",
"#1B717E",
"#59B9BC",
"#3BB68A",
"#8CB354",
"#F8CD3C",
"#F2994A",
"#EC5753",
"#D03627",
"#523E93",
"#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 (
<AdvancedColorPicker
color={selectedColor}
onAccept={handleColorSelect}
onCancel={() => setPickerOpen(false)}
anchorEl={anchorEl}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
open={true}
/>
);
}
return (
<Popover
open={properties.open}
<StyledMenu
anchorEl={anchorEl.current}
open={true}
onClose={handleClose}
anchorEl={properties.anchorEl.current}
anchorOrigin={
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
}
transformOrigin={
properties.transformOrigin || { vertical: "top", horizontal: "left" }
}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
>
<ColorPickerDialog>
<HexColorPicker
color={color}
onChange={(newColor): void => {
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 />
<MenuItemWrapper onClick={() => handleColorSelect(defaultColor)}>
<MenuItemSquare style={{ backgroundColor: defaultColor }} />
<MenuItemText>{title}</MenuItemText>
</MenuItemWrapper>
<HorizontalDivider />
<ColorsWrapper>
<ColorList>
{presetColors.map((presetColor) => (
<RecentColorButton
{mainColors.map((presetColor) => (
<ColorSwatch
key={presetColor}
$color={presetColor}
onClick={(): void => {
setColor(presetColor);
setSelectedColor(presetColor);
handleColorSelect(presetColor);
}}
/>
))}
</ColorList>
{recentColors.current.length > 0 ? (
<>
<HorizontalDivider />
<RecentLabel>{"Recent"}</RecentLabel>
<ColorList>
{recentColors.current.map((recentColor) => (
<RecentColorButton
key={recentColor}
$color={recentColor}
<ColorGrid>
{toneArrays.map((tones) => (
<ColorGridCol key={tones.join("-")}>
{tones.map((presetColor) => (
<ColorSwatch
key={presetColor}
$color={presetColor}
onClick={(): void => {
setColor(recentColor);
setSelectedColor(presetColor);
handleColorSelect(presetColor);
}}
/>
))}
</ColorList>
</ColorGridCol>
))}
</ColorGrid>
</ColorsWrapper>
<HorizontalDivider />
<RecentLabel>{t("color_picker.custom")}</RecentLabel>
<RecentColorsList>
{recentColors.current.length > 0 ? (
<>
{recentColors.current.map((recentColor) => (
<ColorSwatch
key={recentColor}
$color={recentColor}
onClick={(): void => {
setSelectedColor(recentColor);
handleColorSelect(recentColor);
}}
/>
))}
</>
) : (
<div />
<EmptyContainer />
)}
<Buttons>
<StyledButton
onClick={(): void => {
closePicker(color);
}}
>
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
{t("color_picker.apply")}
</StyledButton>
</Buttons>
</ColorPickerDialog>
</Popover>
<StyledPlusButton
onClick={() => setPickerOpen(true)}
title={t("color_picker.add")}
>
<Plus />
</StyledPlusButton>
</RecentColorsList>
</StyledMenu>
);
};
const Buttons = styled.div`
display: flex;
justify-content: flex-end;
margin: 8px;
`;
const StyledButton = styled("div")`
cursor: pointer;
color: #ffffff;
background: #f2994a;
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;
// Styled Components
const StyledMenu = styled(Menu)`
& .MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
margin-left: -4px;
max-width: 220px;
}
& .MuiList-root {
padding: 0;
}
`;
const RecentLabel = styled.div`
font-family: "Inter";
const MenuItemWrapper = styled(MenuItem)`
display: flex;
flex-direction: row;
justify-content: flex-start;
font-size: 12px;
font-family: Inter;
margin: 8px 8px 0px 8px;
color: ${theme.palette.text.secondary};
gap: 8px;
width: calc(100% - 8px);
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`
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 8px;
margin: 8px 8px 0px 8px;
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;
height: 16px;
${({ $color }): string => {
@@ -189,7 +279,7 @@ const RecentColorButton = styled.button<{ $color: string }>`
return `border: 1px solid ${$color};`;
}}
background-color: ${({ $color }): string => {
return $color;
return $color === "transparent" ? "none" : $color;
}};
box-sizing: border-box;
margin-top: 0px;
@@ -207,111 +297,50 @@ const HorizontalDivider = styled.div`
border-top: 1px solid ${theme.palette.grey["200"]};
`;
const ColorPickerDialog = styled.div`
background: ${theme.palette.background.default};
width: ${colorPickerWidth}px;
padding: 0px;
display: flex;
flex-direction: column;
& .react-colorful {
height: ${colorfulHeight}px;
width: ${colorPickerWidth}px;
}
& .react-colorful__saturation {
border-bottom: none;
border-radius: 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;
border-bottom: 1px solid #eee;
}
`;
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;
const RecentLabel = styled.div`
font-family: "Inter";
font-size: 12px;
display: inline-flex;
font-family: ${theme.typography.button.fontFamily};
font-family: Inter;
margin: 8px 12px 0px 12px;
color: ${theme.palette.text.secondary};
`;
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 {
border: 1px solid ${theme.palette.grey["600"]};
const RecentColorsList = styled.div`
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding: 8px;
margin: 0px 4px;
justify-content: flex-start;
gap: 4px;
`;
const StyledPlusButton = styled("button")`
display: flex;
justify-content: center;
flex-wrap: wrap;
align-items: center;
border: none;
background: none;
font-size: 12px;
height: 16px;
width: 16px;
margin: 0;
padding: 0;
border-radius: 4px;
svg {
width: 16px;
height: 16px;
}
&:focus-within {
outline: 2px solid ${theme.palette.secondary.main};
&:hover {
cursor: pointer;
outline: 1px solid ${theme.palette.grey["300"]};
outline-offset: 1px;
}
`;
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};
width: 28px;
height: 28px;
border-radius: 5px;
`;
const ColorPickerInput = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin: 8px;
gap: 8px;
const EmptyContainer = styled.div`
display: none;
`;
export default ColorPicker;

View File

@@ -13,6 +13,7 @@ import type { WorkbookState } from "../workbookState";
type FormulaBarProps = {
cellAddress: string;
formulaValue: string;
isPartOfArray: boolean;
model: Model;
workbookState: WorkbookState;
onChange: () => void;
@@ -23,6 +24,7 @@ function FormulaBar(properties: FormulaBarProps) {
const {
cellAddress,
formulaValue,
isPartOfArray,
model,
onChange,
onTextUpdated,
@@ -62,6 +64,9 @@ function FormulaBar(properties: FormulaBarProps) {
event.stopPropagation();
event.preventDefault();
}}
sx={{
color: isPartOfArray ? "grey" : "black",
}}
>
<Editor
originalText={formulaValue}

View File

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

View File

@@ -7,8 +7,6 @@ import type {
import { styled } from "@mui/material/styles";
import type {} from "@mui/system";
import {
AArrowDown,
AArrowUp,
AlignCenter,
AlignLeft,
AlignRight,
@@ -22,9 +20,11 @@ import {
Grid2x2X,
ImageDown,
Italic,
Minus,
PaintBucket,
PaintRoller,
Percent,
Plus,
Redo2,
RemoveFormatting,
Strikethrough,
@@ -32,6 +32,7 @@ import {
Type,
Underline,
Undo2,
WrapText,
} from "lucide-react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -64,6 +65,7 @@ type ToolbarProperties = {
onToggleStrike: (v: boolean) => void;
onToggleHorizontalAlign: (v: string) => void;
onToggleVerticalAlign: (v: string) => void;
onToggleWrapText: (v: boolean) => void;
onCopyStyles: () => void;
onTextColorPicked: (hex: string) => void;
onFillColorPicked: (hex: string) => void;
@@ -74,12 +76,14 @@ type ToolbarProperties = {
onDownloadPNG: () => void;
fillColor: string;
fontColor: string;
fontSize: number;
bold: boolean;
underline: boolean;
italic: boolean;
strike: boolean;
horizontalAlign: HorizontalAlignment;
verticalAlign: VerticalAlignment;
wrapText: boolean;
canEdit: boolean;
numFmt: string;
showGridLines: boolean;
@@ -205,6 +209,30 @@ function Toolbar(properties: ToolbarProperties) {
</StyledButton>
</FormatMenu>
<Divider />
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(-1);
}}
title={t("toolbar.decrease_font_size")}
>
<Minus />
</StyledButton>
<FontSizeBox>{properties.fontSize}</FontSizeBox>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(1);
}}
title={t("toolbar.increase_font_size")}
>
<Plus />
</StyledButton>
<Divider />
<StyledButton
type="button"
$pressed={properties.bold}
@@ -253,28 +281,6 @@ function Toolbar(properties: ToolbarProperties) {
>
<Type />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(1);
}}
title={t("toolbar.increase_font_size")}
>
<AArrowUp />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(-1);
}}
title={t("toolbar.decrease_font_size")}
>
<AArrowDown />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
@@ -363,6 +369,17 @@ function Toolbar(properties: ToolbarProperties) {
>
<ArrowDownToLine />
</StyledButton>
<StyledButton
type="button"
$pressed={properties.wrapText === true}
onClick={() => {
properties.onToggleWrapText(!properties.wrapText);
}}
disabled={!canEdit}
title={t("toolbar.wrap_text")}
>
<WrapText />
</StyledButton>
<Divider />
<StyledButton
@@ -393,10 +410,10 @@ function Toolbar(properties: ToolbarProperties) {
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onClearFormatting();
}}
disabled={!canEdit}
title={t("toolbar.clear_formatting")}
>
<RemoveFormatting />
@@ -404,10 +421,10 @@ function Toolbar(properties: ToolbarProperties) {
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onDownloadPNG();
}}
disabled={!canEdit}
title={t("toolbar.selected_png")}
>
<ImageDown />
@@ -415,6 +432,8 @@ function Toolbar(properties: ToolbarProperties) {
<ColorPicker
color={properties.fontColor}
defaultColor="#000000"
title={t("color_picker.default")}
onChange={(color): void => {
properties.onTextColorPicked(color);
setFontColorPickerOpen(false);
@@ -424,11 +443,17 @@ function Toolbar(properties: ToolbarProperties) {
}}
anchorEl={fontColorButton}
open={fontColorPickerOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
/>
<ColorPicker
color={properties.fillColor}
defaultColor="#FFFFFF"
title={t("color_picker.default")}
onChange={(color): void => {
properties.onFillColorPicked(color);
if (color !== null) {
properties.onFillColorPicked(color);
}
setFillColorPickerOpen(false);
}}
onClose={() => {
@@ -436,6 +461,8 @@ function Toolbar(properties: ToolbarProperties) {
}}
anchorEl={fillColorButton}
open={fillColorPickerOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
/>
<BorderPicker
onChange={(border): void => {
@@ -534,4 +561,16 @@ const Divider = styled("div")({
margin: "0px 12px",
});
const FontSizeBox = styled("div")({
width: "24px",
height: "24px",
lineHeight: "24px",
textAlign: "center",
fontFamily: "Inter",
fontSize: "11px",
border: `1px solid ${theme.palette.grey["300"]}`,
borderRadius: "4px",
minWidth: "24px",
});
export default Toolbar;

View File

@@ -112,6 +112,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("alignment.vertical", value);
};
const onToggleWrapText = (value: boolean) => {
updateRangeStyle("alignment.wrap_text", `${value}`);
};
const onTextColorPicked = (hex: string) => {
updateRangeStyle("font.color", hex);
};
@@ -344,7 +348,28 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
return workbookState.getEditingText();
}
const { sheet, row, column } = model.getSelectedView();
return model.getCellContent(sheet, row, column);
const r = model.getCellArrayStructure(sheet, row, column);
if (r === "SingleCell") {
return model.getCellContent(sheet, row, column);
}
if ("DynamicMother" in r) {
return model.getCellContent(sheet, row, column);
}
const [mother_row, mother_column, _] = r.DynamicChild;
return model.getCellContent(sheet, mother_row, mother_column);
};
// returns true if it is either single cell or the root cell of an array
const isRootCellOfArray = () => {
const { sheet, row, column } = model.getSelectedView();
const r = model.getCellArrayStructure(sheet, row, column);
if (r === "SingleCell") {
return false;
}
if ("DynamicMother" in r) {
return false;
}
return true;
};
const getCellStyle = useCallback(() => {
@@ -532,6 +557,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
onToggleStrike={onToggleStrike}
onToggleHorizontalAlign={onToggleHorizontalAlign}
onToggleVerticalAlign={onToggleVerticalAlign}
onToggleWrapText={onToggleWrapText}
onCopyStyles={onCopyStyles}
onTextColorPicked={onTextColorPicked}
onFillColorPicked={onFillColorPicked}
@@ -628,6 +654,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
fillColor={style.fill.fg_color || "#FFFFFF"}
fontColor={style.font.color}
fontSize={style.font.sz}
bold={style.font.b}
underline={style.font.u}
italic={style.font.i}
@@ -638,6 +665,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
verticalAlign={
style.alignment?.vertical ? style.alignment.vertical : "bottom"
}
wrapText={style.alignment?.wrap_text || false}
canEdit={true}
numFmt={style.num_fmt}
showGridLines={model.getShowGridLines(model.getSelectedSheet())}
@@ -691,6 +719,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
model={model}
workbookState={workbookState}
isPartOfArray={isRootCellOfArray()}
/>
<Worksheet
model={model}

View File

@@ -14,6 +14,7 @@ import {
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
cellArrayStructureColor,
outlineBackgroundColor,
outlineColor,
} from "../WorksheetCanvas/constants";
@@ -60,6 +61,7 @@ const Worksheet = forwardRef(
const cellOutline = useRef<HTMLDivElement>(null);
const areaOutline = useRef<HTMLDivElement>(null);
const cellOutlineHandle = useRef<HTMLDivElement>(null);
const cellArrayStructure = useRef<HTMLDivElement>(null);
const extendToOutline = useRef<HTMLDivElement>(null);
const columnResizeGuide = useRef<HTMLDivElement>(null);
const rowResizeGuide = useRef<HTMLDivElement>(null);
@@ -87,6 +89,7 @@ const Worksheet = forwardRef(
const outline = cellOutline.current;
const handle = cellOutlineHandle.current;
const area = areaOutline.current;
const arrayStructure = cellArrayStructure.current;
const extendTo = extendToOutline.current;
const editor = editorElement.current;
@@ -101,7 +104,8 @@ const Worksheet = forwardRef(
!area ||
!extendTo ||
!scrollElement.current ||
!editor
!editor ||
!arrayStructure
)
return;
// FIXME: This two need to be computed.
@@ -119,6 +123,7 @@ const Worksheet = forwardRef(
columnHeaders: columnHeadersRef,
cellOutline: outline,
cellOutlineHandle: handle,
cellArrayStructure: arrayStructure,
areaOutline: area,
extendToOutline: extendTo,
editor: editor,
@@ -462,6 +467,7 @@ const Worksheet = forwardRef(
/>
</EditorWrapper>
<AreaOutline ref={areaOutline} />
<CellArrayStructure ref={cellArrayStructure} />
<ExtendToOutline ref={extendToOutline} />
<CellOutlineHandle
ref={cellOutlineHandle}
@@ -631,6 +637,12 @@ const AreaOutline = styled("div")`
background-color: ${outlineBackgroundColor};
`;
const CellArrayStructure = styled("div")`
position: absolute;
border: 1px solid ${cellArrayStructureColor};
border-radius: 3px;
`;
const CellOutline = styled("div")`
position: absolute;
border: 2px solid ${outlineColor};

View File

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

View File

@@ -29,6 +29,7 @@ export interface CanvasSettings {
cellOutline: HTMLDivElement;
areaOutline: HTMLDivElement;
cellOutlineHandle: HTMLDivElement;
cellArrayStructure: HTMLDivElement;
extendToOutline: HTMLDivElement;
columnGuide: HTMLDivElement;
rowGuide: HTMLDivElement;
@@ -70,6 +71,53 @@ function hexToRGBA10Percent(colorHex: string): 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 (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;
}
export default class WorksheetCanvas {
sheetWidth: number;
@@ -91,6 +139,8 @@ export default class WorksheetCanvas {
cellOutlineHandle: HTMLDivElement;
cellArrayStructure: HTMLDivElement;
extendToOutline: HTMLDivElement;
workbookState: WorkbookState;
@@ -123,6 +173,7 @@ export default class WorksheetCanvas {
this.cellOutline = options.elements.cellOutline;
this.cellOutlineHandle = options.elements.cellOutlineHandle;
this.cellArrayStructure = options.elements.cellArrayStructure;
this.areaOutline = options.elements.areaOutline;
this.extendToOutline = options.elements.extendToOutline;
this.rowGuide = options.elements.rowGuide;
@@ -371,6 +422,7 @@ export default class WorksheetCanvas {
if (style.alignment?.vertical) {
verticalAlign = style.alignment.vertical;
}
const wrapText = style.alignment?.wrap_text || false;
const context = this.ctx;
context.font = font;
@@ -496,9 +548,14 @@ export default class WorksheetCanvas {
context.rect(x, y, width, height);
context.clip();
// Is there any better parameter?
// Is there any better to determine the line height?
const lineHeight = fontSize * 1.5;
const lines = fullText.split("\n");
const lines = computeWrappedLines(
fullText,
wrapText,
context,
width - padding,
);
const lineCount = lines.length;
lines.forEach((text, line) => {
@@ -682,13 +739,19 @@ export default class WorksheetCanvas {
if (fullText === "") {
continue;
}
const width = this.getColumnWidth(sheet, column);
const style = this.model.getCellStyle(sheet, row, column);
const fontSize = style.font.sz;
const lineHeight = fontSize * 1.5;
let font = `${fontSize}px ${defaultCellFontFamily}`;
font = style.font.b ? `bold ${font}` : `400 ${font}`;
this.ctx.font = font;
const lines = fullText.split("\n");
const lines = computeWrappedLines(
fullText,
style.alignment?.wrap_text || false,
this.ctx,
width,
);
const lineCount = lines.length;
// This is computed so that the y position of the text is independent of the vertical alignment
const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize;
@@ -1186,16 +1249,20 @@ export default class WorksheetCanvas {
}
private drawCellOutline(): void {
const { cellOutline, areaOutline, cellOutlineHandle } = this;
const { cellArrayStructure, cellOutline, areaOutline, cellOutlineHandle } =
this;
if (this.workbookState.getEditingCell()) {
cellOutline.style.visibility = "hidden";
cellOutlineHandle.style.visibility = "hidden";
areaOutline.style.visibility = "hidden";
cellArrayStructure.style.visibility = "hidden";
return;
}
cellOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";
areaOutline.style.visibility = "visible";
// This one is hidden by default
cellArrayStructure.style.visibility = "hidden";
const [selectedSheet, selectedRow, selectedColumn] =
this.model.getSelectedCell();
@@ -1251,6 +1318,34 @@ export default class WorksheetCanvas {
[handleX, handleY] = this.getCoordinatesByCell(rowStart, columnStart);
handleX += this.getColumnWidth(selectedSheet, columnStart);
handleY += this.getRowHeight(selectedSheet, rowStart);
// we draw the array structure if needed only in this case
const arrayStructure = this.model.getCellArrayStructure(
selectedSheet,
selectedRow,
selectedColumn,
);
let array = null;
if (arrayStructure === "SingleCell") {
// nothing to see here
} else if ("DynamicMother" in arrayStructure) {
cellArrayStructure.style.visibility = "visible";
const [arrayWidth, arrayHeight] = arrayStructure.DynamicMother;
array = [selectedRow, selectedColumn, arrayWidth, arrayHeight];
} else {
cellArrayStructure.style.visibility = "visible";
array = arrayStructure.DynamicChild;
}
if (array !== null) {
const [arrayX, arrayY] = this.getCoordinatesByCell(array[0], array[1]);
const [arrayX1, arrayY1] = this.getCoordinatesByCell(
array[0] + array[3],
array[1] + array[2],
);
cellArrayStructure.style.left = `${arrayX}px`;
cellArrayStructure.style.top = `${arrayY}px`;
cellArrayStructure.style.width = `${arrayX1 - arrayX}px`;
cellArrayStructure.style.height = `${arrayY1 - arrayY}px`;
}
} else {
areaOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";

View File

@@ -26,6 +26,7 @@
"vertical_align_middle": " Align middle",
"vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG",
"wrap_text": "Wrap text",
"format_menu": {
"auto": "Auto",
"number": "Number",
@@ -120,6 +121,11 @@
"insert_column": "Insert column"
},
"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

@@ -1,7 +1,7 @@
import styled from "@emotion/styled";
import type { Model } from "@ironcalc/workbook";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
import { useRef, useState } from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { FileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton";
import ShareWorkbookDialog from "./ShareWorkbookDialog";
@@ -9,6 +9,20 @@ import { WorkbookTitle } from "./WorkbookTitle";
import { downloadModel } from "./rpc";
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: {
model: Model;
newModel: () => void;
@@ -16,8 +30,19 @@ export function FileBar(properties: {
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void;
}) {
const hiddenInputRef = useRef<HTMLInputElement>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const spacerRef = useRef<HTMLDivElement>(null);
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
const width = useWindowWidth();
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
useLayoutEffect(() => {
const el = spacerRef.current;
if (el) {
const bb = el.getBoundingClientRect();
setMaxTitleWidth(bb.right - bb.left - 50);
}
}, [width]);
return (
<FileBarWrapper>
@@ -41,19 +66,17 @@ export function FileBar(properties: {
>
Help
</HelpButton>
<WorkbookTitle
name={properties.model.getName()}
onNameChange={(name) => {
properties.model.setName(name);
updateNameSelectedWorkbook(properties.model, name);
}}
/>
<input
ref={hiddenInputRef}
type="text"
style={{ position: "absolute", left: -9999, top: -9999 }}
/>
<div style={{ marginLeft: "auto" }} />
<WorkbookTitleWrapper>
<WorkbookTitle
name={properties.model.getName()}
onNameChange={(name) => {
properties.model.setName(name);
updateNameSelectedWorkbook(properties.model, name);
}}
maxWidth={maxTitleWidth}
/>
</WorkbookTitleWrapper>
<Spacer ref={spacerRef} />
<DialogContainer>
<ShareButton onClick={() => setIsDialogOpen(true)} />
{isDialogOpen && (
@@ -68,6 +91,19 @@ export function FileBar(properties: {
);
}
// We want the workbook title to be exactly an the center of the page,
// so we need an absolute position
const WorkbookTitleWrapper = styled("div")`
position: absolute;
left: 50%;
transform: translateX(-50%);
`;
// The "Spacer" component occupies as much space as possible between the menu and the share button
const Spacer = styled("div")`
flex-grow: 1;
`;
const StyledDesktopLogo = styled(IronCalcLogo)`
width: 120px;
margin-left: 12px;
@@ -103,14 +139,15 @@ const Divider = styled("div")`
border-left: 1px solid #e0e0e0;
`;
// The container must be relative positioned so we can position the title absolutely
const FileBarWrapper = styled("div")`
position: relative;
height: 60px;
width: 100%;
background: #fff;
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
position: relative;
justify-content: space-between;
`;

View File

@@ -1,89 +1,105 @@
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;
onNameChange: (name: string) => void;
maxWidth: number;
}) {
const [width, setWidth] = useState(0);
const [value, setValue] = useState(props.name);
const [name, setName] = useState(properties.name);
const mirrorDivRef = useRef<HTMLDivElement>(null);
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setValue(event.target.value);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth);
}
};
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) {
setWidth(mirrorDivRef.current.scrollWidth);
}
}, []);
useEffect(() => {
setValue(props.name);
}, [props.name]);
}, [name]);
return (
<div
<Container
style={{
position: "absolute",
left: "50%",
textAlign: "center",
transform: "translateX(-50%)",
// height: "60px",
// lineHeight: "60px",
padding: "8px",
fontSize: "14px",
fontWeight: "700",
fontFamily: "Inter",
width,
width: Math.min(width, properties.maxWidth),
}}
>
<TitleWrapper
value={value}
rows={1}
onChange={handleChange}
<TitleInput
value={name}
onInput={handleChange}
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"
>
{value}
</TitleWrapper>
<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>
/>
<MirrorDiv ref={mirrorDivRef}>{name}</MirrorDiv>
</Container>
);
}
const TitleWrapper = styled("textarea")`
const Container = styled("div")`
text-align: center;
padding: 8px;
font-size: 14px;
font-weight: 700;
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;
text-align: center;
height: 20px;
line-height: 20px;
border-radius: 4px;
padding: inherit;
overflow: hidden;
outline: none;
resize: none;
text-wrap: nowrap;
@@ -97,8 +113,6 @@ const TitleWrapper = styled("textarea")`
font-weight: inherit;
font-family: inherit;
font-size: inherit;
max-width: 520px;
overflow: hidden;
overflow: ellipsis;
white-space: nowrap;
text-overflow: ellipsis;
`;

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ironcalc_server"
version = "0.3.0"
version = "0.5.0"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "ironcalc"
version = "0.3.0"
version = "0.5.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
homepage = "https://www.ironcalc.com"
@@ -20,7 +20,7 @@ thiserror = "1.0"
# Uses `../base` when used locally, and uses
# the inicated version from crates.io when published.
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../base", version = "0.3" }
ironcalc_base = { path = "../base", version = "0.5" }
itertools = "0.12"
chrono = "0.4"
bitcode = "0.6.0"

View File

@@ -22,7 +22,7 @@ use itertools::Itertools;
use ironcalc_base::{
expressions::{
parser::{stringify::to_excel_string, Node},
parser::{static_analysis::StaticResult, stringify::to_excel_string, Node},
types::CellReferenceRC,
utils::number_to_column,
},
@@ -56,7 +56,7 @@ fn get_formula_attribute(
pub(crate) fn get_worksheet_xml(
worksheet: &Worksheet,
parsed_formulas: &[Node],
parsed_formulas: &[(Node, StaticResult)],
dimension: &str,
is_sheet_selected: bool,
) -> String {
@@ -104,7 +104,7 @@ pub(crate) fn get_worksheet_xml(
let style = get_cell_style_attribute(*s);
row_data_str.push(format!("<c r=\"{cell_name}\"{style}/>"));
}
Cell::BooleanCell { v, s } => {
Cell::SpillBooleanCell { v, s, .. } | Cell::BooleanCell { v, s } => {
// <c r="A8" t="b" s="1">
// <v>1</v>
// </c>
@@ -114,7 +114,7 @@ pub(crate) fn get_worksheet_xml(
"<c r=\"{cell_name}\" t=\"b\"{style}><v>{b}</v></c>"
));
}
Cell::NumberCell { v, s } => {
Cell::SpillNumberCell { v, s, .. } | Cell::NumberCell { v, s } => {
// Normally the type number is left out. Example:
// <c r="C6" s="1">
// <v>3</v>
@@ -122,7 +122,7 @@ pub(crate) fn get_worksheet_xml(
let style = get_cell_style_attribute(*s);
row_data_str.push(format!("<c r=\"{cell_name}\"{style}><v>{v}</v></c>"));
}
Cell::ErrorCell { ei, s } => {
Cell::SpillErrorCell { ei, s, .. } | Cell::ErrorCell { ei, s } => {
let style = get_cell_style_attribute(*s);
row_data_str.push(format!(
"<c r=\"{cell_name}\" t=\"e\"{style}><v>{ei}</v></c>"
@@ -153,7 +153,7 @@ pub(crate) fn get_worksheet_xml(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize],
&parsed_formulas[*f as usize].0,
);
let b = i32::from(*v);
@@ -172,7 +172,7 @@ pub(crate) fn get_worksheet_xml(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize],
&parsed_formulas[*f as usize].0,
);
let style = get_cell_style_attribute(*s);
@@ -189,14 +189,14 @@ pub(crate) fn get_worksheet_xml(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize],
&parsed_formulas[*f as usize].0,
);
let style = get_cell_style_attribute(*s);
let escaped_v = escape_xml(v);
row_data_str.push(format!(
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{escaped_v}</v></c>"
));
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{escaped_v}</v></c>"
));
}
Cell::CellFormulaError {
f,
@@ -213,13 +213,135 @@ pub(crate) fn get_worksheet_xml(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize],
&parsed_formulas[*f as usize].0,
);
let style = get_cell_style_attribute(*s);
row_data_str.push(format!(
"<c r=\"{cell_name}\" t=\"e\"{style}><f>{formula}</f><v>{ei}</v></c>"
));
}
Cell::SpillStringCell { v, s, .. } => {
// inline string
// <c r="A1" t="str">
let style = get_cell_style_attribute(*s);
let escaped_v = escape_xml(v);
row_data_str.push(format!(
"<c r=\"{cell_name}\" t=\"str\"{style}><v>{escaped_v}</v></c>"
));
}
Cell::DynamicCellFormula { .. } => {
panic!("Model needs to be evaluated before saving!");
}
Cell::DynamicCellFormulaBoolean { f, v, s, r, a: _ } => {
// <c r="A1" s="3" cm="1">
// <f t="array" ref="A1:A10">A1:A10</f>
// <v>1</v>
// </c>
let style = get_cell_style_attribute(*s);
let range = format!(
"{}{}:{}{}",
column_name,
row_index,
number_to_column(r.0 + column_index).unwrap(),
r.1 + row_index
);
let formula = get_formula_attribute(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize].0,
);
let b = i32::from(*v);
row_data_str.push(format!(
r#"<c r="{cell_name}" t="b" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{b}</v></c>"#
));
}
Cell::DynamicCellFormulaNumber { f, v, s, r, a: _ } => {
// <c r="C4" s="3" cm="1">
// <f t="array" ref="C4:C10">C4:C10</f>
// <v>123</v>
// </c>
let style = get_cell_style_attribute(*s);
let range = format!(
"{}{}:{}{}",
column_name,
row_index,
number_to_column(r.0 + column_index).unwrap(),
r.1 + row_index
);
let formula = get_formula_attribute(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize].0,
);
row_data_str.push(format!(
r#"<c r="{cell_name}" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{v}</v></c>"#
));
}
Cell::DynamicCellFormulaString { f, v, s, r, a: _ } => {
// <c r="C6" t="str" s="5" cm="1">
// <f t="array" ref="C6:C10">C6:C10</f>
// <v>Hello world!</v>
// </c>
let style = get_cell_style_attribute(*s);
let range = format!(
"{}{}:{}{}",
column_name,
row_index,
number_to_column(r.0 + column_index).unwrap(),
r.1 + row_index
);
let formula = get_formula_attribute(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize].0,
);
let escaped_v = escape_xml(v);
row_data_str.push(format!(
r#"<c r="{cell_name}" t="str" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{escaped_v}</v></c>"#
));
}
Cell::DynamicCellFormulaError {
f,
ei,
s,
o: _,
m: _,
r,
a: _,
} => {
// <c r="C6" t="e" s="4" cm="1">
// <f t="array" ref="C6:C10">C6:C10</f>
// <v>#DIV/0!</v>
// </c>
let style = get_cell_style_attribute(*s);
let range = format!(
"{}{}:{}{}",
column_name,
row_index,
number_to_column(r.0 + column_index).unwrap(),
r.1 + row_index
);
let formula = get_formula_attribute(
worksheet.get_name(),
*row_index,
*column_index,
&parsed_formulas[*f as usize].0,
);
row_data_str.push(format!(
r#"<c r="{cell_name}" t="e" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{ei}</v></c>"#
));
}
}
}
let row_style_str = match row_style_dict.get(row_index) {

View File

@@ -75,30 +75,21 @@ pub(crate) fn load_table<R: Read + std::io::Seek>(
// style index of the header row of the table
let header_row_dxf_id = if let Some(index_str) = table.attribute("headerRowDxfId") {
match index_str.parse::<u32>() {
Ok(i) => Some(i),
Err(_) => None,
}
index_str.parse::<u32>().ok()
} else {
None
};
// style index of the header row of the table
let data_dxf_id = if let Some(index_str) = table.attribute("headerRowDxfId") {
match index_str.parse::<u32>() {
Ok(i) => Some(i),
Err(_) => None,
}
index_str.parse::<u32>().ok()
} else {
None
};
// style index of the totals row of the table
let totals_row_dxf_id = if let Some(index_str) = table.attribute("totalsRowDxfId") {
match index_str.parse::<u32>() {
Ok(i) => Some(i),
Err(_) => None,
}
index_str.parse::<u32>().ok()
} else {
None
};
@@ -143,30 +134,21 @@ pub(crate) fn load_table<R: Read + std::io::Seek>(
// style index of the header row of the table
let header_row_dxf_id = if let Some(index_str) = table_column.attribute("headerRowDxfId") {
match index_str.parse::<u32>() {
Ok(i) => Some(i),
Err(_) => None,
}
index_str.parse::<u32>().ok()
} else {
None
};
// style index of the header row of the table column
let data_dxf_id = if let Some(index_str) = table_column.attribute("headerRowDxfId") {
match index_str.parse::<u32>() {
Ok(i) => Some(i),
Err(_) => None,
}
index_str.parse::<u32>().ok()
} else {
None
};
// style index of the totals row of the table column
let totals_row_dxf_id = if let Some(index_str) = table_column.attribute("totalsRowDxfId") {
match index_str.parse::<u32>() {
Ok(i) => Some(i),
Err(_) => None,
}
index_str.parse::<u32>().ok()
} else {
None
};

View File

@@ -1,10 +1,11 @@
#![allow(clippy::unwrap_used)]
use ironcalc_base::expressions::parser::static_analysis::add_implicit_intersection;
use std::{collections::HashMap, io::Read, num::ParseIntError};
use ironcalc_base::{
expressions::{
parser::{stringify::to_rc_format, Parser},
parser::{stringify::to_rc_format, DefinedNameS, Parser},
token::{get_error_by_english_name, Error},
types::CellReferenceRC,
utils::{column_to_number, parse_reference_a1},
@@ -42,7 +43,7 @@ pub(crate) struct Relationship {
}
impl WorkbookXML {
fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>)> {
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 defined_names = self
@@ -58,7 +59,7 @@ impl WorkbookXML {
// convert Option<usize> to Option<u32>
.map(|pos| pos as u32);
(dn.name.clone(), index)
(dn.name.clone(), index, dn.formula.clone())
})
.collect::<Vec<_>>();
defined_names
@@ -304,12 +305,16 @@ fn from_a1_to_rc(
worksheets: &[String],
context: String,
tables: HashMap<String, Table>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
is_array: bool,
) -> Result<String, XlsxError> {
let mut parser = Parser::new(worksheets.to_owned(), defined_names, tables);
let cell_reference =
parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?;
let t = parser.parse(&formula, &cell_reference);
let mut t = parser.parse(&formula, &cell_reference);
if !is_array {
add_implicit_intersection(&mut t, true);
}
Ok(to_rc_format(&t))
}
@@ -706,7 +711,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
worksheets: &[String],
tables: &HashMap<String, Table>,
shared_strings: &mut Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
defined_names: Vec<DefinedNameS>,
) -> Result<(Worksheet, bool), XlsxError> {
let sheet_name = &settings.name;
let sheet_id = settings.id;
@@ -805,8 +810,9 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
// r: reference. A1 style
// s: style index
// t: cell type
// Unused attributes
// cm (cell metadata), ph (Show Phonetic), vm (value metadata)
// cm: cell metadata (used for dynamic arrays)
// vm: value metadata (used for #SPILL! and #CALC! errors)
// ph: Show Phonetic, unused
for cell in row.children() {
let cell_ref = get_attribute(&cell, "r")?;
let column_letter = get_column_from_ref(cell_ref);
@@ -822,6 +828,9 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
None
};
let cell_metadata = cell.attribute("cm");
let is_dynamic_array = cell_metadata == Some("1");
// type, the default type being "n" for number
// If the cell does not have a value is an empty cell
let cell_type = match cell.attribute("t") {
@@ -887,6 +896,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
context,
tables.clone(),
defined_names.clone(),
is_dynamic_array,
)?;
match index_map.get(&si) {
Some(index) => {
@@ -931,13 +941,15 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
}
}
}
"array" => {
return Err(XlsxError::NotImplemented("array formulas".to_string()));
}
"dataTable" => {
return Err(XlsxError::NotImplemented("data table formulas".to_string()));
}
"normal" => {
"array" | "normal" => {
if formula_type == "array" && !is_dynamic_array {
// Dynamic formulas in Excel are formulas of type array with the cm=1, those we support.
// On the other hand the old CSE formulas or array formulas are not supported in IronCalc for the time being
return Err(XlsxError::NotImplemented("array formulas".to_string()));
}
// Its a cell with a simple formula
let formula = fs[0].text().unwrap_or("").to_string();
let context = format!("{}!{}", sheet_name, cell_ref);
@@ -947,6 +959,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
context,
tables.clone(),
defined_names.clone(),
is_dynamic_array,
)?;
match get_formula_index(&formula, &shared_formulas) {

View File

@@ -9,11 +9,9 @@
//!
//! ```toml
//! [dependencies]
//! ironcalc = { git = "https://github.com/ironcalc/IronCalc" }
//! ironcalc = { git = "https://github.com/ironcalc/IronCalc", tag = "v0.5.0" }
//! ```
//!
//! <small> until version 0.5.0 you should use the git dependencies as stated </small>
//!
//! A simple example with some numbers, a new sheet and a formula:
//!
//!

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -48,8 +48,8 @@ fn test_example() {
assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]);
let model2 = load_from_icalc("tests/example.ic").unwrap();
let s = bitcode::encode(&model2.workbook);
assert_eq!(workbook, model2.workbook, "{:?}", s);
let _ = bitcode::encode(&model2.workbook);
assert_eq!(workbook, model2.workbook);
}
#[test]
@@ -346,21 +346,31 @@ fn test_xlsx() {
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {}", file_path_str);
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{}", message);
panic!("Model was evaluated inconsistently with XLSX data.")
is_error = true;
}
let t = test_load_and_saving(file_path_str, &dir);
if t.is_err() {
println!("Error while load and saving file: {file_path_str}");
is_error = true;
}
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
} else {
println!("skipping");
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
);
}
#[test]
@@ -375,20 +385,26 @@ fn no_export() {
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {}", file_path_str);
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{}", message);
panic!("Model was evaluated inconsistently with XLSX data.")
is_error = true;
}
} else {
println!("skipping");
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
);
}
// This test verifies whether exporting the merged cells functionality is happening properly or not.
@@ -476,6 +492,7 @@ fn test_documentation_xlsx() {
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
@@ -487,7 +504,7 @@ fn test_documentation_xlsx() {
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("{}", message);
panic!("Model was evaluated inconsistently with XLSX data.")
is_error = true;
}
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
} else {
@@ -495,6 +512,10 @@ fn test_documentation_xlsx() {
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
)
}
#[test]