Compare commits

...

50 Commits

Author SHA1 Message Date
Nicolás Hatcher
ae53901a24 UPDATE: Updates docs on Errors in IronCalc 2024-12-15 07:52:52 +01:00
Nicolás Hatcher
23814ec18c FIX: Several fixes on the FV function
(1+x)^(1+y) was stringifyfied incorrectly
We still need work on this

FV now returns currency

FV(-1,-2,1) should return #DIV/0! not #NUM!
2024-12-14 22:08:44 +01:00
Daniel
8c6aaf2af0 FIX: Replace inline styles with StyledMenu component for improved readability 2024-12-14 18:34:13 +01:00
Daniel González-Albo
ed24e57555 Merge pull request #185 from ironcalc/fix/dani-webapp-navigator
FIX: remove legacy scrollbar styles
2024-12-14 18:14:17 +01:00
Daniel González-Albo
e8ced73b9c Merge pull request #186 from ironcalc/fix/dani-rename-sheet-dialog
FIX: Makes styling in Rename Sheet Dialog consistent with rest of dia…
2024-12-14 18:13:50 +01:00
Daniel
7ba002aca4 FIX: Makes footer/navigator scrollable when there are too many sheets 2024-12-14 18:04:35 +01:00
Daniel
b0e72321b4 FIX: Makes styling in Rename Sheet Dialog consistent with rest of dialogs 2024-12-13 20:43:38 +01:00
Nicolás Hatcher
41350fbd73 FIX: Run tests again and disable GEOMEAN test 2024-12-13 16:08:46 +01:00
Gian Hancock
17cd1fee96 Validate arg count for OR function
To be compatible with Excel, at least 1 argument is required.

