Compare commits
38 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcfee794b9 | ||
|
|
91eb66993d | ||
|
|
87e8b7a20b | ||
|
|
97b27006cf | ||
|
|
b7f7e73824 | ||
|
|
ea194ee730 | ||
|
|
cbb413f100 | ||
|
|
a4cf93c49a | ||
|
|
70366ea60c | ||
|
|
9aa1b4574e | ||
|
|
82b2d28663 | ||
|
|
d2ba34166b | ||
|
|
99d42cb1e2 | ||
|
|
ddc785e7a6 | ||
|
|
8ab1382e75 | ||
|
|
ec5714e3ec | ||
|
|
4660f0e456 | ||
|
|
f2757e7d76 | ||
|
|
5ca15033f7 | ||
|
|
75e04696b5 | ||
|
|
832ca02e16 | ||
|
|
cbda30f951 | ||
|
|
564d4bac7a | ||
|
|
0dd26e8fee | ||
|
|
f6fbb4b303 | ||
|
|
c6adf8449b | ||
|
|
d04691b790 | ||
|
|
7c32088480 | ||
|
|
6326c44941 | ||
|
|
d3af994866 | ||
|
|
b859af1dc4 | ||
|
|
f9cfdeb35b | ||
|
|
669a5eec39 | ||
|
|
e268dda9e8 | ||
|
|
e0205d6c9a | ||
|
|
81ad724348 | ||
|
|
dc3bf8826b | ||
|
|
38023d3156 |
@@ -7,6 +7,7 @@
|
|||||||
- New function UNICODE ([#128](https://github.com/ironcalc/IronCalc/pull/128))
|
- New function UNICODE ([#128](https://github.com/ironcalc/IronCalc/pull/128))
|
||||||
- New document server (Thanks Dani!)
|
- New document server (Thanks Dani!)
|
||||||
- New function FORMULATEXT
|
- New function FORMULATEXT
|
||||||
|
- Name Manager ([#212](https://github.com/ironcalc/IronCalc/pull/212) [#220](https://github.com/ironcalc/IronCalc/pull/220))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,10 @@ pub(crate) const LAST_ROW: i32 = 1_048_576;
|
|||||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||||
// The 2 days offset is because of Excel 1900 bug
|
// The 2 days offset is because of Excel 1900 bug
|
||||||
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
|
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
|
||||||
|
|
||||||
|
// We do not support dates before 1899-12-31.
|
||||||
|
pub(crate) const MINIMUM_DATE_SERIAL_NUMBER: i32 = 1;
|
||||||
|
|
||||||
|
// Excel can handle dates until the year 9999-12-31
|
||||||
|
// 2958465 is the number of days from 1900-01-01 to 9999-12-31
|
||||||
|
pub(crate) const MAXIMUM_DATE_SERIAL_NUMBER: i32 = 2_958_465;
|
||||||
|
|||||||
@@ -642,7 +642,38 @@ impl Parser {
|
|||||||
position: 0,
|
position: 0,
|
||||||
message: "Unexpected end of input.".to_string(),
|
message: "Unexpected end of input.".to_string(),
|
||||||
},
|
},
|
||||||
TokenType::Boolean(value) => Node::BooleanKind(value),
|
TokenType::Boolean(value) => {
|
||||||
|
// Could be a function call "TRUE()"
|
||||||
|
let next_token = self.lexer.peek_token();
|
||||||
|
if next_token == TokenType::LeftParenthesis {
|
||||||
|
self.lexer.advance_token();
|
||||||
|
// We parse all the arguments, although technically this is moot
|
||||||
|
// But is has the upside of transforming `=TRUE( 4 )` into `=TRUE(4)`
|
||||||
|
let args = match self.parse_function_args() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
|
||||||
|
return Node::ParseErrorKind {
|
||||||
|
formula: self.lexer.get_formula(),
|
||||||
|
position: err.position,
|
||||||
|
message: err.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if value {
|
||||||
|
return Node::FunctionKind {
|
||||||
|
kind: Function::True,
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Node::FunctionKind {
|
||||||
|
kind: Function::False,
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::BooleanKind(value)
|
||||||
|
}
|
||||||
TokenType::Compare(_) => {
|
TokenType::Compare(_) => {
|
||||||
// A primary Node cannot start with an operator
|
// A primary Node cannot start with an operator
|
||||||
Node::ParseErrorKind {
|
Node::ParseErrorKind {
|
||||||
|
|||||||
@@ -1,18 +1,158 @@
|
|||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
|
use chrono::Days;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use chrono::Months;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use crate::constants::EXCEL_DATE_BASE;
|
use crate::constants::EXCEL_DATE_BASE;
|
||||||
|
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
|
||||||
|
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
|
||||||
|
|
||||||
pub fn from_excel_date(days: i64) -> NaiveDate {
|
#[inline]
|
||||||
|
fn convert_to_serial_number(date: NaiveDate) -> i32 {
|
||||||
|
date.num_days_from_ce() - EXCEL_DATE_BASE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_date_within_range(date: NaiveDate) -> bool {
|
||||||
|
convert_to_serial_number(date) >= MINIMUM_DATE_SERIAL_NUMBER
|
||||||
|
&& convert_to_serial_number(date) <= MAXIMUM_DATE_SERIAL_NUMBER
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
||||||
|
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
|
return Err(format!(
|
||||||
|
"Excel date must be greater than {}",
|
||||||
|
MINIMUM_DATE_SERIAL_NUMBER
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
|
return Err(format!(
|
||||||
|
"Excel date must be less than {}",
|
||||||
|
MAXIMUM_DATE_SERIAL_NUMBER
|
||||||
|
));
|
||||||
|
};
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate");
|
let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate");
|
||||||
dt + Duration::days(days - 2)
|
Ok(dt + Duration::days(days - 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
|
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
|
||||||
match NaiveDate::from_ymd_opt(year, month, day) {
|
match NaiveDate::from_ymd_opt(year, month, day) {
|
||||||
Some(native_date) => Ok(native_date.num_days_from_ce() - EXCEL_DATE_BASE),
|
Some(native_date) => Ok(convert_to_serial_number(native_date)),
|
||||||
None => Err("Out of range parameters for date".to_string()),
|
None => Err("Out of range parameters for date".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Result<i32, String> {
|
||||||
|
// Excel parses `DATE` very permissively. It allows not just for valid date values, but it
|
||||||
|
// allows for invalid dates as well. If you for example enter `DATE(1900, 1, 32)` it will
|
||||||
|
// return the date `1900-02-01`. Despite giving a day that is out of range it will just
|
||||||
|
// wrap the month and year around.
|
||||||
|
//
|
||||||
|
// This function applies that same logic to dates. And does it in the most compatible way as
|
||||||
|
// possible.
|
||||||
|
|
||||||
|
// Special case for the minimum date
|
||||||
|
if year == 1899 && month == 12 && day == 31 {
|
||||||
|
return Ok(MINIMUM_DATE_SERIAL_NUMBER);
|
||||||
|
}
|
||||||
|
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
|
||||||
|
return Err("Out of range parameters for date".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
// One thing to note for example is that even if you started with a year out of range
|
||||||
|
// but tried to increment the months so that it wraps around into within range, excel
|
||||||
|
// would still return an error.
|
||||||
|
//
|
||||||
|
// I.E. DATE(0,13,-1) will return an error, despite it being equivalent to DATE(1,1,0) which
|
||||||
|
// is within range.
|
||||||
|
//
|
||||||
|
// As a result, we have to run range checks as we parse the date from the biggest unit to the
|
||||||
|
// smallest unit.
|
||||||
|
if !is_date_within_range(date) {
|
||||||
|
return Err("Out of range parameters for date".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
date = {
|
||||||
|
let month_diff = month - 1;
|
||||||
|
let abs_month = month_diff.unsigned_abs();
|
||||||
|
if month_diff <= 0 {
|
||||||
|
date = date - Months::new(abs_month);
|
||||||
|
} else {
|
||||||
|
date = date + Months::new(abs_month);
|
||||||
|
}
|
||||||
|
if !is_date_within_range(date) {
|
||||||
|
return Err("Out of range parameters for date".to_string());
|
||||||
|
}
|
||||||
|
date
|
||||||
|
};
|
||||||
|
|
||||||
|
date = {
|
||||||
|
let day_diff = day - 1;
|
||||||
|
let abs_day = day_diff.unsigned_abs() as u64;
|
||||||
|
if day_diff <= 0 {
|
||||||
|
date = date - Days::new(abs_day);
|
||||||
|
} else {
|
||||||
|
date = date + Days::new(abs_day);
|
||||||
|
}
|
||||||
|
if !is_date_within_range(date) {
|
||||||
|
return Err("Out of range parameters for date".to_string());
|
||||||
|
}
|
||||||
|
date
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(convert_to_serial_number(date))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permissive_date_to_serial_number() {
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(42, 42, 2002),
|
||||||
|
date_to_serial_number(12, 7, 2005)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(1, 42, 2002),
|
||||||
|
date_to_serial_number(1, 6, 2005)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(1, 15, 2000),
|
||||||
|
date_to_serial_number(1, 3, 2001)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(1, 49, 2000),
|
||||||
|
date_to_serial_number(1, 1, 2004)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(1, 49, 2000),
|
||||||
|
date_to_serial_number(1, 1, 2004)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(31, 49, 2000),
|
||||||
|
date_to_serial_number(31, 1, 2004)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(256, 49, 2000),
|
||||||
|
date_to_serial_number(12, 9, 2004)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(256, 1, 2004),
|
||||||
|
date_to_serial_number(12, 9, 2004)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_and_min_dates() {
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(31, 12, 9999),
|
||||||
|
Ok(MAXIMUM_DATE_SERIAL_NUMBER),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
permissive_date_to_serial_number(31, 12, 1899),
|
||||||
|
Ok(MINIMUM_DATE_SERIAL_NUMBER),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -154,15 +154,16 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
ParsePart::Date(p) => {
|
ParsePart::Date(p) => {
|
||||||
let tokens = &p.tokens;
|
let tokens = &p.tokens;
|
||||||
let mut text = "".to_string();
|
let mut text = "".to_string();
|
||||||
if !(1.0..=2_958_465.0).contains(&value) {
|
let date = match from_excel_date(value as i64) {
|
||||||
// 2_958_465 is 31 December 9999
|
Ok(d) => d,
|
||||||
return Formatted {
|
Err(e) => {
|
||||||
text: "#VALUE!".to_owned(),
|
return Formatted {
|
||||||
color: None,
|
text: "#VALUE!".to_owned(),
|
||||||
error: Some("Date negative or too long".to_owned()),
|
color: None,
|
||||||
};
|
error: Some(e),
|
||||||
}
|
}
|
||||||
let date = from_excel_date(value as i64);
|
}
|
||||||
|
};
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
match token {
|
match token {
|
||||||
TextToken::Literal(c) => {
|
TextToken::Literal(c) => {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ use chrono::Datelike;
|
|||||||
use chrono::Months;
|
use chrono::Months;
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
|
|
||||||
|
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
|
||||||
|
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
use crate::formatter::dates::date_to_serial_number;
|
use crate::formatter::dates::date_to_serial_number;
|
||||||
|
use crate::formatter::dates::permissive_date_to_serial_number;
|
||||||
use crate::model::get_milliseconds_since_epoch;
|
use crate::model::get_milliseconds_since_epoch;
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult, constants::EXCEL_DATE_BASE, expressions::parser::Node,
|
calc_result::CalcResult, constants::EXCEL_DATE_BASE, expressions::parser::Node,
|
||||||
@@ -18,20 +21,19 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let serial_number = match self.get_number(&args[0], cell) {
|
let serial_number = match self.get_number(&args[0], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => c.floor() as i64,
|
||||||
let t = c.floor() as i64;
|
|
||||||
if t < 0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NUM,
|
|
||||||
origin: cell,
|
|
||||||
message: "Function DAY parameter 1 value is negative. It should be positive or zero.".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
t
|
|
||||||
}
|
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let date = from_excel_date(serial_number);
|
let date = match from_excel_date(serial_number) {
|
||||||
|
Ok(date) => date,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Out of range parameters for date".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
let day = date.day() as f64;
|
let day = date.day() as f64;
|
||||||
CalcResult::Number(day)
|
CalcResult::Number(day)
|
||||||
}
|
}
|
||||||
@@ -42,20 +44,19 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let serial_number = match self.get_number(&args[0], cell) {
|
let serial_number = match self.get_number(&args[0], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => c.floor() as i64,
|
||||||
let t = c.floor() as i64;
|
|
||||||
if t < 0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NUM,
|
|
||||||
origin: cell,
|
|
||||||
message: "Function MONTH parameter 1 value is negative. It should be positive or zero.".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
t
|
|
||||||
}
|
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let date = from_excel_date(serial_number);
|
let date = match from_excel_date(serial_number) {
|
||||||
|
Ok(date) => date,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Out of range parameters for date".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
let month = date.month() as f64;
|
let month = date.month() as f64;
|
||||||
CalcResult::Number(month)
|
CalcResult::Number(month)
|
||||||
}
|
}
|
||||||
@@ -79,6 +80,23 @@ impl Model {
|
|||||||
}
|
}
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
|
let date = match from_excel_date(serial_number) {
|
||||||
|
Ok(date) => date,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Out of range parameters for date".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Function DAY parameter 1 value is too large.".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let months = match self.get_number_no_bools(&args[1], cell) {
|
let months = match self.get_number_no_bools(&args[1], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
@@ -91,9 +109,9 @@ impl Model {
|
|||||||
let months_abs = months.unsigned_abs();
|
let months_abs = months.unsigned_abs();
|
||||||
|
|
||||||
let native_date = if months > 0 {
|
let native_date = if months > 0 {
|
||||||
from_excel_date(serial_number) + Months::new(months_abs)
|
date + Months::new(months_abs)
|
||||||
} else {
|
} else {
|
||||||
from_excel_date(serial_number) - Months::new(months_abs)
|
date - Months::new(months_abs)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Instead of calculating the end of month we compute the first day of the following month
|
// Instead of calculating the end of month we compute the first day of the following month
|
||||||
@@ -137,32 +155,18 @@ impl Model {
|
|||||||
let month = match self.get_number(&args[1], cell) {
|
let month = match self.get_number(&args[1], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
let t = c.floor();
|
let t = c.floor();
|
||||||
if t < 0.0 {
|
t as i32
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NUM,
|
|
||||||
origin: cell,
|
|
||||||
message: "Out of range parameters for date".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
t as u32
|
|
||||||
}
|
}
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let day = match self.get_number(&args[2], cell) {
|
let day = match self.get_number(&args[2], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
let t = c.floor();
|
let t = c.floor();
|
||||||
if t < 0.0 {
|
t as i32
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NUM,
|
|
||||||
origin: cell,
|
|
||||||
message: "Out of range parameters for date".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
t as u32
|
|
||||||
}
|
}
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
match date_to_serial_number(day, month, year) {
|
match permissive_date_to_serial_number(day, month, year) {
|
||||||
Ok(serial_number) => CalcResult::Number(serial_number as f64),
|
Ok(serial_number) => CalcResult::Number(serial_number as f64),
|
||||||
Err(message) => CalcResult::Error {
|
Err(message) => CalcResult::Error {
|
||||||
error: Error::NUM,
|
error: Error::NUM,
|
||||||
@@ -178,20 +182,19 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let serial_number = match self.get_number(&args[0], cell) {
|
let serial_number = match self.get_number(&args[0], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => c.floor() as i64,
|
||||||
let t = c.floor() as i64;
|
|
||||||
if t < 0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NUM,
|
|
||||||
origin: cell,
|
|
||||||
message: "Function YEAR parameter 1 value is negative. It should be positive or zero.".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
t
|
|
||||||
}
|
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let date = from_excel_date(serial_number);
|
let date = match from_excel_date(serial_number) {
|
||||||
|
Ok(date) => date,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Out of range parameters for date".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
let year = date.year() as f64;
|
let year = date.year() as f64;
|
||||||
CalcResult::Number(year)
|
CalcResult::Number(year)
|
||||||
}
|
}
|
||||||
@@ -203,20 +206,19 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let serial_number = match self.get_number(&args[0], cell) {
|
let serial_number = match self.get_number(&args[0], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => c.floor() as i64,
|
||||||
let t = c.floor() as i64;
|
|
||||||
if t < 0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NUM,
|
|
||||||
origin: cell,
|
|
||||||
message: "Parameter 1 value is negative. It should be positive or zero."
|
|
||||||
.to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
t
|
|
||||||
}
|
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
|
let date = match from_excel_date(serial_number) {
|
||||||
|
Ok(date) => date,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Out of range parameters for date".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let months = match self.get_number(&args[1], cell) {
|
let months = match self.get_number(&args[1], cell) {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
@@ -229,13 +231,13 @@ impl Model {
|
|||||||
let months_abs = months.unsigned_abs();
|
let months_abs = months.unsigned_abs();
|
||||||
|
|
||||||
let native_date = if months > 0 {
|
let native_date = if months > 0 {
|
||||||
from_excel_date(serial_number) + Months::new(months_abs)
|
date + Months::new(months_abs)
|
||||||
} else {
|
} else {
|
||||||
from_excel_date(serial_number) - Months::new(months_abs)
|
date - Months::new(months_abs)
|
||||||
};
|
};
|
||||||
|
|
||||||
let serial_number = native_date.num_days_from_ce() - EXCEL_DATE_BASE;
|
let serial_number = native_date.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||||
if serial_number < 0 {
|
if serial_number < MINIMUM_DATE_SERIAL_NUMBER {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::NUM,
|
error: Error::NUM,
|
||||||
origin: cell,
|
origin: cell,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::Datelike;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult,
|
calc_result::CalcResult,
|
||||||
constants::{LAST_COLUMN, LAST_ROW},
|
constants::{LAST_COLUMN, LAST_ROW, MAXIMUM_DATE_SERIAL_NUMBER, MINIMUM_DATE_SERIAL_NUMBER},
|
||||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||||
formatter::dates::from_excel_date,
|
formatter::dates::from_excel_date,
|
||||||
model::Model,
|
model::Model,
|
||||||
@@ -13,37 +13,38 @@ use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr
|
|||||||
// See:
|
// See:
|
||||||
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
|
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
|
||||||
|
|
||||||
// FIXME: Is this enough?
|
fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> {
|
||||||
fn is_valid_date(date: f64) -> bool {
|
let end = match from_excel_date(end_date) {
|
||||||
date > 0.0
|
Ok(s) => s,
|
||||||
}
|
Err(s) => return Err(s),
|
||||||
|
};
|
||||||
fn is_less_than_one_year(start_date: i64, end_date: i64) -> bool {
|
let start = match from_excel_date(start_date) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return Err(s),
|
||||||
|
};
|
||||||
if end_date - start_date < 365 {
|
if end_date - start_date < 365 {
|
||||||
return true;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
let end = from_excel_date(end_date);
|
|
||||||
let start = from_excel_date(start_date);
|
|
||||||
let end_year = end.year();
|
let end_year = end.year();
|
||||||
let start_year = start.year();
|
let start_year = start.year();
|
||||||
if end_year == start_year {
|
if end_year == start_year {
|
||||||
return true;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
if end_year != start_year + 1 {
|
if end_year != start_year + 1 {
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let start_month = start.month();
|
let start_month = start.month();
|
||||||
let end_month = end.month();
|
let end_month = end.month();
|
||||||
if end_month < start_month {
|
if end_month < start_month {
|
||||||
return true;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
if end_month > start_month {
|
if end_month > start_month {
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
// we are one year later same month
|
// we are one year later same month
|
||||||
let start_day = start.day();
|
let start_day = start.day();
|
||||||
let end_day = end.day();
|
let end_day = end.day();
|
||||||
end_day <= start_day
|
Ok(end_day <= start_day)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_payment(
|
fn compute_payment(
|
||||||
@@ -436,7 +437,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
if rate == -1.0 {
|
if rate == -1.0 {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::NUM,
|
error: Error::DIV,
|
||||||
origin: cell,
|
origin: cell,
|
||||||
message: "Rate must be != -1".to_string(),
|
message: "Rate must be != -1".to_string(),
|
||||||
};
|
};
|
||||||
@@ -923,7 +924,9 @@ impl Model {
|
|||||||
}
|
}
|
||||||
let first_date = dates[0];
|
let first_date = dates[0];
|
||||||
for date in &dates {
|
for date in &dates {
|
||||||
if !is_valid_date(*date) {
|
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|
||||||
|
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
||||||
|
{
|
||||||
// Excel docs claim that if any number in dates is not a valid date,
|
// Excel docs claim that if any number in dates is not a valid date,
|
||||||
// XNPV returns the #VALUE! error value, but it seems to return #VALUE!
|
// XNPV returns the #VALUE! error value, but it seems to return #VALUE!
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
@@ -989,7 +992,9 @@ impl Model {
|
|||||||
}
|
}
|
||||||
let first_date = dates[0];
|
let first_date = dates[0];
|
||||||
for date in &dates {
|
for date in &dates {
|
||||||
if !is_valid_date(*date) {
|
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|
||||||
|
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
||||||
|
{
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
cell,
|
cell,
|
||||||
@@ -1373,9 +1378,10 @@ impl Model {
|
|||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
if !is_valid_date(settlement) || !is_valid_date(maturity) {
|
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
|
Ok(f) => f,
|
||||||
}
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
||||||
|
};
|
||||||
if settlement > maturity {
|
if settlement > maturity {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
@@ -1383,7 +1389,7 @@ impl Model {
|
|||||||
"settlement should be <= maturity".to_string(),
|
"settlement should be <= maturity".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !is_less_than_one_year(settlement as i64, maturity as i64) {
|
if !less_than_one_year {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
cell,
|
cell,
|
||||||
@@ -1437,9 +1443,10 @@ impl Model {
|
|||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
if !is_valid_date(settlement) || !is_valid_date(maturity) {
|
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
|
Ok(f) => f,
|
||||||
}
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
||||||
|
};
|
||||||
if settlement > maturity {
|
if settlement > maturity {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
@@ -1447,7 +1454,7 @@ impl Model {
|
|||||||
"settlement should be <= maturity".to_string(),
|
"settlement should be <= maturity".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !is_less_than_one_year(settlement as i64, maturity as i64) {
|
if !less_than_one_year {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
cell,
|
cell,
|
||||||
@@ -1487,9 +1494,10 @@ impl Model {
|
|||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
if !is_valid_date(settlement) || !is_valid_date(maturity) {
|
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
|
Ok(f) => f,
|
||||||
}
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
||||||
|
};
|
||||||
if settlement > maturity {
|
if settlement > maturity {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
@@ -1497,7 +1505,7 @@ impl Model {
|
|||||||
"settlement should be <= maturity".to_string(),
|
"settlement should be <= maturity".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !is_less_than_one_year(settlement as i64, maturity as i64) {
|
if !less_than_one_year {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
Error::NUM,
|
Error::NUM,
|
||||||
cell,
|
cell,
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ use crate::{
|
|||||||
use super::util::compare_values;
|
use super::util::compare_values;
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
|
pub(crate) fn fn_true(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
CalcResult::Boolean(true)
|
||||||
|
} else {
|
||||||
|
CalcResult::new_args_number_error(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_false(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
CalcResult::Boolean(false)
|
||||||
|
} else {
|
||||||
|
CalcResult::new_args_number_error(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 2 || args.len() == 3 {
|
if args.len() == 2 || args.len() == 3 {
|
||||||
let cond_result = self.get_boolean(&args[0], cell);
|
let cond_result = self.get_boolean(&args[0], cell);
|
||||||
|
|||||||
@@ -949,7 +949,7 @@ impl Model {
|
|||||||
match kind {
|
match kind {
|
||||||
// Logical
|
// Logical
|
||||||
Function::And => self.fn_and(args, cell),
|
Function::And => self.fn_and(args, cell),
|
||||||
Function::False => CalcResult::Boolean(false),
|
Function::False => self.fn_false(args, cell),
|
||||||
Function::If => self.fn_if(args, cell),
|
Function::If => self.fn_if(args, cell),
|
||||||
Function::Iferror => self.fn_iferror(args, cell),
|
Function::Iferror => self.fn_iferror(args, cell),
|
||||||
Function::Ifna => self.fn_ifna(args, cell),
|
Function::Ifna => self.fn_ifna(args, cell),
|
||||||
@@ -957,7 +957,7 @@ impl Model {
|
|||||||
Function::Not => self.fn_not(args, cell),
|
Function::Not => self.fn_not(args, cell),
|
||||||
Function::Or => self.fn_or(args, cell),
|
Function::Or => self.fn_or(args, cell),
|
||||||
Function::Switch => self.fn_switch(args, cell),
|
Function::Switch => self.fn_switch(args, cell),
|
||||||
Function::True => CalcResult::Boolean(true),
|
Function::True => self.fn_true(args, cell),
|
||||||
Function::Xor => self.fn_xor(args, cell),
|
Function::Xor => self.fn_xor(args, cell),
|
||||||
// Math and trigonometry
|
// Math and trigonometry
|
||||||
Function::Sin => self.fn_sin(args, cell),
|
Function::Sin => self.fn_sin(args, cell),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ mod test_fn_averageifs;
|
|||||||
mod test_fn_choose;
|
mod test_fn_choose;
|
||||||
mod test_fn_concatenate;
|
mod test_fn_concatenate;
|
||||||
mod test_fn_count;
|
mod test_fn_count;
|
||||||
|
mod test_fn_day;
|
||||||
mod test_fn_exact;
|
mod test_fn_exact;
|
||||||
mod test_fn_financial;
|
mod test_fn_financial;
|
||||||
mod test_fn_formulatext;
|
mod test_fn_formulatext;
|
||||||
@@ -41,6 +42,7 @@ mod test_sheet_markup;
|
|||||||
mod test_sheets;
|
mod test_sheets;
|
||||||
mod test_styles;
|
mod test_styles;
|
||||||
mod test_trigonometric;
|
mod test_trigonometric;
|
||||||
|
mod test_true_false;
|
||||||
mod test_workbook;
|
mod test_workbook;
|
||||||
mod test_worksheet;
|
mod test_worksheet;
|
||||||
pub(crate) mod util;
|
pub(crate) mod util;
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ fn test_fn_date_arguments() {
|
|||||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
assert_eq!(model._get_text("A5"), *"10/10/1974");
|
||||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
assert_eq!(model._get_text("A6"), *"21/01/1975");
|
||||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
assert_eq!(model._get_text("A7"), *"10/02/1976");
|
||||||
assert_eq!(model._get_text("A8"), *"#NUM!");
|
assert_eq!(model._get_text("A8"), *"02/03/1975");
|
||||||
|
|
||||||
assert_eq!(model._get_text("A9"), *"#NUM!");
|
assert_eq!(model._get_text("A9"), *"01/03/1975");
|
||||||
assert_eq!(model._get_text("A10"), *"29/02/1976");
|
assert_eq!(model._get_text("A10"), *"29/02/1976");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_value_by_ref("Sheet1!A10"),
|
model.get_cell_value_by_ref("Sheet1!A10"),
|
||||||
@@ -64,15 +64,18 @@ fn test_date_out_of_range() {
|
|||||||
|
|
||||||
// year (actually years < 1900 don't really make sense)
|
// year (actually years < 1900 don't really make sense)
|
||||||
model._set("C1", "=DATE(-1, 5, 5)");
|
model._set("C1", "=DATE(-1, 5, 5)");
|
||||||
|
// excel is not compatible with years past 9999
|
||||||
|
model._set("C2", "=DATE(10000, 5, 5)");
|
||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
assert_eq!(model._get_text("A1"), *"10/12/2021");
|
||||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
assert_eq!(model._get_text("A2"), *"10/01/2023");
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
assert_eq!(model._get_text("B1"), *"30/04/2042");
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
assert_eq!(model._get_text("B2"), *"01/06/2025");
|
||||||
|
|
||||||
assert_eq!(model._get_text("C1"), *"#NUM!");
|
assert_eq!(model._get_text("C1"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("C2"), *"#NUM!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -129,8 +132,7 @@ fn test_day_small_serial() {
|
|||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||||
// This agrees with Google Docs and disagrees with Excel
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
assert_eq!(model._get_text("A2"), *"30");
|
|
||||||
// Excel thinks is Feb 29, 1900
|
// Excel thinks is Feb 29, 1900
|
||||||
assert_eq!(model._get_text("A3"), *"28");
|
assert_eq!(model._get_text("A3"), *"28");
|
||||||
|
|
||||||
@@ -150,8 +152,7 @@ fn test_month_small_serial() {
|
|||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||||
// This agrees with Google Docs and disagrees with Excel
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
assert_eq!(model._get_text("A2"), *"12");
|
|
||||||
// We agree with Excel here (We are both in Feb)
|
// We agree with Excel here (We are both in Feb)
|
||||||
assert_eq!(model._get_text("A3"), *"2");
|
assert_eq!(model._get_text("A3"), *"2");
|
||||||
|
|
||||||
@@ -171,8 +172,7 @@ fn test_year_small_serial() {
|
|||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||||
// This agrees with Google Docs and disagrees with Excel
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
assert_eq!(model._get_text("A2"), *"1899");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("A3"), *"1900");
|
assert_eq!(model._get_text("A3"), *"1900");
|
||||||
|
|
||||||
@@ -204,7 +204,10 @@ fn test_date_early_dates() {
|
|||||||
model.get_cell_value_by_ref("Sheet1!A2"),
|
model.get_cell_value_by_ref("Sheet1!A2"),
|
||||||
Ok(CellValue::Number(60.0))
|
Ok(CellValue::Number(60.0))
|
||||||
);
|
);
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
|
// This does not agree with Excel, instead of mistakenly allowing
|
||||||
|
// for Feb 29, it will auto-wrap to the next day after Feb 28.
|
||||||
|
assert_eq!(model._get_text("B2"), *"01/03/1900");
|
||||||
|
|
||||||
// This agrees with Excel from he onward
|
// This agrees with Excel from he onward
|
||||||
assert_eq!(model._get_text("A3"), *"01/03/1900");
|
assert_eq!(model._get_text("A3"), *"01/03/1900");
|
||||||
|
|||||||
15
base/src/test/test_fn_day.rs
Normal file
15
base/src/test/test_fn_day.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_date_arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A1", "=DAY(95051806)");
|
||||||
|
model._set("A2", "=DAY(2958465)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"31");
|
||||||
|
}
|
||||||
25
base/src/test/test_true_false.rs
Normal file
25
base/src/test/test_true_false.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn true_false_arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=TRUE( )");
|
||||||
|
model._set("A2", "=FALSE( )");
|
||||||
|
model._set("A3", "=TRUE( 4 )");
|
||||||
|
model._set("A4", "=FALSE( 4 )");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"TRUE");
|
||||||
|
assert_eq!(model._get_text("A2"), *"FALSE");
|
||||||
|
|
||||||
|
assert_eq!(model._get_formula("A1"), *"=TRUE()");
|
||||||
|
assert_eq!(model._get_formula("A2"), *"=FALSE()");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_formula("A3"), *"=TRUE(4)");
|
||||||
|
assert_eq!(model._get_formula("A4"), *"=FALSE(4)");
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ all:
|
|||||||
cp README.pkg.md pkg/README.md
|
cp README.pkg.md pkg/README.md
|
||||||
tsc types.ts --target esnext --module esnext
|
tsc types.ts --target esnext --module esnext
|
||||||
python fix_types.py
|
python fix_types.py
|
||||||
|
rm -f types.js
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
wasm-pack build --target nodejs && node tests/test.mjs
|
wasm-pack build --target nodejs && node tests/test.mjs
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export default defineConfig({
|
|||||||
{ text: "About the web application", link: "/web-application/about" },
|
{ text: "About the web application", link: "/web-application/about" },
|
||||||
{ text: "Importing Files", link: "/web-application/importing-files" },
|
{ text: "Importing Files", link: "/web-application/importing-files" },
|
||||||
{ text: "Sharing Files", link: "/web-application/sharing-files" },
|
{ text: "Sharing Files", link: "/web-application/sharing-files" },
|
||||||
|
{
|
||||||
|
text: "Name Manager",
|
||||||
|
link: "/web-application/name-manager",
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,6 +74,14 @@ export default defineConfig({
|
|||||||
text: "Units",
|
text: "Units",
|
||||||
link: "/features/units",
|
link: "/features/units",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Dates and serial numbers",
|
||||||
|
link: "/features/serial-numbers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Numbers in IronCalc",
|
||||||
|
link: "/features/numbers-in-ironcalc",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "Unsupported Features",
|
text: "Unsupported Features",
|
||||||
link: "/features/unsupported-features",
|
link: "/features/unsupported-features",
|
||||||
@@ -2020,6 +2032,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Spreadhsheet Engines",
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: "Spreadsheet Engines",
|
||||||
|
link: "/other-spreadsheets/index",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "Contributing",
|
text: "Contributing",
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
|
|||||||
38
docs/src/features/numbers-in-ironcalc.md
Normal file
38
docs/src/features/numbers-in-ironcalc.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# Numbers in IronCalc
|
||||||
|
::: warning
|
||||||
|
**Note:** This draft page is under construction 🚧
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**Note:** This page contains technical documentation
|
||||||
|
|
||||||
|
Numbers in IronCalc are [IEE 754](https://en.wikipedia.org/wiki/IEEE_754) doubles (64 bit) and are displayed uo to 15 decimal digits.
|
||||||
|
|
||||||
|
## Integers
|
||||||
|
|
||||||
|
Some Integers are well represented by IEEE 754 doubles. The largest integer that can be stored perfectly as a double is:
|
||||||
|
|
||||||
|
$$
|
||||||
|
2^53 = 9,007,199,254,740,992
|
||||||
|
$$
|
||||||
|
|
||||||
|
## Floating points
|
||||||
|
|
||||||
|
The reader should be aware that numbers like 0.1 or 0.3 are not stored perfectly by computers, _only an approximation to them_ is stored.
|
||||||
|
This results in imperfect operations like the famous `0.1 + 0.2 != 0.3`.
|
||||||
|
|
||||||
|
When comparing numbers we also compare up to 15 significant figures. With this 'trick' `=IF(0.2+0.1=0.3,TRUE,FALSE)` is actually `TRUE`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Compatibility issues
|
||||||
|
|
||||||
|
Excel [mostly follows IEEE 754](https://learn.microsoft.com/en-us/office/troubleshoot/excel/floating-point-arithmetic-inaccurate-result). Like IronCalc displays numbers with 15 significant digits. Excel does a few other undisclosed 'hacks'.
|
||||||
|
If the result of an addition (or subtraction) of two non very small numbers is a number close to EPS and it is the end of the calculation then it is zero.
|
||||||
|
|
||||||
|
That's is how it gets `=0.3-0.2-0.1` as `0`. However `=1*(0.3-0.2-0.1)` in Excel is `-2.77556E-17`
|
||||||
50
docs/src/features/serial-numbers.md
Normal file
50
docs/src/features/serial-numbers.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# Serial Numbers
|
||||||
|
::: warning
|
||||||
|
**Note:** This draft page is under construction 🚧
|
||||||
|
:::
|
||||||
|
**Note**: For convenience, dates presented on this page are formatted in accordance with the ISO 8601 international standard. IronCalc can recognize and display dates in other formats.
|
||||||
|
|
||||||
|
IronCalc stores dates and times as positive numbers, referred to as *serial numbers*. Serial numbers can be formatted to display the date and time.
|
||||||
|
|
||||||
|
The integer part of a serial number represents the date, as a count of the days since the fixed starting date of 1899-12-30. Hence dates are represented by a unique, sequential integer value, for example:
|
||||||
|
* 1 corresponds to 1899-12-31.
|
||||||
|
* 2 corresponds to 1900-01-01.
|
||||||
|
* 36,526 corresponds to 2000-01-01.
|
||||||
|
* 45,658 corresponds to 2025-01-01.
|
||||||
|
* 2,958,465 corresponds to 9999-12-31.
|
||||||
|
|
||||||
|
To illustrate the concept, type the value 2 into an empty cell that is initially formatted as a number. When you subsequently change the cell to a date format, it will update to show the date 1900-01-01.
|
||||||
|
|
||||||
|
The fractional part of a serial number represents time, as a fraction of the day. For example:
|
||||||
|
* 0.0 corresponds to 00:00:00 (midnight)
|
||||||
|
* 0.041666667 corresponds to 01:00:00.
|
||||||
|
* 0.5 corresponds to 12:00:00 (noon)
|
||||||
|
* 0.75 corresponds to 18:00:00.
|
||||||
|
* 0.99 corresponds to 23:45:36.
|
||||||
|
|
||||||
|
Since date-times are stored as numbers, they can be used for arithmetic operations in formulas. For example, it is possible to determine the difference between two dates by subtracting one serial number from the other.
|
||||||
|
|
||||||
|
**Note**: A #VALUE! error is reported if a date-formatted cell contains a number less than 1 or greater than 2,958,465.
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
Excel has an infamous [feature](https://learn.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year) that was ported from a bug in Lotus 1-2-3 that assumes that the year 1900 is a leap year.
|
||||||
|
|
||||||
|
That means that serial numbers 1 to 60 in IronCalc are different than Excel.
|
||||||
|
|
||||||
|
In IronCalc, Google Sheets, Libre Office and Zoho Date(1900,1,1) returns 2
|
||||||
|
|
||||||
|
In Excel Date(1900,1,1) returns 1.
|
||||||
|
|
||||||
|
Gnumeric solves the problem in yet another way. It follows Excel from 1 to 59, skips 60, and it follows Excel (and all other engines from there on).
|
||||||
|
A formula like `=DAY(60)` produces `#NUM!` in Gnumeric.
|
||||||
|
|
||||||
|
Serial number 61 corresponds to 1 March 1900, and from there on most spreadsheet engines agree.
|
||||||
|
|
||||||
|
IronCalc, like Excel, doesn't deal with serial numbers outside of the range [1, 2,958,465]. Other engines like Google sheets, do not have an upper limit.
|
||||||
@@ -14,7 +14,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| DATE | <Badge type="tip" text="Available" /> | – |
|
| DATE | <Badge type="tip" text="Available" /> | – |
|
||||||
| DATEDIF | <Badge type="info" text="Not implemented yet" /> | – |
|
| DATEDIF | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| DATEVALUE | <Badge type="info" text="Not implemented yet" /> | – |
|
| DATEVALUE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| DAY | <Badge type="tip" text="Available" /> | – |
|
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
|
||||||
| DAYS | <Badge type="info" text="Not implemented yet" /> | – |
|
| DAYS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| DAYS360 | <Badge type="info" text="Not implemented yet" /> | – |
|
| DAYS360 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| EDATE | <Badge type="tip" text="Available" /> | – |
|
| EDATE | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -3,9 +3,49 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# DAY function
|
||||||
# DAY
|
|
||||||
|
|
||||||
::: warning
|
::: 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).
|
**Note:** This draft page is under construction 🚧
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
DAY is a function of the Date and Time category that extracts the day of the month from a valid date [serial number](/features/serial-numbers.md), returning a number in the range [1, 31].
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**DAY(<span title="Number" style="color:#1E88E5">date</span>) => <span title="Number" style="color:#1E88E5">day</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *date* ([number](/features/value-types#numbers), required). The date for which the day of the month is to be calculated, expressed as a [serial number](/features/serial-numbers.md) in the range [1, 2958465]. The value corresponds to the date 1899-12-31, while 2958465 corresponds to 9999-12-31.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
If the supplied _date_ argument has a fractional part, DAY uses its [floor value](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions).
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
DAY returns an integer [number](/features/value-types#numbers) in the range [1, 31], that is the day of the month according to the [Gregorian calendar](https://en.wikipedia.org/wiki/Gregorian_calendar).
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, DAY propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then DAY returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *date* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then DAY returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, DAY may return the [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
* If date is less than 1, or greater than 2,958,465, then DAY returns the [`#NUM!`](/features/error-types.md#num) error.
|
||||||
|
* At present, DAY does not accept a string representation of a date literal as an argument. For example, the formula `=DAY("2024-12-31")` returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
## Details
|
||||||
|
IronCalc utilizes Rust's [chrono](https://docs.rs/chrono/latest/chrono/) crate to implement the DAY function.
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
In IronCalc the argument of DAY cannot be text. This is confusing, and error prone, as in "10/12/2026" it is not clear if the day is 10 or 12.
|
||||||
|
Most of all other spreadsheet engines like Excel, Google Sheets, Libre Office and Gnumeric accept text as input.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=day).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* See also IronCalc's [MONTH](/functions/date_and_time/month.md) and [YEAR](/functions/date_and_time/year.md) functions.
|
||||||
|
* Visit Microsoft Excel's [DAY function](https://support.microsoft.com/en-gb/office/day-function-8a7d1cbb-6c7d-4ba1-8aea-25c134d03101) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093040) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/DAY) provide versions of the DAY function.
|
||||||
@@ -51,7 +51,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| PRICE | <Badge type="info" text="Not implemented yet" /> | – |
|
| PRICE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| PRICEDISC | <Badge type="info" text="Not implemented yet" /> | – |
|
| PRICEDISC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| PRICEMAT | <Badge type="info" text="Not implemented yet" /> | – |
|
| PRICEMAT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| PV | <Badge type="tip" text="Available" /> | – |
|
| PV | <Badge type="tip" text="Available" /> | [PV](financial/pv) |
|
||||||
| RATE | <Badge type="tip" text="Available" /> | – |
|
| RATE | <Badge type="tip" text="Available" /> | – |
|
||||||
| RECEIVED | <Badge type="info" text="Not implemented yet" /> | – |
|
| RECEIVED | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| RRI | <Badge type="tip" text="Available" /> | - |
|
| RRI | <Badge type="tip" text="Available" /> | - |
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ If your interest rate varies between periods, use the [FVSCHEDULE](/functions/fi
|
|||||||
* *nper* ([number](/features/value-types#numbers), required). "nper" stands for <u>n</u>umber of <u>per</u>iods, in this case the number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
|
* *nper* ([number](/features/value-types#numbers), required). "nper" stands for <u>n</u>umber of <u>per</u>iods, in this case the number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
|
||||||
* *pmt* ([number](/features/value-types#numbers), required). "pmt" stands for <u>p</u>ay<u>m</u>en<u>t</u>, in this case the fixed amount paid or deposited each compounding period.
|
* *pmt* ([number](/features/value-types#numbers), required). "pmt" stands for <u>p</u>ay<u>m</u>en<u>t</u>, in this case the fixed amount paid or deposited each compounding period.
|
||||||
* *pv* ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0).
|
* *pv* ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0).
|
||||||
* *type* ([boolean](/features/value-types/#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
|
* *type* ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
|
||||||
### Additional guidance
|
### 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*.
|
* 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 *pmt* and *pv* arguments should be expressed in the same currency unit.
|
||||||
* To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero.
|
* 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.
|
* The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument.
|
||||||
### Returned value
|
### Returned value
|
||||||
FV returns a [number](/features/value-types/#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the *pmt* and *pv* arguments.
|
FV returns a [number](/features/value-types#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the *pmt* and *pv* arguments.
|
||||||
### Error conditions
|
### Error conditions
|
||||||
* In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
|
* In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
|
||||||
* If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
|
* If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
* If the value of any of the *rate*, *nper*, *pmt* or *pv* arguments is not (or cannot be converted to) a [number](/features/value-types#value), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
|
* If the value of any of the *rate*, *nper*, *pmt* or *pv* arguments is not (or cannot be converted to) a [number](/features/value-types#numbers), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
* If the value of the *type* argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
|
* If the value of the *type* argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
* For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
* For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|||||||
@@ -3,9 +3,57 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# PV function
|
||||||
# PV
|
|
||||||
|
|
||||||
::: warning
|
::: 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).
|
**Note:** This draft page is under construction 🚧
|
||||||
:::
|
:::
|
||||||
|
## Overview
|
||||||
|
PV (<u>P</u>resent <u>V</u>alue) is a function of the Financial category that can be used to calculate the present value of a series of future cash flows.
|
||||||
|
|
||||||
|
PV can be used to calculate present 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.
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**PV(<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">fv</span>=0, <span title="Boolean" style="color:#43A047">type</span>=FALSE) => <span title="Number" style="color:#1E88E5">pv</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *rate* ([number](/features/value-types#numbers), required). The fixed percentage interest rate or yield per period.
|
||||||
|
* *nper* ([number](/features/value-types#numbers), required). "nper" stands for <u>n</u>umber of <u>per</u>iods, in this case the number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
|
||||||
|
* *pmt* ([number](/features/value-types#numbers), required). "pmt" stands for <u>p</u>ay<u>m</u>en<u>t</u>, in this case the fixed amount paid or deposited each compounding period.
|
||||||
|
* *fv* ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "fv" is the <u>f</u>uture <u>v</u>alue at the end of the final compounding period (default 0).
|
||||||
|
* *type* ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
|
||||||
|
### 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 *fv* arguments should be expressed in the same currency unit.
|
||||||
|
* To ensure a worthwhile result, one of the *pmt* and *fv* arguments should be non-zero.
|
||||||
|
* The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument.
|
||||||
|
### Returned value
|
||||||
|
PV returns a [number](/features/value-types#numbers) representing the present value expressed in the same [currency unit](/features/units) that was used for the *pmt* and *fv* arguments.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, PV propagates errors that are found in any of its arguments.
|
||||||
|
* If too few or too many arguments are supplied, PV returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of any of the *rate*, *nper*, *pmt* or *fv* arguments is not (or cannot be converted to) a [number](/features/value-types#numbers), then PV returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the value of the *type* argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then PV again returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some combinations of valid argument values, PV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
In paticular, PV always returns a [`#DIV/0!`](/features/error-types.md#div-0) error if the value of the *rate* argument is set to -1.
|
||||||
|
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* If $\text{type} \neq 0$, $\text{pv}$ is given by the equation:
|
||||||
|
$$ \text{pv} = - \Biggl(\dfrac{(\text{fv} \times \text{rate}) + \bigl(\text{pmt} \times (1+\text{rate})\times \bigl({(1+\text{rate})^{\text{nper}}-1\bigr)\bigr)}}{\text{rate} \times (1+\text{rate})^{\text{nper}}}\Biggl)
|
||||||
|
$$
|
||||||
|
|
||||||
|
* If $\text{type} = 0$, $\text{pv}$ is given by the equation:
|
||||||
|
$$ \text{pv} = - \Biggl(\dfrac{(\text{fv} \times \text{rate}) + \bigl(\text{pmt}\times \bigl({(1+\text{rate})^{\text{nper}}-1\bigr)\bigr)}}{\text{rate} \times (1+\text{rate})^\text{{nper}}}\Biggl)
|
||||||
|
$$
|
||||||
|
|
||||||
|
* For any $\text{type}$, in the special case of $\text{rate} = 0$, $\text{pv}$ is given by the equation:
|
||||||
|
$$
|
||||||
|
\text{pv} = -\text{fv} - (\text{pmt} \times \text{nper})
|
||||||
|
$$
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=pv).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about the concept of "present value" in finance, visit Wikipedia's [Present value](https://en.wikipedia.org/wiki/present_value) page.
|
||||||
|
* See also IronCalc's [FV](/functions/financial/fv), [NPER](/functions/financial/nper), [PMT](/functions/financial/pmt) and [RATE](/functions/financial/rate) functions.
|
||||||
|
* Visit Microsoft Excel's [PV function](https://support.microsoft.com/en-gb/office/pv-function-23879d31-0e02-4321-be01-da16e8168cbd) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093243) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/PV) provide versions of the PV function.
|
||||||
BIN
docs/src/functions/images/cosine-curve.png
Normal file
BIN
docs/src/functions/images/cosine-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
docs/src/functions/images/sine-curve.png
Normal file
BIN
docs/src/functions/images/sine-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
docs/src/functions/images/tangent-curve.png
Normal file
BIN
docs/src/functions/images/tangent-curve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -29,7 +29,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
|
| CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COMBIN | <Badge type="info" text="Not implemented yet" /> | – |
|
| COMBIN | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COMBINA | <Badge type="info" text="Not implemented yet" /> | – |
|
| COMBINA | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COS | <Badge type="tip" text="Available" /> | – |
|
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
|
||||||
| COSH | <Badge type="tip" text="Available" /> | – |
|
| COSH | <Badge type="tip" text="Available" /> | – |
|
||||||
| COT | <Badge type="info" text="Not implemented yet" /> | – |
|
| COT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COTH | <Badge type="info" text="Not implemented yet" /> | – |
|
| COTH | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
@@ -77,7 +77,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SERIESSUM | <Badge type="info" text="Not implemented yet" /> | – |
|
| SERIESSUM | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | – |
|
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SIGN | <Badge type="info" text="Not implemented yet" /> | – |
|
| SIGN | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SIN | <Badge type="tip" text="Available" /> | – |
|
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
|
||||||
| SINH | <Badge type="tip" text="Available" /> | – |
|
| SINH | <Badge type="tip" text="Available" /> | – |
|
||||||
| 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="Not implemented yet" /> | – |
|
||||||
@@ -90,6 +90,6 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| TAN | <Badge type="tip" text="Available" /> | – |
|
| TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) |
|
||||||
| TANH | <Badge type="tip" text="Available" /> | – |
|
| TANH | <Badge type="tip" text="Available" /> | – |
|
||||||
| TRUNC | <Badge type="info" text="Not implemented yet" /> | – |
|
| TRUNC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
|
|||||||
@@ -3,9 +3,41 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# COS function
|
||||||
# COS
|
|
||||||
|
|
||||||
::: warning
|
::: 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).
|
**Note:** This draft page is under construction 🚧
|
||||||
:::
|
:::
|
||||||
|
## Overview
|
||||||
|
COS is a function of the Math and Trigonometry category that calculates the trigonometric cosine of an angle, returning a value in the range [-1, +1].
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**COS(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">cos</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *angle* ([number](/features/value-types#numbers), required). The angle whose cosine is to be calculated, expressed in radians. To convert between degrees and radians, use the relation below. Alternatively, use the [DEGREES](/functions/math_and_trigonometry/degrees) or [RADIANS](/functions/math_and_trigonometry/radians) functions.
|
||||||
|
$$
|
||||||
|
1~\:~\text{degree} = \dfrac{\pi}{180} = 0.01745329252~\text{radians}
|
||||||
|
$$
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
### Returned value
|
||||||
|
COS returns a unitless [number](/features/value-types#numbers) that is the trigonometric cosine of the specified angle.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, COS propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then COS returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then COS returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, COS may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The COS function utilizes the *cos()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the output of the COS function for angles $x$ in the range -2$\pi$ to +2$\pi$ radians.
|
||||||
|
<center><img src="/functions/images/cosine-curve.png" width="350" alt="Graph showing cos(x) for x between -2π and +2π radians."></center>
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=cos).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about trigonometric cosine, visit Wikipedia's [Sine and cosine](https://en.wikipedia.org/wiki/Sine_and_cosine) page.
|
||||||
|
* See also IronCalc's [ACOS](/functions/math_and_trigonometry/acos), [SIN](/functions/math_and_trigonometry/sin) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [COS function](https://support.microsoft.com/en-gb/office/cos-function-0fb808a5-95d6-4553-8148-22aebdce5f05) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093476) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/COS) provide versions of the COS function.
|
||||||
@@ -3,9 +3,41 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# SIN function
|
||||||
# SIN
|
|
||||||
|
|
||||||
::: warning
|
::: 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).
|
**Note:** This draft page is under construction 🚧
|
||||||
:::
|
:::
|
||||||
|
## Overview
|
||||||
|
SIN is a function of the Math and Trigonometry category that calculates the trigonometric sine of an angle, returning a value in the range [-1, +1].
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**SIN(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">sin</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *angle* ([number](/features/value-types#numbers), required). The angle whose sine is to be calculated, expressed in radians. To convert between degrees and radians, use the relation below. Alternatively, use the [DEGREES](/functions/math_and_trigonometry/degrees) or [RADIANS](/functions/math_and_trigonometry/radians) functions.
|
||||||
|
$$
|
||||||
|
1~\:~\text{degree} = \dfrac{\pi}{180} = 0.01745329252~\text{radians}
|
||||||
|
$$
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
### Returned value
|
||||||
|
SIN returns a unitless [number](/features/value-types#numbers) that is the trigonometric sine of the specified angle.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, SIN propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then SIN returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then SIN returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, SIN may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The SIN function utilizes the *sin()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the output of the SIN function for angles $x$ in the range -2$\pi$ to +2$\pi$ radians.
|
||||||
|
<center><img src="/functions/images/sine-curve.png" width="350" alt="Graph showing sin(x) for x between -2π and +2π."></center>
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=sin).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about trigonometric sine, visit Wikipedia's [Sine and cosine](https://en.wikipedia.org/wiki/Sine_and_cosine) page.
|
||||||
|
* See also IronCalc's [ASIN](/functions/math_and_trigonometry/asin), [COS](/functions/math_and_trigonometry/cos) and [TAN](/functions/math_and_trigonometry/tan) functions.
|
||||||
|
* Visit Microsoft Excel's [SIN function](https://support.microsoft.com/en-gb/office/sin-function-cf0e3432-8b9e-483c-bc55-a76651c95602) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093447) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/SIN) provide versions of the SIN function.
|
||||||
@@ -3,9 +3,42 @@ layout: doc
|
|||||||
outline: deep
|
outline: deep
|
||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
# TAN function
|
||||||
# TAN
|
|
||||||
|
|
||||||
::: warning
|
::: 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).
|
**Note:** This draft page is under construction 🚧
|
||||||
:::
|
:::
|
||||||
|
## Overview
|
||||||
|
TAN is a function of the Math and Trigonometry category that calculates the trigonometric tangent of an angle, returning a value in the range (-$\infty$, +$\infty$).
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**TAN(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">tan</span>**
|
||||||
|
### Argument descriptions
|
||||||
|
* *angle* ([number](/features/value-types#numbers), required). The angle whose tangent is to be calculated, expressed in radians. To convert between degrees and radians, use the relation below. Alternatively, use the [DEGREES](/functions/math_and_trigonometry/degrees) or [RADIANS](/functions/math_and_trigonometry/radians) functions.
|
||||||
|
$$
|
||||||
|
1~\:~\text{degree} = \dfrac{\pi}{180} = 0.01745329252~\text{radians}
|
||||||
|
$$
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
### Returned value
|
||||||
|
TAN returns a unitless [number](/features/value-types#numbers) that is the trigonometric tangent of the specified angle.
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, TAN propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then TAN returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then TAN returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* For some argument values, TAN may return a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
## Details
|
||||||
|
* The TAN function utilizes the *tan()* method provided by the [Rust Standard Library](https://doc.rust-lang.org/std/).
|
||||||
|
* The figure below illustrates the output of the TAN function for angles $x$ in the range -2$π$ to +2$π$.
|
||||||
|
<center><img src="/functions/images/tangent-curve.png" width="350" alt="Graph showing tan(x) for x between -2π and +2π."></center>
|
||||||
|
|
||||||
|
* Theoretically, $\text{tan}(x)$ is undefined for any critical $x$ that satisfies $x = \frac{\pi}{2} + k\pi$ (where $k$ is any integer). However, an exact representation of the mathmatical constant $\pi$ requires infinite precision, which cannot be achieved with the floating-point representation available. Hence, TAN will return very large or very small values close to critical $x$ values.
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=tan).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about trigonometric tangent, visit Wikipedia's [Trigonometric functions](https://en.wikipedia.org/wiki/Trigonometric_functions) page.
|
||||||
|
* See also IronCalc's [ATAN](/functions/math_and_trigonometry/atan), [COS](/functions/math_and_trigonometry/cos) and [SIN](/functions/math_and_trigonometry/sin) functions.
|
||||||
|
* Visit Microsoft Excel's [TAN function](https://support.microsoft.com/en-gb/office/tan-function-08851a40-179f-4052-b789-d7f699447401) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093586) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/TAN) provide versions of the TAN function.
|
||||||
30
docs/src/other-spreadsheets/index.md
Normal file
30
docs/src/other-spreadsheets/index.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**Note:** This draft page is under construction 🚧
|
||||||
|
:::
|
||||||
|
|
||||||
|
# Other spreadsheet engines
|
||||||
|
|
||||||
|
There are numerous spreadsheet engines out there
|
||||||
|
|
||||||
|
## Excel, the Friendly Giant
|
||||||
|
|
||||||
|
## Google Sheets
|
||||||
|
|
||||||
|
## LibreOffice
|
||||||
|
|
||||||
|
## Gnumeric
|
||||||
|
|
||||||
|
|
||||||
|
# Links
|
||||||
|
|
||||||
|
For a list of spreadsheet software you can consult [wikipedia](https://en.wikipedia.org/wiki/List_of_spreadsheet_software).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
12
docs/src/web-application/name-manager.md
Normal file
12
docs/src/web-application/name-manager.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# Name Manager
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**Note:** This draft page is under construction 🚧
|
||||||
|
:::
|
||||||
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import type { DefinedName, Model } from "@ironcalc/wasm";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
styled,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
styled,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { BookOpen, Check, X } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import NamedRange from "./NamedRange";
|
|
||||||
import { getFullRangeToString } from "./util";
|
|
||||||
|
|
||||||
type NameManagerDialogProperties = {
|
|
||||||
onClose: () => void;
|
|
||||||
open: boolean;
|
|
||||||
model: Model;
|
|
||||||
onClose: () => void;
|
|
||||||
open: boolean;
|
|
||||||
model: Model;
|
|
||||||
};
|
|
||||||
|
|
||||||
function NameManagerDialog(props: NameManagerDialogProperties) {
|
|
||||||
const [definedNamesLocal, setDefinedNamesLocal] = useState<DefinedName[]>();
|
|
||||||
const [definedName, setDefinedName] = useState<DefinedName>({
|
|
||||||
name: "",
|
|
||||||
scope: undefined,
|
|
||||||
formula: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// render definedNames from model
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.open) {
|
|
||||||
const definedNamesModel = props.model.getDefinedNameList();
|
|
||||||
setDefinedNamesLocal(definedNamesModel);
|
|
||||||
console.log("definedNamesModel EFFECT", definedNamesModel);
|
|
||||||
}
|
|
||||||
}, [props.open]);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
try {
|
|
||||||
console.log("SAVE", definedName);
|
|
||||||
|
|
||||||
props.model.newDefinedName(
|
|
||||||
definedName.name,
|
|
||||||
definedName.scope,
|
|
||||||
definedName.formula,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("DefinedName save failed", error);
|
|
||||||
}
|
|
||||||
props.onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
field: keyof DefinedName,
|
|
||||||
value: string | number | undefined,
|
|
||||||
) => {
|
|
||||||
setDefinedName((prev: DefinedName) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (name: string, scope: number | undefined) => {
|
|
||||||
try {
|
|
||||||
props.model.deleteDefinedName(name, scope);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("DefinedName delete failed", error);
|
|
||||||
}
|
|
||||||
// should re-render modal
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = (
|
|
||||||
name: string,
|
|
||||||
scope: number | undefined,
|
|
||||||
newName: string,
|
|
||||||
newScope: number | undefined,
|
|
||||||
newFormula: string,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
// partially update?
|
|
||||||
props.model.updateDefinedName(name, scope, newName, newScope, newFormula);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("DefinedName update failed", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatFormula = (): string => {
|
|
||||||
const worksheets = props.model.getWorksheetsProperties();
|
|
||||||
const selectedView = props.model.getSelectedView();
|
|
||||||
const formatFormula = (): string => {
|
|
||||||
const worksheets = props.model.getWorksheetsProperties();
|
|
||||||
const selectedView = props.model.getSelectedView();
|
|
||||||
|
|
||||||
return getFullRangeToString(selectedView, worksheets);
|
|
||||||
};
|
|
||||||
return getFullRangeToString(selectedView, worksheets);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledDialog
|
|
||||||
open={props.open}
|
|
||||||
onClose={props.onClose}
|
|
||||||
maxWidth={false}
|
|
||||||
scroll="paper"
|
|
||||||
>
|
|
||||||
<StyledDialogTitle>
|
|
||||||
Named Ranges
|
|
||||||
<IconButton onClick={() => props.onClose()}>
|
|
||||||
<X size={16} />
|
|
||||||
</IconButton>
|
|
||||||
</StyledDialogTitle>
|
|
||||||
<StyledDialogContent dividers>
|
|
||||||
<StyledRangesHeader>
|
|
||||||
<Box width="171px">{t("name_manager_dialog.name")}</Box>
|
|
||||||
<Box width="171px">{t("name_manager_dialog.range")}</Box>
|
|
||||||
<Box width="171px">{t("name_manager_dialog.scope")}</Box>
|
|
||||||
</StyledRangesHeader>
|
|
||||||
{definedNamesLocal?.map((definedName) => (
|
|
||||||
<NamedRange
|
|
||||||
worksheets={props.model.getWorksheetsProperties()}
|
|
||||||
name={definedName.name}
|
|
||||||
scope={definedName.scope}
|
|
||||||
formula={definedName.formula}
|
|
||||||
key={definedName.name}
|
|
||||||
model={props.model}
|
|
||||||
onChange={handleChange}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
canDelete={true}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<NamedRange
|
|
||||||
worksheets={props.model.getWorksheetsProperties()}
|
|
||||||
formula={formatFormula()}
|
|
||||||
onChange={handleChange}
|
|
||||||
canDelete={false}
|
|
||||||
model={props.model}
|
|
||||||
/>
|
|
||||||
</StyledDialogContent>
|
|
||||||
<StyledDialogActions>
|
|
||||||
<Box display="flex" alignItems="center" gap={"8px"}>
|
|
||||||
<BookOpen color="grey" size={16} />
|
|
||||||
<span style={{ fontSize: "12px", fontFamily: "Inter" }}>
|
|
||||||
{t("name_manager_dialog.help")}
|
|
||||||
</span>
|
|
||||||
</Box>
|
|
||||||
<Box display="flex" gap="8px" width={"155px"}>
|
|
||||||
{/* change hover color? */}
|
|
||||||
<Button
|
|
||||||
onClick={() => props.onClose()}
|
|
||||||
variant="contained"
|
|
||||||
disableElevation
|
|
||||||
color="info"
|
|
||||||
sx={{
|
|
||||||
bgcolor: (theme): string => theme.palette.grey["200"],
|
|
||||||
color: (theme): string => theme.palette.grey["700"],
|
|
||||||
textTransform: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
variant="contained"
|
|
||||||
disableElevation
|
|
||||||
sx={{ textTransform: "none" }}
|
|
||||||
startIcon={<Check size={16} />}
|
|
||||||
// disabled={} // disable when error
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</StyledDialogActions>
|
|
||||||
</StyledDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledDialog = styled(Dialog)(() => ({
|
|
||||||
"& .MuiPaper-root": {
|
|
||||||
height: "380px",
|
|
||||||
minWidth: "620px",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDialogTitle = styled(DialogTitle)`
|
|
||||||
padding: 12px 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDialogContent = styled(DialogContent)`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 20px 12px 20px 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: "12px",
|
|
||||||
fontFamily: theme.typography.fontFamily,
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: "700",
|
|
||||||
color: theme.palette.info.main,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDialogActions = styled(DialogActions)`
|
|
||||||
padding: 12px 20px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #757575;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default NameManagerDialog;
|
|
||||||
import type { DefinedName, Model } from "@ironcalc/wasm";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
styled,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { BookOpen, Check, X } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import NamedRange from "./NamedRange";
|
|
||||||
import { getShortRangeToString } from "./util";
|
|
||||||
|
|
||||||
type NameManagerDialogProperties = {
|
|
||||||
onClose: () => void;
|
|
||||||
open: boolean;
|
|
||||||
model: Model;
|
|
||||||
};
|
|
||||||
|
|
||||||
function NameManagerDialog(props: NameManagerDialogProperties) {
|
|
||||||
const [definedNamesLocal, setDefinedNamesLocal] = useState<DefinedName[]>();
|
|
||||||
const [definedName, setDefinedName] = useState<DefinedName>({
|
|
||||||
name: "",
|
|
||||||
scope: 0,
|
|
||||||
formula: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// render named ranges from model
|
|
||||||
useEffect(() => {
|
|
||||||
const definedNamesModel = props.model.getDefinedNameList();
|
|
||||||
setDefinedNamesLocal(definedNamesModel);
|
|
||||||
console.log("definedNamesModel EFFECT", definedNamesModel);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
console.log("SAVE DIALOG", definedName);
|
|
||||||
// create newDefinedName and close
|
|
||||||
// props.model.newDefinedName(
|
|
||||||
// definedName.name,
|
|
||||||
// definedName.scope,
|
|
||||||
// definedName.formula,
|
|
||||||
// );
|
|
||||||
props.onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (field: keyof DefinedName, value: string | number) => {
|
|
||||||
setDefinedName((prev: DefinedName) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
console.log("definedName marked for deletion");
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatFormula = (): string => {
|
|
||||||
const selectedView = props.model.getSelectedView();
|
|
||||||
|
|
||||||
return getShortRangeToString(selectedView);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledDialog
|
|
||||||
open={props.open}
|
|
||||||
onClose={props.onClose}
|
|
||||||
maxWidth={false}
|
|
||||||
scroll="paper"
|
|
||||||
>
|
|
||||||
<StyledDialogTitle>
|
|
||||||
Named Ranges
|
|
||||||
<IconButton onClick={() => props.onClose()}>
|
|
||||||
<X size={16} />
|
|
||||||
</IconButton>
|
|
||||||
</StyledDialogTitle>
|
|
||||||
<StyledDialogContent dividers>
|
|
||||||
<StyledRangesHeader>
|
|
||||||
<Box width="171px">{t("name_manager_dialog.name")}</Box>
|
|
||||||
<Box width="171px">{t("name_manager_dialog.range")}</Box>
|
|
||||||
<Box width="171px">{t("name_manager_dialog.scope")}</Box>
|
|
||||||
</StyledRangesHeader>
|
|
||||||
{definedNamesLocal?.map((definedName) => (
|
|
||||||
<NamedRange
|
|
||||||
worksheets={props.model.getWorksheetsProperties()}
|
|
||||||
name={definedName.name}
|
|
||||||
scope={definedName.scope}
|
|
||||||
formula={definedName.formula}
|
|
||||||
key={definedName.name}
|
|
||||||
model={props.model}
|
|
||||||
onChange={handleChange}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<NamedRange
|
|
||||||
worksheets={props.model.getWorksheetsProperties()}
|
|
||||||
formula={formatFormula()}
|
|
||||||
onChange={handleChange}
|
|
||||||
model={props.model}
|
|
||||||
/>
|
|
||||||
</StyledDialogContent>
|
|
||||||
<StyledDialogActions>
|
|
||||||
<Box display="flex" alignItems="center" gap={"8px"}>
|
|
||||||
<BookOpen color="grey" size={16} />
|
|
||||||
<span style={{ fontSize: "12px", fontFamily: "Inter" }}>
|
|
||||||
{t("name_manager_dialog.help")}
|
|
||||||
</span>
|
|
||||||
</Box>
|
|
||||||
<Box display="flex" gap="8px" width={"155px"}>
|
|
||||||
{/* change hover color? */}
|
|
||||||
<Button
|
|
||||||
onClick={() => props.onClose()}
|
|
||||||
variant="contained"
|
|
||||||
disableElevation
|
|
||||||
color="info"
|
|
||||||
sx={{
|
|
||||||
bgcolor: (theme): string => theme.palette.grey["200"],
|
|
||||||
color: (theme): string => theme.palette.grey["700"],
|
|
||||||
textTransform: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
variant="contained"
|
|
||||||
disableElevation
|
|
||||||
sx={{ textTransform: "none" }}
|
|
||||||
startIcon={<Check size={16} />}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</StyledDialogActions>
|
|
||||||
</StyledDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledDialog = styled(Dialog)(() => ({
|
|
||||||
"& .MuiPaper-root": {
|
|
||||||
height: "380px",
|
|
||||||
minWidth: "620px",
|
|
||||||
},
|
|
||||||
"& .MuiPaper-root": {
|
|
||||||
height: "380px",
|
|
||||||
minWidth: "620px",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDialogTitle = styled(DialogTitle)`
|
|
||||||
padding: 12px 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDialogContent = styled(DialogContent)`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 20px 12px 20px 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: "12px",
|
|
||||||
fontFamily: theme.typography.fontFamily,
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: "700",
|
|
||||||
color: theme.palette.info.main,
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: "12px",
|
|
||||||
fontFamily: theme.typography.fontFamily,
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: "700",
|
|
||||||
color: theme.palette.info.main,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDialogActions = styled(DialogActions)`
|
|
||||||
padding: 12px 20px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #757575;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default NameManagerDialog;
|
|
||||||
244
webapp/src/components/NameManagerDialog/NameManagerDialog.tsx
Normal file
244
webapp/src/components/NameManagerDialog/NameManagerDialog.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
styled,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { BookOpen, Plus, X } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import NamedRangeActive from "./NamedRangeActive";
|
||||||
|
import NamedRangeInactive from "./NamedRangeInactive";
|
||||||
|
|
||||||
|
export interface NameManagerProperties {
|
||||||
|
newDefinedName: (
|
||||||
|
name: string,
|
||||||
|
scope: number | undefined,
|
||||||
|
formula: string,
|
||||||
|
) => void;
|
||||||
|
updateDefinedName: (
|
||||||
|
name: string,
|
||||||
|
scope: number | undefined,
|
||||||
|
newName: string,
|
||||||
|
newScope: number | undefined,
|
||||||
|
newFormula: string,
|
||||||
|
) => void;
|
||||||
|
deleteDefinedName: (name: string, scope: number | undefined) => void;
|
||||||
|
selectedArea: () => string;
|
||||||
|
worksheets: WorksheetProperties[];
|
||||||
|
definedNameList: DefinedName[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NameManagerDialogProperties {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
model: NameManagerProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NameManagerDialog(properties: NameManagerDialogProperties) {
|
||||||
|
const { open, model, onClose } = properties;
|
||||||
|
const {
|
||||||
|
newDefinedName,
|
||||||
|
updateDefinedName,
|
||||||
|
deleteDefinedName,
|
||||||
|
selectedArea,
|
||||||
|
worksheets,
|
||||||
|
definedNameList,
|
||||||
|
} = model;
|
||||||
|
// If editingNameIndex is -1, then we are adding a new name
|
||||||
|
// If editingNameIndex is -2, then we are not editing any name
|
||||||
|
// If editingNameIndex is a positive number, then we are editing that index
|
||||||
|
const [editingNameIndex, setEditingNameIndex] = useState(-2);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setEditingNameIndex(-2);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDialog open={open} onClose={onClose} maxWidth={false} scroll="paper">
|
||||||
|
<StyledDialogTitle>
|
||||||
|
{t("name_manager_dialog.title")}
|
||||||
|
<IconButton onClick={onClose}>
|
||||||
|
<X size={16} />
|
||||||
|
</IconButton>
|
||||||
|
</StyledDialogTitle>
|
||||||
|
<StyledDialogContent dividers>
|
||||||
|
<StyledRangesHeader>
|
||||||
|
<StyledBox>{t("name_manager_dialog.name")}</StyledBox>
|
||||||
|
<StyledBox>{t("name_manager_dialog.range")}</StyledBox>
|
||||||
|
<StyledBox>{t("name_manager_dialog.scope")}</StyledBox>
|
||||||
|
</StyledRangesHeader>
|
||||||
|
<NameListWrapper>
|
||||||
|
{definedNameList.map((definedName, index) => {
|
||||||
|
const scopeName = definedName.scope
|
||||||
|
? worksheets[definedName.scope].name
|
||||||
|
: "[global]";
|
||||||
|
if (index === editingNameIndex) {
|
||||||
|
return (
|
||||||
|
<NamedRangeActive
|
||||||
|
worksheets={worksheets}
|
||||||
|
name={definedName.name}
|
||||||
|
scope={scopeName}
|
||||||
|
formula={definedName.formula}
|
||||||
|
key={definedName.name + definedName.scope}
|
||||||
|
onSave={(
|
||||||
|
newName,
|
||||||
|
newScope,
|
||||||
|
newFormula,
|
||||||
|
): string | undefined => {
|
||||||
|
const scope_index = worksheets.findIndex(
|
||||||
|
(s) => s.name === newScope,
|
||||||
|
);
|
||||||
|
const scope = scope_index > 0 ? scope_index : undefined;
|
||||||
|
try {
|
||||||
|
updateDefinedName(
|
||||||
|
definedName.name,
|
||||||
|
definedName.scope,
|
||||||
|
newName,
|
||||||
|
scope,
|
||||||
|
newFormula,
|
||||||
|
);
|
||||||
|
setEditingNameIndex(-2);
|
||||||
|
} catch (e) {
|
||||||
|
return `${e}`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => setEditingNameIndex(-2)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<NamedRangeInactive
|
||||||
|
name={definedName.name}
|
||||||
|
scope={scopeName}
|
||||||
|
formula={definedName.formula}
|
||||||
|
key={definedName.name + definedName.scope}
|
||||||
|
showOptions={editingNameIndex === -2}
|
||||||
|
onEdit={() => setEditingNameIndex(index)}
|
||||||
|
onDelete={() => {
|
||||||
|
deleteDefinedName(definedName.name, definedName.scope);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</NameListWrapper>
|
||||||
|
{editingNameIndex === -1 && (
|
||||||
|
<NamedRangeActive
|
||||||
|
worksheets={worksheets}
|
||||||
|
name={""}
|
||||||
|
formula={selectedArea()}
|
||||||
|
scope={"[global]"}
|
||||||
|
onSave={(name, scope, formula): string | undefined => {
|
||||||
|
const scope_index = worksheets.findIndex((s) => s.name === scope);
|
||||||
|
const scope_value = scope_index > 0 ? scope_index : undefined;
|
||||||
|
try {
|
||||||
|
newDefinedName(name, scope_value, formula);
|
||||||
|
setEditingNameIndex(-2);
|
||||||
|
} catch (e) {
|
||||||
|
return `${e}`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => setEditingNameIndex(-2)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledDialogContent>
|
||||||
|
<StyledDialogActions>
|
||||||
|
<Box display="flex" alignItems="center" gap={"8px"}>
|
||||||
|
<BookOpen color="grey" size={16} />
|
||||||
|
<UploadFooterLink
|
||||||
|
href="https://docs.ironcalc.com/web-application/name-manager.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{t("name_manager_dialog.help")}
|
||||||
|
</UploadFooterLink>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
onClick={() => setEditingNameIndex(-1)}
|
||||||
|
variant="contained"
|
||||||
|
disableElevation
|
||||||
|
sx={{ textTransform: "none" }}
|
||||||
|
startIcon={<Plus size={16} />}
|
||||||
|
disabled={editingNameIndex > -2}
|
||||||
|
>
|
||||||
|
{t("name_manager_dialog.new")}
|
||||||
|
</Button>
|
||||||
|
</StyledDialogActions>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)(() => ({
|
||||||
|
"& .MuiPaper-root": {
|
||||||
|
height: "380px",
|
||||||
|
minHeight: "200px",
|
||||||
|
minWidth: "620px",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDialogTitle = styled(DialogTitle)`
|
||||||
|
padding: 12px 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NameListWrapper = styled(Stack)`
|
||||||
|
overflow-y: auto;
|
||||||
|
gap: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)`
|
||||||
|
width: 161.67px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDialogContent = styled(DialogContent)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px 12px 20px 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
|
||||||
|
flexDirection: "row",
|
||||||
|
padding: "0 8px",
|
||||||
|
gap: "12px",
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "700",
|
||||||
|
color: theme.palette.info.main,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDialogActions = styled(DialogActions)`
|
||||||
|
padding: 12px 20px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #757575;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UploadFooterLink = styled("a")`
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: "Inter";
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default NameManagerDialog;
|
||||||
154
webapp/src/components/NameManagerDialog/NamedRangeActive.tsx
Normal file
154
webapp/src/components/NameManagerDialog/NamedRangeActive.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type { WorksheetProperties } from "@ironcalc/wasm";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
styled,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { Check, X } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
|
||||||
|
interface NamedRangeProperties {
|
||||||
|
worksheets: WorksheetProperties[];
|
||||||
|
name: string;
|
||||||
|
scope: string;
|
||||||
|
formula: string;
|
||||||
|
onSave: (name: string, scope: string, formula: string) => string | undefined;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NamedRangeActive(properties: NamedRangeProperties) {
|
||||||
|
const { worksheets, onSave, onCancel } = properties;
|
||||||
|
const [name, setName] = useState(properties.name);
|
||||||
|
const [scope, setScope] = useState(properties.scope);
|
||||||
|
const [formula, setFormula] = useState(properties.formula);
|
||||||
|
|
||||||
|
const [formulaError, setFormulaError] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledBox>
|
||||||
|
<StyledTextField
|
||||||
|
id="name"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
margin="none"
|
||||||
|
fullWidth
|
||||||
|
error={formulaError}
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<StyledTextField
|
||||||
|
id="scope"
|
||||||
|
variant="outlined"
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
margin="none"
|
||||||
|
fullWidth
|
||||||
|
error={formulaError}
|
||||||
|
value={scope}
|
||||||
|
onChange={(event) => {
|
||||||
|
setScope(event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value={"[global]"}>
|
||||||
|
<MenuSpan>{t("name_manager_dialog.workbook")}</MenuSpan>
|
||||||
|
<MenuSpanGrey>{` ${t("name_manager_dialog.global")}`}</MenuSpanGrey>
|
||||||
|
</MenuItem>
|
||||||
|
{worksheets.map((option) => (
|
||||||
|
<MenuItem key={option.name} value={option.name}>
|
||||||
|
<MenuSpan>{option.name}</MenuSpan>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</StyledTextField>
|
||||||
|
<StyledTextField
|
||||||
|
id="formula"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
margin="none"
|
||||||
|
fullWidth
|
||||||
|
error={formulaError}
|
||||||
|
value={formula}
|
||||||
|
onChange={(event) => setFormula(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<IconsWrapper>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
const error = onSave(name, scope, formula);
|
||||||
|
if (error) {
|
||||||
|
setFormulaError(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledCheck size={12} />
|
||||||
|
</IconButton>
|
||||||
|
<StyledIconButton onClick={onCancel}>
|
||||||
|
<X size={12} />
|
||||||
|
</StyledIconButton>
|
||||||
|
</IconsWrapper>
|
||||||
|
</StyledBox>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuSpan = styled("span")`
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: "Inter";
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuSpanGrey = styled("span")`
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: "Inter";
|
||||||
|
color: ${theme.palette.grey[400]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)`
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 577px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextField = styled(TextField)(() => ({
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
height: "28px",
|
||||||
|
width: "161.67px",
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: "Inter",
|
||||||
|
fontSize: "12px",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
padding: "8px",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
opacity: 0.6,
|
||||||
|
color: theme.palette.error.light,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCheck = styled(Check)(({ theme }) => ({
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const IconsWrapper = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NamedRangeActive;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Box, Divider, IconButton, styled } from "@mui/material";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { PencilLine, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface NamedRangeInactiveProperties {
|
||||||
|
name: string;
|
||||||
|
scope: string;
|
||||||
|
formula: string;
|
||||||
|
onDelete: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
showOptions: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NamedRangeInactive(properties: NamedRangeInactiveProperties) {
|
||||||
|
const { name, scope, formula, onDelete, onEdit, showOptions } = properties;
|
||||||
|
|
||||||
|
const scopeName =
|
||||||
|
scope === "[global]"
|
||||||
|
? `${t("name_manager_dialog.workbook")} ${t(
|
||||||
|
"name_manager_dialog.global",
|
||||||
|
)}`
|
||||||
|
: scope;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WrappedLine>
|
||||||
|
<StyledDiv>{name}</StyledDiv>
|
||||||
|
<StyledDiv>{scopeName}</StyledDiv>
|
||||||
|
<StyledDiv>{formula}</StyledDiv>
|
||||||
|
<IconsWrapper>
|
||||||
|
<StyledIconButtonBlack onClick={onEdit} disabled={!showOptions}>
|
||||||
|
<PencilLine size={12} />
|
||||||
|
</StyledIconButtonBlack>
|
||||||
|
<StyledIconButtonRed onClick={onDelete} disabled={!showOptions}>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</StyledIconButtonRed>
|
||||||
|
</IconsWrapper>
|
||||||
|
</WrappedLine>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledIconButtonBlack = styled(IconButton)(({ theme }) => ({
|
||||||
|
color: theme.palette.common.black,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIconButtonRed = styled(IconButton)(({ theme }) => ({
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
opacity: 0.6,
|
||||||
|
color: theme.palette.error.light,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const WrappedLine = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
height: "28px",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "12px",
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledDiv = styled("div")(({ theme }) => ({
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "400",
|
||||||
|
color: theme.palette.common.black,
|
||||||
|
width: "153.67px",
|
||||||
|
paddingLeft: "8px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const IconsWrapper = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NamedRangeInactive;
|
||||||
1
webapp/src/components/NameManagerDialog/index.ts
Normal file
1
webapp/src/components/NameManagerDialog/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./NameManagerDialog";
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import type { DefinedName, Model, WorksheetProperties } from "@ironcalc/wasm";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
|
||||||
TextField,
|
|
||||||
styled,
|
|
||||||
Box,
|
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
|
||||||
TextField,
|
|
||||||
styled,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { Trash2 } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
type NamedRangeProperties = {
|
|
||||||
name?: string;
|
|
||||||
scope?: number;
|
|
||||||
formula: string;
|
|
||||||
model: Model;
|
|
||||||
worksheets: WorksheetProperties[];
|
|
||||||
onChange: (
|
|
||||||
field: keyof DefinedName,
|
|
||||||
value: string | number | undefined,
|
|
||||||
) => void;
|
|
||||||
onDelete?: (name: string, scope: number | undefined) => void;
|
|
||||||
canDelete: boolean;
|
|
||||||
onUpdate?: (
|
|
||||||
name: string,
|
|
||||||
scope: number | undefined,
|
|
||||||
newName: string,
|
|
||||||
newScope: number | undefined,
|
|
||||||
newFormula: string,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function NamedRange(props: NamedRangeProperties) {
|
|
||||||
const [name, setName] = useState(props.name || "");
|
|
||||||
const [scope, setScope] = useState(props.scope || undefined);
|
|
||||||
const [formula, setFormula] = useState(props.formula);
|
|
||||||
const [nameError, setNameError] = useState(false);
|
|
||||||
const [formulaError, setFormulaError] = useState(false);
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
field: keyof DefinedName,
|
|
||||||
value: string | number | undefined,
|
|
||||||
) => {
|
|
||||||
if (field === "name") {
|
|
||||||
setName(value as string);
|
|
||||||
props.onChange("name", value);
|
|
||||||
}
|
|
||||||
if (field === "scope") {
|
|
||||||
setScope(value as number | undefined);
|
|
||||||
props.onChange("scope", value);
|
|
||||||
}
|
|
||||||
if (field === "formula") {
|
|
||||||
setFormula(value as string);
|
|
||||||
props.onChange("formula", value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// send initial formula value to parent
|
|
||||||
handleChange("formula", formula);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
props.onDelete?.(name, scope);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledBox>
|
|
||||||
<StyledTextField
|
|
||||||
id="name"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
margin="none"
|
|
||||||
fullWidth
|
|
||||||
error={nameError}
|
|
||||||
value={name}
|
|
||||||
onChange={(event) => handleChange("name", event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
/>
|
|
||||||
<StyledTextField
|
|
||||||
id="scope"
|
|
||||||
variant="outlined"
|
|
||||||
select
|
|
||||||
size="small"
|
|
||||||
margin="none"
|
|
||||||
fullWidth
|
|
||||||
value={scope ?? "Workbook (Global)"}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleChange(
|
|
||||||
"scope",
|
|
||||||
event.target.value === "Workbook (Global)"
|
|
||||||
? undefined
|
|
||||||
: event.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItem value="Workbook (Global)">Workbook (Global)</MenuItem>
|
|
||||||
{props.worksheets.map((option, index) => (
|
|
||||||
<MenuItem key={option.sheet_id} value={index}>
|
|
||||||
{option.name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</StyledTextField>
|
|
||||||
<StyledTextField
|
|
||||||
id="formula"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
margin="none"
|
|
||||||
fullWidth
|
|
||||||
error={formulaError}
|
|
||||||
value={formula}
|
|
||||||
onChange={(event) => handleChange("formula", event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
/>
|
|
||||||
<StyledIconButton onClick={handleDelete} disabled={!props.canDelete}>
|
|
||||||
<Trash2 size={12} />
|
|
||||||
</StyledIconButton>
|
|
||||||
</StyledBox>
|
|
||||||
<Divider />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledBox = styled(Box)`
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
width: 577px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTextField = styled(TextField)(() => ({
|
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
height: "28px",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
height: "28px",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
"&.Mui-disabled": {
|
|
||||||
opacity: 0.6,
|
|
||||||
color: theme.palette.error.light,
|
|
||||||
},
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
"&.Mui-disabled": {
|
|
||||||
opacity: 0.6,
|
|
||||||
color: theme.palette.error.light,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default NamedRange;
|
|
||||||
@@ -62,7 +62,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
|||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We reset the styles, every time we open (or close) the widget
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We reset the styles, every time we open (or close) the widget
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBorderSelected(null);
|
setBorderSelected(null);
|
||||||
setBorderColor(theme.palette.common.white);
|
setBorderColor(theme.palette.common.black);
|
||||||
setBorderStyle(BorderStyle.Thin);
|
setBorderStyle(BorderStyle.Thin);
|
||||||
}, [properties.open]);
|
}, [properties.open]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { type SelectedView, initSync } from "@ironcalc/wasm";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { decreaseDecimalPlaces, increaseDecimalPlaces } from "../formatUtil";
|
import { decreaseDecimalPlaces, increaseDecimalPlaces } from "../formatUtil";
|
||||||
import { isNavigationKey } from "../util";
|
import { getFullRangeToString, isNavigationKey } from "../util";
|
||||||
|
|
||||||
test("checks arrow left is a navigation key", () => {
|
test("checks arrow left is a navigation key", () => {
|
||||||
expect(isNavigationKey("ArrowLeft")).toBe(true);
|
expect(isNavigationKey("ArrowLeft")).toBe(true);
|
||||||
@@ -24,3 +26,22 @@ test("decrease decimals", () => {
|
|||||||
'dddd"," mmmm dd"," yyyy',
|
'dddd"," mmmm dd"," yyyy',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("format range to get the full formula", async () => {
|
||||||
|
const buffer = await readFile("node_modules/@ironcalc/wasm/wasm_bg.wasm");
|
||||||
|
initSync(buffer);
|
||||||
|
|
||||||
|
const selectedView: SelectedView = {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 8,
|
||||||
|
range: [1, 8, 1, 8],
|
||||||
|
top_row: 1,
|
||||||
|
left_column: 8,
|
||||||
|
};
|
||||||
|
const worksheetNames = ["Sheet1", "Notes"];
|
||||||
|
|
||||||
|
expect(getFullRangeToString(selectedView, worksheetNames)).toBe(
|
||||||
|
"Sheet1!$H$1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
BorderOptions,
|
BorderOptions,
|
||||||
HorizontalAlignment,
|
HorizontalAlignment,
|
||||||
Model,
|
|
||||||
VerticalAlignment,
|
VerticalAlignment,
|
||||||
} from "@ironcalc/wasm";
|
} from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
@@ -37,7 +36,7 @@ import {
|
|||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { theme } from "../theme";
|
import { theme } from "../theme";
|
||||||
import NameManagerDialog from "./NameManagerDialog";
|
import NameManagerDialog from "./NameManagerDialog";
|
||||||
import NameManagerDialog from "./NameManagerDialog";
|
import type { NameManagerProperties } from "./NameManagerDialog/NameManagerDialog";
|
||||||
import BorderPicker from "./borderPicker";
|
import BorderPicker from "./borderPicker";
|
||||||
import ColorPicker from "./colorPicker";
|
import ColorPicker from "./colorPicker";
|
||||||
import { TOOLBAR_HEIGHT } from "./constants";
|
import { TOOLBAR_HEIGHT } from "./constants";
|
||||||
@@ -76,7 +75,7 @@ type ToolbarProperties = {
|
|||||||
numFmt: string;
|
numFmt: string;
|
||||||
showGridLines: boolean;
|
showGridLines: boolean;
|
||||||
onToggleShowGridLines: (show: boolean) => void;
|
onToggleShowGridLines: (show: boolean) => void;
|
||||||
model: Model;
|
nameManagerProperties: NameManagerProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Toolbar(properties: ToolbarProperties) {
|
function Toolbar(properties: ToolbarProperties) {
|
||||||
@@ -398,7 +397,7 @@ function Toolbar(properties: ToolbarProperties) {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setNameManagerDialogOpen(false);
|
setNameManagerDialogOpen(false);
|
||||||
}}
|
}}
|
||||||
model={properties.model}
|
model={properties.nameManagerProperties}
|
||||||
/>
|
/>
|
||||||
</ToolbarContainer>
|
</ToolbarContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { Area, Cell } from "./types";
|
import type { Area, Cell } from "./types";
|
||||||
|
|
||||||
import {
|
import { type SelectedView, columnNameFromNumber } from "@ironcalc/wasm";
|
||||||
type SelectedView,
|
|
||||||
type WorksheetProperties,
|
|
||||||
columnNameFromNumber,
|
|
||||||
} from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the keypress should start editing
|
* Returns true if the keypress should start editing
|
||||||
@@ -61,24 +57,24 @@ export function rangeToStr(
|
|||||||
if (rowStart === rowEnd && columnStart === columnEnd) {
|
if (rowStart === rowEnd && columnStart === columnEnd) {
|
||||||
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
|
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
|
||||||
}
|
}
|
||||||
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber(
|
return `${sheetName}${columnNameFromNumber(
|
||||||
columnEnd,
|
columnStart,
|
||||||
)}${rowEnd}`;
|
)}${rowStart}:${columnNameFromNumber(columnEnd)}${rowEnd}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the full range of the selected view as a string in absolute form
|
||||||
|
// e.g. 'Sheet1!$A$1:$B$2' or 'Sheet1!$A$1'
|
||||||
export function getFullRangeToString(
|
export function getFullRangeToString(
|
||||||
selectedView: SelectedView,
|
selectedView: SelectedView,
|
||||||
worksheets: WorksheetProperties[], // solo pasar names
|
worksheetNames: string[],
|
||||||
): string {
|
): string {
|
||||||
// order of values is confusing compared to rangeToStr range type, needs refactoring for consistency
|
|
||||||
const [rowStart, columnStart, rowEnd, columnEnd] = selectedView.range;
|
const [rowStart, columnStart, rowEnd, columnEnd] = selectedView.range;
|
||||||
const sheetNames = worksheets.map((s) => s.name);
|
const sheetName = `${worksheetNames[selectedView.sheet]}`;
|
||||||
const sheetName = `${sheetNames[selectedView.sheet]}!`;
|
|
||||||
|
|
||||||
if (rowStart === rowEnd && columnStart === columnEnd) {
|
if (rowStart === rowEnd && columnStart === columnEnd) {
|
||||||
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
|
return `${sheetName}!$${columnNameFromNumber(columnStart)}$${rowStart}`;
|
||||||
}
|
}
|
||||||
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber(
|
return `${sheetName}!$${columnNameFromNumber(
|
||||||
columnEnd,
|
columnStart,
|
||||||
)}${rowEnd}`;
|
)}$${rowStart}:$${columnNameFromNumber(columnEnd)}$${rowEnd}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import {
|
|||||||
import FormulaBar from "./formulabar";
|
import FormulaBar from "./formulabar";
|
||||||
import Toolbar from "./toolbar";
|
import Toolbar from "./toolbar";
|
||||||
import useKeyboardNavigation from "./useKeyboardNavigation";
|
import useKeyboardNavigation from "./useKeyboardNavigation";
|
||||||
import { type NavigationKey, getCellAddress } from "./util";
|
import {
|
||||||
|
type NavigationKey,
|
||||||
|
getCellAddress,
|
||||||
|
getFullRangeToString,
|
||||||
|
} from "./util";
|
||||||
import type { WorkbookState } from "./workbookState";
|
import type { WorkbookState } from "./workbookState";
|
||||||
import Worksheet from "./worksheet";
|
import Worksheet from "./worksheet";
|
||||||
|
|
||||||
@@ -32,11 +36,13 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
// Calling `setRedrawId((id) => id + 1);` forces a redraw
|
// Calling `setRedrawId((id) => id + 1);` forces a redraw
|
||||||
// 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 info = model
|
|
||||||
.getWorksheetsProperties()
|
const worksheets = model.getWorksheetsProperties();
|
||||||
.map(({ name, color, sheet_id, state }: WorksheetProperties) => {
|
const info = worksheets.map(
|
||||||
|
({ name, color, sheet_id, state }: WorksheetProperties) => {
|
||||||
return { name, color: color ? color : "#FFF", sheetId: sheet_id, state };
|
return { name, color: color ? color : "#FFF", sheetId: sheet_id, state };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
const focusWorkbook = useCallback(() => {
|
const focusWorkbook = useCallback(() => {
|
||||||
if (rootRef.current) {
|
if (rootRef.current) {
|
||||||
rootRef.current.focus();
|
rootRef.current.focus();
|
||||||
@@ -115,10 +121,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
updateRangeStyle("num_fmt", numberFmt);
|
updateRangeStyle("num_fmt", numberFmt);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNamedRangesUpdate = () => {
|
|
||||||
// update named ranges in model
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCopyStyles = () => {
|
const onCopyStyles = () => {
|
||||||
const {
|
const {
|
||||||
sheet,
|
sheet,
|
||||||
@@ -573,7 +575,38 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
model.setShowGridLines(sheet, show);
|
model.setShowGridLines(sheet, show);
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
model={model}
|
nameManagerProperties={{
|
||||||
|
newDefinedName: (
|
||||||
|
name: string,
|
||||||
|
scope: number | undefined,
|
||||||
|
formula: string,
|
||||||
|
) => {
|
||||||
|
model.newDefinedName(name, scope, formula);
|
||||||
|
setRedrawId((id) => id + 1);
|
||||||
|
},
|
||||||
|
updateDefinedName: (
|
||||||
|
name: string,
|
||||||
|
scope: number | undefined,
|
||||||
|
newName: string,
|
||||||
|
newScope: number | undefined,
|
||||||
|
newFormula: string,
|
||||||
|
) => {
|
||||||
|
model.updateDefinedName(name, scope, newName, newScope, newFormula);
|
||||||
|
setRedrawId((id) => id + 1);
|
||||||
|
},
|
||||||
|
deleteDefinedName: (name: string, scope: number | undefined) => {
|
||||||
|
model.deleteDefinedName(name, scope);
|
||||||
|
setRedrawId((id) => id + 1);
|
||||||
|
},
|
||||||
|
selectedArea: () => {
|
||||||
|
const worksheetNames = worksheets.map((s) => s.name);
|
||||||
|
const selectedView = model.getSelectedView();
|
||||||
|
|
||||||
|
return getFullRangeToString(selectedView, worksheetNames);
|
||||||
|
},
|
||||||
|
worksheets,
|
||||||
|
definedNameList: model.getDefinedNameList(),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<FormulaBar
|
<FormulaBar
|
||||||
cellAddress={cellAddress()}
|
cellAddress={cellAddress()}
|
||||||
|
|||||||
@@ -1,93 +1,90 @@
|
|||||||
{
|
{
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"redo": "Redo",
|
"redo": "Redo",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"copy_styles": "Copy styles",
|
"copy_styles": "Copy styles",
|
||||||
"euro": "Format as Euro",
|
"euro": "Format as Euro",
|
||||||
"percentage": "Format as Percentage",
|
"percentage": "Format as Percentage",
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
"italic": "Italic",
|
"italic": "Italic",
|
||||||
"underline": "Underline",
|
"underline": "Underline",
|
||||||
"strike_through": "Strikethrough",
|
"strike_through": "Strikethrough",
|
||||||
"align_left": "Align left",
|
"align_left": "Align left",
|
||||||
"align_right": "Align right",
|
"align_right": "Align right",
|
||||||
"align_center": "Align center",
|
"align_center": "Align center",
|
||||||
"format_number": "Format number",
|
"format_number": "Format number",
|
||||||
"font_color": "Font color",
|
"font_color": "Font color",
|
||||||
"fill_color": "Fill color",
|
"fill_color": "Fill color",
|
||||||
"decimal_places_increase": "Increase decimal places",
|
"decimal_places_increase": "Increase decimal places",
|
||||||
"decimal_places_decrease": "Decrease decimal places",
|
"decimal_places_decrease": "Decrease decimal places",
|
||||||
"show_hide_grid_lines": "Show/hide grid lines",
|
"show_hide_grid_lines": "Show/hide grid lines",
|
||||||
"name_manager": "Name manager",
|
"name_manager": "Name manager",
|
||||||
"name_manager": "Name manager",
|
"vertical_align_bottom": "Align bottom",
|
||||||
"vertical_align_bottom": "Align bottom",
|
"vertical_align_middle": " Align middle",
|
||||||
"vertical_align_middle": " Align middle",
|
"vertical_align_top": "Align top",
|
||||||
"vertical_align_top": "Align top",
|
"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 (GBD)",
|
"date_short": "Short date",
|
||||||
"date_short": "Short date",
|
"date_long": "Long date",
|
||||||
"date_long": "Long date",
|
"custom": "Custom",
|
||||||
"custom": "Custom",
|
"number_example": "1,000.00",
|
||||||
"number_example": "1,000.00",
|
"percentage_example": "10%",
|
||||||
"percentage_example": "10%",
|
"currency_eur_example": "€",
|
||||||
"currency_eur_example": "€",
|
"currency_usd_example": "$",
|
||||||
"currency_usd_example": "$",
|
"currency_gbp_example": "£",
|
||||||
"currency_gbp_example": "£",
|
"date_short_example": "09/24/2024",
|
||||||
"date_short_example": "09/24/2024",
|
"date_long_example": "Tuesday, September 24, 2024"
|
||||||
"date_long_example": "Tuesday, September 24, 2024"
|
},
|
||||||
},
|
"borders": {
|
||||||
"borders": {
|
"title": "Borders",
|
||||||
"title": "Borders",
|
"all": "All borders",
|
||||||
"all": "All borders",
|
"inner": "Inner borders",
|
||||||
"inner": "Inner borders",
|
"outer": "Outer borders",
|
||||||
"outer": "Outer borders",
|
"top": "Top borders",
|
||||||
"top": "Top borders",
|
"bottom": "Bottom borders",
|
||||||
"bottom": "Bottom borders",
|
"clear": "Clear borders",
|
||||||
"clear": "Clear borders",
|
"left": "Left borders",
|
||||||
"left": "Left borders",
|
"right": "Right borders",
|
||||||
"right": "Right borders",
|
"horizontal": "Horizontal borders",
|
||||||
"horizontal": "Horizontal borders",
|
"vertical": "Vertical borders",
|
||||||
"vertical": "Vertical borders",
|
"color": "Border color",
|
||||||
"color": "Border color",
|
"style": "Border style"
|
||||||
"style": "Border style"
|
}
|
||||||
}
|
},
|
||||||
},
|
"num_fmt": {
|
||||||
"num_fmt": {
|
"title": "Custom number format",
|
||||||
"title": "Custom number format",
|
"label": "Number format",
|
||||||
"label": "Number format",
|
"close": "Close dialog",
|
||||||
"close": "Close dialog",
|
"save": "Save"
|
||||||
"save": "Save"
|
},
|
||||||
},
|
"sheet_rename": {
|
||||||
"sheet_rename": {
|
"rename": "Save",
|
||||||
"rename": "Save",
|
"label": "New name",
|
||||||
"label": "New name",
|
"title": "Rename Sheet",
|
||||||
"title": "Rename Sheet",
|
"close": "Close dialog"
|
||||||
"close": "Close dialog"
|
},
|
||||||
},
|
"formula_input": {
|
||||||
"formula_input": {
|
"update": "Update",
|
||||||
"update": "Update",
|
"label": "Formula",
|
||||||
"label": "Formula",
|
"title": "Update formula"
|
||||||
"title": "Update formula"
|
},
|
||||||
},
|
"navigation": {
|
||||||
"navigation": {
|
"add_sheet": "Add sheet",
|
||||||
"add_sheet": "Add sheet",
|
"sheet_list": "Sheet list"
|
||||||
"sheet_list": "Sheet list"
|
},
|
||||||
},
|
"name_manager_dialog": {
|
||||||
"name_manager_dialog": {
|
"title": "Named Ranges",
|
||||||
"help": "Learn more about Named Ranges",
|
"name": "Name",
|
||||||
"name": "Name",
|
"range": "Scope",
|
||||||
"range": "Scope",
|
"scope": "Range",
|
||||||
"scope": "Range"
|
"help": "Learn more about Named Ranges",
|
||||||
},
|
"new": "Add new",
|
||||||
"name_manager_dialog": {
|
"workbook": "Workbook",
|
||||||
"help": "Learn more about Named Ranges",
|
"global": "(Global)"
|
||||||
"name": "Name",
|
}
|
||||||
"range": "Scope",
|
|
||||||
"scope": "Range"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,9 +192,10 @@ pub(crate) fn get_worksheet_xml(
|
|||||||
&parsed_formulas[*f as usize],
|
&parsed_formulas[*f as usize],
|
||||||
);
|
);
|
||||||
let style = get_cell_style_attribute(*s);
|
let style = get_cell_style_attribute(*s);
|
||||||
|
let escaped_v = escape_xml(v);
|
||||||
|
|
||||||
row_data_str.push(format!(
|
row_data_str.push(format!(
|
||||||
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{v}</v></c>"
|
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{escaped_v}</v></c>"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Cell::CellFormulaError {
|
Cell::CellFormulaError {
|
||||||
|
|||||||
BIN
xlsx/tests/calc_tests/DATE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/DATE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/escape_strings.xlsx
Normal file
BIN
xlsx/tests/calc_tests/escape_strings.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/docs/COS.xlsx
Normal file
BIN
xlsx/tests/docs/COS.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/docs/TAN.xlsx
Normal file
BIN
xlsx/tests/docs/TAN.xlsx
Normal file
Binary file not shown.
@@ -465,11 +465,13 @@ fn test_documentation_xlsx() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
entries.sort();
|
entries.sort();
|
||||||
// We can't test volatiles
|
// We can't test volatiles
|
||||||
let skip = ["DATE.xlsx", "DAY.xlsx", "MONTH.xlsx", "YEAR.xlsx"];
|
let mut skip = vec!["DATE.xlsx", "DAY.xlsx", "MONTH.xlsx", "YEAR.xlsx"];
|
||||||
let skip = skip.map(|s| format!("tests/docs/{s}"));
|
// Numerically unstable
|
||||||
|
skip.push("TAN.xlsx");
|
||||||
|
let skip: Vec<String> = skip.iter().map(|s| format!("tests/docs/{s}")).collect();
|
||||||
println!("{:?}", skip);
|
println!("{:?}", skip);
|
||||||
// dumb counter to make sure we are actually testing the files
|
// dumb counter to make sure we are actually testing the files
|
||||||
assert_eq!(entries.len(), 7);
|
assert!(entries.len() > 7);
|
||||||
let temp_folder = env::temp_dir();
|
let temp_folder = env::temp_dir();
|
||||||
let path = format!("{}", Uuid::new_v4());
|
let path = format!("{}", Uuid::new_v4());
|
||||||
let dir = temp_folder.join(path);
|
let dir = temp_folder.join(path);
|
||||||
|
|||||||
Reference in New Issue
Block a user