UPDATE: Update README to include a minimal ROADMAD

This commit is contained in:
Nicolás Hatcher
2023-12-01 08:53:12 +01:00
parent c5b8efd83d
commit c63acd72d0
8 changed files with 229 additions and 11 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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)]

View File

@@ -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;

View 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())
);
}

View 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());
}

View File

@@ -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>,
}

View File

@@ -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()
}
}