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/*
.DS_Store

12
Cargo.lock generated
View File

@@ -370,7 +370,6 @@ dependencies = [
"ryu",
"serde",
"serde_json",
"serde_repr",
]
[[package]]
@@ -679,17 +678,6 @@ dependencies = [
"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]]
name = "sha1"
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]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
ryu = "1.0"
chrono = "0.4"
chrono-tz = "0.9"
@@ -21,6 +19,9 @@ regex = "1.0"
once_cell = "1.16.0"
bitcode = "0.6.0"
[dev-dependencies]
serde_json = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }

View File

@@ -69,21 +69,26 @@ impl Model {
target_row: i32,
target_column: i32,
) -> Result<(), String> {
let source_cell = self
if let Some(source_cell) = self
.workbook
.worksheet(sheet)?
.cell(source_row, source_column)
.ok_or("Expected Cell to exist")?;
{
let style = source_cell.get_style();
// FIXME: we need some user_input getter instead of get_text
let formula_or_value = self
.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.workbook
.worksheet_mut(sheet)?
.set_cell_style(target_row, target_column, style);
self.cell_clear_all(sheet, source_row, source_column)?;
} else {
self.cell_clear_all(sheet, target_row, target_column)?;
}
Ok(())
}
@@ -106,7 +111,7 @@ impl Model {
return Err("Cannot add a negative number of cells :)".to_string());
}
// 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;
if last_column > LAST_COLUMN {
return Err(
@@ -263,7 +268,7 @@ impl Model {
return Err("Cannot add a negative number of cells :)".to_string());
}
// 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;
if last_row > LAST_ROW {
return Err(
@@ -367,13 +372,162 @@ impl Model {
}
}
self.workbook.worksheets[sheet as usize].rows = new_rows;
self.displace_cells(
&(DisplaceData::Row {
self.displace_cells(&DisplaceData::Row {
sheet,
row,
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(())
}

View File

@@ -1,12 +1,9 @@
use crate::{
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.
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
#[derive(Debug, PartialEq)]
pub enum CellValue {
None,
String(String),
@@ -14,17 +11,6 @@ pub enum CellValue {
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 {
fn from(value: f64) -> Self {
Self::Number(value)

View File

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

View File

@@ -1,3 +1,5 @@
#![deny(missing_docs)]
//! A tokenizer for spreadsheet formulas.
//!
//! This is meant to feed a formula parser.
@@ -7,8 +9,10 @@
//! It supports two working modes:
//!
//! 1. A1 or display mode
//!
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
//! 2. 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.
@@ -55,7 +59,8 @@ use super::token::{Error, TokenType};
use super::types::*;
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)]
mod test;
@@ -63,17 +68,28 @@ mod test;
mod ranges;
mod structured_references;
/// This is the TokenType we return if we cannot recognize a token
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LexerError {
/// Position of the beginning of the token in the byte string.
pub position: usize,
/// Message describing what we think the error is.
pub message: String,
}
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)]
pub enum LexerMode {
/// Cell references are written `=S34`. This is the display mode
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,
}
@@ -308,9 +324,9 @@ impl Lexer {
return self.consume_range(None);
}
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);
} else if name_upper == self.language.booleans.false_value {
} else if name_upper == self.language.booleans.r#false {
return TokenType::Boolean(false);
}
if self.mode == LexerMode::A1 {
@@ -660,8 +676,8 @@ impl Lexer {
fn consume_error(&mut self) -> TokenType {
let errors = &self.language.errors;
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
if rest_of_formula.starts_with(&errors.ref_value) {
self.position += errors.ref_value.chars().count() - 1;
if rest_of_formula.starts_with(&errors.r#ref) {
self.position += errors.r#ref.chars().count() - 1;
return TokenType::Error(Error::REF);
} else if rest_of_formula.starts_with(&errors.name) {
self.position += errors.name.chars().count() - 1;

View File

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

View File

@@ -6,11 +6,11 @@ use crate::{
token::TokenType,
},
language::get_language,
locale::get_locale_fix,
locale::get_locale,
};
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();
Lexer::new(formula, LexerMode::A1, locale, language)
}

View File

@@ -1,5 +1,5 @@
use crate::expressions::{
lexer::util::get_tokens,
lexer::marked_token::{get_tokens, MarkedToken},
token::{OpCompare, OpSum, TokenType},
};
@@ -22,6 +22,29 @@ fn test_get_tokens() {
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]
fn test_simple_tokens() {
assert_eq!(

View File

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

View File

@@ -1,31 +1,29 @@
/*!
# GRAMAR
<pre class="rust">
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
opFactor => '*' | '/'
unaryOp => '-' | '+'
expr => concat (opComp concat)*
concat => term ('&' term)*
term => factor (opFactor factor)*
factor => prod (opProd prod)*
prod => power ('^' power)*
power => (unaryOp)* range '%'*
range => primary (':' primary)?
primary => '(' expr ')'
=> number
=> function '(' f_args ')'
=> name
=> string
=> '{' a_args '}'
=> bool
=> bool()
=> error
f_args => e (',' e)*
</pre>
*/
//! # GRAMMAR
//!
//! <pre class="rust">
//! opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
//! opFactor => '*' | '/'
//! unaryOp => '-' | '+'
//!
//! expr => concat (opComp concat)*
//! concat => term ('&' term)*
//! term => factor (opFactor factor)*
//! factor => prod (opProd prod)*
//! prod => power ('^' power)*
//! power => (unaryOp)* range '%'*
//! range => primary (':' primary)?
//! primary => '(' expr ')'
//! => number
//! => function '(' f_args ')'
//! => name
//! => string
//! => '{' a_args '}'
//! => bool
//! => bool()
//! => error
//!
//! f_args => e (',' e)*
//! </pre>
use std::collections::HashMap;
@@ -44,21 +42,15 @@ use super::utils::number_to_column;
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 walk;
#[cfg(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> {
let mut lexer = lexer::Lexer::new(
formula,
@@ -222,7 +214,7 @@ impl Parser {
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
self.lexer.set_formula(formula);
self.context = context.clone();
self.context.clone_from(context);
self.parse_expr()
}

View File

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

View File

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

View File

@@ -2,9 +2,9 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use super::super::parser::stringify::{to_rc_format, to_string};
use super::super::types::CellReferenceRC;
use super::Parser;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> {
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::types::{Table, TableColumn, TableStyleInfo};
use super::super::types::CellReferenceRC;
use super::Parser;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
fn create_test_table(
table_name: &str,

View File

@@ -2,7 +2,6 @@ use std::fmt;
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
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+")
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
/// Note that they are serialized/deserialized by index
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum Error {
REF,
NAME,
@@ -120,7 +118,7 @@ impl Error {
pub fn to_localized_error_string(&self, language: &Language) -> String {
match self {
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::VALUE => language.errors.value.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> {
let errors = &language.errors;
if name == errors.ref_value {
if name == errors.r#ref {
return Some(Error::REF);
} else if name == errors.name {
return Some(Error::NAME);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -255,7 +255,7 @@ impl Model {
.workbook
.worksheet(left.sheet)
.expect("Sheet expected during evaluation.")
.dimension()
.get_dimension()
.max_row;
}
if column1 == 1 && column2 == LAST_COLUMN {
@@ -263,7 +263,7 @@ impl Model {
.workbook
.worksheet(left.sheet)
.expect("Sheet expected during evaluation.")
.dimension()
.get_dimension()
.max_column;
}
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;
#[derive(Serialize, Deserialize, Clone)]
use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)]
pub struct Booleans {
#[serde(rename = "true")]
pub true_value: String,
#[serde(rename = "false")]
pub false_value: String,
pub r#true: String,
pub r#false: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Errors {
#[serde(rename = "ref")]
pub ref_value: String,
pub r#ref: String,
pub name: String,
pub value: String,
pub div: String,
@@ -28,14 +25,14 @@ pub struct Errors {
pub null: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Language {
pub booleans: Booleans,
pub errors: Errors,
}
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> {

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 serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Locale {
pub dates: Dates,
pub numbers: NumbersProperties,
pub currency: Currency,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Currency {
pub iso: String,
pub symbol: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct NumbersProperties {
#[serde(rename = "symbols-numberSystem-latn")]
pub symbols: NumbersSymbols,
#[serde(rename = "decimalFormats-numberSystem-latn")]
pub decimal_formats: DecimalFormats,
#[serde(rename = "currencyFormats-numberSystem-latn")]
pub currency_formats: CurrencyFormats,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Dates {
pub day_names: Vec<String>,
pub day_names_short: Vec<String>,
@@ -35,8 +32,7 @@ pub struct Dates {
pub months_letter: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[derive(Encode, Decode, Clone)]
pub struct NumbersSymbols {
pub decimal: String,
pub group: String,
@@ -54,40 +50,26 @@ pub struct NumbersSymbols {
}
// 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 standard: String,
#[serde(rename = "standard-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub standard_alpha_next_to_number: Option<String>,
#[serde(rename = "standard-noCurrency")]
pub standard_no_currency: String,
pub accounting: String,
#[serde(rename = "accounting-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub accounting_alpha_next_to_number: Option<String>,
#[serde(rename = "accounting-noCurrency")]
pub accounting_no_currency: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[derive(Encode, Decode, Clone)]
pub struct DecimalFormats {
pub standard: String,
}
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
});
static LOCALES: Lazy<HashMap<String, 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
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")?;
Ok(locale)
}

View File

@@ -798,7 +798,7 @@ impl Model {
None
}
/// Returns a model from a String representation of a workbook
/// Returns a model from an internal binary representation of a workbook
///
/// # Examples
///
@@ -816,9 +816,12 @@ impl Model {
/// # Ok(())
/// # }
/// ```
///
/// See also:
/// * [Model::to_bytes]
pub fn from_bytes(s: &[u8]) -> Result<Model, String> {
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)
}
@@ -1760,7 +1763,10 @@ impl Model {
.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> {
bitcode::encode(&self.workbook)
}
@@ -1782,7 +1788,7 @@ impl Model {
/// Returns markup representation of the given `sheet`.
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
let worksheet = self.workbook.worksheet(sheet)?;
let dimension = worksheet.dimension();
let dimension = worksheet.get_dimension();
let mut rows = Vec::new();

View File

@@ -6,14 +6,16 @@ use crate::{
calc_result::Range,
expressions::{
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,
},
language::get_language,
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
types::{Metadata, Selection, SheetState, Workbook, WorkbookSettings, Worksheet},
utils::ParsedReference,
};
@@ -48,6 +50,12 @@ impl Model {
color: Default::default(),
frozen_columns: 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_binary_search;
mod test_cell;
@@ -8,49 +10,30 @@ mod test_criteria;
mod test_currency;
mod test_date_and_time;
mod test_error_propagation;
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_product;
mod test_fn_rept;
mod test_fn_sum;
mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_escape_quotes;
mod test_extend;
mod test_forward_references;
mod test_frozen_rows_and_columns;
mod test_frozen_rows_columns;
mod test_general;
mod test_get_cell_content;
mod test_math;
mod test_metadata;
mod test_model_cell_clear_all;
mod test_model_is_empty_cell;
mod test_move_formula;
mod test_number_format;
mod test_percentage;
mod test_quote_prefix;
mod test_set_user_input;
mod test_sheet_markup;
mod test_sheets;
mod test_shift_cells;
mod test_styles;
mod test_today;
mod test_trigonometric;
mod test_types;
mod test_workbook;
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;
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() {
let model = new_empty_model();
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 1,
min_column: 1,
@@ -25,7 +25,7 @@ fn test_worksheet_dimension_single_cell() {
let mut model = new_empty_model();
model._set("W11", "1");
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 11,
min_column: 23,
@@ -41,7 +41,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
model._set("W11", "1");
model.cell_clear_contents(0, 11, 23).unwrap();
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 11,
min_column: 23,
@@ -57,7 +57,7 @@ fn test_worksheet_dimension_single_cell_deleted() {
model._set("W11", "1");
model.cell_clear_all(0, 11, 23).unwrap();
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 1,
min_column: 1,
@@ -77,7 +77,7 @@ fn test_worksheet_dimension_multiple_cells() {
model._set("B19", "1");
model.cell_clear_all(0, 11, 23).unwrap();
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 11,
min_column: 2,
@@ -91,7 +91,7 @@ fn test_worksheet_dimension_multiple_cells() {
fn test_worksheet_dimension_progressive() {
let mut model = new_empty_model();
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 1,
min_column: 1,
@@ -102,7 +102,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 30, 50, "Hello World".to_string());
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 30,
min_column: 50,
@@ -113,7 +113,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 10, 15, "Hello World".to_string());
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 10,
min_column: 15,
@@ -124,7 +124,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 5, 25, "Hello World".to_string());
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 5,
min_column: 15,
@@ -135,7 +135,7 @@ fn test_worksheet_dimension_progressive() {
model.set_user_input(0, 10, 250, "Hello World".to_string());
assert_eq!(
model.workbook.worksheet(0).unwrap().dimension(),
model.workbook.worksheet(0).unwrap().get_dimension(),
WorksheetDimension {
min_row: 5,
min_column: 15,

View File

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

View File

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

View File

@@ -25,6 +25,6 @@ fn errors() {
let model_bytes = "Early in the morning, late in the century, Cricklewood Broadway.".as_bytes();
assert_eq!(
&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;
// Useful for `#[serde(default = "default_as_true")]`
fn default_as_true() -> bool {
true
}
fn default_as_false() -> bool {
false
}
// Useful for `#[serde(skip_serializing_if = "is_true")]`
fn is_true(b: &bool) -> bool {
*b
}
fn is_false(b: &bool) -> bool {
!*b
}
fn is_zero(num: &i32) -> bool {
*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)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Metadata {
pub application: String,
pub app_version: String,
@@ -44,14 +22,13 @@ pub struct Metadata {
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 tz: String,
pub locale: String,
}
/// An internal representation of an IronCalc Workbook
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
#[serde(deny_unknown_fields)]
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Workbook {
pub shared_strings: Vec<String>,
pub defined_names: Vec<DefinedName>,
@@ -60,17 +37,14 @@ pub struct Workbook {
pub name: String,
pub settings: WorkbookSettings,
pub metadata: Metadata,
#[serde(default)]
#[serde(skip_serializing_if = "hashmap_is_empty")]
pub tables: HashMap<String, Table>,
}
/// 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 name: String,
pub formula: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sheet_id: Option<u32>,
}
@@ -80,8 +54,7 @@ pub struct DefinedName {
/// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum SheetState {
Visible,
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
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Worksheet {
pub dimension: String,
pub cols: Vec<Col>,
@@ -109,16 +90,12 @@ pub struct Worksheet {
pub shared_formulas: Vec<String>,
pub sheet_id: u32,
pub state: SheetState,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
pub merge_cells: Vec<String>,
pub comments: Vec<Comment>,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_rows: i32,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_columns: i32,
pub selection: Selection,
}
/// Internal representation of Excel's sheet_data
@@ -126,7 +103,7 @@ pub struct Worksheet {
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
// 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 {
/// Row index
pub r: i32,
@@ -134,23 +111,19 @@ pub struct Row {
pub custom_format: bool,
pub custom_height: bool,
pub s: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub hidden: bool,
}
// 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 {
// 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.
pub min: i32,
/// Last column affected by this record. Settings apply to column in \[min, max\] range.
pub max: i32,
pub width: f64,
pub custom_width: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<i32>,
}
@@ -165,32 +138,55 @@ pub enum CellType {
CompoundData = 128,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq)]
#[serde(tag = "t", deny_unknown_fields)]
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub enum Cell {
#[serde(rename = "empty")]
EmptyCell { s: i32 },
#[serde(rename = "b")]
BooleanCell { v: bool, s: i32 },
#[serde(rename = "n")]
NumberCell { v: f64, s: i32 },
EmptyCell {
s: i32,
},
BooleanCell {
v: bool,
s: i32,
},
NumberCell {
v: f64,
s: i32,
},
// Maybe we should not have this type. In Excel this is just a string
#[serde(rename = "e")]
ErrorCell { ei: Error, s: i32 },
ErrorCell {
ei: Error,
s: i32,
},
// Always a shared string
#[serde(rename = "s")]
SharedString { si: i32, s: i32 },
SharedString {
si: i32,
s: i32,
},
// Non evaluated Formula
#[serde(rename = "u")]
CellFormula { f: i32, s: i32 },
#[serde(rename = "fb")]
CellFormulaBoolean { f: i32, v: bool, s: i32 },
#[serde(rename = "fn")]
CellFormulaNumber { f: i32, v: f64, s: i32 },
CellFormula {
f: i32,
s: i32,
},
CellFormulaBoolean {
f: i32,
v: bool,
s: i32,
},
CellFormulaNumber {
f: i32,
v: f64,
s: i32,
},
// always inline string
#[serde(rename = "str")]
CellFormulaString { f: i32, v: String, s: i32 },
#[serde(rename = "fe")]
CellFormulaString {
f: i32,
v: String,
s: i32,
},
CellFormulaError {
f: i32,
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 text: String,
pub author_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_id: Option<String>,
pub cell_ref: String,
}
// 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 name: String,
pub display_name: String,
@@ -227,34 +222,24 @@ pub struct Table {
pub reference: String,
pub totals_row_count: u32,
pub header_row_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>,
pub columns: Vec<TableColumn>,
pub style_info: TableStyleInfo,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub has_filters: bool,
}
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
// 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 id: u32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
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 {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_first_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_last_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_row_stripes: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
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 num_fmts: Vec<NumFmt>,
pub fonts: Vec<Font>,
@@ -326,7 +302,7 @@ pub struct Style {
pub quote_prefix: bool,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct NumFmt {
pub num_fmt_id: i32,
pub format_code: String,
@@ -516,29 +492,17 @@ pub struct Alignment {
pub wrap_text: bool,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyleXfs {
pub num_fmt_id: i32,
pub font_id: i32,
pub fill_id: i32,
pub border_id: i32,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_number_format: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_border: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_alignment: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_protection: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_font: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
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 xf_id: i32,
pub num_fmt_id: i32,
pub font_id: i32,
pub fill_id: i32,
pub border_id: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_number_format: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_border: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_alignment: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_protection: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_font: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_fill: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub quote_prefix: bool,
#[serde(skip_serializing_if = "is_default_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 name: String,
pub xf_id: i32,

View File

@@ -2,7 +2,7 @@
use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize, Serialize};
use bitcode::{Decode, Encode};
use crate::{
constants,
@@ -18,19 +18,19 @@ use crate::{
utils::is_valid_hex_color,
};
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Encode, Decode)]
struct RowData {
row: Option<Row>,
data: HashMap<i32, Cell>,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Encode, Decode)]
struct ColumnData {
column: Option<Col>,
data: HashMap<i32, Cell>,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Encode, Decode)]
enum Diff {
// Cell diffs
SetCellValue {
@@ -91,6 +91,36 @@ enum Diff {
column: i32,
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 {
sheet: u32,
new_value: i32,
@@ -160,13 +190,13 @@ impl History {
}
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Encode, Decode)]
enum DiffType {
Undo,
Redo,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Encode, Decode)]
struct QueueDiffs {
r#type: DiffType,
list: DiffList,
@@ -408,9 +438,9 @@ impl UserModel {
///
/// See also:
/// * [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:
let q = serde_json::to_string(&self.send_queue).unwrap();
let q = bitcode::encode(&self.send_queue);
self.send_queue = vec![];
q
}
@@ -421,8 +451,8 @@ impl UserModel {
///
/// See also:
/// * [UserModel::flush_send_queue]
pub fn apply_external_diffs(&mut self, diff_list_str: &str) -> Result<(), String> {
if let Ok(queue_diffs_list) = serde_json::from_str::<Vec<QueueDiffs>>(diff_list_str) {
pub fn apply_external_diffs(&mut self, diff_list_str: &[u8]) -> Result<(), String> {
if let Ok(queue_diffs_list) = bitcode::decode::<Vec<QueueDiffs>>(diff_list_str) {
for queue_diff in queue_diffs_list {
if matches!(queue_diff.r#type, DiffType::Redo) {
self.apply_diff_list(&queue_diff.list)?;
@@ -713,6 +743,123 @@ impl UserModel {
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
///
/// See also:
@@ -845,8 +992,11 @@ impl UserModel {
"fill.bg_color" => {
style.fill.bg_color = color(value)?;
}
"fill.fg_color" => {
style.fill.fg_color = color(value)?;
}
"num_fmt" => {
style.num_fmt = value.to_owned();
value.clone_into(&mut style.num_fmt);
}
"border.left" => {
style.border.left = border(value)?;
@@ -1095,6 +1245,94 @@ impl UserModel {
} => {
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 {
@@ -1215,6 +1453,72 @@ impl UserModel {
} => {
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.
pub fn dimension(&self) -> WorksheetDimension {
pub fn get_dimension(&self) -> WorksheetDimension {
// FIXME: It's probably better to just track the size as operations happen.
if self.sheet_data.is_empty() {
return WorksheetDimension {

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ use wasm_bindgen::{
};
use ironcalc_base::{
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
expressions::{lexer::marked_token::get_tokens as tokenizer, types::Area},
types::CellType,
UserModel as BaseModel,
};
@@ -71,12 +71,12 @@ impl Model {
}
#[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()
}
#[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)
}

View File

@@ -8,7 +8,7 @@
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() {
let args: Vec<_> = std::env::args().collect();
@@ -27,7 +27,7 @@ fn main() {
let file_path = path::Path::new(file_name);
let base_name = file_path.file_stem().unwrap().to_str().unwrap();
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();
println!("Saving result as: {output_file_name}. Please open with Excel and test.");
save_to_xlsx(&model, output_file_name).unwrap();

View File

@@ -1,26 +1,21 @@
//! Tests an Excel xlsx file.
//! 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.
//! Converts an xlsx file into the binary IronCalc format
//!
//! Usage: test file.xlsx
//! Usage: xlsx_2_icalc file.xlsx
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() {
let args: Vec<_> = std::env::args().collect();
if args.len() != 2 {
panic!("Usage: {} <file.xlsx>", args[0]);
}
// first test the file
let file_name = &args[1];
let file_path = path::Path::new(file_name);
let base_name = file_path.file_stem().unwrap().to_str().unwrap();
let output_file_name = &format!("{base_name}.ic");
let model = load_model_from_xlsx(file_name, "en", "UTC").unwrap();
save_to_json(model.workbook, output_file_name);
let model = load_from_xlsx(file_name, "en", "UTC").unwrap();
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 crate::export::save_to_xlsx;
use crate::import::load_model_from_xlsx;
use crate::import::load_from_xlsx;
pub struct CompareError {
message: String,
@@ -164,13 +164,13 @@ pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
let mut message = "".to_string();
for diff in diffs {
message = format!(
"{}\n.Diff: {}!{}{}, value1: {}, value2 {}\n {}",
"{}\n.Diff: {}!{}{}, value1: {:?}, value2 {:?}\n {}",
message,
diff.sheet_name,
number_to_column(diff.column).unwrap(),
diff.row,
serde_json::to_string(&diff.value1).unwrap(),
serde_json::to_string(&diff.value2).unwrap(),
&diff.value1,
&diff.value2,
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.
pub fn test_file(file_path: &str) -> Result<(), String> {
let model1 = load_model_from_xlsx(file_path, "en", "UTC").unwrap();
let mut model2 = load_model_from_xlsx(file_path, "en", "UTC").unwrap();
let model1 = load_from_xlsx(file_path, "en", "UTC").unwrap();
let mut model2 = load_from_xlsx(file_path, "en", "UTC").unwrap();
model2.evaluate();
compare_models(&model1, &model2)
}
/// 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> {
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();
@@ -200,7 +200,7 @@ pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(),
// test can save
save_to_xlsx(&model1, temp_file_path).unwrap();
// 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();
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
.worksheet(sheet_index as u32)
.unwrap()
.dimension();
.get_dimension();
let column_min_str = number_to_column(dimension.min_column).unwrap();
let column_max_str = number_to_column(dimension.max_column).unwrap();
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
pub fn save_to_json(workbook: Workbook, output: &str) {
let s = serde_json::to_string(&workbook).unwrap();
pub fn save_to_icalc(workbook: Workbook, output: &str) {
let s = bitcode::encode(&workbook);
let file_path = std::path::Path::new(output);
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 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 {
Model::new_empty("model", "en", "UTC").unwrap()
@@ -26,11 +28,11 @@ fn test_values() {
// noop
model.evaluate();
{
let temp_file_name = "temp_file_test_values.xlsx";
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, 2, 1).unwrap(),
@@ -50,6 +52,31 @@ fn test_values() {
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]
fn test_formulas() {
@@ -67,7 +94,7 @@ fn test_formulas() {
let temp_file_name = "temp_file_test_formulas.xlsx";
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, 2, 2).unwrap(), "13");
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";
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.workbook.get_worksheet_names(),
vec!["Sheet1", "With space", "Tango & Cash", "你好世界"]
@@ -118,7 +145,7 @@ fn test_named_styles() {
let temp_file_name = "temp_file_test_named_styles.xlsx";
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
.workbook
.styles

View File

@@ -106,7 +106,7 @@ fn load_xlsx_from_reader<R: Read + std::io::Seek>(
// Public methods
/// 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 = fs::File::open(file_path)?;
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)
}
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)?;
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},
token::{get_error_by_english_name, Error},
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 thiserror::Error;
@@ -47,6 +49,50 @@ fn get_column_from_ref(s: &str) -> String {
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 {
// <dimension ref="A1:O18"/>
let application_nodes = ws
@@ -490,7 +536,29 @@ fn load_sheet_rels<R: Read + std::io::Seek>(
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>
// <sheetView workbookViewId="0">
// <selection activeCell="E10" sqref="E10"/>
@@ -511,19 +579,20 @@ fn get_frozen_rows_and_columns(ws: Node) -> (i32, i32) {
// bottomLeft, bottomRight, topLeft, topRight
// 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_columns = 0;
// In Calc there can only be one sheetView
// In IronCalc there can only be one sheetView
let sheet_views = ws
.children()
.filter(|n| n.has_tag_name("sheetViews"))
.collect::<Vec<Node>>();
// We are only expecting one `sheetViews` element. Otherwise return a default
if sheet_views.len() != 1 {
return (0, 0);
return SheetView::default();
}
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"))
.collect::<Vec<Node>>();
// We are only expecting one `sheetView` element. Otherwise return a default
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()
.filter(|n| n.has_tag_name("pane"))
.collect::<Vec<Node>>();
// 18.18.53 ST_PaneState (Pane State)
// 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?
// let top_left_cell = pane[0].attribute("topLeftCell").unwrap_or("A1").to_string();
frozen_columns = get_number(pane[0], "xSplit");
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 {
@@ -583,7 +691,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
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 color = load_sheet_color(ws)?;
@@ -856,8 +964,14 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
color,
merge_cells,
comments: settings.comments,
frozen_rows,
frozen_columns,
frozen_rows: sheet_view.frozen_rows,
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::export::save_to_xlsx;
use ironcalc::import::{load_from_excel, load_model_from_xlsx};
use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment, Workbook};
use ironcalc::import::{load_from_icalc, load_from_xlsx};
use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment};
use ironcalc_base::Model;
// This is a functional test.
// We check that the output of example.xlsx is what we expect.
#[test]
fn test_example() {
let model = load_from_excel("tests/example.xlsx", "en", "UTC").unwrap();
assert_eq!(model.worksheets[0].frozen_rows, 0);
assert_eq!(model.worksheets[0].frozen_columns, 0);
let contents =
fs::read_to_string("tests/example.json").expect("Something went wrong reading the file");
let model2: Workbook = serde_json::from_str(&contents).unwrap();
let s = serde_json::to_string(&model).unwrap();
assert_eq!(model, model2, "{s}");
let model = load_from_xlsx("tests/example.xlsx", "en", "UTC").unwrap();
// We should use the API once it is in place
let workbook = model.workbook;
let ws = &workbook.worksheets;
let expected_names = vec![
("Sheet1".to_string(), false),
("Second".to_string(), false),
("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]
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();
let temp_file_name = "temp_file_example.xlsx";
// test can safe
save_to_xlsx(&model, temp_file_name).unwrap();
// 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;
assert_eq!(metadata.application, "IronCalc Sheets");
// FIXME: This will need to be updated once we fix versioning
@@ -41,7 +69,9 @@ fn test_save_to_xlsx() {
#[test]
fn test_freeze() {
// 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_columns, 3);
}
@@ -49,7 +79,9 @@ fn test_freeze() {
#[test]
fn test_split() {
// 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_columns, 0);
}
@@ -145,14 +177,14 @@ fn test_model_has_correct_styles(model: &Model) {
#[test]
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);
let temp_file_name = "temp_file_test_named_styles.xlsx";
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();
test_model_has_correct_styles(&model);
}
@@ -160,7 +192,9 @@ fn test_simple_text() {
#[test]
fn test_defined_names_casing() {
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 (row, column) = (2, 13); // B13