Compare commits

...

30 Commits

Author SHA1 Message Date
francisco aloi
517221fac5 new components for name manager dialog and fields
new text added to locale en_us

added name manager to toolbar

added model and worksheets as prop

UPDATE: API for defined names

NamedRange component changes

NameManager dialog component changes

new util formatting functions

UPDATE: API for defined names

new components for name manager dialog and fields

new text added to locale en_us

added name manager to toolbar

added model and worksheets as prop

UPDATE: API for defined names

NamedRange component changes

NameManager dialog component changes

new util formatting functions

UPDATE: API for defined names

last changes

corrected styling

updates to namedRange and nameManagerDialog
2024-12-26 18:28:48 +01:00
Gian Hancock
655d663590 FIX: Make XOR, OR, AND functions more consistent with Excel
The way these functions interpret their arguments is inconsistent with
Excel in a few ways:

- EmptyCell: Excel ignores arguments evaluating to these types of
  values, treating them as if they didn't exist.

- Text: Text cells are ignored unless they are "TRUE" or "FALSE" (case
  insensitive). EXCEPT if the string value comes from a reference, in
  which case it is always ignored regardless of its value.

- Error if no args: Excel returns a #VALUE! error for these functions if
  no arguments are provided, or if all arguments are ignored (see
  above).

- EmptyArg: Bizarrely, Unlike EmptyCell, EmptyArg is not ignored and is
  treated as if it were FALSE by Excel.

- ErrorPropagation: Excel propagates errors in the arguments and in
  cells belonging to any Range arguments.

Additionally, these functions are not consistent with each other, XOR,
OR, AND vary in how they handle the cases mentioned above.

Rectify these consistency issues by re-implementing them all in terms of
a single base function which is more consistent with Excel behavior.
2024-12-26 15:06:54 +01:00
Gian Hancock
8ba30fde33 Add more tests for OR and XOR
Some of these tests fail due to inconsistencies with Excel

cleanup
2024-12-26 15:06:54 +01:00
Nicolás Hatcher
690032c811 FIX: Remove optional context in parser
The context was optional because I thought that paring an RC formula
did not need context.

You at least need the sheet in which you are parsing
For instance toknow if a defined name is local
2024-12-26 10:21:21 +01:00
Nicolás Hatcher
86213a8434 FIX: Add test for get_defined_name_list
Also uses the scope instead of the opaque sheet_id
2024-12-26 10:21:21 +01:00
Nicolás Hatcher
2ed5fb9bbc FIX: Adds some validation and tests 2024-12-26 10:21:21 +01:00
Nicolás Hatcher
e455ed14ea UPDATE: API for defined names 2024-12-26 10:21:21 +01:00
Daniel
ad2efad3ae FIX: Remove tabIndex and onKeyDown as they were not used 2024-12-25 20:04:48 +01:00
Daniel
40461b897b FIX: Adds crossRef back to UploadFileDialog 2024-12-25 20:04:48 +01:00
Daniel
2e7410552f FIX: Apply useTranslation on dialog tooltips 2024-12-25 20:04:48 +01:00
Daniel
095002710b Fix: Removed unnecessary ref 2024-12-25 20:04:48 +01:00
Daniel
8ba131011e FIX: Replace SVG "X" icons with Lucide icons for consistency in dialogs 2024-12-25 20:04:48 +01:00
Nicolás Hatcher Andrés
dbddc027fb Update test-coverage.yaml 2024-12-25 16:57:54 +01:00
Steve Fanning
de997f38f5 Minor updates to Error Types and Value Types documentation 2024-12-25 13:47:21 +01:00
Steve Fanning
df4b4ca353 Update to FV description and associated pages in the Features area. 2024-12-25 13:47:21 +01:00
Daniel
3b944cd659 FIX: Use the right icon for paste format action 2024-12-20 20:13:25 +01:00
Nicolás Hatcher Andrés
d1f2b2acdd Update test-coverage.yaml 2024-12-20 14:10:02 +01:00
Daniel González-Albo
36f915b193 Merge pull request #207 from ironcalc/feature/nicolas-hidden
UPDATE: Hide/Unhide sheets
2024-12-20 10:14:09 +01:00
Nicolás Hatcher
5d8e6255a3 UPDATE: Hide/Unhide sheets 2024-12-20 00:49:33 +01:00
Daniel González-Albo
73f3c06203 Merge pull request #205 from ironcalc/bugfix/nico-dialog-fixes
FIX[WebApp]: Some fixes to the DeleteWorkbook and Import dialogs
2024-12-20 00:16:42 +01:00
Nicolás Hatcher
13b1157c61 FIX[WebApp]: Some fixes to the DeleteWorkbook and Import dialogs 2024-12-19 21:14:05 +01:00
Daniel
44f7929f4e FIX: responsiveness 2024-12-19 17:48:54 +01:00
Daniel
23643f0fae UPDATE: connect FileMenu button to open confirmation modal 2024-12-19 17:48:54 +01:00
Daniel
ad91d47db0 UPDATE: add confirmation modal for deleting workbooks 2024-12-19 17:48:54 +01:00
Daniel
8f36a1f750 FIX: make grid header colors consistent 2024-12-19 17:37:10 +01:00
Sinan Yumurtaci
8ad407432f lint 2024-12-19 17:36:27 +01:00
Sinan Yumurtaci
ebc31780ab FIX[WebApp]: Disable delete for sheet if it is the last one 2024-12-19 17:36:27 +01:00
Daniel
6e8c47d4f6 UPDATE: Replace one preset color from color picker 2024-12-18 20:00:52 +01:00
Daniel González-Albo
ed42667e87 Merge pull request #198 from ironcalc/bugfix/nicolas-modal-fixes
FIX[UI]: Rename modal dialog fixes
2024-12-16 22:35:49 +01:00
Nicolás Hatcher
0cd3470a97 FIX[UI]: Rename modal dialog fixes
This will be a standard "Prompt" widget

* ESC closes the dialog without changes
* Can copy/cut paste
2024-12-16 21:24:41 +01:00
59 changed files with 3180 additions and 820 deletions

View File

@@ -17,8 +17,9 @@ jobs:
- name: Generate code coverage - name: Generate code coverage
run: cargo llvm-cov --all-features --workspace --exclude pyroncalc --exclude wasm --lcov --output-path lcov.info run: cargo llvm-cov --all-features --workspace --exclude pyroncalc --exclude wasm --lcov --output-path lcov.info
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info files: lcov.info
fail_ci_if_error: true fail_ci_if_error: true

View File

@@ -2,6 +2,7 @@ use std::cmp::Ordering;
use crate::expressions::{token::Error, types::CellReferenceIndex}; use crate::expressions::{token::Error, types::CellReferenceIndex};
#[derive(Clone)]
pub struct Range { pub struct Range {
pub left: CellReferenceIndex, pub left: CellReferenceIndex,
pub right: CellReferenceIndex, pub right: CellReferenceIndex,

View File

@@ -164,7 +164,9 @@ pub enum Node {
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Node>), ArrayKind(Vec<Node>),
VariableKind(String), DefinedNameKind((String, Option<u32>)),
TableNameKind(String),
WrongVariableKind(String),
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -187,12 +189,17 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
context: Option<CellReferenceRC>, defined_names: Vec<(String, Option<u32>)>,
context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
impl Parser { impl Parser {
pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser { pub fn new(
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
"", "",
lexer::LexerMode::A1, lexer::LexerMode::A1,
@@ -201,10 +208,16 @@ impl Parser {
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
get_language("en").expect(""), get_language("en").expect(""),
); );
let context = CellReferenceRC {
sheet: worksheets.first().map_or("", |v| v).to_string(),
column: 1,
row: 1,
};
Parser { Parser {
lexer, lexer,
worksheets, worksheets,
context: None, defined_names,
context,
tables, tables,
} }
} }
@@ -212,13 +225,18 @@ impl Parser {
self.lexer.set_lexer_mode(mode) self.lexer.set_lexer_mode(mode)
} }
pub fn set_worksheets(&mut self, worksheets: Vec<String>) { pub fn set_worksheets_and_names(
&mut self,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names;
} }
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node { pub fn parse(&mut self, formula: &str, context: &CellReferenceRC) -> Node {
self.lexer.set_formula(formula); self.lexer.set_formula(formula);
self.context.clone_from(context); self.context = context.clone();
self.parse_expr() self.parse_expr()
} }
@@ -232,6 +250,24 @@ impl Parser {
None None
} }
// 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(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 {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope);
}
}
for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some(None);
}
}
None
}
fn parse_expr(&mut self) -> Node { fn parse_expr(&mut self) -> Node {
let mut t = self.parse_concat(); let mut t = self.parse_concat();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
@@ -446,16 +482,7 @@ impl Parser {
absolute_column, absolute_column,
absolute_row, absolute_row,
} => { } => {
let context = match &self.context { let context = &self.context;
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -490,16 +517,7 @@ impl Parser {
} }
} }
TokenType::Range { sheet, left, right } => { TokenType::Range { sheet, left, right } => {
let context = match &self.context { let context = &self.context;
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -585,11 +603,33 @@ impl Parser {
kind: function_kind, kind: function_kind,
args, args,
}; };
} else { }
return Node::InvalidFunctionKind { name, args }; return Node::InvalidFunctionKind { name, args };
} }
let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
} }
Node::VariableKind(name) };
// 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));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
if table_name.to_lowercase() == name_lower {
return Node::TableNameKind(name);
}
}
Node::WrongVariableKind(name)
} }
TokenType::Error(kind) => Node::ErrorKind(kind), TokenType::Error(kind) => Node::ErrorKind(kind),
TokenType::Illegal(error) => Node::ParseErrorKind { TokenType::Illegal(error) => Node::ParseErrorKind {
@@ -661,7 +701,7 @@ impl Parser {
// We will try to convert to a normal reference // We will try to convert to a normal reference
// table_name[column_name] => cell1:cell2 // table_name[column_name] => cell1:cell2
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2 // table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
if let Some(context) = &self.context { let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) { let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i, Some(i) => i,
None => { None => {
@@ -747,8 +787,7 @@ impl Parser {
} }
} }
match table_reference { match table_reference {
None => { None => Node::RangeKind {
return Node::RangeKind {
sheet_name, sheet_name,
sheet_index: table_sheet_index, sheet_index: table_sheet_index,
absolute_row1: true, absolute_row1: true,
@@ -759,8 +798,7 @@ impl Parser {
absolute_column2: true, absolute_column2: true,
row2: row_end, row2: row_end,
column2: column_end, column2: column_end,
}; },
}
Some(TableReference::ColumnReference(s)) => { Some(TableReference::ColumnReference(s)) => {
let column_index = match get_table_column_by_name(&s, table) { let column_index = match get_table_column_by_name(&s, table) {
Some(s) => s + column_start, Some(s) => s + column_start,
@@ -768,9 +806,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize, position: self.lexer.get_position() as usize,
message: format!( message: format!("Expecting column: {s} in table {table_name}"),
"Expecting column: {s} in table {table_name}"
),
}; };
} }
}; };
@@ -784,7 +820,7 @@ impl Parser {
column: column_index, column: column_index,
}; };
} }
return Node::RangeKind { Node::RangeKind {
sheet_name, sheet_name,
sheet_index: table_sheet_index, sheet_index: table_sheet_index,
absolute_row1: true, absolute_row1: true,
@@ -795,7 +831,7 @@ impl Parser {
absolute_column2: true, absolute_column2: true,
row2: row_end, row2: row_end,
column2: column_index, column2: column_index,
}; }
} }
Some(TableReference::RangeReference((left, right))) => { Some(TableReference::RangeReference((left, right))) => {
let left_column_index = match get_table_column_by_name(&left, table) { let left_column_index = match get_table_column_by_name(&left, table) {
@@ -823,7 +859,7 @@ impl Parser {
}; };
} }
}; };
return Node::RangeKind { Node::RangeKind {
sheet_name, sheet_name,
sheet_index: table_sheet_index, sheet_index: table_sheet_index,
absolute_row1: true, absolute_row1: true,
@@ -834,15 +870,9 @@ impl Parser {
absolute_column2: true, absolute_column2: true,
row2: row_end, row2: row_end,
column2: right_column_index, column2: right_column_index,
};
} }
} }
} }
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Structured references not supported in R1C1 mode".to_string(),
}
} }
} }
} }

View File

@@ -375,7 +375,9 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
format!("{{{}}}", arguments) format!("{{{}}}", arguments)
} }
VariableKind(value) => value.to_string(), DefinedNameKind((name, _)) => name.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
to_string_moved(left, move_context), to_string_moved(left, move_context),

View File

@@ -464,7 +464,9 @@ fn stringify(
| ReferenceKind { .. } | ReferenceKind { .. }
| RangeKind { .. } | RangeKind { .. }
| WrongReferenceKind { .. } | WrongReferenceKind { .. }
| VariableKind(_) | DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, use_original_name)
} }
@@ -492,7 +494,9 @@ fn stringify(
| ReferenceKind { .. } | ReferenceKind { .. }
| RangeKind { .. } | RangeKind { .. }
| WrongReferenceKind { .. } | WrongReferenceKind { .. }
| VariableKind(_) | DefinedNameKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, use_original_name)
} }
@@ -543,7 +547,9 @@ fn stringify(
} }
format!("{{{}}}", arguments) format!("{{{}}}", arguments)
} }
VariableKind(value) => value.to_string(), TableNameKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
format!( format!(
@@ -660,7 +666,90 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::ErrorKind(_) => {} Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {} Node::ArrayKind(_) => {}
Node::VariableKind(_) => {} Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::EmptyArgKind => {} Node::EmptyArgKind => {}
} }
} }
pub(crate) fn rename_defined_name_in_node(
node: &mut Node,
name: &str,
scope: Option<u32>,
new_name: &str,
) {
match node {
// Rename
Node::DefinedNameKind((n, s)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string();
}
}
// Go next level
Node::OpRangeKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpConcatenateKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpPowerKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {}
Node::EmptyArgKind => {}
Node::ReferenceKind { .. } => {}
Node::RangeKind { .. } => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
}
}

View File

