Compare commits

..

17 Commits

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

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

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

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

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

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

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

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

Fixes #343
2025-05-17 11:49:42 +02:00
83 changed files with 3499 additions and 4354 deletions

32
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -828,7 +828,7 @@ impl Parser {
| TokenType::Percent => Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: format!("Unexpected token: '{:?}'", next_token),
message: format!("Unexpected token: '{next_token:?}'"),
},
TokenType::LeftBracket => Node::ParseErrorKind {
formula: self.lexer.get_formula(),

View File

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

View File

@@ -182,7 +182,7 @@ pub fn add_implicit_intersection(node: &mut Node, add: bool) {
};
}
pub enum StaticResult {
pub(crate) enum StaticResult {
Scalar,
Array(i32, i32),
Range(i32, i32),
@@ -218,7 +218,7 @@ fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
// * Array(a, b) if we know it will be an a x b array.
// * Range(a, b) if we know it will be a a x b range.
// * Unknown if we cannot guaranty either
pub(crate) fn run_static_analysis_on_node(node: &Node) -> StaticResult {
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
match node {
Node::BooleanKind(_)
| Node::NumberKind(_)

View File

@@ -184,16 +184,16 @@ pub(crate) fn stringify_reference(
return "#REF!".to_string();
}
let mut row_abs = if absolute_row {
format!("${}", row)
format!("${row}")
} else {
format!("{}", row)
format!("{row}")
};
let column = match crate::expressions::utils::number_to_column(column) {
Some(s) => s,
None => return "#REF!".to_string(),
};
let mut col_abs = if absolute_column {
format!("${}", column)
format!("${column}")
} else {
column
};
@@ -208,27 +208,27 @@ pub(crate) fn stringify_reference(
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
}
None => {
format!("{}{}", col_abs, row_abs)
format!("{col_abs}{row_abs}")
}
}
}
None => {
let row_abs = if absolute_row {
format!("R{}", row)
format!("R{row}")
} else {
format!("R[{}]", row)
format!("R[{row}]")
};
let col_abs = if absolute_column {
format!("C{}", column)
format!("C{column}")
} else {
format!("C[{}]", column)
format!("C[{column}]")
};
match &sheet_name {
Some(name) => {
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
}
None => {
format!("{}{}", row_abs, col_abs)
format!("{row_abs}{col_abs}")
}
}
}
@@ -256,7 +256,7 @@ fn format_function(
arguments = stringify(el, context, displace_data, export_to_excel);
}
}
format!("{}({})", name, arguments)
format!("{name}({arguments})")
}
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
@@ -292,9 +292,9 @@ fn stringify(
) -> String {
use self::Node::*;
match node {
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
NumberKind(number) => to_excel_precision_str(*number),
StringKind(value) => format!("\"{}\"", value),
StringKind(value) => format!("\"{value}\""),
WrongReferenceKind {
sheet_name,
column,
@@ -384,7 +384,7 @@ fn stringify(
full_row,
full_column,
);
format!("{}:{}", s1, s2)
format!("{s1}:{s2}")
}
WrongRangeKind {
sheet_name,
@@ -433,7 +433,7 @@ fn stringify(
full_row,
full_column,
);
format!("{}:{}", s1, s2)
format!("{s1}:{s2}")
}
OpRangeKind { left, right } => format!(
"{}:{}",
@@ -484,7 +484,7 @@ fn stringify(
),
_ => stringify(right, context, displace_data, export_to_excel),
};
format!("{}{}{}", x, kind, y)
format!("{x}{kind}{y}")
}
OpPowerKind { left, right } => {
let x = match **left {
@@ -547,7 +547,7 @@ fn stringify(
stringify(right, context, displace_data, export_to_excel)
),
};
format!("{}^{}", x, y)
format!("{x}^{y}")
}
InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, export_to_excel)
@@ -582,7 +582,7 @@ fn stringify(
}
matrix_string.push_str(&row_string);
}
format!("{{{}}}", matrix_string)
format!("{{{matrix_string}}}")
}
TableNameKind(value) => value.to_string(),
DefinedNameKind((name, ..)) => name.to_string(),
@@ -601,7 +601,7 @@ fn stringify(
)
}
},
ErrorKind(kind) => format!("{}", kind),
ErrorKind(kind) => format!("{kind}"),
ParseErrorKind {
formula,
position: _,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,8 @@ use crate::{
lexer::LexerMode,
parser::{
move_formula::{move_formula, MoveContext},
static_analysis::{run_static_analysis_on_node, StaticResult},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
ArrayNode, Node, Parser,
Node, Parser,
},
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*,
@@ -100,22 +99,22 @@ pub struct Model {
/// A Rust internal representation of an Excel workbook
pub workbook: Workbook,
/// A list of parsed formulas
pub parsed_formulas: Vec<Vec<(Node, StaticResult)>>,
pub parsed_formulas: Vec<Vec<Node>>,
/// A list of parsed defined names
pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
/// An optimization to lookup strings faster
pub(crate) shared_strings: HashMap<String, usize>,
/// An instance of the parser
pub(crate) parser: Parser,
/// The list of cells with formulas that are evaluated of being evaluated
/// The list of cells with formulas that are evaluated or being evaluated
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
/// The locale of the model
pub(crate) locale: Locale,
/// Tha language used
/// The language used
pub(crate) language: Language,
/// The timezone used to evaluate the model
pub(crate) tz: Tz,
/// The view id. A view consist of a selected sheet and ranges.
/// The view id. A view consists of a selected sheet and ranges.
pub(crate) view_id: u32,
}
@@ -216,7 +215,7 @@ impl Model {
_ => CalcResult::new_error(
Error::ERROR,
cell,
format!("Error with Implicit Intersection in cell {:?}", cell),
format!("Error with Implicit Intersection in cell {cell:?}"),
),
},
_ => self.evaluate_node_in_context(node, cell),
@@ -356,7 +355,7 @@ impl Model {
return s;
}
};
let result = format!("{}{}", l, r);
let result = format!("{l}{r}");
CalcResult::String(result)
}
OpProductKind { kind, left, right } => match kind {
@@ -376,7 +375,7 @@ impl Model {
}
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
InvalidFunctionKind { name, args: _ } => {
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {name}"))
}
ArrayKind(s) => CalcResult::Array(s.to_owned()),
DefinedNameKind((name, scope, _)) => {
@@ -392,26 +391,26 @@ impl Model {
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NAME,
cell,
format!("Defined name \"{}\" is not a reference.", name),
format!("Defined name \"{name}\" is not a reference."),
),
}
} else {
CalcResult::new_error(
Error::NAME,
cell,
format!("Defined name \"{}\" not found.", name),
format!("Defined name \"{name}\" not found."),
)
}
}
TableNameKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("table name \"{}\" not supported.", s),
format!("table name \"{s}\" not supported."),
),
WrongVariableKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("Variable name \"{}\" not found.", s),
format!("Variable name \"{s}\" not found."),
),
CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell);
@@ -488,7 +487,7 @@ impl Model {
} => CalcResult::new_error(
Error::ERROR,
cell,
format!("Error parsing {}: {}", formula, message),
format!("Error parsing {formula}: {message}"),
),
EmptyArgKind => CalcResult::EmptyArg,
ImplicitIntersection {
@@ -501,7 +500,7 @@ impl Model {
None => CalcResult::new_error(
Error::VALUE,
cell,
format!("Error with Implicit Intersection in cell {:?}", cell),
format!("Error with Implicit Intersection in cell {cell:?}"),
),
}
}
@@ -523,195 +522,14 @@ impl Model {
}
Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row))
}
/// Sets sheet, target_row, target_column, (width, height), &v
#[allow(clippy::too_many_arguments)]
fn set_spill_cell_with_formula_value(
&mut self,
sheet: u32,
row: i32,
column: i32,
r: (i32, i32),
v: &CalcResult,
s: i32,
f: i32,
) -> Result<(), String> {
let new_cell = match v {
CalcResult::EmptyCell => Cell::DynamicCellFormulaNumber {
f,
v: 0.0,
s,
r,
a: false,
},
CalcResult::String(v) => Cell::DynamicCellFormulaString {
f,
v: v.clone(),
s,
r,
a: false,
},
CalcResult::Number(v) => Cell::DynamicCellFormulaNumber {
v: *v,
s,
r,
f,
a: false,
},
CalcResult::Boolean(b) => Cell::DynamicCellFormulaBoolean {
v: *b,
s,
r,
f,
a: false,
},
CalcResult::Error { error, .. } => Cell::DynamicCellFormulaError {
ei: error.clone(),
s,
r,
f,
a: false,
o: "".to_string(),
m: "".to_string(),
},
// These cannot happen
// FIXME: Maybe the type of get_cell_value should be different
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
Cell::DynamicCellFormulaError {
ei: Error::ERROR,
s,
r,
f,
a: false,
o: "".to_string(),
m: "".to_string(),
}
}
};
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
match sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
sheet_data.insert(row, column_data);
}
}
Ok(())
}
/// Sets a cell with a "spill" value
fn set_spill_cell_with_value(
&mut self,
sheet: u32,
row: i32,
column: i32,
m: (i32, i32),
v: &CalcResult,
) -> Result<(), String> {
let style_index = self.get_cell_style_index(sheet, row, column)?;
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
self.workbook
.styles
.get_style_without_quote_prefix(style_index)?
} else {
style_index
};
let new_cell = match v {
CalcResult::EmptyCell => Cell::SpillNumberCell {
v: 0.0,
s: style_index,
m,
},
CalcResult::String(s) => Cell::SpillStringCell {
v: s.clone(),
s: new_style_index,
m,
},
CalcResult::Number(f) => Cell::SpillNumberCell {
v: *f,
s: new_style_index,
m,
},
CalcResult::Boolean(b) => Cell::SpillBooleanCell {
v: *b,
s: new_style_index,
m,
},
CalcResult::Error { error, .. } => Cell::SpillErrorCell {
ei: error.clone(),
s: style_index,
m,
},
// These cannot happen
// FIXME: Maybe the type of get_cell_value should be different
CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => {
Cell::SpillErrorCell {
ei: Error::ERROR,
s: style_index,
m,
}
}
};
let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data;
match sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
sheet_data.insert(row, column_data);
}
}
Ok(())
}
/// Sets `result` in the cell given by `sheet` sheet index, row and column
/// Note that will panic if the cell does not exist
/// It will do nothing if the cell does not have a formula
#[allow(clippy::expect_used)]
fn set_cell_value(
&mut self,
cell_reference: CellReferenceIndex,
result: &CalcResult,
) -> Result<(), String> {
fn set_cell_value(&mut self, cell_reference: CellReferenceIndex, result: &CalcResult) {
let CellReferenceIndex { sheet, column, row } = cell_reference;
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column];
let s = cell.get_style();
// If the cell is a dynamic cell we need to delete all the cells in the range
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
// skip the "mother" cell
if r == row && c == column {
continue;
}
self.cell_clear_contents(sheet, r, c)?;
}
}
}
if let Some(f) = cell.get_formula() {
match result {
CalcResult::Number(value) => {
@@ -776,138 +594,19 @@ impl Model {
ei: error.clone(),
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
}
CalcResult::Range { left, right } => {
if left.sheet == right.sheet
&& left.row == right.row
&& left.column == right.column
{
// There is only one cell
let single_cell = CellReferenceIndex {
let intersection_cell = CellReferenceIndex {
sheet: left.sheet,
column: left.column,
row: left.row,
};
let v = self.evaluate_cell(single_cell);
self.set_cell_value(cell_reference, &v)?;
let v = self.evaluate_cell(intersection_cell);
self.set_cell_value(cell_reference, &v);
} else {
// We need to check if all the cells are empty, otherwise we mark the cell as #SPILL!
let mut all_empty = true;
for r in row..=row + right.row - left.row {
for c in column..=column + right.column - left.column {
if r == row && c == column {
continue;
}
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
all_empty = false;
break;
}
}
}
if !all_empty {
let o = match self.cell_reference_to_string(&cell_reference) {
Ok(s) => s,
Err(_) => "".to_string(),
};
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::DynamicCellFormulaError {
f,
s,
o,
m: "Result would spill to non empty cells".to_string(),
ei: Error::SPILL,
r: (1, 1),
a: false,
};
return Ok(());
}
// evaluate all the cells in that range
for r in left.row..=right.row {
for c in left.column..=right.column {
let cell_reference = CellReferenceIndex {
sheet: left.sheet,
row: r,
column: c,
};
// FIXME: We ned to return an error
self.evaluate_cell(cell_reference);
}
}
// now write the result in the target
for r in left.row..=right.row {
let row_delta = r - left.row;
for c in left.column..=right.column {
let column_delta = c - left.column;
// We need to put whatever is in (left.sheet, r, c) in
// (sheet, row + row_delta, column + column_delta)
// But we need to preserve the style
let target_row = row + row_delta;
let target_column = column + column_delta;
let cell = self
.workbook
.worksheet(left.sheet)?
.cell(r, c)
.cloned()
.unwrap_or_default();
let cell_reference = CellReferenceIndex {
sheet: left.sheet,
row: r,
column: c,
};
let v = self.get_cell_value(&cell, cell_reference);
if row == target_row && column == target_column {
// let cell_reference = CellReferenceIndex { sheet, row, column };
// self.set_cell_value(cell_reference, &v);
self.set_spill_cell_with_formula_value(
sheet,
target_row,
target_column,
(right.column - left.column + 1, right.row - left.row + 1),
&v,
s,
f,
)?;
continue;
}
self.set_spill_cell_with_value(
sheet,
target_row,
target_column,
(row, column),
&v,
)?;
}
}
}
}
CalcResult::Array(array) => {
let width = array[0].len() as i32;
let height = array.len() as i32;
// First we check that we don't spill:
let mut all_empty = true;
for r in row..row + height {
for c in column..column + width {
if r == row && c == column {
continue;
}
if !self.is_empty_cell(sheet, r, c).unwrap_or(false) {
all_empty = false;
break;
}
}
}
if !all_empty {
let o = match self.cell_reference_to_string(&cell_reference) {
Ok(s) => s,
Err(_) => "".to_string(),
@@ -921,65 +620,57 @@ impl Model {
f,
s,
o,
m: "Result would spill to non empty cells".to_string(),
ei: Error::SPILL,
m: "Implicit Intersection not implemented".to_string(),
ei: Error::NIMPL,
};
return Ok(());
}
let mut target_row = row;
for data_row in array {
let mut target_column = column;
for value in data_row {
if row == target_row && column == target_column {
// This is the root cell of the dynamic array
let cell_reference = CellReferenceIndex { sheet, row, column };
let v = match value {
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
ArrayNode::Number(f) => CalcResult::Number(*f),
ArrayNode::String(s) => CalcResult::String(s.clone()),
ArrayNode::Error(error) => CalcResult::new_error(
error.clone(),
cell_reference,
error.to_localized_error_string(&self.language),
),
};
self.set_spill_cell_with_formula_value(
sheet,
target_row,
target_column,
(width, height),
&v,
s,
f,
)?;
target_column += 1;
continue;
}
let v = match value {
ArrayNode::Boolean(b) => CalcResult::Boolean(*b),
ArrayNode::Number(f) => CalcResult::Number(*f),
ArrayNode::String(s) => CalcResult::String(s.clone()),
ArrayNode::Error(error) => CalcResult::new_error(
error.clone(),
cell_reference,
error.to_localized_error_string(&self.language),
),
};
self.set_spill_cell_with_value(
sheet,
target_row,
target_column,
(row, column),
&v,
)?;
target_column += 1;
}
target_row += 1;
}
// if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
// {
// let v = self.evaluate_cell(intersection_cell);
// self.set_cell_value(cell_reference, &v);
// } else {
// let o = match self.cell_reference_to_string(&cell_reference) {
// Ok(s) => s,
// Err(_) => "".to_string(),
// };
// *self.workbook.worksheets[sheet as usize]
// .sheet_data
// .get_mut(&row)
// .expect("expected a row")
// .get_mut(&column)
// .expect("expected a column") = Cell::CellFormulaError {
// f,
// s,
// o,
// m: "Invalid reference".to_string(),
// ei: Error::VALUE,
// };
// }
}
CalcResult::EmptyCell | CalcResult::EmptyArg => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
}
CalcResult::Array(_) => {
*self.workbook.worksheets[sheet as usize]
.sheet_data
.get_mut(&row)
.expect("expected a row")
.get_mut(&column)
.expect("expected a column") = Cell::CellFormulaError {
f,
s,
o: "".to_string(),
m: "Arrays not supported yet".to_string(),
ei: Error::NIMPL,
};
}
}
}
Ok(())
}
/// Sets the color of the sheet tab.
@@ -1006,7 +697,7 @@ impl Model {
worksheet.color = Some(color.to_string());
return Ok(());
}
Err(format!("Invalid color: {}", color))
Err(format!("Invalid color: {color}"))
}
/// Changes the visibility of a sheet
@@ -1023,18 +714,16 @@ impl Model {
Ok(())
}
// EmptyCell, Boolean, Number, String, Error
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
use Cell::*;
match cell {
EmptyCell { .. } => CalcResult::EmptyCell,
BooleanCell { v, .. } | SpillBooleanCell { v, .. } => CalcResult::Boolean(*v),
NumberCell { v, .. } | SpillNumberCell { v, .. } => CalcResult::Number(*v),
ErrorCell { ei, .. } | SpillErrorCell { ei, .. } => {
BooleanCell { v, .. } => CalcResult::Boolean(*v),
NumberCell { v, .. } => CalcResult::Number(*v),
ErrorCell { ei, .. } => {
let message = ei.to_localized_error_string(&self.language);
CalcResult::new_error(ei.clone(), cell_reference, message)
}
SpillStringCell { v, .. } => CalcResult::String(v.clone()),
SharedString { si, .. } => {
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
CalcResult::String(s.clone())
@@ -1043,21 +732,15 @@ impl Model {
CalcResult::new_error(Error::ERROR, cell_reference, message)
}
}
DynamicCellFormula { .. } | CellFormula { .. } => CalcResult::Error {
CellFormula { .. } => CalcResult::Error {
error: Error::ERROR,
origin: cell_reference,
message: "Unevaluated formula".to_string(),
},
DynamicCellFormulaBoolean { v, .. } | CellFormulaBoolean { v, .. } => {
CalcResult::Boolean(*v)
}
DynamicCellFormulaNumber { v, .. } | CellFormulaNumber { v, .. } => {
CalcResult::Number(*v)
}
DynamicCellFormulaString { v, .. } | CellFormulaString { v, .. } => {
CalcResult::String(v.clone())
}
DynamicCellFormulaError { ei, o, m, .. } | CellFormulaError { ei, o, m, .. } => {
CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v),
CellFormulaNumber { v, .. } => CalcResult::Number(*v),
CellFormulaString { v, .. } => CalcResult::String(v.clone()),
CellFormulaError { ei, o, m, .. } => {
if let Some(cell_reference) = self.parse_reference(o) {
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
} else {
@@ -1127,10 +810,9 @@ impl Model {
self.cells.insert(key, CellState::Evaluating);
}
}
let (node, _static_result) =
&self.parsed_formulas[cell_reference.sheet as usize][f as usize];
let result = self.evaluate_node_in_context(&node.clone(), cell_reference);
let _ = self.set_cell_value(cell_reference, &result);
let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone();
let result = self.evaluate_node_in_context(node, cell_reference);
self.set_cell_value(cell_reference, &result);
// mark cell as evaluated
self.cells.insert(key, CellState::Evaluated);
result
@@ -1345,7 +1027,7 @@ impl Model {
let source_sheet_name = self
.workbook
.worksheet(source.sheet)
.map_err(|e| format!("Could not find source worksheet: {}", e))?
.map_err(|e| format!("Could not find source worksheet: {e}"))?
.get_name();
if source.sheet != area.sheet {
return Err("Source and area are in different sheets".to_string());
@@ -1359,7 +1041,7 @@ impl Model {
let target_sheet_name = self
.workbook
.worksheet(target.sheet)
.map_err(|e| format!("Could not find target worksheet: {}", e))?
.map_err(|e| format!("Could not find target worksheet: {e}"))?
.get_name();
if let Some(formula) = value.strip_prefix('=') {
let cell_reference = CellReferenceRC {
@@ -1379,7 +1061,7 @@ impl Model {
column_delta: target.column - source.column,
},
);
Ok(format!("={}", formula_str))
Ok(format!("={formula_str}"))
} else {
Ok(value.to_string())
}
@@ -1418,7 +1100,7 @@ impl Model {
Some(cell) => match cell.get_formula() {
None => cell.get_text(&self.workbook.shared_strings, &self.language),
Some(i) => {
let formula = &self.parsed_formulas[sheet as usize][i as usize].0;
let formula = &self.parsed_formulas[sheet as usize][i as usize];
let cell_ref = CellReferenceRC {
sheet: self.workbook.worksheets[sheet as usize].get_name(),
row: target_row,
@@ -1521,8 +1203,7 @@ impl Model {
.get(sheet as usize)
.ok_or("missing sheet")?
.get(formula_index as usize)
.ok_or("missing formula")?
.0;
.ok_or("missing formula")?;
let cell_ref = CellReferenceRC {
sheet: worksheet.get_name(),
row,
@@ -1756,25 +1437,6 @@ impl Model {
column: i32,
value: String,
) -> Result<(), String> {
// We need to check if the cell is part of a dynamic array
let cell = self
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
// If the cell is a dynamic cell we need to delete all the cells in the range
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
// skip the "mother" cell
if r == row && c == column {
continue;
}
self.cell_clear_contents(sheet, r, c)?;
}
}
}
// If value starts with "'" then we force the style to be quote_prefix
let style_index = self.get_cell_style_index(sheet, row, column)?;
if let Some(new_value) = value.strip_prefix('\'') {
@@ -1800,8 +1462,7 @@ impl Model {
self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?;
// Update the style if needed
let cell = CellReferenceIndex { sheet, row, column };
let parsed_formula =
&self.parsed_formulas[sheet as usize][formula_index as usize].0;
let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
if let Some(units) = self.compute_node_units(parsed_formula, &cell) {
let new_style_index = self
.workbook
@@ -1877,13 +1538,12 @@ impl Model {
// If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference);
let new_parsed_formula = self.parser.parse(&format!("{formula})"), &cell_reference);
match new_parsed_formula {
Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula,
}
}
let static_result = run_static_analysis_on_node(&parsed_formula);
let s = to_rc_format(&parsed_formula);
let mut formula_index: i32 = -1;
@@ -1892,7 +1552,7 @@ impl Model {
}
if formula_index == -1 {
shared_formulas.push(s);
self.parsed_formulas[sheet as usize].push((parsed_formula, static_result));
self.parsed_formulas[sheet as usize].push(parsed_formula);
formula_index = (shared_formulas.len() as i32) - 1;
}
worksheet.set_cell_with_formula(row, column, formula_index, style)?;
@@ -2087,7 +1747,7 @@ impl Model {
};
match cell.get_formula() {
Some(formula_index) => {
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize].0;
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
let cell_ref = CellReferenceRC {
sheet: worksheet.get_name(),
row,
@@ -2102,14 +1762,6 @@ impl Model {
/// Returns a list of all cells
pub fn get_all_cells(&self) -> Vec<CellIndex> {
let mut cells = Vec::new();
for (sheet, row, column) in &self.workbook.calc_chain {
let cell = CellIndex {
row: *row,
column: *column,
index: *sheet,
};
cells.push(cell);
}
for (index, sheet) in self.workbook.worksheets.iter().enumerate() {
let mut sorted_rows: Vec<_> = sheet.sheet_data.keys().collect();
sorted_rows.sort_unstable();
@@ -2136,8 +1788,6 @@ impl Model {
let cells = self.get_all_cells();
// First evaluate all dynamic arrays
for cell in cells {
self.evaluate_cell(CellReferenceIndex {
sheet: cell.index,
@@ -2168,22 +1818,9 @@ impl Model {
/// # }
/// ```
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
// If it has a spill formula we need to delete the contents of all the spilled cells
let worksheet = self.workbook.worksheet_mut(sheet)?;
if let Some(cell) = worksheet.cell(row, column) {
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
if row == r && column == c {
// we skip the root cell
continue;
}
worksheet.cell_clear_contents(r, c)?;
}
}
}
}
worksheet.cell_clear_contents(row, column)?;
self.workbook
.worksheet_mut(sheet)?
.cell_clear_contents(row, column)?;
Ok(())
}
@@ -2208,18 +1845,6 @@ impl Model {
/// # }
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
// Delete the contents of spilled cells if any
if let Some(cell) = worksheet.cell(row, column) {
if let Some((width, height)) = cell.get_dynamic_range() {
for r in row..row + height {
for c in column..column + width {
if row == r && c == column {
worksheet.cell_clear_contents(r, c)?;
}
}
}
}
}
let sheet_data = &mut worksheet.sheet_data;
if let Some(row_data) = sheet_data.get_mut(&row) {

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,6 @@ mod test_fn_offset;
mod test_number_format;
mod test_arrays;
mod test_dynamic_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_fv;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,8 +13,8 @@ use crate::{
},
model::Model,
types::{
Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties,
SheetState, Style, VerticalAlignment,
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
Style, VerticalAlignment,
},
utils::is_valid_hex_color,
};
@@ -24,18 +24,6 @@ use crate::user_model::history::{
};
use super::border_utils::is_max_border;
#[derive(Serialize, Deserialize)]
pub enum CellArrayStructure {
// It s just a single cell
SingleCell,
// It is part o a dynamic array
// (mother_row, mother_column, width, height)
DynamicChild(i32, i32, i32, i32),
// Mother of a dynamic array (width, height)
DynamicMother(i32, i32),
}
/// Data for the clipboard
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
@@ -639,7 +627,6 @@ impl UserModel {
}
}
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
@@ -669,7 +656,6 @@ impl UserModel {
}
}
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
@@ -1501,10 +1487,10 @@ impl UserModel {
return Err(format!("Invalid row: '{first_row}'"));
}
if !is_valid_column_number(last_column) {
return Err(format!("Invalid column: '{}'", last_column));
return Err(format!("Invalid column: '{last_column}'"));
}
if !is_valid_row(last_row) {
return Err(format!("Invalid row: '{}'", last_row));
return Err(format!("Invalid row: '{last_row}'"));
}
if !is_valid_row(to_column) {
@@ -1609,65 +1595,6 @@ impl UserModel {
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
}
/// Returns the geometric structure of a cell
pub fn get_cell_array_structure(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<CellArrayStructure, String> {
let cell = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned()
.unwrap_or_default();
match cell {
Cell::EmptyCell { .. }
| Cell::BooleanCell { .. }
| Cell::NumberCell { .. }
| Cell::ErrorCell { .. }
| Cell::SharedString { .. }
| Cell::CellFormula { .. }
| Cell::CellFormulaBoolean { .. }
| Cell::CellFormulaNumber { .. }
| Cell::CellFormulaString { .. }
| Cell::CellFormulaError { .. } => Ok(CellArrayStructure::SingleCell),
Cell::SpillNumberCell { m, .. }
| Cell::SpillBooleanCell { m, .. }
| Cell::SpillErrorCell { m, .. }
| Cell::SpillStringCell { m, .. } => {
let (m_row, m_column) = m;
let m_cell = self
.model
.workbook
.worksheet(sheet)?
.cell(m_row, m_column)
.cloned()
.unwrap_or_default();
let (width, height) = match m_cell {
Cell::DynamicCellFormula { r, .. }
| Cell::DynamicCellFormulaBoolean { r, .. }
| Cell::DynamicCellFormulaNumber { r, .. }
| Cell::DynamicCellFormulaString { r, .. }
| Cell::DynamicCellFormulaError { r, .. } => (r.0, r.1),
_ => return Err("Invalid structure".to_string()),
};
Ok(CellArrayStructure::DynamicChild(
m_row, m_column, width, height,
))
}
Cell::DynamicCellFormula { r, .. }
| Cell::DynamicCellFormulaBoolean { r, .. }
| Cell::DynamicCellFormulaNumber { r, .. }
| Cell::DynamicCellFormulaString { r, .. }
| Cell::DynamicCellFormulaError { r, .. } => {
Ok(CellArrayStructure::DynamicMother(r.0, r.1))
}
}
}
/// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view();
@@ -1696,15 +1623,15 @@ impl UserModel {
text_row.push(text);
}
wtr.write_record(text_row)
.map_err(|e| format!("Error while processing csv: {}", e))?;
.map_err(|e| format!("Error while processing csv: {e}"))?;
data.insert(row, data_row);
}
let csv = String::from_utf8(
wtr.into_inner()
.map_err(|e| format!("Processing error: '{}'", e))?,
.map_err(|e| format!("Processing error: '{e}'"))?,
)
.map_err(|e| format!("Error converting from utf8: '{}'", e))?;
.map_err(|e| format!("Error converting from utf8: '{e}'"))?;
Ok(Clipboard {
csv,
@@ -1970,24 +1897,6 @@ impl UserModel {
old_value,
} => {
needs_evaluation = true;
let cell = self
.model
.workbook
.worksheet(*sheet)?
.cell(*row, *column)
.cloned()
.unwrap_or_default();
if let Some((width, height)) = cell.get_dynamic_range() {
for r in *row..*row + height {
for c in *column..*column + width {
// skip the "mother" cell
if r == *row && c == *column {
continue;
}
self.model.cell_clear_contents(*sheet, r, c)?;
}
}
}
match *old_value.clone() {
Some(value) => {
self.model
@@ -2482,7 +2391,7 @@ mod tests {
VerticalAlignment::Top,
];
for a in all {
assert_eq!(vertical(&format!("{}", a)), Ok(a));
assert_eq!(vertical(&format!("{a}")), Ok(a));
}
}
@@ -2499,7 +2408,7 @@ mod tests {
HorizontalAlignment::Right,
];
for a in all {
assert_eq!(horizontal(&format!("{}", a)), Ok(a));
assert_eq!(horizontal(&format!("{a}")), Ok(a));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -272,6 +272,7 @@ const ColorGridCol = styled.div`
const ColorSwatch = styled.button<{ $color: string }>`
width: 16px;
height: 16px;
padding: 0px;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["300"]};`;

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
);
const focusWorkbook = useCallback(() => {
if (rootRef.current) {
rootRef.current.focus();
rootRef.current.focus({ preventScroll: true });
// HACK: We need to select something inside the root for onCopy to work
const selection = window.getSelection();
if (selection) {
@@ -348,28 +348,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
return workbookState.getEditingText();
}
const { sheet, row, column } = model.getSelectedView();
const r = model.getCellArrayStructure(sheet, row, column);
if (r === "SingleCell") {
return model.getCellContent(sheet, row, column);
}
if ("DynamicMother" in r) {
return model.getCellContent(sheet, row, column);
}
const [mother_row, mother_column, _] = r.DynamicChild;
return model.getCellContent(sheet, mother_row, mother_column);
};
// returns true if it is either single cell or the root cell of an array
const isRootCellOfArray = () => {
const { sheet, row, column } = model.getSelectedView();
const r = model.getCellArrayStructure(sheet, row, column);
if (r === "SingleCell") {
return false;
}
if ("DynamicMother" in r) {
return false;
}
return true;
return model.getCellContent(sheet, row, column);
};
const getCellStyle = useCallback(() => {
@@ -588,19 +567,15 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
const { topLeftCell, bottomRightCell } =
worksheetCanvas.getVisibleCells();
const firstRow = Math.max(rowStart, topLeftCell.row);
const firstColumn = Math.max(columnStart, topLeftCell.column);
const lastRow = Math.min(rowEnd, bottomRightCell.row);
const lastColumn = Math.min(columnEnd, bottomRightCell.column);
// NB: cells outside of the displayed area are not rendered
// I think the only reasonable way to do this would be server side.
let [x, y] = worksheetCanvas.getCoordinatesByCell(
firstRow,
firstColumn,
rowStart,
columnStart,
);
const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
lastRow + 1,
lastColumn + 1,
rowEnd + 1,
columnEnd + 1,
);
const width = (x1 - x) * devicePixelRatio;
const height = (y1 - y) * devicePixelRatio;
@@ -719,7 +694,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
model={model}
workbookState={workbookState}
isPartOfArray={isRootCellOfArray()}
/>
<Worksheet
model={model}

View File

@@ -14,7 +14,6 @@ import {
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
cellArrayStructureColor,
outlineBackgroundColor,
outlineColor,
} from "../WorksheetCanvas/constants";
@@ -25,7 +24,7 @@ import {
TOOLBAR_HEIGHT,
} from "../constants";
import type { Cell } from "../types";
import { AreaType, type WorkbookState } from "../workbookState";
import type { WorkbookState } from "../workbookState";
import CellContextMenu from "./CellContextMenu";
import usePointer from "./usePointer";
@@ -60,8 +59,6 @@ const Worksheet = forwardRef(
const spacerElement = useRef<HTMLDivElement>(null);
const cellOutline = useRef<HTMLDivElement>(null);
const areaOutline = useRef<HTMLDivElement>(null);
const cellOutlineHandle = useRef<HTMLDivElement>(null);
const cellArrayStructure = useRef<HTMLDivElement>(null);
const extendToOutline = useRef<HTMLDivElement>(null);
const columnResizeGuide = useRef<HTMLDivElement>(null);
const rowResizeGuide = useRef<HTMLDivElement>(null);
@@ -87,9 +84,7 @@ const Worksheet = forwardRef(
const worksheetRef = worksheetElement.current;
const outline = cellOutline.current;
const handle = cellOutlineHandle.current;
const area = areaOutline.current;
const arrayStructure = cellArrayStructure.current;
const extendTo = extendToOutline.current;
const editor = editorElement.current;
@@ -100,12 +95,10 @@ const Worksheet = forwardRef(
!columnHeadersRef ||
!worksheetRef ||
!outline ||
!handle ||
!area ||
!extendTo ||
!scrollElement.current ||
!editor ||
!arrayStructure
!editor
)
return;
// FIXME: This two need to be computed.
@@ -122,8 +115,6 @@ const Worksheet = forwardRef(
rowGuide: rowGuideRef,
columnHeaders: columnHeadersRef,
cellOutline: outline,
cellOutlineHandle: handle,
cellArrayStructure: arrayStructure,
areaOutline: area,
extendToOutline: extendTo,
editor: editor,
@@ -196,203 +187,74 @@ const Worksheet = forwardRef(
worksheetCanvas.current = canvas;
});
const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } =
usePointer({
model,
workbookState,
refresh,
onColumnSelected: (column: number, shift: boolean) => {
let firstColumn = column;
let lastColumn = column;
if (shift) {
const { range } = model.getSelectedView();
firstColumn = Math.min(range[1], column, range[3]);
lastColumn = Math.max(range[3], column, range[1]);
}
model.setSelectedCell(1, firstColumn);
model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
refresh();
},
onRowSelected: (row: number, shift: boolean) => {
let firstRow = row;
let lastRow = row;
if (shift) {
const { range } = model.getSelectedView();
firstRow = Math.min(range[0], row, range[2]);
lastRow = Math.max(range[2], row, range[0]);
}
model.setSelectedCell(firstRow, 1);
model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
refresh();
},
onAllSheetSelected: () => {
model.setSelectedCell(1, 1);
model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
},
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
model.setSelectedCell(cell.row, cell.column);
refresh();
},
onAreaSelecting: (cell: Cell) => {
const { onPointerMove, onPointerDown, onPointerUp } = usePointer({
model,
workbookState,
refresh,
onColumnSelected: (column: number, shift: boolean) => {
let firstColumn = column;
let lastColumn = column;
if (shift) {
const { range } = model.getSelectedView();
firstColumn = Math.min(range[1], column, range[3]);
lastColumn = Math.max(range[3], column, range[1]);
}
model.setSelectedCell(1, firstColumn);
model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
refresh();
},
onRowSelected: (row: number, shift: boolean) => {
let firstRow = row;
let lastRow = row;
if (shift) {
const { range } = model.getSelectedView();
firstRow = Math.min(range[0], row, range[2]);
lastRow = Math.max(range[2], row, range[0]);
}
model.setSelectedCell(firstRow, 1);
model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
refresh();
},
onAllSheetSelected: () => {
model.setSelectedCell(1, 1);
model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
},
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
model.setSelectedCell(cell.row, cell.column);
refresh();
},
onAreaSelecting: (cell: Cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
model.onAreaSelecting(row, column);
canvas.renderSheet();
refresh();
},
onAreaSelected: () => {
const styles = workbookState.getCopyStyles();
if (styles?.length) {
model.onPasteStyles(styles);
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
model.onAreaSelecting(row, column);
canvas.renderSheet();
refresh();
},
onAreaSelected: () => {
const styles = workbookState.getCopyStyles();
if (styles?.length) {
model.onPasteStyles(styles);
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
canvas.renderSheet();
}
workbookState.setCopyStyles(null);
if (worksheetElement.current) {
worksheetElement.current.style.cursor = "auto";
}
refresh();
},
onExtendToCell: (cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
// We are either extending by rows or by columns
// And we could be doing it in the positive direction (downwards or right)
// or the negative direction (upwards or left)
if (
row > rowEnd &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < row - rowEnd) ||
(column > columnEnd && column - columnEnd < row - rowEnd))
) {
// rows downwards
const area = {
type: AreaType.rowsDown,
rowStart: rowEnd + 1,
rowEnd: row,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
row < rowStart &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < rowStart - row) ||
(column > columnEnd && column - columnEnd < rowStart - row))
) {
// rows upwards
const area = {
type: AreaType.rowsUp,
rowStart: row,
rowEnd: rowStart,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column > columnEnd &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < column - columnEnd) ||
(row > rowEnd && row - rowEnd < column - columnEnd))
) {
// columns right
const area = {
type: AreaType.columnsRight,
rowStart,
rowEnd,
columnStart: columnEnd + 1,
columnEnd: column,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column < columnStart &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < columnStart - column) ||
(row > rowEnd && row - rowEnd < columnStart - column))
) {
// columns left
const area = {
type: AreaType.columnsLeft,
rowStart,
rowEnd,
columnStart: column,
columnEnd: columnStart,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
}
},
onExtendToEnd: () => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { sheet, range } = model.getSelectedView();
const extendedArea = workbookState.getExtendToArea();
if (!extendedArea) {
return;
}
const rowStart = Math.min(range[0], range[2]);
const height = Math.abs(range[2] - range[0]) + 1;
const width = Math.abs(range[3] - range[1]) + 1;
const columnStart = Math.min(range[1], range[3]);
const area = {
sheet,
row: rowStart,
column: columnStart,
width,
height,
};
switch (extendedArea.type) {
case AreaType.rowsDown:
model.autoFillRows(area, extendedArea.rowEnd);
break;
case AreaType.rowsUp: {
model.autoFillRows(area, extendedArea.rowStart);
break;
}
case AreaType.columnsRight: {
model.autoFillColumns(area, extendedArea.columnEnd);
break;
}
case AreaType.columnsLeft: {
model.autoFillColumns(area, extendedArea.columnStart);
break;
}
}
model.setSelectedRange(
Math.min(rowStart, extendedArea.rowStart),
Math.min(columnStart, extendedArea.columnStart),
Math.max(rowStart + height - 1, extendedArea.rowEnd),
Math.max(columnStart + width - 1, extendedArea.columnEnd),
);
workbookState.clearExtendToArea();
canvas.renderSheet();
},
canvasElement,
worksheetElement,
worksheetCanvas,
});
}
workbookState.setCopyStyles(null);
if (worksheetElement.current) {
worksheetElement.current.style.cursor = "auto";
}
refresh();
},
canvasElement,
worksheetElement,
worksheetCanvas,
});
const onScroll = (): void => {
if (!scrollElement.current || !worksheetCanvas.current) {
@@ -467,12 +329,7 @@ const Worksheet = forwardRef(
/>
</EditorWrapper>
<AreaOutline ref={areaOutline} />
<CellArrayStructure ref={cellArrayStructure} />
<ExtendToOutline ref={extendToOutline} />
<CellOutlineHandle
ref={cellOutlineHandle}
onPointerDown={onPointerHandleDown}
/>
<ColumnResizeGuide ref={columnResizeGuide} />
<RowResizeGuide ref={rowResizeGuide} />
<ColumnHeaders ref={columnHeaders} />
@@ -637,12 +494,6 @@ const AreaOutline = styled("div")`
background-color: ${outlineBackgroundColor};
`;
const CellArrayStructure = styled("div")`
position: absolute;
border: 1px solid ${cellArrayStructureColor};
border-radius: 3px;
`;
const CellOutline = styled("div")`
position: absolute;
border: 2px solid ${outlineColor};
@@ -652,15 +503,6 @@ const CellOutline = styled("div")`
display: flex;
`;
const CellOutlineHandle = styled("div")`
position: absolute;
width: 5px;
height: 5px;
background: ${outlineColor};
cursor: crosshair;
border-radius: 1px;
`;
const ExtendToOutline = styled("div")`
position: absolute;
border: 1px dashed ${outlineColor};

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ import {
headerTextColor,
outlineColor,
} from "./constants";
import { attachOutlineHandle } from "./outlineHandle";
import { computeWrappedLines, hexToRGBA10Percent } from "./util";
export interface CanvasSettings {
model: Model;
@@ -28,8 +30,6 @@ export interface CanvasSettings {
canvas: HTMLCanvasElement;
cellOutline: HTMLDivElement;
areaOutline: HTMLDivElement;
cellOutlineHandle: HTMLDivElement;
cellArrayStructure: HTMLDivElement;
extendToOutline: HTMLDivElement;
columnGuide: HTMLDivElement;
rowGuide: HTMLDivElement;
@@ -54,70 +54,6 @@ export const defaultCellFontFamily = fonts.regular;
export const headerFontFamily = fonts.regular;
export const frozenSeparatorWidth = 3;
// Get a 10% transparency of an hex color
function hexToRGBA10Percent(colorHex: string): string {
// Remove the leading hash (#) if present
const hex = colorHex.replace(/^#/, "");
// Parse the hex color
const red = Number.parseInt(hex.substring(0, 2), 16);
const green = Number.parseInt(hex.substring(2, 4), 16);
const blue = Number.parseInt(hex.substring(4, 6), 16);
// Set the alpha (opacity) to 0.1 (10%)
const alpha = 0.1;
// Return the RGBA color string
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
/**
* Splits the given text into multiple lines. If `wrapText` is true, it applies word-wrapping
* based on the specified canvas context, maximum width, and horizontal padding.
*
* - First, the text is split by newline characters so that explicit newlines are respected.
* - If wrapping is enabled, each line is further split into words and measured against the
* available width. Whenever adding an extra word would exceed
* this limit, a new line is started.
*
* @param text The text to split into lines.
* @param wrapText Whether to apply word-wrapping or just return text split by newlines.
* @param context The `CanvasRenderingContext2D` used for measuring text width.
* @param width The maximum width for each line.
* @returns An array of lines (strings), each fitting within the specified width if wrapping is enabled.
*/
function computeWrappedLines(
text: string,
wrapText: boolean,
context: CanvasRenderingContext2D,
width: number,
): string[] {
// Split the text into lines
const rawLines = text.split("\n");
if (!wrapText) {
// If there is no wrapping, return the raw lines
return rawLines;
}
const wrappedLines = [];
for (const line of rawLines) {
const words = line.split(" ");
let currentLine = words[0];
for (let i = 1; i < words.length; i += 1) {
const word = words[i];
const testLine = `${currentLine} ${word}`;
const textWidth = context.measureText(testLine).width;
if (textWidth < width) {
currentLine = testLine;
} else {
wrappedLines.push(currentLine);
currentLine = word;
}
}
wrappedLines.push(currentLine);
}
return wrappedLines;
}
export default class WorksheetCanvas {
sheetWidth: number;
@@ -139,8 +75,6 @@ export default class WorksheetCanvas {
cellOutlineHandle: HTMLDivElement;
cellArrayStructure: HTMLDivElement;
extendToOutline: HTMLDivElement;
workbookState: WorkbookState;
@@ -172,8 +106,6 @@ export default class WorksheetCanvas {
this.refresh = options.refresh;
this.cellOutline = options.elements.cellOutline;
this.cellOutlineHandle = options.elements.cellOutlineHandle;
this.cellArrayStructure = options.elements.cellArrayStructure;
this.areaOutline = options.elements.areaOutline;
this.extendToOutline = options.elements.extendToOutline;
this.rowGuide = options.elements.rowGuide;
@@ -183,6 +115,7 @@ export default class WorksheetCanvas {
this.onColumnWidthChanges = options.onColumnWidthChanges;
this.onRowHeightChanges = options.onRowHeightChanges;
this.resetHeaders();
this.cellOutlineHandle = attachOutlineHandle(this);
}
setScrollPosition(scrollPosition: { left: number; top: number }): void {
@@ -1249,20 +1182,16 @@ export default class WorksheetCanvas {
}
private drawCellOutline(): void {
const { cellArrayStructure, cellOutline, areaOutline, cellOutlineHandle } =
this;
const { cellOutline, areaOutline, cellOutlineHandle } = this;
if (this.workbookState.getEditingCell()) {
cellOutline.style.visibility = "hidden";
cellOutlineHandle.style.visibility = "hidden";
areaOutline.style.visibility = "hidden";
cellArrayStructure.style.visibility = "hidden";
return;
}
cellOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";
areaOutline.style.visibility = "visible";
// This one is hidden by default
cellArrayStructure.style.visibility = "hidden";
const [selectedSheet, selectedRow, selectedColumn] =
this.model.getSelectedCell();
@@ -1318,34 +1247,6 @@ export default class WorksheetCanvas {
[handleX, handleY] = this.getCoordinatesByCell(rowStart, columnStart);
handleX += this.getColumnWidth(selectedSheet, columnStart);
handleY += this.getRowHeight(selectedSheet, rowStart);
// we draw the array structure if needed only in this case
const arrayStructure = this.model.getCellArrayStructure(
selectedSheet,
selectedRow,
selectedColumn,
);
let array = null;
if (arrayStructure === "SingleCell") {
// nothing to see here
} else if ("DynamicMother" in arrayStructure) {
cellArrayStructure.style.visibility = "visible";
const [arrayWidth, arrayHeight] = arrayStructure.DynamicMother;
array = [selectedRow, selectedColumn, arrayWidth, arrayHeight];
} else {
cellArrayStructure.style.visibility = "visible";
array = arrayStructure.DynamicChild;
}
if (array !== null) {
const [arrayX, arrayY] = this.getCoordinatesByCell(array[0], array[1]);
const [arrayX1, arrayY1] = this.getCoordinatesByCell(
array[0] + array[3],
array[1] + array[2],
);
cellArrayStructure.style.left = `${arrayX}px`;
cellArrayStructure.style.top = `${arrayY}px`;
cellArrayStructure.style.width = `${arrayX1 - arrayX}px`;
cellArrayStructure.style.height = `${arrayY1 - arrayY}px`;
}
} else {
areaOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,10 +72,10 @@ export function WorkbookTitle(properties: {
}
const Container = styled("div")`
text-align: center;
padding: 8px;
text-align: left;
padding: 6px 4px;
font-size: 14px;
font-weight: 700;
font-weight: 600;
font-family: Inter;
`;
@@ -108,7 +108,7 @@ const TitleInput = styled("input")`
background-color: #f2f2f2;
}
&:focus {
border: 1px solid grey;
outline: 1px solid grey;
}
font-weight: inherit;
font-family: inherit;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -148,8 +148,8 @@ pub fn load_from_xlsx(file_name: &str, locale: &str, tz: &str) -> Result<Model,
/// Loads a [Model] from an `ic` file (a file in the IronCalc internal representation)
pub fn load_from_icalc(file_name: &str) -> Result<Model, XlsxError> {
let contents = fs::read(file_name)
.map_err(|e| XlsxError::IO(format!("Could not extract workbook name: {}", e)))?;
.map_err(|e| XlsxError::IO(format!("Could not extract workbook name: {e}")))?;
let workbook: Workbook = bitcode::decode(&contents)
.map_err(|e| XlsxError::IO(format!("Failed to decode file: {}", e)))?;
.map_err(|e| XlsxError::IO(format!("Failed to decode file: {e}")))?;
Model::from_workbook(workbook).map_err(XlsxError::Workbook)
}

View File

@@ -93,7 +93,8 @@ pub(super) fn load_styles<R: Read + std::io::Seek>(
let mut b = false;
let mut i = false;
let mut strike = false;
let mut color = Some("FFFFFF00".to_string());
// Default color is black
let mut color = Some("#000000".to_string());
let mut family = 2;
let mut scheme = FontScheme::default();
for feature in font.children() {
@@ -141,7 +142,7 @@ pub(super) fn load_styles<R: Read + std::io::Seek>(
}
"charset" => {}
_ => {
println!("Unexpected feature {:?}", feature);
println!("Unexpected feature {feature:?}");
}
}
}

View File

@@ -21,7 +21,7 @@ where
{
let attr_name = attr_name.into();
node.attribute(attr_name)
.ok_or_else(|| XlsxError::Xml(format!("Missing \"{:?}\" XML attribute", attr_name)))
.ok_or_else(|| XlsxError::Xml(format!("Missing \"{attr_name:?}\" XML attribute")))
}
pub(super) fn get_value_or_default(node: &Node, tag_name: &str, default: &str) -> String {
@@ -64,7 +64,7 @@ pub(super) fn get_color(node: Node) -> Result<Option<String>, XlsxError> {
// A boolean value indicating the color is automatic and system color dependent.
Ok(None)
} else {
println!("Unexpected color node {:?}", node);
println!("Unexpected color node {node:?}");
Ok(None)
}
}

View File

@@ -40,7 +40,7 @@ pub(super) fn load_workbook<R: Read + std::io::Seek>(
Some("visible") | None => SheetState::Visible,
Some("hidden") => SheetState::Hidden,
Some("veryHidden") => SheetState::VeryHidden,
Some(state) => return Err(XlsxError::Xml(format!("Unknown sheet state: {}", state))),
Some(state) => return Err(XlsxError::Xml(format!("Unknown sheet state: {state}"))),
};
sheets.push(Sheet {
name,

View File

@@ -81,7 +81,7 @@ fn parse_cell_reference(cell: &str) -> Result<(i32, i32), String> {
if let Some(r) = parse_reference_a1(cell) {
Ok((r.row, r.column))
} else {
Err(format!("Invalid cell reference: '{}'", cell))
Err(format!("Invalid cell reference: '{cell}'"))
}
}
@@ -91,17 +91,17 @@ fn parse_range(range: &str) -> Result<(i32, i32, i32, i32), String> {
if let Some(r) = parse_reference_a1(parts[0]) {
Ok((r.row, r.column, r.row, r.column))
} else {
Err(format!("Invalid range: '{}'", range))
Err(format!("Invalid range: '{range}'"))
}
} else if parts.len() == 2 {
match (parse_reference_a1(parts[0]), parse_reference_a1(parts[1])) {
(Some(left), Some(right)) => {
return Ok((left.row, left.column, right.row, right.column));
}
_ => return Err(format!("Invalid range: '{}'", range)),
_ => return Err(format!("Invalid range: '{range}'")),
}
} else {
return Err(format!("Invalid range: '{}'", range));
return Err(format!("Invalid range: '{range}'"));
}
}
@@ -306,15 +306,13 @@ fn from_a1_to_rc(
context: String,
tables: HashMap<String, Table>,
defined_names: Vec<DefinedNameS>,
is_array: bool,
) -> Result<String, XlsxError> {
let mut parser = Parser::new(worksheets.to_owned(), defined_names, tables);
let cell_reference =
parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?;
let mut t = parser.parse(&formula, &cell_reference);
if !is_array {
add_implicit_intersection(&mut t, true);
}
add_implicit_intersection(&mut t, true);
Ok(to_rc_format(&t))
}
@@ -392,7 +390,7 @@ fn get_cell_from_excel(
}
"d" => {
// Not implemented
println!("Invalid type (d) in {}!{}", sheet_name, cell_ref);
println!("Invalid type (d) in {sheet_name}!{cell_ref}");
Cell::ErrorCell {
ei: Error::NIMPL,
s: cell_style,
@@ -400,7 +398,7 @@ fn get_cell_from_excel(
}
"inlineStr" => {
// Not implemented
println!("Invalid type (inlineStr) in {}!{}", sheet_name, cell_ref);
println!("Invalid type (inlineStr) in {sheet_name}!{cell_ref}");
Cell::ErrorCell {
ei: Error::NIMPL,
s: cell_style,
@@ -409,10 +407,7 @@ fn get_cell_from_excel(
"empty" => Cell::EmptyCell { s: cell_style },
_ => {
// error
println!(
"Unexpected type ({}) in {}!{}",
cell_type, sheet_name, cell_ref
);
println!("Unexpected type ({cell_type}) in {sheet_name}!{cell_ref}");
Cell::ErrorCell {
ei: Error::ERROR,
s: cell_style,
@@ -446,15 +441,15 @@ fn get_cell_from_excel(
f: formula_index,
ei: get_error_by_english_name(error_name).unwrap_or(Error::ERROR),
s: cell_style,
o: format!("{}!{}", sheet_name, cell_ref),
o: format!("{sheet_name}!{cell_ref}"),
m: cell_value.unwrap_or("#ERROR!").to_string(),
}
}
"s" => {
// Not implemented
let o = format!("{}!{}", sheet_name, cell_ref);
let o = format!("{sheet_name}!{cell_ref}");
let m = Error::NIMPL.to_string();
println!("Invalid type (s) in {}!{}", sheet_name, cell_ref);
println!("Invalid type (s) in {sheet_name}!{cell_ref}");
Cell::CellFormulaError {
f: formula_index,
ei: Error::NIMPL,
@@ -473,8 +468,8 @@ fn get_cell_from_excel(
}
"d" => {
// Not implemented
println!("Invalid type (d) in {}!{}", sheet_name, cell_ref);
let o = format!("{}!{}", sheet_name, cell_ref);
println!("Invalid type (d) in {sheet_name}!{cell_ref}");
let o = format!("{sheet_name}!{cell_ref}");
let m = Error::NIMPL.to_string();
Cell::CellFormulaError {
f: formula_index,
@@ -486,9 +481,9 @@ fn get_cell_from_excel(
}
"inlineStr" => {
// Not implemented
let o = format!("{}!{}", sheet_name, cell_ref);
let o = format!("{sheet_name}!{cell_ref}");
let m = Error::NIMPL.to_string();
println!("Invalid type (inlineStr) in {}!{}", sheet_name, cell_ref);
println!("Invalid type (inlineStr) in {sheet_name}!{cell_ref}");
Cell::CellFormulaError {
f: formula_index,
ei: Error::NIMPL,
@@ -499,11 +494,8 @@ fn get_cell_from_excel(
}
_ => {
// error
println!(
"Unexpected type ({}) in {}!{}",
cell_type, sheet_name, cell_ref
);
let o = format!("{}!{}", sheet_name, cell_ref);
println!("Unexpected type ({cell_type}) in {sheet_name}!{cell_ref}");
let o = format!("{sheet_name}!{cell_ref}");
let m = Error::ERROR.to_string();
Cell::CellFormulaError {
f: formula_index,
@@ -829,7 +821,6 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
};
let cell_metadata = cell.attribute("cm");
let is_dynamic_array = cell_metadata == Some("1");
// type, the default type being "n" for number
// If the cell does not have a value is an empty cell
@@ -889,14 +880,13 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
Some(_) => {
// It's the mother cell. We do not use the ref attribute in IronCalc
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(),
defined_names.clone(),
is_dynamic_array,
)?;
match index_map.get(&si) {
Some(index) => {
@@ -945,6 +935,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
return Err(XlsxError::NotImplemented("data table formulas".to_string()));
}
"array" | "normal" => {
let is_dynamic_array = cell_metadata == Some("1");
if formula_type == "array" && !is_dynamic_array {
// Dynamic formulas in Excel are formulas of type array with the cm=1, those we support.
// On the other hand the old CSE formulas or array formulas are not supported in IronCalc for the time being
@@ -952,14 +943,13 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
}
// Its a cell with a simple formula
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(),
defined_names.clone(),
is_dynamic_array,
)?;
match get_formula_index(&formula, &shared_formulas) {
@@ -972,8 +962,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
}
_ => {
return Err(XlsxError::Xml(format!(
"Invalid formula type {:?}.",
formula_type,
"Invalid formula type {formula_type:?}.",
)));
}
}

Binary file not shown.

View File

@@ -350,11 +350,11 @@ fn test_xlsx() {
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {}", file_path_str);
println!("Testing file: {file_path_str}");
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{}", message);
println!("{message}");
is_error = true;
}
let t = test_load_and_saving(file_path_str, &dir);
@@ -389,11 +389,11 @@ fn no_export() {
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {}", file_path_str);
println!("Testing file: {file_path_str}");
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{}", message);
println!("{message}");
is_error = true;
}
} else {
@@ -485,7 +485,7 @@ fn test_documentation_xlsx() {
// Numerically unstable
skip.push("TAN.xlsx");
let skip: Vec<String> = skip.iter().map(|s| format!("tests/docs/{s}")).collect();
println!("{:?}", skip);
println!("{skip:?}");
// dumb counter to make sure we are actually testing the files
assert!(entries.len() > 7);
let temp_folder = env::temp_dir();
@@ -497,13 +497,13 @@ fn test_documentation_xlsx() {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
if skip.contains(&file_path_str.to_string()) {
println!("Skipping file: {}", file_path_str);
println!("Skipping file: {file_path_str}");
continue;
}
println!("Testing file: {}", file_path_str);
println!("Testing file: {file_path_str}");
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("{}", message);
println!("{message}");
is_error = true;
}
assert!(test_load_and_saving(file_path_str, &dir).is_ok());