Fixes #175
2024-12-12 13:06:12 +01:00
Gian Hancock
c59148bdf6 Add tests for OR function
Tests currently fail due to issue #175
2024-12-12 13:06:12 +01:00
Andrew Fillmore
d4a2289826 Add GEOMEAN .xlsx, Update .md files, and Run cargo fmt 2024-12-12 05:58:20 +01:00
Andrew Fillmore
e5aff48e36 Add GEOMEAN Tests 2024-12-12 05:58:20 +01:00
Andrew Fillmore
4c3374c0de Add GEOMEAN to Functions 2024-12-12 05:58:20 +01:00
Sinan Yumurtacı
5f3856350b ugh 2024-12-12 05:49:25 +01:00
Sinan Yumurtaci
7058a59c45 two other typos 2024-12-12 05:49:25 +01:00
Sinan Yumurtaci
075760b7ba typo 2024-12-12 05:49:25 +01:00
Nicolás Hatcher
8669962904 FIX: Apply biome "usafe" changes 2024-12-12 05:34:49 +01:00
Nicolás Hatcher
7b30736792 FIX: Make biome happy 2024-12-12 05:34:49 +01:00
Daniel
40c7fc8f80 FIX: Makes styling in Format Dialog consistent with Figmas 2024-12-12 05:34:49 +01:00
Daniel González-Albo
d8d694dd4a FIX: Makes styling in Format Menu consistent with Figmas (#179) 2024-12-12 05:13:07 +01:00
Daniel González-Albo
4b848f26dd FIX: Makes footer/navigator scrollable when there are too many sheets (#180) 2024-12-12 05:09:30 +01:00
Gian Hancock
2f660f85a7 Fix clippy lint (#177) 2024-12-11 11:45:24 +01:00
Sinan Yumurtacı
eee40c1b9a FIX: Prevent negative column width, row height (#167)
* Prevent negative column width / row height in rust

* prevent in front-end
2024-12-11 00:07:06 +01:00
Nicolás Hatcher Andrés
56915ce0b1 Merge pull request #173 from ironcalc/bugfix/nicolas-format-issue
Bugfix/nicolas format issue
2024-12-10 00:39:30 +01:00
Nicolás Hatcher
a4a3b11858 FIX: Avoid freezing the app on frozen columns or rows 2024-12-09 22:33:22 +01:00
Nicolás Hatcher
65f1738473 FIX: Use unicode code points in getFormulaHTML function 2024-12-09 21:46:03 +01:00
Nicolás Hatcher
a05ff18e40 FIX: Stop propagation in Rename window
We will need to do this in every widget in a more efficient manner
2024-12-09 19:51:28 +01:00
Nicolás Hatcher
4ef8a6882f FIX[Format-parser]: Parse [$€]#,##0.00 correctly
We will need to have a look at the format parser sooner rather
than later though
2024-12-09 19:50:41 +01:00
Nicolás Hatcher Andrés
2f2a5e4fba Merge pull request #172 from ironcalc/feature/dani-color-picker
UPDATE: Color picker changes
2024-12-08 22:32:14 +01:00
Daniel
c39540a747 UPDATE: Changing the color picker to look like in the Figmas 2024-12-08 22:22:17 +01:00
Daniel
0fa69045f9 UPDATE: WIP 2024-12-08 22:22:17 +01:00
Nicolás Hatcher Andrés
23e958af0c Merge pull request #171 from ironcalc/bugfix/dani-button-borders
UPDATE: Moved Border button next to Background color button
2024-12-08 22:16:25 +01:00
Daniel
057835627b UPDATE: Moved Border button next to Background color button 2024-12-08 21:54:34 +01:00
Nicolás Hatcher Andrés
4d6fdf9a4a Merge pull request #170 from ironcalc/feature/nicolas-disable-coverage
FIX: Disable coverage as it is failing for all
2024-12-08 17:15:23 +01:00
Nicolás Hatcher
5731b5cc27 FIX: Disable coverage as it is failing for all
I have send them an email, maybe I can fix this
2024-12-08 17:08:25 +01:00
Nicolás Hatcher Andrés
592ef2415d Merge pull request #169 from ironcalc/update/nicolas-fv-documentation
Update/nicolas fv documentation
2024-12-08 16:21:56 +01:00
Nicolás Hatcher
cb6685f72a FIX: Fv documentation. Some updates to the text 2024-12-08 11:11:48 +01:00
Nicolás Hatcher
8402bb0935 FIX: correct links in fv documentation 2024-12-08 01:55:34 +01:00
Nicolás Hatcher
91df91c425 FIX: Make biome happy 2024-12-07 12:45:49 +01:00
Daniel
4aa770c118 UPDATE: Makes 'File' button the same as 'Help', makes manu consistent 2024-12-07 12:45:49 +01:00
Daniel
c92c065073 UPDATE: Added a Help button 2024-12-07 12:45:49 +01:00
Daniel
6f124185b2 UPDATE: Cosmetic changes on file bar 2024-12-07 12:45:49 +01:00
stevethesleeve
17cf519d41 Update fv files (#162)
* Files updated for FV function documentation.

* Further FV changes
2024-12-07 12:25:57 +01:00
Sylvain Zimmer
bd1a1e3c97 Fix typo 2024-12-06 20:51:32 +01:00
Andrew Grosser
3d517a4af4 added updates from cargo fmt 2024-12-05 23:29:40 +01:00
Andrew Grosser
1734fd5740 added missing api functions for get_cell* 2024-12-05 23:29:40 +01:00
Daniel
d9dbd3bf14 UPDATE: Small fixes in Upload file Dialog 2024-12-05 00:11:41 +01:00
Nicolás Hatcher
d8a5c29e2f FIX: Make Biome happy 2024-12-01 20:14:38 +01:00
Daniel
85cd7ab6a3 UPDATE: design adjustments in 'File' menu 2024-12-01 20:14:38 +01:00
Nicolás Hatcher
4b806c357a FIX[parser]: Check the order (row, column) in range before transforming
Fixes #155
2024-11-30 14:37:06 +01:00
70 changed files with 1445 additions and 246 deletions

View File

@@ -1,10 +1,7 @@
name: Coverage
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
coverage:

View File

@@ -12,6 +12,7 @@
- Fixed several issues with pasting content
- Fixed several issues with borders
- Fixed bug where columns and rows could be resized to negative width and height, respectively
## [0.2.0] - 2024-11-06 (The HN release)

View File

@@ -10,6 +10,7 @@ format:
.PHONY: tests
tests: lint
cargo test
make remove-artifacts
# Regretabbly we need to build the wasm twice, once for the nodejs tests
# and a second one for the vitest.

View File

@@ -6,7 +6,7 @@ pub(crate) const DEFAULT_COLUMN_WIDTH: f64 = 125.0;
pub(crate) const DEFAULT_ROW_HEIGHT: f64 = 28.0;
pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0;
pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0;
pub(crate) const DEFAULT_WINDOW_HEIGH: i64 = 600;
pub(crate) const DEFAULT_WINDOW_HEIGHT: i64 = 600;
pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
pub(crate) const LAST_COLUMN: i32 = 16_384;

View File

@@ -24,6 +24,25 @@ fn test_get_tokens() {
assert_eq!(l.end, 10);
}
#[test]
fn get_tokens_unicode() {
let formula = "'🇵🇭 Philippines'!A1";
let t = get_tokens(formula);
assert_eq!(t.len(), 1);
let expected = TokenType::Reference {
sheet: Some("🇵🇭 Philippines".to_string()),
row: 1,
column: 1,
absolute_column: false,
absolute_row: false,
};
let l = t.first().expect("expected token");
assert_eq!(l.token, expected);
assert_eq!(l.start, 0);
assert_eq!(l.end, 19);
}
#[test]
fn test_simple_tokens() {
assert_eq!(

View File

@@ -49,15 +49,7 @@ pub mod stringify;
pub mod walk;
#[cfg(test)]
mod test;
#[cfg(test)]
mod test_ranges;
#[cfg(test)]
mod test_move_formula;
#[cfg(test)]
mod test_tables;
mod tests;
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
let mut lexer = lexer::Lexer::new(
@@ -522,20 +514,6 @@ impl Parser {
let mut absolute_row1 = left.absolute_row;
let mut absolute_row2 = right.absolute_row;
if self.lexer.is_a1_mode() {
if !left.absolute_row {
row1 -= context.row
};
if !left.absolute_column {
column1 -= context.column
};
if !right.absolute_row {
row2 -= context.row
};
if !right.absolute_column {
column2 -= context.column
};
}
if row1 > row2 {
(row2, row1) = (row1, row2);
(absolute_row2, absolute_row1) = (absolute_row1, absolute_row2);
@@ -544,6 +522,22 @@ impl Parser {
(column2, column1) = (column1, column2);
(absolute_column2, absolute_column1) = (absolute_column1, absolute_column2);
}
if self.lexer.is_a1_mode() {
if !absolute_row1 {
row1 -= context.row
};
if !absolute_column1 {
column1 -= context.column
};
if !absolute_row2 {
row2 -= context.row
};
if !absolute_column2 {
column2 -= context.column
};
}
match sheet_index {
Some(index) => Node::RangeKind {
sheet_name: sheet,

View File

@@ -456,11 +456,65 @@ fn stringify(
};
format!("{}{}{}", x, kind, y)
}
OpPowerKind { left, right } => format!(
"{}^{}",
stringify(left, context, displace_data, use_original_name),
stringify(right, context, displace_data, use_original_name)
),
OpPowerKind { left, right } => {
let x = match **left {
BooleanKind(_)
| NumberKind(_)
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| VariableKind(_)
| WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name)
}
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| UnaryKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| EmptyArgKind => format!(
"({})",
stringify(left, context, displace_data, use_original_name)
),
};
let y = match **right {
BooleanKind(_)
| NumberKind(_)
| StringKind(_)
| ReferenceKind { .. }
| RangeKind { .. }
| WrongReferenceKind { .. }
| VariableKind(_)
| WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name)
}
OpRangeKind { .. }
| OpConcatenateKind { .. }
| OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. }
| InvalidFunctionKind { .. }
| ArrayKind(_)
| UnaryKind { .. }
| ErrorKind(_)
| ParseErrorKind { .. }
| OpSumKind { .. }
| CompareKind { .. }
| EmptyArgKind => format!(
"({})",
stringify(right, context, displace_data, use_original_name)
),
};
format!("{}^{}", x, y)
}
InvalidFunctionKind { name, args } => {
format_function(name, args, context, displace_data, use_original_name)
}

View File

@@ -0,0 +1,6 @@
mod test_general;
mod test_issue_155;
mod test_move_formula;
mod test_ranges;
mod test_stringify;
mod test_tables;

View File

@@ -3,17 +3,11 @@
use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::DisplaceData;
use super::super::types::CellReferenceRC;
use super::Parser;
use super::{
super::parser::{
stringify::{to_rc_format, to_string},
Node,
},
stringify::to_string_displaced,
use crate::expressions::parser::stringify::{
to_rc_format, to_string, to_string_displaced, DisplaceData,
};
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> {
initial: &'a str,

View File

@@ -0,0 +1,69 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_155_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 2,
column: 2,
};
let t = parser.parse("A$1:A2", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "A$1:A2");
}
#[test]
fn issue_155_parser_case_2() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
let t = parser.parse("C$1:D2", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "C$1:D2");
}
#[test]
fn issue_155_parser_only_row() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
// This is tricky, I am not sure what to do in these cases
let t = parser.parse("A$2:B1", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "A1:B$2");
}
#[test]
fn issue_155_parser_only_column() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 20,
column: 20,
};
// This is tricky, I am not sure what to do in these cases
let t = parser.parse("D1:$A3", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "$A1:D3");
}

View File

@@ -1,10 +1,8 @@
use std::collections::HashMap;
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
use crate::expressions::types::Area;
use super::super::types::CellReferenceRC;
use super::Parser;
use crate::expressions::parser::Parser;
use crate::expressions::types::{Area, CellReferenceRC};
#[test]
fn test_move_formula() {

View File

@@ -2,9 +2,9 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use super::super::parser::stringify::{to_rc_format, to_string};
use super::super::types::CellReferenceRC;
use super::Parser;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> {
formula_a1: &'a str,

View File

@@ -0,0 +1,34 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
#[test]
fn exp_order() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 1,
column: 1,
};
let t = parser.parse("(1 + 2)^3 + 4", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4");
let t = parser.parse("(C5 + 3)^R4", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4");
let t = parser.parse("(C5 + 3)^(R4*6)", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)");
let t = parser.parse("(C5)^(R4)", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "C5^R4");
let t = parser.parse("(5)^(4)", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "5^4");
}

View File

@@ -6,8 +6,8 @@ use crate::expressions::parser::stringify::to_string;
use crate::expressions::utils::{number_to_column, parse_reference_a1};
use crate::types::{Table, TableColumn, TableStyleInfo};
use super::super::types::CellReferenceRC;
use super::Parser;
use crate::expressions::parser::Parser;
use crate::expressions::types::CellReferenceRC;
fn create_test_table(
table_name: &str,

View File

@@ -245,6 +245,9 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
}
ParsePart::Number(p) => {
let mut text = "".to_string();
if let Some(c) = p.currency {
text = format!("{}", c);
}
let tokens = &p.tokens;
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
// p.precision is the number of significant digits _after_ the decimal point

View File

@@ -10,6 +10,7 @@ pub struct Lexer {
pub enum Token {
Color(i32), // [Red] or [Color 23]
Condition(Compare, f64), // [<=100] (Comparator, number)
Currency(char), // [$€] ($ currency symbol)
Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X
Spacer(char), // *X
Ghost(char), // _X
@@ -274,6 +275,15 @@ impl Lexer {
self.set_error("Failed to parse condition");
Token::ILLEGAL
}
} else if c == '$' {
// currency
self.read_next_char();
if let Some(currency) = self.read_next_char() {
self.read_next_char();
return Token::Currency(currency);
}
self.set_error("Failed to parse currency");
Token::ILLEGAL
} else {
// Color
if let Some(index) = self.consume_color() {

View File

@@ -74,6 +74,7 @@ mod test;
//
// * Color [Red] or [Color 23] or [Color23]
// * Conditions [<100]
// * Currency [$€]
// * Space _X when X is any given char
// * A spacer of chars: *X where X is repeated as much as possible
// * Literals: $, (, ), :, +, - and space

View File

@@ -40,6 +40,7 @@ pub struct NumberPart {
pub is_scientific: bool,
pub scientific_minus: bool,
pub exponent_digit_count: i32,
pub currency: Option<char>,
}
pub struct DatePart {
@@ -114,6 +115,7 @@ impl Parser {
let mut exponent_digit_count = 0;
let mut number = 'i';
let mut index = 0;
let mut currency = None;
while token != Token::EOF && token != Token::Separator {
let next_token = self.lexer.next_token();
@@ -170,6 +172,9 @@ impl Parser {
Token::Condition(cmp, value) => {
condition = Some((cmp, value));
}
Token::Currency(c) => {
currency = Some(c);
}
Token::QuestionMark => {
tokens.push(TextToken::Digit(Digit {
kind: '?',
@@ -291,6 +296,7 @@ impl Parser {
is_scientific,
scientific_minus,
exponent_digit_count,
currency,
})
}
}

View File

@@ -76,6 +76,14 @@ fn test_color() {
assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4));
}
#[test]
fn dollar_euro() {
let locale = get_default_locale();
let format = "[$€]#,##0.00";
let t = format_number(3.1, format, locale);
assert_eq!(t.text, "€3.10");
}
#[test]
fn test_parts() {
let locale = get_default_locale();

View File

@@ -89,6 +89,9 @@ fn compute_future_value(
if rate == 0.0 {
return Ok(-pv - pmt * nper);
}
if rate == -1.0 && nper < 0.0 {
return Err((Error::DIV, "Divide by zero".to_string()));
}
let rate_nper = (1.0 + rate).powf(nper);
let fv = if period_start {

View File

@@ -136,6 +136,9 @@ impl Model {
}
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut result = false;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {

View File

@@ -140,6 +140,7 @@ pub enum Function {
Countifs,
Maxifs,
Minifs,
Geomean,
// Date and time
Date,
@@ -248,7 +249,7 @@ pub enum Function {
}
impl Function {
pub fn into_iter() -> IntoIter<Function, 194> {
pub fn into_iter() -> IntoIter<Function, 195> {
[
Function::And,
Function::False,
@@ -348,6 +349,7 @@ impl Function {
Function::Countifs,
Function::Maxifs,
Function::Minifs,
Function::Geomean,
Function::Year,
Function::Day,
Function::Month,
@@ -611,6 +613,7 @@ impl Function {
"COUNTIFS" => Some(Function::Countifs),
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
"GEOMEAN" => Some(Function::Geomean),
// Date and Time
"YEAR" => Some(Function::Year),
"DAY" => Some(Function::Day),
@@ -818,6 +821,7 @@ impl fmt::Display for Function {
Function::Countifs => write!(f, "COUNTIFS"),
Function::Maxifs => write!(f, "MAXIFS"),
Function::Minifs => write!(f, "MINIFS"),
Function::Geomean => write!(f, "GEOMEAN"),
Function::Year => write!(f, "YEAR"),
Function::Day => write!(f, "DAY"),
Function::Month => write!(f, "MONTH"),
@@ -1054,6 +1058,7 @@ impl Model {
Function::Countifs => self.fn_countifs(args, cell),
Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell),
Function::Geomean => self.fn_geomean(args, cell),
// Date and Time
Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell),

View File

@@ -635,4 +635,85 @@ impl Model {
}
CalcResult::Number(max)
}
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut product = 1.0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
} else {
product *= if b { 1.0 } else { 0.0 };
count += 1.0;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
count += 1.0;
product *= value;
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
} else if let Ok(t) = s.parse::<f64>() {
product *= t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
}
}
_ => {
// Ignore everything else
}
};
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(product.powf(1.0 / count))
}
}

View File

@@ -4,7 +4,7 @@ use std::collections::HashMap;
use crate::{
calc_result::Range,
constants::{DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH},
constants::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
expressions::{
lexer::LexerMode,
parser::{
@@ -359,7 +359,7 @@ impl Model {
WorkbookView {
sheet: 0,
window_width: DEFAULT_WINDOW_WIDTH,
window_height: DEFAULT_WINDOW_HEIGH,
window_height: DEFAULT_WINDOW_HEIGHT,
},
);

View File

@@ -46,13 +46,17 @@ pub(crate) mod util;
mod engineering;
mod test_fn_offset;
mod test_fn_or;
mod test_number_format;
mod test_escape_quotes;
mod test_extend;
mod test_fn_fv;
mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_geomean;
mod test_get_cell_content;
mod test_issue_155;
mod test_percentage;
mod test_set_functions_error_handling;
mod test_today;

View File

@@ -82,3 +82,21 @@ fn test_column_width_higher_edge() {
assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
assert_eq!(model.get_cell_style_index(0, 23, 16), Ok(1));
}
#[test]
fn test_column_width_negative() {
let mut model = new_empty_model();
let result = model
.workbook
.worksheet_mut(0)
.unwrap()
.set_column_width(16, -1.0);
assert_eq!(result, Err("Can not set a negative width: -1".to_string()));
assert_eq!(model.workbook.worksheets[0].cols.len(), 0);
let worksheet = model.workbook.worksheet(0).unwrap();
assert_eq!(
(worksheet.get_column_width(16).unwrap()),
DEFAULT_COLUMN_WIDTH
);
assert_eq!(model.get_cell_style_index(0, 23, 16), Ok(0));
}

View File

@@ -2,9 +2,6 @@
use crate::test::util::new_empty_model;
#[test]
fn simple_cases() {}
#[test]
fn wrong_number_of_arguments() {
let mut model = new_empty_model();

View File

@@ -0,0 +1,36 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn computation() {
let i2 = "=-C2*(1+D2)^E2-F2*((D2+1)*((1+D2)^E2-1))/D2";
let mut model = new_empty_model();
model._set("C2", "1");
model._set("D2", "2");
model._set("E2", "3");
model._set("F2", "4");
model._set("I2", i2);
model.evaluate();
assert_eq!(model._get_text("I2"), "-183");
assert_eq!(model._get_formula("I2"), i2);
}
#[test]
fn format_as_currency() {
let mut model = new_empty_model();
model._set("C2", "1");
model._set("D2", "2");
model._set("E2", "3");
model._set("F2", "4");
model._set("I2", "=FV(D2,E2,F2,C2,1)");
model.evaluate();
assert_eq!(model._get_text("I2"), "-$183.00");
}

View File

@@ -0,0 +1,36 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 0)");
model._set("A2", "=OR(0, 0)");
model._set("A3", "=OR(true, false)");
model._set("A4", "=OR(false, false)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
}
#[test]
fn fn_or_no_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR()");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
#[test]
fn fn_or_missing_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR(,)");
model._set("A2", "=OR(,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
}

View File

@@ -1,5 +1,7 @@
#![allow(clippy::unwrap_used)]
use crate::constants::DEFAULT_ROW_HEIGHT;
use crate::cell::CellValue;
use crate::number_format::to_excel_precision_str;
@@ -113,6 +115,15 @@ fn test_set_row_height() {
worksheet.set_row_height(5, 5.0).unwrap();
let worksheet = model.workbook.worksheet(0).unwrap();
assert!((5.0 - worksheet.row_height(5).unwrap()).abs() < f64::EPSILON);
let worksheet = model.workbook.worksheet_mut(0).unwrap();
let result = worksheet.set_row_height(6, -1.0);
assert_eq!(result, Err("Can not set a negative height: -1".to_string()));
assert_eq!(worksheet.row_height(6).unwrap(), DEFAULT_ROW_HEIGHT);
worksheet.set_row_height(6, 0.0).unwrap();
assert_eq!(worksheet.row_height(6).unwrap(), 0.0);
}
#[test]

View File

@@ -0,0 +1,27 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn test_fn_geomean_arguments() {
let mut model = new_empty_model();
model._set("A1", "=GEOMEAN()");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
#[test]
fn test_fn_geomean_minimal() {
let mut model = new_empty_model();
model._set("B1", "1");
model._set("B2", "2");
model._set("B3", "3");
model._set("B4", "'2");
// B5 is empty
model._set("B6", "true");
model._set("A1", "=GEOMEAN(B1:B6)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"1.817120593");
}

View File

@@ -0,0 +1,14 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn issue_155() {
let mut model = new_empty_model();
model._set("A1", "1");
model._set("A2", "2");
model._set("B2", "=A$1:A2");
model.evaluate();
assert_eq!(model._get_formula("B2"), "=A$1:A2".to_string());
}

View File

@@ -2,7 +2,7 @@
use crate::{
constants::{
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH,
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH,
LAST_COLUMN,
},
test::util::new_empty_model,
@@ -87,7 +87,7 @@ fn last_colum() {
fn page_down() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let window_height = DEFAULT_WINDOW_HEIGH as f64;
let window_height = DEFAULT_WINDOW_HEIGHT as f64;
let row_height = DEFAULT_ROW_HEIGHT;
let row_count = f64::floor(window_height / row_height) as i32;
model.on_page_down().unwrap();

View File

@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::{DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGH, DEFAULT_WINDOW_WIDTH},
constants::{DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
test::util::new_empty_model,
UserModel,
};
@@ -11,7 +11,7 @@ fn basic_test() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
let window_height = model.get_window_height().unwrap();
assert_eq!(window_height, DEFAULT_WINDOW_HEIGH);
assert_eq!(window_height, DEFAULT_WINDOW_HEIGHT);
let window_width = model.get_window_width().unwrap();
assert_eq!(window_width, DEFAULT_WINDOW_WIDTH);

View File

@@ -35,7 +35,7 @@ pub struct WorkbookView {
pub sheet: u32,
/// The current width of the window
pub window_width: i64,
/// The current heigh of the window
/// The current height of the window
pub window_height: i64,
}

View File

@@ -309,6 +309,7 @@ impl Model {
Function::Sum => self.units_fn_sum_like(args, cell),
Function::Average => self.units_fn_sum_like(args, cell),
Function::Pmt => self.units_fn_currency(args, cell),
Function::Fv => self.units_fn_currency(args, cell),
Function::Nper => self.units_fn_currency(args, cell),
Function::Npv => self.units_fn_currency(args, cell),
Function::Irr => self.units_fn_percentage(args, cell),

View File

@@ -342,7 +342,7 @@ impl UserModel {
old_value: Box::new(old_value),
}];
let line_count = value.split("\n").count();
let line_count = value.split('\n').count();
let row_height = self.model.get_row_height(sheet, row)?;
let cell_height = (line_count as f64) * DEFAULT_ROW_HEIGHT;
if cell_height > row_height {
@@ -713,7 +713,7 @@ impl UserModel {
/// Paste `styles` in the selected area
pub fn on_paste_styles(&mut self, styles: &[Vec<Style>]) -> Result<(), String> {
let styles_heigh = styles.len() as i32;
let styles_height = styles.len() as i32;
let styles_width = styles[0].len() as i32;
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
@@ -732,13 +732,13 @@ impl UserModel {
// If the pasted area is smaller than the selected area we increase it
let [row_start, column_start, row_end, column_end] = range;
let last_row = row_end.max(row_start + styles_heigh - 1);
let last_row = row_end.max(row_start + styles_height - 1);
let last_column = column_end.max(column_start + styles_width - 1);
let mut diff_list = Vec::new();
for row in row_start..=last_row {
for column in column_start..=last_column {
let row_index = ((row - row_start) % styles_heigh) as usize;
let row_index = ((row - row_start) % styles_height) as usize;
let column_index = ((column - column_start) % styles_width) as usize;
let style = &styles[row_index][column_index];
let old_value = self.model.get_style_for_cell(sheet, row, column)?;

View File

@@ -255,11 +255,14 @@ impl Worksheet {
/// * If the row does not a have a style we add it.
/// * If it has we modify the height and make sure it is applied.
///
/// Fails if column index is outside allowed range.
/// Fails if row index is outside allowed range or height is negative.
pub fn set_row_height(&mut self, row: i32, height: f64) -> Result<(), String> {
if !is_valid_row(row) {
return Err(format!("Row number '{row}' is not valid."));
}
if height < 0.0 {
return Err(format!("Can not set a negative height: {height}"));
}
let rows = &mut self.rows;
for r in rows.iter_mut() {
@@ -284,7 +287,7 @@ impl Worksheet {
/// * If the column does not a have a width we simply add it
/// * If it has, it might be part of a range and we ned to split the range.
///
/// Fails if column index is outside allowed range.
/// Fails if column index is outside allowed range or width is negative.
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
self.set_column_width_and_style(column, width, None)
}
@@ -298,6 +301,9 @@ impl Worksheet {
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
if width < 0.0 {
return Err(format!("Can not set a negative width: {width}"));
}
let cols = &mut self.cols;
let mut col = Col {
min: column,

View File

@@ -10,6 +10,8 @@ use xlsx::import;
mod types;
use crate::types::PyCellType;
create_exception!(_ironcalc, WorkbookError, PyException);
/// This is a model implementing the 'raw' API
@@ -58,6 +60,21 @@ impl PyModel {
// Get values
/// Get raw value
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> PyResult<String> {
self.model
.get_cell_content(sheet, row, column)
.map_err(|e| WorkbookError::new_err(e.to_string()))
}
/// Get cell type
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> PyResult<PyCellType> {
self.model
.get_cell_type(sheet, row, column)
.map(|cell_type| cell_type.into())
.map_err(|e| WorkbookError::new_err(e.to_string()))
}
/// Get formatted value
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> PyResult<String> {
self.model

View File

@@ -1,7 +1,7 @@
use pyo3::prelude::*;
use xlsx::base::types::{
Alignment, Border, BorderItem, BorderStyle, Fill, Font, FontScheme, HorizontalAlignment, Style,
VerticalAlignment,
Alignment, Border, BorderItem, BorderStyle, CellType, Fill, Font, FontScheme,
HorizontalAlignment, Style, VerticalAlignment,
};
#[derive(Clone)]
@@ -161,6 +161,17 @@ pub struct PyFill {
pub bg_color: Option<String>,
}
#[pyclass(eq, eq_int)]
#[derive(PartialEq, Clone)]
pub enum PyCellType {
Number = 1,
Text = 2,
LogicalValue = 4,
ErrorValue = 16,
Array = 64,
CompoundData = 128,
}
// Conversions from references to Py* types to non-Py types
// Enums
@@ -426,3 +437,31 @@ impl From<Style> for PyStyle {
}
}
}
// Conversion from PyCellType to CellType
impl From<PyCellType> for CellType {
fn from(py_cell_type: PyCellType) -> Self {
match py_cell_type {
PyCellType::Number => CellType::Number,
PyCellType::Text => CellType::Text,
PyCellType::LogicalValue => CellType::LogicalValue,
PyCellType::ErrorValue => CellType::ErrorValue,
PyCellType::Array => CellType::Array,
PyCellType::CompoundData => CellType::CompoundData,
}
}
}
// Conversion from CellType to PyCellType
impl From<CellType> for PyCellType {
fn from(cell_type: CellType) -> Self {
match cell_type {
CellType::Number => PyCellType::Number,
CellType::Text => PyCellType::Text,
CellType::LogicalValue => PyCellType::LogicalValue,
CellType::ErrorValue => PyCellType::ErrorValue,
CellType::Array => PyCellType::Array,
CellType::CompoundData => PyCellType::CompoundData,
}
}
}

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",
@@ -2003,7 +2027,7 @@ export default defineConfig({
link: "/programming/python-bindings",
},
{
text: "JavScript",
text: "JavaScript",
link: "/programming/javascript-bindings",
},
],

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

@@ -4,8 +4,93 @@ outline: deep
lang: en-US
---
# FV
# FV function
## Overview
FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value.
::: 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).
:::
FV can be used to calculate future value over a specified number of compounding periods. A fixed interest rate or yield is assumed over all periods, and a fixed payment or deposit can be applied at the start or end of every period.
If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV.
## Usage
### Syntax
**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*. ([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.txt-->
## Details
* 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.
$$
\text{pmt}\times (1 + \text{rate})^4+\text{pmt}\times (1 + \text{rate})^3+\text{pmt}\times (1 + \text{rate})^2 +\text{pmt}
$$
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}$
$$
\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

@@ -0,0 +1 @@
* For information about the different types of errors that you may encounter when using IronCalc functions, visit our [Error Types](/features/error-types) page.

View File

@@ -9,13 +9,13 @@ lang: en-US
At the moment IronCalc only supports a few function in this section.
You can track the progress in this [GitHub issue](https://github.com/ironcalc/IronCalc/issues/55).
| Function | Status | Documentation |
| ------------------------ | ---------------------------------------------- | ------------- |
| Function | Status | Documentation |
| ------------------------ |--------------------------------------------------| ------------- |
| AVEDEV | <Badge type="info" text="Not implemented yet" /> | |
| AVERAGE | <Badge type="tip" text="Available" /> | |
| AVERAGEA | <Badge type="tip" text="Available" /> | |
| AVERAGEIF | <Badge type="tip" text="Available" /> | |
| AVERAGEIFS | <Badge type="tip" text="Available" /> | |
| AVERAGE | <Badge type="tip" text="Available" /> | |
| AVERAGEA | <Badge type="tip" text="Available" /> | |
| AVERAGEIF | <Badge type="tip" text="Available" /> | |
| AVERAGEIFS | <Badge type="tip" text="Available" /> | |
| BETA.DIST | <Badge type="info" text="Not implemented yet" /> | |
| BETA.INV | <Badge type="info" text="Not implemented yet" /> | |
| BINOM.DIST | <Badge type="info" text="Not implemented yet" /> | |
@@ -29,11 +29,11 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| CONFIDENCE.NORM | <Badge type="info" text="Not implemented yet" /> | |
| CONFIDENCE.T | <Badge type="info" text="Not implemented yet" /> | |
| CORREL | <Badge type="info" text="Not implemented yet" /> | |
| COUNT | <Badge type="tip" text="Available" /> | |
| COUNTA | <Badge type="tip" text="Available" /> | |
| COUNTBLANK | <Badge type="tip" text="Available" /> | |
| COUNTIF | <Badge type="tip" text="Available" /> | |
| COUNTIFS | <Badge type="tip" text="Available" /> | |
| COUNT | <Badge type="tip" text="Available" /> | |
| COUNTA | <Badge type="tip" text="Available" /> | |
| COUNTBLANK | <Badge type="tip" text="Available" /> | |
| COUNTIF | <Badge type="tip" text="Available" /> | |
| COUNTIFS | <Badge type="tip" text="Available" /> | |
| COVARIANCE.P | <Badge type="info" text="Not implemented yet" /> | |
| COVARIANCE.S | <Badge type="info" text="Not implemented yet" /> | |
| DEVSQ | <Badge type="info" text="Not implemented yet" /> | |
@@ -58,7 +58,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| GAMMALN | <Badge type="info" text="Not implemented yet" /> | |
| GAMMALN.PRECISE | <Badge type="info" text="Not implemented yet" /> | |
| GAUSS | <Badge type="info" text="Not implemented yet" /> | |
| GEOMEAN | <Badge type="info" text="Not implemented yet" /> | |
| GEOMEAN | <Badge type="info" text="Available" /> | |
| GROWTH | <Badge type="info" text="Not implemented yet" /> | |
| HARMEAN | <Badge type="info" text="Not implemented yet" /> | |
| HYPGEOM.DIST | <Badge type="info" text="Not implemented yet" /> | |
@@ -69,9 +69,9 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| LOGEST | <Badge type="info" text="Not implemented yet" /> | |
| LOGNORM.DIST | <Badge type="info" text="Not implemented yet" /> | |
| LOGNORM.INV | <Badge type="info" text="Not implemented yet" /> | |
| MAX | <Badge type="tip" text="Available" /> | |
| MAX | <Badge type="tip" text="Available" /> | |
| MAXA | <Badge type="info" text="Not implemented yet" /> | |
| MAXIFS | <Badge type="tip" text="Available" /> | |
| MAXIFS | <Badge type="tip" text="Available" /> | |
| MEDIAN | <Badge type="info" text="Not implemented yet" /> | |
| MODE.MULT | <Badge type="info" text="Not implemented yet" /> | |
| MODE.SNGL | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -7,6 +7,5 @@ lang: en-US
# GEOMEAN
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 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

@@ -35,6 +35,11 @@ export function FileBar(properties: {
}}
onDelete={properties.onDelete}
/>
<HelpButton
onClick={() => window.open("https://docs.ironcalc.com", "_blank")}
>
Help
</HelpButton>
<WorkbookTitle
name={properties.model.getName()}
onNameChange={(name) => {
@@ -84,7 +89,7 @@ export function FileBar(properties: {
const StyledDesktopLogo = styled(IronCalcLogo)`
width: 120px;
margin-left: 10px;
margin-left: 12px;
@media (max-width: 769px) {
display: none;
}
@@ -98,6 +103,19 @@ const StyledIronCalcIcon = styled(IronCalcIcon)`
}
`;
const HelpButton = styled("div")`
display: flex;
align-items: center;
font-size: 12px;
font-family: Inter;
padding: 8px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
}
`;
const Toast = styled("div")`
font-weight: 400;
font-size: 12px;
@@ -107,7 +125,7 @@ const Toast = styled("div")`
`;
const Divider = styled("div")`
margin: 10px;
margin: 0px 8px 0px 16px;
height: 12px;
border-left: 1px solid #e0e0e0;
`;

View File

@@ -1,6 +1,6 @@
import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material";
import { FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { useRef, useState } from "react";
import { UploadFileDialog } from "./UploadFileDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage";
@@ -29,9 +29,9 @@ export function FileMenu(props: {
setMenuOpen(false);
}}
>
<span style={{ width: "20px" }}>
{uuid === selectedUuid ? "•" : ""}
</span>
<CheckIndicator>
{uuid === selectedUuid ? <StyledCheck /> : ""}
</CheckIndicator>
<MenuItemText
style={{
maxWidth: "240px",
@@ -57,9 +57,19 @@ export function FileMenu(props: {
open={isMenuOpen}
onClose={(): void => setMenuOpen(false)}
anchorEl={anchorElement.current}
sx={{
"& .MuiPaper-root": { borderRadius: "8px", padding: "4px 0px" },
"& .MuiList-root": { padding: "0" },
}}
// anchorOrigin={properties.anchorOrigin}
>
<MenuItemWrapper onClick={props.newModel}>
<MenuItemWrapper
onClick={() => {
props.newModel();
setMenuOpen(false);
}}
>
<StyledPlus />
<MenuItemText>New</MenuItemText>
</MenuItemWrapper>
@@ -149,12 +159,19 @@ const StyledTrash = styled(Trash2)`
padding-right: 10px;
`;
const StyledCheck = styled(Check)`
width: 16px;
height: 16px;
color: #333333;
padding-right: 10px;
`;
const MenuDivider = styled("div")`
width: 80%;
width: 100%;
margin: auto;
margin-top: 8px;
margin-bottom: 8px;
border-top: 1px solid #e0e0e0;
margin-top: 4px;
margin-bottom: 4px;
border-top: 1px solid #eeeeee;
`;
const MenuItemText = styled("div")`
@@ -166,7 +183,12 @@ const MenuItemWrapper = styled(MenuItem)`
display: flex;
justify-content: flex-start;
font-size: 14px;
width: 100%;
width: calc(100% - 8px);
min-width: 172px;
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
`;
const FileMenuWrapper = styled("div")`
@@ -174,11 +196,16 @@ const FileMenuWrapper = styled("div")`
align-items: center;
font-size: 12px;
font-family: Inter;
padding: 10px;
height: 20px;
padding: 8px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
}
`;
const CheckIndicator = styled("span")`
display: flex;
justify-content: center;
min-width: 26px;
`;

View File

@@ -134,7 +134,7 @@ export function UploadFileDialog(properties: {
width: 16,
color: "#EFAA6D",
backgroundColor: "#F2994A1A",
padding: "2px 4px",
padding: "2px 6px",
borderRadius: 4,
}}
/>
@@ -192,7 +192,13 @@ export function UploadFileDialog(properties: {
<BookOpen
style={{ width: 16, height: 16, marginLeft: 12, marginRight: 8 }}
/>
<span>Learn more about importing files into IronCalc</span>
<UploadFooterLink
href="https://docs.ironcalc.com/web-application/importing-files.html"
target="_blank"
rel="noopener noreferrer"
>
Learn more about importing files into IronCalc
</UploadFooterLink>
</UploadFooter>
</UploadDialog>
);
@@ -202,34 +208,46 @@ const Cross = styled("div")`
&:hover {
background-color: #f5f5f5;
}
display: flex;
border-radius: 4px;
height: 16px;
width: 16px;
height: 24px;
width: 24px;
cursor: pointer;
align-items: center;
justify-content: center;
`;
const DocLink = styled("span")`
color: #f2994a;
text-decoration: underline;
text-decoration: none;
&:hover {
font-weight: bold;
text-decoration: underline;
}
`;
const UploadFooter = styled("div")`
height: 40px;
height: 44px;
border-top: 1px solid #e0e0e0;
font-size: 12px;
font-weight: 400;
color: #757575;
display: flex;
align-items: center;
`;
const UploadFooterLink = styled("a")`
font-size: 12px;
font-weight: 400;
color: #757575;
text-decoration: none;
&:hover {
text-decoration: underline;
}
`;
const UploadTitle = styled("div")`
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
height: 40px;
height: 44px;
font-size: 14px;
font-weight: 500;
`;
@@ -241,7 +259,8 @@ const UploadDialog = styled("div")`
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 455px;
width: 460px;
max-width: 90%;
height: 285px;
background: #fff;
border: 1px solid #e0e0e0;
@@ -251,6 +270,16 @@ const UploadDialog = styled("div")`
`;
const DropZone = styled("div")`
&:hover {
border: 1px dashed #f2994a;
transition: 0.2s ease-in-out;
gap: 8px;
background: linear-gradient(
180deg,
rgba(242, 153, 74, 0.12) 0%,
rgba(242, 153, 74, 0) 100%
);
}
flex-grow: 2;
border-radius: 10px;
text-align: center;
@@ -259,7 +288,7 @@ const DropZone = styled("div")`
font-family: Arial, sans-serif;
cursor: pointer;
background-color: #faebd7;
border: 1px dashed #f2994a;
border: 1px dashed #efaa6d;
background: linear-gradient(
180deg,
rgba(242, 153, 74, 0.08) 0%,
@@ -268,4 +297,6 @@ const DropZone = styled("div")`
display: flex;
flex-direction: column;
vertical-align: center;
gap: 16px;
transition: 0.2s ease-in-out;
`;

View File

@@ -867,14 +867,18 @@ export default class WorksheetCanvas {
frozenRows,
frozenColumns,
);
xFrozenEnd += this.getColumnWidth(
this.model.getSelectedSheet(),
frozenColumns,
);
yFrozenEnd += this.getRowHeight(
this.model.getSelectedSheet(),
frozenRows,
);
if (frozenColumns > 0) {
xFrozenEnd += this.getColumnWidth(
this.model.getSelectedSheet(),
frozenColumns,
);
}
if (frozenRows > 0) {
yFrozenEnd += this.getRowHeight(
this.model.getSelectedSheet(),
frozenRows,
);
}
if (startRow <= frozenRows && endRow > frozenRows) {
yEnd = Math.max(yEnd, yFrozenEnd);
}

View File

@@ -71,6 +71,7 @@ const ColorPicker = (properties: ColorPickerProps) => {
setColor(newColor);
}}
/>
<HorizontalDivider />
<ColorPickerInput>
<HexWrapper>
<HexLabel>{"Hex"}</HexLabel>
@@ -131,6 +132,8 @@ const ColorPicker = (properties: ColorPickerProps) => {
const RecentLabel = styled.div`
font-family: "Inter";
font-size: 12px;
font-family: Inter;
margin: 8px 8px 0px 8px;
color: ${theme.palette.text.secondary};
`;
@@ -138,14 +141,17 @@ const ColorList = styled.div`
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 8px;
justify-content: flex-start;
gap: 4.7px;
`;
const Button = styled.button<{ $color: string }>`
width: 20px;
height: 20px;
width: 16px;
height: 16px;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["600"]};`;
return `border: 1px solid ${theme.palette.grey["300"]};`;
}
return `border: 1px solid ${$color};`;
}}
@@ -153,17 +159,19 @@ const Button = styled.button<{ $color: string }>`
return $color;
}};
box-sizing: border-box;
margin-top: 10px;
margin-right: 10px;
border-radius: 2px;
margin-top: 0px;
border-radius: 4px;
&:hover {
cursor: pointer;
outline: 1px solid ${theme.palette.grey["300"]};
outline-offset: 1px;
}
`;
const HorizontalDivider = styled.div`
height: 0px;
width: 100%;
border-top: 1px solid ${theme.palette.grey["400"]};
margin-top: 15px;
margin-bottom: 5px;
border-top: 1px solid ${theme.palette.grey["200"]};
`;
// const StyledPopover = styled(Popover)`
@@ -183,7 +191,7 @@ const HorizontalDivider = styled.div`
const ColorPickerDialog = styled.div`
background: ${theme.palette.background.default};
width: ${colorPickerWidth}px;
padding: 15px;
padding: 0px;
display: flex;
flex-direction: column;
@@ -193,11 +201,11 @@ const ColorPickerDialog = styled.div`
}
& .react-colorful__saturation {
border-bottom: none;
border-radius: 5px;
border-radius: 0px;
}
& .react-colorful__hue {
height: 20px;
margin-top: 15px;
height: 8px;
margin: 8px;
border-radius: 5px;
}
& .react-colorful__saturation-pointer {
@@ -206,19 +214,22 @@ const ColorPickerDialog = styled.div`
}
& .react-colorful__hue-pointer {
width: 7px;
border-radius: 3px;
border-radius: 8px;
height: 16px;
width: 16px;
border-bottom: 1px solid #eee;
}
`;
const HashLabel = styled.div`
margin: auto 0px auto 10px;
font-size: 13px;
color: #7d8ec2;
color: #333;
font-family: ${theme.typography.button.fontFamily};
`;
const HexLabel = styled.div`
margin: auto 10px auto 0px;
margin: auto 0px;
font-size: 12px;
display: inline-flex;
font-family: ${theme.typography.button.fontFamily};
@@ -227,15 +238,22 @@ const HexLabel = styled.div`
const HexColorInputBox = styled.div`
display: inline-flex;
flex-grow: 1;
margin-right: 10px;
width: 140px;
height: 28px;
border: 1px solid ${theme.palette.grey["600"]};
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 5px;
&:hover {
border: 1px solid ${theme.palette.grey["600"]};
}
&:focus-within {
outline: 2px solid ${theme.palette.secondary.main};
outline-offset: 1px;
}
`;
const HexWrapper = styled.div`
display: flex;
gap: 8px;
flex-grow: 1;
& input {
min-width: 0px;
@@ -259,7 +277,7 @@ const Swatch = styled.div<{ $color: string }>`
display: inline-flex;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["600"]};`;
return `border: 1px solid ${theme.palette.grey["300"]};`;
}
return `border: 1px solid ${$color};`;
}}
@@ -273,7 +291,8 @@ const ColorPickerInput = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-top: 15px;
margin: 8px;
gap: 8px;
`;
export default ColorPicker;

View File

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

View File

@@ -7,6 +7,16 @@ import {
} from "@ironcalc/wasm";
import type { ActiveRange } from "../workbookState";
function sliceString(
text: string,
startScalar: number,
endScalar: number,
): string {
const scalarValues = Array.from(text);
const sliced = scalarValues.slice(startScalar, endScalar);
return sliced.join("");
}
export function tokenIsReferenceType(token: TokenType): token is Reference {
return typeof token === "object" && "Reference" in token;
}
@@ -127,7 +137,7 @@ function getFormulaHTML(
}
html.push(
<span key={index} style={{ color }}>
{formula.slice(start, end)}
{sliceString(formula, start, end)}
</span>,
);
activeRanges.push({
@@ -162,7 +172,7 @@ function getFormulaHTML(
}
html.push(
<span key={index} style={{ color }}>
{formula.slice(start, end)}
{sliceString(formula, start, end)}
</span>,
);
colorCount += 1;
@@ -176,7 +186,7 @@ function getFormulaHTML(
color,
});
} else {
html.push(<span key={index}>{formula.slice(start, end)}</span>);
html.push(<span key={index}>{sliceString(formula, start, end)}</span>);
}
}
html = [<span key="equals">=</span>].concat(html);

View File

@@ -34,11 +34,18 @@ const FormatMenu = (properties: FormatMenuProps) => {
>
{properties.children}
</ChildrenWrapper>
<Menu
<StyledMenu
open={isMenuOpen}
onClose={(): void => setMenuOpen(false)}
anchorEl={anchorElement.current}
anchorOrigin={properties.anchorOrigin}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<MenuItemWrapper onClick={(): void => onSelect(NumberFormats.AUTO)}>
<MenuItemText>{t("toolbar.format_menu.auto")}</MenuItemText>
@@ -107,7 +114,7 @@ const FormatMenu = (properties: FormatMenuProps) => {
<MenuItemWrapper onClick={(): void => setPickerOpen(true)}>
<MenuItemText>{t("toolbar.format_menu.custom")}</MenuItemText>
</MenuItemWrapper>
</Menu>
</StyledMenu>
<FormatPicker
numFmt={properties.numFmt}
onChange={onSelect}
@@ -119,18 +126,40 @@ const FormatMenu = (properties: FormatMenuProps) => {
);
};
const StyledMenu = styled(Menu)`
& .MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
margin-left: -4px; // Starting with a small offset
}
& .MuiList-root {
padding: 0;
}
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
justify-content: space-between;
font-size: 12px;
width: 100%;
width: calc(100% - 8px);
min-width: 172px;
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
`;
const ChildrenWrapper = styled("div")`
display: flex;
`;
const MenuDivider = styled("div")``;
const MenuDivider = styled("div")`
width: 100%;
margin: auto;
margin-top: 4px;
margin-bottom: 4px;
border-top: 1px solid #eeeeee;
`;
const MenuItemText = styled("div")`
color: #000;

View File

@@ -1,13 +1,9 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
import styled from "@emotion/styled";
import { Dialog, TextField } from "@mui/material";
import { Check } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../theme";
type FormatPickerProps = {
className?: string;
@@ -22,17 +18,53 @@ const FormatPicker = (properties: FormatPickerProps) => {
const { t } = useTranslation();
const [formatCode, setFormatCode] = useState(properties.numFmt);
const handleClose = () => {
properties.onClose();
};
const onSubmit = (format_code: string): void => {
properties.onChange(format_code);
properties.onClose();
};
return (
<Dialog open={properties.open} onClose={properties.onClose}>
<DialogTitle>{t("num_fmt.title")}</DialogTitle>
<DialogContent dividers>
<TextField
<Dialog
open={properties.open}
onClose={properties.onClose}
PaperProps={{
style: { minWidth: "280px" },
}}
>
<StyledDialogTitle>
{t("num_fmt.title")}
<Cross onClick={handleClose} onKeyDown={() => {}}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross>
</StyledDialogTitle>
<StyledDialogContent>
<StyledTextField
autoFocus
defaultValue={properties.numFmt}
label={t("num_fmt.label")}
name="format_code"
onChange={(event) => setFormatCode(event.target.value)}
onKeyDown={(event) => {
@@ -40,15 +72,94 @@ const FormatPicker = (properties: FormatPickerProps) => {
}}
spellCheck="false"
onClick={(event) => event.stopPropagation()}
onFocus={(event) => event.target.select()}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => onSubmit(formatCode)}>
</StyledDialogContent>
<DialogFooter>
<StyledButton onClick={() => onSubmit(formatCode)}>
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
{t("num_fmt.save")}
</Button>
</DialogActions>
</StyledButton>
</DialogFooter>
</Dialog>
);
};
const StyledDialogTitle = styled("div")`
display: flex;
align-items: center;
height: 44px;
font-size: 14px;
font-weight: 500;
font-family: Inter;
padding: 0px 12px;
justify-content: space-between;
border-bottom: 1px solid ${theme.palette.grey["300"]};
`;
const Cross = styled("div")`
&:hover {
background-color: ${theme.palette.grey["100"]};
}
display: flex;
border-radius: 4px;
height: 24px;
width: 24px;
cursor: pointer;
align-items: center;
justify-content: center;
`;
const StyledDialogContent = styled("div")`
font-size: 12px;
margin: 12px;
`;
const StyledTextField = styled(TextField)`
width: 100%;
min-width: 320px;
border-radius: 4px;
overflow: hidden;
& .MuiInputBase-input {
font-size: 14px;
padding: 10px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 4px;
color: ${theme.palette.common.black};
background-color: ${theme.palette.common.white};
}
&:hover .MuiInputBase-input {
border: 1px solid ${theme.palette.grey["500"]};
}
`;
const DialogFooter = styled("div")`
color: #757575;
display: flex;
align-items: center;
border-top: 1px solid ${theme.palette.grey["300"]};
font-family: Inter;
justify-content: flex-end;
padding: 12px;
`;
const StyledButton = styled("div")`
cursor: pointer;
color: #ffffff;
background: #f2994a;
padding: 0px 10px;
height: 36px;
line-height: 36px;
border-radius: 4px;
display: flex;
align-items: center;
font-family: "Inter";
font-size: 14px;
&:hover {
background: #d68742;
}
`;
export default FormatPicker;

View File

@@ -5,7 +5,7 @@ import {
COLUMN_WIDTH_SCALE,
ROW_HEIGH_SCALE,
} from "./WorksheetCanvas/constants";
import { FORMULA_BAR_HEIGH } from "./constants";
import { FORMULA_BAR_HEIGHT } from "./constants";
import Editor from "./editor/editor";
import type { WorkbookState } from "./workbookState";
@@ -122,7 +122,7 @@ const Container = styled("div")`
align-items: center;
background: ${(properties): string =>
properties.theme.palette.background.default};
height: ${FORMULA_BAR_HEIGH}px;
height: ${FORMULA_BAR_HEIGHT}px;
`;
const AddressContainer = styled("div")`

View File

@@ -1,17 +1,10 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
styled,
} from "@mui/material";
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 {
@@ -28,32 +21,65 @@ interface SheetRenameDialogProps {
export const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
const { t } = useTranslation();
const [name, setName] = useState(properties.defaultName);
const handleClose = () => {
properties.close();
};
return (
<Dialog open={properties.isOpen} onClose={properties.close}>
<DialogTitle>{t("sheet_rename.title")}</DialogTitle>
<DialogContent dividers>
<TextField
<StyledDialogTitle>
{t("sheet_rename.title")}
<Cross onClick={handleClose} onKeyDown={() => {}}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross>
</StyledDialogTitle>
<StyledDialogContent>
<StyledTextField
autoFocus
defaultValue={properties.defaultName}
label={t("sheet_rename.label")}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
properties.onNameChanged(name);
properties.close();
}
}}
onChange={(event) => {
setName(event.target.value);
}}
spellCheck="false"
onPaste={(event) => event.stopPropagation()}
/>
</DialogContent>
<DialogActions>
<Button
</StyledDialogContent>
<DialogFooter>
<StyledButton
onClick={() => {
properties.onNameChanged(name);
}}
>
{t("sheet_rename.rename")}
</Button>
</DialogActions>
</StyledButton>
</DialogFooter>
</Dialog>
);
};
@@ -146,4 +172,78 @@ const ItemName = styled("div")`
color: #333;
`;
const StyledDialogTitle = styled("div")`
display: flex;
align-items: center;
height: 44px;
font-size: 14px;
font-weight: 500;
font-family: Inter;
padding: 0px 12px;
justify-content: space-between;
border-bottom: 1px solid ${theme.palette.grey["300"]};
`;
const Cross = styled("div")`
&:hover {
background-color: ${theme.palette.grey["100"]};
}
display: flex;
border-radius: 4px;
height: 24px;
width: 24px;
cursor: pointer;
align-items: center;
justify-content: center;
`;
const StyledDialogContent = styled("div")`
font-size: 12px;
margin: 12px;
`;
const StyledTextField = styled(TextField)`
width: 100%;
border-radius: 4px;
overflow: hidden;
& .MuiInputBase-input {
font-size: 14px;
padding: 10px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 4px;
color: ${theme.palette.common.black};
background-color: ${theme.palette.common.white};
}
&:hover .MuiInputBase-input {
border: 1px solid ${theme.palette.grey["500"]};
}
`;
const DialogFooter = styled("div")`
color: #757575;
display: flex;
align-items: center;
border-top: 1px solid ${theme.palette.grey["300"]};
font-family: Inter;
justify-content: flex-end;
padding: 12px;
`;
const StyledButton = styled("div")`
cursor: pointer;
color: #ffffff;
background: #f2994a;
padding: 0px 10px;
height: 36px;
line-height: 36px;
border-radius: 4px;
display: flex;
align-items: center;
font-family: "Inter";
font-size: 14px;
&:hover {
background: #d68742;
}
`;
export default SheetListMenu;

View File

@@ -2,7 +2,8 @@ import { styled } from "@mui/material";
import { Menu, Plus } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { NAVIGATION_HEIGH } from "../constants";
import { theme } from "../../theme";
import { NAVIGATION_HEIGHT } from "../constants";
import { StyledButton } from "../toolbar";
import type { WorkbookState } from "../workbookState";
import SheetListMenu from "./menus";
@@ -96,17 +97,19 @@ const Container = styled("div")`
left: 0px;
right: 0px;
display: flex;
height: ${NAVIGATION_HEIGH}px;
height: ${NAVIGATION_HEIGHT}px;
align-items: center;
padding-left: 12px;
font-family: Inter;
background-color: #fff;
border-top: 1px solid #E0E0E0;
border-top: 1px solid #e0e0e0;
`;
const Sheets = styled("div")`
flex-grow: 2;
overflow: hidden;
overflow-x: auto;
scrollbar-width: none;
`;
const SheetInner = styled("div")`
@@ -114,10 +117,20 @@ const SheetInner = styled("div")`
`;
const Advert = styled("a")`
display: flex;
align-items: center;
color: #f2994a;
margin-right: 12px;
padding: 0px 12px;
font-size: 12px;
text-decoration: none;
border-left: 1px solid ${theme.palette.grey["300"]};
transition: color 0.2s ease-in-out;
&:hover {
text-decoration: underline;
}
@media (max-width: 769px) {
height: 100%;
}
`;
export default Navigation;

View File

@@ -36,7 +36,7 @@ import {
import { theme } from "../theme";
import BorderPicker from "./borderPicker";
import ColorPicker from "./colorPicker";
import { TOOLBAR_HEIGH } from "./constants";
import { TOOLBAR_HEIGHT } from "./constants";
import FormatMenu from "./formatMenu";
import {
NumberFormats,
@@ -251,6 +251,16 @@ function Toolbar(properties: ToolbarProperties) {
>
<PaintBucket />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
onClick={() => setBorderPickerOpen(true)}
ref={borderButton}
disabled={!canEdit}
title={t("toolbar.borders.title")}
>
<Grid2X2 />
</StyledButton>
<Divider />
<StyledButton
type="button"
@@ -318,17 +328,7 @@ function Toolbar(properties: ToolbarProperties) {
>
<ArrowDownToLine />
</StyledButton>
<Divider />
<StyledButton
type="button"
$pressed={false}
onClick={() => setBorderPickerOpen(true)}
ref={borderButton}
disabled={!canEdit}
title={t("toolbar.borders.title")}
>
<Grid2X2 />
</StyledButton>
<Divider />
<StyledButton
type="button"
@@ -385,8 +385,8 @@ const ToolbarContainer = styled("div")`
flex-shrink: 0;
align-items: center;
background: ${({ theme }) => theme.palette.background.paper};
height: ${TOOLBAR_HEIGH}px;
line-height: ${TOOLBAR_HEIGH}px;
height: ${TOOLBAR_HEIGHT}px;
line-height: ${TOOLBAR_HEIGHT}px;
border-bottom: 1px solid ${({ theme }) => theme.palette.grey["300"]};
font-family: Inter;
border-radius: 4px 4px 0px 0px;

View File

@@ -530,7 +530,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
);
setRedrawId((id) => id + 1);
}}
fillColor={style.fill.fg_color || "#FFF"}
fillColor={style.fill.fg_color || "#FFFFFF"}
fontColor={style.font.color}
bold={style.font.b}
underline={style.font.u}

View File

@@ -9,9 +9,9 @@ import {
} from "./WorksheetCanvas/constants";
import WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
import {
FORMULA_BAR_HEIGH,
NAVIGATION_HEIGH,
TOOLBAR_HEIGH,
FORMULA_BAR_HEIGHT,
NAVIGATION_HEIGHT,
TOOLBAR_HEIGHT,
} from "./constants";
import Editor from "./editor/editor";
import type { Cell } from "./types";
@@ -104,10 +104,16 @@ function Worksheet(props: {
editor: editor,
},
onColumnWidthChanges(sheet, column, width) {
if (width < 0) {
return;
}
model.setColumnWidth(sheet, column, width);
worksheetCanvas.current?.renderSheet();
},
onRowHeightChanges(sheet, row, height) {
if (height < 0) {
return;
}
model.setRowHeight(sheet, row, height);
worksheetCanvas.current?.renderSheet();
},
@@ -433,10 +439,10 @@ const SheetContainer = styled("div")`
const Wrapper = styled("div")({
position: "absolute",
overflow: "scroll",
top: TOOLBAR_HEIGH + FORMULA_BAR_HEIGH + 1,
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1,
left: 0,
right: 0,
bottom: NAVIGATION_HEIGH + 1,
bottom: NAVIGATION_HEIGHT + 1,
overscrollBehavior: "none",
});

Binary file not shown.

Binary file not shown.

Binary file not shown.