@@ -17,7 +17,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_reference() { fn test_parser_reference() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -25,14 +25,14 @@ fn test_parser_reference() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("A2", &Some(cell_reference)); let t = parser.parse("A2", &cell_reference);
assert_eq!(to_rc_format(&t), "R[1]C[0]"); assert_eq!(to_rc_format(&t), "R[1]C[0]");
} }
#[test] #[test]
fn test_parser_absolute_column() { fn test_parser_absolute_column() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -40,14 +40,14 @@ fn test_parser_absolute_column() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A1", &Some(cell_reference)); let t = parser.parse("$A1", &cell_reference);
assert_eq!(to_rc_format(&t), "R[0]C1"); assert_eq!(to_rc_format(&t), "R[0]C1");
} }
#[test] #[test]
fn test_parser_absolute_row_col() { fn test_parser_absolute_row_col() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -55,14 +55,14 @@ fn test_parser_absolute_row_col() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$C$5", &Some(cell_reference)); let t = parser.parse("$C$5", &cell_reference);
assert_eq!(to_rc_format(&t), "R5C3"); assert_eq!(to_rc_format(&t), "R5C3");
} }
#[test] #[test]
fn test_parser_absolute_row_col_1() { fn test_parser_absolute_row_col_1() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -70,14 +70,14 @@ fn test_parser_absolute_row_col_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A$1", &Some(cell_reference)); let t = parser.parse("$A$1", &cell_reference);
assert_eq!(to_rc_format(&t), "R1C1"); assert_eq!(to_rc_format(&t), "R1C1");
} }
#[test] #[test]
fn test_parser_simple_formula() { fn test_parser_simple_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -86,14 +86,14 @@ fn test_parser_simple_formula() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference)); let t = parser.parse("C3+Sheet2!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_boolean() { fn test_parser_boolean() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -102,14 +102,14 @@ fn test_parser_boolean() {
column: 1, column: 1,
}; };
let t = parser.parse("true", &Some(cell_reference)); let t = parser.parse("true", &cell_reference);
assert_eq!(to_rc_format(&t), "TRUE"); assert_eq!(to_rc_format(&t), "TRUE");
} }
#[test] #[test]
fn test_parser_bad_formula() { fn test_parser_bad_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -117,7 +117,7 @@ fn test_parser_bad_formula() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("#Value", &Some(cell_reference)); let t = parser.parse("#Value", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -138,7 +138,7 @@ fn test_parser_bad_formula() {
#[test] #[test]
fn test_parser_bad_formula_1() { fn test_parser_bad_formula_1() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -146,7 +146,7 @@ fn test_parser_bad_formula_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("<5", &Some(cell_reference)); let t = parser.parse("<5", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -167,7 +167,7 @@ fn test_parser_bad_formula_1() {
#[test] #[test]
fn test_parser_bad_formula_2() { fn test_parser_bad_formula_2() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -175,7 +175,7 @@ fn test_parser_bad_formula_2() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("*5", &Some(cell_reference)); let t = parser.parse("*5", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -196,7 +196,7 @@ fn test_parser_bad_formula_2() {
#[test] #[test]
fn test_parser_bad_formula_3() { fn test_parser_bad_formula_3() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -204,7 +204,7 @@ fn test_parser_bad_formula_3() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference)); let t = parser.parse("SUM(#VALVE!)", &cell_reference);
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -225,7 +225,7 @@ fn test_parser_bad_formula_3() {
#[test] #[test]
fn test_parser_formulas() { fn test_parser_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -259,11 +259,11 @@ fn test_parser_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_rc_format(&t), formula.expected); assert_eq!(to_rc_format(&t), formula.expected);
assert_eq!(to_string(&t, &cell_reference), formula.initial); assert_eq!(to_string(&t, &cell_reference), formula.initial);
@@ -273,7 +273,7 @@ fn test_parser_formulas() {
#[test] #[test]
fn test_parser_r1c1_formulas() { fn test_parser_r1c1_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
parser.set_lexer_mode(LexerMode::R1C1); parser.set_lexer_mode(LexerMode::R1C1);
let formulas = vec![ let formulas = vec![
@@ -324,11 +324,11 @@ fn test_parser_r1c1_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_string(&t, &cell_reference), formula.expected); assert_eq!(to_string(&t, &cell_reference), formula.expected);
assert_eq!(to_rc_format(&t), formula.initial); assert_eq!(to_rc_format(&t), formula.initial);
@@ -338,7 +338,7 @@ fn test_parser_r1c1_formulas() {
#[test] #[test]
fn test_parser_quotes() { fn test_parser_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -347,14 +347,14 @@ fn test_parser_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference)); let t = parser.parse("C3+'Second Sheet'!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_escape_quotes() { fn test_parser_escape_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -363,14 +363,14 @@ fn test_parser_escape_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference)); let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &cell_reference);
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_parenthesis() { fn test_parser_parenthesis() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -379,14 +379,14 @@ fn test_parser_parenthesis() {
column: 1, column: 1,
}; };
let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference)); let t = parser.parse("(C3=\"Yes\")*5", &cell_reference);
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5"); assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
} }
#[test] #[test]
fn test_parser_excel_xlfn() { fn test_parser_excel_xlfn() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -395,7 +395,7 @@ fn test_parser_excel_xlfn() {
column: 1, column: 1,
}; };
let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference)); let t = parser.parse("_xlfn.CONCAT(C3)", &cell_reference);
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])"); assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
} }
@@ -407,9 +407,9 @@ fn test_to_string_displaced() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone())); let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -427,9 +427,9 @@ fn test_to_string_displaced_full_ranges() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("SUM(3:3)", &Some(context.clone())); let node = parser.parse("SUM(3:3)", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -440,7 +440,7 @@ fn test_to_string_displaced_full_ranges() {
"SUM(3:3)".to_string() "SUM(3:3)".to_string()
); );
let node = parser.parse("SUM(D:D)", &Some(context.clone())); let node = parser.parse("SUM(D:D)", context);
let displace_data = DisplaceData::Row { let displace_data = DisplaceData::Row {
sheet: 0, sheet: 0,
row: 3, row: 3,
@@ -460,9 +460,9 @@ fn test_to_string_displaced_too_low() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone())); let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -480,9 +480,9 @@ fn test_to_string_displaced_too_high() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let node = parser.parse("C3", &Some(context.clone())); let node = parser.parse("C3", context);
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,

View File

@@ -9,7 +9,7 @@ use crate::expressions::types::CellReferenceRC;
#[test] #[test]
fn issue_155_parser() { fn issue_155_parser() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -17,14 +17,14 @@ fn issue_155_parser() {
row: 2, row: 2,
column: 2, column: 2,
}; };
let t = parser.parse("A$1:A2", &Some(cell_reference.clone())); let t = parser.parse("A$1:A2", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "A$1:A2"); assert_eq!(to_string(&t, &cell_reference), "A$1:A2");
} }
#[test] #[test]
fn issue_155_parser_case_2() { fn issue_155_parser_case_2() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -32,14 +32,14 @@ fn issue_155_parser_case_2() {
row: 20, row: 20,
column: 20, column: 20,
}; };
let t = parser.parse("C$1:D2", &Some(cell_reference.clone())); let t = parser.parse("C$1:D2", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "C$1:D2"); assert_eq!(to_string(&t, &cell_reference), "C$1:D2");
} }
#[test] #[test]
fn issue_155_parser_only_row() { fn issue_155_parser_only_row() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -48,14 +48,14 @@ fn issue_155_parser_only_row() {
column: 20, column: 20,
}; };
// This is tricky, I am not sure what to do in these cases // This is tricky, I am not sure what to do in these cases
let t = parser.parse("A$2:B1", &Some(cell_reference.clone())); let t = parser.parse("A$2:B1", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "A1:B$2"); assert_eq!(to_string(&t, &cell_reference), "A1:B$2");
} }
#[test] #[test]
fn issue_155_parser_only_column() { fn issue_155_parser_only_column() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -64,6 +64,6 @@ fn issue_155_parser_only_column() {
column: 20, column: 20,
}; };
// This is tricky, I am not sure what to do in these cases // This is tricky, I am not sure what to do in these cases
let t = parser.parse("D1:$A3", &Some(cell_reference.clone())); let t = parser.parse("D1:$A3", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "$A1:D3"); assert_eq!(to_string(&t, &cell_reference), "$A1:D3");
} }

View File

@@ -15,7 +15,7 @@ fn test_move_formula() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -27,7 +27,7 @@ fn test_move_formula() {
}; };
// formula AB31 will not change // formula AB31 will not change
let node = parser.parse("AB31", &Some(context.clone())); let node = parser.parse("AB31", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -43,7 +43,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31"); assert_eq!(t, "AB31");
// formula $AB$31 will not change // formula $AB$31 will not change
let node = parser.parse("AB31", &Some(context.clone())); let node = parser.parse("AB31", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -59,7 +59,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31"); assert_eq!(t, "AB31");
// but formula D5 will change to N15 (N = D + 10) // but formula D5 will change to N15 (N = D + 10)
let node = parser.parse("D5", &Some(context.clone())); let node = parser.parse("D5", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -75,7 +75,7 @@ fn test_move_formula() {
assert_eq!(t, "N15"); assert_eq!(t, "N15");
// Also formula $D$5 will change to N15 (N = D + 10) // Also formula $D$5 will change to N15 (N = D + 10)
let node = parser.parse("$D$5", &Some(context.clone())); let node = parser.parse("$D$5", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -102,7 +102,7 @@ fn test_move_formula_context_offset() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -113,7 +113,7 @@ fn test_move_formula_context_offset() {
height: 5, height: 5,
}; };
let node = parser.parse("-X9+C2%", &Some(context.clone())); let node = parser.parse("-X9+C2%", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -140,7 +140,7 @@ fn test_move_formula_area_limits() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -152,7 +152,7 @@ fn test_move_formula_area_limits() {
}; };
// Outside of the area. Not moved // Outside of the area. Not moved
let node = parser.parse("B2+B3+C1+G6+H5", &Some(context.clone())); let node = parser.parse("B2+B3+C1+G6+H5", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -168,7 +168,7 @@ fn test_move_formula_area_limits() {
assert_eq!(t, "B2+B3+C1+G6+H5"); assert_eq!(t, "B2+B3+C1+G6+H5");
// In the area. Moved // In the area. Moved
let node = parser.parse("C2+F4+F5+F6", &Some(context.clone())); let node = parser.parse("C2+F4+F5+F6", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -195,7 +195,7 @@ fn test_move_formula_ranges() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let area = &Area { let area = &Area {
sheet: 0, sheet: 0,
@@ -205,7 +205,7 @@ fn test_move_formula_ranges() {
height: 5, height: 5,
}; };
// Ranges inside the area are fully displaced (absolute or not) // Ranges inside the area are fully displaced (absolute or not)
let node = parser.parse("SUM(C2:F5)", &Some(context.clone())); let node = parser.parse("SUM(C2:F5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -220,7 +220,7 @@ fn test_move_formula_ranges() {
); );
assert_eq!(t, "SUM(M12:P15)"); assert_eq!(t, "SUM(M12:P15)");
let node = parser.parse("SUM($C$2:$F$5)", &Some(context.clone())); let node = parser.parse("SUM($C$2:$F$5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -236,7 +236,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($M$12:$P$15)"); assert_eq!(t, "SUM($M$12:$P$15)");
// Ranges completely outside of the area are not touched // Ranges completely outside of the area are not touched
let node = parser.parse("SUM(A1:B3)", &Some(context.clone())); let node = parser.parse("SUM(A1:B3)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -251,7 +251,7 @@ fn test_move_formula_ranges() {
); );
assert_eq!(t, "SUM(A1:B3)"); assert_eq!(t, "SUM(A1:B3)");
let node = parser.parse("SUM($A$1:$B$3)", &Some(context.clone())); let node = parser.parse("SUM($A$1:$B$3)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -267,7 +267,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($A$1:$B$3)"); assert_eq!(t, "SUM($A$1:$B$3)");
// Ranges that overlap with the area are also NOT displaced // Ranges that overlap with the area are also NOT displaced
let node = parser.parse("SUM(A1:F5)", &Some(context.clone())); let node = parser.parse("SUM(A1:F5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -283,7 +283,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM(A1:F5)"); assert_eq!(t, "SUM(A1:F5)");
// Ranges that contain the area are also NOT displaced // Ranges that contain the area are also NOT displaced
let node = parser.parse("SUM(A1:X50)", &Some(context.clone())); let node = parser.parse("SUM(A1:X50)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -318,10 +318,10 @@ fn test_move_formula_wrong_reference() {
height: 5, height: 5,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Wrong formulas will NOT be displaced // Wrong formulas will NOT be displaced
let node = parser.parse("Sheet3!AB31", &Some(context.clone())); let node = parser.parse("Sheet3!AB31", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -335,7 +335,7 @@ fn test_move_formula_wrong_reference() {
}, },
); );
assert_eq!(t, "Sheet3!AB31"); assert_eq!(t, "Sheet3!AB31");
let node = parser.parse("Sheet3!$X$9", &Some(context.clone())); let node = parser.parse("Sheet3!$X$9", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -350,7 +350,7 @@ fn test_move_formula_wrong_reference() {
); );
assert_eq!(t, "Sheet3!$X$9"); assert_eq!(t, "Sheet3!$X$9");
let node = parser.parse("SUM(Sheet3!D2:D3)", &Some(context.clone())); let node = parser.parse("SUM(Sheet3!D2:D3)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -377,7 +377,7 @@ fn test_move_formula_misc() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
width: 4, width: 4,
height: 5, height: 5,
}; };
let node = parser.parse("X9^C2-F4*H2", &Some(context.clone())); let node = parser.parse("X9^C2-F4*H2", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -402,7 +402,7 @@ fn test_move_formula_misc() {
); );
assert_eq!(t, "X9^M12-P14*H2"); assert_eq!(t, "X9^M12-P14*H2");
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone())); let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -417,7 +417,7 @@ fn test_move_formula_misc() {
); );
assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)"); assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)");
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", &Some(context.clone())); let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", context);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -445,7 +445,7 @@ fn test_move_formula_another_sheet() {
}; };
// we add two sheets and we cut/paste from Sheet1 to Sheet2 // we add two sheets and we cut/paste from Sheet1 to Sheet2
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -457,10 +457,7 @@ fn test_move_formula_another_sheet() {
}; };
// Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4 // Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4
let node = parser.parse( let node = parser.parse("AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)", context);
"AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)",
&Some(context.clone()),
);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {

View File

@@ -14,7 +14,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_formulas_with_full_ranges() { fn test_parser_formulas_with_full_ranges() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -52,11 +52,11 @@ fn test_parser_formulas_with_full_ranges() {
for formula in &formulas { for formula in &formulas {
let t = parser.parse( let t = parser.parse(
formula.formula_a1, formula.formula_a1,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_rc_format(&t), formula.formula_r1c1); assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1); assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
@@ -67,11 +67,11 @@ fn test_parser_formulas_with_full_ranges() {
for formula in &formulas { for formula in &formulas {
let t = parser.parse( let t = parser.parse(
formula.formula_r1c1, formula.formula_r1c1,
&Some(CellReferenceRC { &CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}), },
); );
assert_eq!(to_rc_format(&t), formula.formula_r1c1); assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1); assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
@@ -81,7 +81,7 @@ fn test_parser_formulas_with_full_ranges() {
#[test] #[test]
fn test_range_inverse_order() { fn test_range_inverse_order() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -93,7 +93,7 @@ fn test_range_inverse_order() {
// D4:C2 => C2:D4 // D4:C2 => C2:D4
let t = parser.parse( let t = parser.parse(
"SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)", "SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)",
&Some(cell_reference.clone()), &cell_reference,
); );
assert_eq!( assert_eq!(
to_string(&t, &cell_reference), to_string(&t, &cell_reference),

View File

@@ -9,7 +9,7 @@ use crate::expressions::types::CellReferenceRC;
#[test] #[test]
fn exp_order() { fn exp_order() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new()); let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -17,18 +17,18 @@ fn exp_order() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("(1 + 2)^3 + 4", &Some(cell_reference.clone())); let t = parser.parse("(1 + 2)^3 + 4", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4"); assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4");
let t = parser.parse("(C5 + 3)^R4", &Some(cell_reference.clone())); let t = parser.parse("(C5 + 3)^R4", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4"); assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4");
let t = parser.parse("(C5 + 3)^(R4*6)", &Some(cell_reference.clone())); let t = parser.parse("(C5 + 3)^(R4*6)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)"); assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)");
let t = parser.parse("(C5)^(R4)", &Some(cell_reference.clone())); let t = parser.parse("(C5)^(R4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "C5^R4"); assert_eq!(to_string(&t, &cell_reference), "C5^R4");
let t = parser.parse("(5)^(4)", &Some(cell_reference.clone())); let t = parser.parse("(5)^(4)", &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "5^4"); assert_eq!(to_string(&t, &cell_reference), "5^4");
} }

View File

@@ -63,7 +63,7 @@ fn simple_table() {
let row_count = 3; let row_count = 3;
let tables = create_test_table("tblIncome", &column_names, "A1", row_count); let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
let mut parser = Parser::new(worksheets, tables); let mut parser = Parser::new(worksheets, vec![], tables);
// Reference cell is 'Sheet One'!F2 // Reference cell is 'Sheet One'!F2
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: "Sheet One".to_string(), sheet: "Sheet One".to_string(),
@@ -72,7 +72,7 @@ fn simple_table() {
}; };
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])"; let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
let t = parser.parse(formula, &Some(cell_reference.clone())); let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)"); assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
// Cell A3 // Cell A3
@@ -82,7 +82,7 @@ fn simple_table() {
column: 1, column: 1,
}; };
let formula = "SUBTOTAL(109, tblIncome[Jan])"; let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &Some(cell_reference.clone())); let t = parser.parse(formula, &cell_reference);
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)"); assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
// Cell A3 in 'Second Sheet' // Cell A3 in 'Second Sheet'
@@ -92,7 +92,7 @@ fn simple_table() {
column: 1, column: 1,
}; };
let formula = "SUBTOTAL(109, tblIncome[Jan])"; let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &Some(cell_reference.clone())); let t = parser.parse(formula, &cell_reference);
assert_eq!( assert_eq!(
to_string(&t, &cell_reference), to_string(&t, &cell_reference),
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)" "SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"

View File

@@ -263,7 +263,9 @@ pub(crate) fn forward_references(
// TODO: Not implemented // TODO: Not implemented
Node::ArrayKind(_) => {} Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {} // Do nothing. Note: we could do a blanket _ => {}
Node::VariableKind(_) => {} Node::DefinedNameKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {} Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {} Node::EmptyArgKind => {}

View File

@@ -247,10 +247,13 @@ impl Model {
return CalcResult::Number(cell.sheet as f64 + 1.0); return CalcResult::Number(cell.sheet as f64 + 1.0);
} }
// The arg could be a defined name or a table // The arg could be a defined name or a table
let arg = &args[0]; // let = &args[0];
if let Node::VariableKind(name) = arg { match &args[0] {
Node::DefinedNameKind((name, scope)) => {
// Let's see if it is a defined name // Let's see if it is a defined name
if let Some(defined_name) = self.parsed_defined_names.get(&(None, name.to_lowercase())) if let Some(defined_name) = self
.parsed_defined_names
.get(&(*scope, name.to_lowercase()))
{ {
match defined_name { match defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
@@ -261,13 +264,22 @@ impl Model {
} }
ParsedDefinedName::InvalidDefinedNameFormula => { ParsedDefinedName::InvalidDefinedNameFormula => {
return CalcResult::Error { return CalcResult::Error {
error: Error::NA, error: Error::ERROR,
origin: cell, origin: cell,
message: "Invalid name".to_string(), message: "Invalid name".to_string(),
}; };
} }
} }
} else {
// This should never happen
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid name".to_string(),
};
} }
}
Node::TableNameKind(name) => {
// Now let's see if it is a table // Now let's see if it is a table
for (table_name, table) in &self.workbook.tables { for (table_name, table) in &self.workbook.tables {
if table_name == name { if table_name == name {
@@ -279,6 +291,14 @@ impl Model {
} }
} }
} }
Node::WrongVariableKind(name) => {
return CalcResult::Error {
error: Error::NAME,
origin: cell,
message: format!("Name not found: {name}"),
}
}
arg => {
// Now it should be the name of a sheet // Now it should be the name of a sheet
let sheet_name = match self.get_string(arg, cell) { let sheet_name = match self.get_string(arg, cell) {
Ok(s) => s, Ok(s) => s,
@@ -287,6 +307,8 @@ impl Model {
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) { if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0); return CalcResult::Number(sheet_index as f64 + 1.0);
} }
}
}
CalcResult::Error { CalcResult::Error {
error: Error::NA, error: Error::NA,
origin: cell, origin: cell,

View File

@@ -66,91 +66,61 @@ impl Model {
} }
pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut true_count = 0; self.logical_nary(
for arg in args { args,
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(b) => {
if !b {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::Number(value) => {
if value == 0.0 {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::String(_value) => {
true_count += 1;
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell, cell,
"Ranges are in different sheets".to_string(), |acc, value| acc.unwrap_or(true) && value,
); Some(false),
} )
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Boolean(b) => {
if !b {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::Number(value) => {
if value == 0.0 {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::String(_value) => {
true_count += 1;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {}
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
};
}
if true_count == 0 {
return CalcResult::new_error(
Error::VALUE,
cell,
"Boolean values not found".to_string(),
);
}
CalcResult::Boolean(true)
} }
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.logical_nary(
args,
cell,
|acc, value| acc.unwrap_or(false) || value,
Some(true),
)
}
pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.logical_nary(args, cell, |acc, value| acc.unwrap_or(false) ^ value, None)
}
/// Base function for AND, OR, XOR. These are all n-ary functions that perform a boolean operation on a series of
/// boolean values. These boolean values are sourced from `args`. Note that there is not a 1-1 relationship between
/// arguments and boolean values evaluated (see how Ranges are handled for example).
///
/// Each argument in `args` is evaluated and the resulting value is interpreted as a boolean as follows:
/// - Boolean: The value is used directly.
/// - Number: 0 is FALSE, all other values are TRUE.
/// - Range: Each cell in the range is evaluated as if they were individual arguments with some caveats
/// - Empty arg: FALSE
/// - Empty cell & String: Ignored, behaves exactly like the argument wasn't passed in at all
/// - Error: Propagated
///
/// If no arguments are provided, or all arguments are ignored, the function returns a #VALUE! error
///
/// **`fold_fn`:** The function that combines the running result with the next value boolean value. The running result
/// starts as `None`.
///
/// **`short_circuit_value`:** If the running result reaches `short_circuit_value`, the function returns early.
fn logical_nary(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
fold_fn: fn(Option<bool>, bool) -> bool,
short_circuit_value: Option<bool>,
) -> CalcResult {
if args.is_empty() { if args.is_empty() {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
let mut result = false;
let mut result = None;
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(value) => result = value || result, CalcResult::Boolean(value) => result = Some(fold_fn(result, value)),
CalcResult::Number(value) => { CalcResult::Number(value) => result = Some(fold_fn(result, value != 0.0)),
if value != 0.0 {
return CalcResult::Boolean(true);
}
}
CalcResult::String(_value) => {
return CalcResult::Boolean(true);
}
CalcResult::Range { left, right } => { CalcResult::Range { left, right } => {
if left.sheet != right.sheet { if left.sheet != right.sheet {
return CalcResult::new_error( return CalcResult::new_error(
@@ -166,94 +136,58 @@ impl Model {
row, row,
column, column,
}) { }) {
CalcResult::Boolean(value) => { CalcResult::Boolean(value) => result = Some(fold_fn(result, value)),
result = value || result;
}
CalcResult::Number(value) => { CalcResult::Number(value) => {
if value != 0.0 { result = Some(fold_fn(result, value != 0.0))
return CalcResult::Boolean(true);
}
}
CalcResult::String(_value) => {
return CalcResult::Boolean(true);
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {} CalcResult::EmptyArg => {} // unreachable
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::Range { .. }
| CalcResult::String { .. }
| CalcResult::EmptyCell => {}
}
if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value)
{
if current_result == short_circuit_value {
return CalcResult::Boolean(current_result);
}
} }
} }
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::EmptyArg => result = Some(result.unwrap_or(false)),
}; // Strings are ignored unless they are "TRUE" or "FALSE" (case insensitive). EXCEPT if the string value
// comes from a reference, in which case it is always ignored regardless of its value.
CalcResult::String(..) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
if let Ok(f) = self.get_boolean(arg, cell) {
result = Some(fold_fn(result, f));
} }
CalcResult::Boolean(result) }
}
// References to empty cells are ignored. If all args are ignored the result is #VALUE!
CalcResult::EmptyCell => {}
} }
/// XOR(logical1, [logical]*,...) if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value)
/// Logical1 is required, subsequent logical values are optional. Can be logical values, arrays, or references. {
/// The result of XOR is TRUE when the number of TRUE inputs is odd and FALSE when the number of TRUE inputs is even. if current_result == short_circuit_value {
pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { return CalcResult::Boolean(current_result);
let mut true_count = 0; }
let mut false_count = 0; }
for arg in args { }
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(b) => { if let Some(result) = result {
if b { CalcResult::Boolean(result)
true_count += 1;
} else { } else {
false_count += 1; CalcResult::new_error(
}
}
CalcResult::Number(value) => {
if value != 0.0 {
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE, Error::VALUE,
cell, cell,
"Ranges are in different sheets".to_string(), "No logical values in argument list".to_string(),
); )
} }
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Boolean(b) => {
if b {
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Number(value) => {
if value != 0.0 {
true_count += 1;
} else {
false_count += 1;
}
}
_ => {}
}
}
}
}
_ => {}
};
}
if true_count == 0 && false_count == 0 {
return CalcResult::new_error(Error::VALUE, cell, "No booleans found".to_string());
}
CalcResult::Boolean(true_count % 2 == 1)
} }
/// =SWITCH(expression, case1, value1, [case, value]*, [default]) /// =SWITCH(expression, case1, value1, [case, value]*, [default])

