Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolás Hatcher
ae53901a24 UPDATE: Updates docs on Errors in IronCalc 2024-12-15 07:52:52 +01:00
33 changed files with 650 additions and 1028 deletions

View File

@@ -418,7 +418,6 @@ impl Model {
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
}
VariableKind(defined_name) => {
println!("{:?}", defined_name);
let parsed_defined_name = self
.parsed_defined_names
.get(&(Some(cell.sheet), defined_name.to_lowercase())) // try getting local defined name
@@ -427,7 +426,6 @@ impl Model {
.get(&(None, defined_name.to_lowercase()))
}); // fallback to global
println!("Parsed: {:?}", defined_name);
if let Some(parsed_defined_name) = parsed_defined_name {
match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => {
@@ -1988,95 +1986,6 @@ impl Model {
.worksheet_mut(sheet)?
.set_row_height(column, height)
}
/// Adds a new defined name
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
// if the defined name already exist return error
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
return Err("Defined name already exists".to_string());
}
}
self.workbook.defined_names.push(DefinedName {
name: name.to_string(),
formula: formula.to_string(),
sheet_id: scope,
});
self.reset_parsed_structures();
Ok(())
}
/// Delete defined name of name and scope
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let mut index = None;
for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
index = Some(i);
}
}
if let Some(i) = index {
self.workbook.defined_names.remove(i);
self.reset_parsed_structures();
Ok(())
} else {
Err("Defined name not found".to_string())
}
}
/// returns the formula for a defined name
pub fn get_defined_name_formula(
&self,
name: &str,
scope: Option<u32>,
) -> Result<String, String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
return Ok(df.formula.clone());
}
}
Err("Defined name not found".to_string())
}
/// update defined name
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let mut index = None;
for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
index = Some(i);
}
}
if let Some(i) = index {
if let Some(df) = self.workbook.defined_names.get_mut(i) {
df.name = new_name.to_string();
df.sheet_id = new_scope;
df.formula = new_formula.to_string();
self.reset_parsed_structures();
}
Ok(())
} else {
Err("Defined name not found".to_string())
}
}
}
#[cfg(test)]

View File

@@ -3,7 +3,6 @@ mod test_autofill_columns;
mod test_autofill_rows;
mod test_border;
mod test_clear_cells;
mod test_defined_names;
mod test_diff_queue;
mod test_evaluation;
mod test_general;

View File

@@ -1,40 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
#[test]
fn create_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.new_defined_name("myName", None, "$A$1").unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
// rename it
model
.update_defined_name("myName", None, "myName", None, "$A$1*2")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
// delete it
model.delete_defined_name("myName", None).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("#REF!".to_string())
);
}
#[test]
fn rename_defined_name() {}
#[test]
fn delete_sheet() {}
#[test]
fn change_scope() {}

View File

@@ -99,7 +99,7 @@ fn cut_paste() {
// paste in cell D4 (4, 4)
model
.paste_from_clipboard(0, (1, 1, 2, 2), &copy.data, true)
.paste_from_clipboard((1, 1, 2, 2), &copy.data, true)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
@@ -119,26 +119,6 @@ fn cut_paste() {
assert_eq!(model.get_cell_content(0, 2, 2), Ok("".to_string()));
}
#[test]
fn cut_paste_different_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_selected_range(1, 1, 1, 1).unwrap();
let copy = model.copy_to_clipboard().unwrap();
model.new_sheet().unwrap();
model.set_selected_sheet(1).unwrap();
model.set_selected_cell(4, 4).unwrap();
// paste in cell D4 (4, 4) of Sheet2
model
.paste_from_clipboard(0, (1, 1, 1, 1), &copy.data, true)
.unwrap();
assert_eq!(model.get_cell_content(1, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 1, 1), Ok("".to_string()));
}
#[test]
fn copy_paste_internal() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
@@ -172,7 +152,7 @@ fn copy_paste_internal() {
// paste in cell D4 (4, 4)
model
.paste_from_clipboard(0, (1, 1, 2, 2), &copy.data, false)
.paste_from_clipboard((1, 1, 2, 2), &copy.data, false)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));

View File

@@ -13,8 +13,8 @@ use crate::{
},
model::Model,
types::{
Alignment, BorderItem, CellType, Col, DefinedName, HorizontalAlignment, SheetProperties,
Style, VerticalAlignment,
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, Style,
VerticalAlignment,
},
utils::is_valid_hex_color,
};
@@ -39,7 +39,6 @@ pub struct ClipboardCell {
pub struct Clipboard {
pub(crate) csv: String,
pub(crate) data: ClipboardData,
pub(crate) sheet: u32,
pub(crate) range: (i32, i32, i32, i32),
}
@@ -1521,7 +1520,6 @@ impl UserModel {
Ok(Clipboard {
csv,
data,
sheet,
range: (row_start, column_start, row_end, column_end),
})
}
@@ -1529,7 +1527,6 @@ impl UserModel {
/// Paste text that we copied
pub fn paste_from_clipboard(
&mut self,
source_sheet: u32,
source_range: ClipboardTuple,
clipboard: &ClipboardData,
is_cut: bool,
@@ -1620,17 +1617,17 @@ impl UserModel {
let old_value = self
.model
.workbook
.worksheet(source_sheet)?
.worksheet(sheet)?
.cell(row, column)
.cloned();
diff_list.push(Diff::CellClearContents {
sheet: source_sheet,
sheet,
row,
column,
old_value: Box::new(old_value),
});
self.model.cell_clear_contents(source_sheet, row, column)?;
self.model.cell_clear_contents(sheet, row, column)?;
}
}
}
@@ -1692,66 +1689,6 @@ impl UserModel {
Ok(())
}
/// Returns the list of defined names
pub fn get_defined_name_list(&self) -> Vec<DefinedName> {
self.model.workbook.defined_names.clone()
}
/// Delete an existing defined name
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let old_value = self.model.get_defined_name_formula(name, scope)?;
let diff_list = vec![Diff::DeleteDefinedName {
name: name.to_string(),
scope,
old_value,
}];
self.push_diff_list(diff_list);
self.model.delete_defined_name(name, scope)?;
self.evaluate_if_not_paused();
Ok(())
}
/// Create a new defined name
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), String> {
self.model.new_defined_name(name, scope, formula)?;
let diff_list = vec![Diff::CreateDefinedName {
name: name.to_string(),
scope,
value: formula.to_string(),
}];
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Updates a defined name
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), String> {
let old_formula = self.model.get_defined_name_formula(name, scope)?;
let diff_list = vec![Diff::UpdateDefinedName {
name: name.to_string(),
scope,
old_formula: old_formula.to_string(),
new_name: new_name.to_string(),
new_scope,
new_formula: new_formula.to_string(),
}];
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {
@@ -1922,20 +1859,6 @@ impl UserModel {
} => {
self.model.set_show_grid_lines(*sheet, *old_value)?;
}
Diff::CreateDefinedName { name, scope, value } => todo!(),
Diff::DeleteDefinedName {
name,
scope,
old_value,
} => todo!(),
Diff::UpdateDefinedName {
name,
scope,
old_formula,
new_name,
new_scope,
new_formula,
} => todo!(),
}
}
if needs_evaluation {
@@ -2063,20 +1986,6 @@ impl UserModel {
} => {
self.model.set_show_grid_lines(*sheet, *new_value)?;
}
Diff::CreateDefinedName { name, scope, value } => todo!(),
Diff::DeleteDefinedName {
name,
scope,
old_value,
} => todo!(),
Diff::UpdateDefinedName {
name,
scope,
old_formula,
new_name,
new_scope,
new_formula,
} => todo!(),
}
}

View File

