Compare commits

...

37 Commits

Author SHA1 Message Date
Nicolás Hatcher
443ff6808d UPDATE: WIP 2025-10-12 17:01:52 +02:00
Nicolás Hatcher
ed64716f0f FIX: Refactor both dialogs to get common code 2025-10-12 14:23:12 +02:00
Nicolás Hatcher
dd29287c5a FIX: Lifts TemplateDialog to App.tsx 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
7841abe2d2 update: use a different dialog for templates only 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
49c3d1e03a chore: remove optional props 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b709041f9d chore: move new ic icon to icons folder 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b177a33815 fix: remove obsolete css prefixes 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
b506ccf908 update: rename function 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
eb3e92ffd8 update: make dialogs content change depending on scenario 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
0b925a4d6a update: add a second New from template action to FileMenu 2025-10-12 14:23:12 +02:00
Nicolás Hatcher
6a3e37f4c1 FIX: Integration of Welcome dialog 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
2496227344 fix: lint 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
72355a5201 update: add a compontent for the list items 2025-10-12 14:23:12 +02:00
Daniel Gonzalez Albo
81901ec717 update: add a welcome dialog to the app 2025-10-12 14:23:12 +02:00
Dmitry S
aa664a95a1 Fix example in README to reference Model correctly.
Since https://github.com/ironcalc/IronCalc/pull/27, using
`base::model::Model` complains about `model` being a private modulde,
and `base::Model` should be used instead.
2025-10-08 08:56:03 +02:00
Daniel Gonzalez Albo
c1aa743763 fix: typo in format presets 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
6321030ac8 style: add offset to arrow's tooltips 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
c2c5751ee3 style: add nicer tooltips 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
6c27ae1355 update: show scroll arrows on narrow displays 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
7bcd978998 style: tiny adjustment in dividers 2025-10-07 20:19:57 +02:00
Daniel Gonzalez Albo
3f083d9882 adjust spacing and group button groups 2025-10-07 20:19:57 +02:00
Nicolás Hatcher
8844b80c51 FIX: cargo fmt issue 2025-09-28 17:33:17 +02:00
Nicolás Hatcher
0f8f345aae UPDATE: have xlsx_2_icalc specify its output name
This is nice for deployments
2025-09-28 17:33:17 +02:00
Nicolás Hatcher
3191e12b93 FIX: Make clippy happy 2025-09-28 17:33:17 +02:00
Nicolás Hatcher
61cecb7af5 FIX: Fixes case with unicode characters
This is an ugly bug in ugly code. Pretty much technical deb in here
2025-09-28 12:46:16 +02:00
Nicolás Hatcher
fdeae2c771 UPDATE: Add templates 2025-09-28 12:46:16 +02:00
Matt Lehrer
3e9c69f122 add model.evaluate() call 2025-09-28 12:03:28 +02:00
Tom
c1c43143cc Adds information about references and corrected syntax on Column
This is a test commit
2025-09-25 19:25:32 +02:00
Tom
763b43a590 UPDATE: Added documentation for the Column Function 2025-09-25 19:25:32 +02:00
Daniel González-Albo
8dbfe07392 Merge pull request #443 from elsaminsut/mathfunctions
docs: adds ATAN2, ASINH, ACOSH, ATANH documentation pages
2025-09-24 20:26:53 +02:00
Elsa Minsut
e39bfe912a docs: improve consistency in ATAN2, ASINH, ACOSH, ATANH documentation 2025-09-24 19:36:30 +02:00
Elsa Minsut
9bbf94e033 update: Math and Trigonometry main page links 2025-09-24 19:19:16 +02:00
Elsa Minsut
0194912845 update: adds ATANH documentation page 2025-09-24 18:05:18 +02:00
Elsa Minsut
1d4d84bb57 update: adds ASINH documentation page 2025-09-24 17:46:56 +02:00
Elsa Minsut
e841c17aca update: adds ACOSH documentation page 2025-09-24 17:19:37 +02:00
Elsa Minsut
f2c43f2070 update: adds ATAN2 documentation page 2025-09-24 17:06:20 +02:00
Nicolás Hatcher
32b1f8ef4e FIX: Update documentation of some documented functions 2025-09-23 18:01:17 +02:00
41 changed files with 1485 additions and 535 deletions

View File

@@ -84,7 +84,7 @@ And then use this code in `main.rs`:
```rust ```rust
use ironcalc::{ use ironcalc::{
base::{expressions::utils::number_to_column, model::Model}, base::{expressions::utils::number_to_column, Model},
export::save_to_xlsx, export::save_to_xlsx,
}; };

View File

@@ -717,7 +717,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: format!("sheet not found: {}", context.sheet),
}; };
} }
}; };
@@ -850,7 +850,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: format!("sheet not found: {}", context.sheet),
}; };
} }
}; };
@@ -878,7 +878,7 @@ impl Parser {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: format!("table sheet not found: {}", table.sheet_name),
}; };
} }
}; };

View File