View File

@@ -25,6 +25,8 @@
#![doc = include_str!("../examples/formulas_and_errors.rs")] #![doc = include_str!("../examples/formulas_and_errors.rs")]
//! ``` //! ```
#![warn(clippy::print_stdout)]
pub mod calc_result; pub mod calc_result;
pub mod cell; pub mod cell;
pub mod expressions; pub mod expressions;

View File

@@ -8,14 +8,15 @@ use crate::{
cell::CellValue, cell::CellValue,
constants::{self, LAST_COLUMN, LAST_ROW}, constants::{self, LAST_COLUMN, LAST_ROW},
expressions::{ expressions::{
lexer::LexerMode,
parser::{ parser::{
move_formula::{move_formula, MoveContext}, move_formula::{move_formula, MoveContext},
stringify::{to_rc_format, to_string}, stringify::{rename_defined_name_in_node, to_rc_format, to_string},
Node, Parser, Node, Parser,
}, },
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary}, token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*, types::*,
utils::{self, is_valid_column_number, is_valid_row}, utils::{self, is_valid_column_number, is_valid_identifier, is_valid_row},
}, },
formatter::{ formatter::{
format::{format_number, parse_formatted_number}, format::{format_number, parse_formatted_number},
@@ -72,6 +73,7 @@ pub(crate) enum CellState {
} }
/// A parsed formula for a defined name /// A parsed formula for a defined name
#[derive(Clone)]
pub(crate) enum ParsedDefinedName { pub(crate) enum ParsedDefinedName {
/// CellReference (`=C4`) /// CellReference (`=C4`)
CellReference(CellReferenceIndex), CellReference(CellReferenceIndex),
@@ -79,9 +81,6 @@ pub(crate) enum ParsedDefinedName {
RangeReference(Range), RangeReference(Range),
/// `=SomethingElse` /// `=SomethingElse`
InvalidDefinedNameFormula, InvalidDefinedNameFormula,
// TODO: Support constants in defined names
// TODO: Support formulas in defined names
// TODO: Support tables in defined names
} }
/// A dynamical IronCalc model. /// A dynamical IronCalc model.
@@ -417,38 +416,40 @@ impl Model {
// TODO: NOT IMPLEMENTED // TODO: NOT IMPLEMENTED
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string()) CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
} }
VariableKind(defined_name) => { DefinedNameKind((name, scope)) => {
let parsed_defined_name = self if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
.parsed_defined_names
.get(&(Some(cell.sheet), defined_name.to_lowercase())) // try getting local defined name
.or_else(|| {
self.parsed_defined_names
.get(&(None, defined_name.to_lowercase()))
}); // fallback to global
if let Some(parsed_defined_name) = parsed_defined_name {
match parsed_defined_name { match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
self.evaluate_cell(*reference) self.evaluate_cell(reference)
} }
ParsedDefinedName::RangeReference(range) => CalcResult::Range { ParsedDefinedName::RangeReference(range) => CalcResult::Range {
left: range.left, left: range.left,
right: range.right, right: range.right,
}, },
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NIMPL, Error::NAME,
cell, cell,
format!("Defined name \"{}\" is not a reference.", defined_name), format!("Defined name \"{}\" is not a reference.", name),
), ),
} }
} else { } else {
CalcResult::new_error( CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" not found.", defined_name), format!("Defined name \"{}\" not found.", name),
) )
} }
} }
TableNameKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("table name \"{}\" not supported.", s),
),
WrongVariableKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("Variable name \"{}\" not found.", s),
),
CompareKind { kind, left, right } => { CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell); let l = self.evaluate_node_in_context(left, cell);
if l.is_error() { if l.is_error() {
@@ -682,6 +683,13 @@ impl Model {
Err(format!("Invalid color: {}", color)) Err(format!("Invalid color: {}", color))
} }
/// Changes the visibility of a sheet
pub fn set_sheet_state(&mut self, sheet: u32, state: SheetState) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
worksheet.state = state;
Ok(())
}
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`) /// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> { pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?; let worksheet = self.workbook.worksheet_mut(sheet)?;
@@ -857,6 +865,11 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
// add all tables // add all tables
// let mut tables = Vec::new(); // let mut tables = Vec::new();
// for worksheet in worksheets { // for worksheet in worksheets {
@@ -866,7 +879,7 @@ impl Model {
// } // }
// tables.push(tables_in_sheet); // tables.push(tables_in_sheet);
// } // }
let parser = Parser::new(worksheet_names, workbook.tables.clone()); let parser = Parser::new(worksheet_names, defined_names, workbook.tables.clone());
let cells = HashMap::new(); let cells = HashMap::new();
let locale = get_locale(&workbook.settings.locale) let locale = get_locale(&workbook.settings.locale)
.map_err(|_| "Invalid locale".to_string())? .map_err(|_| "Invalid locale".to_string())?
@@ -1027,7 +1040,7 @@ impl Model {
column: source.column, column: source.column,
}; };
let formula_str = move_formula( let formula_str = move_formula(
&self.parser.parse(formula, &Some(cell_reference)), &self.parser.parse(formula, &cell_reference),
&MoveContext { &MoveContext {
source_sheet_name: &source_sheet_name, source_sheet_name: &source_sheet_name,
row: source.row, row: source.row,
@@ -1135,7 +1148,7 @@ impl Model {
row: source.row, row: source.row,
column: source.column, column: source.column,
}; };
let formula = &self.parser.parse(formula_str, &Some(cell_reference)); let formula = &self.parser.parse(formula_str, &cell_reference);
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: target_sheet_name, sheet: target_sheet_name,
row: target.row, row: target.row,
@@ -1511,13 +1524,11 @@ impl Model {
column, column,
}; };
let shared_formulas = &mut worksheet.shared_formulas; let shared_formulas = &mut worksheet.shared_formulas;
let mut parsed_formula = self.parser.parse(formula, &Some(cell_reference.clone())); let mut parsed_formula = self.parser.parse(formula, &cell_reference);
// If the formula fails to parse try adding a parenthesis // If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3) // SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula { if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference);
.parser
.parse(&format!("{})", formula), &Some(cell_reference));
match new_parsed_formula { match new_parsed_formula {
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula, _ => parsed_formula = new_parsed_formula,
@@ -1596,6 +1607,42 @@ impl Model {
.set_cell_with_number(row, column, value, style) .set_cell_with_number(row, column, value, style)
} }
// Helper function that returns a defined name given the name and scope
fn get_parsed_defined_name(
&self,
name: &str,
scope: Option<u32>,
) -> Result<Option<ParsedDefinedName>, String> {
let name_upper = name.to_uppercase();
for (key, df) in &self.parsed_defined_names {
if key.1.to_uppercase() == name_upper && key.0 == scope {
return Ok(Some(df.clone()));
}
}
Ok(None)
}
// Returns the formula for a defined name
pub(crate) fn get_defined_name_formula(
&self,
name: &str,
scope: Option<u32>,
) -> Result<String, String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Ok(df.formula.clone());
}
}
Err("Defined name not found".to_string())
}
/// Gets the Excel Value (Bool, Number, String) of a cell /// Gets the Excel Value (Bool, Number, String) of a cell
/// ///
/// See also: /// See also:
@@ -1986,6 +2033,137 @@ impl Model {
.worksheet_mut(sheet)? .worksheet_mut(sheet)?
.set_row_height(column, height) .set_row_height(column, height)
} }
/// Adds a new defined name
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), String> {
if !is_valid_identifier(name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
// if the defined name already exist return error
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Err("Defined name already exists".to_string());
}
}
self.workbook.defined_names.push(DefinedName {
name: name.to_string(),
formula: formula.to_string(),
sheet_id,
});
self.reset_parsed_structures();
Ok(())
}
/// Delete defined name of name and scope
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let mut index = None;
for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
index = Some(i);
}
}
if let Some(i) = index {
self.workbook.defined_names.remove(i);
self.reset_parsed_structures();
Ok(())
} else {
Err("Defined name not found".to_string())
}
}
/// Update defined name
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), String> {
if !is_valid_identifier(new_name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase();
let new_name_upper = new_name.to_uppercase();
if name_upper != new_name_upper || scope != new_scope {
for key in self.parsed_defined_names.keys() {
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
return Err("Defined name already exists".to_string());
}
}
}
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let new_sheet_id = match new_scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let mut index = None;
for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
index = Some(i);
}
}
if let Some(i) = index {
if let Some(df) = self.workbook.defined_names.get_mut(i) {
if new_name != df.name {
// We need to rename the name in every formula:
// Parse all formulas with the old name
// All internal formulas are R1C1
self.parser.set_lexer_mode(LexerMode::R1C1);
let worksheets = &mut self.workbook.worksheets;
for worksheet in worksheets {
let cell_reference = CellReferenceRC {
sheet: worksheet.get_name(),
row: 1,
column: 1,
};
let mut formulas = Vec::new();
for formula in &worksheet.shared_formulas {
let mut t = self.parser.parse(formula, &cell_reference);
rename_defined_name_in_node(&mut t, name, scope, new_name);
formulas.push(to_rc_format(&t));
}
worksheet.shared_formulas = formulas;
}
// Se the mode back to A1
self.parser.set_lexer_mode(LexerMode::A1);
}
df.name = new_name.to_string();
df.sheet_id = new_sheet_id;
df.formula = new_formula.to_string();
self.reset_parsed_structures();
}
Ok(())
} else {
Err("Defined name not found".to_string())
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -85,14 +85,14 @@ impl Model {
let worksheets = &self.workbook.worksheets; let worksheets = &self.workbook.worksheets;
for worksheet in worksheets { for worksheet in worksheets {
let shared_formulas = &worksheet.shared_formulas; let shared_formulas = &worksheet.shared_formulas;
let cell_reference = &Some(CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
column: 1, column: 1,
}); };
let mut parse_formula = Vec::new(); let mut parse_formula = Vec::new();
for formula in shared_formulas { for formula in shared_formulas {
let t = self.parser.parse(formula, cell_reference); let t = self.parser.parse(formula, &cell_reference);
parse_formula.push(t); parse_formula.push(t);
} }
self.parsed_formulas.push(parse_formula); self.parsed_formulas.push(parse_formula);
@@ -144,8 +144,14 @@ impl Model {
/// Reparses all formulas and defined names /// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) { pub(crate) fn reset_parsed_structures(&mut self) {
let defined_names = self
.workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
self.parser self.parser
.set_worksheets(self.workbook.get_worksheet_names()); .set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
self.parsed_formulas = vec![]; self.parsed_formulas = vec![];
self.parse_formulas(); self.parse_formulas();
self.parsed_defined_names = HashMap::new(); self.parsed_defined_names = HashMap::new();
@@ -262,11 +268,11 @@ impl Model {
// We use iter because the default would be a mut_iter and we don't need a mutable reference // 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; let worksheets = &mut self.workbook.worksheets;
for worksheet in worksheets { for worksheet in worksheets {
let cell_reference = &Some(CellReferenceRC { let cell_reference = &CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
column: 1, column: 1,
}); };
let mut formulas = Vec::new(); let mut formulas = Vec::new();
for formula in &worksheet.shared_formulas { for formula in &worksheet.shared_formulas {
let mut t = self.parser.parse(formula, cell_reference); let mut t = self.parser.parse(formula, cell_reference);
@@ -388,7 +394,7 @@ impl Model {
let parsed_formulas = Vec::new(); let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets; let worksheets = &workbook.worksheets;
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let parser = Parser::new(worksheet_names, HashMap::new()); let parser = Parser::new(worksheet_names, vec![], HashMap::new());
let cells = HashMap::new(); let cells = HashMap::new();
// FIXME: Add support for display languages // FIXME: Add support for display languages

View File

@@ -19,6 +19,7 @@ mod test_fn_formulatext;
mod test_fn_if; mod test_fn_if;
mod test_fn_maxifs; mod test_fn_maxifs;
mod test_fn_minifs; mod test_fn_minifs;
mod test_fn_or_xor;
mod test_fn_product; mod test_fn_product;
mod test_fn_rept; mod test_fn_rept;
mod test_fn_sum; mod test_fn_sum;
@@ -46,7 +47,6 @@ pub(crate) mod util;
mod engineering; mod engineering;
mod test_fn_offset; mod test_fn_offset;
mod test_fn_or;
mod test_number_format; mod test_number_format;
mod test_escape_quotes; mod test_escape_quotes;

View File

@@ -1,36 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 0)");
model._set("A2", "=OR(0, 0)");
model._set("A3", "=OR(true, false)");
model._set("A4", "=OR(false, false)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
}
#[test]
fn fn_or_no_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR()");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
#[test]
fn fn_or_missing_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR(,)");
model._set("A2", "=OR(,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
}

View File

@@ -0,0 +1,204 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::print_stdout)]
use crate::test::util::new_empty_model;
// These tests are grouped because in many cases XOR and OR have similar behaviour.
// Test specific to xor
#[test]
fn fn_xor() {
let mut model = new_empty_model();
model._set("A1", "=XOR(1, 1, 1, 0, 0)");
model._set("A2", "=XOR(1, 1, 0, 0, 0)");
model._set("A3", "=XOR(TRUE, TRUE, TRUE, FALSE, FALSE)");
model._set("A4", "=XOR(TRUE, TRUE, FALSE, FALSE, FALSE)");
model._set("A5", "=XOR(FALSE, FALSE, FALSE, FALSE, FALSE)");
model._set("A6", "=XOR(TRUE, TRUE)");
model._set("A7", "=XOR(0,0,0)");
model._set("A8", "=XOR(0,0,1)");
model._set("A9", "=XOR(0,1,0)");
model._set("A10", "=XOR(0,1,1)");
model._set("A11", "=XOR(1,0,0)");
model._set("A12", "=XOR(1,0,1)");
model._set("A13", "=XOR(1,1,0)");
model._set("A14", "=XOR(1,1,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"FALSE");
assert_eq!(model._get_text("A6"), *"FALSE");
assert_eq!(model._get_text("A7"), *"FALSE");
assert_eq!(model._get_text("A8"), *"TRUE");
assert_eq!(model._get_text("A9"), *"TRUE");
assert_eq!(model._get_text("A10"), *"FALSE");
assert_eq!(model._get_text("A11"), *"TRUE");
assert_eq!(model._get_text("A12"), *"FALSE");
assert_eq!(model._get_text("A13"), *"FALSE");
assert_eq!(model._get_text("A14"), *"TRUE");
}
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 1, 1, 0, 0)");
model._set("A2", "=OR(1, 1, 0, 0, 0)");
model._set("A3", "=OR(TRUE, TRUE, TRUE, FALSE, FALSE)");
model._set("A4", "=OR(TRUE, TRUE, FALSE, FALSE, FALSE)");
model._set("A5", "=OR(FALSE, FALSE, FALSE, FALSE, FALSE)");
model._set("A6", "=OR(TRUE, TRUE)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"TRUE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"TRUE");
assert_eq!(model._get_text("A5"), *"FALSE");
assert_eq!(model._get_text("A6"), *"TRUE");
}
#[test]
fn fn_or_xor() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
// Text args
model._set("A1", &format!(r#"={func}("")"#));
model._set("A2", &format!(r#"={func}("", "")"#));
model._set("A3", &format!(r#"={func}("", TRUE)"#));
model._set("A4", &format!(r#"={func}("", FALSE)"#));
model._set("A5", &format!("={func}(FALSE, TRUE)"));
model._set("A6", &format!("={func}(FALSE, FALSE)"));
model._set("A7", &format!("={func}(TRUE, FALSE)"));
// Reference to empty cell, plus true argument
model._set("A8", &format!("={func}(Z99, 1)"));
// Reference to empty cell/range
model._set("A9", &format!("={func}(Z99)"));
model._set("A10", &format!("={func}(X99:Z99"));
// Reference to cell with reference to empty range
model._set("B11", "=X99:Z99");
model._set("A11", &format!("={func}(B11)"));
// Reference to cell with non-empty range
model._set("X12", "1");
model._set("B12", "=X12:Z12");
model._set("A12", &format!("={func}(B12)"));
// Reference to text cell
model._set("B13", "some_text");
model._set("A13", &format!("={func}(B13)"));
model._set("A14", &format!("={func}(B13, 0)"));
model._set("A15", &format!("={func}(B13, 1)"));
// Reference to Implicit intersection
model._set("X16", "1");
model._set("B16", "=@X15:X16");
model._set("A16", &format!("={func}(B16)"));
// Non-empty range
model._set("B17", "1");
model._set("A17", &format!("={func}(B17:C17)"));
// Non-empty range with text
model._set("B18", "text");
model._set("A18", &format!("={func}(B18:C18)"));
// Non-empty range with text and number
model._set("B19", "text");
model._set("C19", "1");
model._set("A19", &format!("={func}(B19:C19)"));
// range with error
model._set("B20", "=1/0");
model._set("A20", &format!("={func}(B20:C20)"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
assert_eq!(model._get_text("A2"), *"#VALUE!");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"TRUE");
assert_eq!(model._get_text("A6"), *"FALSE");
assert_eq!(model._get_text("A7"), *"TRUE");
assert_eq!(model._get_text("A8"), *"TRUE");
assert_eq!(model._get_text("A9"), *"#VALUE!");
assert_eq!(model._get_text("A10"), *"#VALUE!");
assert_eq!(model._get_text("A11"), *"#VALUE!");
// TODO: This one depends on spill behaviour which isn't implemented yet
// assert_eq!(model._get_text("A12"), *"TRUE");
assert_eq!(model._get_text("A13"), *"#VALUE!");
assert_eq!(model._get_text("A14"), *"FALSE");
assert_eq!(model._get_text("A15"), *"TRUE");
// TODO: This one depends on @ implicit intersection behaviour which isn't implemented yet
// assert_eq!(model._get_text("A16"), *"TRUE");
assert_eq!(model._get_text("A17"), *"TRUE");
assert_eq!(model._get_text("A18"), *"#VALUE!");
assert_eq!(model._get_text("A19"), *"TRUE");
assert_eq!(model._get_text("A20"), *"#DIV/0!");
}
}
#[test]
fn fn_or_xor_no_arguments() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
model._set("A1", &format!("={}()", func));
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
}
#[test]
fn fn_or_xor_missing_arguments() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
model._set("A1", &format!("={func}(,)"));
model._set("A2", &format!("={func}(,1)"));
model._set("A3", &format!("={func}(1,)"));
model._set("A4", &format!("={func}(,B1)"));
model._set("A5", &format!("={func}(,B1:B4)"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"FALSE");
}
}

View File

@@ -3,6 +3,7 @@ mod test_autofill_columns;
mod test_autofill_rows; mod test_autofill_rows;
mod test_border; mod test_border;
mod test_clear_cells; mod test_clear_cells;
mod test_defined_names;
mod test_diff_queue; mod test_diff_queue;
mod test_evaluation; mod test_evaluation;
mod test_general; mod test_general;
@@ -14,6 +15,7 @@ mod test_on_paste_styles;
mod test_paste_csv; mod test_paste_csv;
mod test_rename_sheet; mod test_rename_sheet;
mod test_row_column; mod test_row_column;
mod test_sheet_state;
mod test_styles; mod test_styles;
mod test_to_from_bytes; mod test_to_from_bytes;
mod test_undo_redo; mod test_undo_redo;

View File

@@ -0,0 +1,398 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
#[test]
fn create_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
assert_eq!(
model.get_defined_name_list(),
vec![("myName".to_string(), None, "Sheet1!$A$1".to_string())]
);
// delete it
model.delete_defined_name("myName", None).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("#NAME?".to_string())
);
assert_eq!(model.get_defined_name_list().len(), 0);
model.undo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
}
#[test]
fn scopes() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
// Global
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap();
// Local to Sheet2
model.new_sheet().unwrap();
model.set_user_input(1, 2, 1, "145").unwrap();
model
.new_defined_name("myName", Some(1), "Sheet2!$A$2")
.unwrap();
model.set_user_input(1, 8, 8, "=myName").unwrap();
// Sheet 3
model.new_sheet().unwrap();
model.set_user_input(2, 2, 2, "=myName").unwrap();
// Global
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
assert_eq!(
model.get_formatted_cell_value(1, 8, 8),
Ok("145".to_string())
);
assert_eq!(
model.get_formatted_cell_value(2, 2, 2),
Ok("42".to_string())
);
}
#[test]
fn delete_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", Some(1), "Sheet1!$A$1")
.unwrap();
model
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " my world!")"#)
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
assert_eq!(
model.get_formatted_cell_value(1, 2, 1),
Ok("Hello my world!".to_string())
);
model.delete_sheet(0).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok(r#"=CONCATENATE(MyName," my world!")"#.to_string())
);
}
#[test]
fn change_scope() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", Some(1), "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
model
.update_defined_name("myName", Some(1), "myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
}
#[test]
fn rename_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
model
.update_defined_name("myName", None, "newName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok(r#"=CONCATENATE(newName," world!")"#.to_string())
);
}
#[test]
fn rename_defined_name_operations() {
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, "123").unwrap();
model
.new_defined_name("answer", None, "Sheet1!$A$1")
.unwrap();
model
.set_user_input(0, 2, 1, "=IF(answer<2, answer*2, answer^2)")
.unwrap();
model
.set_user_input(0, 3, 1, "=badDunction(-answer)")
.unwrap();
model.new_sheet().unwrap();
model.set_user_input(1, 1, 1, "78").unwrap();
model
.new_defined_name("answer", Some(1), "Sheet1!$A$1")
.unwrap();
model.set_user_input(1, 3, 1, "=answer").unwrap();
model
.update_defined_name("answer", None, "respuesta", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok("=IF(respuesta<2,respuesta*2,respuesta^2)".to_string())
);
assert_eq!(
model.get_cell_content(0, 3, 1),
Ok("=badDunction(-respuesta)".to_string())
);
// A defined name with the same name but different scope
assert_eq!(model.get_cell_content(1, 3, 1), Ok("=answer".to_string()));
}
#[test]
fn rename_defined_name_string_operations() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.set_user_input(0, 1, 2, "World").unwrap();
model
.new_defined_name("hello", None, "Sheet1!$A$1")
.unwrap();
model
.new_defined_name("world", None, "Sheet1!$B$1")
.unwrap();
model.set_user_input(0, 2, 1, "=hello&world").unwrap();
model
.update_defined_name("hello", None, "HolaS", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok("=HolaS&world".to_string())
);
}
#[test]
fn invalid_names() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
// spaces
assert_eq!(
model.new_defined_name("A real", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
// Starts with number
assert_eq!(
model.new_defined_name("2real", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
// Updating also fails
assert_eq!(
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
}
#[test]
fn already_existing() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
model
.new_defined_name("Another", None, "Sheet1!$A$1")
.unwrap();
// Can't create a new name with the same name
assert_eq!(
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
Err("Defined name already exists".to_string())
);
// Can't update one into an existing
assert_eq!(
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
Err("Defined name already exists".to_string())
);
}
#[test]
fn invalid_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
}
#[test]
fn invalid_formula() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.new_defined_name("MyName", None, "A1").unwrap();
model.set_user_input(0, 1, 2, "=MyName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("#NAME?".to_string())
);
}
#[test]
fn undo_redo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.set_user_input(0, 2, 1, "Hola").unwrap();
model.set_user_input(0, 1, 2, r#"=MyName&"!""#).unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 0);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("#NAME?".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model
.update_defined_name("MyName", None, "MyName", None, "Sheet1!$A$2")
.unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(model2.get_defined_name_list().len(), 1);
assert_eq!(
model2.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
}

View File

@@ -91,7 +91,6 @@ fn insert_remove_columns() {
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
// column E // column E
let column_width = model.get_column_width(0, 5).unwrap(); let column_width = model.get_column_width(0, 5).unwrap();
println!("{column_width}");
// Insert some data in row 5 (and change the style) in E1 // Insert some data in row 5 (and change the style) in E1
assert!(model.set_user_input(0, 1, 5, "100$").is_ok()); assert!(model.set_user_input(0, 1, 5, "100$").is_ok());

View File

@@ -0,0 +1,57 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_tests() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// add three more sheets
model.new_sheet().unwrap();
model.new_sheet().unwrap();
model.new_sheet().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.len(), 4);
for sheet in &info {
assert_eq!(sheet.state, "visible".to_string());
}
model.set_selected_sheet(2).unwrap();
assert_eq!(info.get(2).unwrap().name, "Sheet3".to_string());
model.hide_sheet(2).unwrap();
let info = model.get_worksheets_properties();
assert_eq!(model.get_selected_sheet(), 3);
assert_eq!(info.get(2).unwrap().state, "hidden".to_string());
model.undo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(2).unwrap().state, "visible".to_string());
model.redo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(2).unwrap().state, "hidden".to_string());
model.set_selected_sheet(3).unwrap();
model.hide_sheet(3).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
model.unhide_sheet(2).unwrap();
model.unhide_sheet(3).unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.len(), 4);
for sheet in &info {
assert_eq!(sheet.state, "visible".to_string());
}
model.undo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(3).unwrap().state, "hidden".to_string());
model.redo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(3).unwrap().state, "visible".to_string());
}

View File

@@ -293,7 +293,9 @@ impl Model {
Node::EmptyArgKind => None, Node::EmptyArgKind => None,
Node::InvalidFunctionKind { .. } => None, Node::InvalidFunctionKind { .. } => None,
Node::ArrayKind(_) => None, Node::ArrayKind(_) => None,
Node::VariableKind(_) => None, Node::DefinedNameKind(_) => None,
Node::TableNameKind(_) => None,
Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None, Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None, Node::OpPowerKind { .. } => None,
} }

View File

@@ -13,8 +13,8 @@ use crate::{
}, },
model::Model, model::Model,
types::{ types::{
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, Style, Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
VerticalAlignment, Style, VerticalAlignment,
}, },
utils::is_valid_hex_color, utils::is_valid_hex_color,
}; };
@@ -440,6 +440,48 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Hides sheet by index
///
/// See also:
/// * [Model::set_sheet_state]
/// * [UserModel::unhide_sheet]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), String> {
let sheet_count = self.model.workbook.worksheets.len() as u32;
for index in 1..sheet_count {
let sheet_index = (sheet + index) % sheet_count;
if self.model.workbook.worksheet(sheet_index)?.state == SheetState::Visible {
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
view.sheet = sheet_index;
};
break;
}
}
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
self.push_diff_list(vec![Diff::SetSheetState {
index: sheet,
new_value: SheetState::Hidden,
old_value,
}]);
self.model.set_sheet_state(sheet, SheetState::Hidden)?;
Ok(())
}
/// Un hides sheet by index
///
/// See also:
/// * [Model::set_sheet_state]
/// * [UserModel::hide_sheet]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), String> {
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
self.push_diff_list(vec![Diff::SetSheetState {
index: sheet,
new_value: SheetState::Visible,
old_value,
}]);
self.model.set_sheet_state(sheet, SheetState::Visible)?;
Ok(())
}
/// Sets sheet color /// Sets sheet color
/// ///
/// Note: an empty string will remove the color /// Note: an empty string will remove the color
@@ -1692,6 +1734,68 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Returns the list of defined names
pub fn get_defined_name_list(&self) -> Vec<(String, Option<u32>, String)> {
self.model.workbook.get_defined_names_with_scope()
}
/// Delete an existing defined name
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let old_value = self.model.get_defined_name_formula(name, scope)?;
let diff_list = vec![Diff::DeleteDefinedName {
name: name.to_string(),
scope,
old_value,
}];
self.push_diff_list(diff_list);
self.model.delete_defined_name(name, scope)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Create a new defined name
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), String> {
self.model.new_defined_name(name, scope, formula)?;
let diff_list = vec![Diff::CreateDefinedName {
name: name.to_string(),
scope,
value: formula.to_string(),
}];
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Updates a defined name
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), String> {
let old_formula = self.model.get_defined_name_formula(name, scope)?;
let diff_list = vec![Diff::UpdateDefinedName {
name: name.to_string(),
scope,
old_formula: old_formula.to_string(),
new_name: new_name.to_string(),
new_scope,
new_formula: new_formula.to_string(),
}];
self.push_diff_list(diff_list);
self.model
.update_defined_name(name, scope, new_name, new_scope, new_formula)?;
self.evaluate_if_not_paused();
Ok(())
}
// **** Private methods ****** // // **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) { fn push_diff_list(&mut self, diff_list: DiffList) {
@@ -1862,6 +1966,41 @@ impl UserModel {
} => { } => {
self.model.set_show_grid_lines(*sheet, *old_value)?; self.model.set_show_grid_lines(*sheet, *old_value)?;
} }
Diff::CreateDefinedName {
name,
scope,
value: _,
} => {
self.model.delete_defined_name(name, *scope)?;
}
Diff::DeleteDefinedName {
name,
scope,
old_value,
} => {
self.model.new_defined_name(name, *scope, old_value)?;
}
Diff::UpdateDefinedName {
name,
scope,
old_formula,
new_name,
new_scope,
new_formula: _,
} => {
self.model.update_defined_name(
new_name,
*new_scope,
name,
*scope,
old_formula,
)?;
}
Diff::SetSheetState {
index,
old_value,
new_value: _,
} => self.model.set_sheet_state(*index, old_value.clone())?,
} }
} }
if needs_evaluation { if needs_evaluation {
@@ -1989,6 +2128,33 @@ impl UserModel {
} => { } => {
self.model.set_show_grid_lines(*sheet, *new_value)?; self.model.set_show_grid_lines(*sheet, *new_value)?;
} }
Diff::CreateDefinedName { name, scope, value } => {
self.model.new_defined_name(name, *scope, value)?
}
Diff::DeleteDefinedName {
name,
scope,
old_value: _,
} => self.model.delete_defined_name(name, *scope)?,
Diff::UpdateDefinedName {
name,
scope,
old_formula: _,
new_name,
new_scope,
new_formula,
} => self.model.update_defined_name(
name,
*scope,
new_name,
*new_scope,
new_formula,
)?,
Diff::SetSheetState {
index,
old_value: _,
new_value,
} => self.model.set_sheet_state(*index, new_value.clone())?,
} }
} }

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use crate::types::{Cell, Col, Row, Style}; use crate::types::{Cell, Col, Row, SheetState, Style};
#[derive(Clone, Encode, Decode)] #[derive(Clone, Encode, Decode)]
pub(crate) struct RowData { pub(crate) struct RowData {
@@ -104,11 +104,35 @@ pub(crate) enum Diff {
old_value: String, old_value: String,
new_value: String, new_value: String,
}, },
SetSheetState {
index: u32,
old_value: SheetState,
new_value: SheetState,
},
SetShowGridLines { SetShowGridLines {
sheet: u32, sheet: u32,
old_value: bool, old_value: bool,
new_value: bool, new_value: bool,
}, // FIXME: we are missing SetViewDiffs },
CreateDefinedName {
name: String,
scope: Option<u32>,
value: String,
},
DeleteDefinedName {
name: String,
scope: Option<u32>,
old_value: String,
},
UpdateDefinedName {
name: String,
scope: Option<u32>,
old_formula: String,
new_name: String,
new_scope: Option<u32>,
new_formula: String,
},
// FIXME: we are missing SetViewDiffs
} }
pub(crate) type DiffList = Vec<Diff>; pub(crate) type DiffList = Vec<Diff>;

View File

@@ -27,4 +27,27 @@ impl Workbook {
.get_mut(worksheet_index as usize) .get_mut(worksheet_index as usize)
.ok_or_else(|| "Invalid sheet index".to_string()) .ok_or_else(|| "Invalid sheet index".to_string())
} }
/// Returns the a list of defined names in the workbook with their scope
pub(crate) fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self
.defined_names
.iter()
.map(|dn| {
let index = dn
.sheet_id
.and_then(|sheet_id| {
// returns an Option<usize>
sheet_id_index.iter().position(|&x| x == sheet_id)
})
// convert Option<usize> to Option<u32>
.map(|pos| pos as u32);
(dn.name.clone(), index, dn.formula.clone())
})
.collect::<Vec<_>>();
defined_names
}
} }

View File

@@ -187,6 +187,20 @@ paste_from_clipboard_types = r"""
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void; pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
""" """
defined_name_list = r"""
/**
* @returns {any}
*/
getDefinedNameList(): any;
"""
defined_name_list_types = r"""
/**
* @returns {DefinedName[]}
*/
getDefinedNameList(): DefinedName[];
"""
def fix_types(text): def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types) text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types) text = text.replace(update_style_str, update_style_str_types)
@@ -200,6 +214,7 @@ def fix_types(text):
text = text.replace(paste_csv_string, paste_csv_string_types) text = text.replace(paste_csv_string, paste_csv_string_types)
text = text.replace(clipboard, clipboard_types) text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types) text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
with open("types.ts") as f: with open("types.ts") as f:
types_str = f.read() types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str) header_types = "{}\n\n{}".format(header, types_str)

View File

@@ -1,3 +1,4 @@
use serde::Serialize;
use wasm_bindgen::{ use wasm_bindgen::{
prelude::{wasm_bindgen, JsError}, prelude::{wasm_bindgen, JsError},
JsValue, JsValue,
@@ -29,6 +30,13 @@ pub fn column_name_from_number(column: i32) -> Result<String, JsError> {
} }
} }
#[derive(Serialize)]
struct DefinedName {
name: String,
scope: Option<u32>,
formula: String,
}
#[wasm_bindgen] #[wasm_bindgen]
pub struct Model { pub struct Model {
model: BaseModel, model: BaseModel,
@@ -106,6 +114,16 @@ impl Model {
self.model.delete_sheet(sheet).map_err(to_js_error) self.model.delete_sheet(sheet).map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "hideSheet")]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.hide_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "unhideSheet")]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.unhide_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "renameSheet")] #[wasm_bindgen(js_name = "renameSheet")]
pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> { pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> {
self.model.rename_sheet(sheet, name).map_err(to_js_error) self.model.rename_sheet(sheet, name).map_err(to_js_error)
@@ -542,4 +560,52 @@ impl Model {
.paste_csv_string(&range, csv) .paste_csv_string(&range, csv)
.map_err(|e| to_js_error(e.to_string())) .map_err(|e| to_js_error(e.to_string()))
} }
#[wasm_bindgen(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
let data: Vec<DefinedName> = self
.model
.get_defined_name_list()
.iter()
.map(|s| DefinedName {
name: s.0.to_owned(),
scope: s.1,
formula: s.2.to_owned(),
})
.collect();
serde_wasm_bindgen::to_value(&data).map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "newDefinedName")]
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), JsError> {
self.model
.new_defined_name(name, scope, formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "updateDefinedName")]
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), JsError> {
self.model
.update_defined_name(name, scope, new_name, new_scope, new_formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "deleteDefinedName")]
pub fn delete_definedname(&mut self, name: &str, scope: Option<u32>) -> Result<(), JsError> {
self.model
.delete_defined_name(name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
} }

View File

@@ -113,6 +113,7 @@ export interface WorksheetProperties {
name: string; name: string;
color: string; color: string;
sheet_id: number; sheet_id: number;
state: string;
} }
interface CellStyleFill { interface CellStyleFill {
@@ -227,3 +228,9 @@ export interface Clipboard {
data: ClipboardData; data: ClipboardData;
range: [number, number, number, number]; range: [number, number, number, number];
} }
export interface DefinedName {
name: string;
scope?: number;
formula: string;
}

View File

@@ -58,6 +58,18 @@ export default defineConfig({
text: "Error Types", text: "Error Types",
link: "/features/error-types", link: "/features/error-types",
}, },
{
text: "Value Types",
link: "/features/value-types",
},
{
text: "Optional Arguments",
link: "/features/optional-arguments",
},
{
text: "Units",
link: "/features/units",
},
{ {
text: "Unsupported Features", text: "Unsupported Features",
link: "/features/unsupported-features", link: "/features/unsupported-features",

View File

@@ -7,31 +7,39 @@ lang: en-US
# Error Types # Error Types
::: warning ::: warning
**Note:** This page is in construction 🚧 **Note:** This draft page is under construction 🚧
::: :::
When working with formulas, you may encounter these common errors: The result of a formula is sometimes an _error_. In some situations those errors are expected and your formulas might be dealing with them.
The error `#N/A` might signal that there is no data to evaluate the formula yet. Maybe the payroll has not been introduced for that month just yet.
--- Some other errors like `#SPILL!`, `#CIRC!` or `#ERROR!` signal an error in your spreadsheet logic and must be corrected.
### **`#ERROR!`** The first kind of errors or 'common errors' are found in other spreadsheet engines like Excel while other errors like `#ERROR!` or `#N/IMPL` are particular to IronCalc.
**Cause:** General formula issue, like syntax errors or invalid references. ## Common Errors
**Fix:** Check the formula for mistakes or invalid cell references.
---
### **`#VALUE!`** ### **`#VALUE!`**
**Cause:** Mismatched data types (e.g., text used where numbers are expected). It might be caused by mismatched data types (e.g., text used where numbers are expected):
**Fix:** Ensure input types are correct; convert text to numbers if needed.
--- ```
=5+"two"
```
The engine doesn't know how to add the number `5` to the string `two` resulting in a `#VALUE!`.
It is an actual error in your spreadsheet. It indicates that the formula isnt working as intended.
### **`#DIV/0!`** ### **`#DIV/0!`**
**Cause:** Division by zero or an empty cell. Division by zero or an empty cell:
**Fix:** Ensure the denominator isnt zero or blank. Use `IF` to handle such cases:
```
=1/0
```
Usually this is an error. However, in cases where a denominator might be blank (e.g., data not yet filled in), this could be expected. Use `IFERROR` or `IF` to handle it:
``` ```
=IF(B1=0, "N/A", A1/B1) =IF(B1=0, "N/A", A1/B1)
@@ -39,23 +47,42 @@ When working with formulas, you may encounter these common errors:
### **`#NAME?`** ### **`#NAME?`**
**Cause:** Unrecognized text in the formula (e.g., misspelled function names or undefined named ranges). Found when a name is not recognized. Maybe a misspelled name for a function or a reference to a previously defined name that has since been deleted:
**Fix:** Correct spelling or define the missing name.
```
=UNKNOWN_FUNCTION(A1)
```
This indicates an error in your spreadsheet logic.
### **`#REF!`** ### **`#REF!`**
**Cause:** Invalid cell reference, often from deleting cells used in a formula. Indicates an invalid cell reference, often from deleting cells used in a formula.
**Fix:** Update the formula with correct references.
They can appear as a result of a computation or in a formula. Example:
```
=Sheet34!A1
```
If `Sheet34` doesn't exist it will return `#REF!`
This is a genuine error. It indicates that part of your formula references a cell or range that is missing.
### **`#NUM!`** ### **`#NUM!`**
**Cause:** Invalid numeric operation (e.g., calculating a square root of a negative number). Invalid numeric operation (e.g., calculating the square root of a negative number).
**Fix:** Adjust the formula to ensure valid numeric operations. Adjust the formula to ensure valid numeric operations.
Sometimes a `#NUM!` error might be expected, signalling to the user that some parameter is out of scope.
### **`#N/A`** ### **`#N/A`**
**Cause:** A value is not available, often in lookup functions like VLOOKUP. A value is not available, often in lookup functions like VLOOKUP.
**Fix:** Ensure the lookup value exists or use IFNA() to handle missing values:
This is frequently not an error in your spreadsheet logic.
You can produce a prettier answer using the [`IFNA`](/functions/information/isna) formula:
``` ```
=IFNA(VLOOKUP(A1, B1:C10, 2, FALSE), "Not Found") =IFNA(VLOOKUP(A1, B1:C10, 2, FALSE), "Not Found")
@@ -63,17 +90,62 @@ When working with formulas, you may encounter these common errors:
### **`#NULL!`** ### **`#NULL!`**
**Cause:** Incorrect range operator in a formula (e.g., missing a colon between cell references). Incorrect range operator in a formula (e.g., missing a colon between cell references).
**Fix:** Use correct range operators (e.g., A1:A10).
### **`#SPILL!`**
A cell in a formula will overwrite content in other cells.
This cannot happen right now in IronCalc as formulas don't spill yet.
### **`#CIRC!`** ### **`#CIRC!`**
**Cause:** Circular reference. Circular reference. This is an error in your spreadsheet and must be fixed.
**Fix:** Remove the circular reference. It means that during the course of a computation, a circular dependency was found.
A circular dependency is a dependency of a formula on itself.
For instance, in the cell `A1` the formula `=A1*2` is a circular dependency.
Other spreadsheet engines use circular dependencies to do "loop computations", run "sensitivity analysis" or "goal seek".
IronCalc doesn't support any of those at the moment.
## IronCalc specific errors
### **`#ERROR!`**
General formula issue, like syntax errors or invalid references.
In general, Excel does not let you enter incorrect formulas, but IronCalc will.
This will make your workbook imcompatible with Excel.
Typical examples might be an incomplete formula, such as `=A1+`, or a function call with too few arguments, such as `=FV(1,2)`.
### **`#N/IMPL!`**
A particular feature is not yet implemented in IronCalc
Check if there is a [Github](https://github.com/ironcalc) ticket or contact us via [email](mailto:hello@ironcalc.com) or [Discord](https://discord.com/invite/zZYWfh3RHJ).
## Error propagation
Some errors are created by some formulas. For instance, the function `SQRT` can create the error `#NUM!`, but can't ceate the error `#DIV/0`.
Once an error is created it is normally _propagated_ by all the formulas. So if cell `C3` evaluates to `#ERROR!`, then the formula
`=SQRT(C3)` will return `#ERROR!`.
Not all functions propagate errors in their arguments. For instance the function `IF(condition, if_true, if_false)` will only propagate an error in the `if_false` argument if the `condition` is `FALSE`. This is called _lazy evaluation_ - the function `IF` is _lazy_ because it only evaluates the arguments when needed. The opposite of lazy evaulaution is called _eager evaluation_.
Some functions also expect an error as an argument like [`ERROR.TYPE`](/functions/information/error.type) and will not propagate the error.
### **`#####`** ## See also
**Cause:** The column isnt wide enough to display the value. The following functions are convenient when working with errors
**Fix:** Resize the column width to fit the content.
- [`ISERR(ref)`](/functions/information/iserr), `TRUE` if `ref` is any error type except the `#N/A` error.
- [`ISERROR(ref)`](/functions/information/iserror), `TRUE` if `ref` is any error.
- [`ISNA(ref)`](/functions/information/isna), `TRUE` if ref is `#N/A`.
- [`ERROR.TYPE`](/functions/information/error.type) returns the numeric code for a given error.
- [`IFERROR(ref, value)`](/functions/logical/iferror) returns `value` if the content of `ref` is an error.
- [`IFNA(ref, value)`](/functions/logical/ifna) returns `value` only if the content of `ref` is the `#N/A` error.

View File

@@ -0,0 +1,49 @@
---
layout: doc
outline: deep
lang: en-US
---
# Optional Arguments
::: warning
**Note:** This draft page is under construction 🚧
:::
Any IronCalc function may accept zero, one or more arguments, which are values passed to the function when it is called from a spreadsheet formula.
Many function arguments are _required_. For such arguments, always pass a suitable value in the function call.
Some function arguments are _optional_. Optional arguments need not be passed in the function call and, in such cases, the function instead uses a predefined default value.
Consider a notional function called _FN\_NAME_, with the following syntax:
<p style="font-weight:bold;text-align:center;">FN_NAME(<span title="Number" style="color:#1E88E5">arg1</span>, <span title="Number" style="color:#1E88E5">arg2</span>, <span title="Number" style="color:#1E88E5">arg3</span>, <span title="Number" style="color:#1E88E5">arg4</span>=def1, <span title="Number" style="color:#1E88E5">arg5</span>=def2, <span title="Number" style="color:#1E88E5">arg6</span>=def3) => <span title="Number" style="color:#1E88E5" >fn_name</span></p>
Notes about this syntax:
* _FN_NAME_ is a function that takes six arguments (_arg1_, _arg2_, _arg3_, _arg4_, _arg5_ and _arg6_) and returns a value referred to as _fn_name_.
* For convenience in this case, all arguments and the returned value are colour-coded to indicate that they are numbers.
* Arguments _arg1_, _arg2_ and _arg3_ are <u>required</u> arguments and this would normally be stated in the **Argument descriptions** section of the function's description page.
* Arguments _arg4_, _arg5_ and _arg6_ are <u>optional</u> arguments and again this would normally be stated in the **Argument descriptions** section of the function's description page. In addition, optional arguments are usually indicated by the specification of a default value in the syntax.
* If _arg4_ is omitted, then the value _def1_ is assumed.
* If _arg5_ is omitted, then the value _def2_ is assumed.
* If _arg6_ is omitted, then the value _def3_ is assumed.
With this syntax, the following would all be valid calls to the _FN_NAME_ function:
**=FN\_NAME(1,2,3)**. All optional arguments omitted.
**=FN\_NAME(1,2,3,4)**. _arg4_ set to 4; optional arguments _arg5_ and _arg6_ assume default values.
**=FN\_NAME(1,2,3,,5)**. _arg5_ set to 5; optional arguments _arg4_ and _arg6_ assume default values.
**=FN\_NAME(1,2,3,,,6)**. _arg6_ set to 6; optional arguments _arg4_ and _arg5_ assume default values.
**=FN\_NAME(1,2,3,4,5)**. _arg4_ and _arg5_ set to 4 and 5 respectively; optional argument _arg6_ assumes default value.
**=FN\_NAME(1,2,3,4,,6)**. _arg4_ and _arg6_ set to 4 and 6 respectively; optional argument _arg5_ assumes default value.
**=FN\_NAME(1,2,3,,5,6)**. _arg5_ and _arg6_ set to 5 and 6 respectively; optional argument _arg4_ assumes default value.
**=FN\_NAME(1,2,3,4,5,6)**. Values passed for all optional arguments.

View File

@@ -0,0 +1,13 @@
---
layout: doc
outline: deep
lang: en-US
---
# Units
::: warning
**Note:** This draft page is under construction 🚧
:::
Some IronCalc functions return values that have units like currencies, percentage or dates.

View File

@@ -0,0 +1,69 @@
---
layout: doc
outline: deep
lang: en-US
---
# Value Types
::: warning
**Note:** This draft page is under construction 🚧
:::
In IronCalc a value, a result of a calculation, can be one of the following.
## Numbers
Numbers in IronCalc are [IEEE 754 double-precision](https://en.wikipedia.org/wiki/Double-precision_floating-point_format).
Numbers are only displayed up to 15 significant figures. That's why `=0.1+0.2` gives `0.3`.
Also, numbers are compared up to 15 significant figures. So `=IF(0.1+0.2=0.3, "Valid", "Invalid")` gives `Valid`.
However, `=0.3-0.2-0.1` will not give exactly `0` in IronCalc.
### Casting into numbers
Strings and booleans are sometimes converted to numbers:
`=1+"2"` => `3`
Some functions cast in weird ways:
`=SUM(1,TRUE)` => `1` and `=SUM(1,"1")` => `1`
And `=SUM(1,A1)` => `1` (where A1 contains `TRUE` or `"1"`)
Sometimes the conversion happens as might be expected. For example, `="123"+1` is `124`, `=SQRT("4")` is `2` and `=SQRT(TRUE)` is `1`.
Some functions, however, are more strict. For example, `=BIN2DEC(TRUE)` gives the #VALUE! error.
### Dates and times
IronCalc uses numbers to represent dates and times.
The integer part of the number represents the date, as a count of days since the fixed starting date of December 30, 1899.
The fractional part of the number represents the time of day. 0.0 corresponds to 00:00:00 (midnight) and 0.5 corresponds to 12:00:00 (noon).
## Strings
### Complex numbers
Using IronCalc, a complex number is a string of the form "1+j3".
## Booleans
### Casting from numbers
## Errors
### Casting from strings
"#N/A" => #N/A
## Arrays

View File

@@ -3,8 +3,10 @@ layout: doc
outline: deep outline: deep
lang: en-US lang: en-US
--- ---
# FV function # FV function
::: warning
**Note:** This draft page is under construction 🚧
:::
## Overview ## Overview
FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value. FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value.
@@ -13,34 +15,38 @@ FV can be used to calculate future value over a specified number of compounding
If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV. If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV.
## Usage ## Usage
### Syntax ### Syntax
**FV(rate, nper, pmt, pv, type)** **FV(<span title="Number" style="color:#1E88E5">rate</span>, <span title="Number" style="color:#1E88E5">nper</span>, <span title="Number" style="color:#1E88E5">pmt</span>, <span title="Number" style="color:#1E88E5">pv</span>=0, <span title="Boolean" style="color:#43A047">type</span>=FALSE) => <span title="Number" style="color:#1E88E5">fv</span>**
### Argument descriptions ### Argument descriptions
* *rate*. The fixed percentage interest rate or yield per period. * *rate* ([number](/features/value-types#numbers), required). The fixed percentage interest rate or yield per period.
* *nper*. The number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed. * *nper* ([number](/features/value-types#numbers), required). "nper" stands for <u>n</u>umber of <u>per</u>iods, in this case the number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
* *pmt*. The fixed amount paid or deposited each compounding period. * *pmt* ([number](/features/value-types#numbers), required). "pmt" stands for <u>p</u>ay<u>m</u>en<u>t</u>, in this case the fixed amount paid or deposited each compounding period.
* *pv* (optional). The present value or starting amount of the asset (default 0). * *pv* ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0).
* *type* (optional). A logical value indicating whether the payment due dates are at the end (0) of the compounding periods or at the beginning (any non-zero value). The default is 0 when omitted. * *type* ([boolean](/features/value-types/#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
### Additional guidance ### Additional guidance
* Make sure that the *rate* argument specifies the interest rate or yield applicable to the compounding period, based on the value chosen for *nper*. * Make sure that the *rate* argument specifies the interest rate or yield applicable to the compounding period, based on the value chosen for *nper*.
* The *pmt* and *pv* arguments should be expressed in the same currency unit. The value returned is expressed in the same currency unit. * The *pmt* and *pv* arguments should be expressed in the same currency unit.
* To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero. * To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero.
* The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument. * The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument.
<!--@include: ../markdown-snippets/error-type-details.md--> ### Returned value
FV returns a [number](/features/value-types/#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the *pmt* and *pv* arguments.
### Error conditions
* In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
* If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of any of the *rate*, *nper*, *pmt* or *pv* arguments is not (or cannot be converted to) a [number](/features/value-types#value), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the value of the *type* argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
* For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details ## Details
* If *rate* = 0, FV is given by the equation: * If $\text{type} \neq 0$, $\text{fv}$ is given by the equation:
$$ $$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$
FV = -pv - (pmt \times nper)
$$
* If *rate* <> 0 and *type* = 0, FV is given by the equation: * If $\text{type} = 0$, $\text{fv}$ is given by the equation:
$$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big)}{rate} $$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$
$$
* If *rate* <> 0 and *type* <> 0, FV is given by the equation: * For any $\text{type}$, in the special case of $\text{rate} = 0$, $\text{fv}$ is given by the equation:
$$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big) \times(1+rate)}{rate} $$ \text{fv} = -\text{pv} - (\text{pmt} \times \text{nper}) $$
$$
## Examples ## Examples
[See this example in IronCalc](https://app.ironcalc.com/?example=fv). [See some examples in IronCalc](https://app.ironcalc.com/?example=fv).
## Links ## Links
* For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page. * For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.

View File

@@ -1 +0,0 @@
* For information about the different types of errors that you may encounter when using IronCalc functions, visit our [Error Types](/features/error-types) page.

View File

@@ -0,0 +1 @@
* For more information about the different types of errors that you may encounter when using IronCalc functions, visit our [Error Types](/features/error-types) page.

View File

@@ -0,0 +1,185 @@
import styled from "@emotion/styled";
import { Trash2 } from "lucide-react";
import { useEffect, useRef } from "react";
interface DeleteWorkbookDialogProperties {
onClose: () => void;
onConfirm: () => void;
workbookName: string;
}
function DeleteWorkbookDialog(properties: DeleteWorkbookDialogProperties) {
const deleteButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(2px)";
}
if (deleteButtonRef.current) {
deleteButtonRef.current.focus();
}
return () => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "none";
}
};
}, []);
return (
<DialogWrapper
tabIndex={-1}
onKeyDown={(event) => {
if (event.code === "Escape") {
properties.onClose();
}
}}
role="dialog"
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<IconWrapper>
<Trash2 />
</IconWrapper>
<ContentWrapper>
<Title>Are you sure?</Title>
<Body>
The workbook <strong>'{properties.workbookName}'</strong> will be
permanently deleted. This action cannot be undone.
</Body>
<ButtonGroup>
<DeleteButton
onClick={() => {
properties.onConfirm();
properties.onClose();
}}
ref={deleteButtonRef}
>
Yes, delete workbook
</DeleteButton>
<CancelButton onClick={properties.onClose}>Cancel</CancelButton>
</ButtonGroup>
</ContentWrapper>
</DialogWrapper>
);
}
DeleteWorkbookDialog.displayName = "DeleteWorkbookDialog";
// some colors taken from the IronCalc palette
const COMMON_WHITE = "#FFF";
const COMMON_BLACK = "#272525";
const ERROR_MAIN = "#EB5757";
const ERROR_DARK = "#CB4C4C";
const GREY_200 = "#EEEEEE";
const GREY_300 = "#E0E0E0";
const GREY_700 = "#616161";
const GREY_900 = "#333333";
const PRIMARY_MAIN = "#F2994A";
const PRIMARY_DARK = "#D68742";
const DialogWrapper = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
border-radius: 8px;
box-shadow: 0px 1px 3px 0px ${COMMON_BLACK}1A;
width: 280px;
max-width: calc(100% - 40px);
z-index: 50;
font-family: "Inter", sans-serif;
`;
const IconWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
border-radius: 4px;
background-color: ${ERROR_MAIN}1A;
margin: 12px auto 0 auto;
color: ${ERROR_MAIN};
svg {
width: 16px;
height: 16px;
}
`;
const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 14px;
word-break: break-word;
`;
const Title = styled.h2`
margin: 0;
font-weight: 600;
font-size: inherit;
color: ${GREY_900};
`;
const Body = styled.p`
margin: 0;
text-align: center;
color: ${GREY_900};
font-size: 12px;
`;
const ButtonGroup = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
width: 100%;
`;
const Button = styled.button`
cursor: pointer;
color: ${COMMON_WHITE};
background-color: ${PRIMARY_MAIN};
padding: 0px 10px;
height: 36px;
border-radius: 4px;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
text-overflow: ellipsis;
transition: background-color 150ms;
&:hover {
background-color: ${PRIMARY_DARK};
}
`;
const DeleteButton = styled(Button)`
background-color: ${ERROR_MAIN};
color: ${COMMON_WHITE};
&:hover {
background-color: ${ERROR_DARK};
}
`;
const CancelButton = styled(Button)`
background-color: ${GREY_200};
color: ${GREY_700};
&:hover {
background-color: ${GREY_300};
}
`;
export default DeleteWorkbookDialog;

View File

@@ -2,7 +2,8 @@ import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material"; import { Menu, MenuItem, Modal } from "@mui/material";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react"; import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { UploadFileDialog } from "./UploadFileDialog"; import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
import UploadFileDialog from "./UploadFileDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage"; import { getModelsMetadata, getSelectedUuid } from "./storage";
export function FileMenu(props: { export function FileMenu(props: {
@@ -18,6 +19,7 @@ export function FileMenu(props: {
const models = getModelsMetadata(); const models = getModelsMetadata();
const uuids = Object.keys(models); const uuids = Object.keys(models);
const selectedUuid = getSelectedUuid(); const selectedUuid = getSelectedUuid();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const elements = []; const elements = [];
for (const uuid of uuids) { for (const uuid of uuids) {
@@ -88,16 +90,14 @@ export function FileMenu(props: {
Download (.xlsx) Download (.xlsx)
</MenuItemText> </MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuItemWrapper> <MenuItemWrapper
<StyledTrash />
<MenuItemText
onClick={() => { onClick={() => {
props.onDelete(); setDeleteDialogOpen(true);
setMenuOpen(false); setMenuOpen(false);
}} }}
> >
Delete workbook <StyledTrash />
</MenuItemText> <MenuItemText>Delete workbook</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuDivider /> <MenuDivider />
{elements} {elements}
@@ -105,10 +105,6 @@ export function FileMenu(props: {
<Modal <Modal
open={isImportMenuOpen} open={isImportMenuOpen}
onClose={() => { onClose={() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "";
}
setImportMenuOpen(false); setImportMenuOpen(false);
}} }}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
@@ -117,16 +113,26 @@ export function FileMenu(props: {
<> <>
<UploadFileDialog <UploadFileDialog
onClose={() => { onClose={() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "";
}
setImportMenuOpen(false); setImportMenuOpen(false);
}} }}
onModelUpload={props.onModelUpload} onModelUpload={props.onModelUpload}
/> />
</> </>
</Modal> </Modal>
<Modal
open={isDeleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<>
<DeleteWorkbookDialog
onClose={() => setDeleteDialogOpen(false)}
onConfirm={props.onDelete}
workbookName={selectedUuid ? models[selectedUuid] : ""}
/>
</>
</Modal>
</> </>
); );
} }

View File

@@ -1,17 +1,34 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { BookOpen, FileUp } from "lucide-react"; import { BookOpen, FileUp, X } from "lucide-react";
import { type DragEvent, useRef, useState } from "react"; import { type DragEvent, useEffect, useRef, useState } from "react";
export function UploadFileDialog(properties: { function UploadFileDialog(properties: {
onClose: () => void; onClose: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
}) { }) {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const crossRef = useRef<HTMLDivElement>(null);
const { onModelUpload } = properties; const { onModelUpload } = properties;
useEffect(() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(2px)";
}
if (crossRef.current) {
crossRef.current.focus();
}
return () => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "none";
}
};
}, []);
const handleClose = () => { const handleClose = () => {
properties.onClose(); properties.onClose();
}; };
@@ -79,12 +96,16 @@ export function UploadFileDialog(properties: {
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}; };
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(4px)";
}
return ( return (
<UploadDialog> <UploadDialog
tabIndex={-1}
role="dialog"
onKeyDown={(event) => {
if (event.code === "Escape") {
handleClose();
}
}}
>
<UploadTitle> <UploadTitle>
<span style={{ flexGrow: 2, marginLeft: 12 }}> <span style={{ flexGrow: 2, marginLeft: 12 }}>
Import an .xlsx file Import an .xlsx file
@@ -92,29 +113,11 @@ export function UploadFileDialog(properties: {
<Cross <Cross
style={{ marginRight: 12 }} style={{ marginRight: 12 }}
onClick={handleClose} onClick={handleClose}
onKeyDown={() => {}} title="Close Dialog"
ref={crossRef}
tabIndex={0}
> >
<svg <X />
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross> </Cross>
</UploadTitle> </UploadTitle>
{message === "" ? ( {message === "" ? (
@@ -215,6 +218,11 @@ const Cross = styled("div")`
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`; `;
const DocLink = styled("span")` const DocLink = styled("span")`
@@ -300,3 +308,5 @@ const DropZone = styled("div")`
gap: 16px; gap: 16px;
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
`; `;
export default UploadFileDialog;

View File

@@ -0,0 +1,430 @@
import type { DefinedName, Model } from "@ironcalc/wasm";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Stack,
styled,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Stack,
styled,
} from "@mui/material";
import { t } from "i18next";
import { BookOpen, Check, X } from "lucide-react";
import { useEffect, useState } from "react";
import NamedRange from "./NamedRange";
import { getFullRangeToString } from "./util";
type NameManagerDialogProperties = {
onClose: () => void;
open: boolean;
model: Model;
onClose: () => void;
open: boolean;
model: Model;
};
function NameManagerDialog(props: NameManagerDialogProperties) {
const [definedNamesLocal, setDefinedNamesLocal] = useState<DefinedName[]>();
const [definedName, setDefinedName] = useState<DefinedName>({
name: "",
scope: undefined,
formula: "",
});
// render definedNames from model
useEffect(() => {
if (props.open) {
const definedNamesModel = props.model.getDefinedNameList();
setDefinedNamesLocal(definedNamesModel);
console.log("definedNamesModel EFFECT", definedNamesModel);
}
}, [props.open]);
const handleSave = () => {
try {
console.log("SAVE", definedName);
props.model.newDefinedName(
definedName.name,
definedName.scope,
definedName.formula,
);
} catch (error) {
console.log("DefinedName save failed", error);
}
props.onClose();
};
const handleChange = (
field: keyof DefinedName,
value: string | number | undefined,
) => {
setDefinedName((prev: DefinedName) => ({
...prev,
[field]: value,
}));
};
const handleDelete = (name: string, scope: number | undefined) => {
try {
props.model.deleteDefinedName(name, scope);
} catch (error) {
console.log("DefinedName delete failed", error);
}
// should re-render modal
};
const handleUpdate = (
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => {
try {
// partially update?
props.model.updateDefinedName(name, scope, newName, newScope, newFormula);
} catch (error) {
console.log("DefinedName update failed", error);
}
};
const formatFormula = (): string => {
const worksheets = props.model.getWorksheetsProperties();
const selectedView = props.model.getSelectedView();
const formatFormula = (): string => {
const worksheets = props.model.getWorksheetsProperties();
const selectedView = props.model.getSelectedView();
return getFullRangeToString(selectedView, worksheets);
};
return getFullRangeToString(selectedView, worksheets);
};
return (
<StyledDialog
open={props.open}
onClose={props.onClose}
maxWidth={false}
scroll="paper"
>
<StyledDialogTitle>
Named Ranges
<IconButton onClick={() => props.onClose()}>
<X size={16} />
</IconButton>
</StyledDialogTitle>
<StyledDialogContent dividers>
<StyledRangesHeader>
<Box width="171px">{t("name_manager_dialog.name")}</Box>
<Box width="171px">{t("name_manager_dialog.range")}</Box>
<Box width="171px">{t("name_manager_dialog.scope")}</Box>
</StyledRangesHeader>
{definedNamesLocal?.map((definedName) => (
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
name={definedName.name}
scope={definedName.scope}
formula={definedName.formula}
key={definedName.name}
model={props.model}
onChange={handleChange}
onDelete={handleDelete}
canDelete={true}
/>
))}
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
formula={formatFormula()}
onChange={handleChange}
canDelete={false}
model={props.model}
/>
</StyledDialogContent>
<StyledDialogActions>
<Box display="flex" alignItems="center" gap={"8px"}>
<BookOpen color="grey" size={16} />
<span style={{ fontSize: "12px", fontFamily: "Inter" }}>
{t("name_manager_dialog.help")}
</span>
</Box>
<Box display="flex" gap="8px" width={"155px"}>
{/* change hover color? */}
<Button
onClick={() => props.onClose()}
variant="contained"
disableElevation
color="info"
sx={{
bgcolor: (theme): string => theme.palette.grey["200"],
color: (theme): string => theme.palette.grey["700"],
textTransform: "none",
}}
>
Cancel
</Button>
<Button
onClick={handleSave}
variant="contained"
disableElevation
sx={{ textTransform: "none" }}
startIcon={<Check size={16} />}
// disabled={} // disable when error
>
Save
</Button>
</Box>
</StyledDialogActions>
</StyledDialog>
);
}
const StyledDialog = styled(Dialog)(() => ({
"& .MuiPaper-root": {
height: "380px",
minWidth: "620px",
},
}));
const StyledDialogTitle = styled(DialogTitle)`
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
`;
const StyledDialogContent = styled(DialogContent)`
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 12px 20px 20px;
`;
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
flexDirection: "row",
gap: "12px",
fontFamily: theme.typography.fontFamily,
fontSize: "12px",
fontWeight: "700",
color: theme.palette.info.main,
}));
const StyledDialogActions = styled(DialogActions)`
padding: 12px 20px;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #757575;
`;
export default NameManagerDialog;
import type { DefinedName, Model } from "@ironcalc/wasm";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Stack,
styled,
} from "@mui/material";
import { t } from "i18next";
import { BookOpen, Check, X } from "lucide-react";
import { useEffect, useState } from "react";
import NamedRange from "./NamedRange";
import { getShortRangeToString } from "./util";
type NameManagerDialogProperties = {
onClose: () => void;
open: boolean;
model: Model;
};
function NameManagerDialog(props: NameManagerDialogProperties) {
const [definedNamesLocal, setDefinedNamesLocal] = useState<DefinedName[]>();
const [definedName, setDefinedName] = useState<DefinedName>({
name: "",
scope: 0,
formula: "",
});
// render named ranges from model
useEffect(() => {
const definedNamesModel = props.model.getDefinedNameList();
setDefinedNamesLocal(definedNamesModel);
console.log("definedNamesModel EFFECT", definedNamesModel);
});
const handleSave = () => {
console.log("SAVE DIALOG", definedName);
// create newDefinedName and close
// props.model.newDefinedName(
// definedName.name,
// definedName.scope,
// definedName.formula,
// );
props.onClose();
};
const handleChange = (field: keyof DefinedName, value: string | number) => {
setDefinedName((prev: DefinedName) => ({
...prev,
[field]: value,
}));
};
const handleDelete = () => {
console.log("definedName marked for deletion");
};
const formatFormula = (): string => {
const selectedView = props.model.getSelectedView();
return getShortRangeToString(selectedView);
};
return (
<StyledDialog
open={props.open}
onClose={props.onClose}
maxWidth={false}
scroll="paper"
>
<StyledDialogTitle>
Named Ranges
<IconButton onClick={() => props.onClose()}>
<X size={16} />
</IconButton>
</StyledDialogTitle>
<StyledDialogContent dividers>
<StyledRangesHeader>
<Box width="171px">{t("name_manager_dialog.name")}</Box>
<Box width="171px">{t("name_manager_dialog.range")}</Box>
<Box width="171px">{t("name_manager_dialog.scope")}</Box>
</StyledRangesHeader>
{definedNamesLocal?.map((definedName) => (
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
name={definedName.name}
scope={definedName.scope}
formula={definedName.formula}
key={definedName.name}
model={props.model}
onChange={handleChange}
onDelete={handleDelete}
/>
))}
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
formula={formatFormula()}
onChange={handleChange}
model={props.model}
/>
</StyledDialogContent>
<StyledDialogActions>
<Box display="flex" alignItems="center" gap={"8px"}>
<BookOpen color="grey" size={16} />
<span style={{ fontSize: "12px", fontFamily: "Inter" }}>
{t("name_manager_dialog.help")}
</span>
</Box>
<Box display="flex" gap="8px" width={"155px"}>
{/* change hover color? */}
<Button
onClick={() => props.onClose()}
variant="contained"
disableElevation
color="info"
sx={{
bgcolor: (theme): string => theme.palette.grey["200"],
color: (theme): string => theme.palette.grey["700"],
textTransform: "none",
}}
>
Cancel
</Button>
<Button
onClick={handleSave}
variant="contained"
disableElevation
sx={{ textTransform: "none" }}
startIcon={<Check size={16} />}
>
Save
</Button>
</Box>
</StyledDialogActions>
</StyledDialog>
);
}
const StyledDialog = styled(Dialog)(() => ({
"& .MuiPaper-root": {
height: "380px",
minWidth: "620px",
},
"& .MuiPaper-root": {
height: "380px",
minWidth: "620px",
},
}));
const StyledDialogTitle = styled(DialogTitle)`
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
`;
const StyledDialogContent = styled(DialogContent)`
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 12px 20px 20px;
`;
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
flexDirection: "row",
gap: "12px",
fontFamily: theme.typography.fontFamily,
fontSize: "12px",
fontWeight: "700",
color: theme.palette.info.main,
flexDirection: "row",
gap: "12px",
fontFamily: theme.typography.fontFamily,
fontSize: "12px",
fontWeight: "700",
color: theme.palette.info.main,
}));
const StyledDialogActions = styled(DialogActions)`
padding: 12px 20px;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #757575;
`;
export default NameManagerDialog;

View File

@@ -0,0 +1,168 @@
import type { DefinedName, Model, WorksheetProperties } from "@ironcalc/wasm";
import {
Box,
Divider,
IconButton,
MenuItem,
TextField,
styled,
Box,
Divider,
IconButton,
MenuItem,
TextField,
styled,
} from "@mui/material";
import { Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
type NamedRangeProperties = {
name?: string;
scope?: number;
formula: string;
model: Model;
worksheets: WorksheetProperties[];
onChange: (
field: keyof DefinedName,
value: string | number | undefined,
) => void;
onDelete?: (name: string, scope: number | undefined) => void;
canDelete: boolean;
onUpdate?: (
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => void;
};
function NamedRange(props: NamedRangeProperties) {
const [name, setName] = useState(props.name || "");
const [scope, setScope] = useState(props.scope || undefined);
const [formula, setFormula] = useState(props.formula);
const [nameError, setNameError] = useState(false);
const [formulaError, setFormulaError] = useState(false);
const handleChange = (
field: keyof DefinedName,
value: string | number | undefined,
) => {
if (field === "name") {
setName(value as string);
props.onChange("name", value);
}
if (field === "scope") {
setScope(value as number | undefined);
props.onChange("scope", value);
}
if (field === "formula") {
setFormula(value as string);
props.onChange("formula", value);
}
};
useEffect(() => {
// send initial formula value to parent
handleChange("formula", formula);
}, []);
const handleDelete = () => {
props.onDelete?.(name, scope);
};
return (
<>
<StyledBox>
<StyledTextField
id="name"
variant="outlined"
size="small"
margin="none"
fullWidth
error={nameError}
value={name}
onChange={(event) => handleChange("name", event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
/>
<StyledTextField
id="scope"
variant="outlined"
select
size="small"
margin="none"
fullWidth
value={scope ?? "Workbook (Global)"}
onChange={(event) =>
handleChange(
"scope",
event.target.value === "Workbook (Global)"
? undefined
: event.target.value,
)
}
>
<MenuItem value="Workbook (Global)">Workbook (Global)</MenuItem>
{props.worksheets.map((option, index) => (
<MenuItem key={option.sheet_id} value={index}>
{option.name}
</MenuItem>
))}
</StyledTextField>
<StyledTextField
id="formula"
variant="outlined"
size="small"
margin="none"
fullWidth
error={formulaError}
value={formula}
onChange={(event) => handleChange("formula", event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
/>
<StyledIconButton onClick={handleDelete} disabled={!props.canDelete}>
<Trash2 size={12} />
</StyledIconButton>
</StyledBox>
<Divider />
</>
);
}
const StyledBox = styled(Box)`
display: flex;
gap: 12px;
width: 577px;
`;
const StyledTextField = styled(TextField)(() => ({
"& .MuiInputBase-root": {
height: "28px",
margin: 0,
},
"& .MuiInputBase-root": {
height: "28px",
margin: 0,
},
}));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
color: theme.palette.error.main,
"&.Mui-disabled": {
opacity: 0.6,
color: theme.palette.error.light,
},
color: theme.palette.error.main,
"&.Mui-disabled": {
opacity: 0.6,
color: theme.palette.error.light,
},
}));
export default NamedRange;

View File

@@ -59,7 +59,10 @@ const SheetListMenu = (properties: SheetListMenuProps) => {
)} )}
{hasColors && <ItemColor style={{ backgroundColor: tab.color }} />} {hasColors && <ItemColor style={{ backgroundColor: tab.color }} />}
<ItemName <ItemName
style={{ fontWeight: index === selectedIndex ? "bold" : "normal" }} style={{
fontWeight: index === selectedIndex ? "bold" : "normal",
color: tab.state === "visible" ? "#333" : "#888",
}}
> >
{tab.name} {tab.name}
</ItemName> </ItemName>

View File

@@ -1,4 +1,5 @@
import { Dialog, TextField, styled } from "@mui/material"; import { Dialog, TextField, styled } from "@mui/material";
import { Check, X } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { theme } from "../../theme"; import { theme } from "../../theme";
@@ -20,28 +21,13 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
<Dialog open={properties.open} onClose={properties.onClose}> <Dialog open={properties.open} onClose={properties.onClose}>
<StyledDialogTitle> <StyledDialogTitle>
{t("sheet_rename.title")} {t("sheet_rename.title")}
<Cross onClick={handleClose} onKeyDown={() => {}}> <Cross
<svg onClick={handleClose}
width="16" title={t("sheet_rename.close")}
height="16" tabIndex={-1}
viewBox="0 0 16 16" onKeyDown={() => {}}
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<title>Close</title> <X />
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross> </Cross>
</StyledDialogTitle> </StyledDialogTitle>
<StyledDialogContent> <StyledDialogContent>
@@ -54,6 +40,8 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
if (event.key === "Enter") { if (event.key === "Enter") {
properties.onNameChanged(name); properties.onNameChanged(name);
properties.onClose(); properties.onClose();
} else if (event.key === "Escape") {
properties.onClose();
} }
}} }}
onChange={(event) => { onChange={(event) => {
@@ -61,6 +49,8 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
}} }}
spellCheck="false" spellCheck="false"
onPaste={(event) => event.stopPropagation()} onPaste={(event) => event.stopPropagation()}
onCopy={(event) => event.stopPropagation()}
onCut={(event) => event.stopPropagation()}
/> />
</StyledDialogContent> </StyledDialogContent>
<DialogFooter> <DialogFooter>
@@ -69,6 +59,9 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
properties.onNameChanged(name); properties.onNameChanged(name);
}} }}
> >
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
{t("sheet_rename.rename")} {t("sheet_rename.rename")}
</StyledButton> </StyledButton>
</DialogFooter> </DialogFooter>
@@ -90,7 +83,7 @@ const StyledDialogTitle = styled("div")`
const Cross = styled("div")` const Cross = styled("div")`
&:hover { &:hover {
background-color: ${theme.palette.grey["100"]}; background-color: ${theme.palette.grey["50"]};
} }
display: flex; display: flex;
border-radius: 4px; border-radius: 4px;
@@ -99,6 +92,11 @@ const Cross = styled("div")`
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`; `;
const StyledDialogContent = styled("div")` const StyledDialogContent = styled("div")`

