UPDATE: Update README to include a minimal ROADMAD
This commit is contained in:
55
README.md
55
README.md
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
IronCalc is a new, modern, work-in-progress spreadsheet engine and set of tools to work with spreadsheets in diverse settings.
|
IronCalc is a new, modern, work-in-progress spreadsheet engine and set of tools to work with spreadsheets in diverse settings.
|
||||||
|
|
||||||
This repository contains the main engine and the xlsx importer and exporter.
|
This repository contains the main engine and the xlsx reader and writer.
|
||||||
|
|
||||||
Programmed in Rust, you can use it from a variety of programming languages like [Python](https://github.com/ironcalc/bindings-python), [JavaScript (wasm)](https://github.com/ironcalc/bindings-js), [nodejs](https://github.com/ironcalc/bindings-nodejs) and soon R, Julia, Go and possibly others.
|
Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go.
|
||||||
|
|
||||||
It has several different _skins_. You can use it in the [terminal](https://github.com/ironcalc/skin-terminal), as a [desktop application](https://github.com/ironcalc/bindings-desktop) or use it in you own [web application](https://github.com/ironcalc/skin-web).
|
We will build different _skins_: in the terminal, as a desktop application or use it in you own web application.
|
||||||
|
|
||||||
# 🛠️ Building
|
# 🛠️ Building
|
||||||
|
|
||||||
@@ -51,9 +51,54 @@ $ cargo doc --no-deps
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This is work-in-progress. IronCalc in developed in the open. Expect things to be broken and change quickly until version 0.5
|
> This is work-in-progress. IronCalc in developed in the open. Expect things to be broken and change quickly until version 0.5
|
||||||
|
|
||||||
* We intend to have a working version by mid January 2024 (version 0.5, MVP)
|
Major milestones:
|
||||||
* Version 1.0.0 will come later in 2024
|
|
||||||
|
|
||||||
|
* MVP, version 0.5.0: We intend to have a working version by mid January 2024 (version 0.5, MVP)
|
||||||
|
* Stable, version 1.0.0 will come later in December 2024
|
||||||
|
|
||||||
|
MVP stands for _Minimum Viable Product_
|
||||||
|
|
||||||
|
### Version 0.5 or MVP (15 January 2024)
|
||||||
|
|
||||||
|
Version 0.5 includes the engine, javascript and nodejs bindings and a web application
|
||||||
|
|
||||||
|
Features of the engine include:
|
||||||
|
|
||||||
|
* Read and write xlsx files
|
||||||
|
* API to set and read values from cells
|
||||||
|
* Implemented 192 Excel functions
|
||||||
|
* Time functions with timezones
|
||||||
|
* Prepared for i18n but will only support English
|
||||||
|
* Wide test coverage
|
||||||
|
|
||||||
|
UI features of the web application (backed by the engine):
|
||||||
|
|
||||||
|
* Enter values and formulas. Browse mode
|
||||||
|
* Italics, bold, underline, horizontal alignment
|
||||||
|
* Number formatting
|
||||||
|
* Add/remove/rename sheets
|
||||||
|
* Copy/Paste extend values
|
||||||
|
* Keyboard navigation
|
||||||
|
* Delete/Add rows and columns
|
||||||
|
* Resize rows and columns
|
||||||
|
* Correct scrolling and navigation
|
||||||
|
|
||||||
|
### Version 1.0 or Stable (December 2024)
|
||||||
|
|
||||||
|
Minor milestones in the ROADMAD for version 1.0.0 (engine and UI):
|
||||||
|
|
||||||
|
* Implementation of arrays and array formulas
|
||||||
|
* Formula documentation and context help
|
||||||
|
* Merge cells
|
||||||
|
* Pivot tables
|
||||||
|
* Define name manager (mostly UI)
|
||||||
|
* Update main evaluation algorithm with a support graph
|
||||||
|
* Dynamic arrays (SORT, UNIQUE, ..)
|
||||||
|
* Full i18n support with different locales and languages
|
||||||
|
* Python bindings
|
||||||
|
* Full test coverage
|
||||||
|
|
||||||
|
I will be creating issues during the first two months of 2024
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ impl Cell {
|
|||||||
Cell::BooleanCell { .. } => CellType::LogicalValue,
|
Cell::BooleanCell { .. } => CellType::LogicalValue,
|
||||||
Cell::NumberCell { .. } => CellType::Number,
|
Cell::NumberCell { .. } => CellType::Number,
|
||||||
Cell::ErrorCell { .. } => CellType::ErrorValue,
|
Cell::ErrorCell { .. } => CellType::ErrorValue,
|
||||||
// TODO: An empty string should likely be considered a Number (like an empty cell).
|
|
||||||
Cell::SharedString { .. } => CellType::Text,
|
Cell::SharedString { .. } => CellType::Text,
|
||||||
Cell::CellFormula { .. } => CellType::Number,
|
Cell::CellFormula { .. } => CellType::Number,
|
||||||
Cell::CellFormulaBoolean { .. } => CellType::LogicalValue,
|
Cell::CellFormulaBoolean { .. } => CellType::LogicalValue,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::vec::Vec;
|
|||||||
use crate::{
|
use crate::{
|
||||||
calc_result::{CalcResult, CellReference, Range},
|
calc_result::{CalcResult, CellReference, Range},
|
||||||
cell::CellValue,
|
cell::CellValue,
|
||||||
constants,
|
constants::{self, LAST_COLUMN, LAST_ROW},
|
||||||
expressions::token::{Error, OpCompare, OpProduct, OpSum, OpUnary},
|
expressions::token::{Error, OpCompare, OpProduct, OpSum, OpUnary},
|
||||||
expressions::{
|
expressions::{
|
||||||
parser::move_formula::{move_formula, MoveContext},
|
parser::move_formula::{move_formula, MoveContext},
|
||||||
@@ -1207,7 +1207,6 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Can't put it in Workbook, because language is outside of workbook, sic!
|
|
||||||
/// Gets the Excel Value (Bool, Number, String) of a cell
|
/// Gets the Excel Value (Bool, Number, String) of a cell
|
||||||
pub fn get_cell_value_by_ref(&self, cell_ref: &str) -> Result<CellValue, String> {
|
pub fn get_cell_value_by_ref(&self, cell_ref: &str) -> Result<CellValue, String> {
|
||||||
let cell_reference = match self.parse_reference(cell_ref) {
|
let cell_reference = match self.parse_reference(cell_ref) {
|
||||||
@@ -1221,7 +1220,6 @@ impl Model {
|
|||||||
self.get_cell_value_by_index(sheet_index, row, column)
|
self.get_cell_value_by_index(sheet_index, row, column)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Can't put it in Workbook, because language is outside of workbook, sic!
|
|
||||||
pub fn get_cell_value_by_index(
|
pub fn get_cell_value_by_index(
|
||||||
&self,
|
&self,
|
||||||
sheet_index: u32,
|
sheet_index: u32,
|
||||||
@@ -1238,7 +1236,6 @@ impl Model {
|
|||||||
Ok(cell_value)
|
Ok(cell_value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Can't put it in Workbook, because locale and language are outside of workbook, sic!
|
|
||||||
pub fn formatted_cell_value(
|
pub fn formatted_cell_value(
|
||||||
&self,
|
&self,
|
||||||
sheet_index: u32,
|
sheet_index: u32,
|
||||||
@@ -1259,6 +1256,28 @@ impl Model {
|
|||||||
Ok(formatted_value)
|
Ok(formatted_value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a string with the cell content. If there is a formula returns the formula
|
||||||
|
/// If the cell is empty returns the empty string
|
||||||
|
/// Raises an error if there is no worksheet
|
||||||
|
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, String> {
|
||||||
|
let worksheet = self.workbook.worksheet(sheet)?;
|
||||||
|
let cell = match worksheet.cell(row, column) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok("".to_string()),
|
||||||
|
};
|
||||||
|
match cell.get_formula() {
|
||||||
|
Some(formula_index) => {
|
||||||
|
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
||||||
|
let cell_ref = CellReferenceRC {
|
||||||
|
sheet: worksheet.get_name(),
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
};
|
||||||
|
Ok(format!("={}", to_string(formula, &cell_ref)))
|
||||||
|
}
|
||||||
|
None => Ok(cell.get_text(&self.workbook.shared_strings, &self.language)),
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Returns a list of all cells
|
/// Returns a list of all cells
|
||||||
pub fn get_all_cells(&self) -> Vec<CellIndex> {
|
pub fn get_all_cells(&self) -> Vec<CellIndex> {
|
||||||
let mut cells = Vec::new();
|
let mut cells = Vec::new();
|
||||||
@@ -1316,7 +1335,6 @@ impl Model {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: expect
|
|
||||||
pub fn get_cell_style_index(&self, sheet: u32, row: i32, column: i32) -> i32 {
|
pub fn get_cell_style_index(&self, sheet: u32, row: i32, column: i32) -> i32 {
|
||||||
// First check the cell, then row, the column
|
// First check the cell, then row, the column
|
||||||
let cell = self
|
let cell = self
|
||||||
@@ -1414,6 +1432,50 @@ impl Model {
|
|||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_frozen_rows(&self, sheet: u32) -> Result<i32, String> {
|
||||||
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||||
|
Ok(worksheet.frozen_rows)
|
||||||
|
} else {
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_frozen_columns(&self, sheet: u32) -> Result<i32, String> {
|
||||||
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||||
|
Ok(worksheet.frozen_columns)
|
||||||
|
} else {
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frozen_rows(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> {
|
||||||
|
if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) {
|
||||||
|
if frozen_rows < 0 {
|
||||||
|
return Err("Frozen rows cannot be negative".to_string());
|
||||||
|
} else if frozen_rows >= LAST_ROW {
|
||||||
|
return Err("Too many rows".to_string());
|
||||||
|
}
|
||||||
|
worksheet.frozen_rows = frozen_rows;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frozen_columns(&mut self, sheet: u32, frozen_columns: i32) -> Result<(), String> {
|
||||||
|
if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) {
|
||||||
|
if frozen_columns < 0 {
|
||||||
|
return Err("Frozen columns cannot be negative".to_string());
|
||||||
|
} else if frozen_columns >= LAST_COLUMN {
|
||||||
|
return Err("Too many columns".to_string());
|
||||||
|
}
|
||||||
|
worksheet.frozen_columns = frozen_columns;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -47,5 +47,6 @@ mod test_number_format;
|
|||||||
|
|
||||||
mod test_escape_quotes;
|
mod test_escape_quotes;
|
||||||
mod test_fn_type;
|
mod test_fn_type;
|
||||||
|
mod test_get_cell_content;
|
||||||
mod test_percentage;
|
mod test_percentage;
|
||||||
mod test_today;
|
mod test_today;
|
||||||
|
|||||||
71
base/src/test/test_frozen_rows_and_columns.rs
Normal file
71
base/src/test/test_frozen_rows_and_columns.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::expressions::utils::{LAST_COLUMN, LAST_ROW};
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_model() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
assert_eq!(model.get_frozen_rows(0), Ok(0));
|
||||||
|
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||||
|
|
||||||
|
let e = model.set_frozen_rows(0, 3);
|
||||||
|
assert!(e.is_ok());
|
||||||
|
assert_eq!(model.get_frozen_rows(0), Ok(3));
|
||||||
|
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||||
|
|
||||||
|
let e = model.set_frozen_columns(0, 53);
|
||||||
|
assert!(e.is_ok());
|
||||||
|
assert_eq!(model.get_frozen_rows(0), Ok(3));
|
||||||
|
assert_eq!(model.get_frozen_columns(0), Ok(53));
|
||||||
|
|
||||||
|
// Set them back to zero
|
||||||
|
let e = model.set_frozen_rows(0, 0);
|
||||||
|
assert!(e.is_ok());
|
||||||
|
let e = model.set_frozen_columns(0, 0);
|
||||||
|
assert!(e.is_ok());
|
||||||
|
assert_eq!(model.get_frozen_rows(0), Ok(0));
|
||||||
|
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_sheet() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
assert_eq!(model.get_frozen_rows(1), Err("Invalid sheet".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
model.get_frozen_columns(3),
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.set_frozen_rows(3, 3),
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.set_frozen_columns(3, 5),
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_rows_columns() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.set_frozen_rows(0, -3),
|
||||||
|
Err("Frozen rows cannot be negative".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.set_frozen_columns(0, -5),
|
||||||
|
Err("Frozen columns cannot be negative".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.set_frozen_rows(0, LAST_ROW),
|
||||||
|
Err("Too many rows".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.set_frozen_columns(0, LAST_COLUMN),
|
||||||
|
Err("Too many columns".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
18
base/src/test/test_get_cell_content.rs
Normal file
18
base/src/test/test_get_cell_content.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_formulas() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model.set_user_input(0, 1, 1, "$100.348".to_string());
|
||||||
|
model.set_user_input(0, 1, 2, "=ISNUMBER(A1)".to_string());
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model.get_cell_content(0, 1, 1).unwrap(), "100.348");
|
||||||
|
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=ISNUMBER(A1)");
|
||||||
|
assert_eq!(model.get_cell_content(0, 5, 5).unwrap(), "");
|
||||||
|
|
||||||
|
assert!(model.get_cell_content(1, 1, 2).is_err());
|
||||||
|
}
|
||||||
@@ -651,3 +651,13 @@ pub struct Border {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub diagonal: Option<BorderItem>,
|
pub diagonal: Option<BorderItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Information need to show a sheet tab in the UI
|
||||||
|
/// The color is serialized only if it is not Color::None
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct SheetInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub state: String,
|
||||||
|
pub sheet_id: u32,
|
||||||
|
pub color: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,4 +27,16 @@ impl Workbook {
|
|||||||
.get_mut(worksheet_index as usize)
|
.get_mut(worksheet_index as usize)
|
||||||
.ok_or_else(|| "Invalid sheet index".to_string())
|
.ok_or_else(|| "Invalid sheet index".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_worksheets_info(&self) -> Vec<SheetInfo> {
|
||||||
|
self.worksheets
|
||||||
|
.iter()
|
||||||
|
.map(|worksheet| SheetInfo {
|
||||||
|
name: worksheet.get_name(),
|
||||||
|
state: worksheet.state.to_string(),
|
||||||
|
color: worksheet.color.clone(),
|
||||||
|
sheet_id: worksheet.sheet_id,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user