@@ -108,26 +108,7 @@ pub(crate) enum Diff {
sheet: u32,
old_value: bool,
new_value: bool,
},
CreateDefinedName {
name: String,
scope: Option<u32>,
value: String,
},
DeleteDefinedName {
name: String,
scope: Option<u32>,
old_value: String,
},
UpdateDefinedName {
name: String,
scope: Option<u32>,
old_formula: String,
new_name: String,
new_scope: Option<u32>,
new_formula: String,
},
// FIXME: we are missing SetViewDiffs
}, // FIXME: we are missing SetViewDiffs
}
pub(crate) type DiffList = Vec<Diff>;

View File

@@ -169,36 +169,20 @@ clipboard_types = r"""
paste_from_clipboard = r"""
/**
* @param {number} source_sheet
* @param {any} source_range
* @param {any} clipboard
* @param {boolean} is_cut
*/
pasteFromClipboard(source_sheet: number, source_range: any, clipboard: any, is_cut: boolean): void;
pasteFromClipboard(source_range: any, clipboard: any, is_cut: boolean): void;
"""
paste_from_clipboard_types = r"""
/**
* @param {number} source_sheet
* @param {[number, number, number, number]} source_range
* @param {ClipboardData} clipboard
* @param {boolean} is_cut
*/
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
"""
defined_name_list = r"""
/**
* @returns {any}
*/
getDefinedNameList(): any;
"""
defined_name_list_types = r"""
/**
* @returns {DefinedName[]}
*/
getDefinedNameList(): DefinedName[];
pasteFromClipboard(source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
"""
def fix_types(text):
@@ -214,7 +198,6 @@ def fix_types(text):
text = text.replace(paste_csv_string, paste_csv_string_types)
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
with open("types.ts") as f:
types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str)

View File

@@ -1,4 +1,3 @@
use serde::Serialize;
use wasm_bindgen::{
prelude::{wasm_bindgen, JsError},
JsValue,
@@ -30,13 +29,6 @@ pub fn column_name_from_number(column: i32) -> Result<String, JsError> {
}
}
#[derive(Serialize)]
struct DefinedName {
name: String,
scope: Option<u32>,
formula: String,
}
#[wasm_bindgen]
pub struct Model {
model: BaseModel,
@@ -528,7 +520,6 @@ impl Model {
#[wasm_bindgen(js_name = "pasteFromClipboard")]
pub fn paste_from_clipboard(
&mut self,
source_sheet: u32,
source_range: JsValue,
clipboard: JsValue,
is_cut: bool,
@@ -538,7 +529,7 @@ impl Model {
let clipboard: ClipboardData =
serde_wasm_bindgen::from_value(clipboard).map_err(|e| to_js_error(e.to_string()))?;
self.model
.paste_from_clipboard(source_sheet, source_range, &clipboard, is_cut)
.paste_from_clipboard(source_range, &clipboard, is_cut)
.map_err(|e| to_js_error(e.to_string()))
}
@@ -550,52 +541,4 @@ impl Model {
.paste_csv_string(&range, csv)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
let data: Vec<DefinedName> =
self.model
.get_defined_name_list()
.iter()
.map(|s| DefinedName {
name: s.name.to_string(),
scope: s.sheet_id,
formula: s.formula.to_string(),
}).collect();
// Ok(data)
serde_wasm_bindgen::to_value(&data).map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "newDefinedName")]
pub fn new_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), JsError> {
self.model
.new_defined_name(name, scope, formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "updateDefinedName")]
pub fn update_defined_name(
&mut self,
name: &str,
scope: Option<u32>,
new_name: &str,
new_scope: Option<u32>,
new_formula: &str,
) -> Result<(), JsError> {
self.model
.update_defined_name(name, scope, new_name, new_scope, new_formula)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "deleteDefinedName")]
pub fn delete_definedname(&mut self, name: &str, scope: Option<u32>) -> Result<(), JsError> {
self.model
.delete_defined_name(name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
}

View File

@@ -226,10 +226,4 @@ export interface Clipboard {
csv: string;
data: ClipboardData;
range: [number, number, number, number];
}
export interface DefinedName {
name: string;
scope?: number;
formula: string;
}

View File

@@ -58,6 +58,30 @@ export default defineConfig({
text: "Error Types",
link: "/features/error-types",
},
{
text: "Value Types",
link: "/features/value-types",
},
{
text: "Optional arguments",
link: "/features/optional-arguments",
},
{
text: "Units",
link: "/features/units",
},
{
text: "Implicit Intersection",
link: "/features/implicit-intersection",
},
{
text: "Arrays and array formulas",
link: "/features/arrays",
},
{
text: "Dynamic Arrays",
link: "/features/dynamic-arrays",
},
{
text: "Unsupported Features",
link: "/features/unsupported-features",

View File

@@ -0,0 +1,15 @@
---
layout: doc
outline: deep
lang: en-US
---
# Arrays
::: warning
**Note:** This page is in construction 🚧
:::
::: warning
**Note:** This feature is not yet available on IronCalc 🚧
:::

View File

@@ -0,0 +1,15 @@
---
layout: doc
outline: deep
lang: en-US
---
# Dynamic Arrays
::: warning
**Note:** This page is in construction 🚧
:::
::: warning
**Note:** This feature is not yet available on IronCalc 🚧
:::

View File

@@ -10,28 +10,36 @@ lang: en-US
**Note:** This page is in construction 🚧
:::
When working with formulas, you may encounter these common errors:
The result of a formula is sometimes an _error_. In some situations those errors are expected and your formulas might be dealing with them.
The error `#N/A` might signal that there is no data to evaluate the formula yet. Maybe the payrol has not been introduced for that month just yet.
---
Some other errors like `#SPILL!`, `#CIRC!` or `#ERROR!` signal an error in your spreadsheet logic and must be corrected.
### **`#ERROR!`**
The first kind of errors or 'common errors' are found in other spreadsheet engines like Excel while other errrors like `#ERROR!` or `#N/IMPL` are particular to IronCalc.
**Cause:** General formula issue, like syntax errors or invalid references.
**Fix:** Check the formula for mistakes or invalid cell references.
---
## Common Errors
### **`#VALUE!`**
**Cause:** Mismatched data types (e.g., text used where numbers are expected).
**Fix:** Ensure input types are correct; convert text to numbers if needed.
It might be caused by mismatched data types (e.g., text used where numbers are expected):
---
```
5+"two"
```
The engine doesn't know how to add the number `5` to the string `two` resulting in a `#VALUE!`.
It is an actual error in your spreadsheet. It indicates that the formula isnt working as intended.
### **`#DIV/0!`**
**Cause:** Division by zero or an empty cell.
**Fix:** Ensure the denominator isnt zero or blank. Use `IF` to handle such cases:
Division by zero or an empty cell.
```
=1/0
```
Usually this is an error. However, in cases where a denominator might be blank (e.g., data not yet filled in), this could be expected. Use `IFERROR` or `IF` to handle it.
```
=IF(B1=0, "N/A", A1/B1)
@@ -39,23 +47,42 @@ When working with formulas, you may encounter these common errors:
### **`#NAME?`**
**Cause:** Unrecognized text in the formula (e.g., misspelled function names or undefined named ranges).
**Fix:** Correct spelling or define the missing name.
Found when a name is not recognized. Maybe a misspeled name for a function. Could be a referenceto defined name that has been deleted.
```
=UNKOWN_FUNCTION(A1)
```
This indicates an error in your spreadsheet logic.
### **`#REF!`**
**Cause:** Invalid cell reference, often from deleting cells used in a formula.
**Fix:** Update the formula with correct references.
Indicates an invalid cell reference, often from deleting cells used in a formula.
They can appear as a result of a computation or in a formula. Examples:
```
=Sheet34!A1
```
If `Sheet34` doesn't exist it will return `#REF!`
This is a genuine error. It indicates that part of your formula references a cell or range that is missing.
### **`#NUM!`**
**Cause:** Invalid numeric operation (e.g., calculating a square root of a negative number).
**Fix:** Adjust the formula to ensure valid numeric operations.
Invalid numeric operation (e.g., calculating a square root of a negative number).
Adjust the formula to ensure valid numeric operations.
Sometimes a `#NUM!` might be expected signalling the user that some parameter is out of scope.
### **`#N/A`**
**Cause:** A value is not available, often in lookup functions like VLOOKUP.
**Fix:** Ensure the lookup value exists or use IFNA() to handle missing values:
A value is not available, often in lookup functions like VLOOKUP.
This is frequnly not an error in your spreadsheetlogic.
You can produce a prettier answer using the [`IFNA`](/functions/information/isna) formula:
```
=IFNA(VLOOKUP(A1, B1:C10, 2, FALSE), "Not Found")
@@ -63,17 +90,68 @@ When working with formulas, you may encounter these common errors:
### **`#NULL!`**
**Cause:** Incorrect range operator in a formula (e.g., missing a colon between cell references).
**Fix:** Use correct range operators (e.g., A1:A10).
Incorrect range operator in a formula (e.g., missing a colon between cell references).
### **`#SPILL!`**
A cell in a formula will overwrite content in other cells.
This cannot happen riht now in IronCalc as formulas don't spill yet.
### **`#CIRC!`**
**Cause:** Circular reference.
**Fix:** Remove the circular reference.
Circular reference. This is an error is your spreadsheet and must be fixed.
Means that during teh course of a computation a circular dependency was found.
A circular dependency is a dependency of a formula on itself.
For instance in the cell `A1` the formula `=A1*2` is a circular dependency.
Other spreadsheet engines use circular dependencies to do "loop computations", run "sensitivity analysis" or "goal seek".
IronCalc doesn't support any of those at the moment.
## IronCalc specific errors
---
### **`#ERROR!`**
General formula issue, like syntax errors or invalid references.
In general Excel does not let you enter incorrect formulas but IronCalc will.
This will make your workbook imcompatible with Excel
For instace an incomplete formula
```
=A1+
```
### **`#N/IMPL!`**
A particular feature is not yet implemented in IronCalc
Look if there is a Github ticket or contact us via email, Discord or bluesky
## Error propagation
Some errors a created by some formulas. For instance the function `SQRT` can create the error `#NUM!` but can't ceate the error `#DIV/0`.
Once an error is created it is normally _propagated_ by all the formulas. So if cell `C3` evaluates to `#ERROR!` then the formula
`=SQRT(C3)` will return `#ERROR!`.
Not all functions propagate errors in their arguments. For instancethe function `IF(condition, if_true, if_false)` will only propagate an error in the `if_false` argument if the `condition` is `FALSE`. This is called _lazy evaluation_, the function `IF` is _lazy_, it only evaluates the arguments when needed. The opposite of lazy evaulaution is called _eager evaluation_.
Some functions also expect an error as an argument like [`ERROR.TYPE`](/functions/information/error.type) and will not propagate the error.
### **`#####`**
## See also
**Cause:** The column isnt wide enough to display the value.
**Fix:** Resize the column width to fit the content.
The following functions are convenient when working with errors
- [`ISERR(ref)`](/functions/information/iserr), `TRUE` if `ref` is any error type except the `#N/A` error.
- [`ISERROR(ref)`](/functions/information/iserror), `TRUE` if `ref` is any error.
- [`ISNA(ref)`](/functions/information/isna), `TRUE` if ref is `#N/A`.
- [`ERROR.TYPE`](/functions/information/error.type) returns the numeric code for a given error.
- [`IFERROR(ref, value)`](/functions/logical/iferror) returns `value` if the content of `ref` is an error.
- [`IFNA(ref, value)`](/functions/logical/ifna) return `value` if `ref` is #N/A errors only.

View File

@@ -0,0 +1,15 @@
---
layout: doc
outline: deep
lang: en-US
---
# Implicit Intersection
::: warning
**Note:** This page is in construction 🚧
:::
::: warning
**Note:** This feature is not yet available on IronCalc 🚧
:::

View File

@@ -0,0 +1,26 @@
---
layout: doc
outline: deep
lang: en-US
---
# Error Types
::: warning
**Note:** This page is in construction 🚧
:::
Some functions have optional arguments. For instance:
XLOOKUP(lookup_value, lookup_array, return_array, if_not_found="#N/A", match_mode=0, search_mode=1)
The three first arguments are mandatory and the last three are optional.
Optional argumenst must have a default value.
In this example if you don't want to specify the if_not_found and match_mode arguments you can leave them out:
XLOOKUP(lookup_value, lookup_array, return_array, , , -2)
That would use the default arguments for if_not_found and match_mode arguments.

View File

@@ -0,0 +1,13 @@
---
layout: doc
outline: deep
lang: en-US
---
# Units
::: warning
**Note:** This page is in construction 🚧
:::
Some IronCalc functions return values that have units like currencies, percentage or dates.

View File

@@ -0,0 +1,67 @@
---
layout: doc
outline: deep
lang: en-US
---
# Value types
::: warning
**Note:** This page is in construction 🚧
:::
In IronCalc a value, a result of a calculation can be one of:
## Numbers
Numbers in IronCalc are [IEEE 754 double precission](https://en.wikipedia.org/wiki/Double-precision_floating-point_format).
Numbers are only displayed up to 15 significant figures. That's why '=0.1+0.2' is actually '0.3'
Also numbers are compared up to 15 significant figures. So `=IF(0.1+0.2=0.3, "Valid", "Invalid")` will return `Valid`.
However `=0.3-0.2-0.1` will not result in `0` in IronCalc.
### Casting into numbers
Strings and booleans are sometimes coverted to numbers
`=1+"2"` => 3
Some functions cast in weird ways:
SUM(1, TRUE) => 2
SUM(1, "1") => 2
But SUM(1, A1) => 1 (where A1 is TRUE or "1")
Sometimes the conversion happens like => "123"+1 is actually 124 and the SQRT("4") is 2 or the SQRT(TRUE) is 1.
Some functions, however are more strict BIN2DEC(TRUE) is #VALUE!
### Dates
On spreadsheets a date is just the number of days since January 1, 1900.
## Strings
### Complex numbers
On IronCal a complex number is just a string like "1+j3".
## Booleans
### Casting from numbers
## Errors
### Casting from strings
"#N/A" => #N/A
## Arrays

View File

@@ -13,37 +13,84 @@ FV can be used to calculate future value over a specified number of compounding
If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV.
## Usage
### Syntax
**FV(rate, nper, pmt, pv, type)**
**FV(<span title="Number" style="color:#1E88E5">rate</span>, <span title="Number" style="color:#1E88E5">nper</span>, <span title="Number" style="color:#1E88E5">pmt</span>, <span title="Number" style="color:#1E88E5">pv</span>=0, <span title="Boolean" style="color:#43A047">type</span>=false) => <span title="Number" style="color:#1E88E5">fv</span>**
### Argument descriptions
* *rate*. The fixed percentage interest rate or yield per period.
* *nper*. The number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
* *pmt*. The fixed amount paid or deposited each compounding period.
* *pv* (optional). The present value or starting amount of the asset (default 0).
* *type* (optional). A logical value indicating whether the payment due dates are at the end (0) of the compounding periods or at the beginning (any non-zero value). The default is 0 when omitted.
* *rate*. ([number](/features/value-types)) The fixed percentage interest rate or yield per period.
* *nper*. ([number](/features/value-types)) The number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
It stands for <u>n</u>umber of <u>per</u>iods.
* *pmt*. ([number](/features/value-types)) The fixed amount paid or deposited each compounding period. Short for <u>p</u>ay<u>m</u>en<u>t</u>
* *pv* ([number](/features/value-types), optional). The present value or starting amount of the asset (default 0). Short for <u>p</u>resent <u>v</u>alue.
* *type* ([boolean](/features/value-types), optional). A logical value indicating whether the payment due dates are at the end (0) of the compounding periods or at the beginning (any non-zero value). The default is 0 when omitted.
### Returned value
The retruned value is a [number](/features/value-types) with [currency units](/features/units)
### Errors
* If any of the arguments is an error returns the error
* If any of the argumets is not a number (or cannot be converted to a number) it returns `#VALUE!`
* Some ranges of the parameters produce `#NUM!` error. For instnace `=FV(-3,1/2,1)`.
* Some ranges of the parameters produce the `#DIV/0!` error. For instance `=FV(-1, -1, 1)`
### Additional guidance
* Make sure that the *rate* argument specifies the interest rate or yield applicable to the compounding period, based on the value chosen for *nper*.
* The *pmt* and *pv* arguments should be expressed in the same currency unit. The value returned is expressed in the same currency unit.
* To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero.
* The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument.
<!--@include: ../markdown-snippets/error-type-details.md-->
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* If *rate* = 0, FV is given by the equation:
* If $\text{type} \neq 0$, $\text{fv}$ is given by the equation:
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$
* If $\text{type} = 0$
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$
* In both cases, in the limmit $\text{rate} = 0$, fv is given by the equation:
$$ \text{fv} = -\text{pv} - (\text{pmt} \times \text{nper}) $$
## Formula derivation
The money you have now might grow in a bank by _[compound interest](https://en.wikipedia.org/wiki/Compound_interest)_. Say you have $100, that is the present value, and your bank gives you 10% interest rate yearly.
At the end of 1 year you will have $110. In general that is $\text{pv}\times (1 + \text{rate})$. At the end of two years (the second _[annuity](https://en.wikipedia.org/wiki/Annuity)_) you will have 10\% more of the $110. That is the _compound_ part. You will have at the end of the second period $121 or in general if you invest an ammount $\text{pv}$ at an interest $\text{rate}$ and wait for $\text{nper}$ periods the future value of this _[lump sum](https://en.wikipedia.org/wiki/Lump_sum)_ will be:
$$\text{fv}_\text{ls} = \text{pv} \times (1 + \text{rate})^\text{nper}$$
Note that the periods may be years, months or anything else.
Now, supose that you also make regular payments of ammount $\text{pmt}$ each period.
To find the future value of these payments, you sum the future value of each payment at the end of the investment horizon.
There are two posibilities here:
* You make the payments at the end of the periods (type 0). This is also called an _ordinary annuity_.
* You make the payments at the beginning of each period (type 1). This is the _annuity due_ case.
To derive the formula for either of them we need to add an geometric progression. To simplify things.
Say we are at the end of period 5 and we are making the payments at the end of the period.
$$
FV = -pv - (pmt \times nper)
\text{pmt}\times (1 + \text{rate})^4+\text{pmt}\times (1 + \text{rate})^3+\text{pmt}\times (1 + \text{rate})^2 +\text{pmt}
$$
* If *rate* <> 0 and *type* = 0, FV is given by the equation:
$$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big)}{rate}
This is because the first payment has been around for 4 periods, and the second payment has been around for 3 periods...
The general formula for the sum of $\text{nper}$ terms in a geometric progression is given by $a(1 - r^n) / (1 - r)$, where $a = \text{pmt}$ and $r = 1 + \text{rate}$
$$
* If *rate* <> 0 and *type* <> 0, FV is given by the equation:
$$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big) \times(1+rate)}{rate}
\text{pmt}\times\dfrac{ (1+\text{rate})^{\text{nper}}-1}{\text{rate}}
$$
## Examples
[See this example in IronCalc](https://app.ironcalc.com/?example=fv).
## Links
* For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.
* [Investorpedia](https://www.investopedia.com/terms/f/futurevalue.asp) has a nice article on the future value.
* See also IronCalc's [NPER](/functions/financial/nper), [PMT](/functions/financial/pmt), [PV](/functions/financial/pv) and [RATE](/functions/financial/rate) functions.
* Visit Microsoft Excel's [FV function](https://support.microsoft.com/en-gb/office/fv-function-2eef9f44-a084-4c61-bdd8-4fe4bb1b71b3) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093224) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/FV) provide versions of the FV function.

View File

@@ -1,127 +0,0 @@
import type { Model } from "@ironcalc/wasm";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
styled,
} from "@mui/material";
import { t } from "i18next";
import { BookOpen, X } from "lucide-react";
import { useState } from "react";
import NamedRange from "./NamedRange";
import type { NamedRangeObject } from "./NamedRange";
type NameManagerDialogProperties = {
onClose: () => void;
onSave: () => void;
open: boolean;
model: Model;
};
function NameManagerDialog(props: NameManagerDialogProperties) {
const handleClose = () => {
props.onClose();
};
// update child component values in model
const handleSave = () => {
props.onSave(); // => onNamedRangesUpdate from toolbar
props.onClose();
};
//! Why are fields editable only while clicking them?
// update child component values in UI
const handleChange = (id: string, field: string, value: string) => {
console.log("change:", id, field, value);
// previous array elements, plus updated value
// setNamedRangesLocal((prev) =>
// prev.map((namedRange) =>
// namedRange.id === id ? { ...namedRange, [field]: value } : namedRange,
// ),
// );
};
const nameList = props.model.getDefinedNameList();
return (
<StyledDialog
open={props.open}
onClose={props.onClose}
maxWidth="md"
fullWidth
scroll="paper"
>
<StyledDialogTitle>
Named Ranges
<IconButton onClick={handleClose}>
<X size={16} />
</IconButton>
</StyledDialogTitle>
<DialogContent dividers>
{nameList.map((e) => (
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
name={e.name}
scope={e.scope}
range={e.formula}
key={`${e.name}-${e.scope}`}
onChange={handleChange}
model={props.model}
/>
))}
</DialogContent>
<StyledDialogActions>
<Box display="flex" alignItems="center">
<BookOpen
color="grey"
style={{ width: 16, height: 16, marginLeft: 12, marginRight: 8 }}
/>
<span style={{ fontSize: "12px", fontFamily: "Inter" }}>
{t("name_manager_dialog.help")}
</span>
</Box>
<Box display="flex" gap="10px">
<Button onClick={handleClose} variant="contained" color="info">
Cancel
</Button>
<Button onClick={handleSave} variant="contained">
Save changes
</Button>
</Box>
</StyledDialogActions>
</StyledDialog>
);
}
const StyledDialog = styled(Dialog)(() => ({
"& .MuiPaper-root": {
maxHeight: "60%",
minHeight: "40%",
},
}));
// font-weight: 600 is too bold compared to design, should be between 500 & 600
const StyledDialogTitle = styled(DialogTitle)`
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
`;
const StyledDialogActions = styled(DialogActions)`
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #757575;
`;
export default NameManagerDialog;

View File

@@ -1,91 +0,0 @@
import type { Model, WorksheetProperties } from "@ironcalc/wasm";
import { Box, IconButton, MenuItem, TextField, styled } from "@mui/material";
import { Trash2 } from "lucide-react";
export type NamedRangeObject = {
id: string;
name: string;
scope: string;
range: string;
};
type NamedRangeProperties = {
name: string;
scope: string;
range: string;
model: Model;
worksheets: WorksheetProperties[];
// update namedRange in model
onChange: (field: string, value: string) => void;
};
function NamedRange(props: NamedRangeProperties) {
// define onChange in parent for updating the model and values
const handleDelete = () => {
// update model
console.log("deleted named range");
};
return (
<StyledBox>
<TextField
variant="outlined"
size="small"
margin="normal"
fullWidth
value={props.name}
onChange={(event) =>
props.onChange("name", event.target.value)
}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
/>
<TextField
variant="outlined"
select
defaultValue="Workbook"
size="small"
margin="normal"
fullWidth
value={props.scope}
onChange={(event) =>
props.onChange("scope", event.target.value)
}
>
{props.worksheets.map((option) => (
<MenuItem key={option.sheet_id} value={option.name}>
{option.name}
</MenuItem>
))}
</TextField>
<TextField
variant="outlined"
size="small"
margin="normal"
fullWidth
value={props.range}
onChange={(event) =>
props.onChange("range", event.target.value)
}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
/>
{/* remove round hover animation */}
<IconButton onClick={handleDelete}>
<Trash2 size={16} absoluteStrokeWidth />
</IconButton>
</StyledBox>
);
}
const StyledBox = styled(Box)`
display: flex;
gap: 15px;
`;
export default NamedRange;

View File

@@ -1,99 +0,0 @@
import { styled } from "@mui/material";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { Check } from "lucide-react";
import type { SheetOptions } from "./types";
function isWhiteColor(color: string): boolean {
return ["#FFF", "#FFFFFF"].includes(color);
}
interface SheetListMenuProps {
open: boolean;
onClose: () => void;
anchorEl: HTMLButtonElement | null;
onSheetSelected: (index: number) => void;
sheetOptionsList: SheetOptions[];
selectedIndex: number;
}
const SheetListMenu = (properties: SheetListMenuProps) => {
const {
open,
onClose,
anchorEl,
onSheetSelected,
sheetOptionsList,
selectedIndex,
} = properties;
const hasColors = sheetOptionsList.some((tab) => !isWhiteColor(tab.color));
return (
<StyledMenu
open={open}
onClose={onClose}
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "bottom",
horizontal: 6,
}}
>
{sheetOptionsList.map((tab, index) => (
<StyledMenuItem
key={tab.sheetId}
onClick={() => onSheetSelected(index)}
>
{index === selectedIndex ? (
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
) : (
<div
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
)}
{hasColors && <ItemColor style={{ backgroundColor: tab.color }} />}
<ItemName
style={{ fontWeight: index === selectedIndex ? "bold" : "normal" }}
>
{tab.name}
</ItemName>
</StyledMenuItem>
))}
</StyledMenu>
);
};
const StyledMenu = styled(Menu)({
"& .MuiPaper-root": {
borderRadius: 8,
padding: 4,
},
"& .MuiList-padding": {
padding: 0,
},
});
const StyledMenuItem = styled(MenuItem)({
padding: 8,
borderRadius: 4,
});
const ItemColor = styled("div")`
width: 12px;
height: 12px;
border-radius: 4px;
margin-right: 8px;
`;
const ItemName = styled("div")`
font-size: 12px;
color: #333;
`;
export default SheetListMenu;

View File

@@ -1 +0,0 @@
export { default } from "./SheetTabBar";

View File

@@ -38,7 +38,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
const { t } = useTranslation();
const [borderSelected, setBorderSelected] = useState<BorderType | null>(null);
const [borderColor, setBorderColor] = useState(theme.palette.common.white);
const [borderColor, setBorderColor] = useState("#000000");
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
const [colorPickerOpen, setColorPickerOpen] = useState(false);
const [stylePickerOpen, setStylePickerOpen] = useState(false);
@@ -62,7 +62,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
// biome-ignore lint/correctness/useExhaustiveDependencies: We reset the styles, every time we open (or close) the widget
useEffect(() => {
setBorderSelected(null);
setBorderColor(theme.palette.common.white);
setBorderColor("#000000");
setBorderStyle(BorderStyle.Thin);
}, [properties.open]);
@@ -240,21 +240,31 @@ const BorderPicker = (properties: BorderPickerProps) => {
</Borders>
<Divider />
<Styles>
<ButtonWrapper
onClick={() => setColorPickerOpen(true)}
ref={borderColorButton}
>
<PencilLine />
<ButtonWrapper onClick={() => setColorPickerOpen(true)}>
<Button
type="button"
$pressed={false}
disabled={false}
ref={borderColorButton}
title={t("toolbar.borders.color")}
>
<PencilLine />
</Button>
<div style={{ flexGrow: 2 }}>Border color</div>
<ChevronRightStyled />
</ButtonWrapper>
<ButtonWrapper
onClick={() => setStylePickerOpen(true)}
ref={borderStyleButton}
>
<BorderStyleIcon />
<Button
type="button"
$pressed={false}
disabled={false}
title={t("toolbar.borders.style")}
>
<BorderStyleIcon />
</Button>
<div style={{ flexGrow: 2 }}>Border style</div>
<ChevronRightStyled />
</ButtonWrapper>
@@ -271,14 +281,6 @@ const BorderPicker = (properties: BorderPickerProps) => {
}}
anchorEl={borderColorButton}
open={colorPickerOpen}
anchorOrigin={{
vertical: "top", // Keep vertical alignment at the top
horizontal: "right", // Set horizontal alignment to right
}}
transformOrigin={{
vertical: "top", // Keep vertical alignment at the top
horizontal: "left", // Set horizontal alignment to left
}}
/>
<StyledPopover
open={stylePickerOpen}
@@ -286,10 +288,8 @@ const BorderPicker = (properties: BorderPickerProps) => {
setStylePickerOpen(false);
}}
anchorEl={borderStyleButton.current}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: 38, horizontal: -6 }}
>
<BorderStyleDialog>
<LineWrapper
@@ -336,12 +336,12 @@ const LineWrapper = styled("div")<LineWrapperProperties>`
align-items: center;
background-color: ${({ $checked }): string => {
if ($checked) {
return theme.palette.grey["200"];
return "#EEEEEE;";
}
return "inherit;";
}};
&:hover {
border: 1px solid ${theme.palette.grey["200"]};
border: 1px solid #eeeeee;
}
padding: 8px;
cursor: pointer;
@@ -351,59 +351,52 @@ const LineWrapper = styled("div")<LineWrapperProperties>`
const SolidLine = styled("div")`
width: 68px;
border-top: 1px solid ${theme.palette.grey["900"]};
border-top: 1px solid #333333;
`;
const MediumLine = styled("div")`
width: 68px;
border-top: 2px solid ${theme.palette.grey["900"]};
border-top: 2px solid #333333;
`;
const ThickLine = styled("div")`
width: 68px;
border-top: 1px solid ${theme.palette.grey["900"]};
border-top: 3px solid #333333;
`;
const Divider = styled("div")`
width: 100%;
margin: auto;
border-top: 1px solid ${theme.palette.grey["200"]};
display: inline-flex;
heigh: 1px;
border-bottom: 1px solid #eee;
margin-left: 0px;
margin-right: 0px;
`;
const Borders = styled("div")`
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px;
padding-bottom: 4px;
`;
const Styles = styled("div")`
display: flex;
flex-direction: column;
padding: 4px;
`;
const Line = styled("div")`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
`;
const ButtonWrapper = styled("div")`
display: flex;
flex-direction: row;
align-items: center;
border-radius: 4px;
gap: 8px;
&:hover {
background-color: ${theme.palette.grey["200"]};
border-top-color: ${(): string => theme.palette.grey["200"]};
background-color: #eee;
border-top-color: ${(): string => theme.palette.grey["400"]};
}
cursor: pointer;
padding: 8px;
svg {
width: 16px;
height: 16px;
}
`;
const BorderStyleDialog = styled("div")`
@@ -416,7 +409,7 @@ const BorderStyleDialog = styled("div")`
const StyledPopover = styled(Popover)`
.MuiPopover-paper {
border-radius: 8px;
border-radius: 10px;
border: 0px solid ${({ theme }): string => theme.palette.background.default};
box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
}
@@ -432,6 +425,7 @@ const StyledPopover = styled(Popover)`
const BorderPickerDialog = styled("div")`
background: ${({ theme }): string => theme.palette.background.default};
padding: 4px;
display: flex;
flex-direction: column;
`;
@@ -450,8 +444,10 @@ const Button = styled("button")<TypeButtonProperties>(
alignItems: "center",
justifyContent: "center",
// fontSize: "26px",
border: `0px solid ${theme.palette.common.white}`,
border: "0px solid #fff",
borderRadius: "4px",
marginRight: "5px",
transition: "all 0.2s",
cursor: "pointer",
padding: "0px",
};
@@ -464,15 +460,13 @@ const Button = styled("button")<TypeButtonProperties>(
}
return {
...result,
borderTop: $underlinedColor
? `3px solid ${theme.palette.common.white}`
: "none",
borderTop: $underlinedColor ? "3px solid #FFF" : "none",
borderBottom: $underlinedColor ? `3px solid ${$underlinedColor}` : "none",
color: `${theme.palette.grey["900"]}`,
color: "#21243A",
backgroundColor: $pressed ? theme.palette.grey["200"] : "inherit",
"&:hover": {
outline: `1px solid ${theme.palette.grey["200"]}`,
borderTopColor: theme.palette.grey["200"],
backgroundColor: "#F1F2F8",
borderTopColor: "#F1F2F8",
},
svg: {
width: "16px",

View File

@@ -0,0 +1,2 @@
export { default } from "./navigation";
export type { NavigationProps } from "./navigation";

View File

@@ -1,23 +1,31 @@
import { Dialog, TextField, styled } from "@mui/material";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { Check } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import type { SheetOptions } from "./types";
function isWhiteColor(color: string): boolean {
return ["#FFF", "#FFFFFF"].includes(color);
}
interface SheetRenameDialogProps {
open: boolean;
onClose: () => void;
isOpen: boolean;
close: () => void;
onNameChanged: (name: string) => void;
defaultName: string;
}
const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
export const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
const { t } = useTranslation();
const [name, setName] = useState(properties.defaultName);
const handleClose = () => {
properties.onClose();
properties.close();
};
return (
<Dialog open={properties.open} onClose={properties.onClose}>
<Dialog open={properties.isOpen} onClose={properties.close}>
<StyledDialogTitle>
{t("sheet_rename.title")}
<Cross onClick={handleClose} onKeyDown={() => {}}>
@@ -53,7 +61,7 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
event.stopPropagation();
if (event.key === "Enter") {
properties.onNameChanged(name);
properties.onClose();
properties.close();
}
}}
onChange={(event) => {
@@ -76,6 +84,94 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
);
};
interface SheetListMenuProps {
isOpen: boolean;
close: () => void;
anchorEl: HTMLButtonElement | null;
onSheetSelected: (index: number) => void;
sheetOptionsList: SheetOptions[];
selectedIndex: number;
}
const SheetListMenu = (properties: SheetListMenuProps) => {
const {
isOpen,
close,
anchorEl,
onSheetSelected,
sheetOptionsList,
selectedIndex,
} = properties;
const hasColors = sheetOptionsList.some((tab) => !isWhiteColor(tab.color));
return (
<StyledMenu
open={isOpen}
onClose={close}
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "bottom",
horizontal: 6,
}}
>
{sheetOptionsList.map((tab, index) => (
<StyledMenuItem
key={tab.sheetId}
onClick={() => onSheetSelected(index)}
>
{index === selectedIndex ? (
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
) : (
<div
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
)}
{hasColors && <ItemColor style={{ backgroundColor: tab.color }} />}
<ItemName
style={{ fontWeight: index === selectedIndex ? "bold" : "normal" }}
>
{tab.name}
</ItemName>
</StyledMenuItem>
))}
</StyledMenu>
);
};
const StyledMenu = styled(Menu)({
"& .MuiPaper-root": {
borderRadius: 8,
padding: 4,
},
"& .MuiList-padding": {
padding: 0,
},
});
const StyledMenuItem = styled(MenuItem)({
padding: 8,
borderRadius: 4,
});
const ItemColor = styled("div")`
width: 12px;
height: 12px;
border-radius: 4px;
margin-right: 8px;
`;
const ItemName = styled("div")`
font-size: 12px;
color: #333;
`;
const StyledDialogTitle = styled("div")`
display: flex;
align-items: center;
@@ -150,4 +246,4 @@ const StyledButton = styled("div")`
}
`;
export default SheetRenameDialog;
export default SheetListMenu;

View File

@@ -6,11 +6,11 @@ import { theme } from "../../theme";
import { NAVIGATION_HEIGHT } from "../constants";
import { StyledButton } from "../toolbar";
import type { WorkbookState } from "../workbookState";
import SheetListMenu from "./SheetListMenu";
import SheetTab from "./SheetTab";
import SheetListMenu from "./menus";
import Sheet from "./sheet";
import type { SheetOptions } from "./types";
export interface SheetTabBarProps {
export interface NavigationProps {
sheets: SheetOptions[];
selectedIndex: number;
workbookState: WorkbookState;
@@ -21,7 +21,7 @@ export interface SheetTabBarProps {
onSheetDeleted: () => void;
}
function SheetTabBar(props: SheetTabBarProps) {
function Navigation(props: NavigationProps) {
const { t } = useTranslation();
const { workbookState, onSheetSelected, sheets, selectedIndex } = props;
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
@@ -35,27 +35,24 @@ function SheetTabBar(props: SheetTabBarProps) {
return (
<Container>
<LeftButtonsContainer>
<StyledButton
title={t("navigation.add_sheet")}
$pressed={false}
onClick={props.onAddBlankSheet}
>
<Plus />
</StyledButton>
<StyledButton
onClick={handleClick}
title={t("navigation.sheet_list")}
$pressed={false}
>
<Menu />
</StyledButton>
</LeftButtonsContainer>
<VerticalDivider />
<StyledButton
title={t("navigation.add_sheet")}
$pressed={false}
onClick={props.onAddBlankSheet}
>
<Plus />
</StyledButton>
<StyledButton
onClick={handleClick}
title={t("navigation.sheet_list")}
$pressed={false}
>
<Menu />
</StyledButton>
<Sheets>
<SheetInner>
{sheets.map((tab, index) => (
<SheetTab
<Sheet
key={tab.sheetId}
name={tab.name}
color={tab.color}
@@ -80,8 +77,8 @@ function SheetTabBar(props: SheetTabBarProps) {
</Advert>
<SheetListMenu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
isOpen={open}
close={handleClose}
sheetOptionsList={sheets}
onSheetSelected={(index) => {
onSheetSelected(index);
@@ -95,8 +92,6 @@ function SheetTabBar(props: SheetTabBarProps) {
// Note I have to specify the font-family in every component that can be considered stand-alone
const Container = styled("div")`
display: flex;
flex-direction: row;
position: absolute;
bottom: 0px;
left: 0px;
@@ -104,10 +99,10 @@ const Container = styled("div")`
display: flex;
height: ${NAVIGATION_HEIGHT}px;
align-items: center;
padding: 0px 12px;
padding-left: 12px;
font-family: Inter;
background-color: ${theme.palette.common.white};
border-top: 1px solid ${theme.palette.grey["300"]};
background-color: #fff;
border-top: 1px solid #e0e0e0;
`;
const Sheets = styled("div")`
@@ -115,9 +110,6 @@ const Sheets = styled("div")`
overflow: hidden;
overflow-x: auto;
scrollbar-width: none;
padding-left: 12px;
display: flex;
flex-direction: row;
`;
const SheetInner = styled("div")`
@@ -127,8 +119,8 @@ const SheetInner = styled("div")`
const Advert = styled("a")`
display: flex;
align-items: center;
color: ${theme.palette.primary.main};
padding: 0px 0px 0px 12px;
color: #f2994a;
padding: 0px 12px;
font-size: 12px;
text-decoration: none;
border-left: 1px solid ${theme.palette.grey["300"]};
@@ -141,19 +133,4 @@ const Advert = styled("a")`
}
`;
const LeftButtonsContainer = styled("div")`
display: flex;
flex-direction: row;
gap: 4px;
padding-right: 12px;
`;
const VerticalDivider = styled("div")`
height: 100%;
width: 0px;
@media (max-width: 769px) {
border-right: 1px solid ${theme.palette.grey["200"]};
}
`;
export default SheetTabBar;
export default Navigation;

View File

@@ -1,13 +1,12 @@
import { Button, Menu, MenuItem, styled } from "@mui/material";
import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react";
import { theme } from "../../theme";
import ColorPicker from "../colorPicker";
import { isInReferenceMode } from "../editor/util";
import type { WorkbookState } from "../workbookState";
import SheetRenameDialog from "./SheetRenameDialog";
import { SheetRenameDialog } from "./menus";
interface SheetTabProps {
interface SheetProps {
name: string;
color: string;
selected: boolean;
@@ -18,7 +17,7 @@ interface SheetTabProps {
workbookState: WorkbookState;
}
function SheetTab(props: SheetTabProps) {
function Sheet(props: SheetProps) {
const { name, color, selected, workbookState, onSelected } = props;
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
const [colorPickerOpen, setColorPickerOpen] = useState(false);
@@ -39,9 +38,8 @@ function SheetTab(props: SheetTabProps) {
};
return (
<>
<TabWrapper
$color={color}
$selected={selected}
<Wrapper
style={{ borderBottomColor: color, fontWeight: selected ? 600 : 400 }}
onClick={(event) => {
onSelected();
event.stopPropagation();
@@ -57,11 +55,11 @@ function SheetTab(props: SheetTabProps) {
}}
ref={colorButton}
>
<Name onDoubleClick={handleOpenRenameDialog}>{name}</Name>
<Name>{name}</Name>
<StyledButton onClick={handleOpen}>
<ChevronDown />
</StyledButton>
</TabWrapper>
</Wrapper>
<StyledMenu
anchorEl={anchorEl}
open={open}
@@ -102,8 +100,8 @@ function SheetTab(props: SheetTabProps) {
</StyledMenuItem>
</StyledMenu>
<SheetRenameDialog
open={renameDialogOpen}
onClose={handleCloseRenameDialog}
isOpen={renameDialogOpen}
close={handleCloseRenameDialog}
defaultName={name}
onNameChanged={(newName) => {
props.onRenamed(newName);
@@ -126,39 +124,10 @@ function SheetTab(props: SheetTabProps) {
);
}
const StyledMenu = styled(Menu)`
& .MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
margin-left: -4px;
}
& .MuiList-root {
padding: 0;
}
`;
const StyledMenu = styled(Menu)``;
const StyledMenuItem = styled(MenuItem)`
display: flex;
justify-content: space-between;
font-size: 12px;
width: calc(100% - 8px);
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
`;
const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
display: flex;
margin-right: 12px;
border-bottom: 3px solid ${(props) => props.$color};
line-height: 37px;
padding: 0px 4px;
align-items: center;
cursor: pointer;
font-weight: ${(props) => (props.$selected ? 600 : 400)};
background-color: ${(props) =>
props.$selected ? `${theme.palette.grey[50]}80` : "transparent"};
`;
const StyledButton = styled(Button)`
@@ -168,27 +137,26 @@ const StyledButton = styled(Button)`
padding: 0px;
color: inherit;
font-weight: inherit;
&:hover {
background-color: transparent;
}
&:active {
background-color: transparent;
}
svg {
width: 15px;
height: 15px;
transition: transform 0.2s;
}
&:hover svg {
transform: translateY(2px);
}
`;
const Wrapper = styled("div")`
display: flex;
margin-left: 20px;
border-bottom: 3px solid;
border-top: 3px solid white;
line-height: 34px;
align-items: center;
cursor: pointer;
`;
const Name = styled("div")`
font-size: 12px;
margin-right: 5px;
text-wrap: nowrap;
user-select: none;
`;
export default SheetTab;
export default Sheet;

View File

@@ -1,7 +1,6 @@
import type {
BorderOptions,
HorizontalAlignment,
Model,
VerticalAlignment,
} from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
@@ -19,11 +18,10 @@ import {
Grid2x2X,
Italic,
PaintBucket,
PaintRoller,
Paintbrush2,
Percent,
Redo2,
Strikethrough,
Tags,
Type,
Underline,
Undo2,
@@ -36,7 +34,6 @@ import {
DecimalPlacesIncreaseIcon,
} from "../icons";
import { theme } from "../theme";
import NameManagerDialog from "./NameManagerDialog";
import BorderPicker from "./borderPicker";
import ColorPicker from "./colorPicker";
import { TOOLBAR_HEIGHT } from "./constants";
@@ -63,7 +60,6 @@ type ToolbarProperties = {
onFillColorPicked: (hex: string) => void;
onNumberFormatPicked: (numberFmt: string) => void;
onBorderChanged: (border: BorderOptions) => void;
onNamedRangesUpdate: () => void;
fillColor: string;
fontColor: string;
bold: boolean;
@@ -76,14 +72,12 @@ type ToolbarProperties = {
numFmt: string;
showGridLines: boolean;
onToggleShowGridLines: (show: boolean) => void;
model: Model;
};
function Toolbar(properties: ToolbarProperties) {
const [fontColorPickerOpen, setFontColorPickerOpen] = useState(false);
const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false);
const [borderPickerOpen, setBorderPickerOpen] = useState(false);
const [nameManagerDialogOpen, setNameManagerDialogOpen] = useState(false);
const fontColorButton = useRef(null);
const fillColorButton = useRef(null);
@@ -120,7 +114,7 @@ function Toolbar(properties: ToolbarProperties) {
onClick={properties.onCopyStyles}
title={t("toolbar.copy_styles")}
>
<PaintRoller />
<Paintbrush2 />
</StyledButton>
<Divider />
<StyledButton
@@ -189,7 +183,8 @@ function Toolbar(properties: ToolbarProperties) {
title={t("toolbar.format_number")}
sx={{
width: "40px", // Keep in sync with anchorOrigin in FormatMenu above
padding: "0px 4px",
fontSize: "13px",
fontWeight: 400,
}}
>
{"123"}
@@ -346,18 +341,6 @@ function Toolbar(properties: ToolbarProperties) {
>
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
</StyledButton>
<Divider />
<StyledButton
type="button"
$pressed={false}
onClick={() => {
setNameManagerDialogOpen(true);
}}
disabled={!canEdit}
title={t("toolbar.name_manager")}
>
<Tags />
</StyledButton>
<ColorPicker
color={properties.fontColor}
@@ -393,18 +376,6 @@ function Toolbar(properties: ToolbarProperties) {
anchorEl={borderButton}
open={borderPickerOpen}
/>
<NameManagerDialog
open={nameManagerDialogOpen}
onClose={() => {
setNameManagerDialogOpen(false);
}}
onSave={() => {
console.log(
"update NamedRanges in model => properties.onNamedRangesUpdate",
);
}}
model={properties.model}
/>
</ToolbarContainer>
);
}
@@ -420,9 +391,7 @@ const ToolbarContainer = styled("div")`
font-family: Inter;
border-radius: 4px 4px 0px 0px;
overflow-x: auto;
padding: 0px 12px;
gap: 4px;
scrollbar-width: none;
padding-left: 11px;
`;
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
@@ -430,16 +399,15 @@ export const StyledButton = styled("button")<TypeButtonProperties>(
({ disabled, $pressed, $underlinedColor }) => {
const result = {
width: "24px",
minWidth: "24px",
height: "24px",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontSize: "12px",
border: `0px solid ${theme.palette.common.white}`,
borderRadius: "4px",
fontSize: "26px",
border: "0px solid #fff",
borderRadius: "2px",
marginRight: "5px",
transition: "all 0.2s",
outline: `1px solid ${theme.palette.common.white}`,
cursor: "pointer",
backgroundColor: "white",
padding: "0px",
@@ -451,28 +419,19 @@ export const StyledButton = styled("button")<TypeButtonProperties>(
if (disabled) {
return {
...result,
color: theme.palette.grey["400"],
color: theme.palette.grey["600"],
cursor: "default",
};
}
return {
...result,
borderTop: $underlinedColor
? `3px solid ${theme.palette.common.white}`
: "none",
borderTop: $underlinedColor ? "3px solid #FFF" : "none",
borderBottom: $underlinedColor ? `3px solid ${$underlinedColor}` : "none",
color: theme.palette.grey["900"],
backgroundColor: $pressed
? theme.palette.grey["300"]
: theme.palette.common.white,
color: "#21243A",
backgroundColor: $pressed ? "#EEE" : "#FFF",
"&:hover": {
transition: "all 0.2s",
outline: `1px solid ${theme.palette.grey["200"]}`,
borderTopColor: theme.palette.common.white,
},
"&:active": {
backgroundColor: theme.palette.grey["300"],
outline: `1px solid ${theme.palette.grey["300"]}`,
backgroundColor: "#F1F2F8",
borderTopColor: "#F1F2F8",
},
};
},
@@ -480,9 +439,10 @@ export const StyledButton = styled("button")<TypeButtonProperties>(
const Divider = styled("div")({
width: "0px",
height: "12px",
borderLeft: `1px solid ${theme.palette.grey["300"]}`,
margin: "0px 12px",
height: "10px",
borderLeft: "1px solid #E0E0E0",
marginLeft: "5px",
marginRight: "10px",
});
export default Toolbar;

View File

@@ -6,7 +6,6 @@ import type {
} from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useCallback, useEffect, useRef, useState } from "react";
import SheetTabBar from "./SheetTabBar/SheetTabBar";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
@@ -17,6 +16,7 @@ import {
getNewClipboardId,
} from "./clipboard";
import FormulaBar from "./formulabar";
import Navigation from "./navigation/navigation";
import Toolbar from "./toolbar";
import useKeyboardNavigation from "./useKeyboardNavigation";
import { type NavigationKey, getCellAddress } from "./util";
@@ -113,10 +113,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("num_fmt", numberFmt);
};
const onNamedRangesUpdate = () => {
// update named ranges in model
}
const onCopyStyles = () => {
const {
sheet,
@@ -394,12 +390,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}
data.set(Number.parseInt(row, 10), rowMap);
}
model.pasteFromClipboard(
source.sheet,
source.area,
data,
source.type === "cut",
);
model.pasteFromClipboard(source.area, data, source.type === "cut");
setRedrawId((id) => id + 1);
} else if (mimeType === "text/plain") {
const {
@@ -425,7 +416,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
onCopy={(event: React.ClipboardEvent) => {
const data = model.copyToClipboard();
const sheet = model.getSelectedSheet();
// '2024-10-18T14:07:37.599Z'
let clipboardId = sessionStorage.getItem(
@@ -453,7 +443,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
type: "copy",
area: data.range,
sheetData,
sheet,
clipboardId,
});
event.clipboardData.setData("text/plain", data.csv);
@@ -463,7 +452,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
onCut={(event: React.ClipboardEvent) => {
const data = model.copyToClipboard();
const sheet = model.getSelectedSheet();
// '2024-10-18T14:07:37.599Z'
let clipboardId = sessionStorage.getItem(
@@ -491,7 +479,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
type: "cut",
area: data.range,
sheetData,
sheet,
clipboardId,
});
event.clipboardData.setData("text/plain", data.csv);
@@ -563,8 +550,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.setShowGridLines(sheet, show);
setRedrawId((id) => id + 1);
}}
onNamedRangesUpdate={onNamedRangesUpdate}
model={model}
/>
<FormulaBar
cellAddress={cellAddress()}
@@ -587,7 +572,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
/>
<SheetTabBar
<Navigation
sheets={info}
selectedIndex={model.getSelectedSheet()}
workbookState={workbookState}

View File

@@ -1,3 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8H2M14 8H13M7 8H5M11 8H9M14 4H2M2.01 12H2M4.01 12H4M6.01 12H6M8.01 12H8M10.01 12H10M12.01 12H12M14.01 12H14" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="0" y1="2" x2="16" y2="2" stroke="#000"/>
<!-- Dashes and gaps of the same size -->
<line x1="0" y1="8" x2="16" y2="8" stroke-dasharray="2.28 2.28" stroke="#000"/>
<!-- Dashes and gaps of different sizes -->
<line x1="0" y1="14" x2="16" y2="14" stroke-dasharray="1 2" stroke="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -1,81 +1,77 @@
{
"toolbar": {
"redo": "Redo",
"undo": "Undo",
"copy_styles": "Copy styles",
"euro": "Format as Euro",
"percentage": "Format as Percentage",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike_through": "Strikethrough",
"align_left": "Align left",
"align_right": "Align right",
"align_center": "Align center",
"format_number": "Format number",
"font_color": "Font color",
"fill_color": "Fill color",
"decimal_places_increase": "Increase decimal places",
"decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines",
"name_manager": "Name manager",
"vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle",
"vertical_align_top": "Align top",
"format_menu": {
"auto": "Auto",
"number": "Number",
"percentage": "Percentage",
"currency_eur": "Euro (EUR)",
"currency_usd": "Dollar (USD)",
"currency_gbp": "British Pound (GBD)",
"date_short": "Short date",
"date_long": "Long date",
"custom": "Custom",
"number_example": "1,000.00",
"percentage_example": "10%",
"currency_eur_example": "",
"currency_usd_example": "$",
"currency_gbp_example": "£",
"date_short_example": "09/24/2024",
"date_long_example": "Tuesday, September 24, 2024"
},
"borders": {
"title": "Borders",
"all": "All borders",
"inner": "Inner borders",
"outer": "Outer borders",
"top": "Top borders",
"bottom": "Bottom borders",
"clear": "Clear borders",
"left": "Left borders",
"right": "Right borders",
"horizontal": "Horizontal borders",
"vertical": "Vertical borders",
"color": "Border color",
"style": "Border style"
}
},
"num_fmt": {
"title": "Custom number format",
"label": "Number format",
"save": "Save"
},
"sheet_rename": {
"rename": "Save",
"label": "New name",
"title": "Rename Sheet"
},
"formula_input": {
"update": "Update",
"label": "Formula",
"title": "Update formula"
},
"navigation": {
"add_sheet": "Add sheet",
"sheet_list": "Sheet list"
},
"name_manager_dialog": {
"help": "Learn more about Named Ranges"
}
"toolbar": {
"redo": "Redo",
"undo": "Undo",
"copy_styles": "Copy styles",
"euro": "Format as Euro",
"percentage": "Format as Percentage",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike_through": "Strikethrough",
"align_left": "Align left",
"align_right": "Align right",
"align_center": "Align center",
"format_number": "Format number",
"font_color": "Font color",
"fill_color": "Fill color",
"decimal_places_increase": "Increase decimal places",
"decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines",
"vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle",
"vertical_align_top": "Align top",
"format_menu": {
"auto": "Auto",
"number": "Number",
"percentage": "Percentage",
"currency_eur": "Euro (EUR)",
"currency_usd": "Dollar (USD)",
"currency_gbp": "British Pound (GBD)",
"date_short": "Short date",
"date_long": "Long date",
"custom": "Custom",
"number_example": "1,000.00",
"percentage_example": "10%",
"currency_eur_example": "",
"currency_usd_example": "$",
"currency_gbp_example": "£",
"date_short_example": "09/24/2024",
"date_long_example": "Tuesday, September 24, 2024"
},
"borders": {
"title": "Borders",
"all": "All borders",
"inner": "Inner borders",
"outer": "Outer borders",
"top": "Top borders",
"bottom": "Bottom borders",
"clear": "Clear borders",
"left": "Left borders",
"right": "Right borders",
"horizontal": "Horizontal borders",
"vertical": "Vertical borders",
"color": "Border color",
"style": "Border style"
}
},
"num_fmt": {
"title": "Custom number format",
"label": "Number format",
"save": "Save"
},
"sheet_rename": {
"rename": "Save",
"label": "New name",
"title": "Rename Sheet"
},
"formula_input": {
"update": "Update",
"label": "Formula",
"title": "Update formula"
},
"navigation": {
"add_sheet": "Add sheet",
"sheet_list": "Sheet list"
}
}