View File

@@ -1,4 +1,5 @@
import { Button, Menu, MenuItem, styled } from "@mui/material"; import { Button, Menu, MenuItem, styled } from "@mui/material";
import type { MenuItemProps } from "@mui/material";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { theme } from "../../theme"; import { theme } from "../../theme";
@@ -14,7 +15,9 @@ interface SheetTabProps {
onSelected: () => void; onSelected: () => void;
onColorChanged: (hex: string) => void; onColorChanged: (hex: string) => void;
onRenamed: (name: string) => void; onRenamed: (name: string) => void;
canDelete: boolean;
onDeleted: () => void; onDeleted: () => void;
onHideSheet: () => void;
workbookState: WorkbookState; workbookState: WorkbookState;
} }
@@ -92,14 +95,23 @@ function SheetTab(props: SheetTabProps) {
Change Color Change Color
</StyledMenuItem> </StyledMenuItem>
<StyledMenuItem <StyledMenuItem
disabled={!props.canDelete}
onClick={() => { onClick={() => {
props.onDeleted(); props.onDeleted();
handleClose(); handleClose();
}} }}
> >
{" "}
Delete Delete
</StyledMenuItem> </StyledMenuItem>
<StyledMenuItem
disabled={!props.canDelete}
onClick={() => {
props.onHideSheet();
handleClose();
}}
>
Hide sheet
</StyledMenuItem>
</StyledMenu> </StyledMenu>
<SheetRenameDialog <SheetRenameDialog
open={renameDialogOpen} open={renameDialogOpen}
@@ -137,16 +149,19 @@ const StyledMenu = styled(Menu)`
} }
`; `;
const StyledMenuItem = styled(MenuItem)` const StyledMenuItem = styled(MenuItem)<MenuItemProps>(() => ({
display: flex; display: "flex",
justify-content: space-between; justifyContent: "space-between",
font-size: 12px; fontSize: "12px",
width: calc(100% - 8px); width: "calc(100% - 8px)",
margin: 0px 4px; margin: "0px 4px",
border-radius: 4px; borderRadius: "4px",
padding: 8px; padding: "8px",
height: 32px; height: "32px",
`; "&:disabled": {
color: "#BDBDBD",
},
}));
const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>` const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
display: flex; display: flex;

View File

@@ -19,6 +19,7 @@ export interface SheetTabBarProps {
onSheetColorChanged: (hex: string) => void; onSheetColorChanged: (hex: string) => void;
onSheetRenamed: (name: string) => void; onSheetRenamed: (name: string) => void;
onSheetDeleted: () => void; onSheetDeleted: () => void;
onHideSheet: () => void;
} }
function SheetTabBar(props: SheetTabBarProps) { function SheetTabBar(props: SheetTabBarProps) {
@@ -33,6 +34,18 @@ function SheetTabBar(props: SheetTabBarProps) {
setAnchorEl(null); setAnchorEl(null);
}; };
const nonHidenSheets = sheets
.map((s, index) => {
return {
state: s.state,
index,
name: s.name,
color: s.color,
sheetId: s.sheetId,
};
})
.filter((s) => s.state === "visible");
return ( return (
<Container> <Container>
<LeftButtonsContainer> <LeftButtonsContainer>
@@ -54,22 +67,24 @@ function SheetTabBar(props: SheetTabBarProps) {
<VerticalDivider /> <VerticalDivider />
<Sheets> <Sheets>
<SheetInner> <SheetInner>
{sheets.map((tab, index) => ( {nonHidenSheets.map((tab) => (
<SheetTab <SheetTab
key={tab.sheetId} key={tab.sheetId}
name={tab.name} name={tab.name}
color={tab.color} color={tab.color}
selected={index === selectedIndex} selected={tab.index === selectedIndex}
onSelected={() => onSheetSelected(index)} onSelected={() => onSheetSelected(tab.index)}
onColorChanged={(hex: string): void => { onColorChanged={(hex: string): void => {
props.onSheetColorChanged(hex); props.onSheetColorChanged(hex);
}} }}
onRenamed={(name: string): void => { onRenamed={(name: string): void => {
props.onSheetRenamed(name); props.onSheetRenamed(name);
}} }}
canDelete={nonHidenSheets.length > 1}
onDeleted={(): void => { onDeleted={(): void => {
props.onSheetDeleted(); props.onSheetDeleted();
}} }}
onHideSheet={props.onHideSheet}
workbookState={workbookState} workbookState={workbookState}
/> />
))} ))}

View File

@@ -2,4 +2,5 @@ export interface SheetOptions {
name: string; name: string;
color: string; color: string;
sheetId: number; sheetId: number;
state: string;
} }

View File

@@ -5,10 +5,10 @@ export const headerGlobalSelectorColor = "#EAECF4";
export const headerSelectedBackground = "#EEEEEE"; export const headerSelectedBackground = "#EEEEEE";
export const headerFullSelectedBackground = "#D3D6E9"; export const headerFullSelectedBackground = "#D3D6E9";
export const headerSelectedColor = "#333"; export const headerSelectedColor = "#333";
export const headerBorderColor = "#DEE0EF"; export const headerBorderColor = "#E0E0E0";
export const gridColor = "#E0E0E0"; export const gridColor = "#E0E0E0";
export const gridSeparatorColor = "#D3D6E9"; export const gridSeparatorColor = "#E0E0E0";
export const defaultTextColor = "#2E414D"; export const defaultTextColor = "#2E414D";
export const outlineColor = "#F2994A"; export const outlineColor = "#F2994A";

View File

@@ -45,8 +45,8 @@ const ColorPicker = (properties: ColorPickerProps) => {
"#3BB68A", "#3BB68A",
"#8CB354", "#8CB354",
"#F8CD3C", "#F8CD3C",
"#F2994A",
"#EC5753", "#EC5753",
"#A23C52",
"#D03627", "#D03627",
"#523E93", "#523E93",
"#3358B7", "#3358B7",

View File

@@ -1,6 +1,6 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { Dialog, TextField } from "@mui/material"; import { Dialog, TextField } from "@mui/material";
import { Check } from "lucide-react"; import { Check, X } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { theme } from "../theme"; import { theme } from "../theme";
@@ -36,28 +36,8 @@ const FormatPicker = (properties: FormatPickerProps) => {
> >
<StyledDialogTitle> <StyledDialogTitle>
{t("num_fmt.title")} {t("num_fmt.title")}
<Cross onClick={handleClose} onKeyDown={() => {}}> <Cross onClick={handleClose} title={t("num_fmt.close")}>
<svg <X />
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross> </Cross>
</StyledDialogTitle> </StyledDialogTitle>
@@ -101,7 +81,7 @@ const StyledDialogTitle = styled("div")`
const Cross = styled("div")` const Cross = styled("div")`
&:hover { &:hover {
background-color: ${theme.palette.grey["100"]}; background-color: ${theme.palette.grey["50"]};
} }
display: flex; display: flex;
border-radius: 4px; border-radius: 4px;
@@ -110,6 +90,11 @@ const Cross = styled("div")`
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`; `;
const StyledDialogContent = styled("div")` const StyledDialogContent = styled("div")`

View File

@@ -1,6 +1,7 @@
import type { import type {
BorderOptions, BorderOptions,
HorizontalAlignment, HorizontalAlignment,
Model,
VerticalAlignment, VerticalAlignment,
} from "@ironcalc/wasm"; } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
@@ -22,6 +23,7 @@ import {
Percent, Percent,
Redo2, Redo2,
Strikethrough, Strikethrough,
Tags,
Type, Type,
Underline, Underline,
Undo2, Undo2,
@@ -34,6 +36,8 @@ import {
DecimalPlacesIncreaseIcon, DecimalPlacesIncreaseIcon,
} from "../icons"; } from "../icons";
import { theme } from "../theme"; import { theme } from "../theme";
import NameManagerDialog from "./NameManagerDialog";
import NameManagerDialog from "./NameManagerDialog";
import BorderPicker from "./borderPicker"; import BorderPicker from "./borderPicker";
import ColorPicker from "./colorPicker"; import ColorPicker from "./colorPicker";
import { TOOLBAR_HEIGHT } from "./constants"; import { TOOLBAR_HEIGHT } from "./constants";
@@ -72,12 +76,14 @@ type ToolbarProperties = {
numFmt: string; numFmt: string;
showGridLines: boolean; showGridLines: boolean;
onToggleShowGridLines: (show: boolean) => void; onToggleShowGridLines: (show: boolean) => void;
model: Model;
}; };
function Toolbar(properties: ToolbarProperties) { function Toolbar(properties: ToolbarProperties) {
const [fontColorPickerOpen, setFontColorPickerOpen] = useState(false); const [fontColorPickerOpen, setFontColorPickerOpen] = useState(false);
const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false); const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false);
const [borderPickerOpen, setBorderPickerOpen] = useState(false); const [borderPickerOpen, setBorderPickerOpen] = useState(false);
const [nameManagerDialogOpen, setNameManagerDialogOpen] = useState(false);
const fontColorButton = useRef(null); const fontColorButton = useRef(null);
const fillColorButton = useRef(null); const fillColorButton = useRef(null);
@@ -340,6 +346,18 @@ function Toolbar(properties: ToolbarProperties) {
> >
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />} {properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
</StyledButton> </StyledButton>
<Divider />
<StyledButton
type="button"
$pressed={false}
onClick={() => {
setNameManagerDialogOpen(true);
}}
disabled={!canEdit}
title={t("toolbar.name_manager")}
>
<Tags />
</StyledButton>
<ColorPicker <ColorPicker
color={properties.fontColor} color={properties.fontColor}
@@ -375,6 +393,13 @@ function Toolbar(properties: ToolbarProperties) {
anchorEl={borderButton} anchorEl={borderButton}
open={borderPickerOpen} open={borderPickerOpen}
/> />
<NameManagerDialog
open={nameManagerDialogOpen}
onClose={() => {
setNameManagerDialogOpen(false);
}}
model={properties.model}
/>
</ToolbarContainer> </ToolbarContainer>
); );
} }

View File

@@ -1,6 +1,10 @@
import type { Area, Cell } from "./types"; import type { Area, Cell } from "./types";
import { columnNameFromNumber } from "@ironcalc/wasm"; import {
type SelectedView,
type WorksheetProperties,
columnNameFromNumber,
} from "@ironcalc/wasm";
/** /**
* Returns true if the keypress should start editing * Returns true if the keypress should start editing
@@ -57,7 +61,24 @@ export function rangeToStr(
if (rowStart === rowEnd && columnStart === columnEnd) { if (rowStart === rowEnd && columnStart === columnEnd) {
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`; return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
} }
return `${sheetName}${columnNameFromNumber( return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber(
columnStart, columnEnd,
)}${rowStart}:${columnNameFromNumber(columnEnd)}${rowEnd}`; )}${rowEnd}`;
}
export function getFullRangeToString(
selectedView: SelectedView,
worksheets: WorksheetProperties[], // solo pasar names
): string {
// order of values is confusing compared to rangeToStr range type, needs refactoring for consistency
const [rowStart, columnStart, rowEnd, columnEnd] = selectedView.range;
const sheetNames = worksheets.map((s) => s.name);
const sheetName = `${sheetNames[selectedView.sheet]}!`;
if (rowStart === rowEnd && columnStart === columnEnd) {
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
}
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber(
columnEnd,
)}${rowEnd}`;
} }

View File

@@ -5,7 +5,9 @@ import type {
WorksheetProperties, WorksheetProperties,
} from "@ironcalc/wasm"; } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { PaintRoller } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import ReactDOMServer from "react-dom/server";
import SheetTabBar from "./SheetTabBar/SheetTabBar"; import SheetTabBar from "./SheetTabBar/SheetTabBar";
import { import {
COLUMN_WIDTH_SCALE, COLUMN_WIDTH_SCALE,
@@ -32,8 +34,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const setRedrawId = useState(0)[1]; const setRedrawId = useState(0)[1];
const info = model const info = model
.getWorksheetsProperties() .getWorksheetsProperties()
.map(({ name, color, sheet_id }: WorksheetProperties) => { .map(({ name, color, sheet_id, state }: WorksheetProperties) => {
return { name, color: color ? color : "#FFF", sheetId: sheet_id }; return { name, color: color ? color : "#FFF", sheetId: sheet_id, state };
}); });
const focusWorkbook = useCallback(() => { const focusWorkbook = useCallback(() => {
if (rootRef.current) { if (rootRef.current) {
@@ -113,6 +115,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("num_fmt", numberFmt); updateRangeStyle("num_fmt", numberFmt);
}; };
const onNamedRangesUpdate = () => {
// update named ranges in model
};
const onCopyStyles = () => { const onCopyStyles = () => {
const { const {
sheet, sheet,
@@ -136,7 +142,15 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const el = rootRef.current?.getElementsByClassName("sheet-container")[0]; const el = rootRef.current?.getElementsByClassName("sheet-container")[0];
if (el) { if (el) {
(el as HTMLElement).style.cursor = (el as HTMLElement).style.cursor =
`url('data:image/svg+xml;utf8,<svg data-v-56bd7dfc="" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paintbrush-vertical"><path d="M10 2v2"></path><path d="M14 2v4"></path><path d="M17 2a1 1 0 0 1 1 1v9H6V3a1 1 0 0 1 1-1z"></path><path d="M6 12a1 1 0 0 0-1 1v1a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v2.9a2 2 0 1 0 4 0V17a1 1 0 0 1 1-1h2a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1"></path></svg>'), auto`; `url('data:image/svg+xml;utf8,${encodeURIComponent(
ReactDOMServer.renderToString(
<PaintRoller
width={24}
height={24}
style={{ transform: "rotate(-8deg)" }}
/>,
),
)}'), auto`;
} }
}; };
@@ -559,6 +573,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.setShowGridLines(sheet, show); model.setShowGridLines(sheet, show);
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
}} }}
model={model}
/> />
<FormulaBar <FormulaBar
cellAddress={cellAddress()} cellAddress={cellAddress()}
@@ -586,6 +601,9 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
selectedIndex={model.getSelectedSheet()} selectedIndex={model.getSelectedSheet()}
workbookState={workbookState} workbookState={workbookState}
onSheetSelected={(sheet: number): void => { onSheetSelected={(sheet: number): void => {
if (info[sheet].state !== "visible") {
model.unhideSheet(sheet);
}
model.setSelectedSheet(sheet); model.setSelectedSheet(sheet);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
}} }}
@@ -616,6 +634,11 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.deleteSheet(selectedSheet); model.deleteSheet(selectedSheet);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
}} }}
onHideSheet={(): void => {
const selectedSheet = model.getSelectedSheet();
model.hideSheet(selectedSheet);
setRedrawId((value) => value + 1);
}}
/> />
</Container> </Container>
); );

View File

@@ -18,6 +18,8 @@
"decimal_places_increase": "Increase decimal places", "decimal_places_increase": "Increase decimal places",
"decimal_places_decrease": "Decrease decimal places", "decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines", "show_hide_grid_lines": "Show/hide grid lines",
"name_manager": "Name manager",
"name_manager": "Name manager",
"vertical_align_bottom": "Align bottom", "vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle", "vertical_align_middle": " Align middle",
"vertical_align_top": "Align top", "vertical_align_top": "Align top",
@@ -58,12 +60,14 @@
"num_fmt": { "num_fmt": {
"title": "Custom number format", "title": "Custom number format",
"label": "Number format", "label": "Number format",
"close": "Close dialog",
"save": "Save" "save": "Save"
}, },
"sheet_rename": { "sheet_rename": {
"rename": "Save", "rename": "Save",
"label": "New name", "label": "New name",
"title": "Rename Sheet" "title": "Rename Sheet",
"close": "Close dialog"
}, },
"formula_input": { "formula_input": {
"update": "Update", "update": "Update",
@@ -73,5 +77,17 @@
"navigation": { "navigation": {
"add_sheet": "Add sheet", "add_sheet": "Add sheet",
"sheet_list": "Sheet list" "sheet_list": "Sheet list"
},
"name_manager_dialog": {
"help": "Learn more about Named Ranges",
"name": "Name",
"range": "Scope",
"scope": "Range"
},
"name_manager_dialog": {
"help": "Learn more about Named Ranges",
"name": "Name",
"range": "Scope",
"scope": "Range"
} }
} }

View File

@@ -41,6 +41,30 @@ pub(crate) struct Relationship {
pub(crate) rel_type: String, pub(crate) rel_type: String,
} }
impl WorkbookXML {
fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>)> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self
.defined_names
.iter()
.map(|dn| {
let index = dn
.sheet_id
.and_then(|sheet_id| {
// returns an Option<usize>
sheet_id_index.iter().position(|&x| x == sheet_id)
})
// convert Option<usize> to Option<u32>
.map(|pos| pos as u32);
(dn.name.clone(), index)
})
.collect::<Vec<_>>();
defined_names
}
}
fn get_column_from_ref(s: &str) -> String { fn get_column_from_ref(s: &str) -> String {
let cs = s.chars(); let cs = s.chars();
let mut column = Vec::<char>::new(); let mut column = Vec::<char>::new();
@@ -280,11 +304,12 @@ fn from_a1_to_rc(
worksheets: &[String], worksheets: &[String],
context: String, context: String,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
defined_names: Vec<(String, Option<u32>)>,
) -> Result<String, XlsxError> { ) -> Result<String, XlsxError> {
let mut parser = Parser::new(worksheets.to_owned(), tables); let mut parser = Parser::new(worksheets.to_owned(), defined_names, tables);
let cell_reference = let cell_reference =
parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?; parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?;
let t = parser.parse(&formula, &Some(cell_reference)); let t = parser.parse(&formula, &cell_reference);
Ok(to_rc_format(&t)) Ok(to_rc_format(&t))
} }
@@ -681,6 +706,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
worksheets: &[String], worksheets: &[String],
tables: &HashMap<String, Table>, tables: &HashMap<String, Table>,
shared_strings: &mut Vec<String>, shared_strings: &mut Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
) -> Result<(Worksheet, bool), XlsxError> { ) -> Result<(Worksheet, bool), XlsxError> {
let sheet_name = &settings.name; let sheet_name = &settings.name;
let sheet_id = settings.id; let sheet_id = settings.id;
@@ -855,8 +881,13 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
// It's the mother cell. We do not use the ref attribute in IronCalc // It's the mother cell. We do not use the ref attribute in IronCalc
let formula = fs[0].text().unwrap_or("").to_string(); let formula = fs[0].text().unwrap_or("").to_string();
let context = format!("{}!{}", sheet_name, cell_ref); let context = format!("{}!{}", sheet_name, cell_ref);
let formula = let formula = from_a1_to_rc(
from_a1_to_rc(formula, worksheets, context, tables.clone())?; formula,
worksheets,
context,
tables.clone(),
defined_names.clone(),
)?;
match index_map.get(&si) { match index_map.get(&si) {
Some(index) => { Some(index) => {
// The index for that formula already exists meaning we bumped into a daughter cell first // The index for that formula already exists meaning we bumped into a daughter cell first
@@ -910,7 +941,13 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
// Its a cell with a simple formula // Its a cell with a simple formula
let formula = fs[0].text().unwrap_or("").to_string(); let formula = fs[0].text().unwrap_or("").to_string();
let context = format!("{}!{}", sheet_name, cell_ref); let context = format!("{}!{}", sheet_name, cell_ref);
let formula = from_a1_to_rc(formula, worksheets, context, tables.clone())?; let formula = from_a1_to_rc(
formula,
worksheets,
context,
tables.clone(),
defined_names.clone(),
)?;
match get_formula_index(&formula, &shared_formulas) { match get_formula_index(&formula, &shared_formulas) {
Some(index) => formula_index = index, Some(index) => formula_index = index,
@@ -1023,6 +1060,9 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
let mut sheets = Vec::new(); let mut sheets = Vec::new();
let mut selected_sheet = 0; let mut selected_sheet = 0;
let mut sheet_index = 0; let mut sheet_index = 0;
let defined_names = workbook.get_defined_names_with_scope();
for sheet in &workbook.worksheets { for sheet in &workbook.worksheets {
let sheet_name = &sheet.name; let sheet_name = &sheet.name;
let rel_id = &sheet.id; let rel_id = &sheet.id;
@@ -1044,8 +1084,15 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
.ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))? .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?
.to_vec(), .to_vec(),
}; };
let (s, is_selected) = let (s, is_selected) = load_sheet(
load_sheet(archive, &path, settings, worksheets, tables, shared_strings)?; archive,
&path,
settings,
worksheets,
tables,
shared_strings,
defined_names.clone(),
)?;
if is_selected { if is_selected {
selected_sheet = sheet_index; selected_sheet = sheet_index;
} }

Binary file not shown.

Binary file not shown.