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.
|
||||
|
||||
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
|
||||
|
||||
@@ -51,9 +51,54 @@ $ cargo doc --no-deps
|
||||
> [!WARNING]
|
||||
> 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)
|
||||
* Version 1.0.0 will come later in 2024
|
||||
Major milestones:
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
@@ -127,7 +127,6 @@ impl Cell {
|
||||
Cell::BooleanCell { .. } => CellType::LogicalValue,
|
||||
Cell::NumberCell { .. } => CellType::Number,
|
||||
Cell::ErrorCell { .. } => CellType::ErrorValue,
|
||||
// TODO: An empty string should likely be considered a Number (like an empty cell).
|
||||
Cell::SharedString { .. } => CellType::Text,
|
||||
Cell::CellFormula { .. } => CellType::Number,
|
||||
Cell::CellFormulaBoolean { .. } => CellType::LogicalValue,
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::vec::Vec;
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference, Range},
|
||||
cell::CellValue,
|
||||
constants,
|
||||
constants::{self, LAST_COLUMN, LAST_ROW},
|
||||
expressions::token::{Error, OpCompare, OpProduct, OpSum, OpUnary},
|
||||
expressions::{
|
||||
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
|
||||
pub fn get_cell_value_by_ref(&self, cell_ref: &str) -> Result<CellValue, String> {
|
||||
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)
|
||||
}
|
||||
|
||||
// FIXME: Can't put it in Workbook, because language is outside of workbook, sic!
|
||||
pub fn get_cell_value_by_index(
|
||||
&self,
|
||||
sheet_index: u32,
|
||||
@@ -1238,7 +1236,6 @@ impl Model {
|
||||
Ok(cell_value)
|
||||
}
|
||||
|
||||
// FIXME: Can't put it in Workbook, because locale and language are outside of workbook, sic!
|
||||
pub fn formatted_cell_value(
|
||||
&self,
|
||||
sheet_index: u32,
|
||||
@@ -1259,6 +1256,28 @@ impl Model {
|
||||
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
|
||||
pub fn get_all_cells(&self) -> Vec<CellIndex> {
|
||||
let mut cells = Vec::new();
|
||||
@@ -1316,7 +1335,6 @@ impl Model {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: expect
|
||||
pub fn get_cell_style_index(&self, sheet: u32, row: i32, column: i32) -> i32 {
|
||||
// First check the cell, then row, the column
|
||||
let cell = self
|
||||
@@ -1414,6 +1432,50 @@ impl Model {
|
||||
};
|
||||
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)]
|
||||
|
||||
@@ -47,5 +47,6 @@ mod test_number_format;
|
||||
|
||||
mod test_escape_quotes;
|
||||
mod test_fn_type;
|
||||
mod test_get_cell_content;
|
||||
mod test_percentage;
|
||||
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")]
|
||||
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)
|
||||
.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