Files
IronCalc/xlsx/tests/test.rs
Nicolás Hatcher da017b6113 UPDATE: Implement the implicit Intersection Operator
The II operator takes a range and returns a single cell that is in the same column or the same row
as the present cell.

This is needed for backwards compatibility with old Excel models and as a first step towards dynamic arrays.

In the past Excel would evaluate `=A1:A10` in cell `C3` as `A3`, but today in results in an array containing all
values in the range. To be compatible with old workbooks Excel inserts the II operator
on those cases.

So this PR performs an static analysis on all formulas inserting on import automatically the II operator
where necessary. This we call the _automatic implicit operator_. When exporting to Excel the operator is striped away.
You can also manually use the II. For instance `=SUM(@A1:A10)` in cell `C3`.
This was not possible before and such a formula would break backwards compatibility with Excel. To Excel that "non automatic"
form of the II is exported as `_xlfn.SINGLE()`.

Th static analysis has to be done for all arithmetic operations and all functions.
This is a bit of a daunting task and it is not done fully in this PR. We also need to implement arrays and dynamic arrays.
My believe is that once the core operations have been implemented we can go formula by formula writing proper tests and documentation.

After this PR formulas like `=A1:A10` for instance will return `#N/IMPL!` instead of performing the implicit intersection
2025-03-03 21:59:42 +01:00

534 lines
17 KiB
Rust