@@ -246,7 +246,7 @@ impl Model {
} }
// None of the cases matched so we return the default // None of the cases matched so we return the default
// If there is an even number of args is the last one otherwise is #N/A // If there is an even number of args is the last one otherwise is #N/A
if args_count % 2 == 0 { if args_count.is_multiple_of(2) {
return self.evaluate_node_in_context(&args[args_count - 1], cell); return self.evaluate_node_in_context(&args[args_count - 1], cell);
} }
CalcResult::Error { CalcResult::Error {
@@ -262,7 +262,7 @@ impl Model {
if args_count < 2 { if args_count < 2 {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
if args_count % 2 != 0 { if !args_count.is_multiple_of(2) {
// Missing value for last condition // Missing value for last condition
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }

View File

@@ -350,7 +350,7 @@ impl Model {
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them? // FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len(); let args_count = args.len();
if args_count < 2 || args_count % 2 == 1 { if args_count < 2 || !args_count.is_multiple_of(2) {
return CalcResult::new_args_number_error(cell); return CalcResult::new_args_number_error(cell);
} }
@@ -476,7 +476,7 @@ impl Model {
F: FnMut(f64), F: FnMut(f64),
{ {
let args_count = args.len(); let args_count = args.len();
if args_count < 3 || args_count % 2 == 0 { if args_count < 3 || args_count.is_multiple_of(2) {
return Err(CalcResult::new_args_number_error(cell)); return Err(CalcResult::new_args_number_error(cell));
} }
let arg_0 = self.evaluate_node_in_context(&args[0], cell); let arg_0 = self.evaluate_node_in_context(&args[0], cell);

View File

@@ -4,14 +4,19 @@
Example usage: Example usage:
```javascript ```javascript
import { Model } from '@ironcalc/wasm'; import { Model } from '@ironcalc/nodejs';
const model = new Model("Workbook1", "en", "UTC"); const model = new Model("Workbook1", "en", "UTC");
model.setUserInput(0, 1, 1, "=1+1"); model.setUserInput(0, 1, 1, "=1+1");
const result1 = model.getFormattedCellValue(0, 1, 1);
console.log('Cell value', result1); const result1 = model.getFormattedCellValue(0, 1, 1);
console.log('Cell value', result1); // "#ERROR"
model.evaluate();
const resultAfterEvaluate = model.getFormattedCellValue(0, 1, 1);
console.log('Cell value', resultAfterEvaluate); // 2
let result2 = model.getCellStyle(0, 1, 1); let result2 = model.getCellStyle(0, 1, 1);
console.log('Cell style', result2); console.log('Cell style', result2);

View File

@@ -67,3 +67,7 @@ Using IronCalc, a complex number is a string of the form "1+j3".
"#N/A" => #N/A "#N/A" => #N/A
## Arrays ## Arrays
## References
A reference is a pointer to a single cell or a range of cells. The reference can either be entered manually, for example "A4", or as the result of a calculation, such as the OFFSET Function or the INDIRECT Function. A reference can also be built, for example with the Colon (\:) Operator.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -16,7 +16,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| CHOOSE | <Badge type="tip" text="Available" /> | | | CHOOSE | <Badge type="tip" text="Available" /> | |
| CHOOSECOLS | <Badge type="info" text="Not implemented yet" /> | | | CHOOSECOLS | <Badge type="info" text="Not implemented yet" /> | |
| CHOOSEROWS | <Badge type="info" text="Not implemented yet" /> | | | CHOOSEROWS | <Badge type="info" text="Not implemented yet" /> | |
| COLUMN | <Badge type="tip" text="Available" /> | | | COLUMN | <Badge type="tip" text="Available" /> | [COLUMN](lookup_and_reference/column) |
| COLUMNS | <Badge type="tip" text="Available" /> | | | COLUMNS | <Badge type="tip" text="Available" /> | |
| DROP | <Badge type="info" text="Not implemented yet" /> | | | DROP | <Badge type="info" text="Not implemented yet" /> | |
| EXPAND | <Badge type="info" text="Not implemented yet" /> | | | EXPAND | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -4,8 +4,28 @@ outline: deep
lang: en-US lang: en-US
--- ---
# COLUMN # COLUMN function
## Overview
::: warning The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced Column or Cell.
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). ## Usage
::: ### Syntax
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
### Argument descriptions
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
### Additional guidance
* When referencing a range of cells, only the column number of the left most cell will be returned.
* You are also able to reference complete columns instead of individual cells.
### Returned value
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
### Error conditions
* IronCalc currently does not support the referencing of cells with names.
## Details
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
## Examples
### No Cell Reference
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
### With Cell Reference
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
### Range References
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
## Links

View File

@@ -13,16 +13,16 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| --------------- | ---------------------------------------------- | ------------- | | --------------- | ---------------------------------------------- | ------------- |
| ABS | <Badge type="tip" text="Available" /> | | | ABS | <Badge type="tip" text="Available" /> | |
| ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) | | ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) |
| ACOSH | <Badge type="tip" text="Available" /> | | | ACOSH | <Badge type="tip" text="Available" /> | [ACOSH](math_and_trigonometry/acosh) |
| ACOT | <Badge type="info" text="Not implemented yet" /> | | | ACOT | <Badge type="info" text="Not implemented yet" /> | |
| ACOTH | <Badge type="info" text="Not implemented yet" /> | | | ACOTH | <Badge type="info" text="Not implemented yet" /> | |
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | | | AGGREGATE | <Badge type="info" text="Not implemented yet" /> | |
| ARABIC | <Badge type="info" text="Not implemented yet" /> | | | ARABIC | <Badge type="info" text="Not implemented yet" /> | |
| ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) | | ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) |
| ASINH | <Badge type="tip" text="Available" /> | | | ASINH | <Badge type="tip" text="Available" /> | [ASINH](math_and_trigonometry/asinh) |
| ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) | | ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) |
| ATAN2 | <Badge type="tip" text="Available" /> | | | ATAN2 | <Badge type="tip" text="Available" /> | [ATAN2](math_and_trigonometry/atan2) |
| ATANH | <Badge type="tip" text="Available" /> | | | ATANH | <Badge type="tip" text="Available" /> | [ATANH](math_and_trigonometry/atanh) |
| BASE | <Badge type="info" text="Not implemented yet" /> | | | BASE | <Badge type="info" text="Not implemented yet" /> | |
| CEILING | <Badge type="info" text="Not implemented yet" /> | | | CEILING | <Badge type="info" text="Not implemented yet" /> | |
| CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | | | CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | |
@@ -49,9 +49,9 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | | | ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | |
| LCM | <Badge type="info" text="Not implemented yet" /> | | | LCM | <Badge type="info" text="Not implemented yet" /> | |
| LET | <Badge type="info" text="Not implemented yet" /> | | | LET | <Badge type="info" text="Not implemented yet" /> | |
| LN | <Badge type="info" text="Not implemented yet" /> | | | LN | <Badge type="info" text="Available" /> | |
| LOG | <Badge type="info" text="Not implemented yet" /> | | | LOG | <Badge type="info" text="Available" /> | |
| LOG10 | <Badge type="info" text="Not implemented yet" /> | | | LOG10 | <Badge type="info" text="Available" /> | |
| MDETERM | <Badge type="info" text="Not implemented yet" /> | | | MDETERM | <Badge type="info" text="Not implemented yet" /> | |
| MINVERSE | <Badge type="info" text="Not implemented yet" /> | | | MINVERSE | <Badge type="info" text="Not implemented yet" /> | |
| MMULT | <Badge type="info" text="Not implemented yet" /> | | | MMULT | <Badge type="info" text="Not implemented yet" /> | |
@@ -80,11 +80,11 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) | | SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) | | SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
| SQRT | <Badge type="tip" text="Available" /> | | | SQRT | <Badge type="tip" text="Available" /> | |
| SQRTPI | <Badge type="info" text="Not implemented yet" /> | | | SQRTPI | <Badge type="info" text="Available" /> | |
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | | | SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | |
| SUM | <Badge type="tip" text="Available" /> | | | SUM | <Badge type="tip" text="Available" /> | |
| SUMIF | <Badge type="tip" text="Available" /> | | | SUMIF | <Badge type="tip" text="Available" /> | |
| SUMIFS | <Badge type="info" text="Not implemented yet" /> | | | SUMIFS | <Badge type="info" text="Available" /> | |
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | | | SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | |
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | | | SUMSQ | <Badge type="info" text="Not implemented yet" /> | |
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | | | SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -3,9 +3,40 @@ layout: doc
outline: deep outline: deep
lang: en-US lang: en-US
--- ---
# ACOSH function
## Overview
ACOSH is a function of the Math and Trigonometry category that calculates the inverse hyperbolic cosine (hyperbolic arccosine) of a number, returning a non-negative value in the range [0, +∞).
## Usage
### Syntax
**ACOSH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">acosh</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The value whose hyperbolic arccosine is to be calculated. The value must be greater than or equal to 1.
# ACOSH ### Additional guidance
The hyperbolic arccosine function is defined as:
::: warning $$
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). \operatorname{acosh}(x) = \ln(x + \sqrt{x^2 - 1})
::: $$
### Returned value
ACOSH returns a [number](/features/value-types#numbers) in the range [0, +∞) that is the hyperbolic arccosine of the specified value, expressed in radians.
### Error conditions
* In common with many other IronCalc functions, ACOSH propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ACOSH returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ACOSH returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the value of the *number* argument is less than 1, then ACOSH returns the [`#NUM!`](/features/error-types.md#num) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ACOSH function utilizes the *acosh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
* The figure below illustrates the output of the ACOSH function for values $x \geq 1$ in the range [0, +∞).
<center><img src="/functions/images/hyperbolicarccosine-curve.png" width="350" alt="Graph showing acosh(x) for x ≥ 1."></center>
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=acosh).
## Links
* For more information about inverse hyperbolic functions, visit Wikipedia's [Inverse hyperbolic functions](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) page.
* See also IronCalc's [COSH](/functions/math_and_trigonometry/cosh), [ASINH](/functions/math_and_trigonometry/asinh) and [ATANH](/functions/math_and_trigonometry/atanh) functions.
* Visit Microsoft Excel's [ACOSH function](https://support.microsoft.com/en-us/office/acosh-function-e3992cc1-103f-4e72-9f04-624b9ef5ebfe) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093391) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ACOSH) provide versions of the ACOSH function.

View File

@@ -4,8 +4,36 @@ outline: deep
lang: en-US lang: en-US
--- ---
# ASINH # ASINH function
## Overview
ASINH is a function of the Math and Trigonometry category that calculates the inverse hyperbolic sine (hyperbolic arcsine) of a number, returning the hyperbolic angle expressed in radians.
## Usage
### Syntax
**ASINH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">asinh</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The value whose inverse hyperbolic sine is to be calculated.
### Additional guidance
The hyperbolic arcsine function is defined as:
$$
\operatorname{asinh}(x) = \ln\!\left(x + \sqrt{x^2 + 1}\,\right)
$$
### Returned value
ASINH returns a real [number](/features/value-types#numbers) in the range (-∞, +∞) that is the hyperbolic arcsine of the specified value, expressed in radians.
### Error conditions
* In common with many other IronCalc functions, ASINH propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ASINH returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ASINH returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ASINH function utilizes the *asinh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
* The figure below illustrates the output of the ASINH function.
<center><img src="/functions/images/hyperbolicarcsine-curve.png" width="350" alt="Graph showing asinh(x)."></center>
::: warning ## Examples
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [See some examples in IronCalc](https://app.ironcalc.com/?example=asinh).
:::
## Links
* For more information about inverse hyperbolic functions, visit Wikipedia's [Inverse hyperbolic functions](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) page.
* See also IronCalc's [SINH](/functions/math_and_trigonometry/sinh), [ACOSH](/functions/math_and_trigonometry/acosh) and [ATANH](/functions/math_and_trigonometry/atanh) functions.
* Visit Microsoft Excel's [ASINH function](https://support.microsoft.com/de-de/office/asinh-function-62b4f5b6-d9cc-4c17-9d04-aa5371806c74) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093393) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ASINH) provide versions of the ASINH function.

View File

@@ -4,8 +4,34 @@ outline: deep
lang: en-US lang: en-US
--- ---
# ATAN2 # ATAN2 function
## Overview
ATAN2 is a function of the Math and Trigonometry category that calculates the inverse tangent (arctangent) for the specified *x* and *y* coordinates. The arctangent returns the angle defined by the x-axis and a line defined by the origin and a point with coordinates (x,y). The returned angle is expressed in radians, in the range (-$\pi$, +$\pi$].
## Usage
### Syntax
**ATAN2(<span title="Number" style="color:#1E88E5">x,y</span>) => <span title="Number" style="color:#1E88E5">atan2</span>**
### Argument descriptions
* *x* ([number](/features/value-types#numbers), required). Value of the x coordinate.
* *y* ([number](/features/value-types#numbers), required). Value of the y coordinate.
### Additional guidance
If the returned value is positive, it represents a counterclockwise angle from the x-axis, while a negative value represents a clockwise angle.
ATAN2(x,y) is equivalent to ATAN(y/x), with the difference that the x argument in ATAN2 can be 0.
### Returned value
ATAN2 returns a number in radians in the range (-$\pi$, +$\pi$] that is the inverse tangent for the specified x and y coordinates.
### Error conditions
* In common with many other IronCalc functions, ATAN2 propagates errors that are found in its argument.
* If no argument, or arguments other than 2, are supplied, then ATAN2 returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of either the *x* or *y* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ATAN2 returns the [`#VALUE!`](/features/error-types.md#value) error.
* If both *x* and *y* are equal to 0, ATAN2 returns a [`#DIV/0!`](/features/error-types.md#div-0) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ATAN2 function utilizes the *atan2()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=atan2).
## Links
* For more information about inverse trigonometric functions, visit Wikipedia's [Inverse trigonometric functions](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) page.
* See also IronCalc's [ATAN](/functions/math_and_trigonometry/atan), [TAN](/functions/math_and_trigonometry/tan) and [ASIN](/functions/math_and_trigonometry/asin) functions.
* Visit Microsoft Excel's [ATAN2 function](https://support.microsoft.com/en-us/office/atan2-function-51123ced-348c-416a-b2e2-833f7868569f) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093468) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ATAN2) provide versions of the ATAN2 function.
::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::

View File

@@ -4,8 +4,37 @@ outline: deep
lang: en-US lang: en-US
--- ---
# ATANH # ATANH function
## Overview
ATANH is a function of the Math and Trigonometry category that calculates the inverse hyperbolic tangent (hyperbolic arctangent) of a number in the range (-1, +1), returning the hyperbolic angle expressed in radians.
## Usage
### Syntax
**ATANH(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">atanh</span>**
### Argument descriptions
* *number* ([number](/features/value-types#numbers), required). The value whose inverse hyperbolic tangent is to be calculated, in the range (-1,+1).
### Additional guidance
The hyperbolic arctangent function is defined as:
$$
\operatorname{atanh}(x) = \tfrac{1}{2}\,\ln\!\left(\dfrac{1+x}{1-x}\right),\quad |x| < 1
$$
### Returned value
ATANH returns a real [number](/features/value-types#numbers) in the range (-∞, +∞) that is the hyperbolic arctangent of the specified value, expressed in radians.
### Error conditions
* In common with many other IronCalc functions, ATANH propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then ATANH returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ATANH returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the value of the *number* argument lies outside the domain (-1, +1), then ATANH returns the [`#NUM!`](/features/error-types.md#num) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* The ATANH function utilizes the *atanh()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
* The figure below illustrates the output of the ATANH function.
<center><img src="/functions/images/hyperbolicarctangent-curve.png" width="350" alt="Graph showing atanh(x)."></center>
::: warning ## Examples
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [See some examples in IronCalc](https://app.ironcalc.com/?example=atanh).
:::
## Links
* For more information about inverse hyperbolic functions, visit Wikipedia's [Inverse hyperbolic functions](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) page.
* See also IronCalc's [ASINH](/functions/math_and_trigonometry/asinh), [ACOSH](/functions/math_and_trigonometry/acosh) and [TANH](/functions/math_and_trigonometry/tanh) functions.
* Visit Microsoft Excel's [ATANH function](https://support.microsoft.com/de-de/office/atanh-function-453534d1-76a5-4f17-8c04-c3f2feee0dd5) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093397) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ATANH) provide versions of the ATANH function.

View File

@@ -7,6 +7,5 @@ lang: en-US
# LN # LN
::: warning ::: warning
🚧 This function is not yet available in IronCalc. 🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,6 +7,5 @@ lang: en-US
# LOG # LOG
::: warning ::: warning
🚧 This function is not yet available in IronCalc. 🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,6 +7,5 @@ lang: en-US
# LOG10 # LOG10
::: warning ::: warning
🚧 This function is not yet available in IronCalc. 🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -99,10 +99,9 @@ const FormulaSymbolButton = styled(StyledButton)`
const Divider = styled("div")` const Divider = styled("div")`
background-color: ${theme.palette.grey["300"]}; background-color: ${theme.palette.grey["300"]};
width: 1px; min-width: 1px;
height: 20px; height: 16px;
margin-left: 16px; margin: 0px 16px;
margin-right: 16px;
`; `;
const FormulaContainer = styled("div")` const FormulaContainer = styled("div")`

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ import {
CLIPBOARD_ID_SESSION_STORAGE_KEY, CLIPBOARD_ID_SESSION_STORAGE_KEY,
getNewClipboardId, getNewClipboardId,
} from "../clipboard"; } from "../clipboard";
import { TOOLBAR_HEIGHT } from "../constants";
import { import {
type NavigationKey, type NavigationKey,
getCellAddress, getCellAddress,
@@ -41,6 +42,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
// This is needed because `model` or `workbookState` can change without React being aware of it // This is needed because `model` or `workbookState` can change without React being aware of it
const setRedrawId = useState(0)[1]; const setRedrawId = useState(0)[1];
const [isDrawerOpen, setDrawerOpen] = useState(false);
const worksheets = model.getWorksheetsProperties(); const worksheets = model.getWorksheetsProperties();
const info = worksheets.map( const info = worksheets.map(
({ name, color, sheet_id, state }: WorksheetProperties) => { ({ name, color, sheet_id, state }: WorksheetProperties) => {
@@ -692,77 +695,119 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
worksheets, worksheets,
definedNameList: model.getDefinedNameList(), definedNameList: model.getDefinedNameList(),
}} }}
/> openDrawer={() => {
<FormulaBar setDrawerOpen(true);
cellAddress={cellAddress()}
formulaValue={formulaValue()}
onChange={() => {
setRedrawId((id) => id + 1);
focusWorkbook();
}} }}
onTextUpdated={() => {
setRedrawId((id) => id + 1);
}}
model={model}
workbookState={workbookState}
/>
<Worksheet
model={model}
workbookState={workbookState}
refresh={(): void => {
setRedrawId((id) => id + 1);
}}
ref={worksheetRef}
/> />
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? DRAWER_WIDTH : 0}>
<FormulaBar
cellAddress={cellAddress()}
formulaValue={formulaValue()}
onChange={() => {
setRedrawId((id) => id + 1);
focusWorkbook();
}}
onTextUpdated={() => {
setRedrawId((id) => id + 1);
}}
model={model}
workbookState={workbookState}
/>
<Worksheet
model={model}
workbookState={workbookState}
refresh={(): void => {
setRedrawId((id) => id + 1);
}}
ref={worksheetRef}
/>
<SheetTabBar <SheetTabBar
sheets={info} sheets={info}
selectedIndex={model.getSelectedSheet()} selectedIndex={model.getSelectedSheet()}
workbookState={workbookState} workbookState={workbookState}
onSheetSelected={(sheet: number): void => { onSheetSelected={(sheet: number): void => {
if (info[sheet].state !== "visible") { if (info[sheet].state !== "visible") {
model.unhideSheet(sheet); model.unhideSheet(sheet);
} }
model.setSelectedSheet(sheet); model.setSelectedSheet(sheet);
setRedrawId((value) => value + 1);
}}
onAddBlankSheet={(): void => {
model.newSheet();
setRedrawId((value) => value + 1);
}}
onSheetColorChanged={(hex: string): void => {
try {
model.setSheetColor(model.getSelectedSheet(), hex);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
} catch (e) { }}
// TODO: Show a proper modal dialog onAddBlankSheet={(): void => {
alert(`${e}`); model.newSheet();
}
}}
onSheetRenamed={(name: string): void => {
try {
model.renameSheet(model.getSelectedSheet(), name);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
} catch (e) { }}
// TODO: Show a proper modal dialog onSheetColorChanged={(hex: string): void => {
alert(`${e}`); try {
} model.setSheetColor(model.getSelectedSheet(), hex);
}} setRedrawId((value) => value + 1);
onSheetDeleted={(): void => { } catch (e) {
const selectedSheet = model.getSelectedSheet(); // TODO: Show a proper modal dialog
model.deleteSheet(selectedSheet); alert(`${e}`);
setRedrawId((value) => value + 1); }
}} }}
onHideSheet={(): void => { onSheetRenamed={(name: string): void => {
const selectedSheet = model.getSelectedSheet(); try {
model.hideSheet(selectedSheet); model.renameSheet(model.getSelectedSheet(), name);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
}} } catch (e) {
/> // TODO: Show a proper modal dialog
alert(`${e}`);
}
}}
onSheetDeleted={(): void => {
const selectedSheet = model.getSelectedSheet();
model.deleteSheet(selectedSheet);
setRedrawId((value) => value + 1);
}}
onHideSheet={(): void => {
const selectedSheet = model.getSelectedSheet();
model.hideSheet(selectedSheet);
setRedrawId((value) => value + 1);
}}
/>
</WorksheetAreaLeft>
<WorksheetAreaRight $drawerWidth={isDrawerOpen ? DRAWER_WIDTH : 0}>
<span
onClick={() => setDrawerOpen(false)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setDrawerOpen(false);
}
}}
aria-label="Close drawer"
>
x
</span>
</WorksheetAreaRight>
</Container> </Container>
); );
}; };
const DRAWER_WIDTH = 300;
type WorksheetAreaLeftProps = { $drawerWidth: number };
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
({ $drawerWidth }) => ({
position: "absolute",
top: `${TOOLBAR_HEIGHT + 1}px`,
width: `calc(100% - ${$drawerWidth}px)`,
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
}),
);
const WorksheetAreaRight = styled("div")<WorksheetAreaLeftProps>(
({ $drawerWidth }) => ({
position: "absolute",
overflow: "hidden",
backgroundColor: "red",
right: 0,
top: `${TOOLBAR_HEIGHT + 1}px`,
bottom: 0,
width: `${$drawerWidth}px`,
}),
);
const Container = styled("div")` const Container = styled("div")`
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -18,11 +18,7 @@ import {
outlineColor, outlineColor,
} from "../WorksheetCanvas/constants"; } from "../WorksheetCanvas/constants";
import WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas"; import WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
import { import { FORMULA_BAR_HEIGHT, NAVIGATION_HEIGHT } from "../constants";
FORMULA_BAR_HEIGHT,
NAVIGATION_HEIGHT,
TOOLBAR_HEIGHT,
} from "../constants";
import type { Cell } from "../types"; import type { Cell } from "../types";
import type { WorkbookState } from "../workbookState"; import type { WorkbookState } from "../workbookState";
import CellContextMenu from "./CellContextMenu"; import CellContextMenu from "./CellContextMenu";
@@ -459,7 +455,7 @@ const SheetContainer = styled("div")`
const Wrapper = styled("div")({ const Wrapper = styled("div")({
position: "absolute", position: "absolute",
overflow: "scroll", overflow: "scroll",
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1, top: FORMULA_BAR_HEIGHT + 1,
left: 0, left: 0,
right: 0, right: 0,
bottom: NAVIGATION_HEIGHT + 1, bottom: NAVIGATION_HEIGHT + 1,

View File

@@ -1,3 +1,3 @@
export const TOOLBAR_HEIGHT = 48; export const TOOLBAR_HEIGHT = 40;
export const FORMULA_BAR_HEIGHT = 40; export const FORMULA_BAR_HEIGHT = 40;
export const NAVIGATION_HEIGHT = 40; export const NAVIGATION_HEIGHT = 40;

View File

@@ -18,6 +18,7 @@ import InsertRowAboveIcon from "./insert-row-above.svg?react";
import InsertRowBelow from "./insert-row-below.svg?react"; import InsertRowBelow from "./insert-row-below.svg?react";
import IronCalcIcon from "./ironcalc_icon.svg?react"; import IronCalcIcon from "./ironcalc_icon.svg?react";
import IronCalcIconWhite from "./ironcalc_icon_white.svg?react";
import IronCalcLogo from "./orange+black.svg?react"; import IronCalcLogo from "./orange+black.svg?react";
import Fx from "./fx.svg?react"; import Fx from "./fx.svg?react";
@@ -41,6 +42,7 @@ export {
InsertRowAboveIcon, InsertRowAboveIcon,
InsertRowBelow, InsertRowBelow,
IronCalcIcon, IronCalcIcon,
IronCalcIconWhite,
IronCalcLogo, IronCalcLogo,
Fx, Fx,
}; };

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.8" d="M9.95898 8.08594C9.60893 8.35318 9.27389 8.64313 8.95898 8.95801C7.09126 10.8257 6.042 13.3586 6.04199 16H6.04102V7.91406C6.39142 7.64662 6.72781 7.35715 7.04297 7.04199C8.90157 5.18307 9.9492 2.6648 9.95898 0.0371094V8.08594Z" fill="white"/>
<path opacity="0.8" d="M6.04102 7.91406C4.31493 9.23162 2.19571 9.95898 0 9.95898V6.04102C1.60208 6.04102 3.13861 5.40429 4.27148 4.27148C5.40436 3.13861 6.04101 1.60213 6.04102 0L6.04102 7.91406Z" fill="white"/>
<path opacity="0.8" d="M9.95947 8.08594C11.6856 6.76838 13.8048 6.04102 16.0005 6.04102V9.95898C14.3984 9.95898 12.8619 10.5957 11.729 11.7285C10.5961 12.8614 9.95948 14.3979 9.95947 16L9.95947 8.08594Z" fill="white"/>
<path d="M9.95898 0C9.95898 2.64126 8.90957 5.17429 7.04199 7.04199C6.727 7.35698 6.39119 7.64674 6.04102 7.91406L6.04102 0H9.95898Z" fill="white"/>
<path d="M6.04102 16C6.04102 13.3587 7.09042 10.8257 8.95801 8.95801C9.273 8.64302 9.60881 8.35326 9.95898 8.08594V16H6.04102Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,5 +1,5 @@
import init, { Model } from "@ironcalc/wasm"; import init, { Model } from "@ironcalc/wasm";
import IronCalc from "./IronCalc"; import IronCalc from "./IronCalc";
import { IronCalcIcon, IronCalcLogo } from "./icons"; import { IronCalcIcon, IronCalcIconWhite, IronCalcLogo } from "./icons";
export { init, Model, IronCalc, IronCalcIcon, IronCalcLogo }; export { init, Model, IronCalc, IronCalcIcon, IronCalcIconWhite, IronCalcLogo };

View File

@@ -27,13 +27,15 @@
"vertical_align_top": "Align top", "vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG", "selected_png": "Export Selected area as PNG",
"wrap_text": "Wrap text", "wrap_text": "Wrap text",
"scroll_left": "Scroll left",
"scroll_right": "Scroll right",
"format_menu": { "format_menu": {
"auto": "Auto", "auto": "Auto",
"number": "Number", "number": "Number",
"percentage": "Percentage", "percentage": "Percentage",
"currency_eur": "Euro (EUR)", "currency_eur": "Euro (EUR)",
"currency_usd": "Dollar (USD)", "currency_usd": "Dollar (USD)",
"currency_gbp": "British Pound (GBD)", "currency_gbp": "British Pound (GBP)",
"date_short": "Short date", "date_short": "Short date",
"date_long": "Long date", "date_long": "Long date",
"custom": "Custom", "custom": "Custom",

View File

@@ -2,6 +2,7 @@ import "./App.css";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FileBar } from "./components/FileBar"; import { FileBar } from "./components/FileBar";
import WelcomeDialog from "./components/WelcomeDialog/WelcomeDialog";
import { import {
get_documentation_model, get_documentation_model,
get_model, get_model,
@@ -10,7 +11,8 @@ import {
import { import {
createNewModel, createNewModel,
deleteSelectedModel, deleteSelectedModel,
loadModelFromStorageOrCreate, isStorageEmpty,
loadSelectedModelFromStorage,
saveModelToStorage, saveModelToStorage,
saveSelectedModelInStorage, saveSelectedModelInStorage,
selectModelFromStorage, selectModelFromStorage,
@@ -18,9 +20,13 @@ import {
// From IronCalc // From IronCalc
import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook"; import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook";
import { Modal } from "@mui/material";
import TemplatesDialog from "./components/WelcomeDialog/TemplatesDialog";
function App() { function App() {
const [model, setModel] = useState<Model | null>(null); const [model, setModel] = useState<Model | null>(null);
const [showWelcomeDialog, setShowWelcomeDialog] = useState(false);
const [isTemplatesDialogOpen, setTemplatesDialogOpen] = useState(false);
useEffect(() => { useEffect(() => {
async function start() { async function start() {
@@ -52,8 +58,14 @@ function App() {
} }
} else { } else {
// try to load from local storage // try to load from local storage
const newModel = loadModelFromStorageOrCreate(); const newModel = loadSelectedModelFromStorage();
setModel(newModel); if (!newModel) {
setShowWelcomeDialog(true);
const createdModel = new Model("template", "en", "UTC");
setModel(createdModel);
} else {
setModel(newModel);
}
} }
} }
start(); start();
@@ -93,7 +105,11 @@ function App() {
setModel(newModel); setModel(newModel);
}} }}
newModel={() => { newModel={() => {
setModel(createNewModel()); const createdModel = createNewModel();
setModel(createdModel);
}}
newModelFromTemplate={() => {
setTemplatesDialogOpen(true);
}} }}
setModel={(uuid: string) => { setModel={(uuid: string) => {
const newModel = selectModelFromStorage(uuid); const newModel = selectModelFromStorage(uuid);
@@ -109,6 +125,51 @@ function App() {
}} }}
/> />
<IronCalc model={model} /> <IronCalc model={model} />
{showWelcomeDialog && (
<WelcomeDialog
onClose={() => {
if (isStorageEmpty()) {
const createdModel = createNewModel();
setModel(createdModel);
}
setShowWelcomeDialog(false);
}}
onSelectTemplate={async (templateId) => {
switch (templateId) {
case "blank": {
const createdModel = createNewModel();
setModel(createdModel);
break;
}
default: {
const model_bytes = await get_documentation_model(templateId);
const importedModel = Model.from_bytes(model_bytes);
saveModelToStorage(importedModel);
setModel(importedModel);
break;
}
}
setShowWelcomeDialog(false);
}}
/>
)}
<Modal
open={isTemplatesDialogOpen}
onClose={() => setTemplatesDialogOpen(false)}
aria-labelledby="templates-dialog-title"
aria-describedby="templates-dialog-description"
>
<TemplatesDialog
onClose={() => setTemplatesDialogOpen(false)}
onSelectTemplate={async (fileName) => {
const model_bytes = await get_documentation_model(fileName);
const importedModel = Model.from_bytes(model_bytes);
saveModelToStorage(importedModel);
setModel(importedModel);
setTemplatesDialogOpen(false);
}}
/>
</Modal>
</Wrapper> </Wrapper>
); );
} }

View File

@@ -27,6 +27,7 @@ function useWindowWidth() {
export function FileBar(properties: { export function FileBar(properties: {
model: Model; model: Model;
newModel: () => void; newModel: () => void;
newModelFromTemplate: () => void;
setModel: (key: string) => void; setModel: (key: string) => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void; onDelete: () => void;
@@ -52,6 +53,7 @@ export function FileBar(properties: {
<Divider /> <Divider />
<FileMenu <FileMenu
newModel={properties.newModel} newModel={properties.newModel}
newModelFromTemplate={properties.newModelFromTemplate}
setModel={properties.setModel} setModel={properties.setModel}
onModelUpload={properties.onModelUpload} onModelUpload={properties.onModelUpload}
onDownload={async () => { onDownload={async () => {

View File

@@ -1,13 +1,15 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material"; import { Menu, MenuItem, Modal } from "@mui/material";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react"; import { Check, FileDown, FileUp, Plus, Table2, Trash2 } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import DeleteWorkbookDialog from "./DeleteWorkbookDialog"; import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
import UploadFileDialog from "./UploadFileDialog"; import UploadFileDialog from "./UploadFileDialog";
// import TemplatesDialog from "./WelcomeDialog/TemplatesDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage"; import { getModelsMetadata, getSelectedUuid } from "./storage";
export function FileMenu(props: { export function FileMenu(props: {
newModel: () => void; newModel: () => void;
newModelFromTemplate: () => void;
setModel: (key: string) => void; setModel: (key: string) => void;
onDownload: () => void; onDownload: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
@@ -20,7 +22,6 @@ export function FileMenu(props: {
const uuids = Object.keys(models); const uuids = Object.keys(models);
const selectedUuid = getSelectedUuid(); const selectedUuid = getSelectedUuid();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const elements = []; const elements = [];
for (const uuid of uuids) { for (const uuid of uuids) {
elements.push( elements.push(
@@ -92,7 +93,18 @@ export function FileMenu(props: {
<StyledIcon> <StyledIcon>
<Plus /> <Plus />
</StyledIcon> </StyledIcon>
<MenuItemText>New</MenuItemText> <MenuItemText>New blank workbook</MenuItemText>
</MenuItemWrapper>
<MenuItemWrapper
onClick={() => {
props.newModelFromTemplate();
setMenuOpen(false);
}}
>
<StyledIcon>
<Table2 />
</StyledIcon>
<MenuItemText>New from template</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuItemWrapper <MenuItemWrapper
onClick={() => { onClick={() => {
@@ -105,6 +117,7 @@ export function FileMenu(props: {
</StyledIcon> </StyledIcon>
<MenuItemText>Import</MenuItemText> <MenuItemText>Import</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuDivider />
<MenuItemWrapper onClick={props.onDownload}> <MenuItemWrapper onClick={props.onDownload}>
<StyledIcon> <StyledIcon>
<FileDown /> <FileDown />

View File

@@ -0,0 +1,79 @@
import { Dialog, styled } from "@mui/material";
import { X } from "lucide-react";
import { useState } from "react";
import TemplatesList, {
Cross,
DialogContent,
DialogFooter,
DialogFooterButton,
} from "./TemplatesList";
function TemplatesDialog(properties: {
onClose: () => void;
onSelectTemplate: (templateId: string) => void;
}) {
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
const handleClose = () => {
properties.onClose();
};
const handleTemplateSelect = (templateId: string) => {
setSelectedTemplate(templateId);
};
return (
<DialogWrapper open={true} onClose={() => {}}>
<DialogTemplateHeader>
<span style={{ flexGrow: 2, marginLeft: 12 }}>Choose a template</span>
<Cross
style={{ marginRight: 12 }}
onClick={handleClose}
title="Close Dialog"
tabIndex={0}
onKeyDown={(event) => event.key === "Enter" && properties.onClose()}
>
<X />
</Cross>
</DialogTemplateHeader>
<DialogContent>
<TemplatesList
selectedTemplate={selectedTemplate}
handleTemplateSelect={handleTemplateSelect}
/>
</DialogContent>
<DialogFooter>
<DialogFooterButton
onClick={() => properties.onSelectTemplate(selectedTemplate)}
>
Create workbook
</DialogFooterButton>
</DialogFooter>
</DialogWrapper>
);
}
export const DialogWrapper = styled(Dialog)`
font-family: Inter;
.MuiDialog-paper {
width: 440px;
border-radius: 12px;
margin: 16px;
border: 1px solid #e0e0e0;
}
.MuiBackdrop-root {
background-color: rgba(0, 0, 0, 0.4);
}
`;
const DialogTemplateHeader = styled("div")`
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
height: 44px;
font-size: 14px;
font-weight: 500;
font-family: Inter;
`;
export default TemplatesDialog;

View File

@@ -0,0 +1,103 @@
import { Dialog, styled } from "@mui/material";
import { House, TicketsPlane } from "lucide-react";
import TemplatesListItem from "./TemplatesListItem";
function TemplatesList(props: {
selectedTemplate: string;
handleTemplateSelect: (templateId: string) => void;
}) {
const { selectedTemplate, handleTemplateSelect } = props;
return (
<TemplatesListWrapper>
<TemplatesListItem
title="Mortgage calculator"
description="Estimate payments, interest, and overall cost."
icon={<House />}
iconColor="#2F80ED"
active={selectedTemplate === "mortgage_calculator"}
onClick={() => handleTemplateSelect("mortgage_calculator")}
/>
<TemplatesListItem
title="Travel expenses tracker"
description="Track trip costs and stay on budget."
icon={<TicketsPlane />}
iconColor="#EB5757"
active={selectedTemplate === "travel_expenses_tracker"}
onClick={() => handleTemplateSelect("travel_expenses_tracker")}
/>
</TemplatesListWrapper>
);
}
export const DialogWrapper = styled(Dialog)`
font-family: Inter;
.MuiDialog-paper {
width: 440px;
border-radius: 12px;
margin: 16px;
border: 1px solid #e0e0e0;
}
.MuiBackdrop-root {
background-color: rgba(0, 0, 0, 0.4);
}
`;
export const Cross = styled("div")`
&:hover {
background-color: #f5f5f5;
}
display: flex;
border-radius: 4px;
min-height: 24px;
min-width: 24px;
cursor: pointer;
align-items: center;
justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`;
export const DialogContent = styled("div")`
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
max-height: 300px;
overflow: hidden;
overflow-y: auto;
`;
export const TemplatesListWrapper = styled("div")`
display: flex;
flex-direction: column;
gap: 10px;
`;
export const DialogFooter = styled("div")`
border-top: 1px solid #e0e0e0;
padding: 16px;
`;
export const DialogFooterButton = styled("button")`
background-color: #f2994a;
border: none;
color: #fff;
padding: 12px;
border-radius: 4px;
cursor: pointer;
width: 100%;
font-size: 12px;
font-family: Inter;
&:hover {
background-color: #d68742;
}
&:active {
background-color: #d68742;
}
`;
// export default TemplatesDialog;
export default TemplatesList;

View File

@@ -0,0 +1,107 @@
import { styled } from "@mui/material";
import type { ReactNode } from "react";
interface TemplatesListItemProps {
title: string;
description: string;
icon: ReactNode;
iconColor: string;
active: boolean;
onClick: () => void;
}
function TemplatesListItem({
title,
description,
icon,
iconColor,
active,
onClick,
}: TemplatesListItemProps) {
return (
<ListItemWrapper active={active} iconColor={iconColor} onClick={onClick}>
<StyledIcon iconColor={iconColor}>{icon}</StyledIcon>
<TemplatesListItemTitle>
<Title>{title}</Title>
<Subtitle>{description}</Subtitle>
</TemplatesListItemTitle>
<RadioButton active={active}>
<RadioButtonDot />
</RadioButton>
</ListItemWrapper>
);
}
const ListItemWrapper = styled("div")<{ active?: boolean; iconColor?: string }>`
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
font-size: 12px;
color: #424242;
border: 1px solid ${(props) => (props.active ? props.iconColor || "#424242" : "rgba(224, 224, 224, 0.60)")};
background-color: #FFFFFF;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
cursor: pointer;
outline: ${(props) => (props.active ? `4px solid ${props.iconColor || "#424242"}24` : "none")};
transition: border 0.1s ease-in-out;
user-select: none;
&:hover {
border: 1px solid ${(props) => props.iconColor};
transition: border 0.1s ease-in-out;
}
`;
const TemplatesListItemTitle = styled("div")`
display: flex;
flex-direction: column;
color: #424242;
width: 100%;
gap: 2px;
`;
const Title = styled("div")`
font-weight: 600;
color: #424242;
line-height: 16px;
`;
const Subtitle = styled("div")`
color: #757575;
`;
const StyledIcon = styled("div")<{ iconColor?: string }>`
display: flex;
align-items: center;
margin-top: -1px;
svg {
width: 18px;
height: 100%;
color: ${(props) => props.iconColor || "#424242"};
}
`;
const RadioButton = styled("div")<{ active?: boolean }>`
display: flex;
align-items: center;
justify-content: center;
width: 16px;
min-width: 16px;
height: 16px;
border-radius: 16px;
margin-top: -4px;
margin-right: -4px;
background-color: ${(props) => (props.active ? "#F2994A" : "#FFFFFF")};
border: ${(props) => (props.active ? "none" : "1px solid #E0E0E0")};
`;
const RadioButtonDot = styled("div")`
width: 6px;
height: 6px;
border-radius: 6px;
background-color: #FFF;
`;
export default TemplatesListItem;

View File

@@ -0,0 +1,136 @@
import { IronCalcIconWhite as IronCalcIcon } from "@ironcalc/workbook";
import { styled } from "@mui/material";
import { Table, X } from "lucide-react";
import { useState } from "react";
import TemplatesListItem from "./TemplatesListItem";
import TemplatesList, {
Cross,
DialogContent,
DialogFooter,
DialogFooterButton,
DialogWrapper,
TemplatesListWrapper,
} from "./TemplatesList";
function WelcomeDialog(properties: {
onClose: () => void;
onSelectTemplate: (templateId: string) => void;
}) {
const [selectedTemplate, setSelectedTemplate] = useState<string>("blank");
const handleClose = () => {
properties.onClose();
};
const handleTemplateSelect = (templateId: string) => {
setSelectedTemplate(templateId);
};
return (
<DialogWrapper open={true} onClose={() => {}}>
<DialogWelcomeHeader>
<DialogHeaderTitleWrapper>
<DialogHeaderLogoWrapper>
<IronCalcIcon />
</DialogHeaderLogoWrapper>
<DialogHeaderTitle>Welcome to IronCalc</DialogHeaderTitle>
<DialogHeaderTitleSubtitle>
Start with a blank workbook or a ready-made template.
</DialogHeaderTitleSubtitle>
</DialogHeaderTitleWrapper>
<Cross
onClick={handleClose}
title="Close Dialog"
tabIndex={0}
onKeyDown={(event) => event.key === "Enter" && properties.onClose()}
>
<X />
</Cross>
</DialogWelcomeHeader>
<DialogContent>
<ListTitle>New</ListTitle>
<TemplatesListWrapper>
<TemplatesListItem
title="Blank workbook"
description="Create from scratch or upload your own file."
icon={<Table />}
iconColor="#F2994A"
active={selectedTemplate === "blank"}
onClick={() => handleTemplateSelect("blank")}
/>
</TemplatesListWrapper>
<ListTitle>Templates</ListTitle>
<TemplatesList
selectedTemplate={selectedTemplate}
handleTemplateSelect={handleTemplateSelect}
/>
</DialogContent>
<DialogFooter>
<DialogFooterButton
onClick={() => properties.onSelectTemplate(selectedTemplate)}
>
Create workbook
</DialogFooterButton>
</DialogFooter>
</DialogWrapper>
);
}
const DialogWelcomeHeader = styled("div")`
display: flex;
flex-direction: row;
align-items: flex-start;
border-bottom: 1px solid #e0e0e0;
padding: 16px;
font-family: Inter;
`;
const DialogHeaderTitleWrapper = styled("span")`
display: flex;
flex-direction: column;
align-items: flex-start;
font-size: 14px;
font-weight: 500;
padding: 4px 0px;
gap: 4px;
width: 100%;
`;
const DialogHeaderTitle = styled("span")`
font-weight: 700;
`;
const DialogHeaderTitleSubtitle = styled("span")`
font-size: 12px;
color: #757575;
`;
const DialogHeaderLogoWrapper = styled("div")`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
max-width: 20px;
max-height: 20px;
background-color: #f2994a;
padding: 10px;
margin-bottom: 12px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: rotate(-8deg);
user-select: none;
svg {
width: 18px;
height: 18px;
}
`;
const ListTitle = styled("div")`
font-size: 12px;
font-weight: 600;
color: #424242;
`;
export default WelcomeDialog;

View File

@@ -60,7 +60,7 @@ export function createNewModel(): Model {
return model; return model;
} }
export function loadModelFromStorageOrCreate(): Model { export function loadSelectedModelFromStorage(): Model | null {
const uuid = localStorage.getItem("selected"); const uuid = localStorage.getItem("selected");
if (uuid) { if (uuid) {
// We try to load the selected model // We try to load the selected model
@@ -68,14 +68,22 @@ export function loadModelFromStorageOrCreate(): Model {
if (modelBytesString) { if (modelBytesString) {
return Model.from_bytes(base64ToBytes(modelBytesString)); return Model.from_bytes(base64ToBytes(modelBytesString));
} }
// If it doesn't exist we create one at that uuid
const newModel = new Model("Workbook1", "en", "UTC");
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(newModel.toBytes()));
return newModel;
} }
// If there was no selected model we create a new one return null;
return createNewModel(); }
// check if storage is empty
export function isStorageEmpty(): boolean {
const modelsJson = localStorage.getItem("models");
if (!modelsJson) {
return true;
}
try {
const models = JSON.parse(modelsJson);
return Object.keys(models).length === 0;
} catch (e) {
return true;
}
} }
export function saveSelectedModelInStorage(model: Model) { export function saveSelectedModelInStorage(model: Model) {

View File

@@ -7,7 +7,7 @@
//! This is primary for QA internal testing and will be superseded by an official //! This is primary for QA internal testing and will be superseded by an official
//! IronCalc CLI. //! IronCalc CLI.
//! //!
//! Usage: test file.xlsx //! Usage: test file.xlsx [output.icalc]
use std::path; use std::path;
@@ -15,15 +15,20 @@ use ironcalc::{export::save_to_icalc, import::load_from_xlsx};
fn main() { fn main() {
let args: Vec<_> = std::env::args().collect(); let args: Vec<_> = std::env::args().collect();
if args.len() != 2 { if args.len() != 2 && args.len() != 3 {
panic!("Usage: {} <file.xlsx>", args[0]); panic!("Usage: {} <file.xlsx> [output.icalc]", args[0]);
} }
// first test the file // first test the file
let file_name = &args[1]; let file_name = &args[1];
let file_path = path::Path::new(file_name); let output_file_name = if args.len() == 3 {
let base_name = file_path.file_stem().unwrap().to_str().unwrap(); &args[2]
let output_file_name = &format!("{base_name}.ic"); } else {
let file_path = path::Path::new(file_name);
let base_name = file_path.file_stem().unwrap().to_str().unwrap();
&format!("{base_name}.ic")
};
let model = load_from_xlsx(file_name, "en", "UTC").unwrap(); let model = load_from_xlsx(file_name, "en", "UTC").unwrap();
save_to_icalc(&model, output_file_name).unwrap(); save_to_icalc(&model, output_file_name).unwrap();
} }

View File

@@ -264,30 +264,29 @@ enum ParseReferenceError {
// There is a similar named function in ironcalc_base. We probably should fix both at the same time. // There is a similar named function in ironcalc_base. We probably should fix both at the same time.
// NB: Maybe use regexes for this? // NB: Maybe use regexes for this?
fn parse_reference(s: &str) -> Result<CellReferenceRC, ParseReferenceError> { fn parse_reference(s: &str) -> Result<CellReferenceRC, ParseReferenceError> {
let bytes = s.as_bytes();
let mut sheet_name = "".to_string(); let mut sheet_name = "".to_string();
let mut column = "".to_string(); let mut column = "".to_string();
let mut row = "".to_string(); let mut row = "".to_string();
let mut state = "sheet"; // "sheet", "col", "row" let mut state = "sheet"; // "sheet", "col", "row"
for &byte in bytes { for ch in s.chars() {
match state { match state {
"sheet" => { "sheet" => {
if byte == b'!' { if ch == '!' {
state = "col" state = "col"
} else { } else {
sheet_name.push(byte as char); sheet_name.push(ch);
} }
} }
"col" => { "col" => {
if byte.is_ascii_alphabetic() { if ch.is_ascii_alphabetic() {
column.push(byte as char); column.push(ch);
} else { } else {
state = "row"; state = "row";
row.push(byte as char); row.push(ch);
} }
} }
_ => { _ => {
row.push(byte as char); row.push(ch);
} }
} }
} }
@@ -1122,3 +1121,16 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
} }
Ok((sheets, selected_sheet)) Ok((sheets, selected_sheet))
} }
#[cfg(test)]
mod tests {
use crate::import::worksheets::parse_reference;
#[test]
fn parse_reference_works() {
let cell_reference = parse_reference("📈 Overview!B2");
assert!(cell_reference.is_ok());
let cell_reference = cell_reference.unwrap();
assert_eq!(cell_reference.sheet, "📈 Overview");
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -472,6 +472,45 @@ fn test_exporting_merged_cells() {
fs::remove_file(temp_file_name).unwrap(); fs::remove_file(temp_file_name).unwrap();
} }
#[test]
fn test_templates_xlsx() {
let mut entries = fs::read_dir("tests/templates/")
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()
.unwrap();
entries.sort();
let temp_folder = env::temp_dir();
let path = format!("{}", Uuid::new_v4());
let dir = temp_folder.join(path);
fs::create_dir(&dir).unwrap();
let mut is_error = false;
for file_path in entries {
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
println!("Testing file: {file_path_str}");
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
if let Err(message) = test_file(file_path_str) {
println!("Error with file: '{file_path_str}'");
println!("{message}");
is_error = true;
}
let t = test_load_and_saving(file_path_str, &dir);
if t.is_err() {
println!("Error while load and saving file: {file_path_str}");
is_error = true;
}
} else {
println!("skipping");
}
}
fs::remove_dir_all(&dir).unwrap();
assert!(
!is_error,
"Models were evaluated inconsistently with XLSX data."
);
}
#[test] #[test]
fn test_documentation_xlsx() { fn test_documentation_xlsx() {
let mut entries = fs::read_dir("tests/docs/") let mut entries = fs::read_dir("tests/docs/")