Compare commits

...

8 Commits

Author SHA1 Message Date
Nicolás Hatcher
fd12881972 UPDATE: Implement Insert/Delete Cells 2024-05-11 14:28:48 +02:00
Nicolás Hatcher Andrés
f752c90058 UPDATE: Parses selected row/column/range and selected sheet (#67)
* FIX: Update to Rust 1.78.0

* UPDATE: Parses selected row/column/range and selected sheet
2024-05-09 11:46:26 +02:00
Daniel González-Albo
a78d5593f2 Merge pull request #60 from ironcalc/feature/dani-logo
UPDATE: adds missing favicons
2024-04-27 18:11:29 +02:00
Daniel
079208a1bd UPDATE: adds missing favicons 2024-04-27 18:02:04 +02:00
Daniel González-Albo
4721582dfe Merge pull request #42 from ironcalc/feature/dani-logo
UPDATE: adds logo
2024-04-25 19:49:43 +02:00
Daniel
1746eec5da UPDATE: adds logo 2024-04-25 19:42:10 +02:00
Nicolás Hatcher Andrés
f9cf86a17c Bugfix/nicolas more fixes (#36)
* FIX: Remove the serde_json depndendency

* UPDATE: Use binary representation also for languages and locales
2024-04-15 19:25:38 +02:00
Nicolás Hatcher Andrés
49ef846ebd FIX: small diverse fixes (#35) 2024-04-14 21:50:14 +02:00
93 changed files with 1322 additions and 3004 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
target/* target/*
.DS_Store

12
Cargo.lock generated
View File

@@ -370,7 +370,6 @@ dependencies = [
"ryu", "ryu",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr",
] ]
[[package]] [[package]]
@@ -679,17 +678,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_repr"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

BIN
assets/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/logo/png/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/logo/png/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -12,8 +12,6 @@ readme = "README.md"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
ryu = "1.0" ryu = "1.0"
chrono = "0.4" chrono = "0.4"
chrono-tz = "0.9" chrono-tz = "0.9"
@@ -21,6 +19,9 @@ regex = "1.0"
once_cell = "1.16.0" once_cell = "1.16.0"
bitcode = "0.6.0" bitcode = "0.6.0"
[dev-dependencies]
serde_json = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.69" }

View File

@@ -69,21 +69,26 @@ impl Model {
target_row: i32, target_row: i32,
target_column: i32, target_column: i32,
) -> Result<(), String> { ) -> Result<(), String> {
let source_cell = self if let Some(source_cell) = self
.workbook .workbook
.worksheet(sheet)? .worksheet(sheet)?
.cell(source_row, source_column) .cell(source_row, source_column)
.ok_or("Expected Cell to exist")?; {
let style = source_cell.get_style(); let style = source_cell.get_style();
// FIXME: we need some user_input getter instead of get_text // FIXME: we need some user_input getter instead of get_text
let formula_or_value = self let formula_or_value = self
.get_cell_formula(sheet, source_row, source_column)? .get_cell_formula(sheet, source_row, source_column)?
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language)); .unwrap_or_else(|| {
source_cell.get_text(&self.workbook.shared_strings, &self.language)
});
self.set_user_input(sheet, target_row, target_column, formula_or_value); self.set_user_input(sheet, target_row, target_column, formula_or_value);
self.workbook self.workbook
.worksheet_mut(sheet)? .worksheet_mut(sheet)?
.set_cell_style(target_row, target_column, style); .set_cell_style(target_row, target_column, style);
self.cell_clear_all(sheet, source_row, source_column)?; self.cell_clear_all(sheet, source_row, source_column)?;
} else {
self.cell_clear_all(sheet, target_row, target_column)?;
}
Ok(()) Ok(())
} }
@@ -106,7 +111,7 @@ impl Model {
return Err("Cannot add a negative number of cells :)".to_string()); return Err("Cannot add a negative number of cells :)".to_string());
} }
// check if it is possible: // check if it is possible:
let dimensions = self.workbook.worksheet(sheet)?.dimension(); let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
let last_column = dimensions.max_column + column_count; let last_column = dimensions.max_column + column_count;
if last_column > LAST_COLUMN { if last_column > LAST_COLUMN {
return Err( return Err(
@@ -263,7 +268,7 @@ impl Model {
return Err("Cannot add a negative number of cells :)".to_string()); return Err("Cannot add a negative number of cells :)".to_string());
} }
// Check if it is possible: // Check if it is possible:
let dimensions = self.workbook.worksheet(sheet)?.dimension(); let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
let last_row = dimensions.max_row + row_count; let last_row = dimensions.max_row + row_count;
if last_row > LAST_ROW { if last_row > LAST_ROW {
return Err( return Err(
@@ -367,13 +372,162 @@ impl Model {
} }
} }
self.workbook.worksheets[sheet as usize].rows = new_rows; self.workbook.worksheets[sheet as usize].rows = new_rows;
self.displace_cells( self.displace_cells(&DisplaceData::Row {
&(DisplaceData::Row {
sheet, sheet,
row, row,
delta: -row_count, delta: -row_count,
}), });
); Ok(())
}
pub fn delete_cells_and_shift_left(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let max_column = worksheet.get_dimension().max_column;
// Delete all cells in the range
for r in row..row + row_delta {
for c in column..column + column_delta {
self.cell_clear_all(sheet, r, c)?;
}
}
// Move all cells in the range
for r in row..row + row_delta {
for c in column + 1..max_column + 1 {
println!("{r}-{c}");
self.move_cell(sheet, r, c, r, c - column_delta)?;
}
}
// Update all formulas in the workbook
self.displace_cells(&DisplaceData::ShiftCellsRight {
sheet,
row,
column,
column_delta: -column_delta,
row_delta,
});
Ok(())
}
/// Insert cells and shift right
pub fn insert_cells_and_shift_right(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let max_column = worksheet.get_dimension().max_column;
// Move all cells in the range
for r in row..row + row_delta {
for c in (column..max_column + 1).rev() {
self.move_cell(sheet, r, c, r, c + column_delta)?;
}
}
// Delete all cells in the range
for r in row..row + row_delta {
for c in column..column + column_delta {
self.cell_clear_all(sheet, r, c)?;
}
}
// Update all formulas in the workbook
self.displace_cells(&DisplaceData::ShiftCellsRight {
sheet,
row,
column,
column_delta,
row_delta,
});
Ok(())
}
/// Insert cells and shift down
pub fn insert_cells_and_shift_down(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let max_row = worksheet.get_dimension().max_row;
// Move all cells in the range
for r in (row..row + max_row + 1).rev() {
for c in column..column + column_delta {
self.move_cell(sheet, r, c, r, c + column_delta)?;
}
}
// Delete all cells in the range
for r in row..row + row_delta {
for c in column..column + column_delta {
self.cell_clear_all(sheet, r, c)?;
}
}
// Update all formulas in the workbook
self.displace_cells(&DisplaceData::ShiftCellsDown {
sheet,
row,
column,
column_delta,
row_delta,
});
Ok(())
}
pub fn delete_cells_and_shift_up(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
let max_row = worksheet.get_dimension().max_row;
// Delete all cells in the range
for r in row..row + row_delta {
for c in column..column + column_delta {
self.cell_clear_all(sheet, r, c)?;
}
}
// Move all cells in the range
for r in row..max_row + 1 {
for c in column + 1..column + column_delta {
self.move_cell(sheet, r, c, r - row_delta, c)?;
}
}
// Update all formulas in the workbook
self.displace_cells(&DisplaceData::ShiftCellsDown {
sheet,
row,
column,
column_delta,
row_delta: -row_delta,
});
Ok(()) Ok(())
} }

View File

@@ -1,12 +1,9 @@
use crate::{ use crate::{
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*, expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
}; };
use serde::{Deserialize, Serialize};
use serde_json::json;
/// A CellValue is the representation of the cell content. /// A CellValue is the representation of the cell content.
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Debug, PartialEq)]
#[serde(untagged)]
pub enum CellValue { pub enum CellValue {
None, None,
String(String), String(String),
@@ -14,17 +11,6 @@ pub enum CellValue {
Boolean(bool), Boolean(bool),
} }
impl CellValue {
pub fn to_json_str(&self) -> String {
match &self {
CellValue::None => "null".to_string(),
CellValue::String(s) => json!(s).to_string(),
CellValue::Number(f) => json!(f).to_string(),
CellValue::Boolean(b) => json!(b).to_string(),
}
}
}
impl From<f64> for CellValue { impl From<f64> for CellValue {
fn from(value: f64) -> Self { fn from(value: f64) -> Self {
Self::Number(value) Self::Number(value)

View File

@@ -1,3 +1,5 @@
#![deny(missing_docs)]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::expressions::token; use crate::expressions::token;
@@ -9,8 +11,11 @@ use super::{Lexer, LexerMode};
/// A MarkedToken is a token together with its position on a formula /// A MarkedToken is a token together with its position on a formula
#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct MarkedToken { pub struct MarkedToken {
/// Token type (see [token::TokenType])
pub token: token::TokenType, pub token: token::TokenType,
/// Position of the start of the token (in bytes)
pub start: i32, pub start: i32,
/// Position of the end of the token (in bytes)
pub end: i32, pub end: i32,
} }
@@ -19,7 +24,7 @@ pub struct MarkedToken {
/// # Examples /// # Examples
/// ``` /// ```
/// use ironcalc_base::expressions::{ /// use ironcalc_base::expressions::{
/// lexer::util::{get_tokens, MarkedToken}, /// lexer::marked_token::{get_tokens, MarkedToken},
/// token::{OpSum, TokenType}, /// token::{OpSum, TokenType},
/// }; /// };
/// ///

View File

@@ -1,3 +1,5 @@
#![deny(missing_docs)]
//! A tokenizer for spreadsheet formulas. //! A tokenizer for spreadsheet formulas.
//! //!
//! This is meant to feed a formula parser. //! This is meant to feed a formula parser.
@@ -7,8 +9,10 @@
//! It supports two working modes: //! It supports two working modes:
//! //!
//! 1. A1 or display mode //! 1. A1 or display mode
//!
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10` //! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
//! 2. R1C1, internal or runtime mode //! 2. R1C1, internal or runtime mode
//!
//! A reference like R1C1 refers to $A$1 and R3C4 to $D$4 //! A reference like R1C1 refers to $A$1 and R3C4 to $D$4
//! R[2]C[5] refers to a cell two rows below and five columns to the right //! R[2]C[5] refers to a cell two rows below and five columns to the right
//! It uses the 'en' locale and language. //! It uses the 'en' locale and language.
@@ -55,7 +59,8 @@ use super::token::{Error, TokenType};
use super::types::*; use super::types::*;
use super::utils; use super::utils;
pub mod util; /// Returns an iterator over tokens together with their position in the byte string.
pub mod marked_token;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@@ -63,17 +68,28 @@ mod test;
mod ranges; mod ranges;
mod structured_references; mod structured_references;
/// This is the TokenType we return if we cannot recognize a token
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LexerError { pub struct LexerError {
/// Position of the beginning of the token in the byte string.
pub position: usize, pub position: usize,
/// Message describing what we think the error is.
pub message: String, pub message: String,
} }
pub(super) type Result<T> = std::result::Result<T, LexerError>; pub(super) type Result<T> = std::result::Result<T, LexerError>;
/// Whether we try to parse formulas in A1 mode or in the internal R1C1 mode
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub enum LexerMode { pub enum LexerMode {
/// Cell references are written `=S34`. This is the display mode
A1, A1,
/// R1C1, internal or runtime mode
///
/// A reference like R1C1 refers to $A$1 and R3C4 to $D$4
/// R[2]C[5] refers to a cell two rows below and five columns to the right
/// It uses the 'en' locale and language.
/// This is used internally at runtime.
R1C1, R1C1,
} }
@@ -308,9 +324,9 @@ impl Lexer {
return self.consume_range(None); return self.consume_range(None);
} }
let name_upper = name.to_ascii_uppercase(); let name_upper = name.to_ascii_uppercase();
if name_upper == self.language.booleans.true_value { if name_upper == self.language.booleans.r#true {
return TokenType::Boolean(true); return TokenType::Boolean(true);
} else if name_upper == self.language.booleans.false_value { } else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false); return TokenType::Boolean(false);
} }
if self.mode == LexerMode::A1 { if self.mode == LexerMode::A1 {
@@ -660,8 +676,8 @@ impl Lexer {
fn consume_error(&mut self) -> TokenType { fn consume_error(&mut self) -> TokenType {
let errors = &self.language.errors; let errors = &self.language.errors;
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect(); let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
if rest_of_formula.starts_with(&errors.ref_value) { if rest_of_formula.starts_with(&errors.r#ref) {
self.position += errors.ref_value.chars().count() - 1; self.position += errors.r#ref.chars().count() - 1;
return TokenType::Error(Error::REF); return TokenType::Error(Error::REF);
} else if rest_of_formula.starts_with(&errors.name) { } else if rest_of_formula.starts_with(&errors.name) {
self.position += errors.name.chars().count() - 1; self.position += errors.name.chars().count() - 1;

View File

@@ -1,6 +1,6 @@
mod test_common; mod test_common;
mod test_language; mod test_language;
mod test_locale; mod test_locale;
mod test_marked_token;
mod test_ranges; mod test_ranges;
mod test_tables; mod test_tables;
mod test_util;

View File

@@ -6,11 +6,11 @@ use crate::{
token::TokenType, token::TokenType,
}, },
language::get_language, language::get_language,
locale::get_locale_fix, locale::get_locale,
}; };
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer { fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
let locale = get_locale_fix(locale).unwrap(); let locale = get_locale(locale).unwrap();
let language = get_language(language).unwrap(); let language = get_language(language).unwrap();
Lexer::new(formula, LexerMode::A1, locale, language) Lexer::new(formula, LexerMode::A1, locale, language)
} }

View File

@@ -1,5 +1,5 @@
use crate::expressions::{ use crate::expressions::{
lexer::util::get_tokens, lexer::marked_token::{get_tokens, MarkedToken},
token::{OpCompare, OpSum, TokenType}, token::{OpCompare, OpSum, TokenType},
}; };
@@ -22,6 +22,29 @@ fn test_get_tokens() {
assert_eq!(l.end, 10); assert_eq!(l.end, 10);
} }
#[test]
fn chinese_characters() {
let formula = "\"你好\" & \"世界!\"";
let marked_tokens = get_tokens(formula);
assert_eq!(marked_tokens.len(), 3);
let first_t = MarkedToken {
token: TokenType::String("你好".to_string()),
start: 0,
end: 4,
};
let second_t = MarkedToken {
token: TokenType::And,
start: 4,
end: 6,
};
let third_t = MarkedToken {
token: TokenType::String("世界!".to_string()),
start: 6,
end: 12,
};
assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
}
#[test] #[test]
fn test_simple_tokens() { fn test_simple_tokens() {
assert_eq!( assert_eq!(

View File

@@ -1,4 +1,3 @@
// public modules
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
pub mod token; pub mod token;

View File

@@ -1,31 +1,29 @@
/*! //! # GRAMMAR
# GRAMAR //!
//! <pre class="rust">
<pre class="rust"> //! opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>' //! opFactor => '*' | '/'
opFactor => '*' | '/' //! unaryOp => '-' | '+'
unaryOp => '-' | '+' //!
//! expr => concat (opComp concat)*
expr => concat (opComp concat)* //! concat => term ('&' term)*
concat => term ('&' term)* //! term => factor (opFactor factor)*
term => factor (opFactor factor)* //! factor => prod (opProd prod)*
factor => prod (opProd prod)* //! prod => power ('^' power)*
prod => power ('^' power)* //! power => (unaryOp)* range '%'*
power => (unaryOp)* range '%'* //! range => primary (':' primary)?
range => primary (':' primary)? //! primary => '(' expr ')'
primary => '(' expr ')' //! => number
=> number //! => function '(' f_args ')'
=> function '(' f_args ')' //! => name
=> name //! => string
=> string //! => '{' a_args '}'
=> '{' a_args '}' //! => bool
=> bool //! => bool()
=> bool() //! => error
=> error //!
//! f_args => e (',' e)*
f_args => e (',' e)* //! </pre>
</pre>
*/
use std::collections::HashMap; use std::collections::HashMap;
@@ -44,21 +42,15 @@ use super::utils::number_to_column;
use token::OpCompare; use token::OpCompare;
pub mod move_formula; pub(crate) mod move_formula;
pub(crate) mod walk;
/// Produces a string representation of a formula from the AST.
pub mod stringify; pub mod stringify;
pub mod walk;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
#[cfg(test)]
mod test_ranges;
#[cfg(test)]
mod test_move_formula;
#[cfg(test)]
mod test_tables;
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> { pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
let mut lexer = lexer::Lexer::new( let mut lexer = lexer::Lexer::new(
formula, formula,
@@ -222,7 +214,7 @@ impl Parser {
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node { pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
self.lexer.set_formula(formula); self.lexer.set_formula(formula);
self.context = context.clone(); self.context.clone_from(context);
self.parse_expr() self.parse_expr()
} }

View File

@@ -1,43 +1,75 @@
#![deny(missing_docs)]
use super::{super::utils::quote_name, Node, Reference}; use super::{super::utils::quote_name, Node, Reference};
use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::token::OpUnary; use crate::expressions::token::OpUnary;
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str}; use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
/// Displaced data
pub enum DisplaceData { pub enum DisplaceData {
/// Displaces columns (inserting or deleting columns)
Column { Column {
/// Sheet in which the displace data applies
sheet: u32, sheet: u32,
/// Column from which the data is displaced
column: i32, column: i32,
/// Number of columns displaced (might be negative, e.g. when deleting columns)
delta: i32, delta: i32,
}, },
/// Displaces rows (Inserting or deleting rows)
Row { Row {
/// Sheet in which the displace data applies
sheet: u32, sheet: u32,
/// Row from which the data is displaced
row: i32, row: i32,
/// Number of rows displaced (might be negative, e.g. when deleting rows)
delta: i32, delta: i32,
}, },
CellHorizontal { /// Displaces cells horizontally
ShiftCellsRight {
/// Sheet in which the displace data applies
sheet: u32, sheet: u32,
/// Row of the to left corner
row: i32, row: i32,
/// Column of the top left corner
column: i32, column: i32,
delta: i32, /// Number of rows to be displaced
row_delta: i32,
/// Number of columns to be displaced (might be negative)
column_delta: i32,
}, },
CellVertical { /// Displaces cells vertically
ShiftCellsDown {
/// Sheet in which the displace data applies
sheet: u32, sheet: u32,
/// Row of the to left corner
row: i32, row: i32,
/// Column of the top left corner
column: i32, column: i32,
delta: i32, /// Number of rows displaced (might be negative)
row_delta: i32,
/// Number of columns to be displaced
column_delta: i32,
}, },
/// Displaces data due to a column move from column to column + delta
ColumnMove { ColumnMove {
/// Sheet in which the displace data applies
sheet: u32, sheet: u32,
/// Column that is moved
column: i32, column: i32,
/// The position of the new column is column + delta (might be negative)
delta: i32, delta: i32,
}, },
/// Doesn't do any cell displacement
None, None,
} }
/// Stringifies the AST formula in its internal R1C1 format
pub fn to_rc_format(node: &Node) -> String { pub fn to_rc_format(node: &Node) -> String {
stringify(node, None, &DisplaceData::None, false) stringify(node, None, &DisplaceData::None, false)
} }
/// Stringifies the formula applying the _displace_data_.
pub fn to_string_displaced( pub fn to_string_displaced(
node: &Node, node: &Node,
context: &CellReferenceRC, context: &CellReferenceRC,
@@ -46,10 +78,12 @@ pub fn to_string_displaced(
stringify(node, Some(context), displace_data, false) stringify(node, Some(context), displace_data, false)
} }
/// Stringifies a formula from the AST
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String { pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, false) stringify(node, Some(context), &DisplaceData::None, false)
} }
/// Stringifies the formula for Excel compatibility
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String { pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
stringify(node, Some(context), &DisplaceData::None, true) stringify(node, Some(context), &DisplaceData::None, true)
} }
@@ -116,41 +150,49 @@ pub(crate) fn stringify_reference(
} }
} }
} }
DisplaceData::CellHorizontal { DisplaceData::ShiftCellsRight {
sheet, sheet,
row: displace_row, row: displace_row,
column: displace_column, column: displace_column,
delta, column_delta,
row_delta,
} => { } => {
if sheet_index == *sheet && displace_row == &row { if sheet_index == *sheet
if *delta < 0 { && displace_row >= &row
&& *displace_row < row + *row_delta
{
if *column_delta < 0 {
if &column >= displace_column { if &column >= displace_column {
if column < displace_column - *delta { if column < displace_column - *column_delta {
return "#REF!".to_string(); return "#REF!".to_string();
} }
column += *delta; column += *column_delta;
} }
} else if &column >= displace_column { } else if &column >= displace_column {
column += *delta; column += *column_delta;
} }
} }
} }
DisplaceData::CellVertical { DisplaceData::ShiftCellsDown {
sheet, sheet,
row: displace_row, row: displace_row,
column: displace_column, column: displace_column,
delta, row_delta,
column_delta,
} => { } => {
if sheet_index == *sheet && displace_column == &column { if sheet_index == *sheet
if *delta < 0 { && displace_column >= &column
&& *displace_column < column + *column_delta
{
if *row_delta < 0 {
if &row >= displace_row { if &row >= displace_row {
if row < displace_row - *delta { if row < displace_row - *row_delta {
return "#REF!".to_string(); return "#REF!".to_string();
} }
row += *delta; row += *row_delta;
} }
} else if &row >= displace_row { } else if &row >= displace_row {
row += *delta; row += *row_delta;
} }
} }
} }