#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use std::io::Read;
use std::{env, fs, io};
use uuid::Uuid;
use ironcalc::compare::{test_file, test_load_and_saving};
use ironcalc::export::save_to_xlsx;
use ironcalc::import::{load_from_icalc, load_from_xlsx, load_from_xlsx_bytes};
use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment};
use ironcalc_base::{Model, UserModel};
// 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_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(),
"Second".to_string(),
"Sheet4".to_string(),
"shared".to_string(),
"Table".to_string(),
"Sheet2".to_string(),
"Created fourth".to_string(),
"Frozen".to_string(),
"Split".to_string(),
"Hidden".to_string(),
];
let names: Vec<String> = ws.iter().map(|s| s.name.clone()).collect();
// One is not not imported and one is hidden
assert_eq!(expected_names, names);
assert_eq!(workbook.views[&0].sheet, 7);
// 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].views[&0].row, 13);
assert_eq!(ws[0].views[&0].column, 5);
assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]);
let model2 = load_from_icalc("tests/example.ic").unwrap();
let _ = bitcode::encode(&model2.workbook);
assert_eq!(workbook, model2.workbook);
}
#[test]
fn test_load_from_xlsx_bytes() {
let file_path = std::path::Path::new("tests/example.xlsx");
let mut file = fs::File::open(file_path).unwrap();
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).unwrap();
let workbook = load_from_xlsx_bytes(&bytes, "home", "en", "UTC").unwrap();
assert_eq!(workbook.views[&0].sheet, 7);
}
#[test]
fn no_grid() {
let model = load_from_xlsx("tests/NoGrid.xlsx", "en", "UTC").unwrap();
{
let workbook = &model.workbook;
let ws = &workbook.worksheets;
// NoGrid does not show grid lines
let no_grid_sheet = &ws[0];
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
assert!(!no_grid_sheet.show_grid_lines);
let sheet2 = &ws[1];
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
assert!(sheet2.show_grid_lines);
let no_grid_no_headers_sheet = &ws[2];
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
// There is also no headers
assert!(!no_grid_no_headers_sheet.show_grid_lines);
}
{
// save it and check again
let temp_file_name = "temp_file_no_grid.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
let workbook = &model.workbook;
let ws = &workbook.worksheets;
// NoGrid does not show grid lines
let no_grid_sheet = &ws[0];
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
assert!(!no_grid_sheet.show_grid_lines);
let sheet2 = &ws[1];
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
assert!(sheet2.show_grid_lines);
let no_grid_no_headers_sheet = &ws[2];
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
// There is also no headers
assert!(!no_grid_no_headers_sheet.show_grid_lines);
fs::remove_file(temp_file_name).unwrap();
}
}
#[test]
fn test_save_to_xlsx() {
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_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
assert_eq!(metadata.app_version, "10.0000");
let workbook = model.workbook;
let ws = &workbook.worksheets;
assert_eq!(workbook.views[&0].sheet, 7);
// 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].views[&0].row, 13);
assert_eq!(ws[0].views[&0].column, 5);
assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]);
// TODO: can we show it is the 'same' model?
fs::remove_file(temp_file_name).unwrap();
}
#[test]
fn test_freeze() {
// freeze has 3 frozen columns and 2 frozen rows
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);
}
#[test]
fn test_split() {
// We test that a workbook with split panes do not produce frozen rows and columns
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);
}
fn test_model_has_correct_styles(model: &Model) {
// A1 is bold
let style_a1 = model.get_style_for_cell(0, 1, 1).unwrap();
assert!(style_a1.font.b);
assert!(!style_a1.font.i);
assert!(!style_a1.font.u);
// B1 is Italics
let style_b1 = model.get_style_for_cell(0, 1, 2).unwrap();
assert!(style_b1.font.i);
assert!(!style_b1.font.b);
assert!(!style_b1.font.u);
// C1 Underlined
let style_c1 = model.get_style_for_cell(0, 1, 3).unwrap();
assert!(style_c1.font.u);
assert!(!style_c1.font.b);
assert!(!style_c1.font.i);
// D1 Bold and Italics
let style_d1 = model.get_style_for_cell(0, 1, 4).unwrap();
assert!(style_d1.font.b);
assert!(style_d1.font.i);
assert!(!style_d1.font.u);
// E1 Bold, italics and underlined
let style_e1 = model.get_style_for_cell(0, 1, 5).unwrap();
assert!(style_e1.font.b);
assert!(style_e1.font.i);
assert!(style_e1.font.u);
assert!(!style_e1.font.strike);
// F1 strikethrough
let style_f1 = model.get_style_for_cell(0, 1, 6).unwrap();
assert!(style_f1.font.strike);
// G1 Double underlined just get simple underlined
let style_g1 = model.get_style_for_cell(0, 1, 7).unwrap();
assert!(style_g1.font.u);
let height_row_3 = model.workbook.worksheet(0).unwrap().row_height(3).unwrap();
assert_eq!(height_row_3, 136.0);
let height_row_5 = model.workbook.worksheet(0).unwrap().row_height(5).unwrap();
assert_eq!(height_row_5, 62.0);
// Second sheet has alignment
// Horizontal
let alignment = model.get_style_for_cell(1, 2, 1).unwrap().alignment;
assert_eq!(alignment, None);
let alignment = model
.get_style_for_cell(1, 3, 1)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.horizontal, HorizontalAlignment::Left);
let alignment = model
.get_style_for_cell(1, 4, 1)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.horizontal, HorizontalAlignment::Distributed);
let alignment = model
.get_style_for_cell(1, 5, 1)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.horizontal, HorizontalAlignment::Right);
let alignment = model
.get_style_for_cell(1, 6, 1)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.horizontal, HorizontalAlignment::Center);
let alignment = model
.get_style_for_cell(1, 7, 1)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.horizontal, HorizontalAlignment::Fill);
let alignment = model
.get_style_for_cell(1, 8, 1)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.horizontal, HorizontalAlignment::Justify);
// Vertical
let alignment = model.get_style_for_cell(1, 2, 2).unwrap().alignment;
assert_eq!(alignment, None);
let alignment = model.get_style_for_cell(1, 3, 2).unwrap().alignment;
assert_eq!(alignment, None);
let alignment = model
.get_style_for_cell(1, 4, 2)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.vertical, VerticalAlignment::Top);
let alignment = model
.get_style_for_cell(1, 5, 2)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.vertical, VerticalAlignment::Center);
let alignment = model
.get_style_for_cell(1, 6, 2)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.vertical, VerticalAlignment::Justify);
let alignment = model
.get_style_for_cell(1, 7, 2)
.unwrap()
.alignment
.unwrap();
assert_eq!(alignment.vertical, VerticalAlignment::Distributed);
}
#[test]
fn test_simple_text() {
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_from_xlsx(temp_file_name, "en", "UTC").unwrap();
fs::remove_file(temp_file_name).unwrap();
test_model_has_correct_styles(&model);
}
#[test]
fn test_defined_names_casing() {
let test_file_path = "tests/calc_tests/defined_names_for_unit_test.xlsx";
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
let test_cases = [
("=named1", "11"),
("=NAMED1", "11"),
("=NaMeD1", "11"),
("=named2", "22"),
("=NAMED2", "22"),
("=NaMeD2", "22"),
("=named3", "33"),
("=NAMED3", "33"),
("=NaMeD3", "33"),
];
for (formula, expected_value) in test_cases {
model
.set_user_input(0, row, column, formula.to_string())
.unwrap();
model.evaluate();
assert_eq!(
model.get_formatted_cell_value(0, row, column).unwrap(),
expected_value
);
}
}
#[test]
fn test_xlsx() {
let mut entries = fs::read_dir("tests/calc_tests/")
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()
.unwrap();
entries.sort();
let temp_folder = env::temp_dir();
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {}", file_path_str);
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{}", message);
is_error = true;
}
let t = test_load_and_saving(file_path_str, &dir);
if t.is_err() {
println!("Error while load and saving file: {file_path_str}");
is_error = true;
}
} else {
println!("skipping");
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
);
}
#[test]
fn no_export() {
let mut entries = fs::read_dir("tests/calc_test_no_export/")
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()
.unwrap();
entries.sort();
let temp_folder = env::temp_dir();
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {}", file_path_str);
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{}", message);
is_error = true;
}
} else {
println!("skipping");
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
);
}
// This test verifies whether exporting the merged cells functionality is happening properly or not.
// It first loads the Excel having the merged cell and exports it to another xlsx and verifies whether merged
// cell node is same in both of the xlsx file or not.
#[test]
fn test_exporting_merged_cells() {
let temp_file_name = "temp_file_test_export_merged_cells.xlsx";
let expected_merge_cell_ref = {
// loading the xlsx file containing merged cells
let example_file_name = "tests/example.xlsx";
let mut model = load_from_xlsx(example_file_name, "en", "UTC").unwrap();
let expected_merge_cell_ref = model
.workbook
.worksheets
.first()
.unwrap()
.merge_cells
.clone();
// exporting and saving it in another xlsx
model.evaluate();
save_to_xlsx(&model, temp_file_name).unwrap();
expected_merge_cell_ref
};
{
let mut temp_model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
{
// loading the previous file back and verifying whether
// merged cells got exported properly or not
let got_merge_cell_ref = &temp_model
.workbook
.worksheets
.first()
.unwrap()
.merge_cells
.clone();
assert_eq!(expected_merge_cell_ref, *got_merge_cell_ref);
fs::remove_file(temp_file_name).unwrap();
}
{
// this block is to verify that if there are no
// merged cells, exported xml should not have the
// <mergeCells/> xml node
temp_model
.workbook
.worksheets
.get_mut(0)
.unwrap()
.merge_cells
.clear();
save_to_xlsx(&temp_model, temp_file_name).unwrap();
let temp_model2 = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
let got_merge_cell_ref_cnt = &temp_model2
.workbook
.worksheets
.first()
.unwrap()
.merge_cells
.len();
assert!(*got_merge_cell_ref_cnt == 0);
}
}
fs::remove_file(temp_file_name).unwrap();
}
#[test]
fn test_documentation_xlsx() {
let mut entries = fs::read_dir("tests/docs/")
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()
.unwrap();
entries.sort();
// We can't test volatiles
let mut skip = vec!["DATE.xlsx", "DAY.xlsx", "MONTH.xlsx", "YEAR.xlsx"];
// Numerically unstable
skip.push("TAN.xlsx");
let skip: Vec<String> = skip.iter().map(|s| format!("tests/docs/{s}")).collect();
println!("{:?}", skip);
// dumb counter to make sure we are actually testing the files
assert!(entries.len() > 7);
let temp_folder = env::temp_dir();
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
if skip.contains(&file_path_str.to_string()) {
println!("Skipping file: {}", file_path_str);
continue;
}
println!("Testing file: {}", file_path_str);
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("{}", message);
is_error = true;
}
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
} else {
println!("skipping");
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
)
}
#[test]
fn test_user_model() {
let temp_file_name = "temp_file_test_user_model.xlsx";
let mut model = UserModel::new_empty("my_model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "=1+1").unwrap();
// test we can use `get_model` to save the model
save_to_xlsx(model.get_model(), temp_file_name).unwrap();
fs::remove_file(temp_file_name).unwrap();
// we can still use the model afterwards
model.set_rows_height(0, 1, 1, 100.0).unwrap();
}