View File

@@ -0,0 +1,4 @@
mod test_genertal;
mod test_move_formula;
mod test_ranges;
mod test_tables;

View File

@@ -1,17 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::lexer::LexerMode; use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::DisplaceData; use crate::expressions::parser::stringify::{to_string_displaced, DisplaceData};
use crate::expressions::parser::{
use super::super::types::CellReferenceRC;
use super::Parser;
use super::{
super::parser::{
stringify::{to_rc_format, to_string}, stringify::{to_rc_format, to_string},
Node, Node, Parser,
},
stringify::to_string_displaced,
}; };
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> { struct Formula<'a> {
initial: &'a str, initial: &'a str,

View File

@@ -1,10 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::expressions::parser::move_formula::{move_formula, MoveContext}; use crate::expressions::parser::move_formula::{move_formula, MoveContext};
use crate::expressions::types::Area; use crate::expressions::parser::Parser;
use crate::expressions::types::{Area, CellReferenceRC};
use super::super::types::CellReferenceRC;
use super::Parser;
#[test] #[test]
fn test_move_formula() { fn test_move_formula() {

View File

@@ -2,9 +2,9 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode; use crate::expressions::lexer::LexerMode;
use super::super::parser::stringify::{to_rc_format, to_string}; use crate::expressions::parser::stringify::{to_rc_format, to_string};
use super::super::types::CellReferenceRC; use crate::expressions::parser::Parser;
use super::Parser; use crate::expressions::types::CellReferenceRC;
struct Formula<'a> { struct Formula<'a> {
formula_a1: &'a str, formula_a1: &'a str,

View File

@@ -6,8 +6,8 @@ use crate::expressions::parser::stringify::to_string;
use crate::expressions::utils::{number_to_column, parse_reference_a1}; use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo}; use crate::types::{Table, TableColumn, TableStyleInfo};
use super::super::types::CellReferenceRC; use crate::expressions::parser::Parser;
use super::Parser; use crate::expressions::types::CellReferenceRC;
fn create_test_table( fn create_test_table(
table_name: &str, table_name: &str,

View File

@@ -2,7 +2,6 @@ use std::fmt;
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::language::Language; use crate::language::Language;
@@ -81,8 +80,7 @@ impl fmt::Display for OpProduct {
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+") /// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc /// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
/// Note that they are serialized/deserialized by index /// Note that they are serialized/deserialized by index
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
pub enum Error { pub enum Error {
REF, REF,
NAME, NAME,
@@ -120,7 +118,7 @@ impl Error {
pub fn to_localized_error_string(&self, language: &Language) -> String { pub fn to_localized_error_string(&self, language: &Language) -> String {
match self { match self {
Error::NULL => language.errors.null.to_string(), Error::NULL => language.errors.null.to_string(),
Error::REF => language.errors.ref_value.to_string(), Error::REF => language.errors.r#ref.to_string(),
Error::NAME => language.errors.name.to_string(), Error::NAME => language.errors.name.to_string(),
Error::VALUE => language.errors.value.to_string(), Error::VALUE => language.errors.value.to_string(),
Error::DIV => language.errors.div.to_string(), Error::DIV => language.errors.div.to_string(),
@@ -137,7 +135,7 @@ impl Error {
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> { pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
let errors = &language.errors; let errors = &language.errors;
if name == errors.ref_value { if name == errors.r#ref {
return Some(Error::REF); return Some(Error::REF);
} else if name == errors.name { } else if name == errors.name {
return Some(Error::NAME); return Some(Error::NAME);

View File

@@ -3,7 +3,7 @@ use std::fmt;
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
expressions::{ expressions::{
lexer::util::get_tokens, lexer::marked_token::get_tokens,
parser::Node, parser::Node,
token::{Error, OpSum, TokenType}, token::{Error, OpSum, TokenType},
types::CellReferenceIndex, types::CellReferenceIndex,

View File

@@ -221,7 +221,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -229,7 +229,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {
@@ -284,7 +284,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -292,7 +292,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {
@@ -360,7 +360,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -368,7 +368,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {
@@ -866,7 +866,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -874,7 +874,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {

View File

@@ -132,7 +132,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -140,7 +140,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {
@@ -199,7 +199,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -207,7 +207,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {

View File

@@ -385,7 +385,7 @@ impl Model {
.workbook .workbook
.worksheet(first_range.left.sheet) .worksheet(first_range.left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension(); .get_dimension();
let max_row = dimension.max_row; let max_row = dimension.max_row;
let max_column = dimension.max_column; let max_column = dimension.max_column;
@@ -530,7 +530,7 @@ impl Model {
.workbook .workbook
.worksheet(sum_range.left.sheet) .worksheet(sum_range.left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if left_column == 1 && right_column == LAST_COLUMN { if left_column == 1 && right_column == LAST_COLUMN {
@@ -538,7 +538,7 @@ impl Model {
.workbook .workbook
.worksheet(sum_range.left.sheet) .worksheet(sum_range.left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }

View File

@@ -892,7 +892,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -900,7 +900,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
for row in row1..row2 + 1 { for row in row1..row2 + 1 {

View File

@@ -255,7 +255,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_row; .max_row;
} }
if column1 == 1 && column2 == LAST_COLUMN { if column1 == 1 && column2 == LAST_COLUMN {
@@ -263,7 +263,7 @@ impl Model {
.workbook .workbook
.worksheet(left.sheet) .worksheet(left.sheet)
.expect("Sheet expected during evaluation.") .expect("Sheet expected during evaluation.")
.dimension() .get_dimension()
.max_column; .max_column;
} }
let left = CellReferenceIndex { let left = CellReferenceIndex {

View File

@@ -0,0 +1 @@
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!

View File

@@ -1,20 +1,17 @@
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)] use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)]
pub struct Booleans { pub struct Booleans {
#[serde(rename = "true")] pub r#true: String,
pub true_value: String, pub r#false: String,
#[serde(rename = "false")]
pub false_value: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Errors { pub struct Errors {
#[serde(rename = "ref")] pub r#ref: String,
pub ref_value: String,
pub name: String, pub name: String,
pub value: String, pub value: String,
pub div: String, pub div: String,
@@ -28,14 +25,14 @@ pub struct Errors {
pub null: String, pub null: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Language { pub struct Language {
pub booleans: Booleans, pub booleans: Booleans,
pub errors: Errors, pub errors: Errors,
} }
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| { static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file") bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
}); });
pub fn get_language(id: &str) -> Result<&Language, String> { pub fn get_language(id: &str) -> Result<&Language, String> {

BIN
base/src/locale/locales.bin Normal file

Binary file not shown.

View File

@@ -1,32 +1,29 @@
use bitcode::{Decode, Encode};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Locale { pub struct Locale {
pub dates: Dates, pub dates: Dates,
pub numbers: NumbersProperties, pub numbers: NumbersProperties,
pub currency: Currency, pub currency: Currency,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Currency { pub struct Currency {
pub iso: String, pub iso: String,
pub symbol: String, pub symbol: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct NumbersProperties { pub struct NumbersProperties {
#[serde(rename = "symbols-numberSystem-latn")]
pub symbols: NumbersSymbols, pub symbols: NumbersSymbols,
#[serde(rename = "decimalFormats-numberSystem-latn")]
pub decimal_formats: DecimalFormats, pub decimal_formats: DecimalFormats,
#[serde(rename = "currencyFormats-numberSystem-latn")]
pub currency_formats: CurrencyFormats, pub currency_formats: CurrencyFormats,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct Dates { pub struct Dates {
pub day_names: Vec<String>, pub day_names: Vec<String>,
pub day_names_short: Vec<String>, pub day_names_short: Vec<String>,
@@ -35,8 +32,7 @@ pub struct Dates {
pub months_letter: Vec<String>, pub months_letter: Vec<String>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
#[serde(rename_all = "camelCase")]
pub struct NumbersSymbols { pub struct NumbersSymbols {
pub decimal: String, pub decimal: String,
pub group: String, pub group: String,
@@ -54,40 +50,26 @@ pub struct NumbersSymbols {
} }
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns // See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
pub struct CurrencyFormats { pub struct CurrencyFormats {
pub standard: String, pub standard: String,
#[serde(rename = "standard-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub standard_alpha_next_to_number: Option<String>, pub standard_alpha_next_to_number: Option<String>,
#[serde(rename = "standard-noCurrency")]
pub standard_no_currency: String, pub standard_no_currency: String,
pub accounting: String, pub accounting: String,
#[serde(rename = "accounting-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub accounting_alpha_next_to_number: Option<String>, pub accounting_alpha_next_to_number: Option<String>,
#[serde(rename = "accounting-noCurrency")]
pub accounting_no_currency: String, pub accounting_no_currency: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Encode, Decode, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DecimalFormats { pub struct DecimalFormats {
pub standard: String, pub standard: String,
} }
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| { static LOCALES: Lazy<HashMap<String, Locale>> =
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale") Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
});
pub fn get_locale(_id: &str) -> Result<&Locale, String> { pub fn get_locale(id: &str) -> Result<&Locale, String> {
// TODO: pass the locale once we implement locales in Rust // TODO: pass the locale once we implement locales in Rust
let locale = LOCALES.get("en").ok_or("Invalid locale")?;
Ok(locale)
}
// TODO: Remove this function one we implement locales properly
pub fn get_locale_fix(id: &str) -> Result<&Locale, String> {
let locale = LOCALES.get(id).ok_or("Invalid locale")?; let locale = LOCALES.get(id).ok_or("Invalid locale")?;
Ok(locale) Ok(locale)
} }

View File

@@ -798,7 +798,7 @@ impl Model {
None None
} }
/// Returns a model from a String representation of a workbook /// Returns a model from an internal binary representation of a workbook
/// ///
/// # Examples /// # Examples
/// ///
@@ -816,9 +816,12 @@ impl Model {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
///
/// See also:
/// * [Model::to_bytes]
pub fn from_bytes(s: &[u8]) -> Result<Model, String> { pub fn from_bytes(s: &[u8]) -> Result<Model, String> {
let workbook: Workbook = let workbook: Workbook =
bitcode::decode(s).map_err(|_| "Error parsing workbook".to_string())?; bitcode::decode(s).map_err(|e| format!("Error parsing workbook: {e}"))?;
Model::from_workbook(workbook) Model::from_workbook(workbook)
} }
@@ -1760,7 +1763,10 @@ impl Model {
.get_style(self.get_cell_style_index(sheet, row, column)) .get_style(self.get_cell_style_index(sheet, row, column))
} }
/// Returns a JSON string of the workbook /// Returns an internal binary representation of the workbook
///
/// See also:
/// * [Model::from_bytes]
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
bitcode::encode(&self.workbook) bitcode::encode(&self.workbook)
} }
@@ -1782,7 +1788,7 @@ impl Model {
/// Returns markup representation of the given `sheet`. /// Returns markup representation of the given `sheet`.
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> { pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
let worksheet = self.workbook.worksheet(sheet)?; let worksheet = self.workbook.worksheet(sheet)?;
let dimension = worksheet.dimension(); let dimension = worksheet.get_dimension();
let mut rows = Vec::new(); let mut rows = Vec::new();

View File

@@ -6,14 +6,16 @@ use crate::{
calc_result::Range, calc_result::Range,
expressions::{ expressions::{
lexer::LexerMode, lexer::LexerMode,
parser::stringify::{rename_sheet_in_node, to_rc_format}, parser::{
parser::Parser, stringify::{rename_sheet_in_node, to_rc_format},
Parser,
},
types::CellReferenceRC, types::CellReferenceRC,
}, },
language::get_language, language::get_language,
locale::get_locale, locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName}, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet}, types::{Metadata, Selection, SheetState, Workbook, WorkbookSettings, Worksheet},
utils::ParsedReference, utils::ParsedReference,
}; };
@@ -48,6 +50,12 @@ impl Model {
color: Default::default(), color: Default::default(),
frozen_columns: 0, frozen_columns: 0,
frozen_rows: 0, frozen_rows: 0,
selection: Selection {
is_selected: false,
row: 1,
column: 1,
range: [1, 1, 1, 1],
},
} }
} }

View File

@@ -0,0 +1,18 @@
mod test_fn_average;
mod test_fn_averageifs;
mod test_fn_choose;
mod test_fn_concatenate;
mod test_fn_count;
mod test_fn_exact;
mod test_fn_financial;
mod test_fn_if;
mod test_fn_maxifs;
mod test_fn_minifs;
mod test_fn_offset;
mod test_fn_product;
mod test_fn_rept;
mod test_fn_sum;
mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_fn_type;

View File

@@ -1,3 +1,5 @@
mod engineering;
mod functions;
mod test_actions; mod test_actions;
mod test_binary_search; mod test_binary_search;
mod test_cell; mod test_cell;
@@ -8,49 +10,30 @@ mod test_criteria;
mod test_currency; mod test_currency;
mod test_date_and_time; mod test_date_and_time;
mod test_error_propagation; mod test_error_propagation;
mod test_fn_average; mod test_escape_quotes;
mod test_fn_averageifs; mod test_extend;
mod test_fn_choose;
mod test_fn_concatenate;
mod test_fn_count;
mod test_fn_exact;
mod test_fn_financial;
mod test_fn_if;
mod test_fn_maxifs;
mod test_fn_minifs;
mod test_fn_product;
mod test_fn_rept;
mod test_fn_sum;
mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_forward_references; mod test_forward_references;
mod test_frozen_rows_and_columns;
mod test_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
mod test_get_cell_content;
mod test_math; mod test_math;
mod test_metadata; mod test_metadata;
mod test_model_cell_clear_all; mod test_model_cell_clear_all;
mod test_model_is_empty_cell; mod test_model_is_empty_cell;
mod test_move_formula; mod test_move_formula;
mod test_number_format;
mod test_percentage;
mod test_quote_prefix; mod test_quote_prefix;
mod test_set_user_input; mod test_set_user_input;
mod test_sheet_markup; mod test_sheet_markup;
mod test_sheets; mod test_sheets;
mod test_shift_cells;
mod test_styles; mod test_styles;
mod test_today;
mod test_trigonometric; mod test_trigonometric;
mod test_types;
mod test_workbook; mod test_workbook;
mod test_worksheet; mod test_worksheet;
pub(crate) mod util;
mod engineering;
mod test_fn_offset;
mod test_number_format;
mod test_escape_quotes;
mod test_extend;
mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_get_cell_content;
mod test_percentage;
mod test_today;
mod user_model; mod user_model;
pub(crate) mod util;

View File

@@ -0,0 +1,110 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn shift_cells_right() {
let mut model = new_empty_model();
let (sheet, row, column) = (0, 5, 3); // C5
model.set_user_input(sheet, row, column, "Hi".to_string());
model.set_user_input(sheet, row, column + 1, "world".to_string());
model.set_user_input(sheet, row, column + 2, "!".to_string());
model
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
.unwrap();
model.evaluate();
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
assert_eq!(model.get_cell_content(0, 5, 4), Ok("Hi".to_string()));
}
#[test]
fn shift_cells_right_with_formulas() {
let mut model = new_empty_model();
let (sheet, row, column) = (0, 5, 3); // C5
model.set_user_input(sheet, row, column - 1, "23".to_string());
model.set_user_input(sheet, row, column, "42".to_string());
model.set_user_input(sheet, row, column + 1, "=C5*2".to_string());
model.set_user_input(sheet, row, column + 2, "=A5+2".to_string());
model.set_user_input(sheet, 20, 3, "=C5*A2".to_string());
model.evaluate();
model
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
.unwrap();
model.evaluate();
assert_eq!(
model.get_cell_content(0, row, column - 1),
Ok("23".to_string())
);
assert_eq!(model.get_cell_content(0, row, column), Ok("".to_string()));
assert_eq!(
model.get_cell_content(0, row, column + 1),
Ok("42".to_string())
);
assert_eq!(
model.get_cell_content(0, row, column + 2),
Ok("=D5*2".to_string())
);
assert_eq!(
model.get_cell_content(0, row, column + 3),
Ok("=A5+2".to_string())
);
assert_eq!(model.get_cell_content(0, 20, 3), Ok("=D5*A2".to_string()));
}
#[test]
fn shift_cells_left() {
let mut model = new_empty_model();
let (sheet, row, column) = (0, 5, 10); // J5
model.set_user_input(sheet, row, column - 1, "23".to_string());
model.set_user_input(sheet, row, column, "42".to_string());
model.set_user_input(sheet, row, column + 1, "Hi".to_string());
model.set_user_input(sheet, row, column + 2, "honey!".to_string());
model.evaluate();
model
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
.unwrap();
model.evaluate();
assert_eq!(
model.get_cell_content(0, row, column - 1),
Ok("23".to_string())
);
assert_eq!(model.get_cell_content(0, row, column), Ok("Hi".to_string()));
assert_eq!(
model.get_cell_content(0, row, column + 1),
Ok("honey!".to_string())
);
}
#[test]
fn shift_cells_left_with_formulas() {
let mut model = new_empty_model();
let (sheet, row, column) = (0, 5, 10); // J5
model.set_user_input(sheet, row, column - 1, "23".to_string());
model.set_user_input(sheet, row, column, "42".to_string());
model.set_user_input(sheet, row, column + 1, "33".to_string());
model.set_user_input(sheet, row, column + 2, "=K5*A5".to_string());
model.set_user_input(sheet, row, column + 20, "=K5*A5".to_string());
model.evaluate();
model
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
.unwrap();
model.evaluate();
assert_eq!(
model.get_cell_content(0, row, column + 1),
Ok("=J5*A5".to_string())
);
assert_eq!(
model.get_cell_content(0, row, column + 19),
Ok("=J5*A5".to_string())
);
}

View File

@@ -0,0 +1,24 @@
#![allow(clippy::unwrap_used)]
use crate::types::{Alignment, HorizontalAlignment, VerticalAlignment};
#[test]
fn alignment_default() {
let alignment = Alignment::default();
assert_eq!(
alignment,
Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Bottom,
wrap_text: false
}
);
let s = serde_json::to_string(&alignment).unwrap();
// defaults stringifies as an empty object
assert_eq!(s, "{}");
let a: Alignment = serde_json::from_str("{}").unwrap();
assert_eq!(a, alignment)
}

View File

@@ -10,7 +10,7 @@ use crate::{
fn test_worksheet_dimension_empty_sheet() { fn test_worksheet_dimension_empty_sheet() {
let model = new_empty_model(); let model = new_empty_model();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 1, min_row: 1,
min_column: 1, min_column: 1,
@@ -25,7 +25,7 @@ fn test_worksheet_dimension_single_cell() {
let mut model = new_empty_model(); let mut model = new_empty_model();
model._set("W11", "1"); model._set("W11", "1");
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 11, min_row: 11,
min_column: 23, min_column: 23,
@@ -41,7 +41,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
model._set("W11", "1"); model._set("W11", "1");
model.cell_clear_contents(0, 11, 23).unwrap(); model.cell_clear_contents(0, 11, 23).unwrap();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 11, min_row: 11,
min_column: 23, min_column: 23,
@@ -57,7 +57,7 @@ fn test_worksheet_dimension_single_cell_deleted() {
model._set("W11", "1"); model._set("W11", "1");
model.cell_clear_all(0, 11, 23).unwrap(); model.cell_clear_all(0, 11, 23).unwrap();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 1, min_row: 1,
min_column: 1, min_column: 1,
@@ -77,7 +77,7 @@ fn test_worksheet_dimension_multiple_cells() {
model._set("B19", "1"); model._set("B19", "1");
model.cell_clear_all(0, 11, 23).unwrap(); model.cell_clear_all(0, 11, 23).unwrap();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 11, min_row: 11,
min_column: 2, min_column: 2,
@@ -91,7 +91,7 @@ fn test_worksheet_dimension_multiple_cells() {
fn test_worksheet_dimension_progressive() { fn test_worksheet_dimension_progressive() {
let mut model = new_empty_model(); let mut model = new_empty_model();
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 1, min_row: 1,
min_column: 1, min_column: 1,
@@ -102,7 +102,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 30, 50, "Hello World".to_string()); model.set_user_input(0, 30, 50, "Hello World".to_string());
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 30, min_row: 30,
min_column: 50, min_column: 50,
@@ -113,7 +113,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 10, 15, "Hello World".to_string()); model.set_user_input(0, 10, 15, "Hello World".to_string());
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 10, min_row: 10,
min_column: 15, min_column: 15,
@@ -124,7 +124,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 5, 25, "Hello World".to_string()); model.set_user_input(0, 5, 25, "Hello World".to_string());
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 5, min_row: 5,
min_column: 15, min_column: 15,
@@ -135,7 +135,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 10, 250, "Hello World".to_string()); model.set_user_input(0, 10, 250, "Hello World".to_string());
assert_eq!( assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(), model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension { WorksheetDimension {
min_row: 5, min_row: 5,
min_column: 15, min_column: 15,

View File

@@ -5,6 +5,7 @@ mod test_evaluation;
mod test_general; mod test_general;
mod test_rename_sheet; mod test_rename_sheet;
mod test_row_column; mod test_row_column;
mod test_shift_cells;
mod test_styles; mod test_styles;
mod test_to_from_bytes; mod test_to_from_bytes;
mod test_undo_redo; mod test_undo_redo;

View File

@@ -25,7 +25,7 @@ fn send_queue() {
#[test] #[test]
fn apply_external_diffs_wrong_str() { fn apply_external_diffs_wrong_str() {
let mut model1 = UserModel::from_model(new_empty_model()); let mut model1 = UserModel::from_model(new_empty_model());
assert!(model1.apply_external_diffs("invalid").is_err()); assert!(model1.apply_external_diffs("invalid".as_bytes()).is_err());
} }
#[test] #[test]
@@ -155,5 +155,7 @@ fn new_sheet() {
#[test] #[test]
fn wrong_diffs_handled() { fn wrong_diffs_handled() {
let mut model = UserModel::from_model(new_empty_model()); let mut model = UserModel::from_model(new_empty_model());
assert!(model.apply_external_diffs("Hello world").is_err()); assert!(model
.apply_external_diffs("Hello world".as_bytes())
.is_err());
} }

View File

@@ -0,0 +1,23 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
#[test]
fn shift_cells_general() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
// some reference value in A1
model.set_user_input(0, 1, 1, "42").unwrap();
// We put some values in row 5
model.set_user_input(0, 5, 3, "=1 + 1").unwrap(); // C5
model.set_user_input(0, 5, 7, "=C5*A1").unwrap();
// Insert one cell in C5 and push right
model.insert_cells_and_shift_right(0, 5, 3, 1, 1).unwrap();
// C5 should now be empty
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
// D5 should have 2
assert_eq!(model.get_cell_content(0, 5, 4), Ok("=1+1".to_string()));
}

View File

@@ -144,13 +144,18 @@ fn basic_fill() {
let style = model.get_cell_style(0, 1, 1).unwrap(); let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, None); assert_eq!(style.fill.bg_color, None);
assert_eq!(style.fill.fg_color, None);
// bg_color // bg_color
model model
.update_range_style(&range, "fill.bg_color", "#F2F2F2") .update_range_style(&range, "fill.bg_color", "#F2F2F2")
.unwrap(); .unwrap();
model
.update_range_style(&range, "fill.fg_color", "#F3F4F5")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap(); let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned())); assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
let send_queue = model.flush_send_queue(); let send_queue = model.flush_send_queue();
@@ -159,6 +164,7 @@ fn basic_fill() {
let style = model2.get_cell_style(0, 1, 1).unwrap(); let style = model2.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned())); assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
} }
#[test] #[test]
@@ -171,9 +177,15 @@ fn fill_errors() {
width: 1, width: 1,
height: 1, height: 1,
}; };
assert!(model assert_eq!(
.update_range_style(&range, "fill.bg_color", "#FFF") model.update_range_style(&range, "fill.bg_color", "#FFF"),
.is_err()); Err("Invalid color: '#FFF'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "fill.fg_color", "#FFF"),
Err("Invalid color: '#FFF'.".to_string())
);
} }
#[test] #[test]

View File

@@ -25,6 +25,6 @@ fn errors() {
let model_bytes = "Early in the morning, late in the century, Cricklewood Broadway.".as_bytes(); let model_bytes = "Early in the morning, late in the century, Cricklewood Broadway.".as_bytes();
assert_eq!( assert_eq!(
&UserModel::from_bytes(model_bytes).unwrap_err(), &UserModel::from_bytes(model_bytes).unwrap_err(),
"Error parsing workbook" "Error parsing workbook: invalid packing"
); );
} }

View File

@@ -4,37 +4,15 @@ use std::{collections::HashMap, fmt::Display};
use crate::expressions::token::Error; use crate::expressions::token::Error;
// Useful for `#[serde(default = "default_as_true")]`
fn default_as_true() -> bool {
true
}
fn default_as_false() -> bool { fn default_as_false() -> bool {
false false
} }
// Useful for `#[serde(skip_serializing_if = "is_true")]`
fn is_true(b: &bool) -> bool {
*b
}
fn is_false(b: &bool) -> bool { fn is_false(b: &bool) -> bool {
!*b !*b
} }
fn is_zero(num: &i32) -> bool { #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
*num == 0
}
fn is_default_alignment(o: &Option<Alignment>) -> bool {
o.is_none() || *o == Some(Alignment::default())
}
fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
h.values().len() == 0
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Metadata { pub struct Metadata {
pub application: String, pub application: String,
pub app_version: String, pub app_version: String,
@@ -44,14 +22,13 @@ pub struct Metadata {
pub last_modified: String, //"2020-11-20T16:24:35" pub last_modified: String, //"2020-11-20T16:24:35"
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct WorkbookSettings { pub struct WorkbookSettings {
pub tz: String, pub tz: String,
pub locale: String, pub locale: String,
} }
/// An internal representation of an IronCalc Workbook /// An internal representation of an IronCalc Workbook
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
#[serde(deny_unknown_fields)]
pub struct Workbook { pub struct Workbook {
pub shared_strings: Vec<String>, pub shared_strings: Vec<String>,
pub defined_names: Vec<DefinedName>, pub defined_names: Vec<DefinedName>,
@@ -60,17 +37,14 @@ pub struct Workbook {
pub name: String, pub name: String,
pub settings: WorkbookSettings, pub settings: WorkbookSettings,
pub metadata: Metadata, pub metadata: Metadata,
#[serde(default)]
#[serde(skip_serializing_if = "hashmap_is_empty")]
pub tables: HashMap<String, Table>, pub tables: HashMap<String, Table>,
} }
/// A defined name. The `sheet_id` is the sheet index in case the name is local /// A defined name. The `sheet_id` is the sheet index in case the name is local
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct DefinedName { pub struct DefinedName {
pub name: String, pub name: String,
pub formula: String, pub formula: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sheet_id: Option<u32>, pub sheet_id: Option<u32>,
} }
@@ -80,8 +54,7 @@ pub struct DefinedName {
/// * state: /// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types) /// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible /// hidden, veryHidden, visible
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum SheetState { pub enum SheetState {
Visible, Visible,
Hidden, Hidden,
@@ -98,8 +71,16 @@ impl Display for SheetState {
} }
} }
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Selection {
pub is_selected: bool,
pub row: i32,
pub column: i32,
pub range: [i32; 4],
}
/// Internal representation of a worksheet Excel object /// Internal representation of a worksheet Excel object
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Worksheet { pub struct Worksheet {
pub dimension: String, pub dimension: String,
pub cols: Vec<Col>, pub cols: Vec<Col>,
@@ -109,16 +90,12 @@ pub struct Worksheet {
pub shared_formulas: Vec<String>, pub shared_formulas: Vec<String>,
pub sheet_id: u32, pub sheet_id: u32,
pub state: SheetState, pub state: SheetState,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>, pub color: Option<String>,
pub merge_cells: Vec<String>, pub merge_cells: Vec<String>,
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_rows: i32, pub frozen_rows: i32,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_columns: i32, pub frozen_columns: i32,
pub selection: Selection,
} }
/// Internal representation of Excel's sheet_data /// Internal representation of Excel's sheet_data
@@ -126,7 +103,7 @@ pub struct Worksheet {
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>; pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
// ECMA-376-1:2016 section 18.3.1.73 // ECMA-376-1:2016 section 18.3.1.73
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Row { pub struct Row {
/// Row index /// Row index
pub r: i32, pub r: i32,
@@ -134,23 +111,19 @@ pub struct Row {
pub custom_format: bool, pub custom_format: bool,
pub custom_height: bool, pub custom_height: bool,
pub s: i32, pub s: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub hidden: bool, pub hidden: bool,
} }
// ECMA-376-1:2016 section 18.3.1.13 // ECMA-376-1:2016 section 18.3.1.13
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Col { pub struct Col {
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries. // Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
/// First column affected by this record. Settings apply to column in \[min, max\] range. /// First column affected by this record. Settings apply to column in \[min, max\] range.
pub min: i32, pub min: i32,
/// Last column affected by this record. Settings apply to column in \[min, max\] range. /// Last column affected by this record. Settings apply to column in \[min, max\] range.
pub max: i32, pub max: i32,
pub width: f64, pub width: f64,
pub custom_width: bool, pub custom_width: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<i32>, pub style: Option<i32>,
} }
@@ -165,32 +138,55 @@ pub enum CellType {
CompoundData = 128, CompoundData = 128,
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq)] #[derive(Encode, Decode, Debug, Clone, PartialEq)]
#[serde(tag = "t", deny_unknown_fields)]
pub enum Cell { pub enum Cell {
#[serde(rename = "empty")] EmptyCell {
EmptyCell { s: i32 }, s: i32,
#[serde(rename = "b")] },
BooleanCell { v: bool, s: i32 },
#[serde(rename = "n")] BooleanCell {
NumberCell { v: f64, s: i32 }, v: bool,
s: i32,
},
NumberCell {
v: f64,
s: i32,
},
// Maybe we should not have this type. In Excel this is just a string // Maybe we should not have this type. In Excel this is just a string
#[serde(rename = "e")] ErrorCell {
ErrorCell { ei: Error, s: i32 }, ei: Error,
s: i32,
},
// Always a shared string // Always a shared string
#[serde(rename = "s")] SharedString {
SharedString { si: i32, s: i32 }, si: i32,
s: i32,
},
// Non evaluated Formula // Non evaluated Formula
#[serde(rename = "u")] CellFormula {
CellFormula { f: i32, s: i32 }, f: i32,
#[serde(rename = "fb")] s: i32,
CellFormulaBoolean { f: i32, v: bool, s: i32 }, },
#[serde(rename = "fn")]
CellFormulaNumber { f: i32, v: f64, s: i32 }, CellFormulaBoolean {
f: i32,
v: bool,
s: i32,
},
CellFormulaNumber {
f: i32,
v: f64,
s: i32,
},
// always inline string // always inline string
#[serde(rename = "str")] CellFormulaString {
CellFormulaString { f: i32, v: String, s: i32 }, f: i32,
#[serde(rename = "fe")] v: String,
s: i32,
},
CellFormulaError { CellFormulaError {
f: i32, f: i32,
ei: Error, ei: Error,
@@ -209,17 +205,16 @@ impl Default for Cell {
} }
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Comment { pub struct Comment {
pub text: String, pub text: String,
pub author_name: String, pub author_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_id: Option<String>, pub author_id: Option<String>,
pub cell_ref: String, pub cell_ref: String,
} }
// ECMA-376-1:2016 section 18.5.1.2 // ECMA-376-1:2016 section 18.5.1.2
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Table { pub struct Table {
pub name: String, pub name: String,
pub display_name: String, pub display_name: String,
@@ -227,34 +222,24 @@ pub struct Table {
pub reference: String, pub reference: String,
pub totals_row_count: u32, pub totals_row_count: u32,
pub header_row_count: u32, pub header_row_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>, pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>, pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>, pub totals_row_dxf_id: Option<u32>,
pub columns: Vec<TableColumn>, pub columns: Vec<TableColumn>,
pub style_info: TableStyleInfo, pub style_info: TableStyleInfo,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub has_filters: bool, pub has_filters: bool,
} }
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum? // totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
// the totals_row_function is an enum not String methinks // the totals_row_function is an enum not String methinks
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct TableColumn { pub struct TableColumn {
pub id: u32, pub id: u32,
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_label: Option<String>, pub totals_row_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>, pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>, pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>, pub totals_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_function: Option<String>, pub totals_row_function: Option<String>,
} }
@@ -272,25 +257,16 @@ impl Default for TableColumn {
} }
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct TableStyleInfo { pub struct TableStyleInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_first_column: bool, pub show_first_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_last_column: bool, pub show_last_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_row_stripes: bool, pub show_row_stripes: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_column_stripes: bool, pub show_column_stripes: bool,
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Styles { pub struct Styles {
pub num_fmts: Vec<NumFmt>, pub num_fmts: Vec<NumFmt>,
pub fonts: Vec<Font>, pub fonts: Vec<Font>,
@@ -326,7 +302,7 @@ pub struct Style {
pub quote_prefix: bool, pub quote_prefix: bool,
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct NumFmt { pub struct NumFmt {
pub num_fmt_id: i32, pub num_fmt_id: i32,
pub format_code: String, pub format_code: String,
@@ -516,29 +492,17 @@ pub struct Alignment {
pub wrap_text: bool, pub wrap_text: bool,
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyleXfs { pub struct CellStyleXfs {
pub num_fmt_id: i32, pub num_fmt_id: i32,
pub font_id: i32, pub font_id: i32,
pub fill_id: i32, pub fill_id: i32,
pub border_id: i32, pub border_id: i32,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_number_format: bool, pub apply_number_format: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_border: bool, pub apply_border: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_alignment: bool, pub apply_alignment: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_protection: bool, pub apply_protection: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_font: bool, pub apply_font: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_fill: bool, pub apply_fill: bool,
} }
@@ -559,39 +523,24 @@ impl Default for CellStyleXfs {
} }
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct CellXfs { pub struct CellXfs {
pub xf_id: i32, pub xf_id: i32,
pub num_fmt_id: i32, pub num_fmt_id: i32,
pub font_id: i32, pub font_id: i32,
pub fill_id: i32, pub fill_id: i32,
pub border_id: i32, pub border_id: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_number_format: bool, pub apply_number_format: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_border: bool, pub apply_border: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_alignment: bool, pub apply_alignment: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_protection: bool, pub apply_protection: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_font: bool, pub apply_font: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_fill: bool, pub apply_fill: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub quote_prefix: bool, pub quote_prefix: bool,
#[serde(skip_serializing_if = "is_default_alignment")]
pub alignment: Option<Alignment>, pub alignment: Option<Alignment>,
} }
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyles { pub struct CellStyles {
pub name: String, pub name: String,
pub xf_id: i32, pub xf_id: i32,

View File

@@ -2,7 +2,7 @@
use std::{collections::HashMap, fmt::Debug}; use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize, Serialize}; use bitcode::{Decode, Encode};
use crate::{ use crate::{
constants, constants,
@@ -18,19 +18,19 @@ use crate::{
utils::is_valid_hex_color, utils::is_valid_hex_color,
}; };
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Encode, Decode)]
struct RowData { struct RowData {
row: Option<Row>, row: Option<Row>,
data: HashMap<i32, Cell>, data: HashMap<i32, Cell>,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Encode, Decode)]
struct ColumnData { struct ColumnData {
column: Option<Col>, column: Option<Col>,
data: HashMap<i32, Cell>, data: HashMap<i32, Cell>,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Encode, Decode)]
enum Diff { enum Diff {
// Cell diffs // Cell diffs
SetCellValue { SetCellValue {
@@ -91,6 +91,36 @@ enum Diff {
column: i32, column: i32,
old_data: Box<ColumnData>, old_data: Box<ColumnData>,
}, },
InsertCellsShiftRight {
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
},
InsertCellsShiftDown {
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
},
DeleteCellsShiftLeft {
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
old_data: Vec<Vec<Option<Cell>>>,
},
DeleteCellsShiftUp {
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
old_data: Vec<Vec<Option<Cell>>>,
},
SetFrozenRowsCount { SetFrozenRowsCount {
sheet: u32, sheet: u32,
new_value: i32, new_value: i32,
@@ -160,13 +190,13 @@ impl History {
} }
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Encode, Decode)]
enum DiffType { enum DiffType {
Undo, Undo,
Redo, Redo,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Encode, Decode)]
struct QueueDiffs { struct QueueDiffs {
r#type: DiffType, r#type: DiffType,
list: DiffList, list: DiffList,
@@ -408,9 +438,9 @@ impl UserModel {
/// ///
/// See also: /// See also:
/// * [UserModel::apply_external_diffs] /// * [UserModel::apply_external_diffs]
pub fn flush_send_queue(&mut self) -> String { pub fn flush_send_queue(&mut self) -> Vec<u8> {
// This can never fail :O: // This can never fail :O:
let q = serde_json::to_string(&self.send_queue).unwrap(); let q = bitcode::encode(&self.send_queue);
self.send_queue = vec![]; self.send_queue = vec![];
q q
} }
@@ -421,8 +451,8 @@ impl UserModel {
/// ///
/// See also: /// See also:
/// * [UserModel::flush_send_queue] /// * [UserModel::flush_send_queue]
pub fn apply_external_diffs(&mut self, diff_list_str: &str) -> Result<(), String> { pub fn apply_external_diffs(&mut self, diff_list_str: &[u8]) -> Result<(), String> {
if let Ok(queue_diffs_list) = serde_json::from_str::<Vec<QueueDiffs>>(diff_list_str) { if let Ok(queue_diffs_list) = bitcode::decode::<Vec<QueueDiffs>>(diff_list_str) {
for queue_diff in queue_diffs_list { for queue_diff in queue_diffs_list {
if matches!(queue_diff.r#type, DiffType::Redo) { if matches!(queue_diff.r#type, DiffType::Redo) {
self.apply_diff_list(&queue_diff.list)?; self.apply_diff_list(&queue_diff.list)?;
@@ -713,6 +743,123 @@ impl UserModel {
self.model.delete_columns(sheet, column, 1) self.model.delete_columns(sheet, column, 1)
} }
/// Insert cells in the area pushing the existing ones to the right
///
/// See also:
/// * [Model::insert_cells_and_shift_right]
pub fn insert_cells_and_shift_right(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let diff_list = vec![Diff::InsertCellsShiftRight {
sheet,
row,
column,
row_delta,
column_delta,
}];
self.push_diff_list(diff_list);
self.model
.insert_cells_and_shift_right(sheet, row, column, row_delta, column_delta)?;
self.model.evaluate();
Ok(())
}
/// Insert cells in the area pushing the existing ones down
pub fn insert_cells_and_shift_down(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let diff_list = vec![Diff::InsertCellsShiftDown {
sheet,
row,
column,
row_delta,
column_delta,
}];
self.push_diff_list(diff_list);
self.model
.insert_cells_and_shift_down(sheet, row, column, row_delta, column_delta)?;
self.model.evaluate();
Ok(())
}
/// Delete cells in the specified area and then shift cells left to fill the gap.
pub fn delete_cells_and_shift_left(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let mut old_data = Vec::new();
let worksheet = self.model.workbook.worksheet(sheet)?;
for r in row..row + row_delta {
let mut row_data = Vec::new();
for c in column..column + column_delta {
let cell = worksheet.cell(r, c);
row_data.push(cell.cloned());
}
old_data.push(row_data);
}
let diff_list = vec![Diff::DeleteCellsShiftLeft {
sheet,
row,
column,
row_delta,
column_delta,
old_data,
}];
self.push_diff_list(diff_list);
self.model
.delete_cells_and_shift_left(sheet, row, column, row_delta, column_delta)?;
self.model.evaluate();
Ok(())
}
/// Delete cells in the specified area and then shift cells upward to fill the gap.
pub fn delete_cells_and_shift_up(
&mut self,
sheet: u32,
row: i32,
column: i32,
row_delta: i32,
column_delta: i32,
) -> Result<(), String> {
let mut old_data = Vec::new();
let worksheet = self.model.workbook.worksheet(sheet)?;
for r in row..row + row_delta {
let mut row_data = Vec::new();
for c in column..column + column_delta {
let cell = worksheet.cell(r, c);
row_data.push(cell.cloned());
}
old_data.push(row_data);
}
let diff_list = vec![Diff::DeleteCellsShiftUp {
sheet,
row,
column,
row_delta,
column_delta,
old_data,
}];
self.push_diff_list(diff_list);
self.model
.delete_cells_and_shift_up(sheet, row, column, row_delta, column_delta)?;
self.model.evaluate();
Ok(())
}
/// Sets the width of a column /// Sets the width of a column
/// ///
/// See also: /// See also:
@@ -845,8 +992,11 @@ impl UserModel {
"fill.bg_color" => { "fill.bg_color" => {
style.fill.bg_color = color(value)?; style.fill.bg_color = color(value)?;
} }
"fill.fg_color" => {
style.fill.fg_color = color(value)?;
}
"num_fmt" => { "num_fmt" => {
style.num_fmt = value.to_owned(); value.clone_into(&mut style.num_fmt);
} }
"border.left" => { "border.left" => {
style.border.left = border(value)?; style.border.left = border(value)?;
@@ -1095,6 +1245,94 @@ impl UserModel {
} => { } => {
self.model.set_sheet_color(*index, old_value)?; self.model.set_sheet_color(*index, old_value)?;
} }
Diff::InsertCellsShiftRight {
sheet,
row,
column,
row_delta,
column_delta,
} => {
needs_evaluation = true;
self.model.delete_cells_and_shift_left(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
}
Diff::InsertCellsShiftDown {
sheet,
row,
column,
row_delta,
column_delta,
} => {
needs_evaluation = true;
self.model.delete_cells_and_shift_up(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
}
Diff::DeleteCellsShiftLeft {
sheet,
row,
column,
row_delta,
column_delta,
old_data,
} => {
needs_evaluation = true;
// Sets old data
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
for r in *row..*row + *row_delta {
for c in *column..*column + *column_delta {
if let Some(cell) = &old_data[r as usize][c as usize] {
worksheet.update_cell(r, c, cell.clone());
} else {
worksheet.cell_clear_contents(r, c);
}
}
}
self.model.insert_cells_and_shift_right(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
}
Diff::DeleteCellsShiftUp {
sheet,
row,
column,
row_delta,
column_delta,
old_data,
} => {
needs_evaluation = true;
// Sets old data
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
for r in *row..*row + *row_delta {
for c in *column..*column + *column_delta {
if let Some(cell) = &old_data[r as usize][c as usize] {
worksheet.update_cell(r, c, cell.clone());
} else {
worksheet.cell_clear_contents(r, c);
}
}
}
self.model.insert_cells_and_shift_down(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
}
} }
} }
if needs_evaluation { if needs_evaluation {
@@ -1215,6 +1453,72 @@ impl UserModel {
} => { } => {
self.model.set_sheet_color(*index, new_value)?; self.model.set_sheet_color(*index, new_value)?;
} }
Diff::InsertCellsShiftRight {
sheet,
row,
column,
row_delta,
column_delta,
} => {
self.model.insert_cells_and_shift_right(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
needs_evaluation = true;
}
Diff::InsertCellsShiftDown {
sheet,
row,
column,
row_delta,
column_delta,
} => {
self.model.insert_cells_and_shift_down(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
needs_evaluation = true;
}
Diff::DeleteCellsShiftLeft {
sheet,
row,
column,
row_delta,
column_delta,
old_data: _,
} => {
self.model.delete_cells_and_shift_left(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
needs_evaluation = true;
}
Diff::DeleteCellsShiftUp {
sheet,
row,
column,
row_delta,
column_delta,
old_data: _,
} => {
self.model.delete_cells_and_shift_up(
*sheet,
*row,
*column,
*row_delta,
*column_delta,
)?;
needs_evaluation = true;
}
} }
} }

View File

@@ -394,7 +394,7 @@ impl Worksheet {
} }
/// Calculates dimension of the sheet. This function isn't cheap to calculate. /// Calculates dimension of the sheet. This function isn't cheap to calculate.
pub fn dimension(&self) -> WorksheetDimension { pub fn get_dimension(&self) -> WorksheetDimension {
// FIXME: It's probably better to just track the size as operations happen. // FIXME: It's probably better to just track the size as operations happen.
if self.sheet_data.is_empty() { if self.sheet_data.is_empty() {
return WorksheetDimension { return WorksheetDimension {

View File

@@ -1,9 +1,12 @@
all: all:
wasm-pack build --target web --scope ironcalc wasm-pack build --target web --scope ironcalc --release
cp README.pkg.md pkg/README.md cp README.pkg.md pkg/README.md
tsc types.ts --target esnext --module esnext tsc types.ts --target esnext --module esnext
python fix_types.py python fix_types.py
tests:
wasm-pack build --target nodejs && node tests/test.mjs
lint: lint:
cargo check cargo check
cargo fmt -- --check cargo fmt -- --check

View File

@@ -14,9 +14,9 @@ export function getTokens(formula: string): any;
""".strip() """.strip()
get_tokens_str_types = r""" get_tokens_str_types = r"""
* @returns {TokenType[]} * @returns {MarkedToken[]}
*/ */
export function getTokens(formula: string): TokenType[]; export function getTokens(formula: string): MarkedToken[];
""".strip() """.strip()
update_style_str = r""" update_style_str = r"""

View File

@@ -4,7 +4,7 @@ use wasm_bindgen::{
}; };
use ironcalc_base::{ use ironcalc_base::{
expressions::{lexer::util::get_tokens as tokenizer, types::Area}, expressions::{lexer::marked_token::get_tokens as tokenizer, types::Area},
types::CellType, types::CellType,
UserModel as BaseModel, UserModel as BaseModel,
}; };
@@ -71,12 +71,12 @@ impl Model {
} }
#[wasm_bindgen(js_name = "flushSendQueue")] #[wasm_bindgen(js_name = "flushSendQueue")]
pub fn flush_send_queue(&mut self) -> String { pub fn flush_send_queue(&mut self) -> Vec<u8> {
self.model.flush_send_queue() self.model.flush_send_queue()
} }
#[wasm_bindgen(js_name = "applyExternalDiffs")] #[wasm_bindgen(js_name = "applyExternalDiffs")]
pub fn apply_external_diffs(&mut self, diffs: &str) -> Result<(), JsError> { pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<(), JsError> {
self.model.apply_external_diffs(diffs).map_err(to_js_error) self.model.apply_external_diffs(diffs).map_err(to_js_error)
} }

View File

@@ -8,7 +8,7 @@
use std::path; use std::path;
use ironcalc::{compare::test_file, export::save_to_xlsx, import::load_model_from_xlsx}; use ironcalc::{compare::test_file, export::save_to_xlsx, import::load_from_xlsx};
fn main() { fn main() {
let args: Vec<_> = std::env::args().collect(); let args: Vec<_> = std::env::args().collect();
@@ -27,7 +27,7 @@ fn main() {
let file_path = path::Path::new(file_name); let file_path = path::Path::new(file_name);
let base_name = file_path.file_stem().unwrap().to_str().unwrap(); let base_name = file_path.file_stem().unwrap().to_str().unwrap();
let output_file_name = &format!("{base_name}.output.xlsx"); let output_file_name = &format!("{base_name}.output.xlsx");
let mut model = load_model_from_xlsx(file_name, "en", "UTC").unwrap(); let mut model = load_from_xlsx(file_name, "en", "UTC").unwrap();
model.evaluate(); model.evaluate();
println!("Saving result as: {output_file_name}. Please open with Excel and test."); println!("Saving result as: {output_file_name}. Please open with Excel and test.");
save_to_xlsx(&model, output_file_name).unwrap(); save_to_xlsx(&model, output_file_name).unwrap();

View File

@@ -1,26 +1,21 @@
//! Tests an Excel xlsx file. //! Converts an xlsx file into the binary IronCalc format
//! Returns a list of differences in json format.
//! Saves an IronCalc version
//! This is primary for QA internal testing and will be superseded by an official
//! IronCalc CLI.
//! //!
//! Usage: test file.xlsx //! Usage: xlsx_2_icalc file.xlsx
use std::path; use std::path;
use ironcalc::{export::save_to_json, import::load_model_from_xlsx}; use ironcalc::{export::save_to_icalc, import::load_from_xlsx};
fn main() { fn main() {
let args: Vec<_> = std::env::args().collect(); let args: Vec<_> = std::env::args().collect();
if args.len() != 2 { if args.len() != 2 {
panic!("Usage: {} <file.xlsx>", args[0]); panic!("Usage: {} <file.xlsx>", args[0]);
} }
// first test the file
let file_name = &args[1]; let file_name = &args[1];
let file_path = path::Path::new(file_name); let file_path = path::Path::new(file_name);
let base_name = file_path.file_stem().unwrap().to_str().unwrap(); let base_name = file_path.file_stem().unwrap().to_str().unwrap();
let output_file_name = &format!("{base_name}.ic"); let output_file_name = &format!("{base_name}.ic");
let model = load_model_from_xlsx(file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(file_name, "en", "UTC").unwrap();
save_to_json(model.workbook, output_file_name); save_to_icalc(model.workbook, output_file_name);
} }

View File

@@ -5,7 +5,7 @@ use ironcalc_base::types::*;
use ironcalc_base::{expressions::utils::number_to_column, Model}; use ironcalc_base::{expressions::utils::number_to_column, Model};
use crate::export::save_to_xlsx; use crate::export::save_to_xlsx;
use crate::import::load_model_from_xlsx; use crate::import::load_from_xlsx;
pub struct CompareError { pub struct CompareError {
message: String, message: String,
@@ -164,13 +164,13 @@ pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
let mut message = "".to_string(); let mut message = "".to_string();
for diff in diffs { for diff in diffs {
message = format!( message = format!(
"{}\n.Diff: {}!{}{}, value1: {}, value2 {}\n {}", "{}\n.Diff: {}!{}{}, value1: {:?}, value2 {:?}\n {}",
message, message,
diff.sheet_name, diff.sheet_name,
number_to_column(diff.column).unwrap(), number_to_column(diff.column).unwrap(),
diff.row, diff.row,
serde_json::to_string(&diff.value1).unwrap(), &diff.value1,
serde_json::to_string(&diff.value2).unwrap(), &diff.value2,
diff.reason diff.reason
); );
} }
@@ -183,15 +183,15 @@ pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
/// Tests that file in file_path produces the same results in Excel and in IronCalc. /// Tests that file in file_path produces the same results in Excel and in IronCalc.
pub fn test_file(file_path: &str) -> Result<(), String> { pub fn test_file(file_path: &str) -> Result<(), String> {
let model1 = load_model_from_xlsx(file_path, "en", "UTC").unwrap(); let model1 = load_from_xlsx(file_path, "en", "UTC").unwrap();
let mut model2 = load_model_from_xlsx(file_path, "en", "UTC").unwrap(); let mut model2 = load_from_xlsx(file_path, "en", "UTC").unwrap();
model2.evaluate(); model2.evaluate();
compare_models(&model1, &model2) compare_models(&model1, &model2)
} }
/// Tests that file in file_path can be converted to xlsx and read again /// Tests that file in file_path can be converted to xlsx and read again
pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(), String> { pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(), String> {
let model1 = load_model_from_xlsx(file_path, "en", "UTC").unwrap(); let model1 = load_from_xlsx(file_path, "en", "UTC").unwrap();
let base_name = Path::new(file_path).file_name().unwrap().to_str().unwrap(); let base_name = Path::new(file_path).file_name().unwrap().to_str().unwrap();
@@ -200,7 +200,7 @@ pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(),
// test can save // test can save
save_to_xlsx(&model1, temp_file_path).unwrap(); save_to_xlsx(&model1, temp_file_path).unwrap();
// test can open // test can open
let mut model2 = load_model_from_xlsx(temp_file_path, "en", "UTC").unwrap(); let mut model2 = load_from_xlsx(temp_file_path, "en", "UTC").unwrap();
model2.evaluate(); model2.evaluate();
compare_models(&model1, &model2) compare_models(&model1, &model2)
} }

View File

@@ -108,7 +108,7 @@ pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<
.workbook .workbook
.worksheet(sheet_index as u32) .worksheet(sheet_index as u32)
.unwrap() .unwrap()
.dimension(); .get_dimension();
let column_min_str = number_to_column(dimension.min_column).unwrap(); let column_min_str = number_to_column(dimension.min_column).unwrap();
let column_max_str = number_to_column(dimension.max_column).unwrap(); let column_max_str = number_to_column(dimension.max_column).unwrap();
let min_row = dimension.min_row; let min_row = dimension.min_row;
@@ -129,9 +129,9 @@ pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<
} }
/// Exports an internal representation of a workbook into an equivalent IronCalc json format /// Exports an internal representation of a workbook into an equivalent IronCalc json format
pub fn save_to_json(workbook: Workbook, output: &str) { pub fn save_to_icalc(workbook: Workbook, output: &str) {
let s = serde_json::to_string(&workbook).unwrap(); let s = bitcode::encode(&workbook);
let file_path = std::path::Path::new(output); let file_path = std::path::Path::new(output);
let mut file = fs::File::create(file_path).unwrap(); let mut file = fs::File::create(file_path).unwrap();
file.write_all(s.as_bytes()).unwrap(); file.write_all(&s).unwrap();
} }

View File

@@ -3,7 +3,9 @@ use std::fs;
use ironcalc_base::Model; use ironcalc_base::Model;
use crate::error::XlsxError; use crate::error::XlsxError;
use crate::{export::save_to_xlsx, import::load_model_from_xlsx}; use crate::export::save_to_icalc;
use crate::import::load_from_icalc;
use crate::{export::save_to_xlsx, import::load_from_xlsx};
pub fn new_empty_model() -> Model { pub fn new_empty_model() -> Model {
Model::new_empty("model", "en", "UTC").unwrap() Model::new_empty("model", "en", "UTC").unwrap()
@@ -26,11 +28,11 @@ fn test_values() {
// noop // noop
model.evaluate(); model.evaluate();
{
let temp_file_name = "temp_file_test_values.xlsx"; let temp_file_name = "temp_file_test_values.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap(); save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "123.456"); assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "123.456");
assert_eq!( assert_eq!(
model.get_formatted_cell_value(0, 2, 1).unwrap(), model.get_formatted_cell_value(0, 2, 1).unwrap(),
@@ -49,6 +51,31 @@ fn test_values() {
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "#VALUE!"); assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "#VALUE!");
fs::remove_file(temp_file_name).unwrap(); fs::remove_file(temp_file_name).unwrap();
}
{
let temp_file_name = "temp_file_test_values.ic";
save_to_icalc(model.workbook, temp_file_name);
let model = load_from_icalc(temp_file_name).unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "123.456");
assert_eq!(
model.get_formatted_cell_value(0, 2, 1).unwrap(),
"Hello world!"
);
assert_eq!(
model.get_formatted_cell_value(0, 3, 1).unwrap(),
"Hello world!"
);
assert_eq!(
model.get_formatted_cell_value(0, 4, 1).unwrap(),
"你好世界!"
);
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "TRUE");
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "FALSE");
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "#VALUE!");
fs::remove_file(temp_file_name).unwrap();
}
} }
#[test] #[test]
@@ -67,7 +94,7 @@ fn test_formulas() {
let temp_file_name = "temp_file_test_formulas.xlsx"; let temp_file_name = "temp_file_test_formulas.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap(); save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "11"); assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "11");
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "13"); assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "13");
assert_eq!(model.get_formatted_cell_value(0, 3, 2).unwrap(), "15"); assert_eq!(model.get_formatted_cell_value(0, 3, 2).unwrap(), "15");
@@ -89,7 +116,7 @@ fn test_sheets() {
let temp_file_name = "temp_file_test_sheets.xlsx"; let temp_file_name = "temp_file_test_sheets.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap(); save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert_eq!( assert_eq!(
model.workbook.get_worksheet_names(), model.workbook.get_worksheet_names(),
vec!["Sheet1", "With space", "Tango & Cash", "你好世界"] vec!["Sheet1", "With space", "Tango & Cash", "你好世界"]
@@ -118,7 +145,7 @@ fn test_named_styles() {
let temp_file_name = "temp_file_test_named_styles.xlsx"; let temp_file_name = "temp_file_test_named_styles.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap(); save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert!(model assert!(model
.workbook .workbook
.styles .styles

View File

@@ -106,7 +106,7 @@ fn load_xlsx_from_reader<R: Read + std::io::Seek>(
// Public methods // Public methods
/// Imports a file from disk into an internal representation /// Imports a file from disk into an internal representation
pub fn load_from_excel(file_name: &str, locale: &str, tz: &str) -> Result<Workbook, XlsxError> { fn load_from_excel(file_name: &str, locale: &str, tz: &str) -> Result<Workbook, XlsxError> {
let file_path = std::path::Path::new(file_name); let file_path = std::path::Path::new(file_name);
let file = fs::File::open(file_path)?; let file = fs::File::open(file_path)?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
@@ -118,7 +118,14 @@ pub fn load_from_excel(file_name: &str, locale: &str, tz: &str) -> Result<Workbo
load_xlsx_from_reader(name, reader, locale, tz) load_xlsx_from_reader(name, reader, locale, tz)
} }
pub fn load_model_from_xlsx(file_name: &str, locale: &str, tz: &str) -> Result<Model, XlsxError> { pub fn load_from_xlsx(file_name: &str, locale: &str, tz: &str) -> Result<Model, XlsxError> {
let workbook = load_from_excel(file_name, locale, tz)?; let workbook = load_from_excel(file_name, locale, tz)?;
Model::from_workbook(workbook).map_err(XlsxError::Workbook) Model::from_workbook(workbook).map_err(XlsxError::Workbook)
} }
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)))?;
let workbook: Workbook = bitcode::decode(&contents).unwrap();
Model::from_workbook(workbook).map_err(XlsxError::Workbook)
}

View File

@@ -5,9 +5,11 @@ use ironcalc_base::{
parser::{stringify::to_rc_format, Parser}, parser::{stringify::to_rc_format, Parser},
token::{get_error_by_english_name, Error}, token::{get_error_by_english_name, Error},
types::CellReferenceRC, types::CellReferenceRC,
utils::column_to_number, utils::{column_to_number, parse_reference_a1},
},
types::{
Cell, Col, Comment, DefinedName, Row, Selection, SheetData, SheetState, Table, Worksheet,
}, },
types::{Cell, Col, Comment, DefinedName, Row, SheetData, SheetState, Table, Worksheet},
}; };
use roxmltree::Node; use roxmltree::Node;
use thiserror::Error; use thiserror::Error;
@@ -47,6 +49,50 @@ fn get_column_from_ref(s: &str) -> String {
column.into_iter().collect() column.into_iter().collect()
} }
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))
}
}
fn parse_range(range: &str) -> Result<(i32, i32, i32, i32), String> {
let parts: Vec<&str> = range.split(':').collect();
if parts.len() == 1 {
if let Some(r) = parse_reference_a1(parts[0]) {
Ok((r.row, r.column, r.row, r.column))
} else {
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)),
}
} else {
return Err(format!("Invalid range: '{}'", range));
}
}
#[cfg(test)]
mod test {
use crate::import::worksheets::parse_range;
#[test]
fn test_parse_range() {
assert!(parse_range("3Aw").is_err());
assert_eq!(parse_range("A1"), Ok((1, 1, 1, 1)));
assert_eq!(parse_range("B5:C6"), Ok((5, 2, 6, 3)));
assert!(parse_range("A1:A2:A3").is_err());
assert!(parse_range("A1:34").is_err());
assert!(parse_range("A").is_err());
assert!(parse_range("12").is_err());
}
}
fn load_dimension(ws: Node) -> String { fn load_dimension(ws: Node) -> String {
// <dimension ref="A1:O18"/> // <dimension ref="A1:O18"/>
let application_nodes = ws let application_nodes = ws
@@ -490,7 +536,29 @@ fn load_sheet_rels<R: Read + std::io::Seek>(
Ok(comments) Ok(comments)
} }
fn get_frozen_rows_and_columns(ws: Node) -> (i32, i32) { struct SheetView {
is_selected: bool,
selected_row: i32,
selected_column: i32,
frozen_columns: i32,
frozen_rows: i32,
range: [i32; 4],
}
impl Default for SheetView {
fn default() -> Self {
Self {
is_selected: false,
selected_row: 1,
selected_column: 1,
frozen_rows: 0,
frozen_columns: 0,
range: [1, 1, 1, 1],
}
}
}
fn get_sheet_view(ws: Node) -> SheetView {
// <sheetViews> // <sheetViews>
// <sheetView workbookViewId="0"> // <sheetView workbookViewId="0">
// <selection activeCell="E10" sqref="E10"/> // <selection activeCell="E10" sqref="E10"/>
@@ -511,19 +579,20 @@ fn get_frozen_rows_and_columns(ws: Node) -> (i32, i32) {
// bottomLeft, bottomRight, topLeft, topRight // bottomLeft, bottomRight, topLeft, topRight
// NB: bottomLeft is used when only rows are frozen, etc // NB: bottomLeft is used when only rows are frozen, etc
// Calc ignores all those. // IronCalc ignores all those.
let mut frozen_rows = 0; let mut frozen_rows = 0;
let mut frozen_columns = 0; let mut frozen_columns = 0;
// In Calc there can only be one sheetView // In IronCalc there can only be one sheetView
let sheet_views = ws let sheet_views = ws
.children() .children()
.filter(|n| n.has_tag_name("sheetViews")) .filter(|n| n.has_tag_name("sheetViews"))
.collect::<Vec<Node>>(); .collect::<Vec<Node>>();
// We are only expecting one `sheetViews` element. Otherwise return a default
if sheet_views.len() != 1 { if sheet_views.len() != 1 {
return (0, 0); return SheetView::default();
} }
let sheet_view = sheet_views[0] let sheet_view = sheet_views[0]
@@ -531,25 +600,64 @@ fn get_frozen_rows_and_columns(ws: Node) -> (i32, i32) {
.filter(|n| n.has_tag_name("sheetView")) .filter(|n| n.has_tag_name("sheetView"))
.collect::<Vec<Node>>(); .collect::<Vec<Node>>();
// We are only expecting one `sheetView` element. Otherwise return a default
if sheet_view.len() != 1 { if sheet_view.len() != 1 {
return (0, 0); return SheetView::default();
} }
let pane = sheet_view[0] let sheet_view = sheet_view[0];
let is_selected = sheet_view.attribute("tabSelected").unwrap_or("0") == "1";
let pane = sheet_view
.children() .children()
.filter(|n| n.has_tag_name("pane")) .filter(|n| n.has_tag_name("pane"))
.collect::<Vec<Node>>(); .collect::<Vec<Node>>();
// 18.18.53 ST_PaneState (Pane State) // 18.18.53 ST_PaneState (Pane State)
// frozen, frozenSplit, split // frozen, frozenSplit, split
if pane.len() == 1 && pane[0].attribute("state").unwrap_or("split") == "frozen" { if pane.len() == 1 {
if let Some("frozen") = pane[0].attribute("state") {
// TODO: Should we assert that topLeft is consistent? // TODO: Should we assert that topLeft is consistent?
// let top_left_cell = pane[0].attribute("topLeftCell").unwrap_or("A1").to_string(); // let top_left_cell = pane[0].attribute("topLeftCell").unwrap_or("A1").to_string();
frozen_columns = get_number(pane[0], "xSplit"); frozen_columns = get_number(pane[0], "xSplit");
frozen_rows = get_number(pane[0], "ySplit"); frozen_rows = get_number(pane[0], "ySplit");
} }
(frozen_rows, frozen_columns) }
let selections = sheet_view
.children()
.filter(|n| n.has_tag_name("selection"))
.collect::<Vec<Node>>();
if let Some(selection) = selections.last() {
let active_cell = match selection.attribute("activeCell").map(parse_cell_reference) {
Some(Ok(s)) => Some(s),
_ => None,
};
let sqref = match selection.attribute("sqref").map(parse_range) {
Some(Ok(s)) => Some(s),
_ => None,
};
let (selected_row, selected_column, row1, column1, row2, column2) =
match (active_cell, sqref) {
(Some(cell), Some(range)) => (cell.0, cell.1, range.0, range.1, range.2, range.3),
(Some(cell), None) => (cell.0, cell.1, cell.0, cell.1, cell.0, cell.1),
(None, Some(range)) => (range.0, range.1, range.0, range.1, range.2, range.3),
_ => (1, 1, 1, 1, 1, 1),
};
SheetView {
frozen_rows,
frozen_columns,
selected_row,
selected_column,
is_selected,
range: [row1, column1, row2, column2],
}
} else {
SheetView::default()
}
} }
pub(super) struct SheetSettings { pub(super) struct SheetSettings {
@@ -583,7 +691,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
let dimension = load_dimension(ws); let dimension = load_dimension(ws);
let (frozen_rows, frozen_columns) = get_frozen_rows_and_columns(ws); let sheet_view = get_sheet_view(ws);
let cols = load_columns(ws)?; let cols = load_columns(ws)?;
let color = load_sheet_color(ws)?; let color = load_sheet_color(ws)?;
@@ -856,8 +964,14 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
color, color,
merge_cells, merge_cells,
comments: settings.comments, comments: settings.comments,
frozen_rows, frozen_rows: sheet_view.frozen_rows,
frozen_columns, frozen_columns: sheet_view.frozen_columns,
selection: Selection {
is_selected: sheet_view.is_selected,
row: sheet_view.selected_row,
column: sheet_view.selected_column,
range: sheet_view.range,
},
}) })
} }

BIN
xlsx/tests/example.ic Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -3,33 +3,61 @@ use uuid::Uuid;
use ironcalc::compare::{test_file, test_load_and_saving}; use ironcalc::compare::{test_file, test_load_and_saving};
use ironcalc::export::save_to_xlsx; use ironcalc::export::save_to_xlsx;
use ironcalc::import::{load_from_excel, load_model_from_xlsx}; use ironcalc::import::{load_from_icalc, load_from_xlsx};
use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment, Workbook}; use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment};
use ironcalc_base::Model; use ironcalc_base::Model;
// This is a functional test. // This is a functional test.
// We check that the output of example.xlsx is what we expect. // We check that the output of example.xlsx is what we expect.
#[test] #[test]
fn test_example() { fn test_example() {
let model = load_from_excel("tests/example.xlsx", "en", "UTC").unwrap(); let model = load_from_xlsx("tests/example.xlsx", "en", "UTC").unwrap();
assert_eq!(model.worksheets[0].frozen_rows, 0); // We should use the API once it is in place
assert_eq!(model.worksheets[0].frozen_columns, 0); let workbook = model.workbook;
let contents = let ws = &workbook.worksheets;
fs::read_to_string("tests/example.json").expect("Something went wrong reading the file"); let expected_names = vec![
let model2: Workbook = serde_json::from_str(&contents).unwrap(); ("Sheet1".to_string(), false),
let s = serde_json::to_string(&model).unwrap(); ("Second".to_string(), false),
assert_eq!(model, model2, "{s}"); ("Sheet4".to_string(), false),
("shared".to_string(), false),
("Table".to_string(), false),
("Sheet2".to_string(), false),
("Created fourth".to_string(), false),
("Frozen".to_string(), true),
("Split".to_string(), false),
("Hidden".to_string(), false),
];
let names: Vec<(String, bool)> = ws
.iter()
.map(|s| (s.name.clone(), s.selection.is_selected))
.collect();
// One is not not imported and one is hidden
assert_eq!(expected_names, names);
// Test selection:
// First sheet (Sheet1)
// E13 and E13:N20
assert_eq!(ws[0].frozen_rows, 0);
assert_eq!(ws[0].frozen_columns, 0);
assert_eq!(ws[0].selection.row, 13);
assert_eq!(ws[0].selection.column, 5);
assert_eq!(ws[0].selection.range, [13, 5, 20, 14]);
let model2 = load_from_icalc("tests/example.ic").unwrap();
let s = bitcode::encode(&model2.workbook);
assert_eq!(workbook, model2.workbook, "{:?}", s);
} }
#[test] #[test]
fn test_save_to_xlsx() { fn test_save_to_xlsx() {
let mut model = load_model_from_xlsx("tests/example.xlsx", "en", "UTC").unwrap(); let mut model = load_from_xlsx("tests/example.xlsx", "en", "UTC").unwrap();
model.evaluate(); model.evaluate();
let temp_file_name = "temp_file_example.xlsx"; let temp_file_name = "temp_file_example.xlsx";
// test can safe // test can safe
save_to_xlsx(&model, temp_file_name).unwrap(); save_to_xlsx(&model, temp_file_name).unwrap();
// test can open // test can open
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
let metadata = &model.workbook.metadata; let metadata = &model.workbook.metadata;
assert_eq!(metadata.application, "IronCalc Sheets"); assert_eq!(metadata.application, "IronCalc Sheets");
// FIXME: This will need to be updated once we fix versioning // FIXME: This will need to be updated once we fix versioning
@@ -41,7 +69,9 @@ fn test_save_to_xlsx() {
#[test] #[test]
fn test_freeze() { fn test_freeze() {
// freeze has 3 frozen columns and 2 frozen rows // freeze has 3 frozen columns and 2 frozen rows
let model = load_from_excel("tests/freeze.xlsx", "en", "UTC").unwrap(); let model = load_from_xlsx("tests/freeze.xlsx", "en", "UTC")
.unwrap()
.workbook;
assert_eq!(model.worksheets[0].frozen_rows, 2); assert_eq!(model.worksheets[0].frozen_rows, 2);
assert_eq!(model.worksheets[0].frozen_columns, 3); assert_eq!(model.worksheets[0].frozen_columns, 3);
} }
@@ -49,7 +79,9 @@ fn test_freeze() {
#[test] #[test]
fn test_split() { fn test_split() {
// We test that a workbook with split panes do not produce frozen rows and columns // We test that a workbook with split panes do not produce frozen rows and columns
let model = load_from_excel("tests/split.xlsx", "en", "UTC").unwrap(); let model = load_from_xlsx("tests/split.xlsx", "en", "UTC")
.unwrap()
.workbook;
assert_eq!(model.worksheets[0].frozen_rows, 0); assert_eq!(model.worksheets[0].frozen_rows, 0);
assert_eq!(model.worksheets[0].frozen_columns, 0); assert_eq!(model.worksheets[0].frozen_columns, 0);
} }
@@ -145,14 +177,14 @@ fn test_model_has_correct_styles(model: &Model) {
#[test] #[test]
fn test_simple_text() { fn test_simple_text() {
let model = load_model_from_xlsx("tests/basic_text.xlsx", "en", "UTC").unwrap(); let model = load_from_xlsx("tests/basic_text.xlsx", "en", "UTC").unwrap();
test_model_has_correct_styles(&model); test_model_has_correct_styles(&model);
let temp_file_name = "temp_file_test_named_styles.xlsx"; let temp_file_name = "temp_file_test_named_styles.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap(); save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
fs::remove_file(temp_file_name).unwrap(); fs::remove_file(temp_file_name).unwrap();
test_model_has_correct_styles(&model); test_model_has_correct_styles(&model);
} }
@@ -160,7 +192,9 @@ fn test_simple_text() {
#[test] #[test]
fn test_defined_names_casing() { fn test_defined_names_casing() {
let test_file_path = "tests/calc_tests/defined_names_for_unit_test.xlsx"; let test_file_path = "tests/calc_tests/defined_names_for_unit_test.xlsx";
let loaded_workbook = load_from_excel(test_file_path, "en", "UTC").unwrap(); let loaded_workbook = load_from_xlsx(test_file_path, "en", "UTC")
.unwrap()
.workbook;
let mut model = Model::from_bytes(&bitcode::encode(&loaded_workbook)).unwrap(); let mut model = Model::from_bytes(&bitcode::encode(&loaded_workbook)).unwrap();
let (row, column) = (2, 13); // B13 let (row, column) = (2, 13); // B13